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 on the API Keys settings page.

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.

Start consuming await galaxy.NextEvent() immediately after connecting. The server may already have queued initial login events such as compile-profile information, a welcome system message and the current MOTD. Read those first, then continue with your normal event loop and additional checks such as your current ping.

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.

Formula Cheat Sheet

These formulas are taken from the current live server code. One server tick is 100 ms. Runtime values such as Maximum, MaximumRate, fullCost, tier limits and tier efficiencies come from the active subsystem tier on the mirrored connector objects.

Symbols

L_raw: raw structural load from all installed subsystems.

L_eff: effective structural load after the structure optimizer.

p: normalized power or rate, usually value / maximum.

d: distance, usually center distance or surface distance as stated below.

visibleShare: currently unmasked visible arc fraction of a scanned sun.

fullCost: the tier-dependent energy constant of the active subsystem.

Structure And Ship Stats

L_eff = L_raw * (1 - r_opt)
shipRadius = 1 + 47 * L_eff / 100
shipGravity = 0.01 + 0.11 * L_eff / 100
classicSpeedLimit = 6 - 2 * (L_eff / 100)0.8
modernSpeedLimit = 6.5 - 2 * (L_eff / 100)0.8
engineEfficiency = 1.2 - 0.45 * (L_eff / 100)0.85

r_opt is the current structure-optimizer reduction percent.

World Gravity And Soft Caps

dx = sourceX - targetX
dy = sourceY - targetY
d2 = dx * dx + dy * dy
if d2 > 3600: delta = (dx, dy) * gravity * 60 / d2
else if d2 > 0: delta = normalize(dx, dy) * gravity
else: delta = (gravity, 0)
if speed > speedLimit: newSpeed = speedLimit + 0.9 * (speed - speedLimit)

The gravity rule is used by normal gravity sources in the current runtime. The soft cap is the global post-movement limiter used for ships and projectiles with a speed limit.

Sun Transfer And Environment

surfaceDistance = max(0, centerDistance - sunRadius - shipRadius)
distanceFactor = 1 / (1 + surfaceDistance / 60)sqrt(2)
transferFactor = visibleShare * distanceFactor
if transferFactor < 0.01: no passive transfer
receivedEnergy = sunEnergy * transferFactor
receivedIons = sunIons * transferFactor
receivedNeutrinos = sunNeutrinos * transferFactor
receivedHeat = sunHeat * transferFactor
receivedDrain = sunDrain * transferFactor
heatEnergyCost = receivedHeat * 15
overflowHeat = max(0, heatEnergyCost - availableEnergy) / 15
radiationDamageBeforeArmor = (receivedDrain + overflowHeat) * 0.125
radiationHullDamage = max(0, radiationDamageBeforeArmor - armorReduction)

Cells, Batteries And Cargo

cellCollected = offeredResource * cellEfficiency
batteryDelta = min(batteryFree, cellCollected)
resourceStored = min(resourceFree, requestedResource)
nebulaStored = min(nebulaFree, requestedNebula)

Energy, ions and neutrinos are first filtered through their matching cell efficiency, then clamped by the matching battery free space. Cargo storage is clamped per resource channel.

Scanner

widthCost = 0.141176 * width - 0.705882
rangeCost = 0.3926 * length0.5 + 2.76e-10 * length4 - 0.617
scannerEnergy = max(0, widthCost + rangeCost)
tier5ScannerNeutrinos = scannerEnergy / 100

Runtime limits are width in [5, MaximumWidth] and length in [20, MaximumLength]. Classic scanners use an absolute world angle. Modern scanners use an angle offset around their fixed hull mount.

Engine-Like Cost Curve

p = value / maximum
cost = fullCost * (0.30 * p + 0.70 * p3)

This same curve is used for classic and modern engine thrust, resource miner rate, nebula collector rate, shot fabricator rate and interceptor fabricator rate.

Shield And Repair

p = rate / maximumRate
System Tier Runtime Cost
Shield 1 fullCost * (0.70 * p + 0.30 * p3)
Shield 2 fullCost * (0.55 * p + 0.45 * p3)
Shield 3 fullCost * (0.40 * p + 0.60 * p3)
Shield 4 fullCost * (0.46 * p + 0.54 * p3)
Shield 5 ions = 0.9 * p
Repair 1 18 * p0.35 + 7 * p3
Repair 2 24 * p0.35 + 12 * p3
Repair 3 32 * p0.35 + 20 * p3
Repair 4 44 * p0.35 + 32 * p3
Repair 5 58 * p0.35 + 50 * p3

Repair only affects hull and requires movement < 0.1. Shield loading stops once the current shield reaches its maximum.

Mining And Nebula Collection

mineableRangeCheck: centerDistance <= 25 + sourceRadius + shipRadius
availableResources = sum(all mineable resources in range)
requestedResources = availableResources * rate
storedResources = cargo.Store(requestedResources)
nebulaCollectable if edgeDistance <= 0
storedNebula = cargo.StoreNebula(rate, nearestNebulaHue)

Both subsystems require movement < 0.1. The nebula collector uses the same engine-like cost curve as above.

Shots, Railgun, Interceptors And Jump

shotEnergy = 20 + 60 * relativeSpeed + 3 * ticks + 15 * load + 20 * damage
explosionRadius = load
railFinalSpeed = |shipMovement + railRelativeMovement|
railDamage = 4 * railFinalSpeed
railEnergy = 300
railMetal = 1
railLifetime = 250 ticks
interceptorLaunchEnergy = 450 (tier 1), 900 (tier 2)
jumpDriveEnergy = 6000

Orbit And Current Fields

phaseTick = tick mod abs(rotationTicks)
angle = startAngle + 360 * phaseTick / rotationTicks
orbitOffset = fromAngle(angle, distance)
position(t) = configuredCenter + sum(orbitOffset_i(t))
directionalFieldDelta = flow
relativeFieldDelta = radialUnit * radialForce + tangentialUnit * tangentialForce

Black-Hole Note

The map format currently exposes GravityWellRadius and GravityWellForce on BlackHole, but the current runtime gravity path only applies the normal unit Gravity field. There is therefore no extra active black-hole well formula to document on the live server at the moment.