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,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../connection/status_listener"
|
4
|
+
|
5
|
+
module Sc2
|
6
|
+
# Runs a match using a map and player configuration
|
7
|
+
class Match
|
8
|
+
include Sc2::Connection::StatusListener
|
9
|
+
|
10
|
+
# Callback when game status changes
|
11
|
+
def on_status_change(status)
|
12
|
+
Sc2.logger.debug { "Status from Match: #{status}" }
|
13
|
+
|
14
|
+
# if status == :ended
|
15
|
+
# # Go through each player, looking for result if we don't have one.
|
16
|
+
# api_players.each do |player|
|
17
|
+
# Sc2.logger.debug { "TODO: Get results for players" }
|
18
|
+
# # result = player.result
|
19
|
+
# Sc2.logger.debug { "Leaving Game and Disconnecting players" }
|
20
|
+
# player.leave_game
|
21
|
+
# player.disconnect
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: DEFINE REALTIME AS A PARAM
|
27
|
+
|
28
|
+
# @!attribute players Sets the Player(s) for the match
|
29
|
+
# @return [Array<Sc2::Player>] an array of assigned players (ai,bots,humans,observers)
|
30
|
+
attr_accessor :players
|
31
|
+
|
32
|
+
# @!attribute map Sets the Map for the match
|
33
|
+
# @return [Sc2::MapFile] the Map for the match
|
34
|
+
attr_reader :map
|
35
|
+
|
36
|
+
# @param players [Array<Sc2::Player>]
|
37
|
+
# @param map [String, Sc2::MapFile] String path or map name, or Map
|
38
|
+
# @return [Sc2::Match]
|
39
|
+
def initialize(players:, map: nil)
|
40
|
+
@players = players || []
|
41
|
+
@map = if map.is_a?(String)
|
42
|
+
MapFile.new(map.to_s)
|
43
|
+
else
|
44
|
+
map
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Validates a runnable match and raises an error if invalid
|
49
|
+
# @return [void]
|
50
|
+
# @raise [Sc2:Error]
|
51
|
+
def validate
|
52
|
+
@players.select! { |player| player.is_a?(Player) }
|
53
|
+
raise Error, "player count greater than 1 expected" unless @players.length >= 2
|
54
|
+
|
55
|
+
raise Error, "invalid map" if !@map.is_a?(MapFile) || @map.path.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Connects players to instances, creates a game and joins everyone to play!
|
59
|
+
# @return [void]
|
60
|
+
def run
|
61
|
+
validate
|
62
|
+
Sc2.logger.debug { "Connecting players to client..." }
|
63
|
+
|
64
|
+
# Holds the game process and finishes when a status triggers it to end
|
65
|
+
Async do |run_task|
|
66
|
+
connect_players
|
67
|
+
setup_player_hooks
|
68
|
+
|
69
|
+
player_host.create_game(map:, players: @players)
|
70
|
+
|
71
|
+
api_players.each_with_index do |player, player_index|
|
72
|
+
run_task.async do
|
73
|
+
player.join_game(
|
74
|
+
server_host: ClientManager.get(player_index).host,
|
75
|
+
port_config:
|
76
|
+
)
|
77
|
+
|
78
|
+
result = player.play
|
79
|
+
Sc2.logger.debug { "Player(#{player_index}) Result: #{result}" }
|
80
|
+
autosave_replay(player)
|
81
|
+
ensure
|
82
|
+
Sc2.logger.debug { "Game over, disconnect players." }
|
83
|
+
# Suppress interrupt errors #$stderr.reopen File.new(File::NULL, "w")
|
84
|
+
player.disconnect
|
85
|
+
ClientManager.stop(player_index) # unless keep_clients_alive
|
86
|
+
end
|
87
|
+
end
|
88
|
+
rescue
|
89
|
+
# no op - clean exit from game may cause ws disconnection error
|
90
|
+
end.wait
|
91
|
+
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Saves the replay from the player's perspective.
|
98
|
+
# Requires active client and connection.
|
99
|
+
def autosave_replay(player)
|
100
|
+
safe_player_name = player.name.gsub(/\s*[^A-Za-z0-9.-]\s*/, "_").downcase
|
101
|
+
|
102
|
+
response = player.api.save_replay
|
103
|
+
path = Pathname(Paths.bot_data_replay_dir).join("autosave-#{safe_player_name}.SC2Replay")
|
104
|
+
f = File.new(path, "wb:ASCII-8BIT")
|
105
|
+
f.write(response.data)
|
106
|
+
f.close
|
107
|
+
end
|
108
|
+
|
109
|
+
# Gets a PortConfig
|
110
|
+
# @return [Sc2::PortConfig] port configuration based on players
|
111
|
+
def port_config
|
112
|
+
# Sc2.logger.debug { "Get port config..." }
|
113
|
+
# Detect open ports
|
114
|
+
@port_config = Ports.port_config_auto(num_players: api_players.length)
|
115
|
+
rescue
|
116
|
+
# Fall back to increment method
|
117
|
+
@port_config = Ports.port_config_basic(start_port: 5000, num_players: api_players.length)
|
118
|
+
end
|
119
|
+
|
120
|
+
# def prepare_clients
|
121
|
+
# Async do |task|
|
122
|
+
# api_players.each_with_index do |player, i|
|
123
|
+
# Sc2.logger.debug { "Obtain client for player #{i}: #{player.class}, #{player.name}"
|
124
|
+
# ClientManager.obtain(i)
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
|
129
|
+
# Gets a Sc2 client from Sc2::ClientManager and connects them
|
130
|
+
def connect_players
|
131
|
+
# Depending on number of players, api has different ready states
|
132
|
+
ready_statuses = (api_players.size > 1) ? [:launched] : %i[launched ready]
|
133
|
+
|
134
|
+
# For each player, attempt to connect and retry once after closing Sc2 in worst-case
|
135
|
+
api_players.each_with_index do |player, i|
|
136
|
+
1.upto(max_retires = 2) do |attempt|
|
137
|
+
Sc2.logger.debug { "Player(#{i}) Obtain client for: #{player.class}, #{player.name}" }
|
138
|
+
client = ClientManager.obtain(i)
|
139
|
+
sleep(8) if ENV["SC2PF"] == Paths::PF_WINDOWS
|
140
|
+
Sc2.logger.debug { "Player(#{i}) Connect to client: #{client.host}:#{client.port}" }
|
141
|
+
player.connect(host: client.host, port: client.port)
|
142
|
+
Sc2.logger.debug { "Player(#{i}) Connected." }
|
143
|
+
Sc2.logger.debug { "Player(#{i}) Status is: #{player.status}" }
|
144
|
+
break if ready_statuses.include?(player.status)
|
145
|
+
|
146
|
+
Sc2.logger.debug { "Player(#{i}) Attempt to leave game..." }
|
147
|
+
player.leave_game
|
148
|
+
break if ready_statuses.include?(player.status)
|
149
|
+
|
150
|
+
raise Error, "Player(#{i}) Unable to get client" unless attempt < max_retires
|
151
|
+
|
152
|
+
Sc2.logger.debug { "Player(#{i}) Leave failed. Retry by stopping client..." }
|
153
|
+
player.disconnect
|
154
|
+
ClientManager.stop(i)
|
155
|
+
next
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Configure hooks
|
161
|
+
def setup_player_hooks
|
162
|
+
api_players.each do |player|
|
163
|
+
# Clean surrender locally. Lapizistik taught me bad things.
|
164
|
+
def player.leave_game
|
165
|
+
debug_end_game(end_result: :Surrender)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns a list of players which requires an Sc2 instance
|
171
|
+
# @return [Array<Sc2::Player>] players which requires_client?
|
172
|
+
def api_players
|
173
|
+
players.select(&:requires_client?)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the first player which requires an Api connection as the host
|
177
|
+
# @return [Sc2::Player] host
|
178
|
+
def player_host
|
179
|
+
raise Sc2::Error, "No host player found. API players are empty." if api_players.nil? || api_players.empty?
|
180
|
+
# noinspection RubyMismatchedReturnType
|
181
|
+
api_players.first
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Array extensions
|
2
|
+
class Array
|
3
|
+
# Turns an Array of Api::Unit into a Sc2::UnitGroup
|
4
|
+
# @return [Sc2::UnitGroup] array converted to a unit group
|
5
|
+
def to_unit_group
|
6
|
+
Sc2::UnitGroup.new(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Creates a Point2D from 0,1 as x,y
|
10
|
+
# @return [Api::Point2D]
|
11
|
+
def to_p2d
|
12
|
+
Api::Point2D.new(x: at(0), y: at(1))
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Overrides process spawn for windows pgroup
|
2
|
+
|
3
|
+
require "async/process"
|
4
|
+
|
5
|
+
module Async
|
6
|
+
module Process
|
7
|
+
class Child
|
8
|
+
def initialize(*args, **options)
|
9
|
+
# Setup a cross-thread notification pipe - nio4r can't monitor pids unfortunately:
|
10
|
+
pipe = ::IO.pipe
|
11
|
+
@input = Async::IO::Generic.new(pipe.first)
|
12
|
+
@output = pipe.last
|
13
|
+
|
14
|
+
@exit_status = nil
|
15
|
+
|
16
|
+
if Gem.win_platform?
|
17
|
+
options[:new_pgroup] = true
|
18
|
+
else
|
19
|
+
options[:pgroup] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
@pid = ::Process.spawn(*args, **options)
|
23
|
+
|
24
|
+
@thread = Thread.new do
|
25
|
+
_, @exit_status = ::Process.wait2(@pid)
|
26
|
+
@output.close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @private
|
4
|
+
# Patched from: rails/activesupport/lib/active_support/core_ext/kernel/reporting.rb
|
5
|
+
# https://github.com/rails/rails/blob/04972d9b9ef60796dc8f0917817b5392d61fcf09/activesupport/lib/active_support/core_ext/kernel/reporting.rb#L26
|
6
|
+
|
7
|
+
# Kernel extensions
|
8
|
+
module Kernel
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Sets $VERBOSE to +nil+ for the duration of the block and back to its original
|
12
|
+
# value afterwards.
|
13
|
+
#
|
14
|
+
# silence_warnings do
|
15
|
+
# value = noisy_call # no warning voiced
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# noisy_call # warning voiced
|
19
|
+
def silence_warnings(&)
|
20
|
+
with_warnings(nil, &)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets $VERBOSE for the duration of the block and back to its original
|
24
|
+
# value afterwards.
|
25
|
+
# noinspection RubyGlobalVariableNamingConvention
|
26
|
+
def with_warnings(flag)
|
27
|
+
old_verbose = $VERBOSE
|
28
|
+
$VERBOSE = flag
|
29
|
+
yield
|
30
|
+
ensure
|
31
|
+
$VERBOSE = old_verbose
|
32
|
+
end
|
33
|
+
end
|
data/lib/sc2ai/paths.rb
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module Sc2
|
7
|
+
# Helps determine common paths to sc2 install dir, executable and maps.
|
8
|
+
# It maintains some semblance of compatibility with python-sc2 config
|
9
|
+
#
|
10
|
+
# ENV['SC2PATH'] can be set manually to Starcraft 2 base directory for Linux
|
11
|
+
# ENV['SC2PF'] can and should be manually set to "WineLinux" when running Wine
|
12
|
+
# Credit to Hannes, Sean and Burny for setting the standard
|
13
|
+
class Paths
|
14
|
+
# Platform name for Windows
|
15
|
+
PF_WINDOWS = "Windows"
|
16
|
+
# Platform name for WSL1
|
17
|
+
PF_WSL1 = "WSL1"
|
18
|
+
# Platform name for WSL2
|
19
|
+
PF_WSL2 = "WSL2"
|
20
|
+
# Platform name macOS
|
21
|
+
PF_DARWIN = "Darwin"
|
22
|
+
# Platform name Linux
|
23
|
+
PF_LINUX = "Linux"
|
24
|
+
# Platform name for Wine
|
25
|
+
PF_WINE = "WineLinux"
|
26
|
+
|
27
|
+
# Where within user directory to locate ExecuteInfo.txt
|
28
|
+
BASE_DIR = {
|
29
|
+
PF_WINDOWS => "C:/Program Files (x86)/StarCraft II",
|
30
|
+
PF_WSL1 => "/mnt/c/Program Files (x86)/StarCraft II",
|
31
|
+
PF_WSL2 => "/mnt/c/Program Files (x86)/StarCraft II",
|
32
|
+
PF_DARWIN => "/Applications/StarCraft II",
|
33
|
+
PF_LINUX => "~/StarCraftII",
|
34
|
+
PF_WINE => "~/.wine/drive_c/Program Files (x86)/StarCraft II"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
# Where within user directory to locate ExecuteInfo.txt
|
38
|
+
EXEC_INFO_PATH = {
|
39
|
+
PF_WINDOWS => "Documents/StarCraft II/ExecuteInfo.txt",
|
40
|
+
PF_WSL1 => "Documents/StarCraft II/ExecuteInfo.txt",
|
41
|
+
PF_WSL2 => "Documents/StarCraft II/ExecuteInfo.txt",
|
42
|
+
PF_DARWIN => "Library/Application Support/Blizzard/StarCraft II/ExecuteInfo.txt",
|
43
|
+
PF_LINUX => nil,
|
44
|
+
PF_WINE => nil
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
# Path helper for finding executable
|
48
|
+
BIN_PATH = {
|
49
|
+
PF_WINDOWS => "SC2_x64.exe",
|
50
|
+
PF_WSL1 => "SC2_x64.exe",
|
51
|
+
PF_WSL2 => "SC2_x64.exe",
|
52
|
+
PF_DARWIN => "SC2.app/Contents/MacOS/SC2",
|
53
|
+
PF_LINUX => "SC2_x64",
|
54
|
+
PF_WINE => "SC2_x64.exe"
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
# Working directory sub-folder per platform.
|
58
|
+
# Used when spawning client process on some platforms.
|
59
|
+
WORKING_DIR = {
|
60
|
+
PF_WINDOWS => "Support64",
|
61
|
+
PF_WSL1 => "Support64",
|
62
|
+
PF_WSL2 => "Support64",
|
63
|
+
PF_DARWIN => nil,
|
64
|
+
PF_LINUX => nil,
|
65
|
+
PF_WINE => "Support64"
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
class << self
|
69
|
+
# An array of available platforms
|
70
|
+
# @return [Array<String>] an array of valid platforms
|
71
|
+
def platforms
|
72
|
+
[PF_WINDOWS, PF_WSL1, PF_WSL2, PF_DARWIN, PF_LINUX, PF_WINE]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Bucketed platform names follows convention
|
76
|
+
# Uses ENV['SC2PF'] if configured. This is the only way to set "WineLinux"
|
77
|
+
# @return ["Windows","WSL1","WSL2","Darwin","Linux","WineLinux"] string platform name
|
78
|
+
def platform
|
79
|
+
return ENV.fetch("SC2PF", nil) unless ENV["SC2PF"].to_s.strip.empty?
|
80
|
+
|
81
|
+
if Gem.win_platform?
|
82
|
+
ENV["SC2PF"] = PF_WINDOWS
|
83
|
+
elsif Gem::Platform.local.os == "darwin"
|
84
|
+
ENV["SC2PF"] = PF_DARWIN
|
85
|
+
elsif Gem::Platform.local.os == "linux"
|
86
|
+
ENV["SC2PF"] = PF_LINUX
|
87
|
+
if wsl?
|
88
|
+
ENV["SC2PF"] = wsl2? ? PF_WSL2 : PF_WSL1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
unless ENV.fetch("SC2PF", false)
|
93
|
+
Sc2.logger.warn "unknown platform detected #{Gem::Platform.local.os}. manually set ENV['SC2PF']"
|
94
|
+
end
|
95
|
+
ENV.fetch("SC2PF", nil)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Attempts to auto-detect the user's install directory via ExecuteInfo.txt
|
99
|
+
# SC2PATH is required on WineLinux and Linux
|
100
|
+
# @return [String] path
|
101
|
+
def install_dir
|
102
|
+
if ENV.fetch("SC2PATH", false)
|
103
|
+
# Use if set via environment
|
104
|
+
path = ENV.fetch("SC2PATH")
|
105
|
+
else
|
106
|
+
# Otherwise try read from ExecuteInfo.txt
|
107
|
+
path = read_exec_info
|
108
|
+
if path.to_s.strip.empty?
|
109
|
+
# If not present in ExecuteInfo, try use sensible default
|
110
|
+
path = BASE_DIR[platform]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
path = File.expand_path(path) if platform != PF_WINDOWS
|
114
|
+
|
115
|
+
ENV["SC2PATH"] = path
|
116
|
+
end
|
117
|
+
|
118
|
+
# Replays directory based on install_dir
|
119
|
+
# @return [String] replays directory
|
120
|
+
def replay_dir
|
121
|
+
Pathname(install_dir).join("Replays").to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
# Maps directory based on install_dir
|
125
|
+
# @return [String] maps directory
|
126
|
+
def maps_dir
|
127
|
+
# Use Maps if exists, or as fallback if alternative not found
|
128
|
+
dir = Pathname(install_dir).join("Maps")
|
129
|
+
return dir.to_s if dir.exist?
|
130
|
+
|
131
|
+
# Use lowercase "maps" only if it exists
|
132
|
+
dir_alt = Pathname(install_dir).join("maps")
|
133
|
+
dir = dir_alt if dir_alt.exist?
|
134
|
+
|
135
|
+
dir.to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
# Working directory if required by platform
|
139
|
+
# Some platforms the correct working directory for launch to find linked libraries
|
140
|
+
# @return [String,nil] string path or nil if not required
|
141
|
+
def exec_working_dir
|
142
|
+
cwd = WORKING_DIR[platform]
|
143
|
+
return nil if cwd.nil?
|
144
|
+
|
145
|
+
Pathname(install_dir).join(cwd).to_s
|
146
|
+
end
|
147
|
+
|
148
|
+
# Gets a path to latest Sc2 executable, or specific build's executable if defined
|
149
|
+
# @param base_build [Integer, nil] version to use if installed
|
150
|
+
# @return [String, nil] path to executable
|
151
|
+
# noinspection RubyMismatchedReturnType
|
152
|
+
def executable(base_build: nil)
|
153
|
+
# Use base_build if supplied
|
154
|
+
unless base_build.nil?
|
155
|
+
path = Pathname.new(version_dir).join("Base#{base_build}")
|
156
|
+
return path.join(BIN_PATH[platform]).to_s if path.exist?
|
157
|
+
end
|
158
|
+
# Get highest build number for folders /^Base\d$/
|
159
|
+
pattern = /Base(\d+)$/
|
160
|
+
path = Pathname.new(version_dir).glob("Base*")
|
161
|
+
.max { |a, b| a.to_s.match(pattern)[1] <=> b.to_s.match(pattern)[1] }
|
162
|
+
return path.join(BIN_PATH[platform]).to_s if path&.exist?
|
163
|
+
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns project root directory
|
168
|
+
# @return [String] path because bundler does.
|
169
|
+
def project_root_dir
|
170
|
+
return Bundler.root.to_s if defined?(Bundler)
|
171
|
+
|
172
|
+
Dir.pwd
|
173
|
+
end
|
174
|
+
|
175
|
+
# For local and ladder play, your files modified at runtime should live in ./data
|
176
|
+
# @return [String] path to ./data
|
177
|
+
def bot_data_dir
|
178
|
+
dir = Pathname("./data")
|
179
|
+
dir.mkdir unless dir.exist?
|
180
|
+
dir.to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
# Returns the local replay folder
|
184
|
+
# For local play, your replay filse are saved to ./replays
|
185
|
+
# @return [String] path to ./replays
|
186
|
+
def bot_data_replay_dir
|
187
|
+
dir = Pathname("./replays")
|
188
|
+
dir.mkdir unless dir.exist?
|
189
|
+
dir.to_s
|
190
|
+
end
|
191
|
+
|
192
|
+
# @private
|
193
|
+
# Root directory of gem itself for other bundled files
|
194
|
+
# @return [Pathname] path to gem folder which contains lib/,data/,etc.
|
195
|
+
def gem_root
|
196
|
+
Pathname.new(__dir__.to_s).parent.parent.expand_path
|
197
|
+
end
|
198
|
+
|
199
|
+
# @private
|
200
|
+
# Directory of template folders
|
201
|
+
# @return [Pathname] path to where template directories live, i.e. "new" and "ladder"
|
202
|
+
def template_root
|
203
|
+
Pathname.new(__dir__.to_s).parent.join("templates").expand_path
|
204
|
+
end
|
205
|
+
|
206
|
+
# @private
|
207
|
+
# Path to shipped versions.json
|
208
|
+
# @return [Pathname] path
|
209
|
+
def gem_data_versions_path
|
210
|
+
Pathname.new(gem_root).join("data", "versions.json")
|
211
|
+
end
|
212
|
+
|
213
|
+
# Gets system temp directory and creates /sc2ai
|
214
|
+
# @return [String] temporary directory for use with -tempDir
|
215
|
+
def generate_temp_dir
|
216
|
+
temp_dir = Pathname(Dir.tmpdir).join("sc2ai").mkpath
|
217
|
+
temp_dir.to_s
|
218
|
+
end
|
219
|
+
|
220
|
+
# Checks if running WSL
|
221
|
+
# @return [Boolean]
|
222
|
+
def wsl?
|
223
|
+
Gem::Platform.local.os == "linux" && (ENV.fetch("IS_WSL", false) || ENV.fetch("WSL_DISTRO_NAME", false))
|
224
|
+
end
|
225
|
+
|
226
|
+
# Checks if running WSL2 specifically on top of wsl? being true
|
227
|
+
# @return [Boolean]
|
228
|
+
def wsl2?
|
229
|
+
wsl? && ENV.fetch("WSL_INTEROP", false)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Convert a path like "C:\\path\\To a\\Location" to "/mnt/c/path/To a/Location"
|
233
|
+
# @param path [String]
|
234
|
+
# @return [String] path converted
|
235
|
+
def win_to_wsl(path:)
|
236
|
+
"/mnt/" + path.sub(/\A([A-Z])(:)/) { ::Regexp.last_match(1).to_s.downcase }.tr("\\", "/")
|
237
|
+
end
|
238
|
+
|
239
|
+
# Convert a path like "/mnt/c/path/To a/Location" to "C:\\path\\To a\\Location"
|
240
|
+
# @param path [String]
|
241
|
+
# @return [String] path converted
|
242
|
+
def wsl_to_win(path:)
|
243
|
+
path.sub(%r{\A/mnt/([a-z])}) { "#{::Regexp.last_match(1).to_s.upcase}:" }.tr("/", "\\")
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# Versions directory based on install_dir
|
249
|
+
# @return [String] install_dir + "/Versions"
|
250
|
+
def version_dir
|
251
|
+
Pathname(install_dir).join("Versions").to_s
|
252
|
+
end
|
253
|
+
|
254
|
+
# Loads version data
|
255
|
+
# @return [JSON] versions.json
|
256
|
+
def version_json
|
257
|
+
JSON.load_file(Pathname(__dir__.to_s).join("../data/versions.json"))
|
258
|
+
end
|
259
|
+
|
260
|
+
# Reads contents for ExecuteInfo.txt
|
261
|
+
# @return [String] contents of exec_info or empty string
|
262
|
+
def read_exec_info
|
263
|
+
return "" unless EXEC_INFO_PATH[platform]
|
264
|
+
|
265
|
+
# Read path ExecuteInfo.txt
|
266
|
+
exec_info_path = File.join(Dir.home, EXEC_INFO_PATH[platform])
|
267
|
+
content = File.read(exec_info_path)
|
268
|
+
dir = content.match(/ = (.*)Versions/)[1].to_s
|
269
|
+
|
270
|
+
# Platform-specific patches
|
271
|
+
if platform == PF_WSL1 || platform == PF_WSL2
|
272
|
+
dir = win_to_wsl(path: dir)
|
273
|
+
raise Pathname(dir).inspect
|
274
|
+
elsif PF_WINDOWS
|
275
|
+
dir = dir.tr("\\", "/")
|
276
|
+
end
|
277
|
+
|
278
|
+
dir = dir.chomp("\\").chomp("/")
|
279
|
+
return dir if File.exist?(dir)
|
280
|
+
rescue
|
281
|
+
""
|
282
|
+
else
|
283
|
+
""
|
284
|
+
end
|
285
|
+
|
286
|
+
# # Access singleton via Paths.instance
|
287
|
+
# private
|
288
|
+
#
|
289
|
+
# def instance
|
290
|
+
# @_instance ||= new(self.platform)
|
291
|
+
# end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|