Class: TeeworldsClient

Inherits:
Object
  • Object
show all
Defined in:
lib/teeworlds_client.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ TeeworldsClient

Returns a new instance of TeeworldsClient.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/teeworlds_client.rb', line 22

def initialize(options = {})
  @verbose = options[:verbose] || false
  @state = NET_CONNSTATE_OFFLINE
  @ip = 'localhost'
  @port = 8303
  @local_client_id = 0
  @config = Config.new(file: options[:config])
  @hooks = {
    chat: [],
    map_change: [],
    client_info: [],
    client_drop: [],
    connected: [],
    disconnect: [],
    rcon_line: [],
    snapshot: [],
    input_timing: [],
    auth_on: [],
    auth_off: [],
    rcon_cmd_add: [],
    rcon_cmd_rem: [],
    tick: [],
    maplist_entry_add: [],
    maplist_entry_rem: []
  }
  @thread_running = false
  @signal_disconnect = false
  @game_client = GameClient.new(self)
  @start_info = {
    name: 'ruby gamer',
    clan: '',
    country: -1,
    body: 'spiky',
    marking: 'duodonny',
    decoration: '',
    hands: 'standard',
    feet: 'standard',
    eyes: 'standard',
    custom_color_body: 0,
    custom_color_marking: 0,
    custom_color_decoration: 0,
    custom_color_hands: 0,
    custom_color_feet: 0,
    custom_color_eyes: 0,
    color_body: 0,
    color_marking: 0,
    color_decoration: 0,
    color_hands: 0,
    color_feet: 0,
    color_eyes: 0
  }
  @rcon_authed = false
end

Instance Attribute Details

#game_clientObject (readonly)

Returns the value of attribute game_client.



19
20
21
# File 'lib/teeworlds_client.rb', line 19

def game_client
  @game_client
end

#hooksObject (readonly)

Returns the value of attribute hooks.



19
20
21
# File 'lib/teeworlds_client.rb', line 19

def hooks
  @hooks
end

#local_client_idObject

Returns the value of attribute local_client_id.



20
21
22
# File 'lib/teeworlds_client.rb', line 20

def local_client_id
  @local_client_id
end

#rcon_authedObject

Returns the value of attribute rcon_authed.



20
21
22
# File 'lib/teeworlds_client.rb', line 20

def rcon_authed
  @rcon_authed
end

#stateObject (readonly)

Returns the value of attribute state.



19
20
21
# File 'lib/teeworlds_client.rb', line 19

def state
  @state
end

Instance Method Details

#connect(ip, port, options = {}) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/teeworlds_client.rb', line 156

def connect(ip, port, options = {})
  options[:detach] = options[:detach] || false
  if options[:detach] && @thread_running
    puts 'Error: connection thread already running call disconnect() first'
    return
  end
  disconnect
  @signal_disconnect = false
  @ticks = 0
  @game_client = GameClient.new(self)
  # me trying to write cool code
  @client_token = (1..4).to_a.map { |_| rand(0..255) }
  @client_token = @client_token.map { |b| b.to_s(16).rjust(2, '0') }.join
  puts "client token #{@client_token}"
  @netbase = NetBase.new(verbose: @verbose)
  NetChunk.reset
  @ip = ip
  @port = port
  puts "connecting to #{@ip}:#{@port} .."
  @s = UDPSocket.new
  @s.connect(ip, port)
  puts "client port: #{@s.addr[1]}"
  @netbase.connect(@s, @ip, @port)
  @token = nil
  send_ctrl_with_token
  if options[:detach]
    @thread_running = true
    Thread.new do
      connection_loop
    end
  else
    connection_loop
  end
end

#disconnectObject



197
198
199
200
201
202
203
# File 'lib/teeworlds_client.rb', line 197

def disconnect
  puts 'disconnecting.'
  send_ctrl_close unless @s.nil?
  @s&.close
  @s = nil
  @signal_disconnect = true
end

#on_auth_off(&block) ⇒ Object



88
89
90
# File 'lib/teeworlds_client.rb', line 88

def on_auth_off(&block)
  @hooks[:auth_off].push(block)
end

#on_auth_on(&block) ⇒ Object



84
85
86
# File 'lib/teeworlds_client.rb', line 84

def on_auth_on(&block)
  @hooks[:auth_on].push(block)
end

#on_chat(&block) ⇒ Object



108
109
110
# File 'lib/teeworlds_client.rb', line 108

def on_chat(&block)
  @hooks[:chat].push(block)
end

#on_client_drop(&block) ⇒ Object



120
121
122
# File 'lib/teeworlds_client.rb', line 120

def on_client_drop(&block)
  @hooks[:client_drop].push(block)
end

#on_client_info(&block) ⇒ Object



