Start working on server side map packet

This commit is contained in:
ChillerDragon 2022-11-11 10:21:48 +01:00
parent 0a04af1cb6
commit 1baf3fcad0
10 changed files with 193 additions and 63 deletions

View file

@ -1,5 +1,10 @@
# frozen_string_literal: true
# I JUST REALIZED I ALREADY USED .scan(/../)
# TO GET .groups_of(2)
# AND DUUUUH .scan() IS BASICALLY ALREADY
# .groups_of()
# TODO: get rid of it?!
class Array
def groups_of(max_size)
return [] if max_size < 1

View file

@ -1,22 +1,31 @@
# frozen_string_literal: true
require_relative 'map'
class GameServer
attr_accessor :pred_game_tick, :ack_game_tick
attr_accessor :pred_game_tick, :ack_game_tick, :map
def initialize(server)
@server = server
@ack_game_tick = -1
@pred_game_tick = 0
@map = Map.new(
name: 'dm1',
crc: 1_683_261_464,
size: 6793,
sha256: '491af17a510214506270904f147a4c30ae0a85b91bb854395bef8c397fc078c3'
)
end
def on_info(chunk)
def on_info(chunk, packet)
u = Unpacker.new(chunk.data[1..])
net_version = u.get_string
password = u.get_string
client_version = u.get_int
puts "vers=#{net_version} vers=#{client_version} pass=#{password}"
# TODO: respond with map info
# here tho? Check tw code when to send map info
# TODO: check version and password
@server.send_map(packet.addr)
end
end

46
lib/map.rb Normal file
View file

@ -0,0 +1,46 @@
# frozen_string_literal: true
require_relative 'bytes'
class Map
attr_reader :name, :crc, :size, :sha256, :sha256_str, :sha256_arr
def initialize(attr = {})
# map name as String
@name = attr[:name]
# crc has to be a positive Integer
@crc = attr[:crc]
# size has to be a positive Integer
@size = attr[:size]
# sha256 can be:
# hex encoded string (64 characters / 32 bytes)
# '491af17a510214506270904f147a4c30ae0a85b91bb854395bef8c397fc078c3'
#
# raw string (32 characters)
# array of integers representing the bytes (32 elements)
@sha256 = attr[:sha256]
if @sha256.instance_of?(String)
if @sha256.match(/[a-fA-F0-9]{64}/) # str encoded hex
@sha256_str = @sha256
@sha256_arr = str_bytes(@sha256)
@sha256 = @sha256_arr.pack('C*')
elsif @sha256.length == 32 # raw byte string
@sha256_arr = @sha256
@sha256 = @sha256_arr.pack('C*')
@sha256_str = str_hex(@sha256).gsub(' ', '')
else
raise "Error: map raw string expects size 32 but got #{@sha256.size}"
end
elsif @sha256.instance_of?(Array) # int byte array
raise "Error: map sha256 array expects size 32 but got #{@sha256.size}" if @sha256.size != 32
@sha256_arr = @sha256
@sha256 = @sha256.pack('C*')
@sha256_str = @sha256.map { |b| b.to_s(16) }.join
end
end
end

11
lib/message.rb Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
##
# Turns int into network byte
#
# Takes a NETMSGTYPE_CL_* integer
# and returns a byte that can be send over
# the network
def pack_msg_id(msg_id, options = { system: false })
(msg_id << 1) | (options[:system] ? 1 : 0)
end

View file

@ -11,4 +11,8 @@ class NetAddr
def to_s
"#{@ip}:#{@port}"
end
def eq(addr)
@ip == addr.ip && @port == addr.port
end
end

View file

