teeworlds_network/lib/game_client.rb

355 lines
8.9 KiB
Ruby

# frozen_string_literal: true
require_relative 'models/player'
require_relative 'models/chat_message'
require_relative 'messages/input_timing'
require_relative 'messages/sv_client_drop'
require_relative 'messages/rcon_cmd_add'
require_relative 'messages/rcon_cmd_rem'
require_relative 'messages/maplist_entry_add'
require_relative 'messages/maplist_entry_rem'
require_relative 'packer'
require_relative 'context'
class GameClient
attr_accessor :players, :pred_game_tick, :ack_game_tick
def initialize(client)
@client = client
@players = {}
@ack_game_tick = -1
@pred_game_tick = 0
end
##
# call_hook
#
# @param: hook_sym [Symbol] name of the symbol to call
# @param: context [Context] context object to pass on data
# @param: optional [Any] optional 2nd parameter passed to the callback
def call_hook(hook_sym, context, optional = nil)
@client.hooks[hook_sym].each do |hook|
hook.call(context, optional)
context.verify
return nil if context.canceld?
end
context
end
def on_auth_on
return if call_hook(:auth_on, Context.new(nil)).nil?
@client.rcon_authed = true
puts 'rcon logged in'
end
def on_auth_off
return if call_hook(:auth_off, Context.new(nil)).nil?
@client.rcon_authed = false
puts 'rcon logged out'
end
def on_rcon_cmd_add(chunk)
todo_rename_this = RconCmdAdd.new(chunk.data[1..])
context = Context.new(todo_rename_this)
return if call_hook(:rcon_cmd_add, context).nil?
p context.todo_rename_this
end
def on_rcon_cmd_rem(chunk)
todo_rename_this = RconCmdRem.new(chunk.data[1..])
context = Context.new(todo_rename_this)
return if call_hook(:rcon_cmd_rem, context).nil?
p context.todo_rename_this
end
def on_maplist_entry_add(chunk)
todo_rename_this = MaplistEntryAdd.new(chunk.data[1..])
context = Context.new(todo_rename_this)
return if call_hook(:maplist_entry_add, context).nil?
p context.todo_rename_this
end
def on_maplist_entry_rem(chunk)
todo_rename_this = MaplistEntryRem.new(chunk.data[1..])
context = Context.new(todo_rename_this)
return if call_hook(:maplist_entry_rem, context).nil?
p context.todo_rename_this
end
def on_client_info(chunk)
# puts "Got playerinfo flags: #{chunk.flags}"
u = Unpacker.new(chunk.data[1..])
player = Player.new(
id: u.get_int,
local: u.get_int,
team: u.get_int,
name: u.get_string,
clan: u.get_string,
country: u.get_int
)
# skinparts and the silent flag
# are currently ignored
context = Context.new(
nil,
player:,
chunk:
)
return if call_hook(:client_info, context).nil?
player = context.data[:player]
@players[player.id] = player
end
def on_input_timing(chunk)
todo_rename_this = InputTiming.new(chunk.data[1..])
context = Context.new(todo_rename_this, chunk:)
call_hook(:input_timing, context)
end
def on_client_drop(chunk)
todo_rename_this = SvClientDrop.new(chunk.data[1..])
context = Context.new(
nil,
player: @players[todo_rename_this.client_id],
chunk:,
client_id: todo_rename_this.client_id,
reason: todo_rename_this.reason,
silent: todo_rename_this.silent?
)
return if call_hook(:client_drop, context).nil?
@players.delete(context.data[:client_id])
end
def on_ready_to_enter(_chunk)
@client.send_enter_game
end
def on_connected
context = Context.new(nil)
return if call_hook(:connected, context).nil?
@client.send_msg_start_info
end
def on_disconnect
call_hook(:disconnect, Context.new(nil))
end
def on_rcon_line(chunk)
u = Unpacker.new(chunk.data[1..])
context = Context.new(
nil,
line: u.get_string
)
call_hook(:rcon_line, context)
end
def on_snapshot(chunk)
u = Unpacker.new(chunk.data)
msg_id = u.get_int
msg_id >>= 1
num_parts = 1
part = 0
game_tick = u.get_int
delta_tick = u.get_int
part_size = 0
crc = 0
# complete_size = 0
# data = nil
chunk_num = 0
# TODO: state check
if msg_id == NETMSG_SNAP
num_parts = u.get_int
part = u.get_int
end
unless msg_id == NETMSG_SNAPEMPTY
crc = u.get_int
part_size = u.get_int
chunk_num = u.get_int
end
snap_name = 'SNAP_INVALID'
case msg_id
when NETMSG_SNAP then snap_name = 'NETMSG_SNAP'
when NETMSG_SNAPSINGLE then snap_name = 'NETMSG_SNAPSINGLE'
when NETMSG_SNAPEMPTY then snap_name = 'NETMSG_SNAPEMPTY'
end
return unless msg_id == NETMSG_SNAPSINGLE
puts ">>> snap #{snap_name} (#{msg_id})"
puts " id=#{msg_id} game_tick=#{game_tick} delta_tick=#{delta_tick}"
puts " num_parts=#{num_parts} part=#{part} crc=#{crc} part_size=#{part_size}"
puts " chunk_num=#{chunk_num}"
puts "\n header:"
header = []
notes = []
u.parsed.each_with_index do |parsed, index|
color = (index % 2).zero? ? :green : :pink
txt = "#{parsed[:type]} #{parsed[:value]}"
txt += " >> 1 = #{parsed[:value] >> 1}" if header.empty?
notes.push([color, parsed[:pos], parsed[:len], txt])
header += parsed[:raw]
end
hexdump_lines(header.pack('C*'), 1, notes, legend: :inline).each do |hex|
puts " #{hex}"
end
puts "\n payload:"
data = u.get_raw
# [:green, 0, 4, 'who dis?']
notes = []
# data.groups_of(4).each_with_index do |item, index|
# # reverse for little endian
# type = item[0...2].reverse.map { |b| b.to_s(2).rjust(8, '0') }.join.to_i(2)
# notes.push([:green, index * 4, 2, "type=#{type}"])
# next unless item.length == 4
# # reverse for little endian
# id = item[2...4].reverse.map { |b| b.to_s(2).rjust(8, '0') }.join.to_i(2)
# notes.push([:yellow, index * 4 + 2, 2, "id=#{id}"])
# end
@sizes = [
0,
10,
6,
5,
3,
3,
3,
2,
4,
15,
22,
3,
4,
58,
5,
32,
2,
2,
2,
2,
3,
3,
5
]
@snap_items = [
{ name: 'placeholder', size: 0 },
{ name: 'obj_player_input', size: 10 },
{ name: 'obj_projectile', size: 6 },
{ name: 'obj_laser', size: 5 },
{ name: 'obj_pickup', size: 3 },
{ name: 'obj_flag', size: 3 },
{ name: 'obj_game_data', size: 3 },
{ name: 'obj_game_data_team', size: 2 },
{ name: 'obj_game_data_flag', size: 4 },
{ name: 'obj_character_core', size: 15 },
{ name: 'obj_character', size: 22 },
{ name: 'obj_player_info', size: 3 },
{ name: 'obj_spectator_info', size: 4 },
{ name: 'obj_client_info', size: 58 },
{ name: 'obj_game_info', size: 5 },
{ name: 'obj_tune_params', size: 32 },
{ name: 'event_common', size: 2 },
{ name: 'event_explosion', size: 2 },
{ name: 'event_spawn', size: 2 },
{ name: 'event_hammerhit', size: 2 },
{ name: 'event_death', size: 3 },
{ name: 'event_sound_world', size: 3 },
{ name: 'event_damage', size: 5 }
]
skip = 0
(0...data.size).each do |i|
skip -= 1
next unless skip.negative?
# reverse for little endian
id = data[i...(i + 2)].reverse.map { |b| b.to_s(2).rjust(8, '0') }.join.to_i(2)
next if data[i + 4].nil? && i > 2
type = data[(i + 2)...(i + 4)].reverse.map { |b| b.to_s(2).rjust(8, '0') }.join.to_i(2)
size = @sizes[type]
# p "id=#{id} type=#{type}"
next if size.nil? && i > 2
size *= 4
notes.push([:green, i, 2, "id=#{id}"])
notes.push([:pink, i + 2, 2, "type=#{type} (#{@snap_items[type][:name]} size: #{size})"])
item_payload = data[(i + 4)..]
u = Unpacker.new(item_payload)
(0...(size / 4)).each do |d|
val = u.get_int
notes.push([:yellow, i + 4 + (d * 4), 4, "data[#{d}]=#{val}"])
end
skip += 3 + size
# next
# next unless item.length == 4
# # reverse for little endian
# id = item[2...4].reverse.map { |b| b.to_s(2).rjust(8, '0') }.join.to_i(2)
# notes.push([:yellow, (index * 4) + 2, 2, "id=#{id}"])
end
hexdump_lines(data.pack('C*'), 1, notes, legend: :inline).each do |hex|
puts " #{hex}"
end
# ack every snapshot no matter how broken
@ack_game_tick = game_tick
return unless (@pred_game_tick - @ack_game_tick).abs > 10
@pred_game_tick = @ack_game_tick + 1
# exit
end
def on_emoticon(chunk); end
def on_map_change(chunk)
context = Context.new(nil, chunk:)
return if call_hook(:map_change, context).nil?
# ignore mapdownload at all times
# and claim to have the map
@client.send_msg_ready
end
def on_chat(chunk)
u = Unpacker.new(chunk.data[1..])
data = {
mode: u.get_int,
client_id: u.get_int,
target_id: u.get_int,
message: u.get_string
}
data[:author] = @players[data[:client_id]]
msg = ChatMesage.new(data)
context = Context.new(nil, chunk:)
call_hook(:chat, context, msg)
end
end