Class: Sc2::Connection

Inherits:
Object
  • Object
show all
Includes:
Requests
Defined in:
lib/sc2ai/connection.rb,
lib/sc2ai/connection/requests.rb,
lib/sc2ai/connection/status_listener.rb,
lib/sc2ai/connection/connection_listener.rb

Overview

Manages client connection to the Api

Defined Under Namespace

Modules: ConnectionListener, Requests, StatusListener

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Requests

#action, #available_maps, #create_game, #data, #debug, #game_info, #join_game, #leave_game, #observation, #observer_action, #observer_action_camera_move, #ping, #query, #query_abilities, #query_abilities_for_unit_tags, #query_ability_ids_for_unit, #query_pathings, #query_placements, #quit, #replay_info, #request_quick_load, #request_quick_save, #restart_game, #save_map, #save_replay, #send_request_for, #start_replay, #step

Constructor Details

#initialize(host:, port:) ⇒ Connection

#param [Sc2::CallbackListener] listener

Parameters:

  • host (String)
  • port (Integer)


39
40
41
42
43
44
45
46
47
48
49
# File 'lib/sc2ai/connection.rb', line 39

def initialize(host:, port:)
  @host = host
  @port = port
  @listeners = {}
  @websocket = nil
  @status = :unknown
  @external_time = 0.0
  # Only allow one request at a time.
  # TODO: Since it turns out the client websocket can only handle 1 request at a time, we don't stricly need Async
  @scheduler = Async::Semaphore.new(1)
end

Instance Attribute Details

#external_timeFloat

Total milliseconds spent waiting on SC2 responses

Returns:

  • (Float)


17
18
19
# File 'lib/sc2ai/connection.rb', line 17

def external_time
  @external_time
end

#hostObject

Returns the value of attribute host.



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

def host
  @host
end

#listenersObject

Returns the value of attribute listeners.



34
35
36
# File 'lib/sc2ai/connection.rb', line 34

def listeners
  @listeners
end

#portObject

Returns the value of attribute port.



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

def port
  @port
end

#statusObject

Last known game status, i.e. :launched, :ended, :unknown

:launched // Game has been launch and is not yet doing anything.
:init_game // Create game has been called, and the host is awaiting players.
:in_game // In a single or multiplayer game.
:in_replay // In a replay.
:ended // Game has ended, can still request game info, but ready for a new game.
:quit // Application is shutting down.
:unknown // Should not happen, but indicates an error if it occurs.
@return [Symbol] game status


30
31
32
# File 'lib/sc2ai/connection.rb', line 30

def status
  @status
end

#websocketObject

Returns the value of attribute websocket.



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

def websocket
  @websocket
end

Instance Method Details

#add_listener(listener, klass:) ⇒ void

This method returns an undefined value.

Add a listener of specific callback type

Parameters:



86
87
88
89
# File 'lib/sc2ai/connection.rb', line 86

def add_listener(listener, klass:)
  @listeners[klass.to_s] ||= []
  @listeners[klass.to_s].push(listener)
end

#closevoid

This method returns an undefined value.

Closes Connection to client



77
78
79
80
# File 'lib/sc2ai/connection.rb', line 77

def close
  @websocket&.close
  @listeners[ConnectionListener.name]&.each { _1.on_disconnect(self) }
end

#connectvoid

This method returns an undefined value.

Attempts to connect for a period of time, ignoring errors nad performing on_* callbacks



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/sc2ai/connection.rb', line 53

def connect
  attempt = 1
  Sc2.logger.debug { "Waiting for client..." } if (attempt % 5).zero?

  begin
    @websocket = Async::WebSocket::Client.connect(endpoint) # , handler: Sc2::Connection::Connection)
    @listeners[ConnectionListener.name]&.each { _1.on_connected(self) }
    # do initial ping to ensure status is set and connection is working
    response_ping = ping
    Sc2.logger.debug { "Game version: #{response_ping.game_version}" }
  rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EADDRNOTAVAIL
    raise Error, "Connection timeout. Max retry exceeded." unless (attempt += 1) < 30 # 30s attempts
    @listeners[ConnectionListener.name]&.each { _1.on_connection_waiting(self) }
    sleep(1)
    retry
  rescue Error => e
    Sc2.logger.error "#{e.class}: #{e.message}"
    @listeners[ConnectionListener.name]&.each { _1.on_disconnect(self) }
  end
  nil
end

#remove_listener(listener, klass:) ⇒ Object

Removes a listener of specific callback type

Parameters:



94
95
96
# File 'lib/sc2ai/connection.rb', line 94

def remove_listener(listener, klass:)
  @listeners[klass.to_s].delete(listener)
end

#send_request(request) ⇒ Api::Response


Sends a request synchronously and returns Api::Response type

Returns:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/sc2ai/connection.rb', line 101

def send_request(request)
  @scheduler.async do |_task|
    request = request.to_proto unless request.is_a?(String)

    time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
    @websocket.send_binary(request)
    response = @websocket.read.to_str
    @external_time += Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - time

    response = Api::Response.decode(response)

    if @status != response.status
      @status = response.status
      @listeners[StatusListener.name]&.each { _1.on_status_change(@status) }
    end

    response
  end.wait
rescue EOFError => e
  Sc2.logger.error e
  close
end

#send_request_and_ignore(request) ⇒ void

This method returns an undefined value.

Sends and ignores response. Meant to be used as optimization for RequestStep. No other command sends and ignores. Expects request to be to_proto’d already



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/sc2ai/connection.rb', line 129

def send_request_and_ignore(request)
  @scheduler.async do |_task|
    time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
    @websocket.send_binary(request)
    while @websocket.read_frame
      if @websocket.frames.last&.finished?
        @websocket.frames = []
        break
      end
    end
    @external_time += Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - time
  end.wait

  nil
end