# 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](https://www.luanti.org/) 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](https://codeberg.org/rustydev/spaceships/issues).

## 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](https://github.com/ron-rs/ron). See the link for more information.

The complete schema of the config files is available inside [`src/net/server/config.rs`](https://codeberg.org/rustydev/spaceships/src/branch/main/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](./1-how-to-play.md) 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 2<sup>20</sup>.
	* `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](https://en.wikipedia.org/wiki/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,
	)),
)
```

## Capture the flag
Here is an example of a capture the flag configuration. This is a teamed game where players can capture flags from other teams and bring those flags to their own team's base.

The full config is in `configs/ctf.ron`.

```
Config(
	...

	flags: [
		( default_pos: (-6, -28), team: TeamId(0) ),
		( default_pos: (0, -28), team: TeamId(0) ),
		( default_pos: (6, -28), team: TeamId(0) ),

		( default_pos: (-6, 28), team: TeamId(1) ),
		( default_pos: (0, 28), team: TeamId(1) ),
		( default_pos: (6, 28), team: TeamId(1) ),
	],
	teams: [
		Team(
			name: "Red",
			colour: "red",
			aliases: ["r", "red"],
			spawn_point: Some((0, -27.5)),
			base: Some((
				p0: (-12, -30),
				p1: (12, -25.5),
			)),
		),
		Team(
			name: "Blue",
			colour: "blue",
			aliases: ["b", "blue"],
			spawn_point: Some((0, 27.5)),
			base: Some((
				p0: (-12, 25.5),
				p1: (12, 30),
			)),
		),
	],
)
```

Here, `default_pos` is the starting position of the flag and `team` is the id (indexing the `teams` array below) of the flag's team. Also in the teams defined below is the team's base. Flags are returned to their default position if they collide with their team's base (and aren't being picked up by a player on the other team), and they emit score events (for the base's team) when a flag is returned to another team's base.

### Increasing the score
Here is an example of how to increase the team's score by one whenever a flag is returned to that team's base.

Here `Flag(None)` increases the score for *all* flags returned. If you want different flags to be worth different scores, instead use `Flag(Some(i))` to only increase the score for the flag of index i being returned.

```
Config(
	...

	scoring: Scoring(
		controller: ScoringController(
			events: [
				(
					listener: Flag(None),
					score: 1,
				),
			],
		),
	),
)
```

## 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.
