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()andOff()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()andFireBack()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 / 100shipGravity = 0.01 + 0.11 * L_eff / 100classicSpeedLimit = 6 - 2 * (L_eff / 100)0.8modernSpeedLimit = 6.5 - 2 * (L_eff / 100)0.8engineEfficiency = 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 - targetXdy = sourceY - targetYd2 = dx * dx + dy * dyif d2 > 3600: delta = (dx, dy) * gravity * 60 / d2else if d2 > 0: delta = normalize(dx, dy) * gravityelse: 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 * distanceFactorif transferFactor < 0.01: no passive transferreceivedEnergy = sunEnergy * transferFactorreceivedIons = sunIons * transferFactorreceivedNeutrinos = sunNeutrinos * transferFactorreceivedHeat = sunHeat * transferFactorreceivedDrain = sunDrain * transferFactorheatEnergyCost = receivedHeat * 15overflowHeat = max(0, heatEnergyCost - availableEnergy) / 15radiationDamageBeforeArmor = (receivedDrain + overflowHeat) * 0.125radiationHullDamage = max(0, radiationDamageBeforeArmor - armorReduction)Cells, Batteries And Cargo
cellCollected = offeredResource * cellEfficiencybatteryDelta = 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.705882rangeCost = 0.3926 * length0.5 + 2.76e-10 * length4 - 0.617scannerEnergy = 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 / maximumcost = 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 + shipRadiusavailableResources = sum(all mineable resources in range)requestedResources = availableResources * ratestoredResources = cargo.Store(requestedResources)nebulaCollectable if edgeDistance <= 0storedNebula = 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 * damageexplosionRadius = loadrailFinalSpeed = |shipMovement + railRelativeMovement|railDamage = 4 * railFinalSpeedrailEnergy = 300railMetal = 1railLifetime = 250 ticksinterceptorLaunchEnergy = 450 (tier 1), 900 (tier 2)jumpDriveEnergy = 6000Orbit And Current Fields
phaseTick = tick mod abs(rotationTicks)angle = startAngle + 360 * phaseTick / rotationTicksorbitOffset = fromAngle(angle, distance)position(t) = configuredCenter + sum(orbitOffset_i(t))directionalFieldDelta = flowrelativeFieldDelta = radialUnit * radialForce + tangentialUnit * tangentialForceBlack-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.