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