Server configuration

The long-term goal of Spaceships is to support configuration of everything in the game, including the physics, weapons, and other objects, similar to how games like Luanti currently are.

Right now Spaceships isn't there yet, but game servers are pretty configurable.

If you find any errors in this documentation, which are likely if I forget to update this in future versions, make an issue here.

Get started

The documentation here is incomplete. I recommend first taking a look at files in the configs/ directory attached with your distribution of the game, or at https://codeberg.org/rustydev/spaceships/src/branch/main/configs for examples of what a config file looks like. These can be used as a template for creating your own server config.

These configuration files are in the RON (Rusty Object Notation) format. See the link for more information.

The complete schema of the config files is available inside src/net/server/config.rs, with some (but not enough) code comments explaining things.

Example

Here's an example of a very simple configuration file that's an infinite world with no blocks in it. This is available at configs/empty.ron.

Config(
	world: World(
		size: (inf, inf),
	),
	lan_discovery: LanDiscovery(
		name: "Empty",
		desc: "An empty map.",
	),
	blocks: Blocks(
		block_size: 1,
		grid_size: (0, 0),
		ops: [ Constant(0) ],
	),
)

Now here's an explanation of each of these fields:

  • world.size: This specifies the width and height of the world. Players cannot go outside this space. In this example, both the width and height are set to inf, which configures an infinite world.
  • lan_discovery: The first chapter of this documentation briefly goes over LAN discovery as a way to easily join game servers hosted on the local network. Here, lan_discovery.name and lan_discovery.desc set the displayed name and description of the game server respectively.
  • blocks: This consists of three fields, each for configuring the physical blocks in the world that constrain the players' movement.
    • blocks.block_size: This is an integer that sets how large each block is in the game.
    • blocks.grid_size: This sets the width and height of the grid which contains these blocks. The number of blocks in this grid cannot exceed 220.
    • blocks.ops: This specifies a list of operations that are performed on block maps stored on a stack to produce a final block map.

Spawning, ammo crates and powerups

To configure these, see the fields with the events prefix. See the full schema for more information.

Special areas

The server has the ability to send an arbitrary list of rectangles of arbitrary position, size and colour to the clients, which the clients should render these in the world. These are designed for highlighting regions of the world where custom server-side configuration can perform certain actions. Right now the two special areas implemented in this game are portals and effect zones, though more special areas can be added in the future and those special areas can still be compatible with older clients.

Here's an example of how to add special areas that are just decorative:

Config(
	...

	special_areas: SpecialAreas(
		areas: [
			( pos: Vec2(15, 15), size: Vec2(4, 4), colour: (127, 255, 0, 63) )
		],
	),
)

Portals

Portals allow players (but not bullets) to teleport from one location to another. Portals can either be sinks (you enter these), sources (you leave these), or bidirectional (you can go both ways). Portals are expressed in the config as a directed graph, with nodes being the portals and edges being the possible transitions between portals. If a node has more than one outgoing edge, players entering that portal will teleport to a random outgoing neighbour of that portal.

It is a desirable feature to allow the velocity of players to change after they leave the portal, like if they entered a horizontal portal and leave a vertical portal. To do this, special_areas.portals.out.rotation can be set to either D0 (default for no rotation), D90, D180 or D270.

When specifying portals, make sure that the player isn't inside a block or outside the world border if they are at the portal's centre. If you don't ensure this, it might be possible for players to teleport inside a block.

Example

Config(
	...

	special_areas: SpecialAreas(
		portals: [
			Portal(
				p0: Vec2(-4.5, -3.0), p1: Vec2(1.5, 0.5),
				out: [PortalLink(
					index: 1,
					rotation: D90,
				), PortalLink(
					index: 2,
				)],
			),
			Portal( p0: Vec2(12.5, -9.5), p1: Vec2(14.5, -1.5) ),
			Portal(
				p0: Vec2(22, 22), p1: Vec2(24, 23),
				out: [PortalLink(
					index: 0,
				)],
			),
		],
	),
)

Effect zones

An effect zone is a region where players get effects applied to them (the same effects that powerups give) while there. These effects can persist for an arbitrary amount of time when the player leaves the effect zone.

Example

Config(
	...

	special_areas: SpecialAreas(
		effect_zones: [
			EffectZone(
				p0: Vec2(15, 15),
				p1: Vec2(19, 19),
				effect: Speed,
				time: 5,
				power: Some(4), // Very fast!
			),
		]
	),
)

Teams

By default teams are disabled. To enable them, you need to specifically add them to the config.

Example

Here is an example of how to create a red and blue team.

Config(
	...

	teams: [
		Team(
			name: "Red",
			colour: "red",
			aliases: ["r", "red"],
		),
		Team(
			name: "Blue",
			colour: "blue",
			aliases: ["b", "blue"],
		),
	],
)

Custom spawn points

It is possible to have all players in a team spawn at a fixed location in the world by adding spawn_point: Some((X, Y)) for some X and Y. This doesn't affect player spawning when in the lobby. See configs/occupation.ron for an example of this.

Scoring

The scoring part of the config schema consists of two parts: the controller and the conditions for winning and elimination. The score controller specifies how the player and team scores should change in response to events, like a player getting a kill, dying or occupying a part of the world (see the occupation game). The win and elimination conditions accept as input the scores of either the player or the teams, as well as the timer. These decide respectively when the game should end and who (either in terms of players or teams) should win the game, or who should be removed from the game until the game finishes.

