rubocop -a

This commit is contained in:
ChillerDragon 2022-11-05 17:19:05 +01:00
parent 0be954538c
commit ddef46991b
19 changed files with 192 additions and 229 deletions

View file

@ -9,27 +9,28 @@ client = TeeworldsClient.new(verbose: true)
# all keys are optional # all keys are optional
# if not provided they will fall back to the default value # if not provided they will fall back to the default value
client.set_startinfo( client.set_startinfo(
name: "ruby gamer", name: 'ruby gamer',
clan: "", clan: '',
country: -1, country: -1,
body: "spiky", body: 'spiky',
marking: "duodonny", marking: 'duodonny',
decoration: "", decoration: '',
hands: "standard", hands: 'standard',
feet: "standard", feet: 'standard',
eyes: "standard", eyes: 'standard',
custom_color_body: 0, custom_color_body: 0,
custom_color_marking: 0, custom_color_marking: 0,
custom_color_decoration: 0, custom_color_decoration: 0,
custom_color_hands: 0, custom_color_hands: 0,
custom_color_feet: 0, custom_color_feet: 0,
custom_color_eyes: 0, custom_color_eyes: 0,
color_body: 0, color_body: 0,
color_marking: 0, color_marking: 0,
color_decoration: 0, color_decoration: 0,
color_hands: 0, color_hands: 0,
color_feet: 0, color_feet: 0,
color_eyes: 0) color_eyes: 0
)
# connect to localhost and block the current thread # connect to localhost and block the current thread
client.connect('localhost', 8303, detach: false) client.connect('localhost', 8303, detach: false)

View file

@ -4,7 +4,7 @@ class Array
groups = [] groups = []
group = [] group = []
self.each do |item| each do |item|
group.push(item) group.push(item)
if group.size >= max_size if group.size >= max_size
@ -16,4 +16,3 @@ class Array
groups groups
end end
end end

View file

@ -1,18 +1,17 @@
# turn byte array into hex string # turn byte array into hex string
def str_hex(data) def str_hex(data)
data.unpack("H*").first.scan(/../).join(' ').upcase data.unpack1('H*').scan(/../).join(' ').upcase
end end
# turn hex string to byte array # turn hex string to byte array
def str_bytes(str) def str_bytes(str)
str.scan(/../).map{ |b| b.to_i(16) } str.scan(/../).map { |b| b.to_i(16) }
end end
def bytes_to_str(data) def bytes_to_str(data)
data.unpack("H*").join('') data.unpack('H*').join('')
end end
def get_byte(data, start = 0, num = 1) def get_byte(data, start = 0, num = 1)
data[start...(start+num)].unpack("H*").join('').upcase data[start...(start + num)].unpack('H*').join('').upcase
end end

View file

@ -27,4 +27,3 @@ class ChatMesage
"#{@author.name}: #{@message}" "#{@author.name}: #{@message}"
end end
end end

View file