116
117
118
# File 'lib/teeworlds_client.rb', line 116

def on_client_info(&block)
  @hooks[:client_info].push(block)
end

#on_connected(&block) ⇒ Object



124
125
126
# File 'lib/teeworlds_client.rb', line 124

def on_connected(&block)
  @hooks[:connected].push(block)
end

#on_disconnect(&block) ⇒ Object



128
129
130
# File 'lib/teeworlds_client.rb', line 128

def on_disconnect(&block)
  @hooks[:disconnect].push(block)
end

#on_input_timing(&block) ⇒ Object



140
141
142
# File 'lib/teeworlds_client.rb', line 140

def on_input_timing(&block)
  @hooks[:input_timing].push(block)
end

#on_map_change(&block) ⇒ Object



112
113
114
# File 'lib/teeworlds_client.rb', line 112

def on_map_change(&block)
  @hooks[:map_change].push(block)
end

#on_maplist_entry_add(&block) ⇒ Object



100
101
102
# File 'lib/teeworlds_client.rb', line 100

def on_maplist_entry_add(&block)
  @hooks[:maplist_entry_add].push(block)
end

#on_maplist_entry_rem(&block) ⇒ Object



104
105
106
# File 'lib/teeworlds_client.rb', line 104

def on_maplist_entry_rem(&block)
  @hooks[:maplist_entry_rem].push(block)
end

#on_rcon_cmd_add(&block) ⇒ Object



92
93
94
# File 'lib/teeworlds_client.rb', line 92

def on_rcon_cmd_add(&block)
  @hooks[:rcon_cmd_add].push(block)
end

#on_rcon_cmd_rem(&block) ⇒ Object



96
97
98
# File 'lib/teeworlds_client.rb', line 96

def on_rcon_cmd_rem(&block)
  @hooks[:rcon_cmd_rem].push(block)
end

#on_rcon_line(&block) ⇒ Object



132
133
134
# File 'lib/teeworlds_client.rb', line 132

def on_rcon_line(&block)
  @hooks[:rcon_line].push(block)
end

#on_snapshot(&block) ⇒ Object



136
137
138
# File 'lib/teeworlds_client.rb', line 136

def on_snapshot(&block)
  @hooks[:snapshot].push(block)
end

#on_tick(&block) ⇒ Object



80
81
82
# File 'lib/teeworlds_client.rb', line 80

def on_tick(&block)
  @hooks[:tick].push(block)
end

#rcon(command) ⇒ Object



274
275
276
277
278
279
280
281
# File 'lib/teeworlds_client.rb', line 274

def rcon(command)
  data = []
  data += Packer.pack_str(command)
  msg = NetChunk.create_header(vital: true, size: data.size + 1) +
        [pack_msg_id(NETMSG_RCON_CMD, system: true)] +
        data
  @netbase.send_packet(msg)
end

#rcon_auth(name, password = nil) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/teeworlds_client.rb', line 247

def rcon_auth(name, password = nil)
  if name.instance_of?(Hash)
    password = name[:password]
    name = name[:name]
  end
  if password.nil?
    raise "Error: password can not be empty\n" \
          "       provide two strings: name, password\n" \
          "       or a hash with the key :password\n" \
          "\n" \
          "       rcon_auth('', '123')\n" \
          "       rcon_auth(password: '123')\n"
  end
  data = []
  if name.nil? || name == ''
    data += Packer.pack_str(password)
  else # ddnet auth using name, password and some int?
    data += Packer.pack_str(name)
    data += Packer.pack_str(password)
    data += Packer.pack_int(1)
  end
  msg = NetChunk.create_header(vital: true, size: data.size + 1) +
        [pack_msg_id(NETMSG_RCON_AUTH, system: true)] +
        data
  @netbase.send_packet(msg)
end

#rcon_authed?Boolean

Returns:

  • (Boolean)


76
77
78
# File 'lib/teeworlds_client.rb', line 76

def rcon_authed?
  @rcon_authed
end

#send_chat(str) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/teeworlds_client.rb', line 144

def send_chat(str)
  @netbase.send_packet(
    NetChunk.create_header(vital: true, size: 4 + str.length) +
    [
      pack_msg_id(NETMSGTYPE_CL_SAY),
      CHAT_ALL,
      64 # should use TARGET_SERVER (-1) instead of hacking 64 in here
    ] +
    Packer.pack_str(str)
  )
end

#send_ctrl_closeObject

TODO: this is same in client and server

move to NetBase???


193
194
195
# File 'lib/teeworlds_client.rb', line 193

def send_ctrl_close
  @netbase&.send_packet([NET_CTRLMSG_CLOSE], chunks: 0, control: true)
end

#send_ctrl_keepaliveObject