By default there is no win or elimination condition and the score of each player is their kill count.

Examples

Here are a few examples of how the scoring system can be configured.

First to 10 kills

To win the game in the following config, you need to be the first player to get at least 10 kills.

It should be noted that multiple players can win the game if they both get to at least 10 kills at the exact same time, though this is very unlikely to occur.

Config(
	...

	scoring: Scoring(
		win: Player(FirstTo((GreaterEqual, 10))),
	),
)

Survival

Here's another config where each player starts off with five lives, and the winning player is the last player remaining. Players get eliminated when they lose all their lives, as shown in the elimination condition.

The full config is in configs/survival.ron.

Config(
	...

	scoring: Scoring(
		controller: ScoringController(
			events: [
				(
					listener: Death,
					score: -1,
				),
			],
			player: (
				init: 5,
			),
		),
		win: Player(LastWith((Greater, 0))),
		elimination: Player(FirstTo((LessEqual, 0))),
		can_join_midgame: false,
	),
)

Occupation

Here is a server config that provides a unique game mode that isn't around killing the other players or staying alive. Instead, the game consists of two teams, red and blue, that spawn at fixed locations in their base. When you occupy the base of the other team, your team's score increases. The winner is the team with the highest score after five minutes. If after five minutes the scores are tied, the first team to then increase their score wins. (Also when occupying the other team's base, you can teleport back to your own base with the team-only portal.)

This config also involves the timer which is displayed on the top-right corner of the screen and is used in the win condition.

The full config is in configs/occupation.ron.

Config(
	...

	scoring: Scoring(
		controller: ScoringController(
			events: [
				(
					listener: Occupation((
						owner: Some(TeamId(0)),
						p0: (-10, -30),
						p1: (10, -25.5),
						capture_time: 1,
						period: 0.2,
					)),
					score: 1,
					output: Team,
				),
				(
					listener: Occupation((
						owner: Some(TeamId(1)),
						p0: (-10, 25.5),
						p1: (10, 30),
						capture_time: 1,
						period: 0.2,
					)),
					score: 1,
					output: Team,
				),
			],
		),
		win: Team(And([
			Extremum( type: Maximum, exclusive: true ),
			Timer((LessEqual, 0)),
		])),
	),

	...

	timer: Some(Timer(
		now: 300,
		limit: 0,
		counting: Down,
	)),
)

Notes

When making your own scoring system and conditions for winning and elimination, here are a few important notes to consider:

  1. Avoid Equal as a comparison operator. If having a goal of the winning player being the first one to 20 kills, you probably don't want to use goal: FirstTo((Equal, 20)). This is because it's possible that a player has 19 kills, and then fires two bullets which kill two players at the exact same time and boost their score to 21. In that case, that player lost their chance of winning the game.
  2. Prevent players from joining mid-game. If your scoring system involves the elimination of players, you should add can_join_midgame: false to the config to prevent players from joining after being eliminated. You should also do this if players joining the game later on have an advantage (like having much fewer deaths).

Lobby

By default as soon as players join, the game starts. However, you can put players in a "lobby" where players can shoot and kill each other but scoring is disabled before the game starts. This can be done to give players a break after playing the game.

To enable the lobby and have it last for, say 60 seconds, add this to your config:

Config(
	...

	lobby: Lobby(
		time: 60,
	),
)

Players can start the game prematurely if they all are ready by running the ready command (or its alias of r). If you want to decrease the proportion of players that need to be ready before the game starts, change ready_threshold to a value in [0, 1].

Timer

A timer can be added while playing the game which can provide useful information to players, such as when the game will end. This timer can be used in the scoring system for the win and elimination conditions.

Here is an example of a timer that starts at zero and counts up forever. This timer is configured to display the raw seconds and use millisecond precision (which is probably overkill).

By default there is no timer.

Config(
	...

	timer: Some(Timer(
		now: 0,
		limit: inf,
		counting: Up,
		precision: Milli,
		format: S,
	)),
)

Stability

I make no guarantees about the stability of this schema between versions. I will try to ensure backwards compatibility between changes in the patch version (third number of the version), and as of writing I have ensured this (maybe, there might've been breaking changes I made and didn't think much about).

Advanced configuration

Everything that can be configured right now through the server config is just a small sample of what you can actually configure. If you want to add something like an effect zone but for giving players ammo, it's possible to modify the server source code without needing all players to update their game to your specific fork.

Here's a list of ideas that can be implemented for custom server-side configuration that unmodified clients can play with:

  • Recoil: When players fire bullets, the server can decrease the player's velocity by a small amount in the opposite direction of the fired bullet.
  • Stealing ammo: When a player kills another player, an ammo crate might spawn containing all ammo that player had.
  • Player-player collision: Right now the game's physics don't support collision between players. However, this functionality can be hacked on by the server arbitrarily controlling the players' positions and velocities.

Even more advanced configuration would require forking the game, such as if you want to add a new type of weapon.

Tips

Here are some tips to be more efficient when customising a game server:

  1. Right-click to print position. When in the world, you can right-click (or whatever you choose to bind it to) and the position your cursor's pointing at will be printed to stderr. This can be very helpful when placing special effects.
  2. reload command. In the server console, you can run reload default to quickly restart the server, which you might do a lot.