@ -4,6 +4,7 @@ require_relative 'bytes'
class NetChunk class NetChunk
attr_reader :next, :data, :msg, :sys, :flags attr_reader :next, :data, :msg, :sys, :flags
@@sent_vital_chunks = 0 @@sent_vital_chunks = 0
def initialize(data) def initialize(data)
@ -14,8 +15,8 @@ class NetChunk
chunk_end = CHUNK_HEADER_SIZE + @size chunk_end = CHUNK_HEADER_SIZE + @size
# puts "data[0]: " + str_hex(data[0]) # puts "data[0]: " + str_hex(data[0])
@data = data[CHUNK_HEADER_SIZE...chunk_end] @data = data[CHUNK_HEADER_SIZE...chunk_end]
@msg = @data[0].unpack("C*").first @msg = @data[0].unpack1('C*')
@sys = @msg & 1 == 1 ? true : false @sys = @msg & 1 == 1
@msg >>= 1 @msg >>= 1
@next = data[chunk_end..] if data.size > chunk_end @next = data[chunk_end..] if data.size > chunk_end
end end
@ -26,9 +27,9 @@ class NetChunk
def to_s def to_s
"NetChunk\n" + "NetChunk\n" +
" msg=#{msg} sys=#{sys}\n" + " msg=#{msg} sys=#{sys}\n" +
" #{@flags}\n" + " #{@flags}\n" +
" data: #{str_hex(data)}" " data: #{str_hex(data)}"
end end
## ##
@ -42,9 +43,7 @@ class NetChunk
# represented as an Array of 3 integers # represented as an Array of 3 integers
def self.create_vital_header(flags, size, seq = nil) def self.create_vital_header(flags, size, seq = nil)
@@sent_vital_chunks += 1 @@sent_vital_chunks += 1
if seq.nil? seq = @@sent_vital_chunks if seq.nil?
seq = @@sent_vital_chunks
end
flag_bits = '00' flag_bits = '00'
flag_bits[0] = flags[:resend] ? '1' : '0' flag_bits[0] = flags[:resend] ? '1' : '0'
@ -54,7 +53,6 @@ class NetChunk
# size_bits[0..5] # size_bits[0..5]
# size_bits[6..] # size_bits[6..]
seq_bits = seq.to_s(2).rjust(10, '0') seq_bits = seq.to_s(2).rjust(10, '0')
# seq_bits[0..1] # seq_bits[0..1]
# seq_bits[2..] # seq_bits[2..]
@ -68,7 +66,7 @@ class NetChunk
# q=sequence # q=sequence
# #
# ffss ssss qqss ssss qqqq qqqq # ffss ssss qqss ssss qqqq qqqq
header_bits = header_bits =
flag_bits + flag_bits +
size_bits[0..5] + size_bits[0..5] +
seq_bits[0..1] + seq_bits[0..1] +
@ -81,17 +79,17 @@ class NetChunk
def parse_header(data) def parse_header(data)
# flags # flags
flags = data[0].unpack("B*").first flags = data[0].unpack1('B*')
flags = flags[0..1] flags = flags[0..1]
@flags[:resend] = flags[0] == "1" @flags[:resend] = flags[0] == '1'
@flags[:vital] = flags[1] == "1" @flags[:vital] = flags[1] == '1'
# size # size
size = data[0..1].unpack("B*").first size = data[0..1].unpack1('B*')
size_bytes = size.chars.groups_of(8) size_bytes = size.chars.groups_of(8)
# trim first 2 bits of both bytes # trim first 2 bits of both bytes
# Size: 2 bytes (..00 0000 ..00 0010) # Size: 2 bytes (..00 0000 ..00 0010)
size_bytes.map! {|b| b[2..].join('') } size_bytes.map! { |b| b[2..].join('') }
@size = size_bytes.join('').to_i(2) @size = size_bytes.join('').to_i(2)
# sequence number # sequence number
@ -117,13 +115,12 @@ class BigChungusTheChunkGetter
while chunk.next while chunk.next
chunk = NetChunk.new(chunk.next) chunk = NetChunk.new(chunk.next)
chunks.push(chunk) chunks.push(chunk)
if chunks.size > MAX_NUM_CHUNKS next unless chunks.size > MAX_NUM_CHUNKS
# inf loop guard case
puts "Warning: abort due to max num chunks bein reached" # inf loop guard case
break puts 'Warning: abort due to max num chunks bein reached'
end break
end end
chunks chunks
end end
end end

View file

@ -14,7 +14,7 @@ class Context
end end
def verify def verify
@data.each do |key, value| @data.each do |key, _value|
next if @old_data.key? key next if @old_data.key? key
raise "Error: invalid data key '#{key}'\n valid keys: #{@old_data.keys}" raise "Error: invalid data key '#{key}'\n valid keys: #{@old_data.keys}"
@ -42,19 +42,20 @@ class GameClient
# puts "Got playerinfo flags: #{chunk.flags}" # puts "Got playerinfo flags: #{chunk.flags}"
u = Unpacker.new(chunk.data[1..]) u = Unpacker.new(chunk.data[1..])
player = Player.new( player = Player.new(
id: u.get_int(), id: u.get_int,
local: u.get_int(), local: u.get_int,
team: u.get_int(), team: u.get_int,
name: u.get_string(), name: u.get_string,
clan: u.get_string(), clan: u.get_string,
country: u.get_int()) country: u.get_int
)
# skinparts and the silent flag # skinparts and the silent flag
# are currently ignored # are currently ignored
context = Context.new( context = Context.new(
@client, @client,
player: player, player:,
chunk: chunk chunk:
) )
if @client.hooks[:client_info] if @client.hooks[:client_info]
@client.hooks[:client_info].call(context) @client.hooks[:client_info].call(context)
@ -68,17 +69,17 @@ class GameClient
def on_client_drop(chunk) def on_client_drop(chunk)
u = Unpacker.new(chunk.data[1..]) u = Unpacker.new(chunk.data[1..])
client_id = u.get_int() client_id = u.get_int
reason = u.get_string() reason = u.get_string
silent = u.get_int() silent = u.get_int
context = Context.new( context = Context.new(
@cliemt, @cliemt,
player: @players[client_id], player: @players[client_id],
chunk: chunk, chunk:,
client_id: client_id, client_id:,
reason: reason == '' ? nil : reason, reason: reason == '' ? nil : reason,
silent: silent silent:
) )
if @client.hooks[:client_drop] if @client.hooks[:client_drop]
@client.hooks[:client_drop].call(context) @client.hooks[:client_drop].call(context)
@ -89,7 +90,7 @@ class GameClient
@players.delete(context.data[:client_id]) @players.delete(context.data[:client_id])
end end
def on_ready_to_enter(chunk) def on_ready_to_enter(_chunk)
@client.send_enter_game @client.send_enter_game
end end
@ -103,11 +104,10 @@ class GameClient
@client.send_msg_startinfo @client.send_msg_startinfo
end end
def on_emoticon(chunk) def on_emoticon(chunk); end
end
def on_map_change(chunk) def on_map_change(chunk)
context = Context.new(@client, chunk: chunk) context = Context.new(@client, chunk:)
if @client.hooks[:map_change] if @client.hooks[:map_change]
@client.hooks[:map_change].call(context) @client.hooks[:map_change].call(context)
context.verify context.verify
@ -121,17 +121,14 @@ class GameClient
def on_chat(chunk) def on_chat(chunk)
u = Unpacker.new(chunk.data[1..]) u = Unpacker.new(chunk.data[1..])
data = { data = {
mode: u.get_int(), mode: u.get_int,
client_id: u.get_int(), client_id: u.get_int,
target_id: u.get_int(), target_id: u.get_int,
message: u.get_string() message: u.get_string
} }
data[:author] = @players[data[:client_id]] data[:author] = @players[data[:client_id]]
msg = ChatMesage.new(data) msg = ChatMesage.new(data)
if @client.hooks[:chat] @client.hooks[:chat].call(msg) if @client.hooks[:chat]
@client.hooks[:chat].call(msg)
end
end end
end end

