Server send game and server info

This commit is contained in:
ChillerDragon 2022-11-11 13:42:11 +01:00
parent 1baf3fcad0
commit 45d1361408
10 changed files with 193 additions and 16 deletions

25
lib/game_info.rb Normal file
View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require_relative 'packer'
class GameInfo
attr_accessor :game_flags, :score_limit, :time_limit, :match_num, :match_current
def initialize(attr = {})
@game_flags = attr[:game_flags] || 0
@score_limit = attr[:score_limit] || 0
@time_limit = attr[:time_limit] || 0
@match_num = attr[:match_num] || 0
@match_current = attr[:match_current] || 0
end
# basically to_network
# int array the server sends to the client
def to_a
Packer.pack_int(@game_flags) +
Packer.pack_int(@score_limit) +
Packer.pack_int(@time_limit) +
Packer.pack_int(@match_num) +
Packer.pack_int(@match_current)
end
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'map' require_relative 'map'
require_relative 'server_info'
require_relative 'game_info'
class GameServer class GameServer
attr_accessor :pred_game_tick, :ack_game_tick, :map attr_accessor :pred_game_tick, :ack_game_tick, :map
@ -28,4 +30,36 @@ class GameServer
@server.send_map(packet.addr) @server.send_map(packet.addr)
end end
def on_ready(_chunk, packet)
# vanilla server sends 3 chunks here usually
# - motd
# - server settings
# - ready
#
# We only send ready for now
@server.send_ready(packet.addr)
end
def on_startinfo(_chunk, packet)
# vanilla server sends 3 chunks here usually
# - vote clear options
# - tune params
# - ready to enter
#
# We only send ready to enter for now
@server.send_ready_to_enter(packet.addr)
end
def on_enter_game(_chunk, packet)
# vanilla server responds to enter game with two packets
# first:
# - server info
# second:
# - game info
# - client info
# - snap single
@server.send_server_info(packet.addr, ServerInfo.new.to_a)
@server.send_game_info(packet.addr, GameInfo.new.to_a)
end
end end

View file

@ -8,7 +8,8 @@
class NetBase class NetBase
attr_accessor :peer_token, :ack attr_accessor :peer_token, :ack
def initialize def initialize(opts = {})
@verbose = opts[:verbose] || false
@ip = nil @ip = nil
@port = nil @port = nil
@s = nil @s = nil

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Player class Player
attr_accessor :id, :local, :team, :name, :clan, :country, :skin_parts, :skin_custom_colors, :skin_colors attr_accessor :id, :local, :team, :name, :clan, :country, :skin_parts, :skin_custom_colors, :skin_colors, :score
def initialize(data = {}) def initialize(data = {})
@id = data[:id] || -1 @id = data[:id] || -1
@ -13,5 +13,7 @@ class Player
@skin_parts = data[:skin_parts] || Array.new(6, 'standard') @skin_parts = data[:skin_parts] || Array.new(6, 'standard')
@skin_custom_colors = data[:skin_custom_colors] || Array.new(6, 0) @skin_custom_colors = data[:skin_custom_colors] || Array.new(6, 0)
@skin_colors = data[:skin_colors] || Array.new(6, 0) @skin_colors = data[:skin_colors] || Array.new(6, 0)
@score = data[:score] || 0
end end
end end

View file

@ -1,16 +1,64 @@
# frozen_string_literal: true # frozen_string_literal: true
class ServerInfo require_relative 'network'
attr_reader :version, :name, :map, :gametype require_relative 'player'
require_relative 'packer'
def initialize(infos) class ServerInfo
@version = infos[0] attr_accessor :version, :name, :hostname, :map, :gametype, :flags, :num_players, :max_players, :num_clients,
@name = infos[1] :max_clients, :players
@map = infos[2]
@gametype = infos[3] def initialize
# short tokenless version
@version = GAME_VERSION # '0.7.5'
@name = 'unnamed ruby server'
@hostname = 'localhost'
@map = 'dm1'
@gametype = 'dm'
@flags = 0
@num_players = 1
@max_players = MAX_PLAYERS
@num_clients = 1
@max_clients = MAX_CLIENTS
# token only
@players = [
Player.new(
id: 0,
local: 0,
team: 0,
name: 'sample player',
clan: '',
country: -1
)
]
end end
def to_s def to_s
"version=#{@version} gametype=#{gametype} map=#{map} name=#{name}" "version=#{@version} gametype=#{gametype} map=#{map} name=#{name}"
end end
# basically to_network
# int array the server sends to the client
def to_a
data = []
data = Packer.pack_str(@version) +
Packer.pack_str(@name) +
Packer.pack_str(@hostname) +
Packer.pack_str(@map) +
Packer.pack_str(@gametype) +
Packer.pack_int(@flags) +
Packer.pack_int(@num_players) +
Packer.pack_int(@max_players) +
Packer.pack_int(@num_clients) +
Packer.pack_int(@max_clients)
@players.each do |player|
data += Packer.pack_str(player.name)
data += Packer.pack_str(player.clan)
data += Packer.pack_int(player.country)
data += Packer.pack_int(player.score)
data += Packer.pack_int(0) # TODO: bot and spec flag
end
data
end
end end

View file

@ -106,7 +106,7 @@ class TeeworldsClient
@client_token = (1..4).to_a.map { |_| rand(0..255) } @client_token = (1..4).to_a.map { |_| rand(0..255) }
@client_token = @client_token.map { |b| b.to_s(16) }.join @client_token = @client_token.map { |b| b.to_s(16) }.join
puts "client token #{@client_token}" puts "client token #{@client_token}"
@netbase = NetBase.new @netbase = NetBase.new(verbose: @verbose)
NetChunk.reset NetChunk.reset
@ip = ip @ip = ip
@port = port @port = port

