2022-10-25 17:42:20 +00:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
require 'socket'
|
|
|
|
|
2022-10-29 10:09:10 +00:00
|
|
|
require_relative 'lib/string'
|
|
|
|
require_relative 'lib/array'
|
|
|
|
require_relative 'lib/bytes'
|
2022-10-29 11:17:42 +00:00
|
|
|
require_relative 'lib/network'
|
2022-10-29 10:09:10 +00:00
|
|
|
require_relative 'lib/packet'
|
2022-10-29 14:18:07 +00:00
|
|
|
require_relative 'lib/chunk'
|
2022-10-30 09:13:18 +00:00
|
|
|
require_relative 'lib/server_info'
|
2022-10-25 17:42:20 +00:00
|
|
|
|
2022-10-30 10:18:15 +00:00
|
|
|
class NetBase
|
2022-10-30 18:00:13 +00:00
|
|
|
attr_accessor :client_token, :server_token, :ack
|
2022-10-30 10:18:15 +00:00
|
|
|
|
|
|
|
def initialize
|
|
|
|
@ip = nil
|
|
|
|
@port = nil
|
|
|
|
@s = nil
|
2022-10-30 18:00:13 +00:00
|
|
|
@ack = 0
|
2022-10-30 10:18:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def connect(socket, ip, port)
|
|
|
|
@s = socket
|
|
|
|
@ip = ip
|
|
|
|
@port = port
|
2022-10-30 18:00:13 +00:00
|
|
|
@ack = 0
|
2022-10-30 10:18:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Sends a packing setting the proper header for you
|
|
|
|
#
|
|
|
|
# @param payload [Array] The Integer list representing the data after the header
|
2022-10-30 18:00:13 +00:00
|
|
|
# @param flags [Hash] Packet header flags for more details check the class +PacketFlags+
|
2022-10-30 18:58:51 +00:00
|
|
|
def send_packet(payload, num_chunks = 1, flags = {})
|
2022-10-30 18:00:13 +00:00
|
|
|
# unsigned char flags_ack; // 6bit flags, 2bit ack
|
|
|
|
# unsigned char ack; // 8bit ack
|
|
|
|
# unsigned char numchunks; // 8bit chunks
|
|
|
|
# unsigned char token[4]; // 32bit token
|
|
|
|
# // ffffffaa
|
|
|
|
# // aaaaaaaa
|
|
|
|
# // NNNNNNNN
|
|
|
|
# // TTTTTTTT
|
|
|
|
# // TTTTTTTT
|
|
|
|
# // TTTTTTTT
|
|
|
|
# // TTTTTTTT
|
|
|
|
flags_bits = PacketFlags.new(flags).bits
|
|
|
|
header_bits =
|
2022-10-30 18:19:10 +00:00
|
|
|
'00' + # unused flags? # ff
|
|
|
|
flags_bits + # ffff
|
|
|
|
@ack.to_s(2).rjust(10, '0') + # aa aaaa aaaa
|
|
|
|
num_chunks.to_s(2).rjust(8, '0') # NNNN NNNN
|
|
|
|
|
|
|
|
header = header_bits.chars.groups_of(8).map do |eight_bits|
|
|
|
|
eight_bits.join('').to_i(2)
|
2022-10-30 18:00:13 +00:00
|
|
|
end
|
|
|
|
|
2022-10-30 18:19:10 +00:00
|
|
|
header = header + str_bytes(@server_token)
|
2022-10-30 10:18:15 +00:00
|
|
|
data = (header + payload).pack('C*')
|
|
|
|
@s.send(data, 0, @ip, @port)
|
|
|
|
|
2022-11-01 12:56:19 +00:00
|
|
|
if @verbose || flags[:test]
|
2022-10-31 07:45:43 +00:00
|
|
|
p = Packet.new(data, '>')
|
|
|
|
puts p.to_s
|
|
|
|
end
|
2022-10-30 10:18:15 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
class TwClient
|
|
|
|
attr_reader :state
|
|
|
|
|
2022-10-31 07:45:43 +00:00
|
|
|
def initialize(options = {})
|
|
|
|
@verbose = options[:verbose] || false
|
2022-10-25 17:42:20 +00:00
|
|
|
@client_token = MY_TOKEN.map { |b| b.to_s(16) }.join('')
|
|
|
|
puts "client token #{@client_token}"
|
|
|
|
@s = UDPSocket.new
|
|
|
|
@state = NET_CONNSTATE_OFFLINE
|
|
|
|
@ip = 'localhost'
|
|
|
|
@port = 8303
|
2022-10-29 10:09:10 +00:00
|
|
|
@packet_flags = {}
|
2022-10-29 15:04:35 +00:00
|
|
|
@ticks = 0
|
2022-10-30 10:18:15 +00:00
|
|
|
@netbase = NetBase.new
|
|
|
|
@netbase.client_token = @client_token
|
2022-11-01 13:11:11 +00:00
|
|
|
@hooks = {}
|
2022-10-25 17:42:20 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def send_msg(data)
|
2022-10-30 10:18:15 +00:00
|
|
|
@netbase.send_packet(data)
|
2022-10-25 17:42:20 +00:00
|
|
|
end
|
|
|
|
|
2022-10-29 15:04:35 +00:00
|
|
|
def send_ctrl_keepalive()
|
2022-10-30 19:07:08 +00:00
|
|
|
@netbase.send_packet([NET_CTRLMSG_KEEPALIVE], 0, control: true)
|
2022-10-29 15:04:35 +00:00
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
def send_msg_connect()
|
|
|
|
header = [0x04, 0x00, 0x00] + str_bytes(@token)
|
|
|
|
msg = header + [NET_CTRLMSG_CONNECT] + str_bytes(@client_token) + Array.new(501, 0x00)
|
|
|
|
@s.send(msg.pack('C*'), 0, @ip, @port)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_ctrl_with_token()
|
|
|
|
@state = NET_CONNSTATE_TOKEN
|
|
|
|
@s.send(MSG_TOKEN.pack('C*'), 0, @ip, @port)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_info()
|
|
|
|
send_msg(MSG_INFO)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_msg_startinfo()
|
|
|
|
header = [0x00, 0x04, 0x01] + str_bytes(@token)
|
|
|
|
msg = header + MSG_STARTINFO
|
|
|
|
@s.send(msg.pack('C*'), 0, @ip, @port)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_msg_ready()
|
|
|
|
header = [0x00, 0x01, 0x01] + str_bytes(@token)
|
|
|
|
msg = header + [0x40, 0x01, 0x02, 0x25]
|
|
|
|
@s.send(msg.pack('C*'), 0, @ip, @port)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_enter_game()
|
2022-11-01 09:52:48 +00:00
|
|
|
@netbase.send_packet(
|
|
|
|
NetChunk.create_vital_header({vital: true}, 1) +
|
2022-11-01 12:56:19 +00:00
|
|
|
[pack_msg_id(NETMSG_ENTERGAME, true)])
|
2022-10-25 17:42:20 +00:00
|
|
|
end
|
|
|
|
|
2022-11-01 09:37:24 +00:00
|
|
|
##
|
|
|
|
# Turns int into network byte
|
|
|
|
#
|
|
|
|
# Takes a NETMSGTYPE_CL_* integer
|
|
|
|
# and returns a byte that can be send over
|
|
|
|
# the network
|
2022-11-01 12:56:19 +00:00
|
|
|
def pack_msg_id(msg_id, system = false)
|
|
|
|
(msg_id << 1) | (system ? 1 : 0)
|
2022-11-01 09:37:24 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def send_chat(str)
|
|
|
|
@netbase.send_packet(
|
|
|
|
NetChunk.create_vital_header({vital: true}, 4 + str.length) +
|
|
|
|
[
|
|
|
|
pack_msg_id(NETMSGTYPE_CL_SAY),
|
|
|
|
CHAT_ALL,
|
|
|
|
64 # should use TARGET_SERVER (-1) instead of hacking 64 in here
|
|
|
|
] +
|
|
|
|
str.chars.map(&:ord) + [0x00])
|
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
def send_input
|
|
|
|
header = [0x10, 0x0A, 01] + str_bytes(@token)
|
|
|
|
random_compressed_input = [
|
|
|
|
0x4D, 0xE9, 0x48, 0x13, 0xD0, 0x0B, 0x6B, 0xFC, 0xB7, 0x2B, 0x6E, 0x00, 0xBA
|
|
|
|
]
|
|
|
|
# this wont work we need to ack the ticks
|
|
|
|
# and then compress it
|
|
|
|
# CMsgPacker Msg(NETMSG_INPUT, true);
|
|
|
|
# Msg.AddInt(m_AckGameTick);
|
|
|
|
# Msg.AddInt(m_PredTick);
|
|
|
|
# Msg.AddInt(Size);
|
|
|
|
msg = header + random_compressed_input
|
|
|
|
@s.send(msg.pack('C*'), 0, @ip, @port)
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_msg_token(data)
|
|
|
|
@token = bytes_to_str(data)
|
2022-10-30 10:18:15 +00:00
|
|
|
@netbase.server_token = @token
|
2022-10-25 17:42:20 +00:00
|
|
|
puts "Got token #{@token}"
|
|
|
|
send_msg_connect()
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_msg_accept
|
|
|
|
puts "got accept. connection online"
|
|
|
|
@state = NET_CONNSTATE_ONLINE
|
|
|
|
send_info
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_msg_close
|
|
|
|
puts "got NET_CTRLMSG_CLOSE"
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_strings(data)
|
|
|
|
strings = []
|
|
|
|
str = ""
|
|
|
|
data.chars.each do |b|
|
|
|
|
# use a bunch of control characters as delimiters
|
|
|
|
# https://en.wikipedia.org/wiki/Control_character
|
|
|
|
if (0x00..0x0F).to_a.include?(b.unpack('C*').first)
|
|
|
|
strings.push(str) unless str.length.zero?
|
|
|
|
str = ""
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
str += b
|
|
|
|
end
|
|
|
|
strings
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_msg_map_change(data)
|
|
|
|
mapname = get_strings(data).first
|
|
|
|
puts "map: #{mapname}"
|
|
|
|
send_msg_ready()
|
|
|
|
end
|
|
|
|
|
|
|
|
def connect(ip, port)
|
|
|
|
@ip = ip
|
|
|
|
@port = port
|
|
|
|
puts "connecting to #{@ip}:#{@port} .."
|
|
|
|
@s.connect(ip, port)
|
2022-10-30 10:18:15 +00:00
|
|
|
@netbase.connect(@s, @ip, @port)
|
2022-10-25 17:42:20 +00:00
|
|
|
send_ctrl_with_token
|
|
|
|
loop do
|
|
|
|
tick
|
2022-10-30 18:19:10 +00:00
|
|
|
# todo: proper tick speed sleep
|
|
|
|
sleep 0.001
|
2022-10-25 17:42:20 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_motd(data)
|
|
|
|
puts "motd: #{get_strings(data)}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_playerinfo(data)
|
|
|
|
puts "playerinfo: #{get_strings(data).join(', ')}"
|
|
|
|
end
|
|
|
|
|
2022-10-29 10:09:10 +00:00
|
|
|
# CClient::ProcessConnlessPacket
|
|
|
|
def on_ctrl_message(msg, data)
|
|
|
|
case msg
|
|
|
|
when NET_CTRLMSG_TOKEN then on_msg_token(data)
|
|
|
|
when NET_CTRLMSG_ACCEPT then on_msg_accept
|
|
|
|
when NET_CTRLMSG_CLOSE then on_msg_close
|
2022-10-30 18:19:10 +00:00
|
|
|
when NET_CTRLMSG_KEEPALIVE then # silently ignore keepalive
|
2022-10-29 10:09:10 +00:00
|
|
|
else
|
|
|
|
puts "Uknown control message #{msg}"
|
|
|
|
exit(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-31 07:22:21 +00:00
|
|
|
def on_player_join(chunk)
|
|
|
|
puts "Got playerinfo flags: #{chunk.flags}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def on_emoticon(chunk)
|
2022-10-31 07:54:13 +00:00
|
|
|
# puts "Got emoticon flags: #{chunk.flags}"
|
2022-10-31 07:22:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def on_chat(chunk)
|
2022-10-31 07:35:13 +00:00
|
|
|
# 06 01 00 40 41 00
|
|
|
|
# msg mode cl_id trgt A nullbyte?
|
|
|
|
# all -1
|
|
|
|
mode = chunk.data[1]
|
|
|
|
client_id = chunk.data[2]
|
|
|
|
target = chunk.data[3]
|
|
|
|
msg = chunk.data[4..]
|
|
|
|
|
2022-11-01 13:11:11 +00:00
|
|
|
if @hooks[:chat]
|
|
|
|
@hooks[:chat].call(msg)
|
|
|
|
end
|
2022-10-31 07:22:21 +00:00
|
|
|
end
|
|
|
|
|
2022-10-30 18:44:49 +00:00
|
|
|
def on_message(chunk)
|
|
|
|
case chunk.msg
|
|
|
|
when NETMSGTYPE_SV_READYTOENTER then send_enter_game
|
2022-10-31 07:22:21 +00:00
|
|
|
when NETMSGTYPE_SV_CLIENTINFO then on_player_join(chunk)
|
|
|
|
when NETMSGTYPE_SV_EMOTICON then on_emoticon(chunk)
|
|
|
|
when NETMSGTYPE_SV_CHAT then on_chat(chunk)
|
2022-10-30 18:44:49 +00:00
|
|
|
else
|
2022-10-31 07:45:43 +00:00
|
|
|
if @verbose
|
|
|
|
puts "todo non sys chunks. skipped msg: #{chunk.msg}"
|
|
|
|
end
|
2022-10-30 18:44:49 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-29 14:18:07 +00:00
|
|
|
def process_chunk(chunk)
|
|
|
|
if !chunk.sys
|
2022-10-30 18:44:49 +00:00
|
|
|
on_message(chunk)
|
2022-10-29 14:18:07 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
puts "proccess chunk with msg: #{chunk.msg}"
|
|
|
|
case chunk.msg
|
|
|
|
when NETMSG_MAP_CHANGE
|
|
|
|
send_msg_ready
|
2022-10-30 18:44:49 +00:00
|
|
|
when NETMSG_SERVERINFO
|
|
|
|
puts "ignore server info for now"
|
2022-10-29 14:18:07 +00:00
|
|
|
when NETMSG_CON_READY
|
|
|
|
send_msg_startinfo
|
2022-10-31 07:54:13 +00:00
|
|
|
when NETMSG_NULL
|
|
|
|
# should we be in alert here?
|
2022-10-29 14:18:07 +00:00
|
|
|
else
|
|
|
|
puts "Unsupported system msg: #{chunk.msg}"
|
|
|
|
exit(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-29 10:09:10 +00:00
|
|
|
def process_server_packet(data)
|
2022-10-29 14:18:07 +00:00
|
|
|
chunks = BigChungusTheChunkGetter.get_chunks(data)
|
|
|
|
chunks.each do |chunk|
|
2022-10-31 07:22:21 +00:00
|
|
|
if chunk.flags_vital && !chunk.flags_resend
|
2022-10-30 18:00:13 +00:00
|
|
|
@netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE
|
2022-10-31 07:45:43 +00:00
|
|
|
if @verbose
|
|
|
|
puts "got ack: #{@netbase.ack}"
|
|
|
|
end
|
2022-10-30 18:00:13 +00:00
|
|
|
end
|
2022-10-29 14:18:07 +00:00
|
|
|
process_chunk(chunk)
|
|
|
|
end
|
2022-10-29 10:09:10 +00:00
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
def tick
|
|
|
|
# puts "tick"
|
|
|
|
begin
|
|
|
|
pck = @s.recvfrom_nonblock(1400)
|
|
|
|
rescue
|
|
|
|
pck = nil
|
|
|
|
end
|
|
|
|
return unless pck
|
|
|
|
|
|
|
|
data = pck.first
|
2022-10-29 10:09:10 +00:00
|
|
|
|
2022-10-30 10:18:15 +00:00
|
|
|
packet = Packet.new(data, '<')
|
2022-10-31 07:45:43 +00:00
|
|
|
if @verbose
|
|
|
|
puts packet.to_s
|
|
|
|
end
|
2022-10-29 10:09:10 +00:00
|
|
|
|
|
|
|
# process connless packets data
|
2022-10-29 10:16:44 +00:00
|
|
|
if packet.flags_control
|
2022-10-29 11:17:42 +00:00
|
|
|
msg = data[PACKET_HEADER_SIZE].unpack("C*").first
|
|
|
|
on_ctrl_message(msg, data[(PACKET_HEADER_SIZE + 1)..])
|
2022-10-29 10:16:44 +00:00
|
|
|
else # process non-connless packets
|
2022-10-29 11:17:42 +00:00
|
|
|
process_server_packet(packet.payload)
|
2022-10-29 10:09:10 +00:00
|
|
|
end
|
2022-10-29 15:04:35 +00:00
|
|
|
|
|
|
|
@ticks += 1
|
2022-10-30 18:44:49 +00:00
|
|
|
if @ticks % 8 == 0
|
2022-10-29 15:04:35 +00:00
|
|
|
send_ctrl_keepalive
|
|
|
|
end
|
2022-11-01 09:52:48 +00:00
|
|
|
if @ticks % 20 == 0
|
|
|
|
send_chat("hello world")
|
|
|
|
end
|
2022-10-25 17:42:20 +00:00
|
|
|
end
|
|
|
|
|
2022-11-01 13:11:11 +00:00
|
|
|
def hook_chat(&block)
|
|
|
|
@hooks[:chat] = block
|
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
def disconnect
|
|
|
|
@s.close
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-31 07:45:43 +00:00
|
|
|
verbose = false
|
|
|
|
|
|
|
|
ARGV.reverse_each do |arg|
|
|
|
|
if arg == '--help' || arg == '-h'
|
|
|
|
puts "usage: teeworlds.rb [OPTIONS] [host] [port]"
|
|
|
|
echo "options:"
|
|
|
|
echo " --help|-h show this help"
|
|
|
|
echo " --verbose|-v verbose output"
|
|
|
|
exit(0)
|
|
|
|
elsif arg == '--verbose' || arg == '-v'
|
|
|
|
verbose = true
|
|
|
|
ARGV.pop
|
|
|
|
end
|
|
|
|
end
|
2022-10-25 17:42:20 +00:00
|
|
|
|
2022-10-31 07:45:43 +00:00
|
|
|
client = TwClient.new(verbose: verbose)
|
2022-11-01 13:11:11 +00:00
|
|
|
|
|
|
|
client.hook_chat do |msg|
|
|
|
|
puts "chat: #{msg}"
|
|
|
|
end
|
|
|
|
|
2022-10-25 17:42:20 +00:00
|
|
|
client.connect(ARGV[0] || "localhost", ARGV[1] ? ARGV[1].to_i : 8303)
|
|
|
|
|