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)


34
35
36
37
38
39
40
41
42
43
# File 'lib/sc2ai/connection.rb', line 34

def initialize(host:, port:)
  @host = host
  @port = port
  @listeners = {}
  @websocket = nil
  @status = :unknown
  # 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

#hostObject

Returns the value of attribute host.



14
15
16
# File 'lib/sc2ai/connection.rb', line 14

def host
  @host
end

#listenersObject

Returns the value of attribute listeners.



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

def listeners
  @listeners
end

#portObject

Returns the value of attribute port.



14
15
16
# File 'lib/sc2ai/connection.rb', line 14

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


25
26
27
# File 'lib/sc2ai/connection.rb', line 25

def status
  @status
end

#websocketObject

Returns the value of attribute websocket.



14
15
16
# File 'lib/sc2ai/connection.rb', line 14

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:



81
82
83
84
# File 'lib/sc2ai/connection.rb', line 81

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



72
73
74
75
# File 'lib/sc2ai/connection.rb', line 72

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



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/sc2ai/connection.rb', line 47

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
    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:



89
90
91
# File 'lib/sc2ai/connection.rb', line 89

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:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/sc2ai/connection.rb', line 96

def send_request(request)
  @scheduler.async do |_task|
    # r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) #debug
    # name =  request.is_a?(String) ? request : request.request #debug
    request = request.to_proto unless request.is_a?(String)
    @websocket.send_binary(request)
    response = Api::Response.decode(@websocket.read.to_str)

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

    # Sc2.logger.debug { response }
    # puts "#{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000} - #{name}" #debug
    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



123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/sc2ai/connection.rb', line 123

def send_request_and_ignore(request)
  @scheduler.async do |_task|
    @websocket.send_binary(request)
    while @websocket.read_frame
      if @websocket.frames.last&.finished?
        @websocket.frames = []
        break
      end
    end
  end.wait

  nil
end