View file

@ -36,7 +36,7 @@ class TeeworldsServer
@server_token = (1..4).to_a.map { |_| rand(0..255) } @server_token = (1..4).to_a.map { |_| rand(0..255) }
@server_token = @server_token.map { |b| b.to_s(16) }.join @server_token = @server_token.map { |b| b.to_s(16) }.join
puts "server token #{@server_token}" puts "server token #{@server_token}"
@netbase = NetBase.new @netbase = NetBase.new(verbose: @verbose)
NetChunk.reset NetChunk.reset
@ip = ip @ip = ip
@port = port @port = port
@ -51,19 +51,29 @@ class TeeworldsServer
end end
end end
def on_system_chunk(chunk) def on_message(chunk, packet)
puts "got system chunk: #{chunk}" puts "got game chunk: #{chunk}"
case chunk.msg
when NETMSGTYPE_CL_STARTINFO then @game_server.on_startinfo(chunk, packet)
else
puts "Unsupported game msg: #{chunk.msg}"
exit(1)
end
end end
def process_chunk(chunk, packet) def process_chunk(chunk, packet)
unless chunk.sys unless chunk.sys
on_system_chunk(chunk) on_message(chunk, packet)
return return
end end
puts "proccess chunk with msg: #{chunk.msg}" puts "proccess chunk with msg: #{chunk.msg}"
case chunk.msg case chunk.msg
when NETMSG_INFO when NETMSG_INFO
@game_server.on_info(chunk, packet) @game_server.on_info(chunk, packet)
when NETMSG_READY
@game_server.on_ready(chunk, packet)
when NETMSG_ENTERGAME
@game_server.on_enter_game(chunk, packet)
else else
puts "Unsupported system msg: #{chunk.msg}" puts "Unsupported system msg: #{chunk.msg}"
exit(1) exit(1)
@ -109,12 +119,38 @@ class TeeworldsServer
data += Packer.pack_int(8) # chunk num? data += Packer.pack_int(8) # chunk num?
data += Packer.pack_int(MAP_CHUNK_SIZE) data += Packer.pack_int(MAP_CHUNK_SIZE)
data += @game_server.map.sha256_arr # poor mans pack_raw() data += @game_server.map.sha256_arr # poor mans pack_raw()
msg = NetChunk.create_non_vital_header(size: data.size + 1) + msg = NetChunk.create_vital_header({ vital: true }, data.size + 1) +
[pack_msg_id(NETMSG_MAP_CHANGE, system: true)] + [pack_msg_id(NETMSG_MAP_CHANGE, system: true)] +
data data
@netbase.send_packet(msg, 1, addr:) @netbase.send_packet(msg, 1, addr:)
end end
def send_ready(addr)
msg = NetChunk.create_vital_header({ vital: true }, 1) +
[pack_msg_id(NETMSG_CON_READY, system: true)]
@netbase.send_packet(msg, 1, addr:)
end
def send_ready_to_enter(addr)
msg = NetChunk.create_vital_header({ vital: true }, 1) +
[pack_msg_id(NETMSGTYPE_SV_READYTOENTER, system: false)]
@netbase.send_packet(msg, 1, addr:)
end
def send_server_info(addr, server_info)
msg = NetChunk.create_vital_header({ vital: true }, 1 + server_info.size) +
[pack_msg_id(NETMSG_SERVERINFO, system: true)] +
server_info
@netbase.send_packet(msg, 1, addr:)
end
def send_game_info(addr, data)
msg = NetChunk.create_vital_header({ vital: true }, 1 + data.size) +
[pack_msg_id(NETMSGTYPE_SV_GAMEINFO, system: false)] +
data
@netbase.send_packet(msg, 1, addr:)
end
def on_ctrl_token(packet) def on_ctrl_token(packet)
u = Unpacker.new(packet.payload[1..]) u = Unpacker.new(packet.payload[1..])
token = u.get_raw(4) token = u.get_raw(4)

View file

@ -3,5 +3,5 @@
require_relative 'lib/teeworlds_server' require_relative 'lib/teeworlds_server'
srv = TeeworldsServer.new(verbose: false) srv = TeeworldsServer.new(verbose: true)
srv.run('127.0.0.1', 8303) srv.run('127.0.0.1', 8303)

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
require_relative '../lib/server_info'
describe 'ServerInfo', :server_info do
context 'Pack to network' do
it 'Should match expected array' do
arr = [
0x30, 0x2E, 0x37, 0x2E, 0x35, 0x00, 0x75, 0x6E, 0x6E, 0x61,
0x6D, 0x65, 0x64, 0x20, 0x72, 0x75, 0x62, 0x79, 0x20, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x00, 0x6C, 0x6F, 0x63, 0x61,
0x6C, 0x68, 0x6F, 0x73, 0x74, 0x00, 0x64, 0x6D, 0x31, 0x00,
0x64, 0x6D, 0x00, 0x00, 0x01, 0x10, 0x01, 0x80, 0x01, 0x73,
0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x70, 0x6C, 0x61, 0x79,
0x65, 0x72, 0x00, 0x00, 0x40, 0x00, 0x00
]
expect(ServerInfo.new.to_a).to eq(arr)
end
end
end

11
spec/07_game_info_spec.rb Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
require_relative '../lib/game_info'
describe 'GameInfo', :game_info do
context 'Pack to network' do
it 'Should match expected array' do
expect(GameInfo.new.to_a).to eq([0, 0, 0, 0, 0])
end
end
end