Compare commits

...

10 commits

Author SHA1 Message Date
ChillerDragon 10ca8238a8 rubocop fixes
Some checks failed
Runtime tests / syntax (push) Has been cancelled
Runtime tests / unit-tests (push) Has been cancelled
Runtime tests / hooks (push) Has been cancelled
Runtime tests / doc-index (push) Has been cancelled
Integration tests / connect-to-server (push) Has been cancelled
Integration tests / srv-chat (push) Has been cancelled
Static analysis / check (push) Has been cancelled
Style / lint (push) Has been cancelled
2024-06-24 11:45:45 +08:00
ChillerDragon af7367be0c Drop known chunks. Fixes duplicated chat messages. 2024-06-24 11:43:10 +08:00
ChillerDragon deb679d1d0 Fix chat with cli arguments 2024-06-22 12:21:48 +08:00
ChillerDragon 1f46adca46 Close sample client instantly on ctrl+c 2024-06-19 08:46:02 +08:00
ChillerDragon ba7bfc6c07 Add interactive chat to sample client 2024-06-18 12:07:24 +08:00
ChillerDragon 7e45bbe038 Let the server resolve the map path 2024-02-23 12:22:27 +08:00
ChillerDragon aa5b755204 Send the connected client its own client info 2024-02-21 20:22:28 +08:00
ChillerDragon f44a370dc0 Progress on server side snaps
No errors in client log but still in connecting screen
2024-02-21 19:41:53 +08:00
ChillerDragon 420c6deb92 Add tests for snapshot builder payload 2024-02-21 18:46:52 +08:00
ChillerDragon cacb75ac62 Add type to snap items 2024-02-21 14:44:49 +08:00
32 changed files with 427 additions and 93 deletions

View file

@ -78,7 +78,13 @@ end
Signal.trap('INT') do Signal.trap('INT') do
client.disconnect client.disconnect
exit
end end
# connect and detach thread # connect and detach thread
client.connect(args[:ip], args[:port], detach: false) client.connect(args[:ip], args[:port], detach: true)
loop do
msg = $stdin.gets.chomp
client.send_chat(msg)
end

View file

@ -13,13 +13,14 @@ require_relative 'bytes'
# #
# https://chillerdragon.github.io/teeworlds-protocol/07/packet_layout.html # https://chillerdragon.github.io/teeworlds-protocol/07/packet_layout.html
class NetChunk class NetChunk
attr_reader :next, :data, :msg, :sys, :flags, :header_raw, :full_raw attr_reader :next, :data, :msg, :sys, :flags, :seq, :header_raw, :full_raw
@@sent_vital_chunks = 0 @@sent_vital_chunks = 0
def initialize(data) def initialize(data)
@next = nil @next = nil
@flags = {} @flags = {}
@seq = 0
@size = 0 @size = 0
parse_header(data[0..2]) parse_header(data[0..2])
header_size = if flags_vital header_size = if flags_vital
@ -135,8 +136,12 @@ class NetChunk
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 if @flags[:vital]
# in da third byte but who needs seq?! data = data[0..2].bytes
@seq = (data[1] & (0xC0 << 2)) | data[2]
else
@seq = 0
end
end end
# @return [Boolean] # @return [Boolean]

View file

@ -3,11 +3,12 @@
class Config class Config
def initialize(options = {}) def initialize(options = {})
filepath = options[:file] || 'autoexec.cfg' filepath = options[:file] || 'autoexec.cfg'
@type = options[:type] || :client
init_configs init_configs
load_cfg(filepath) load_cfg(filepath)
end end
def init_configs def init_client_configs
@configs = { @configs = {
password: { help: 'Password to the server', default: '' } password: { help: 'Password to the server', default: '' }
} }
@ -15,6 +16,23 @@ class Config
echo: { help: 'Echo the text', callback: proc { |arg| puts arg } }, echo: { help: 'Echo the text', callback: proc { |arg| puts arg } },
quit: { help: 'Quit', callback: proc { |_| exit } } quit: { help: 'Quit', callback: proc { |_| exit } }
} }
end
def init_server_configs
@configs = {
sv_map: { help: 'map', default: 'dm1' }
}
@commands = {
shutdown: { help: 'shutdown server', callback: proc { |_| exit } }
}
end
def init_configs
if @type == :client
init_client_configs
else
init_server_configs
end
@configs.each do |cfg, data| @configs.each do |cfg, data|
self.class.send(:attr_accessor, cfg) self.class.send(:attr_accessor, cfg)
instance_variable_set("@#{cfg}", data[:default]) instance_variable_set("@#{cfg}", data[:default])

20
lib/connection.rb Normal file
View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
##
# Only used for chunks where the sequence number does not match the expected value
# to decide wether to drop known chunks silently or request resend if something got lost
#
# true - if the sequence number is already known and the chunk should be dropped
# false - if the sequence number is off and we need to request a resend of lost chunks
#
# @return [Boolean]
def seq_in_backroom?(seq, ack)
bottom = ack - (NET_MAX_SEQUENCE / 2)
if bottom.negative?
return true if seq <= ack
return true if seq >= (bottom + NET_MAX_SEQUENCE)
elsif seq <= ack && seq >= bottom
return true
end
false
end

