Quick Start for C# / .NET

This guide only covers the current C# connector and assumes that you want the shortest path from a homepage account to a running ship. The connector package targets .NET 8.0, so use .NET 8.0 or newer. Before you start, make sure that your homepage account exists, your email address has been confirmed, and you have generated a Player API key under Profile... > API Keys.

Start with the right galaxy

If you want the first mission instead of an empty sandbox, begin with the mission-oriented beginner galaxy. On the current server that is galaxy 0, Beginners Universe, which is explicitly described as Start here. on the Galaxies page. That is the right place for your first connect, first ship, first scan and first mission target.

Create a project and install the connector

Start with a normal console project and add the connector from NuGet. You do not need the full client, the private test tooling or the map editor to begin. A plain console app is enough.

dotnet new console -n FlattiverseQuickStart --framework net8.0
cd FlattiverseQuickStart
dotnet add package Flattiverse.Connector

If you want to look at the C# connector source itself, it is available at github.com/flattiverse/connector-csharp.

Connect, log in and inspect the galaxy

The public galaxy endpoint shape is wss://www.flattiverse.com/galaxies/{id}/api. The beginner mission galaxy is therefore wss://www.flattiverse.com/galaxies/0/api. A Player API key logs you in as a normal player. After Galaxy.Connect(...) returns, the connector has already built a local mirror of the current galaxy state, so you can immediately inspect your own player, the galaxy, all teams, clusters and currently known players.

using Flattiverse.Connector.Events;
using Flattiverse.Connector.GalaxyHierarchy;

string endpoint = "wss://www.flattiverse.com/galaxies/0/api";
string playerApiKey = "<your-player-api-key>";

Galaxy galaxy = await Galaxy.Connect(endpoint, playerApiKey, "Lime");

Console.WriteLine($"Connected as {galaxy.Player.Name}.");
Console.WriteLine($"Ping: {galaxy.Player.Ping:0.0} ms");
Console.WriteLine($"Galaxy: {galaxy.Name} ({galaxy.GameMode})");
Console.WriteLine($"Your team: {galaxy.Player.Team.Name}");

foreach (Team team in galaxy.Teams)
    Console.WriteLine($"Team {team.Id}: {team.Name}, mission={team.Score.Mission}");

foreach (Player player in galaxy.Players)
    Console.WriteLine($"Player {player.Id}: {player.Name}, ping={player.Ping:0.0} ms");

If one galaxy ever requires self-disclosure, the same Galaxy.Connect(...) call also accepts RuntimeDisclosure and BuildDisclosure. The beginner mission galaxy currently does not require them.

Chat, events and live mirrored state

The connector has two views of the same session. First, you have the event stream from await galaxy.NextEvent(). Second, you have live objects such as galaxy.Teams, galaxy.Players, galaxy.Clusters and later your own galaxy.Controllables. Those objects are updated while you process events. That means you usually combine both approaches: use events to react to changes, but read most current data directly from the mirrored objects.

The universe currently runs at one tick every 100 ms, which means 10 ticks per second. Values such as scan energy per tick, weapon runtime, tier-change ticks or projectile lifetime are all expressed against that cadence.

await galaxy.Chat("Hello from my connector.");

while (galaxy.Active)
{
    FlattiverseEvent @event = await galaxy.NextEvent();
    Console.WriteLine(@event);

    Console.WriteLine($"Players currently visible to the connector: {galaxy.Players.Count}");
}

NextEvent() is a single-consumer API. Do not await it concurrently from multiple tasks. If you want to chat privately or to a team, use player.Chat(...) or team.Chat(...).

Create your first ClassicShip

A ClassicShip is the easiest first controllable. The important mental model is that almost everything is a subsystem: engine, scanners, weapons, batteries, shield, repair and more. You do not drive the ship through one monolithic API. You talk to the subsystems that belong to that ship.

using Flattiverse.Connector.GalaxyHierarchy;

ClassicShipControllable ship = await galaxy.CreateClassicShip("Starter");
await ship.Continue();

Console.WriteLine($"Ship position: {ship.Position}");
Console.WriteLine($"Ship movement: {ship.Movement}");
Console.WriteLine($"Energy: {ship.EnergyBattery.Current:0.0} / {ship.EnergyBattery.Maximum:0.0}");
Console.WriteLine($"Shield: {ship.Shield.Current:0.0}");
Console.WriteLine($"Hull: {ship.Hull.Current:0.0}");
Console.WriteLine($"Armor reduction: {ship.Armor.Reduction:0.0}");

Registering the ship is not enough. You still need to call await ship.Continue(); once so that it actually starts running in the galaxy. After that, your own ship position, movement and subsystem runtime belong to the owner-side ClassicShipControllable. Do not try to reconstruct your own state from visible PlayerUnit updates.

Scanner, energy, engine and first shot

The main scanner, the engine and the shot launcher already expose the information you need for a first real control loop: current runtime, target runtime and cost previews. Use the cost helpers before sending commands if you want to keep the ship energy-aware.

using Flattiverse.Connector.GalaxyHierarchy;

float scanEnergy;
bool scanCostValid = ship.MainScanner.CalculateCost(60f, 220f, out scanEnergy, out _, out _);

if (scanCostValid)
    Console.WriteLine($"Main scanner energy per tick: {scanEnergy:0.0}");