@ -87,7 +87,14 @@ NET_CONNSTATE_PENDING = 3
NET_CONNSTATE_ONLINE = 4
NET_CONNSTATE_ERROR = 5
NET_MAX_CHUNKHEADERSIZE = 3
NET_PACKETHEADERSIZE = 7
NET_PACKETHEADERSIZE_CONNLESS = NET_PACKETHEADERSIZE + 2
NET_MAX_PACKETHEADERSIZE = NET_PACKETHEADERSIZE_CONNLESS
NET_MAX_PACKETSIZE = 1400
NET_MAX_PAYLOAD = NET_MAX_PACKETSIZE - NET_MAX_PACKETHEADERSIZE
CHAT_NONE = 0
CHAT_ALL = 1
@ -99,3 +106,8 @@ TARGET_SERVER = -1
PACKET_HEADER_SIZE = 7
CHUNK_HEADER_SIZE = 3
MAX_CLIENTS = 64
MAX_PLAYERS = 16
MAP_CHUNK_SIZE = NET_MAX_PAYLOAD - NET_MAX_CHUNKHEADERSIZE - 4 # msg type

View file

@ -1,53 +1,14 @@
# frozen_string_literal: true
require_relative 'net_addr'
require_relative 'packet_flags'
require 'huffman_tw'
class PacketFlags
attr_reader :bits, :hash
def initialize(data)
@hash = {}
@bits = ''
if data.instance_of?(Hash)
@bits = parse_hash(data)
@hash = data
elsif data.instance_of?(String)
@hash = parse_bits(data)
@bits = data
else
raise 'Flags have to be hash or string'
end
end
def parse_hash(hash)
bits = ''
bits += hash[:connection] ? '1' : '0'
bits += hash[:compressed] ? '1' : '0'
bits += hash[:resend] ? '1' : '0'
bits += hash[:control] ? '1' : '0'
bits
end
def parse_bits(four_bit_str)
# takes a 4 character string
# representing the middle of the first byte sent
# in binary representation
#
# and creates a hash out of it
hash = {}
hash[:connection] = four_bit_str[0] == '1'
hash[:compressed] = four_bit_str[1] == '1'
hash[:resend] = four_bit_str[2] == '1'
hash[:control] = four_bit_str[3] == '1'
hash
end
end
# Class holding the parsed packet data
class Packet
attr_reader :flags, :payload, :addr
attr_accessor :client_id
def initialize(data, prefix = '')
# @data and @payload
@ -60,6 +21,7 @@ class Packet
@prefix = prefix
@addr = NetAddr.new(nil, nil)
@huffman = Huffman.new
@client_id = nil
@data = data
flags_byte = @data[0].unpack('B*')
@flags = PacketFlags.new(flags_byte.first[2..5]).hash

42
lib/packet_flags.rb Normal file
View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
class PacketFlags
attr_reader :bits, :hash
def initialize(data)
@hash = {}
@bits = ''
if data.instance_of?(Hash)
@bits = parse_hash(data)
@hash = data
elsif data.instance_of?(String)
@hash = parse_bits(data)
@bits = data
else
raise 'Flags have to be hash or string'
end
end
def parse_hash(hash)
bits = ''
bits += hash[:connection] ? '1' : '0'
bits += hash[:compressed] ? '1' : '0'
bits += hash[:resend] ? '1' : '0'
bits += hash[:control] ? '1' : '0'
bits
end
def parse_bits(four_bit_str)
# takes a 4 character string
# representing the middle of the first byte sent
# in binary representation
#
# and creates a hash out of it
hash = {}
hash[:connection] = four_bit_str[0] == '1'
hash[:compressed] = four_bit_str[1] == '1'
hash[:resend] = four_bit_str[2] == '1'
hash[:control] = four_bit_str[3] == '1'
hash
end
end

View file

