From 1baf3fcad057df4a17bd8a623e71f69d101420fc Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Fri, 11 Nov 2022 10:21:48 +0100 Subject: [PATCH] Start working on server side map packet --- lib/array.rb | 5 ++++ lib/game_server.rb | 17 ++++++++--- lib/map.rb | 46 +++++++++++++++++++++++++++++ lib/message.rb | 11 +++++++ lib/net_addr.rb | 4 +++ lib/network.rb | 12 ++++++++ lib/packet.rb | 44 ++-------------------------- lib/packet_flags.rb | 42 +++++++++++++++++++++++++++ lib/teeworlds_client.rb | 11 +------ lib/teeworlds_server.rb | 64 +++++++++++++++++++++++++++++++++++------ 10 files changed, 193 insertions(+), 63 deletions(-) create mode 100644 lib/map.rb create mode 100644 lib/message.rb create mode 100644 lib/packet_flags.rb diff --git a/lib/array.rb b/lib/array.rb index dab9c61..abe1bf6 100644 --- a/lib/array.rb +++ b/lib/array.rb @@ -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 diff --git a/lib/game_server.rb b/lib/game_server.rb index 6ad543b..c5fb994 100644 --- a/lib/game_server.rb +++ b/lib/game_server.rb @@ -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 diff --git a/lib/map.rb b/lib/map.rb new file mode 100644 index 0000000..df4c4a6 --- /dev/null +++ b/lib/map.rb @@ -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 diff --git a/lib/message.rb b/lib/message.rb new file mode 100644 index 0000000..da088e0 --- /dev/null +++ b/lib/message.rb @@ -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 diff --git a/lib/net_addr.rb b/lib/net_addr.rb index 15d8fcd..e4d6df4 100644 --- a/lib/net_addr.rb +++ b/lib/net_addr.rb @@ -11,4 +11,8 @@ class NetAddr def to_s "#{@ip}:#{@port}" end + + def eq(addr) + @ip == addr.ip && @port == addr.port + end end diff --git a/lib/network.rb b/lib/network.rb index 359c9dc..5b1cde9 100644 --- a/lib/network.rb +++ b/lib/network.rb @@ -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 diff --git a/lib/packet.rb b/lib/packet.rb index d435739..1e8a4bf 100644 --- a/lib/packet.rb +++ b/lib/packet.rb @@ -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 diff --git a/lib/packet_flags.rb b/lib/packet_flags.rb new file mode 100644 index 0000000..e29477b --- /dev/null +++ b/lib/packet_flags.rb @@ -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 diff --git a/lib/teeworlds_client.rb b/lib/teeworlds_client.rb index a7e80d8..d35fb5b 100644 --- a/lib/teeworlds_client.rb +++ b/lib/teeworlds_client.rb @@ -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, diff --git a/lib/teeworlds_server.rb b/lib/teeworlds_server.rb index 90cc0f7..e53e6e1 100755 --- a/lib/teeworlds_server.rb +++ b/lib/teeworlds_server.rb @@ -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