View file

@ -11,12 +11,15 @@ require_relative 'messages/cl_say'
require_relative 'messages/cl_emoticon' require_relative 'messages/cl_emoticon'
require_relative 'messages/cl_info' require_relative 'messages/cl_info'
require_relative 'messages/cl_input' require_relative 'messages/cl_input'
require_relative 'messages/client_info'
class GameServer class GameServer
attr_accessor :pred_game_tick, :ack_game_tick, :map attr_accessor :pred_game_tick, :ack_game_tick, :map
def initialize(server) def initialize(server)
@server = server @server = server
@config = server.config
@map_path = nil
@ack_game_tick = -1 @ack_game_tick = -1
@pred_game_tick = 0 @pred_game_tick = 0
@map = Map.new( @map = Map.new(
@ -27,6 +30,29 @@ class GameServer
) )
end end
def load_map
puts "loading map '#{@config.sv_map}' ..."
map_path = nil
if File.exist? "data/#{@config.sv_map}.map"
map_path = "data/#{@config.sv_map}.map"
elsif File.exist? "data/maps/#{@config.sv_map}.map"
map_path = "data/maps/#{@config.sv_map}.map"
elsif File.exist? "maps/#{@config.sv_map}.map"
map_path = "maps/#{@config.sv_map}.map"
elsif File.exist? "#{Dir.home}/.teeworlds/maps/#{@config.sv_map}.map"
map_path = "#{Dir.home}/.teeworlds/maps/#{@config.sv_map}.map"
end
if map_path.nil?
puts "map not found '#{@config.sv_map}'"
# TODO: this should error when the feature is done
# exit 1
else
puts "found at #{map_path}"
@map_path = map_path
end
end
## ##
# call_hook # call_hook
# #
@ -97,6 +123,16 @@ class GameServer
puts msg puts msg
end end
# https://chillerdragon.github.io/teeworlds-protocol/07/game_messages.html#NETMSGTYPE_SV_CLIENTINFO
# send infos about currently connected clients to the newly joined client
# send info of the newly joined client to all currently connected clients
#
# @param client [Client] newly joined client
def send_client_infos(client)
client_info = ClientInfo.new(client_id: client.id, local: 1)
@server.send_client_info(client, client_info)
end
def on_enter_game(_chunk, packet) def on_enter_game(_chunk, packet)
# vanilla server responds to enter game with two packets # vanilla server responds to enter game with two packets
# first: # first:
@ -109,6 +145,7 @@ class GameServer
packet.client.in_game = true packet.client.in_game = true
@server.send_server_info(packet.client, ServerInfo.new.to_a) @server.send_server_info(packet.client, ServerInfo.new.to_a)
send_client_infos(packet.client)
@server.send_game_info(packet.client, GameInfo.new.to_a) @server.send_game_info(packet.client, GameInfo.new.to_a)
puts "'#{packet.client.player.name}' joined the game" puts "'#{packet.client.player.name}' joined the game"

View file

@ -55,10 +55,9 @@ class Packer
first = "1#{sign}#{num_bits[-6..]}" first = "1#{sign}#{num_bits[-6..]}"
num_bits = num_bits[0..-7] num_bits = num_bits[0..-7]
bytes = [] bytes = num_bits.chars.groups_of(7).map do |seven_bits|
num_bits.chars.groups_of(7).each do |seven_bits|
# mark all as extended # mark all as extended
bytes << "1#{seven_bits.join.rjust(7, '0')}" "1#{seven_bits.join.rjust(7, '0')}"
end end
# least significant first # least significant first
bytes = bytes.reverse bytes = bytes.reverse

View file

@ -2,37 +2,11 @@
require_relative 'snapshot' require_relative 'snapshot'
# should be merged with SnapItemBase
class SnapItem
# @param type [Integer] type of the item for example 5 is obj_flag
# @param id [Integer] id of said item for characters thats the ClientID
# @param fields [Array] array of uncompressed integers
# for example [0, 0, 1] for obj_flag
# would set
# m_X = 0
# m_Y = 0
# m_Team = 1
def initialize(type, id, size, fields)
@type = type
@id = id
@size = size
@fields = fields
end
# basically to_network
# tee int array that will be sent over
# the wire
def to_a
Packer.pack_int(@type) +
Packer.pack_int(@id) +
fields.map { |field| Packer.pack_int(field) }
end
end
class SnapshotBuilder class SnapshotBuilder
def initialize def initialize
@data_size = 0 @data_size = 0
@num_items = 0 @num_items = 0
# @type items [Array<SnapItemBase>]
@items = [] @items = []
end end
@ -41,21 +15,16 @@ class SnapshotBuilder
# #
# https://chillerdragon.github.io/teeworlds-protocol/07/snap_items.html # https://chillerdragon.github.io/teeworlds-protocol/07/snap_items.html
# #
# @param type [Integer] type of the item for example 5 is obj_flag # @param id [Integer] Id of the snap item. For characters that is the ClientID.
# @param id [Integer] id of said item for characters thats the ClientID # Not to be confused with the type
# @param fields [Array] array of uncompressed integers # @param item [SnapItemBase] Snap item instance. Holding type and payload.
# for example [0, 0, 1] for obj_flag def new_item(id, item)
# would set item.id = id
# m_X = 0
# m_Y = 0
# m_Team = 1
def new_item(type, id, size, fields)
item = SnapItem.new(type, id, size, fields)
@items.push(item) @items.push(item)
end end
# @return [Snapshot] # @return [Snapshot]
def finish def finish
Snapshot.new Snapshot.new(@items)
end end
end end