220
221
222
# File 'lib/teeworlds_client.rb', line 220

def send_ctrl_keepalive
  @netbase.send_packet([NET_CTRLMSG_KEEPALIVE], chunks: 0, control: true)
end

#send_ctrl_with_tokenObject



229
230
231
232
233
# File 'lib/teeworlds_client.rb', line 229

def send_ctrl_with_token
  @state = NET_CONNSTATE_TOKEN
  msg = [NET_CTRLMSG_TOKEN] + str_bytes(@client_token) + Array.new(512, 0x00)
  @netbase.send_packet(msg, chunks: 0, control: true)
end

#send_enter_gameObject



311
312
313
314
315
316
# File 'lib/teeworlds_client.rb', line 311

def send_enter_game
  @netbase.send_packet(
    NetChunk.create_header(vital: true, size: 1) +
    [pack_msg_id(NETMSG_ENTERGAME, system: true)]
  )
end

#send_infoObject



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/teeworlds_client.rb', line 235

def send_info
  data = []
  data += Packer.pack_str(GAME_NETVERSION)
  data += Packer.pack_str(@config.password)
  data += Packer.pack_int(CLIENT_VERSION)
  msg = NetChunk.create_header(vital: true, size: data.size + 1) +
        [pack_msg_id(NETMSG_INFO, system: true)] +
        data

  @netbase.send_packet(msg)
end

#send_input(input = {}) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/teeworlds_client.rb', line 318

def send_input(input = {})
  inp = {
    direction: input[:direction] || -1,
    target_x: input[:target_x] || 10,
    target_y: input[:target_y] || 10,
    jump: input[:jump] || rand(0..1),
    fire: input[:fire] || 0,
    hook: input[:hook] || 0,
    player_flags: input[:player_flags] || 0,
    wanted_weapon: input[:wanted_weapon] || 0,
    next_weapon: input[:next_weapon] || 0,
    prev_weapon: input[:prev_weapon] || 0
  }

  data = []
  data += Packer.pack_int(@game_client.ack_game_tick)
  data += Packer.pack_int(@game_client.pred_game_tick)
  data += Packer.pack_int(40) # magic size 40
  data += Packer.pack_int(inp[:direction])
  data += Packer.pack_int(inp[:target_x])
  data += Packer.pack_int(inp[:target_y])
  data += Packer.pack_int(inp[:jump])
  data += Packer.pack_int(inp[:fire])
  data += Packer.pack_int(inp[:hook])
  data += Packer.pack_int(inp[:player_flags])
  data += Packer.pack_int(inp[:wanted_weapon])
  data += Packer.pack_int(inp[:next_weapon])
  data += Packer.pack_int(inp[:prev_weapon])
  msg = NetChunk.create_header(vital: false, size: data.size + 1) +
        [pack_msg_id(NETMSG_INPUT, system: true)] +
        data
  @netbase.send_packet(msg)
end

#send_msg(data) ⇒ Object



216
217
218
# File 'lib/teeworlds_client.rb', line 216

def send_msg(data)
  @netbase.send_packet(data)
end

#send_msg_connectObject



224
225
226
227
# File 'lib/teeworlds_client.rb', line 224

def send_msg_connect
  msg = [NET_CTRLMSG_CONNECT] + str_bytes(@client_token) + Array.new(501, 0x00)
  @netbase.send_packet(msg, chunks: 0, control: true)
end

#send_msg_readyObject



304
305
306
307
308
309
# File 'lib/teeworlds_client.rb', line 304

def send_msg_ready
  @netbase.send_packet(
    NetChunk.create_header(vital: true, size: 1) +
    [pack_msg_id(NETMSG_READY, system: true)]
  )
end

#send_msg_start_infoObject



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/teeworlds_client.rb', line 283

def send_msg_start_info
  data = []

  @start_info.each do |key, value|
    if value.instance_of?(String)
      data += Packer.pack_str(value)
    elsif value.instance_of?(Integer)
      data += Packer.pack_int(value)
    else
      puts "Error: invalid start info #{key}: #{value}"
      exit 1
    end
  end

  @netbase.send_packet(
    NetChunk.create_header(vital: true, size: data.size + 1) +
    [pack_msg_id(NETMSGTYPE_CL_STARTINFO, system: false)] +
    data
  )
end

#set_startinfo(info) ⇒ Object



205
206
207
208
209
210
211
212
213
214
# File 'lib/teeworlds_client.rb', line 205

def set_startinfo(info)
  info.each do |key, value|
    unless @start_info.key?(key)
      puts "Error: invalid start info key '#{key}'"
      puts "       valid keys: #{@start_info.keys}"
      exit 1
    end
    @start_info[key] = value
  end
end