View file

@ -44,7 +44,7 @@ class NetBase
eight_bits.join('').to_i(2) eight_bits.join('').to_i(2)
end end
header = header + str_bytes(@server_token) header += str_bytes(@server_token)
data = (header + payload).pack('C*') data = (header + payload).pack('C*')
@s.send(data, 0, @ip, @port) @s.send(data, 0, @ip, @port)
@ -54,4 +54,3 @@ class NetBase
end end
end end
end end

View file

@ -1,6 +1,6 @@
GAME_VERSION = "0.7.5" GAME_VERSION = '0.7.5'
GAME_NETVERSION_HASH_FORCED = "802f1be60a05665f" GAME_NETVERSION_HASH_FORCED = '802f1be60a05665f'
GAME_NETVERSION = "0.7 " + GAME_NETVERSION_HASH_FORCED GAME_NETVERSION = '0.7 ' + GAME_NETVERSION_HASH_FORCED
CLIENT_VERSION = 0x0705 CLIENT_VERSION = 0x0705
NETMSG_NULL = 0 NETMSG_NULL = 0
@ -76,7 +76,7 @@ NET_CTRLMSG_ACCEPT = 2
NET_CTRLMSG_CLOSE = 4 NET_CTRLMSG_CLOSE = 4
NET_CTRLMSG_TOKEN = 5 NET_CTRLMSG_TOKEN = 5
NET_MAX_SEQUENCE = 1<<10 NET_MAX_SEQUENCE = 1 << 10
NET_CONNSTATE_OFFLINE = 0 NET_CONNSTATE_OFFLINE = 0
NET_CONNSTATE_TOKEN = 1 NET_CONNSTATE_TOKEN = 1
@ -97,4 +97,3 @@ TARGET_SERVER = -1
PACKET_HEADER_SIZE = 7 PACKET_HEADER_SIZE = 7
CHUNK_HEADER_SIZE = 3 CHUNK_HEADER_SIZE = 3

View file