View file

@ -7,6 +7,7 @@ class NetEvent
attr_accessor :client_id, :angle, :health_ammount, :armor_amount, :self attr_accessor :client_id, :angle, :health_ammount, :armor_amount, :self
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_DAMAGE
@field_names = %i[ @field_names = %i[
client_id client_id
angle angle

View file

@ -7,6 +7,7 @@ class NetEvent
attr_accessor :client_id attr_accessor :client_id
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_DEATH
@field_names = %i[ @field_names = %i[
client_id client_id
] ]

View file

@ -5,6 +5,7 @@ require_relative '../snap_item_base'
class NetEvent class NetEvent
class Explosion < SnapEventBase class Explosion < SnapEventBase
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_EXPLOSION
@field_names = [] @field_names = []
super super
end end

View file

@ -5,6 +5,7 @@ require_relative '../snap_item_base'
class NetEvent class NetEvent
class HammerHit < SnapEventBase class HammerHit < SnapEventBase
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_HAMMERHIT
@field_names = [] @field_names = []
super super
end end

View file

@ -7,6 +7,7 @@ class NetEvent
attr_accessor :sound_id attr_accessor :sound_id
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_SOUNDWORLD
@field_names = %i[ @field_names = %i[
sound_id sound_id
] ]

View file

@ -5,6 +5,7 @@ require_relative '../snap_item_base'
class NetEvent class NetEvent
class Spawn < SnapEventBase class Spawn < SnapEventBase
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETEVENTTYPE_SPAWN
@field_names = [] @field_names = []
super super
end end

View file

@ -9,6 +9,7 @@ class NetObj
:health, :armor, :ammo_count, :weapon, :emote, :attack_tick, :triggered_events :health, :armor, :ammo_count, :weapon, :emote, :attack_tick, :triggered_events
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_CHARACTER
@field_names = %i[ @field_names = %i[
tick tick
x x

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :local, :team attr_accessor :local, :team
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_DE_CLIENTINFO
@field_names = %i[ @field_names = %i[
local local
team team

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :x, :y, :team attr_accessor :x, :y, :team
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_FLAG
@field_names = %i[ @field_names = %i[
x x
y y

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :game_start_tick, :game_state_flags, :game_state_end_tick attr_accessor :game_start_tick, :game_state_flags, :game_state_end_tick
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_GAMEDATA
@field_names = %i[ @field_names = %i[
game_start_tick game_start_tick
game_state_flags game_state_flags

View file

@ -8,6 +8,7 @@ class NetObj
:flag_drop_tick_red, :flag_drop_tick_blue :flag_drop_tick_red, :flag_drop_tick_blue
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_GAMEDATAFLAG
@field_names = %i[ @field_names = %i[
flag_carrier_red flag_carrier_red
flag_carrier_blue flag_carrier_blue

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :teamscore_red, :teamscore_blue attr_accessor :teamscore_red, :teamscore_blue
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_GAMEDATATEAM
@field_names = %i[ @field_names = %i[
teamscore_red teamscore_red
teamscore_blue teamscore_blue

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :x, :y, :from_x, :from_y, :start_tick attr_accessor :x, :y, :from_x, :from_y, :start_tick
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_LASER
@field_names = %i[ @field_names = %i[
x x
y y

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :x, :y, :type attr_accessor :x, :y, :type
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_PICKUP
@field_names = %i[ @field_names = %i[
x x
y y

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :player_flags, :score, :latency attr_accessor :player_flags, :score, :latency
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_PLAYERINFO
@field_names = %i[ @field_names = %i[
player_flags player_flags
score score

View file

@ -10,6 +10,7 @@ class NetObj
:next_weapon, :prev_weapon :next_weapon, :prev_weapon
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_PLAYERINPUT
@field_names = %i[ @field_names = %i[
direction direction
target_x target_x

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :x, :y, :vel_x, :vel_y, :type, :start_tick attr_accessor :x, :y, :vel_x, :vel_y, :type, :start_tick
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_PROJECTILE
@field_names = %i[ @field_names = %i[
x x
y y

View file

@ -7,6 +7,7 @@ class NetObj
attr_accessor :spec_mode, :spectator_id, :x, :y attr_accessor :spec_mode, :spectator_id, :x, :y
def initialize(hash_or_raw) def initialize(hash_or_raw)
@type = NETOBJTYPE_SPECTATORINFO
@field_names = %i[ @field_names = %i[
spec_mode spec_mode
spectator_id spectator_id

View file

@ -59,7 +59,7 @@ class NetObj
end end
def init_hash(attr) def init_hash(attr)
@fields_names.each do |name| @field_names.each do |name|
instance_variable_set("@#{name}", attr[name] || 0) instance_variable_set("@#{name}", attr[name] || 0)
end end
end end

View file

@ -1,9 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../packer' require_relative '../packer'
require_relative '../network'
class SnapItemBase class SnapItemBase
attr_reader :notes, :name, :id attr_reader :notes, :name, :type
attr_accessor :id
def initialize(hash_or_raw) def initialize(hash_or_raw)
@fields = @field_names.map do |_| @fields = @field_names.map do |_|
@ -25,6 +27,10 @@ class SnapItemBase
@fields.none?(&:nil?) @fields.none?(&:nil?)
end end
def size
@fields.size
end
def init_unpacker(u) def init_unpacker(u)
@id = u.get_int @id = u.get_int
p = u.parsed.last p = u.parsed.last
@ -56,8 +62,12 @@ class SnapItemBase
end end
def init_hash(attr) def init_hash(attr)
@fields_names.each do |name| @field_names.each_with_index do |name, i|
instance_variable_set("@#{name}", attr[name] || 0) # direct instance variables work
# but using the @fields array is easier to then pack
# idk how to iterate just the instance variables that i need on packing
# instance_variable_set("@#{name}", attr[name] || 0)
@fields[i] = attr[name] || 0
end end
end end
@ -74,6 +84,8 @@ class SnapItemBase
# int array the server sends to the client # int array the server sends to the client
def to_a def to_a
arr = [] arr = []
arr += Packer.pack_int(@type)
arr += Packer.pack_int(@id)
@fields.each do |value| @fields.each do |value|
arr += Packer.pack_int(value) arr += Packer.pack_int(value)
end end

View file

@ -7,7 +7,7 @@ class Snapshot
def initialize(items) def initialize(items)
# @type game_tick [Integer] # @type game_tick [Integer]
@game_tick = 0 @game_tick = 0
# @type items [Array<SnapItemBase>] # @type items [Array<SnapItemBase>, Array<SnapItem>]
@items = items @items = items
end end

View file

@ -216,6 +216,7 @@ class SnapshotUnpacker
obj = NetEvent::HammerHit.new(u) obj = NetEvent::HammerHit.new(u)
elsif @verbose elsif @verbose
puts "no match #{item_type}" puts "no match #{item_type}"
exit(1)
end end
obj = unpack_ddnet_item(u, notes) if !obj && item_type.zero? obj = unpack_ddnet_item(u, notes) if !obj && item_type.zero?
if obj if obj

View file

@ -14,6 +14,7 @@ require_relative 'packer'
require_relative 'models/player' require_relative 'models/player'
require_relative 'game_client' require_relative 'game_client'
require_relative 'config' require_relative 'config'
require_relative 'connection'
class TeeworldsClient class TeeworldsClient
attr_reader :state, :hooks, :game_client, :verbose_snap attr_reader :state, :hooks, :game_client, :verbose_snap
@ -445,9 +446,20 @@ class TeeworldsClient
end end
chunks = BigChungusTheChunkGetter.get_chunks(data) chunks = BigChungusTheChunkGetter.get_chunks(data)
chunks.each do |chunk| chunks.each do |chunk|
if chunk.flags_vital && !chunk.flags_resend && chunk.msg != NETMSG_NULL if chunk.flags_vital
@netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE if chunk.seq == (@netbase.ack + 1) % NET_MAX_SEQUENCE
puts "got ack: #{@netbase.ack}" if @verbose # in sequence
@netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE
else
puts 'warning: got chunk out of sequence! ' \
"seq=#{chunk.seq} expected_seq=#{(@netbase.ack + 1) % NET_MAX_SEQUENCE}"
if seq_in_backroom?(chunk.seq, @netbase.ack)
puts ' dropping known chunk ...'
next
end
# TODO: request resend
puts ' REQUESTING RESEND NOT IMPLEMENTED'
end
end end
process_chunk(chunk) process_chunk(chunk)
end end

View file

@ -11,9 +11,11 @@ require_relative 'chunk'
require_relative 'net_base' require_relative 'net_base'
require_relative 'models/net_addr' require_relative 'models/net_addr'
require_relative 'packer' require_relative 'packer'
require_relative 'config'
require_relative 'game_server' require_relative 'game_server'
require_relative 'models/token' require_relative 'models/token'
require_relative 'messages/sv_emoticon' require_relative 'messages/sv_emoticon'
require_relative 'snapshot/builder'
class Client class Client
attr_accessor :id, :addr, :vital_sent, :last_recv_time, :token, :player, :in_game, :authed attr_accessor :id, :addr, :vital_sent, :last_recv_time, :token, :player, :in_game, :authed
@ -64,14 +66,17 @@ class Client
end end
class TeeworldsServer class TeeworldsServer
attr_accessor :clients attr_accessor :clients, :config
attr_reader :hooks, :shutdown_reason, :current_game_tick attr_reader :hooks, :shutdown_reason, :current_game_tick
def initialize(options = {}) def initialize(options = {})
@verbose = options[:verbose] || false @verbose = options[:verbose] || false
@ip = '127.0.0.1' @ip = '127.0.0.1'
@port = 8303 @port = 8303
@config = Config.new(file: options[:config], type: :server)
@game_server = GameServer.new(self) @game_server = GameServer.new(self)
@game_server.load_map
# @type clients [Hash<Integer, Client>]
@clients = {} @clients = {}
@current_game_tick = 0 @current_game_tick = 0
@last_snap_time = Time.now @last_snap_time = Time.now
@ -369,6 +374,19 @@ class TeeworldsServer
@netbase.send_packet(msg, chunks: 1, client:) @netbase.send_packet(msg, chunks: 1, client:)
end end
##
# https://chillerdragon.github.io/teeworlds-protocol/07/game_messages.html#NETMSGTYPE_SV_CLIENTINFO
#
# @param client [Client] recipient of the message
# @param client_info [ClientInfo] client info net message
def send_client_info(client, client_info)
data = client_info.to_a
msg = NetChunk.create_header(vital: true, size: 1 + data.size, client:) +
[pack_msg_id(NETMSGTYPE_SV_CLIENTINFO, system: false)] +
data
@netbase.send_packet(msg, chunks: 1, client:)
end
def send_server_info(client, server_info) def send_server_info(client, server_info)
msg = NetChunk.create_header(vital: true, size: 1 + server_info.size, client:) + msg = NetChunk.create_header(vital: true, size: 1 + server_info.size, client:) +
[pack_msg_id(NETMSG_SERVERINFO, system: true)] + [pack_msg_id(NETMSG_SERVERINFO, system: true)] +
@ -470,11 +488,57 @@ class TeeworldsServer
end end
end end
require_relative 'snapshot/items/game_data'
require_relative 'snapshot/items/game_data_team'
require_relative 'snapshot/items/game_data_flag'
require_relative 'snapshot/items/player_info'
require_relative 'snapshot/items/character'
require_relative 'snapshot/items/flag'
def do_snap_single def do_snap_single
builder = SnapshotBuilder.new builder = SnapshotBuilder.new
builder.new_item(0, NetObj::Flag.new(
x: 1200, y: 304, team: 0
))
builder.new_item(1, NetObj::Flag.new(
x: 1296, y: 304, team: 1
))
builder.new_item(0, NetObj::GameData.new(
game_start_tick: 0,
game_state_flags: 1,
game_state_end_tick: 500
))
builder.new_item(0, NetObj::GameDataTeam.new(
teamscore_red: 0,
teamscore_blue: 0
))
builder.new_item(0, NetObj::GameDataFlag.new(
flag_carrier_red: -2,
flag_carrier_blue: -2,
flag_drop_tick_red: 0,
flag_drop_tick_blue: 0
))
builder.new_item(0, NetObj::PlayerInfo.new(
player_flags: 8,
score: 0,
latency: 0
))
builder.new_item(0, NetObj::Character.new(
x: 784, y: 305,
vel_x: 0, vel_y: 0,
angle: 0, direction: 0, jumped: 0,
hooked_player: -1, hook_state: 0,
hook_tick: 0, hook_x: 784, hook_y: 304,
hook_dx: 784, hook_dy: 0,
health: 10, armor: 0, ammo_count: 10,
weapon: 1, emote: 0,
attack_tick: 0, triggered_events: 0
))
snap = builder.finish snap = builder.finish
items = snap.to_a items = snap.to_a
delta_tick = -1
data = [] data = []
# Game tick Int # Game tick Int
data += Packer.pack_int(@current_game_tick) data += Packer.pack_int(@current_game_tick)
@ -483,15 +547,28 @@ class TeeworldsServer
# Crc Int # Crc Int
data += Packer.pack_int(snap.crc) data += Packer.pack_int(snap.crc)
# Part size Int The size of this part. Meaning the size in bytes of the next raw data field. # Part size Int The size of this part. Meaning the size in bytes of the next raw data field.
data += Packer.pack_int(items.size) header = []
header += [0x00] # removed items
header += Packer.pack_int(snap.items.count) # num item deltas
header += [0x00] # _zero
part_size = items.size + header.size
data += Packer.pack_int(part_size)
# Data # Data
data += header
data += items data += items
msg = NetChunk.create_header(vital: false, size: data.size + 1) +
[pack_msg_id(NETMSG_SNAPSINGLE, system: true)] +
data
@clients.each_value do |client|
next unless client.in_game?
p data @netbase.send_packet(msg, chunks: 1, client:)
end
end end
def do_snapshot def do_snapshot
do_snap_empty do_snap_empty
# do_snap_single
end end
def get_player_by_id(id) def get_player_by_id(id)

View file

@ -1,39 +1,199 @@
# frozen_string_literal: true # frozen_string_literal: true
# require_relative '../lib/snapshot/builder.rb' require_relative '../lib/snapshot/builder'
# require_relative '../lib/snapshot/items/character'
# describe 'SnapshotBuilder', :snapshot do require_relative '../lib/snapshot/items/pickup'
# context 'finish' do require_relative '../lib/snapshot/items/flag'
# it 'Should create correct snap' do require_relative '../lib/snapshot/items/game_data'
# builder = SnapshotBuilder.new require_relative '../lib/snapshot/items/game_data_team'
# snap = builder.finish require_relative '../lib/snapshot/items/game_data_flag'
# expected_payload = [ require_relative '../lib/snapshot/items/player_info'
# 0x00, 0x01, 0x00, 0x0a, # removed_items=0 num_item_deltas=1 _zero=0 type=10 NetObj::Character
# 0x00, 0x29, 0x00, 0x0d, # id=0 tick=41 x=0 y=13
# 0x00, 0xb3, 0x36, 0x00, # vel_x=0 vel_y=3507 angle=0
# 0x00, 0x40, 0x00, 0x00, # direction=0 jumped=-1 hooked_player=0 hook_state=0
# 0x00, 0x00, 0x00, 0x00, # hook_tick=0 hook_x=0 hook_y=0 hook_dx=0
# 0x00, 0x00, 0x00, 0x00, # hook_dy=0 health=0 armor=0 ammo_count=0
# 0x00, 0x00, 0x00, 0x00, # weapon=0 emote=0 attack_tick=0 triggered_events=0
# ]
# expect(snap.to_a).to eq(expected_payload)
# end
# end
# end
# >>> snap NETMSG_SNAPSINGLE (8) describe 'SnapshotBuilder', :snapshot do
# id=8 game_tick=1908 delta_tick=38 context '#finish should create snap payload' do
# num_parts=1 part=0 crc=16846 part_size=28 it 'Should build a snap with one character' do
# builder = SnapshotBuilder.new
# header: char = NetObj::Character.new(
# 11 b4 1d 26 ...& int 17 >> 1 = 8 int 1908 int 38 tick: 41, x: 0, y: 13,
# 8e 87 02 1c .... int 16846 int 28 vel_x: 0, vel_y: 3507, angle: 0,
# direction: 0, jumped: -1, hooked_player: 0, hook_state: 0,
# payload: hook_tick: 0, hook_x: 0, hook_y: 0, hook_dx: 0,
# 00 01 00 0a .... removed_items=0 num_item_deltas=1 _zero=0 type=10 NetObj::Character hook_dy: 0, health: 0, armor: 0, ammo_count: 0,
# 00 29 00 0d .).. id=0 tick=41 x=0 y=13 weapon: 0, emote: 0, attack_tick: 0, triggered_events: 0
# 00 b3 36 00 ..6. vel_x=0 vel_y=3507 angle=0 )
# 00 40 00 00 .@.. direction=0 jumped=-1 hooked_player=0 hook_state=0 builder.new_item(0, char)
# 00 00 00 00 .... hook_tick=0 hook_x=0 hook_y=0 hook_dx=0 snap = builder.finish
# 00 00 00 00 .... hook_dy=0 health=0 armor=0 ammo_count=0 expected_payload = [
# 00 00 00 00 .... weapon=0 emote=0 attack_tick=0 triggered_events=0 0x0a, # type=10 NetObj::Character
0x00, 0x29, 0x00, 0x0d, # id=0 tick=41 x=0 y=13
0x00, 0xb3, 0x36, 0x00, # vel_x=0 vel_y=3507 angle=0
0x00, 0x40, 0x00, 0x00, # direction=0 jumped=-1 hooked_player=0 hook_state=0
0x00, 0x00, 0x00, 0x00, # hook_tick=0 hook_x=0 hook_y=0 hook_dx=0
0x00, 0x00, 0x00, 0x00, # hook_dy=0 health=0 armor=0 ammo_count=0
0x00, 0x00, 0x00, 0x00 # weapon=0 emote=0 attack_tick=0 triggered_events=0
]
expect(snap.to_a).to eq(expected_payload)
# >>> snap NETMSG_SNAPSINGLE (8)
# id=8 game_tick=1908 delta_tick=38
# num_parts=1 part=0 crc=16846 part_size=28
#
# header:
# 11 b4 1d 26 ...& int 17 >> 1 = 8 int 1908 int 38
# 8e 87 02 1c .... int 16846 int 28
#
# payload:
# 00 01 00 0a .... removed_items=0 num_item_deltas=1 _zero=0 type=10 NetObj::Character
# 00 29 00 0d .).. id=0 tick=41 x=0 y=13
# 00 b3 36 00 ..6. vel_x=0 vel_y=3507 angle=0
# 00 40 00 00 .@.. direction=0 jumped=-1 hooked_player=0 hook_state=0
# 00 00 00 00 .... hook_tick=0 hook_x=0 hook_y=0 hook_dx=0
# 00 00 00 00 .... hook_dy=0 health=0 armor=0 ammo_count=0
# 00 00 00 00 .... weapon=0 emote=0 attack_tick=0 triggered_events=0
end
it 'Should build a snap with multiple items' do
builder = SnapshotBuilder.new
builder.new_item(0, NetObj::Pickup.new(
x: 1424, y: 272, type: 0
))
builder.new_item(1, NetObj::Pickup.new(
x: 1488, y: 272, type: 2
))
builder.new_item(2, NetObj::Pickup.new(
x: 1552, y: 272, type: 3
))
builder.new_item(3, NetObj::Pickup.new(
x: 1616, y: 272, type: 4
))
builder.new_item(7, NetObj::Pickup.new(
x: 1392, y: 272, type: 1
))
builder.new_item(0, NetObj::Flag.new(
x: 1200, y: 304, team: 0
))
builder.new_item(1, NetObj::Flag.new(
x: 1296, y: 304, team: 1
))
builder.new_item(0, NetObj::GameData.new(
game_start_tick: 1286,
game_state_flags: 0, game_state_end_tick: 0
))
builder.new_item(0, NetObj::GameDataTeam.new(
teamscore_red: 0, teamscore_blue: 0
))
builder.new_item(0, NetObj::GameDataFlag.new(
flag_carrier_red: -2, flag_carrier_blue: -2,
flag_drop_tick_red: 0, flag_drop_tick_blue: 0
))
builder.new_item(0, NetObj::Character.new(
tick: 1314, x: 784, y: 306,
vel_x: 0, vel_y: 256, angle: -707,
direction: 0, jumped: 0, hooked_player: -1, hook_state: 0,
hook_tick: 0, hook_x: 784, hook_y: 305, hook_dx: 0,
hook_dy: 0, health: 0, armor: 0, ammo_count: 0,
weapon: 1, emote: 0, attack_tick: 0, triggered_events: 0
))
builder.new_item(1, NetObj::Character.new(
tick: 1313, x: 848, y: 305,
vel_x: 0, vel_y: 128, angle: 0,
direction: 0, jumped: 0, hooked_player: -1, hook_state: 0,
hook_tick: 0, hook_x: 848, hook_y: 304, hook_dx: 0,
hook_dy: 0, health: 10, armor: 0, ammo_count: 10,
weapon: 1, emote: 0, attack_tick: 0, triggered_events: 0
))
builder.new_item(0, NetObj::PlayerInfo.new(
player_flags: 8, score: 0, latency: 0
))
builder.new_item(1, NetObj::PlayerInfo.new(
player_flags: 8, score: 0, latency: 0
))
snap = builder.finish
expected_payload = [
0x04, # type=4 NetObj::Pickup
0x00, 0x90, 0x16, 0x90, # id=0 x=1424 y=272
0x04, 0x00, 0x04, 0x01, # y=272 type=0 id=1 type=4 NetObj::Pickup
0x90, 0x17, 0x90, 0x04, # x=1488 y=272
0x02, 0x04, 0x02, 0x90, # type=2 id=2 x=1552 type=4 NetObj::Pickup
0x18, 0x90, 0x04, 0x03, # x=1552 y=272 type=3
0x04, 0x03, 0x90, 0x19, # id=3 x=1616 type=4 NetObj::Pickup
0x90, 0x04, 0x04, 0x04, # y=272 type=4 type=4 NetObj::Pickup
0x07, 0xb0, 0x15, 0x90, # id=7 x=1392 y=272
0x04, 0x01, 0x05, 0x00, # y=272 type=1 id=0 type=5 NetObj::Flag
0xb0, 0x12, 0xb0, 0x04, # x=1200 y=304
0x00, 0x05, 0x01, 0x90, # team=0 id=1 x=1296 type=5 NetObj::Flag
0x14, 0xb0, 0x04, 0x01, # x=1296 y=304 team=1
0x06, 0x00, 0x86, 0x14, # id=0 game_start_tick=1286 type=6 NetObj::GameData
0x00, 0x00, 0x07, 0x00, # game_state_flags=0 game_state_end_tick=0 id=0 type=7 NetObj::GameDataTeam
0x00, 0x00, 0x08, 0x00, # teamscore_red=0 teamscore_blue=0 id=0 type=8 NetObj::GameDataFlag
0x41, 0x41, 0x00, 0x00, # flag_carrier_red=-2 flag_carrier_blue=-2 flag_drop_tick_red=0 flag_drop_tick_blue=0
0x0a, 0x00, 0xa2, 0x14, # id=0 tick=1314 type=10 NetObj::Character
0x90, 0x0c, 0xb2, 0x04, # x=784 y=306
0x00, 0x80, 0x04, 0xc2, # vel_x=0 vel_y=256 angle=-707
0x0b, 0x00, 0x00, 0x40, # angle=-707 direction=0 jumped=0 hooked_player=-1
0x00, 0x00, 0x90, 0x0c, # hook_state=0 hook_tick=0 hook_x=784
0xb1, 0x04, 0x00, 0x00, # hook_y=305 hook_dx=0 hook_dy=0
0x00, 0x00, 0x00, 0x01, # health=0 armor=0 ammo_count=0 weapon=1
0x00, 0x00, 0x00, 0x0a, # emote=0 attack_tick=0 triggered_events=0 type=10 NetObj::Character
0x01, 0xa1, 0x14, 0x90, # id=1 tick=1313 x=848
0x0d, 0xb1, 0x04, 0x00, # x=848 y=305 vel_x=0
0x80, 0x02, 0x00, 0x00, # vel_y=128 angle=0 direction=0
0x00, 0x40, 0x00, 0x00, # jumped=0 hooked_player=-1 hook_state=0 hook_tick=0
0x90, 0x0d, 0xb0, 0x04, # hook_x=848 hook_y=304
0x00, 0x00, 0x0a, 0x00, # hook_dx=0 hook_dy=0 health=10 armor=0
0x0a, 0x01, 0x00, 0x00, # ammo_count=10 weapon=1 emote=0 attack_tick=0
0x00, 0x0b, 0x00, 0x08, # triggered_events=0 id=0 player_flags=8 type=11 NetObj::PlayerInfo
0x00, 0x00, 0x0b, 0x01, # score=0 latency=0 id=1 type=11 NetObj::PlayerInfo
0x08, 0x00, 0x00 # player_flags=8 score=0 latency=0
]
# snap.to_a.each_with_index do |s, i|
# p "[#{i}] got=#{s} want=#{expected_payload[i]}"
# expect(s).to eq(expected_payload[i])
# end
expect(snap.to_a).to eq(expected_payload)
# >>> snap NETMSG_SNAPSINGLE (8)
# id=8 game_tick=1420 delta_tick=1421
# num_parts=1 part=0 crc=20053 part_size=139
#
# header:
# 11 8c 16 8d .... int 17 >> 1 = 8 int 1420 int 1421
# 16 95 b9 02 .... int 1421 int 20053
# 8b 02 .. int 139
#
# payload:
# 00 0e 00 04 .... removed_items=0 num_item_deltas=14 _zero=0 type=4 NetObj::Pickup
# 00 90 16 90 .... id=0 x=1424 y=272
# 04 00 04 01 .... y=272 type=0 id=1 type=4 NetObj::Pickup
# 90 17 90 04 .... x=1488 y=272
# 02 04 02 90 .... type=2 id=2 x=1552 type=4 NetObj::Pickup
# 18 90 04 03 .... x=1552 y=272 type=3
# 04 03 90 19 .... id=3 x=1616 type=4 NetObj::Pickup
# 90 04 04 04 .... y=272 type=4 type=4 NetObj::Pickup
# 07 b0 15 90 .... id=7 x=1392 y=272
# 04 01 05 00 .... y=272 type=1 id=0 type=5 NetObj::Flag
# b0 12 b0 04 .... x=1200 y=304
# 00 05 01 90 .... team=0 id=1 x=1296 type=5 NetObj::Flag
# 14 b0 04 01 .... x=1296 y=304 team=1
# 06 00 86 14 .... id=0 game_start_tick=1286 type=6 NetObj::GameData
# 00 00 07 00 .... game_state_flags=0 game_state_end_tick=0 id=0 type=7 NetObj::GameDataTeam
# 00 00 08 00 .... teamscore_red=0 teamscore_blue=0 id=0 type=8 NetObj::GameDataFlag
# 41 41 00 00 AA.. flag_carrier_red=-2 flag_carrier_blue=-2 flag_drop_tick_red=0 flag_drop_tick_blue=0
# 0a 00 a2 14 .... id=0 tick=1314 type=10 NetObj::Character
# 90 0c b2 04 .... x=784 y=306
# 00 80 04 c2 .... vel_x=0 vel_y=256 angle=-707
# 0b 00 00 40 ...@ angle=-707 direction=0 jumped=0 hooked_player=-1
# 00 00 90 0c .... hook_state=0 hook_tick=0 hook_x=784
# b1 04 00 00 .... hook_y=305 hook_dx=0 hook_dy=0
# 00 00 00 01 .... health=0 armor=0 ammo_count=0 weapon=1
# 00 00 00 0a .... emote=0 attack_tick=0 triggered_events=0 type=10 NetObj::Character
# 01 a1 14 90 .... id=1 tick=1313 x=848
# 0d b1 04 00 .... x=848 y=305 vel_x=0
# 80 02 00 00 .... vel_y=128 angle=0 direction=0
# 00 40 00 00 .@.. jumped=0 hooked_player=-1 hook_state=0 hook_tick=0
# 90 0d b0 04 .... hook_x=848 hook_y=304
# 00 00 0a 00 .... hook_dx=0 hook_dy=0 health=10 armor=0
# 0a 01 00 00 .... ammo_count=10 weapon=1 emote=0 attack_tick=0
# 00 0b 00 08 .... triggered_events=0 id=0 player_flags=8 type=11 NetObj::PlayerInfo
# 00 00 0b 01 .... score=0 latency=0 id=1 type=11 NetObj::PlayerInfo
# 08 00 00 ... player_flags=8 score=0 latency=0
end
end
end