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()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.