176 lines
3.6 KiB
Ruby
176 lines
3.6 KiB
Ruby
# Nathan Hinton 20 Dec 2025
|
|
# The main game server. This will handle the clients and the game in one go
|
|
|
|
require 'async'
|
|
require 'async/websocket'
|
|
require 'json'
|
|
require 'yaml'
|
|
|
|
require './game'
|
|
|
|
class GameServer
|
|
def initialize(conf_path)
|
|
@clients = {}
|
|
File.open(conf_path, 'r') do |fi|
|
|
world_config = YAML.safe_load(fi.read)
|
|
end
|
|
puts world_config
|
|
@world = World.new(
|
|
width: world_config['width'],
|
|
height: world_config['height'],
|
|
planets: world_config['planets'],
|
|
game_id: world_config['game_id'],
|
|
name: world_config['name']
|
|
)
|
|
end
|
|
|
|
def start(endpoint)
|
|
Async do |task|
|
|
Async::WebSocket::Server.open(endpoint) do |connection|
|
|
task.async do
|
|
handle_connection(connection)
|
|
end
|
|
end
|
|
task.async {simulation_loop}
|
|
end
|
|
end
|
|
|
|
def handle_connection(ws)
|
|
player = nil
|
|
while message == ws.read
|
|
data = JSON.parse(message)
|
|
response = nil
|
|
|
|
case data['type']
|
|
when 'hello'
|
|
player = handle_hello(ws, data)
|
|
when 'querey'
|
|
response = handle_query(ws, data)
|
|
when 'command'
|
|
response = handle_command(ws, data)
|
|
else
|
|
response = send_error('UNKNOWN_TYPE', 'Unknown message type')
|
|
end
|
|
|
|
ws.write(JSON.dump(response)) if response
|
|
end
|
|
rescue => e
|
|
puts "Connection error: #{e.message}"
|
|
ensure
|
|
disconnect(player)
|
|
end
|
|
|
|
def handle_hello(ws, data)
|
|
token = data.dig("payload", "auth_token")
|
|
player = authenticate(token)
|
|
|
|
@clients[player.id] = ws
|
|
|
|
ws.write(
|
|
JSON.dump(
|
|
{
|
|
type: "update",
|
|
domain: "all",
|
|
payload: {
|
|
player_id: player_id,
|
|
server_time: @world.time,
|
|
game_speed: @world.game_speed
|
|
player: player.snapshot
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
return player
|
|
end
|
|
|
|
def handle_query(player, data)
|
|
domain = data["domain"]
|
|
|
|
payload =
|
|
case domain
|
|
when "player"
|
|
player.snapshot
|
|
when "ship"
|
|
ship_id = data.dig("payload", "ship_id")
|
|
@world.ship(ship_id)&.snapshot
|
|
else
|
|
return error("UNKNOWN_DOMAIN", "Unknown query domain")
|
|
end
|
|
|
|
{
|
|
type: "update",
|
|
domain: domain,
|
|
payload: payload
|
|
}
|
|
end
|
|
|
|
def handle_command(player, data)
|
|
domain = data["domain"]
|
|
payload = data["payload"]
|
|
|
|
case domain
|
|
when "ship"
|
|
validate_and_queue_ship_command(player, payload)
|
|
else
|
|
return error("UNKNOWN_DOMAIN", "Unknown command domain")
|
|
end
|
|
|
|
{
|
|
type: "update",
|
|
domain: "command",
|
|
payload: { status: "accepted" }
|
|
}
|
|
rescue ValidationError => e
|
|
error("INVALID_COMMAND", e.message)
|
|
end
|
|
|
|
def validate_and_queue_ship_command(player, payload)
|
|
ship = @world.ship(payload["ship_id"])
|
|
raise ValidationError, "Ship not found" unless ship
|
|
raise ValidationError, "Not your ship" unless ship.owner == player
|
|
|
|
ship.set_target(payload["target"])
|
|
end
|
|
|
|
def broadcast_event(type, domain, payload)
|
|
message = JSON.dump({
|
|
type: type,
|
|
domain: domain,
|
|
payload: payload
|
|
})
|
|
|
|
@clients.values.each do |ws|
|
|
ws.write(message)
|
|
end
|
|
end
|
|
|
|
|
|
def simulation_loop
|
|
last = Time.now
|
|
|
|
loop do
|
|
now = Time.now
|
|
delta_real = now - last
|
|
delta_sim = delta_real * @world.game_speed
|
|
|
|
@world.advance(delta_sim)
|
|
|
|
last = now
|
|
sleep 0.05 # 20 tps
|
|
end
|
|
end
|
|
|
|
def error(code, message)
|
|
{
|
|
type: "error",
|
|
domain: "system",
|
|
payload: {
|
|
code: code,
|
|
message: message
|
|
}
|
|
}
|
|
end
|
|
|
|
end
|