sc2ai 0.0.0.pre → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/data.json +1 -0
- data/data/data_readable.json +22842 -0
- data/data/sc2ai/protocol/common.proto +59 -0
- data/data/sc2ai/protocol/data.proto +120 -0
- data/data/sc2ai/protocol/debug.proto +127 -0
- data/data/sc2ai/protocol/error.proto +221 -0
- data/data/sc2ai/protocol/query.proto +55 -0
- data/data/sc2ai/protocol/raw.proto +202 -0
- data/data/sc2ai/protocol/sc2api.proto +718 -0
- data/data/sc2ai/protocol/score.proto +108 -0
- data/data/sc2ai/protocol/spatial.proto +115 -0
- data/data/sc2ai/protocol/ui.proto +145 -0
- data/data/setup/setup.SC2Map +0 -0
- data/data/setup/setup.SC2Replay +0 -0
- data/data/stableid.json +35730 -0
- data/data/versions.json +554 -0
- data/exe/sc2ai +35 -0
- data/lib/docker_build/Dockerfile.ruby +74 -0
- data/lib/docker_build/docker-compose-base-image.yml +10 -0
- data/lib/docker_build/docker-compose-ladderzip.yml +9 -0
- data/lib/sc2ai/api/ability_id.rb +1644 -0
- data/lib/sc2ai/api/buff_id.rb +306 -0
- data/lib/sc2ai/api/data.rb +101 -0
- data/lib/sc2ai/api/effect_id.rb +20 -0
- data/lib/sc2ai/api/tech_tree.rb +83 -0
- data/lib/sc2ai/api/tech_tree_data.rb +2338 -0
- data/lib/sc2ai/api/unit_type_id.rb +2022 -0
- data/lib/sc2ai/api/upgrade_id.rb +310 -0
- data/lib/sc2ai/cli/cli.rb +175 -0
- data/lib/sc2ai/cli/ladderzip.rb +154 -0
- data/lib/sc2ai/cli/new.rb +88 -0
- data/lib/sc2ai/configuration.rb +145 -0
- data/lib/sc2ai/connection/connection_listener.rb +30 -0
- data/lib/sc2ai/connection/requests.rb +417 -0
- data/lib/sc2ai/connection/status_listener.rb +15 -0
- data/lib/sc2ai/connection.rb +146 -0
- data/lib/sc2ai/local_play/client/configurable_options.rb +115 -0
- data/lib/sc2ai/local_play/client.rb +159 -0
- data/lib/sc2ai/local_play/client_manager.rb +70 -0
- data/lib/sc2ai/local_play/map_file.rb +48 -0
- data/lib/sc2ai/local_play/match.rb +184 -0
- data/lib/sc2ai/overrides/array.rb +14 -0
- data/lib/sc2ai/overrides/async/process/child.rb +31 -0
- data/lib/sc2ai/overrides/kernel.rb +33 -0
- data/lib/sc2ai/paths.rb +294 -0
- data/lib/sc2ai/player/actions.rb +386 -0
- data/lib/sc2ai/player/debug.rb +224 -0
- data/lib/sc2ai/player/game_state.rb +131 -0
- data/lib/sc2ai/player/geometry.rb +766 -0
- data/lib/sc2ai/player/previous_state.rb +49 -0
- data/lib/sc2ai/player/units.rb +337 -0
- data/lib/sc2ai/player.rb +661 -0
- data/lib/sc2ai/ports.rb +152 -0
- data/lib/sc2ai/protocol/_meta_documentation.rb +39 -0
- data/lib/sc2ai/protocol/common_pb.rb +43 -0
- data/lib/sc2ai/protocol/data_pb.rb +47 -0
- data/lib/sc2ai/protocol/debug_pb.rb +56 -0
- data/lib/sc2ai/protocol/error_pb.rb +36 -0
- data/lib/sc2ai/protocol/extensions/color.rb +20 -0
- data/lib/sc2ai/protocol/extensions/point.rb +23 -0
- data/lib/sc2ai/protocol/extensions/point_2_d.rb +26 -0
- data/lib/sc2ai/protocol/extensions/position.rb +202 -0
- data/lib/sc2ai/protocol/extensions/power_source.rb +19 -0
- data/lib/sc2ai/protocol/extensions/unit.rb +489 -0
- data/lib/sc2ai/protocol/query_pb.rb +47 -0
- data/lib/sc2ai/protocol/raw_pb.rb +57 -0
- data/lib/sc2ai/protocol/sc2api_pb.rb +130 -0
- data/lib/sc2ai/protocol/score_pb.rb +40 -0
- data/lib/sc2ai/protocol/spatial_pb.rb +48 -0
- data/lib/sc2ai/protocol/ui_pb.rb +56 -0
- data/lib/sc2ai/unit_group/action_ext.rb +74 -0
- data/lib/sc2ai/unit_group/filter_ext.rb +379 -0
- data/lib/sc2ai/unit_group.rb +277 -0
- data/lib/sc2ai/version.rb +2 -1
- data/lib/sc2ai.rb +93 -2
- data/lib/templates/ladderzip/bin/ladder.tt +23 -0
- data/lib/templates/new/.ladderignore +20 -0
- data/lib/templates/new/Gemfile.tt +7 -0
- data/lib/templates/new/api/common.proto +59 -0
- data/lib/templates/new/api/data.proto +120 -0
- data/lib/templates/new/api/debug.proto +127 -0
- data/lib/templates/new/api/error.proto +221 -0
- data/lib/templates/new/api/query.proto +55 -0
- data/lib/templates/new/api/raw.proto +202 -0
- data/lib/templates/new/api/sc2api.proto +718 -0
- data/lib/templates/new/api/score.proto +108 -0
- data/lib/templates/new/api/spatial.proto +115 -0
- data/lib/templates/new/api/ui.proto +145 -0
- data/lib/templates/new/boot.rb.tt +6 -0
- data/lib/templates/new/my_bot.rb.tt +23 -0
- data/lib/templates/new/run_example_match.rb.tt +14 -0
- data/sc2ai.gemspec +80 -0
- metadata +344 -13
@@ -0,0 +1,88 @@
|
|
1
|
+
require "sc2ai/paths"
|
2
|
+
|
3
|
+
module Sc2
|
4
|
+
# Command line utilities
|
5
|
+
class Cli < Thor
|
6
|
+
class New < Thor::Group
|
7
|
+
include Thor::Actions
|
8
|
+
desc "Creates a new bot"
|
9
|
+
|
10
|
+
# Define arguments and options
|
11
|
+
argument :botname, required: true, desc: "Bot name as on aiarena. Used as a file name (short, alpha-num, no spaces!)"
|
12
|
+
argument :race, required: true, desc: "Choose a race", enum: %w[Terran Zerg Protoss Random]
|
13
|
+
|
14
|
+
def self.source_root
|
15
|
+
Sc2::Paths.template_root.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def checkname
|
19
|
+
race_arg = Sc2::Cli::New.arguments.find { |a| a.name == "race" }
|
20
|
+
unless race_arg.enum.include?(race)
|
21
|
+
raise Thor::MalformattedArgumentError, "Invalid race #{race}, must be one of #{race_arg.enum}"
|
22
|
+
end
|
23
|
+
|
24
|
+
say "We need to create a filename and classname from botname '#{@botname}'"
|
25
|
+
say "You rename classes and organize files in any way, as long as you generate a valid $bot in boot.rb"
|
26
|
+
|
27
|
+
@botname = botname.gsub(/[^0-9a-z]/i, "")
|
28
|
+
@directory = @botname.downcase
|
29
|
+
@classname = botname.split(/[^0-9a-z]/i).collect { |s| s.sub(/^./, &:upcase) }.join
|
30
|
+
@bot_file = @classname.split(/(?=[A-Z])/).join("_").downcase.concat(".rb")
|
31
|
+
say "Race: #{race}"
|
32
|
+
say "Class name: #{@classname}"
|
33
|
+
say "Create directory: ./#{@directory}"
|
34
|
+
say "Bot file: ./#{@directory}/#{@bot_file}"
|
35
|
+
|
36
|
+
unless ask("Does this look ok?", limited_to: ["y", "n"], default: "y") == "y"
|
37
|
+
raise SystemExit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_target
|
42
|
+
if Pathname("./#{@directory}").exist?
|
43
|
+
say "Folder already exists. Refusing to overwrite.", :red
|
44
|
+
raise SystemExit
|
45
|
+
end
|
46
|
+
|
47
|
+
empty_directory "./#{@directory}"
|
48
|
+
self.destination_root = Pathname("./#{@directory}").to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_boot
|
52
|
+
template("new/boot.rb", "boot.rb")
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_example_match
|
56
|
+
template("new/run_example_match.rb", "run_example_match.rb")
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_gemfile
|
60
|
+
template("new/Gemfile", "Gemfile")
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_botfile
|
64
|
+
template("new/my_bot.rb", @bot_file)
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_ignorefile
|
68
|
+
template("new/.ladderignore", ".ladderignore")
|
69
|
+
end
|
70
|
+
|
71
|
+
def copy_api
|
72
|
+
directory("new/api", "api")
|
73
|
+
end
|
74
|
+
|
75
|
+
def bye
|
76
|
+
say ""
|
77
|
+
say "Bot generated, next steps:"
|
78
|
+
say ""
|
79
|
+
say "cd #{@directory} && bundle install", :green
|
80
|
+
say ""
|
81
|
+
say "Once your project is ready, if you haven't done so, setup SC2 v4.10 with:"
|
82
|
+
say ""
|
83
|
+
say "bundle exec sc2ai setup410", :green
|
84
|
+
say ""
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "pathname"
|
5
|
+
require_relative "local_play/client/configurable_options"
|
6
|
+
|
7
|
+
module Sc2
|
8
|
+
# Global config manager for runtime
|
9
|
+
# @example Manual configuration block
|
10
|
+
# Sc2.config do |config|
|
11
|
+
# config.sc2_platform = "WineLinux"
|
12
|
+
# config.sc2_path = "/c/Program Files (x86)/StarCraft II/"
|
13
|
+
# config.ports = [5001,5002,5003]
|
14
|
+
# config.host = '127.0.0.1'
|
15
|
+
# end
|
16
|
+
class Configuration
|
17
|
+
# Include client launch options
|
18
|
+
include Sc2::Client::ConfigurableOptions
|
19
|
+
|
20
|
+
# Attributes permitted to be read and save from config yaml
|
21
|
+
CONFIG_ATTRIBUTES = %i[
|
22
|
+
sc2_platform
|
23
|
+
sc2_path
|
24
|
+
version
|
25
|
+
ports
|
26
|
+
host
|
27
|
+
display_mode
|
28
|
+
windowwidth
|
29
|
+
windowheight
|
30
|
+
windowx
|
31
|
+
windowy
|
32
|
+
verbose
|
33
|
+
data_dir
|
34
|
+
temp_dir
|
35
|
+
egl_path
|
36
|
+
osmesa_path
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
# @!attribute sc2_platform
|
40
|
+
# Sc2 platform config alias will override ENV["SC2PF"]
|
41
|
+
# @return [String] (see Sc2::Paths#platform)
|
42
|
+
attr_accessor :sc2_platform
|
43
|
+
|
44
|
+
# @!attribute sc2_path
|
45
|
+
# Sc2 Path config alias will override ENV["SC2PATH"]
|
46
|
+
# @return [String] sc2_path (see Sc2::Paths#platform)
|
47
|
+
attr_accessor :sc2_path
|
48
|
+
|
49
|
+
# @!attribute ports
|
50
|
+
# if empty, a random port will be picked when launching
|
51
|
+
# Launch param: -listen
|
52
|
+
# @return [Array<Integer>]
|
53
|
+
attr_accessor :ports
|
54
|
+
|
55
|
+
# Create a new Configuration and sets defaults and loads config from yaml
|
56
|
+
# @return [Sc2::Configuration]
|
57
|
+
def initialize
|
58
|
+
set_defaults
|
59
|
+
|
60
|
+
load_config(config_file) if config_file.exist?
|
61
|
+
|
62
|
+
# create temp dir on linux
|
63
|
+
ensure_temp_dir
|
64
|
+
end
|
65
|
+
|
66
|
+
# Config file location
|
67
|
+
# @return [Pathname] path
|
68
|
+
def config_file
|
69
|
+
# Pathname(Paths.project_root_dir).join("config", "sc2ai.yml")
|
70
|
+
Pathname(Paths.project_root_dir).join("sc2ai.yml")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sets defaults when initializing
|
74
|
+
# @return [void]
|
75
|
+
def set_defaults
|
76
|
+
@sc2_platform = Paths.platform
|
77
|
+
@sc2_path = Paths.install_dir
|
78
|
+
@ports = []
|
79
|
+
|
80
|
+
load_default_launch_options
|
81
|
+
end
|
82
|
+
|
83
|
+
# Writes this instance's attributes to yaml config_file
|
84
|
+
# @return [void]
|
85
|
+
def save_config
|
86
|
+
# config_file.dirname.mkpath unless config_file.dirname.exist?
|
87
|
+
config_file.write(to_yaml.to_s)
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# Converts attributes to yaml
|
92
|
+
# @return [Hash] yaml matching stringified keys from CONFIG_ATTRIBUTES
|
93
|
+
def to_yaml
|
94
|
+
to_h.to_yaml
|
95
|
+
end
|
96
|
+
|
97
|
+
# Converts attributes to hash
|
98
|
+
# @return [Hash] hash matching stringified keys from CONFIG_ATTRIBUTES
|
99
|
+
def to_h
|
100
|
+
CONFIG_ATTRIBUTES.map do |name|
|
101
|
+
[name.to_s, instance_variable_get(:"@#{name}")]
|
102
|
+
end.to_h
|
103
|
+
end
|
104
|
+
|
105
|
+
# Loads YAML config
|
106
|
+
# @param file [Pathname,String] location of config file
|
107
|
+
# @return [Boolean] success/failure
|
108
|
+
def load_config(file)
|
109
|
+
file = Pathname(file) unless file.is_a? Pathname
|
110
|
+
return false if !file.exist? || file.size.nil?
|
111
|
+
|
112
|
+
begin
|
113
|
+
content = ::Psych.safe_load(file.read)
|
114
|
+
unless content.is_a? Hash
|
115
|
+
Sc2.logger.warn "Failed to load #{file} because it doesn't contain valid YAML hash"
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
CONFIG_ATTRIBUTES.map(&:to_s).each do |attribute|
|
119
|
+
next unless content.key?(attribute.to_s)
|
120
|
+
|
121
|
+
instance_variable_set(:"@#{attribute}", content[attribute])
|
122
|
+
end
|
123
|
+
return true
|
124
|
+
rescue ArgumentError, Psych::SyntaxError => e
|
125
|
+
Sc2.logger.warn "Failed to load #{file}, #{e}"
|
126
|
+
rescue Errno::EACCES
|
127
|
+
Sc2.logger.warn "Failed to load #{file} due to permissions problem."
|
128
|
+
end
|
129
|
+
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Makes sure we have a temporary directory on linux if not specified
|
136
|
+
# @return [void]
|
137
|
+
def ensure_temp_dir
|
138
|
+
return unless Paths.platform == Paths::PF_LINUX
|
139
|
+
|
140
|
+
return unless @temp_dir.to_s.empty?
|
141
|
+
|
142
|
+
@temp_dir = Paths.generate_temp_dir
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sc2
|
4
|
+
class Connection
|
5
|
+
# Callbacks should be included on your listening class
|
6
|
+
# noinspection RubyUnusedLocalVariable
|
7
|
+
module ConnectionListener
|
8
|
+
# Called when connection established to application
|
9
|
+
# @param connection [Sc2Ai::Connection]
|
10
|
+
# noinspection
|
11
|
+
def on_connected(connection)
|
12
|
+
Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Called while waiting on connection to application
|
16
|
+
# @param connection [Sc2Ai::Connection]
|
17
|
+
# noinspection Lint/UnusedMethodArgument
|
18
|
+
def on_connection_waiting(connection)
|
19
|
+
Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Called when disconnected from application
|
23
|
+
# @param connection [Sc2Ai::Connection]
|
24
|
+
# noinspection Lint/UnusedMethodArgument
|
25
|
+
def on_disconnect(connection)
|
26
|
+
Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,417 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sc2
|
4
|
+
class Connection
|
5
|
+
# Sends protobuf requests over Connection to Client
|
6
|
+
module Requests
|
7
|
+
# GAME MANAGEMENT ----
|
8
|
+
|
9
|
+
# Send to host to initialize game
|
10
|
+
def create_game(map:, players:, realtime: false)
|
11
|
+
send_request_for create_game: Api::RequestCreateGame.new(
|
12
|
+
local_map: Api::LocalMap.new(map_path: map.path),
|
13
|
+
player_setup: players.map do |player|
|
14
|
+
Api::PlayerSetup.new(
|
15
|
+
type: player.type,
|
16
|
+
race: player.race,
|
17
|
+
player_name: player.name,
|
18
|
+
difficulty: player.difficulty,
|
19
|
+
ai_build: player.ai_build
|
20
|
+
)
|
21
|
+
end,
|
22
|
+
realtime:
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send to host and all clients for game to begin.
|
27
|
+
# @param race [Google::Protobuf::EnumValue] Api::Race
|
28
|
+
# @param name [String] player name
|
29
|
+
# @param server_host [String] hostname or ip of sc2 client
|
30
|
+
# @param port_config [Sc2::PortConfig] port config auto or basic, using start port
|
31
|
+
# @param enable_feature_layer [Boolean] Enables the feature layer at 1x1 pixels
|
32
|
+
# @param interface_options [Hash]
|
33
|
+
# @option interface_options [Boolean] :raw (true) raw interface enabled, default true
|
34
|
+
# @option interface_options [Boolean] :score (false) score game info
|
35
|
+
# @option interface_options [Boolean] :show_cloaked (true) hows details about cloaked units
|
36
|
+
# @option interface_options [Boolean] :show_burrowed_shadows (true) shows some details for those that produce a shadow
|
37
|
+
# @option interface_options [Boolean] :show_placeholders (true) return placeholder units (buildings to be constructed)
|
38
|
+
# @option interface_options [Boolean] :raw_affects_selection (false) for live raw does whatever it wants, for local it keeps your selection by default.
|
39
|
+
# @option interface_options [Boolean] :raw_crop_to_playable_area (true) trims away unplayable parts of map, else map is 255x255 with dead space. performant if true.
|
40
|
+
def join_game(race:, name:, server_host:, port_config:, enable_feature_layer: false, interface_options: {})
|
41
|
+
interface_options ||= {}
|
42
|
+
|
43
|
+
send_request_for join_game: Api::RequestJoinGame.new(
|
44
|
+
# TODO: For Observer support, get player_index for observer,
|
45
|
+
# don't set race and pass observed_player_id: player_index
|
46
|
+
# observed_player_id: 0, # For observer
|
47
|
+
# --
|
48
|
+
race:,
|
49
|
+
player_name: name,
|
50
|
+
host_ip: server_host,
|
51
|
+
server_ports: port_config.server_port_set,
|
52
|
+
client_ports: port_config.client_port_sets,
|
53
|
+
options: Api::InterfaceOptions.new(
|
54
|
+
{
|
55
|
+
raw: true,
|
56
|
+
score: false,
|
57
|
+
feature_layer: feature_layer_interface_options(enable_feature_layer),
|
58
|
+
show_cloaked: true,
|
59
|
+
show_burrowed_shadows: true,
|
60
|
+
show_placeholders: true,
|
61
|
+
raw_affects_selection: Sc2.ladder?,
|
62
|
+
raw_crop_to_playable_area: true
|
63
|
+
}.merge!(interface_options)
|
64
|
+
)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
# Default options for feature layer, which enables it,
|
70
|
+
# but sets the map/minimap size to 1x1 for peak performance.
|
71
|
+
# A user can manually pass in it's own interface options
|
72
|
+
def feature_layer_interface_options(enabled)
|
73
|
+
return nil unless enabled
|
74
|
+
|
75
|
+
::Api::SpatialCameraSetup.new(
|
76
|
+
width: 1.0,
|
77
|
+
resolution: Api::Size2DI.new(x: 1, y: 1),
|
78
|
+
minimap_resolution: Api::Size2DI.new(x: 1, y: 1),
|
79
|
+
# width: 10.0,
|
80
|
+
# resolution: Api::Size2DI.new(x: 128, y: 128),
|
81
|
+
# minimap_resolution: Api::Size2DI.new(x: 16, y: 16),
|
82
|
+
crop_to_playable_area: true, # has no effect. minimap x and y are respected no matter what
|
83
|
+
allow_cheating_layers: false
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
protected :feature_layer_interface_options
|
88
|
+
|
89
|
+
# Single player only. Reinitializes the game with the same player setup.
|
90
|
+
def restart_game
|
91
|
+
send_request_for restart_game: Api::RequestRestartGame.new
|
92
|
+
end
|
93
|
+
|
94
|
+
# Given a replay file path or replay file contents, will start the replay
|
95
|
+
# @example
|
96
|
+
# Sc2.config do |config|
|
97
|
+
# config.version = "4.10"
|
98
|
+
# end
|
99
|
+
# Async do
|
100
|
+
# client = Sc2::ClientManager.obtain(0)
|
101
|
+
# observer = Sc2::Player::Observer.new
|
102
|
+
# observer.connect(host: client.host, port: client.port)
|
103
|
+
# pp observer.api.start_replay(
|
104
|
+
# replay_path: Pathname("./replays/test.SC2Replay").realpath
|
105
|
+
# )
|
106
|
+
# while observer.status == :in_replay
|
107
|
+
# # Step forward
|
108
|
+
# observer.api.step(1)
|
109
|
+
# # fresh observation info
|
110
|
+
# observation = observer.api.observation
|
111
|
+
# # fresh game info
|
112
|
+
# game_info = observer.api.game_info
|
113
|
+
# end
|
114
|
+
# ensure
|
115
|
+
# Sc2::ClientManager.stop(0)
|
116
|
+
# end
|
117
|
+
# @param replay_path [String] path to replay
|
118
|
+
# @param replay_data [String] alternative to file, binary string of replay_file.read
|
119
|
+
# @param map_data [String] optional binary string of SC2 map if not present in paths
|
120
|
+
# @param options [Hash] Api:RequestStartReplay options, such as disable_fog, observed_player_id, map_data
|
121
|
+
# @param [Hash] interface_options
|
122
|
+
def start_replay(replay_path: nil, replay_data: nil, map_data: nil, record_replay: true, interface_options: {}, **options)
|
123
|
+
raise Sc2::Error, "Missing replay." if replay_data.nil? && replay_path.nil?
|
124
|
+
|
125
|
+
interface_options ||= {}
|
126
|
+
send_request_for start_replay: Api::RequestStartReplay.new(
|
127
|
+
{
|
128
|
+
replay_path: replay_path.to_s,
|
129
|
+
replay_data: replay_data,
|
130
|
+
map_data: map_data,
|
131
|
+
realtime: false,
|
132
|
+
disable_fog: true,
|
133
|
+
record_replay: record_replay,
|
134
|
+
observed_player_id: 0,
|
135
|
+
options: Api::InterfaceOptions.new(
|
136
|
+
{
|
137
|
+
raw: true,
|
138
|
+
score: true,
|
139
|
+
feature_layer: feature_layer_interface_options(true),
|
140
|
+
show_cloaked: true,
|
141
|
+
show_burrowed_shadows: true,
|
142
|
+
show_placeholders: true,
|
143
|
+
raw_affects_selection: false,
|
144
|
+
raw_crop_to_playable_area: true
|
145
|
+
}.merge!(interface_options)
|
146
|
+
)
|
147
|
+
}.merge(options)
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Multiplayer only. Disconnects from a multiplayer game, equivalent to surrender. Keeps client alive.
|
152
|
+
def leave_game
|
153
|
+
send_request_for leave_game: Api::RequestLeaveGame.new
|
154
|
+
end
|
155
|
+
|
156
|
+
# Saves game to an in-memory bookmark.
|
157
|
+
def request_quick_save
|
158
|
+
send_request_for quick_save: Api::RequestQuickSave.new
|
159
|
+
end
|
160
|
+
|
161
|
+
# Loads from an in-memory bookmark.
|
162
|
+
def request_quick_load
|
163
|
+
send_request_for quick_load: Api::RequestQuickLoad.new
|
164
|
+
end
|
165
|
+
|
166
|
+
# Quits Sc2. Does not work on ladder.
|
167
|
+
def quit
|
168
|
+
send_request_for quit: Api::RequestQuit.new
|
169
|
+
end
|
170
|
+
|
171
|
+
# DURING GAME -
|
172
|
+
|
173
|
+
# // During Game
|
174
|
+
|
175
|
+
# Static data about the current game and map.
|
176
|
+
# @return [Api::ResponseGameInfo]
|
177
|
+
def game_info
|
178
|
+
send_request_for game_info: Api::RequestGameInfo.new
|
179
|
+
end
|
180
|
+
|
181
|
+
# Data about different gameplay elements. May be different for different games.
|
182
|
+
# Note that buff_id and effect_id gives worse quality data than generated from stableids (EffectId and BuffId)
|
183
|
+
# Those options are disabled by default
|
184
|
+
# @param ability_id [Boolean] to include ability data
|
185
|
+
# @param unit_type_id [Boolean] to include unit data
|
186
|
+
# @param upgrade_id [Boolean] to include upgrade data
|
187
|
+
# @param buff_id [Boolean] to get include buff data
|
188
|
+
# @param effect_id [Boolean] to get to include effect data
|
189
|
+
# @return [Api::ResponseData]
|
190
|
+
def data(ability_id: true, unit_type_id: true, upgrade_id: true, buff_id: true, effect_id: true)
|
191
|
+
send_request_for data: Api::RequestData.new(
|
192
|
+
ability_id:,
|
193
|
+
unit_type_id:,
|
194
|
+
upgrade_id:,
|
195
|
+
buff_id:,
|
196
|
+
effect_id:
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Snapshot of the current game state. Primary source for raw information
|
201
|
+
# @param game_loop [Integer] you wish to wait for (realtime only)
|
202
|
+
def observation(game_loop: nil)
|
203
|
+
# Sc2.logger.debug { "#{self.class}.#{__method__} game_loop: #{game_loop}" }
|
204
|
+
if game_loop.nil?
|
205
|
+
# Uncomment to enable multiple gc
|
206
|
+
# Async do
|
207
|
+
# result = Async do
|
208
|
+
|
209
|
+
@_cached_request_observation ||= Api::Request.new(
|
210
|
+
observation: Api::RequestObservation.new
|
211
|
+
).to_proto
|
212
|
+
@websocket.send_binary(@_cached_request_observation)
|
213
|
+
response = Api::Response.decode(@websocket.read.to_str)
|
214
|
+
|
215
|
+
if @status != response.status
|
216
|
+
@status = response.status
|
217
|
+
@listeners[StatusListener.name]&.each { _1.on_status_change(@status) }
|
218
|
+
end
|
219
|
+
|
220
|
+
response.observation
|
221
|
+
|
222
|
+
# Uncomment to enable manual GC
|
223
|
+
# end
|
224
|
+
|
225
|
+
# Async do
|
226
|
+
# # A step command is synchronous for both bots.
|
227
|
+
# # Bot A will wait for Bot B, then both get responses.
|
228
|
+
# # If we're ahead or even not, we can perform a minor GC sweep while we wait.
|
229
|
+
# # If the server notifies the other machine first
|
230
|
+
# # This smooths out unexpected hiccups and reduces overall major gc sweeps, possibly for free.
|
231
|
+
# begin
|
232
|
+
# GC.start(full_mark: false, immediate_sweep: true)
|
233
|
+
# # if rand(100).zero? # Just below every 5 seconds
|
234
|
+
# # GC.compact
|
235
|
+
# # end
|
236
|
+
# rescue
|
237
|
+
# # noop - just here for cleaner exceptions on interrupt
|
238
|
+
# end
|
239
|
+
# end
|
240
|
+
# result.wait
|
241
|
+
# end.wait
|
242
|
+
|
243
|
+
else
|
244
|
+
send_request_for observation: Api::RequestObservation.new(game_loop:)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Executes an array of [Api::Action] for a participant
|
249
|
+
# @param actions [Array<Api::Action>] to perform
|
250
|
+
# @return [Api::ResponseAction]
|
251
|
+
def action(actions)
|
252
|
+
send_request_for action: Api::RequestAction.new(
|
253
|
+
actions: actions
|
254
|
+
)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Executes an actions for an observer.
|
258
|
+
# @param actions [Array<Api::ObserverAction>]
|
259
|
+
def observer_action(actions)
|
260
|
+
# ActionObserverCameraMove camera_move = 2;
|
261
|
+
# ActionObserverCameraFollowPlayer camera_follow_player = 3;
|
262
|
+
send_request_for obs_action: Api::RequestObserverAction.new(
|
263
|
+
actions: actions
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Moves observer camera to a position at a distance
|
268
|
+
# @param world_pos [Api::Point2D]
|
269
|
+
# @param distance [Float] Distance between camera and terrain. Larger value zooms out camera. Defaults to standard camera distance if set to 0.
|
270
|
+
def observer_action_camera_move(world_pos, distance = 0)
|
271
|
+
observer_action([Api::ObserverAction.new(
|
272
|
+
camera_move: Api::ActionObserverCameraMove.new(
|
273
|
+
world_pos:,
|
274
|
+
distance:
|
275
|
+
)
|
276
|
+
)])
|
277
|
+
end
|
278
|
+
|
279
|
+
# Advances the game simulation by step_count. Not used in realtime mode.
|
280
|
+
# Only constant step size supported - subsequent requests use cache.
|
281
|
+
def step(step_count = 1)
|
282
|
+
@_cached_request_step ||= Api::Request.new(
|
283
|
+
step: Api::RequestStep.new(count: step_count)
|
284
|
+
).to_proto
|
285
|
+
send_request_and_ignore(@_cached_request_step)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Additional methods for inspecting game state. Synchronous and must wait on response
|
289
|
+
# @param pathing [Array<Api::RequestQueryPathing>]
|
290
|
+
# @param abilities [Array<Api::RequestQueryAvailableAbilities>]
|
291
|
+
# @param placements [Array<Api::RequestQueryBuildingPlacement>]
|
292
|
+
# @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
|
293
|
+
# @return [Api::ResponseQuery]
|
294
|
+
def query(pathing: nil, abilities: nil, placements: nil, ignore_resource_requirements: true)
|
295
|
+
send_request_for query: Api::RequestQuery.new(
|
296
|
+
pathing:,
|
297
|
+
abilities:,
|
298
|
+
placements:,
|
299
|
+
ignore_resource_requirements:
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Queries one or more pathing queries
|
304
|
+
# @param queries [Array<Api::RequestQueryPathing>, Api::RequestQueryPathing] one or more pathing queries
|
305
|
+
# @return [Array<Api::ResponseQueryPathing>, Api::ResponseQueryPathing] one or more results depending on input size
|
306
|
+
def query_pathings(queries)
|
307
|
+
arr_queries = queries.is_a?(Array) ? queries : [queries]
|
308
|
+
|
309
|
+
response = send_request_for query: Api::RequestQuery.new(
|
310
|
+
pathing: arr_queries
|
311
|
+
)
|
312
|
+
(arr_queries.size > 1) ? response.pathing : response.pathing.first
|
313
|
+
end
|
314
|
+
|
315
|
+
# Queries one or more ability-available checks
|
316
|
+
# @param queries [Array<Api::RequestQueryAvailableAbilities>, Api::RequestQueryAvailableAbilities] one or more pathing queries
|
317
|
+
# @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
|
318
|
+
# @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
|
319
|
+
def query_abilities(queries, ignore_resource_requirements: true)
|
320
|
+
arr_queries = queries.is_a?(Array) ? queries : [queries]
|
321
|
+
|
322
|
+
response = send_request_for query: Api::RequestQuery.new(
|
323
|
+
abilities: arr_queries,
|
324
|
+
ignore_resource_requirements:
|
325
|
+
)
|
326
|
+
(arr_queries.size > 1) ? response.abilities : response.abilities.first
|
327
|
+
end
|
328
|
+
|
329
|
+
# Queries available abilities for units
|
330
|
+
# @param unit_tags [Array<Integer>, Integer] an array of unit tags or a single tag
|
331
|
+
# @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
|
332
|
+
# @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
|
333
|
+
def query_abilities_for_unit_tags(unit_tags, ignore_resource_requirements: true)
|
334
|
+
queries = []
|
335
|
+
unit_tags = [unit_tags] unless unit_tags.is_a? Array
|
336
|
+
unit_tags.each do |unit_tag|
|
337
|
+
queries << Api::RequestQueryAvailableAbilities.new(unit_tag: unit_tag)
|
338
|
+
end
|
339
|
+
|
340
|
+
query_abilities(queries, ignore_resource_requirements:)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Queries one or more pathing queries
|
344
|
+
# @param queries [Array<Api::RequestQueryBuildingPlacement>, Api::RequestQueryBuildingPlacement] one or more placement queries
|
345
|
+
# @return [Array<Api::ResponseQueryBuildingPlacement>, Api::ResponseQueryBuildingPlacement] one or more results depending on input size
|
346
|
+
def query_placements(queries)
|
347
|
+
arr_queries = queries.is_a?(Array) ? queries : [queries]
|
348
|
+
|
349
|
+
response = query(placements: arr_queries)
|
350
|
+
|
351
|
+
(arr_queries.size > 1) ? response.placements : response.placements.first
|
352
|
+
end
|
353
|
+
|
354
|
+
# Generates a replay.
|
355
|
+
def save_replay
|
356
|
+
send_request_for save_replay: Api::RequestSaveReplay.new
|
357
|
+
end
|
358
|
+
|
359
|
+
# MapCommand does not actually gracefully trigger start/restart
|
360
|
+
# RequestMapCommand map_command = 22; // Execute a particular trigger through a string interface
|
361
|
+
|
362
|
+
# Returns metadata about a replay file. Does not load the replay.
|
363
|
+
# RequestReplayInfo replay_info = 16; //
|
364
|
+
# @param replay_path [String] path to replay
|
365
|
+
# @param replay_data [String] alternative to file, binary string of replay_file.read
|
366
|
+
# @param download_data [String] if true, ensure the data and binary are downloaded if this is an old version replay.
|
367
|
+
# @return [Api::ResponseReplayInfo]
|
368
|
+
def replay_info(replay_path: nil, replay_data: nil, download_data: false)
|
369
|
+
raise Sc2::Error, "Missing replay." if replay_data.nil? && replay_path.nil?
|
370
|
+
|
371
|
+
send_request_for replay_info: Api::RequestReplayInfo.new(
|
372
|
+
replay_path: replay_path.to_s,
|
373
|
+
replay_data: replay_data,
|
374
|
+
download_data: download_data
|
375
|
+
)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Returns directory of maps that can be played on.
|
379
|
+
# @return [Api::ResponseAvailableMaps] which has #local_map_paths and #battlenet_map_names arrays
|
380
|
+
def available_maps
|
381
|
+
send_request_for available_maps: Api::RequestAvailableMaps.new
|
382
|
+
end
|
383
|
+
|
384
|
+
# Saves binary map data to the local temp directory.
|
385
|
+
def save_map
|
386
|
+
send_request_for save_map: Api::RequestSaveMap.new
|
387
|
+
end
|
388
|
+
|
389
|
+
# Network ping for testing connection.
|
390
|
+
def ping
|
391
|
+
send_request_for ping: Api::RequestPing.new
|
392
|
+
end
|
393
|
+
|
394
|
+
# Display debug information and execute debug actions
|
395
|
+
# @param commands [Array<Api::DebugCommand>]
|
396
|
+
# @return [void]
|
397
|
+
def debug(commands)
|
398
|
+
send_request_for debug: Api::RequestDebug.new(
|
399
|
+
debug: commands
|
400
|
+
)
|
401
|
+
end
|
402
|
+
|
403
|
+
# Sends request for type and returns response that type, i.e.
|
404
|
+
# send_request_for(observation: RequestObservation)
|
405
|
+
# Is identical to
|
406
|
+
# send_request(
|
407
|
+
# Api::Request.new(observation: RequestObservation)
|
408
|
+
# )[:observation]
|
409
|
+
def send_request_for(**kwargs)
|
410
|
+
response = send_request(Api::Request.new(kwargs))
|
411
|
+
response[kwargs.keys.first.to_s]
|
412
|
+
end
|
413
|
+
|
414
|
+
private
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sc2
|
4
|
+
class Connection
|
5
|
+
# Callbacks when game status changes
|
6
|
+
module StatusListener
|
7
|
+
# Called when game status changes
|
8
|
+
# @param status [:launched, :in_game, :in_replay, :ended, :quit, :unknown] game state, i.e. :in_game, :ended, :launched
|
9
|
+
# noinspection
|
10
|
+
def on_status_change(status)
|
11
|
+
Sc2.logger.debug { "#{self.class}.#{__method__} #{status}" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|