Skip to content

Ghost server for future events. Boo! 👻

License

Notifications You must be signed in to change notification settings

NeKzor/ghosting

Repository files navigation

ghosting

GhostServer alternative for SourceAutoRecord.

Status

TODO

  • 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

Discoveries

  • ID is not a unique identifier
  • STOP_SERVER packet is a kill switch
  • SPEEDRUN_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

Wishlist

  • Secure protocol
  • Authentication
  • Role system (via plugins)
  • Server plugins (on TODO list)
  • SSR?
  • Client-side scripting?

Development

Requirements

Steps

  • Start the server with:
    • deno task start with CLI
    • deno task server without CLI
  • Connect to the server with:
    • ghost_connect 0.0.0.0 with SAR
    • deno task client --name Player without SAR

Config

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 = ""

Production

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

Plugins (TODO)

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;
      }
    }
  }
}

Protocol

Endianness

SourceAutoRecord uses SFML which encodes integer types like u16, u32, u64 in big-endian (BE).

Packets

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.

Header

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 ✅️

Connect

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)
Loading

connection_packet

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

confirm_connection_packet

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]

connect_packet

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

Disconnect

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)
Loading

disconnect_packet

Field Type Description
header u8 DISCONNECT
id u32

Ping

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
Loading

ping_packet

Field Type Description
header u8 PING
id u32

ping_echo_packet

Field Type Description
header u8 PING

Map Change

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
Loading

map_change_packet

Field Type Description
header u8 MAP_CHANGE
id u32
map_name std::string
ticks u32
tick_total u32

Heart Beat

TCP

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)
Loading

UPD

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
Loading

heart_beat_packet

Field Type Description
header u8 HEART_BEAT
id u32 Server sends 0
token u32 UDP is always 0

Message

Client Message

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)
Loading

Server Message

sequenceDiagram
    participant Server
    Note left of Server: ghost.portal2.sr
    Note right of Clients: SourceAutoRecord
    participant Clients

    Server->>Clients: message_packet (broadcast)
Loading

message_packet

Field Type Description
header u8 MESSAGE
id u32 0 if Server Message
message std::string

Countdown

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)
Loading

countdown_packet

Field Type Description
header u8 COUNTDOWN
id u32 0
step u32 0
duration u32
pre_commands std::string
post_commands std::string

confirm_countdown_packet

Field Type Description
header u8 COUNTDOWN
id u32 Server sends 0
step u32 1

Update

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)
Loading

bulk_update_packet

Field Type Description
header u8 UPDATE
id u32 0
count u32
update DataGhostUpdate[]

update_packet

Field Type Description
header u8 UPDATE
id u32
data DataGhost

Speedrun Finish

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)
Loading

speedrun_finish_packet

Field Type Description
header u8 SPEEDRUN_FINISH
id u32
time std::string

Model Change

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)
Loading

model_change_packet

Field Type Description
header u8 MODEL_CHANGE
id u32
model_name std::string

Color Change

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)
Loading

color_change_packet

Field Type Description
header u8 COLOR_CHANGE
id u32
color Color

Structs

std::string

Field Type Description
length u32
value char[length]

GhostEntity

Field Type Description
id u32
name std::string
data DataGhost
model_name std::string
current_map std::string
color Color
spectator bool

Color

Field Type Description
r u8
g u8
b u8

Vector

Field Type Description
x f32
y f32
z f32

QAngle

Field Type Description
x f32
y f32
z f32

DataGhost

Field Type Description
position Vector
view_angle QAngle
data u8 Encoded fields, see below.
Data
Field Type Description
view_offset f32 data & 0x7F (bit 1 to 7)
grounded bool data & 0x80 (bit 8)

DataGhostUpdate

Field Type Description
id u32
data DataGhost

License

MIT License