await ship.MainScanner.Set(60f, 220f, 0f);
await ship.MainScanner.On();

Vector thrust = new Vector(ship.Engine.Maximum * 0.5f, 0f);
float engineEnergy;
bool engineCostValid = ship.Engine.CalculateCost(thrust, out engineEnergy, out _, out _);

if (engineCostValid)
    Console.WriteLine($"Engine energy per tick: {engineEnergy:0.0}");

await ship.Engine.Set(thrust);

float shotEnergy;
bool shotCostValid = ship.ShotLauncher.CalculateCost(new Vector(3f, 0f), 20, 2.5f, 1f, out shotEnergy, out _, out _);

if (shotCostValid)
    Console.WriteLine($"Shot energy cost: {shotEnergy:0.0}");

await ship.ShotLauncher.Shoot(new Vector(3f, 0f), 20, 2.5f, 1f);
  • ship.MainScanner.Set(width, length, angle) sets the target scanner geometry.
  • ship.MainScanner.On() and Off() switch the scanner.
  • ship.Engine.Set(vector) changes target thrust. Off() sets zero thrust.
  • ship.ShotLauncher.Shoot(...) requests one dynamic shot for the next tick.
  • ship.Railgun.FireFront() and FireBack() are available once you want a fixed rail shot.

Maintenance, upgrades and missing subsystems

Each subsystem reports its current tier, target tier, pending change ticks and the full static tier catalog. This is how you inspect costs, compare capabilities and install something that is currently missing. A slot that does not exist yet is usually tier 0. Calling Upgrade() on such a slot is the normal way to install it.

DynamicScannerSubsystem primaryScanner = ship.MainScanner;

Console.WriteLine($"Main scanner tier: {primaryScanner.Tier}");
Console.WriteLine($"Current tier info: {primaryScanner.TierInfo.Description}");

if (primaryScanner.Tier + 1 < primaryScanner.TierInfos.Count)
{
    SubsystemTierInfo nextTier = primaryScanner.TierInfos[primaryScanner.Tier + 1];
    Console.WriteLine($"Next upgrade cost: {nextTier.UpgradeCost}");
}

if (!ship.SecondaryScanner.Exists)
    await ship.SecondaryScanner.Upgrade();

await ship.Engine.Upgrade();

A ClassicShip can expose, among others, these subsystem families: hull, shield, armor, repair, cargo, resource miner, structure optimizer, batteries, cells, engine, scanners, shot launcher, shot magazine, shot fabricator, interceptor launcher, interceptor magazine, interceptor fabricator, railgun, nebula collector and jump drive. The exact state lives on the corresponding objects, and each of those objects carries its own tier metadata and runtime values.

Scanning in practice

Scanning is not just an on/off switch. Width, length and angle matter. A longer scan reaches farther. A narrower scan reduces the covered area and is useful when you want to focus on one direction instead of paying for a broad cone. Scanner angles are absolute world angles, not relative ship angles. A target angle of 0 points east on the world axis.

foreach (Cluster cluster in galaxy.Clusters)
    foreach (Flattiverse.Connector.Units.Unit unit in cluster.Units)
    {
        Console.WriteLine($"{cluster.Name}: {unit.Kind} {unit.Name}, full={unit.FullStateKnown}");

        if (unit is Flattiverse.Connector.Units.MissionTarget missionTarget && unit.FullStateKnown)
            Console.WriteLine($"Mission target sequence: {missionTarget.SequenceNumber}");
    }

This matters because some unit details are only reliable after the full state has been received. For example, a MissionTarget only becomes interesting once its sequence number is known. The same principle applies to more complex visible units such as player ships with subsystem state. If you want to inspect the universe carefully, keep scanning, keep consuming events, and watch unit.FullStateKnown.

Watch the first mission

If you started in the beginner mission galaxy, mission progress shows up both in live score data and in the event stream. You can watch your own mission score through galaxy.Player.Score.Mission and react to mission system chat when a target is hit in sequence.

if (@event is MissionTargetHitChatEvent missionEvent)
{
    Console.WriteLine(
        $"Mission target #{missionEvent.MissionTargetSequence} hit by {missionEvent.Player.Name} " +
        $"with ship {missionEvent.ControllableInfo.Name}.");
}

Console.WriteLine($"My current mission score: {galaxy.Player.Score.Mission}");

The first sensible milestone is not combat. It is this: connect, create one ship, keep the event loop running, make the scanner useful, move deliberately, and understand why a mission target or another unit became visible when it did.

Common early mistakes

  • Your email is not confirmed yet, so you still cannot really play.
  • You generated an Admin API key, but you actually wanted a Player API key.
  • You connected to the wrong galaxy instead of the beginner mission galaxy.
  • You call NextEvent() from multiple tasks in parallel.
  • You expect your own ship state to come back as a visible unit instead of reading the owner-side controllable.
  • You never turn the scanner on and then wonder why the world stays empty.
  • You forget that one account can only have one active galaxy session at a time.

Good luck

Once this first loop works, the next sensible steps are better scan policies, movement control, weapon automation, and a cleaner internal model of what your connector has already seen. But for the very first session, keep the goal small: get into the right galaxy, see the first mission, and make one ship behave predictably under your code.