@ -36,9 +36,8 @@ class Packer
num += 1 num += 1
end end
num = num.abs num = num.abs
if num > 63 || num < -63 return pack_big_int(sign, num) if num > 63 || num < -63
return self.pack_big_int(sign, num)
end
ext = '0' ext = '0'
bits = ext + sign + num.to_s(2).rjust(6, '0') bits = ext + sign + num.to_s(2).rjust(6, '0')
[bits.to_i(2)] [bits.to_i(2)]
@ -69,26 +68,26 @@ end
class Unpacker class Unpacker
def initialize(data) def initialize(data)
@data = data @data = data
if data.class == String if data.instance_of?(String)
@data = data.unpack("C*") @data = data.unpack('C*')
elsif data.class == Array elsif data.instance_of?(Array)
@data = data @data = data
else else
raise "Error: Unpacker expects array of integers or byte string" raise 'Error: Unpacker expects array of integers or byte string'
end end
end end
def get_string() def get_string
return nil if @data.nil? return nil if @data.nil?
str = '' str = ''
@data.each_with_index do |byte, index| @data.each_with_index do |byte, index|
if byte == 0x00 if byte == 0x00
if index == @data.length - 1 @data = if index == @data.length - 1
@data = nil nil
else else
@data = @data[(index + 1)..] @data[(index + 1)..]
end end
return str return str
end end
str += byte.chr str += byte.chr
@ -98,10 +97,10 @@ class Unpacker
'' ''
end end
def get_int() def get_int
return nil if @data.nil? return nil if @data.nil?
# todo: make this more performant # TODO: make this more performant
# it should not read in ALL bytes # it should not read in ALL bytes
# of the WHOLE packed data # of the WHOLE packed data
# it should be max 4 bytes # it should be max 4 bytes
@ -150,8 +149,8 @@ def todo_make_this_rspec_test
# p Packer.pack_int(-3).first.to_s(2) == '1000010' # p Packer.pack_int(-3).first.to_s(2) == '1000010'
# p Packer.pack_int(-4).first.to_s(2) == '1000011' # p Packer.pack_int(-4).first.to_s(2) == '1000011'
p Packer.pack_int(64).map { |e| e.to_s(2).rjust(8, '0') } == ["10000000", "00000001"] p Packer.pack_int(64).map { |e| e.to_s(2).rjust(8, '0') } == %w[10000000 00000001]
p Packer.pack_int(-64).map { |e| e.to_s(2).rjust(8, '0') } == ["11000000", "00000000"] p Packer.pack_int(-64).map { |e| e.to_s(2).rjust(8, '0') } == %w[11000000 00000000]
# # multi byte int # # multi byte int
# p Packer.pack_int(64) == [128, 1] # p Packer.pack_int(64) == [128, 1]
@ -184,8 +183,8 @@ def todo_also_rspec_unpacker
# p u.get_int() == 64 # p u.get_int() == 64
u = Unpacker.new([128, 1, 128, 1]) u = Unpacker.new([128, 1, 128, 1])
p u.get_int() == 64 p u.get_int == 64
p u.get_int() == 64 p u.get_int == 64
# p u.get_int() == nil # p u.get_int() == nil
# (-128..128).each do |i| # (-128..128).each do |i|
@ -194,15 +193,14 @@ def todo_also_rspec_unpacker
# end # end
u = Unpacker.new(['00000001'.to_i(2)]) u = Unpacker.new(['00000001'.to_i(2)])
p u.get_int() == 1 p u.get_int == 1
u = Unpacker.new(['10000000'.to_i(2), '00000001'.to_i(2)]) u = Unpacker.new(['10000000'.to_i(2), '00000001'.to_i(2)])
p u.get_int() == 64 p u.get_int == 64
# todo should be -64 # TODO: should be -64
# u = Unpacker.new(['11000000'.to_i(2), '00000000'.to_i(2)]) # u = Unpacker.new(['11000000'.to_i(2), '00000000'.to_i(2)])
# p u.get_int() # p u.get_int()
end end
# todo_also_rspec_unpacker # todo_also_rspec_unpacker

View file

@ -6,14 +6,14 @@ class PacketFlags
def initialize(data) def initialize(data)
@hash = {} @hash = {}
@bits = '' @bits = ''
if data.class == Hash if data.instance_of?(Hash)
@bits = parse_hash(data) @bits = parse_hash(data)
@hash = data @hash = data
elsif data.class == String elsif data.instance_of?(String)
@hash = parse_bits(data) @hash = parse_bits(data)
@bits = data @bits = data
else else
raise "Flags have to be hash or string" raise 'Flags have to be hash or string'
end end
end end
@ -56,12 +56,12 @@ class Packet
@prefix = prefix @prefix = prefix
@huffman = Huffman.new @huffman = Huffman.new
@data = data @data = data
flags_byte = @data[0].unpack("B*") flags_byte = @data[0].unpack('B*')
@flags = PacketFlags.new(flags_byte.first[2..5]).hash @flags = PacketFlags.new(flags_byte.first[2..5]).hash
@payload = @data[PACKET_HEADER_SIZE..] @payload = @data[PACKET_HEADER_SIZE..]
if flags_compressed if flags_compressed
@payload = @huffman.decompress(@payload.unpack("C*")) @payload = @huffman.decompress(@payload.unpack('C*'))
@payload = @payload.pack("C*") @payload = @payload.pack('C*')
end end
end end
@ -70,37 +70,36 @@ class Packet
token = bytes[3..6].join(' ').green token = bytes[3..6].join(' ').green
payload = bytes[7..].join(' ') payload = bytes[7..].join(' ')
puts @prefix + " data: #{[header, token, payload].join(' ')}" puts @prefix + " data: #{[header, token, payload].join(' ')}"
print @prefix + " " print @prefix + ' '
print "header".ljust(3 * 3, ' ').yellow print 'header'.ljust(3 * 3, ' ').yellow
print "token".ljust(4 * 3, ' ').green print 'token'.ljust(4 * 3, ' ').green
puts "data" puts 'data'
end end
def to_s() def to_s
puts @prefix + "Packet" puts @prefix + 'Packet'
puts @prefix + " flags: #{@flags}" puts @prefix + " flags: #{@flags}"
bytes = str_hex(@data).split(' ') bytes = str_hex(@data).split(' ')
# todo: check terminal size? # TODO: check terminal size?
max_width = 14 max_width = 14
rows = bytes.groups_of(max_width) rows = bytes.groups_of(max_width)
annotate_first_row(rows.first) annotate_first_row(rows.first)
rows[1..].each do |row| rows[1..].each do |row|
print @prefix + " " print @prefix + ' '
puts row.join(' ') puts row.join(' ')
end end
puts "" puts ''
end end
def flags_compressed() def flags_compressed
@flags[:compressed] @flags[:compressed]
end end
def flags_connless() def flags_connless
@flags[:connection] == false @flags[:connection] == false
end end
def flags_control() def flags_control
@flags[:control] @flags[:control]
end end
end end