@ -13,6 +13,7 @@ require_relative 'net_base'
require_relative 'packer'
require_relative 'player'
require_relative 'game_client'
require_relative 'message'
class TeeworldsClient
attr_reader :state, :hooks, :game_client
@ -246,16 +247,6 @@ class TeeworldsClient
)
end
##
# Turns int into network byte
#
# Takes a NETMSGTYPE_CL_* integer
# and returns a byte that can be send over
# the network
def pack_msg_id(msg_id, options = { system: false })
(msg_id << 1) | (options[:system] ? 1 : 0)
end
def send_input
inp = {
direction: -1,

View file

@ -12,6 +12,16 @@ require_relative 'net_base'
require_relative 'net_addr'
require_relative 'packer'
require_relative 'game_server'
require_relative 'message'
class Client
attr_accessor :id, :addr
def initialize(attr = {})
@id = attr[:id]
@addr = attr[:addr]
end
end
class TeeworldsServer
def initialize(options = {})
@ -19,6 +29,7 @@ class TeeworldsServer
@ip = '127.0.0.1'
@port = 8303
@game_server = GameServer.new(self)
@clients = {}
end
def run(ip, port)
@ -44,7 +55,7 @@ class TeeworldsServer
puts "got system chunk: #{chunk}"
end
def process_chunk(chunk)
def process_chunk(chunk, packet)
unless chunk.sys
on_system_chunk(chunk)
return
@ -52,7 +63,7 @@ class TeeworldsServer
puts "proccess chunk with msg: #{chunk.msg}"
case chunk.msg
when NETMSG_INFO
@game_server.on_info(chunk)
@game_server.on_info(chunk, packet)
else
puts "Unsupported system msg: #{chunk.msg}"
exit(1)
@ -66,7 +77,7 @@ class TeeworldsServer
@netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE
puts "got ack: #{@netbase.ack}" if @verbose
end
process_chunk(chunk)
process_chunk(chunk, packet)
end
end
@ -90,6 +101,20 @@ class TeeworldsServer
@netbase.send_packet(msg, 0, control: true, addr:)
end
def send_map(addr)
data = []
data += Packer.pack_str(@game_server.map.name)
data += Packer.pack_int(@game_server.map.crc)
data += Packer.pack_int(@game_server.map.size)
data += Packer.pack_int(8) # chunk num?
data += Packer.pack_int(MAP_CHUNK_SIZE)
data += @game_server.map.sha256_arr # poor mans pack_raw()
msg = NetChunk.create_non_vital_header(size: data.size + 1) +
[pack_msg_id(NETMSG_MAP_CHANGE, system: true)] +
data
@netbase.send_packet(msg, 1, addr:)
end
def on_ctrl_token(packet)
u = Unpacker.new(packet.payload[1..])
token = u.get_raw(4)
@ -106,7 +131,15 @@ class TeeworldsServer
end
def on_ctrl_connect(packet)
puts "Got connect from #{packet.addr}"
puts 'got connection, sending accept'
id = get_next_client_id
if id == -1
puts 'server full drop packet. TODO: tell the client'
return
end
client = Client.new(id:, addr: packet.addr)
@clients[id] = client
@netbase.send_packet([NET_CTRLMSG_ACCEPT], 0, control: true, addr: packet.addr)
end
@ -119,18 +152,33 @@ class TeeworldsServer
end
end
def get_next_client_id
(0..MAX_CLIENTS).each do |i|
next if @clients[i]
return i
end
-1
end
def tick
begin
data, client = @s.recvfrom_nonblock(1400)
data, sender_inet_addr = @s.recvfrom_nonblock(1400)
rescue IO::EAGAINWaitReadable
data = nil
client = nil
sender_inet_addr = nil
end
return unless data
packet = Packet.new(data, '<')
packet.addr.ip = client[2] # or 3 idk bot 127.0.0.1 in my local test case
packet.addr.port = client[1]
packet.addr.ip = sender_inet_addr[2] # or 3 idk bot 127.0.0.1 in my local test case
packet.addr.port = sender_inet_addr[1]
@clients.each do |id, client|
next unless packet.addr.eq(client.addr)
packet.client_id = id
end
puts packet.to_s if @verbose
on_packet(packet)
end