sc2ai 0.0.0.pre → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/data/data.json +1 -0
  3. data/data/data_readable.json +22842 -0
  4. data/data/sc2ai/protocol/common.proto +59 -0
  5. data/data/sc2ai/protocol/data.proto +120 -0
  6. data/data/sc2ai/protocol/debug.proto +127 -0
  7. data/data/sc2ai/protocol/error.proto +221 -0
  8. data/data/sc2ai/protocol/query.proto +55 -0
  9. data/data/sc2ai/protocol/raw.proto +202 -0
  10. data/data/sc2ai/protocol/sc2api.proto +718 -0
  11. data/data/sc2ai/protocol/score.proto +108 -0
  12. data/data/sc2ai/protocol/spatial.proto +115 -0
  13. data/data/sc2ai/protocol/ui.proto +145 -0
  14. data/data/setup/setup.SC2Map +0 -0
  15. data/data/setup/setup.SC2Replay +0 -0
  16. data/data/stableid.json +35730 -0
  17. data/data/versions.json +554 -0
  18. data/exe/sc2ai +35 -0
  19. data/lib/docker_build/Dockerfile.ruby +74 -0
  20. data/lib/docker_build/docker-compose-base-image.yml +10 -0
  21. data/lib/docker_build/docker-compose-ladderzip.yml +9 -0
  22. data/lib/sc2ai/api/ability_id.rb +1644 -0
  23. data/lib/sc2ai/api/buff_id.rb +306 -0
  24. data/lib/sc2ai/api/data.rb +101 -0
  25. data/lib/sc2ai/api/effect_id.rb +20 -0
  26. data/lib/sc2ai/api/tech_tree.rb +83 -0
  27. data/lib/sc2ai/api/tech_tree_data.rb +2338 -0
  28. data/lib/sc2ai/api/unit_type_id.rb +2022 -0
  29. data/lib/sc2ai/api/upgrade_id.rb +310 -0
  30. data/lib/sc2ai/cli/cli.rb +175 -0
  31. data/lib/sc2ai/cli/ladderzip.rb +154 -0
  32. data/lib/sc2ai/cli/new.rb +88 -0
  33. data/lib/sc2ai/configuration.rb +145 -0
  34. data/lib/sc2ai/connection/connection_listener.rb +30 -0
  35. data/lib/sc2ai/connection/requests.rb +417 -0
  36. data/lib/sc2ai/connection/status_listener.rb +15 -0
  37. data/lib/sc2ai/connection.rb +146 -0
  38. data/lib/sc2ai/local_play/client/configurable_options.rb +115 -0
  39. data/lib/sc2ai/local_play/client.rb +159 -0
  40. data/lib/sc2ai/local_play/client_manager.rb +70 -0
  41. data/lib/sc2ai/local_play/map_file.rb +48 -0
  42. data/lib/sc2ai/local_play/match.rb +184 -0
  43. data/lib/sc2ai/overrides/array.rb +14 -0
  44. data/lib/sc2ai/overrides/async/process/child.rb +31 -0
  45. data/lib/sc2ai/overrides/kernel.rb +33 -0
  46. data/lib/sc2ai/paths.rb +294 -0
  47. data/lib/sc2ai/player/actions.rb +386 -0
  48. data/lib/sc2ai/player/debug.rb +224 -0
  49. data/lib/sc2ai/player/game_state.rb +131 -0
  50. data/lib/sc2ai/player/geometry.rb +766 -0
  51. data/lib/sc2ai/player/previous_state.rb +49 -0
  52. data/lib/sc2ai/player/units.rb +337 -0
  53. data/lib/sc2ai/player.rb +661 -0
  54. data/lib/sc2ai/ports.rb +152 -0
  55. data/lib/sc2ai/protocol/_meta_documentation.rb +39 -0
  56. data/lib/sc2ai/protocol/common_pb.rb +43 -0
  57. data/lib/sc2ai/protocol/data_pb.rb +47 -0
  58. data/lib/sc2ai/protocol/debug_pb.rb +56 -0
  59. data/lib/sc2ai/protocol/error_pb.rb +36 -0
  60. data/lib/sc2ai/protocol/extensions/color.rb +20 -0
  61. data/lib/sc2ai/protocol/extensions/point.rb +23 -0
  62. data/lib/sc2ai/protocol/extensions/point_2_d.rb +26 -0
  63. data/lib/sc2ai/protocol/extensions/position.rb +202 -0
  64. data/lib/sc2ai/protocol/extensions/power_source.rb +19 -0
  65. data/lib/sc2ai/protocol/extensions/unit.rb +489 -0
  66. data/lib/sc2ai/protocol/query_pb.rb +47 -0
  67. data/lib/sc2ai/protocol/raw_pb.rb +57 -0
  68. data/lib/sc2ai/protocol/sc2api_pb.rb +130 -0
  69. data/lib/sc2ai/protocol/score_pb.rb +40 -0
  70. data/lib/sc2ai/protocol/spatial_pb.rb +48 -0
  71. data/lib/sc2ai/protocol/ui_pb.rb +56 -0
  72. data/lib/sc2ai/unit_group/action_ext.rb +74 -0
  73. data/lib/sc2ai/unit_group/filter_ext.rb +379 -0
  74. data/lib/sc2ai/unit_group.rb +277 -0
  75. data/lib/sc2ai/version.rb +2 -1
  76. data/lib/sc2ai.rb +93 -2
  77. data/lib/templates/ladderzip/bin/ladder.tt +23 -0
  78. data/lib/templates/new/.ladderignore +20 -0
  79. data/lib/templates/new/Gemfile.tt +7 -0
  80. data/lib/templates/new/api/common.proto +59 -0
  81. data/lib/templates/new/api/data.proto +120 -0
  82. data/lib/templates/new/api/debug.proto +127 -0
  83. data/lib/templates/new/api/error.proto +221 -0
  84. data/lib/templates/new/api/query.proto +55 -0
  85. data/lib/templates/new/api/raw.proto +202 -0
  86. data/lib/templates/new/api/sc2api.proto +718 -0
  87. data/lib/templates/new/api/score.proto +108 -0
  88. data/lib/templates/new/api/spatial.proto +115 -0
  89. data/lib/templates/new/api/ui.proto +145 -0
  90. data/lib/templates/new/boot.rb.tt +6 -0
  91. data/lib/templates/new/my_bot.rb.tt +23 -0
  92. data/lib/templates/new/run_example_match.rb.tt +14 -0
  93. data/sc2ai.gemspec +80 -0
  94. 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
@@ -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