View file

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

View file

@ -12,4 +12,3 @@ class ServerInfo
"version=#{@version} gametype=#{gametype} map=#{map} name=#{name}" "version=#{@version} gametype=#{gametype} map=#{map} name=#{name}"
end end
end end

View file

@ -22,4 +22,3 @@ class String
colorize(35) colorize(35)
end end
end end

140
lib/teeworlds-client.rb Normal file → Executable file
View file

@ -28,15 +28,15 @@ class TeeworldsClient
@signal_disconnect = false @signal_disconnect = false
@game_client = GameClient.new(self) @game_client = GameClient.new(self)
@start_info = { @start_info = {
name: "ruby gamer", name: 'ruby gamer',
clan: "", clan: '',
country: -1, country: -1,
body: "spiky", body: 'spiky',
marking: "duodonny", marking: 'duodonny',
decoration: "", decoration: '',
hands: "standard", hands: 'standard',
feet: "standard", feet: 'standard',
eyes: "standard", eyes: 'standard',
custom_color_body: 0, custom_color_body: 0,
custom_color_marking: 0, custom_color_marking: 0,
custom_color_decoration: 0, custom_color_decoration: 0,
@ -74,7 +74,7 @@ class TeeworldsClient
def send_chat(str) def send_chat(str)
@netbase.send_packet( @netbase.send_packet(
NetChunk.create_vital_header({vital: true}, 4 + str.length) + NetChunk.create_vital_header({ vital: true }, 4 + str.length) +
[ [
pack_msg_id(NETMSGTYPE_CL_SAY), pack_msg_id(NETMSGTYPE_CL_SAY),
CHAT_ALL, CHAT_ALL,
@ -86,11 +86,9 @@ class TeeworldsClient
def connect(ip, port, options = {}) def connect(ip, port, options = {})
options[:detach] = options[:detach] || false options[:detach] = options[:detach] || false
if options[:detach] if options[:detach] && @thread_running
if @thread_running puts 'Error: connection thread already running call disconnect() first'
puts "Error: connection thread already running call disconnect() first" return
return
end
end end
disconnect disconnect
@signal_disconnect = false @signal_disconnect = false
@ -123,13 +121,9 @@ class TeeworldsClient
end end
def disconnect def disconnect
puts "disconnecting." puts 'disconnecting.'
unless @netbase.nil? @netbase.send_packet([NET_CTRLMSG_CLOSE], 0, control: true) unless @netbase.nil?
@netbase.send_packet([NET_CTRLMSG_CLOSE], 0, control: true) @s.close unless @s.nil?
end
unless @s.nil?
@s.close
end
@signal_disconnect = true @signal_disconnect = true
end end
@ -148,40 +142,40 @@ class TeeworldsClient
@netbase.send_packet(data) @netbase.send_packet(data)
end end
def send_ctrl_keepalive() def send_ctrl_keepalive
@netbase.send_packet([NET_CTRLMSG_KEEPALIVE], 0, control: true) @netbase.send_packet([NET_CTRLMSG_KEEPALIVE], 0, control: true)
end end
def send_msg_connect() def send_msg_connect
msg = [NET_CTRLMSG_CONNECT] + str_bytes(@client_token) + Array.new(501, 0x00) msg = [NET_CTRLMSG_CONNECT] + str_bytes(@client_token) + Array.new(501, 0x00)
@netbase.send_packet(msg, 0, control: true) @netbase.send_packet(msg, 0, control: true)
end end
def send_ctrl_with_token() def send_ctrl_with_token
@state = NET_CONNSTATE_TOKEN @state = NET_CONNSTATE_TOKEN
msg = [NET_CTRLMSG_TOKEN] + str_bytes(@client_token) + Array.new(512, 0x00) msg = [NET_CTRLMSG_TOKEN] + str_bytes(@client_token) + Array.new(512, 0x00)
@netbase.send_packet(msg, 0, control: true) @netbase.send_packet(msg, 0, control: true)
end end
def send_info() def send_info
data = [] data = []
data += Packer.pack_str(GAME_NETVERSION) data += Packer.pack_str(GAME_NETVERSION)
data += Packer.pack_str("password") data += Packer.pack_str('password')
data += Packer.pack_int(CLIENT_VERSION) data += Packer.pack_int(CLIENT_VERSION)
msg = NetChunk.create_vital_header({vital: true}, data.size + 1) + msg = NetChunk.create_vital_header({ vital: true }, data.size + 1) +
[pack_msg_id(NETMSG_INFO, system: true)] + [pack_msg_id(NETMSG_INFO, system: true)] +
data data
@netbase.send_packet(msg, 1) @netbase.send_packet(msg, 1)
end end
def send_msg_startinfo() def send_msg_startinfo
data = [] data = []
@start_info.each do |key, value| @start_info.each do |key, value|
if value.class == String if value.instance_of?(String)
data += Packer.pack_str(value) data += Packer.pack_str(value)
elsif value.class == Integer elsif value.instance_of?(Integer)
data += Packer.pack_int(value) data += Packer.pack_int(value)
else else
puts "Error: invalid startinfo #{key}: #{value}" puts "Error: invalid startinfo #{key}: #{value}"
@ -190,22 +184,24 @@ class TeeworldsClient
end end
@netbase.send_packet( @netbase.send_packet(
NetChunk.create_vital_header({vital: true}, data.size + 1) + NetChunk.create_vital_header({ vital: true }, data.size + 1) +
[pack_msg_id(NETMSGTYPE_CL_STARTINFO, system: false)] + [pack_msg_id(NETMSGTYPE_CL_STARTINFO, system: false)] +
data data
) )
end end
def send_msg_ready() def send_msg_ready
@netbase.send_packet( @netbase.send_packet(
NetChunk.create_vital_header({vital: true}, 1) + NetChunk.create_vital_header({ vital: true }, 1) +
[pack_msg_id(NETMSG_READY, system: true)]) [pack_msg_id(NETMSG_READY, system: true)]
)
end end
def send_enter_game() def send_enter_game
@netbase.send_packet( @netbase.send_packet(
NetChunk.create_vital_header({vital: true}, 1) + NetChunk.create_vital_header({ vital: true }, 1) +
[pack_msg_id(NETMSG_ENTERGAME, system: true)]) [pack_msg_id(NETMSG_ENTERGAME, system: true)]
)
end end
## ##
@ -214,12 +210,12 @@ class TeeworldsClient
# Takes a NETMSGTYPE_CL_* integer # Takes a NETMSGTYPE_CL_* integer
# and returns a byte that can be send over # and returns a byte that can be send over
# the network # the network
def pack_msg_id(msg_id, options = {system: false}) def pack_msg_id(msg_id, options = { system: false })
(msg_id << 1) | (options[:system] ? 1 : 0) (msg_id << 1) | (options[:system] ? 1 : 0)
end end
def send_input def send_input
header = [0x10, 0x0A, 01] + str_bytes(@token) header = [0x10, 0x0A, 0o1] + str_bytes(@token)
random_compressed_input = [ random_compressed_input = [
0x4D, 0xE9, 0x48, 0x13, 0xD0, 0x0B, 0x6B, 0xFC, 0xB7, 0x2B, 0x6E, 0x00, 0xBA 0x4D, 0xE9, 0x48, 0x13, 0xD0, 0x0B, 0x6B, 0xFC, 0xB7, 0x2B, 0x6E, 0x00, 0xBA
] ]
@ -234,20 +230,20 @@ class TeeworldsClient
end end
def on_msg_token(data) def on_msg_token(data)
@token = bytes_to_str(data) @token = bytes_to_str(data)
@netbase.server_token = @token @netbase.server_token = @token
puts "Got token #{@token}" puts "Got token #{@token}"
send_msg_connect() send_msg_connect
end end
def on_msg_accept def on_msg_accept
puts "got accept. connection online" puts 'got accept. connection online'
@state = NET_CONNSTATE_ONLINE @state = NET_CONNSTATE_ONLINE
send_info send_info
end end
def on_msg_close def on_msg_close
puts "got NET_CTRLMSG_CLOSE" puts 'got NET_CTRLMSG_CLOSE'
end end
private private
@ -258,10 +254,10 @@ class TeeworldsClient
when NET_CTRLMSG_TOKEN then on_msg_token(data) when NET_CTRLMSG_TOKEN then on_msg_token(data)
when NET_CTRLMSG_ACCEPT then on_msg_accept when NET_CTRLMSG_ACCEPT then on_msg_accept
when NET_CTRLMSG_CLOSE then on_msg_close when NET_CTRLMSG_CLOSE then on_msg_close
when NET_CTRLMSG_KEEPALIVE then # silently ignore keepalive when NET_CTRLMSG_KEEPALIVE # silently ignore keepalive
else else
puts "Uknown control message #{msg}" puts "Uknown control message #{msg}"
exit(1) exit(1)
end end
end end
@ -273,14 +269,12 @@ class TeeworldsClient
when NETMSGTYPE_SV_EMOTICON then @game_client.on_emoticon(chunk) when NETMSGTYPE_SV_EMOTICON then @game_client.on_emoticon(chunk)
when NETMSGTYPE_SV_CHAT then @game_client.on_chat(chunk) when NETMSGTYPE_SV_CHAT then @game_client.on_chat(chunk)
else else
if @verbose puts "todo non sys chunks. skipped msg: #{chunk.msg}" if @verbose
puts "todo non sys chunks. skipped msg: #{chunk.msg}"
end
end end
end end
def process_chunk(chunk) def process_chunk(chunk)
if !chunk.sys unless chunk.sys
on_message(chunk) on_message(chunk)
return return
end end
@ -289,7 +283,7 @@ class TeeworldsClient
when NETMSG_MAP_CHANGE when NETMSG_MAP_CHANGE
@game_client.on_map_change(chunk) @game_client.on_map_change(chunk)
when NETMSG_SERVERINFO when NETMSG_SERVERINFO
puts "ignore server info for now" puts 'ignore server info for now'
when NETMSG_CON_READY when NETMSG_CON_READY
@game_client.on_connected @game_client.on_connected
when NETMSG_NULL when NETMSG_NULL
@ -303,7 +297,7 @@ class TeeworldsClient
def process_server_packet(packet) def process_server_packet(packet)
data = packet.payload data = packet.payload
if data.size.zero? if data.size.zero?
puts "Error: packet payload is empty" puts 'Error: packet payload is empty'
puts packet.to_s puts packet.to_s
return return
end end
@ -311,9 +305,7 @@ class TeeworldsClient
chunks.each do |chunk| chunks.each do |chunk|
if chunk.flags_vital && !chunk.flags_resend if chunk.flags_vital && !chunk.flags_resend
@netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE @netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE
if @verbose puts "got ack: #{@netbase.ack}" if @verbose
puts "got ack: #{@netbase.ack}"
end
end end
process_chunk(chunk) process_chunk(chunk)
end end
@ -327,12 +319,12 @@ class TeeworldsClient
pck = nil pck = nil
end end
if pck.nil? && @token.nil? if pck.nil? && @token.nil?
@wait_for_token = @wait_for_token || 0 @wait_for_token ||= 0
@wait_for_token += 1 @wait_for_token += 1
if @wait_for_token > 6 if @wait_for_token > 6
@token = nil @token = nil
send_ctrl_with_token send_ctrl_with_token
puts "retrying connection ..." puts 'retrying connection ...'
end end
end end
return unless pck return unless pck
@ -340,36 +332,30 @@ class TeeworldsClient
data = pck.first data = pck.first
packet = Packet.new(data, '<') packet = Packet.new(data, '<')
if @verbose puts packet.to_s if @verbose
puts packet.to_s
end
# process connless packets data # process connless packets data
if packet.flags_control if packet.flags_control
msg = data[PACKET_HEADER_SIZE].unpack("C*").first msg = data[PACKET_HEADER_SIZE].unpack1('C*')
on_ctrl_message(msg, data[(PACKET_HEADER_SIZE + 1)..]) on_ctrl_message(msg, data[(PACKET_HEADER_SIZE + 1)..])
else # process non-connless packets else # process non-connless packets
process_server_packet(packet) process_server_packet(packet)
end end
@ticks += 1 @ticks += 1
if @ticks % 8 == 0 send_ctrl_keepalive if @ticks % 8 == 0
send_ctrl_keepalive
end
# if @ticks % 20 == 0 # if @ticks % 20 == 0
# send_chat("hello world") # send_chat("hello world")
# end # end
end end
def connection_loop def connection_loop
until @signal_disconnect until @signal_disconnect
tick tick
# todo: proper tick speed sleep # TODO: proper tick speed sleep
sleep 0.001 sleep 0.001
end end
@thread_running = false @thread_running = false
@signal_disconnect = false @signal_disconnect = false
end end
end end

View file

@ -2,18 +2,18 @@
require_relative 'lib/teeworlds-client' require_relative 'lib/teeworlds-client'
args = {verbose: false, ip: nil, port: nil} args = { verbose: false, ip: nil, port: nil }
ARGV.each do |arg| ARGV.each do |arg|
if arg == '--help' || arg == '-h' if ['--help', '-h'].include?(arg)
puts "usage: teeworlds.rb [OPTIONS..] [host] [port]" puts 'usage: teeworlds.rb [OPTIONS..] [host] [port]'
echo "options:" echo 'options:'
echo " --help|-h show this help" echo ' --help|-h show this help'
echo " --verbose|-v verbose output" echo ' --verbose|-v verbose output'
echo "example:" echo 'example:'
echo " teeworlds.rb --verbose localhost 8303" echo ' teeworlds.rb --verbose localhost 8303'
exit(0) exit(0)
elsif arg == '--verbose' || arg == '-v' elsif ['--verbose', '-v'].include?(arg)
args[:verbose] = true args[:verbose] = true
elsif args[:ip].nil? elsif args[:ip].nil?
args[:ip] = arg args[:ip] = arg

View file

@ -10,4 +10,3 @@ describe 'Array', :array do
end end
end end
end end

View file

@ -1,11 +1,9 @@
require_relative '../lib/packet' require_relative '../lib/packet'
describe 'Packet', :packet do describe 'Packet', :packet do
context 'Set flag bits' do context 'Set flag bits' do
it 'Should set the control flag bit' do it 'Should set the control flag bit' do
expect(PacketFlags.new(control: true).bits).to eq("0001") expect(PacketFlags.new(control: true).bits).to eq('0001')
end end
end end
end end

View file

@ -3,7 +3,7 @@ require_relative '../lib/chunk'
describe 'NetChunk', :net_chunk do describe 'NetChunk', :net_chunk do
context 'Create vital header' do context 'Create vital header' do
it 'Should set the vital flag' do it 'Should set the vital flag' do
expect(NetChunk.create_vital_header({vital: true}, 20, 5)).to eq([64, 20, 5]) expect(NetChunk.create_vital_header({ vital: true }, 20, 5)).to eq([64, 20, 5])
end end
end end
end end
@ -15,7 +15,7 @@ describe 'BigChungusTheChunkGetter', :chunk_getter do
# one empty motd chunks # one empty motd chunks
data = [ data = [
0x40, 0x02, 0x02, 0x02, 0x00 0x40, 0x02, 0x02, 0x02, 0x00
].pack("C*") ].pack('C*')
chunks = BigChungusTheChunkGetter.get_chunks(data) chunks = BigChungusTheChunkGetter.get_chunks(data)
expect(chunks.size).to eq(1) expect(chunks.size).to eq(1)
end end
@ -28,7 +28,7 @@ describe 'BigChungusTheChunkGetter', :chunk_getter do
data = [ data = [
0x40, 0x02, 0x02, 0x02, 0x00, 0x40, 0x02, 0x02, 0x02, 0x00,
0x40, 0x02, 0x02, 0x02, 0x00 0x40, 0x02, 0x02, 0x02, 0x00
].pack("C*") ].pack('C*')
chunks = BigChungusTheChunkGetter.get_chunks(data) chunks = BigChungusTheChunkGetter.get_chunks(data)
expect(chunks.size).to eq(2) expect(chunks.size).to eq(2)
expect(chunks[0].msg).to eq(NETMSGTYPE_SV_MOTD) expect(chunks[0].msg).to eq(NETMSGTYPE_SV_MOTD)
@ -42,7 +42,7 @@ describe 'BigChungusTheChunkGetter', :chunk_getter do
0x40, 0x02, 0x02, 0x02, 0x00, # motd 0x40, 0x02, 0x02, 0x02, 0x00, # motd
0x40, 0x07, 0x03, 0x22, 0x01, 0x00, 0x01, 0x00, 0x01, 0x08, # server settings 0x40, 0x07, 0x03, 0x22, 0x01, 0x00, 0x01, 0x00, 0x01, 0x08, # server settings
0x40, 0x01, 0x04, 0x0b # ready 0x40, 0x01, 0x04, 0x0b # ready
].pack("C*") ].pack('C*')
chunks = BigChungusTheChunkGetter.get_chunks(data) chunks = BigChungusTheChunkGetter.get_chunks(data)
expect(chunks.size).to eq(3) expect(chunks.size).to eq(3)
expect(chunks[0].msg).to eq(NETMSGTYPE_SV_MOTD) expect(chunks[0].msg).to eq(NETMSGTYPE_SV_MOTD)
@ -56,11 +56,10 @@ describe 'BigChungusTheChunkGetter', :chunk_getter do
0xee, 0xcb, 0xd0, 0xd7, 0x02, 0x9c, 0x0e, 0x08, 0xa8, 0x15, 0x1a, 0xb3, 0xbb, 0xb1, 0xd4, 0x04, 0xee, 0xcb, 0xd0, 0xd7, 0x02, 0x9c, 0x0e, 0x08, 0xa8, 0x15, 0x1a, 0xb3, 0xbb, 0xb1, 0xd4, 0x04,
0x75, 0x68, 0xec, 0xe3, 0x41, 0x6e, 0x83, 0x20, 0xaf, 0x97, 0x0f, 0x49, 0xbe, 0x4f, 0x3c, 0x61, 0x75, 0x68, 0xec, 0xe3, 0x41, 0x6e, 0x83, 0x20, 0xaf, 0x97, 0x0f, 0x49, 0xbe, 0x4f, 0x3c, 0x61,
0x04, 0xf4, 0xbe, 0x60, 0xd2, 0x87, 0x39, 0x91, 0x59, 0xab 0x04, 0xf4, 0xbe, 0x60, 0xd2, 0x87, 0x39, 0x91, 0x59, 0xab
].pack("C*") ].pack('C*')
chunks = BigChungusTheChunkGetter.get_chunks(map_change) chunks = BigChungusTheChunkGetter.get_chunks(map_change)
expect(chunks.size).to eq(1) expect(chunks.size).to eq(1)
expect(chunks[0].sys).to eq(true) expect(chunks[0].sys).to eq(true)
end end
end end
end end

View file

@ -1,4 +1,3 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rake' require 'rake'