GhostServer alternative for SourceAutoRecord.
- Protocol
- Ping
- Connect
- Disconnect
- Stop Server (not supported)
- Map Change
- Heart Beat
- Message
- Countdown
- Update
- Speedrun Finish
- Model Change
- Color Change
- TCP/UDP mode
- Implement old server commands
- exit
- help
- list
- countdown_set
- countdown
- disconnect
- disconnect_id
- ban
- ban_id
- accept_players
- refuse_players
- accept_spectators
- refuse_spectators
- server_msg
- Deployment
- CI
- Docker image
- ghosting.portal2.sr
- Testing
- Client + GhostServer
- Server + SourceAutoRecord
- Test on prod server
- Server as API/library
- Plugin system
- Server-side hooks
- Publish package
- Examples
- ID is not a unique identifier
STOP_SERVER
packet is a kill switchSPEEDRUN_FINISH
formats and sends the timer value as a string- Disconnect is checked by IP
- Header value gets ignored when starting a connection
- "Heart Beat" should be called "Heartbeat"
CMD_SERVER_MSG
(CLI) does not schedule on server thread
- Secure protocol
- Authentication
- Role system (via plugins)
- Server plugins (on TODO list)
- SSR?
- Client-side scripting?
- Start the server with:
deno task start
with CLIdeno task server
without CLI
- Connect to the server with:
ghost_connect 0.0.0.0
with SARdeno task client --name Player
without SAR
Config file is called: config.toml
[server]
hostname = "127.0.0.1"
port = 53000
[logging]
level = "debug" # debug, info, warn, error, critical
console = true # might want to turn it off in CLI mode
file = true
filename = "logs/server.log"
[countdown]
delay = 0
pre_commands = ""
post_commands = ""
It's recommended to use Docker Engine. The latest server image is on Docker Hub.
- Create config.toml
- Create
logs
folder - Create
docker-compose.yml
(see below) - Run
docker compose up -d
version: "3.8"
services:
server:
image: p2sr/ghosting:latest
container_name: "ghosting"
restart: always
ports:
- "127.0.0.1:53000:53000"
volumes:
- ./logs:/logs:rw
- ./config.toml:/config.toml:rw
This project also comes with a package that allows developers to write server plugins.
Warning
WIP. Subject to change.
import {
command,
IClient,
on,
Plugin,
ServerEvent,
} from 'jsr:@p2sr/ghosting';
export class ExamplePlugin extends Plugin {
name = 'Example Plugin';
version = '1.0.0';
authors = ['NeKz'];
license = 'MIT';
@command('ping', 'Ping the server!')
async onPing() {
await this.respond('pong!');
}
@command('dm <player> <message...>', 'DM a player!')
async onDirectMessage(
client: IClient,
message: string,
) {
await this.respondToClient(client, message);
}
@on(ServerEvent.ClientMessage)
async onClientMessage(message: string) {
// Use own command handler
const [command, ..._rest] = message.split(' ');
switch (command) {
case '/ping': {
await this.respond('pong');
break;
}
case '/dm': {
// ...
break;
}
}
}
}
SourceAutoRecord uses SFML which encodes integer types like u16
, u32
, u64
in big-endian (BE).
This is the encoded representation of sf::Packet from SFML. Every TCP packet includes a field for the total length of the data.
Field | Type | Description |
---|---|---|
length | u32 | TCP only! Length of data field. |
data | Header + Packet | Packet of type Connect, Ping etc. |
The header value (u8
) defines the type of the packet. Since UDP is optional for clients the implementation is only
required for HEART_BEAT
, COUNTDOWN
and UPDATE
packets.
Name | Value | TCP | UDP |
---|---|---|---|
NONE | 0 | ✅️ | |
PING | 1 | ✅️ | |
CONNECT | 2 | ✅️ | |
DISCONNECT | 3 | ✅️ | |
STOP_SERVER | 4 | ✅️ | |
MAP_CHANGE | 5 | ✅️ | |
HEART_BEAT | 6 | ✅️ | ✅️ |
MESSAGE | 7 | ✅️ | |
COUNTDOWN | 8 | ✅️ | ✅️ |
UPDATE | 9 | ✅️ | ✅️ |
SPEEDRUN_FINISH | 10 | ✅️ | |
MODEL_CHANGE | 11 | ✅️ | |
COLOR_CHANGE | 12 | ✅️ |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: connection_packet
Server->>Client: confirm_connection_packet
Server->>Clients: connect_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | CONNECT |
port | u16 | |
name | std::string | |
data | DataGhost | |
model_name | std::string | |
level_name | std::string | |
tcp_only | bool | |
color | Color | |
spectator | bool |
Note: This packet does not have a header. It is handled immediately after the connection_packet.
Field | Type | Description |
---|---|---|
id | u32 | |
nb_ghosts | u32 | |
ghosts | GhostEntity[nb_ghosts] |
Field | Type | Description |
---|---|---|
header | u8 | CONNECT |
id | u32 | |
name | std::string | |
data | DataGhost | |
model_name | std::string | |
level_name | std::string | |
tcp_only | bool | |
color | Color | |
spectator | bool |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: disconnect_packet
Server->>Clients: disconnect_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | DISCONNECT |
id | u32 |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
Client->>Server: ping_packet
Server->>Client: ping_echo_packet
Field | Type | Description |
---|---|---|
header | u8 | PING |
id | u32 |
Field | Type | Description |
---|---|---|
header | u8 | PING |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: map_change_packet
Server->>Clients: map_change_packet
Field | Type | Description |
---|---|---|
header | u8 | MAP_CHANGE |
id | u32 | |
map_name | std::string | |
ticks | u32 | |
tick_total | u32 |
Two missed heartbeats will mark a client as disconnected.
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Server->>Clients: heart_beat_packet (broadcast)
Server->>Server: delay 5sec
Server->>Clients: heart_beat_packet (broadcast)
Client->>Server: heart_beat_packet
Server->>Client: heart_beat_packet
Server->>Server: delay 5sec
Server->>Client: heart_beat_packet
Server->>Server: delay 5sec
Server->>Clients: disconnect_packet (broadcast)
Used to keep connection alive.
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Server->>Clients: heart_beat_packet (broadcast)
Server->>Server: delay 1sec
Server->>Clients: heart_beat_packet (broadcast)
Client->>Server: heart_beat_packet
Field | Type | Description |
---|---|---|
header | u8 | HEART_BEAT |
id | u32 | Server sends 0 |
token | u32 | UDP is always 0 |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: message_packet
Server->>Clients: message_packet (broadcast)
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
Note right of Clients: SourceAutoRecord
participant Clients
Server->>Clients: message_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | MESSAGE |
id | u32 | 0 if Server Message |
message | std::string |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
Server->>Clients: countdown_packet (broadcast)
Client->>Server: confirm_countdown_packet
Server->>Client: confirm_countdown_packet (id = 0)
Field | Type | Description |
---|---|---|
header | u8 | COUNTDOWN |
id | u32 | 0 |
step | u32 | 0 |
duration | u32 | |
pre_commands | std::string | |
post_commands | std::string |
Field | Type | Description |
---|---|---|
header | u8 | COUNTDOWN |
id | u32 | Server sends 0 |
step | u32 | 1 |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Server->>Clients: bulk_update_packet (broadcast)
Server->>Server: delay 50ms
Server->>Clients: bulk_update_packet (broadcast)
Server->>Server: delay 50ms
Client->>Server: update_packet
Server->>Clients: bulk_update_packet (broadcast)
Server->>Server: delay 50ms
Server->>Clients: bulk_update_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | UPDATE |
id | u32 | 0 |
count | u32 | |
update | DataGhostUpdate[] |
Field | Type | Description |
---|---|---|
header | u8 | UPDATE |
id | u32 | |
data | DataGhost |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: speedrun_finish_packet
Server->>Clients: speedrun_finish_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | SPEEDRUN_FINISH |
id | u32 | |
time | std::string |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: model_change_packet
Server->>Clients: model_change_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | MODEL_CHANGE |
id | u32 | |
model_name | std::string |
sequenceDiagram
participant Server
Note left of Server: ghost.portal2.sr
participant Client
Note right of Client: SourceAutoRecord
participant Clients
Client->>Server: color_change_packet
Server->>Clients: color_change_packet (broadcast)
Field | Type | Description |
---|---|---|
header | u8 | COLOR_CHANGE |
id | u32 | |
color | Color |
Field | Type | Description |
---|---|---|
length | u32 | |
value | char[length] |
Field | Type | Description |
---|---|---|
id | u32 | |
name | std::string | |
data | DataGhost | |
model_name | std::string | |
current_map | std::string | |
color | Color | |
spectator | bool |
Field | Type | Description |
---|---|---|
r | u8 | |
g | u8 | |
b | u8 |
Field | Type | Description |
---|---|---|
x | f32 | |
y | f32 | |
z | f32 |
Field | Type | Description |
---|---|---|
x | f32 | |
y | f32 | |
z | f32 |
Field | Type | Description |
---|---|---|
position | Vector | |
view_angle | QAngle | |
data | u8 | Encoded fields, see below. |
Field | Type | Description |
---|---|---|
view_offset | f32 | data & 0x7F (bit 1 to 7) |
grounded | bool | data & 0x80 (bit 8) |
Field | Type | Description |
---|---|---|
id | u32 | |
data | DataGhost |