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,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/io/stream"
5
+ require "async/http/endpoint"
6
+ require "async/websocket"
7
+ require_relative "connection/requests"
8
+
9
+ module Sc2
10
+ # Manages client connection to the Api
11
+ class Connection
12
+ # Api requests
13
+ include Sc2::Connection::Requests
14
+
15
+ attr_accessor :host, :port, :websocket
16
+
17
+ # Last known game status, i.e. :launched, :ended, :unknown
18
+ # :launched // Game has been launch and is not yet doing anything.
19
+ # :init_game // Create game has been called, and the host is awaiting players.
20
+ # :in_game // In a single or multiplayer game.
21
+ # :in_replay // In a replay.
22
+ # :ended // Game has ended, can still request game info, but ready for a new game.
23
+ # :quit // Application is shutting down.
24
+ # :unknown // Should not happen, but indicates an error if it occurs.
25
+ # @return [Symbol] game status
26
+ attr_reader :status
27
+
28
+ # @!attribute listeners
29
+ # @return [Hash<String,Array>] of callbacks
30
+ attr_reader :listeners
31
+
32
+ # @param host [String]
33
+ # @param port [Integer]
34
+ # #param [Sc2::CallbackListener] listener
35
+ def initialize(host:, port:)
36
+ @host = host
37
+ @port = port
38
+ @listeners = {}
39
+ @websocket = nil
40
+ @status = :unknown
41
+ # Only allow one request at a time.
42
+ # TODO: Since it turns out the client websocket can only handle 1 request at a time, we don't stricly need Async
43
+ @scheduler = Async::Semaphore.new(1)
44
+ end
45
+
46
+ # Attempts to connect for a period of time, ignoring errors nad performing on_* callbacks
47
+ # @return [void]
48
+ def connect
49
+ attempt = 1
50
+ Sc2.logger.debug { "Waiting for client..." } if (attempt % 5).zero?
51
+
52
+ begin
53
+ @websocket = Async::WebSocket::Client.connect(endpoint) # , handler: Sc2::Connection::Connection)
54
+ @listeners[ConnectionListener.name]&.each { _1.on_connected(self) }
55
+ # do initial ping to ensure status is set and connection is working
56
+ response_ping = ping
57
+ Sc2.logger.debug { "Game version: #{response_ping.game_version}" }
58
+ rescue Errno::ECONNREFUSED
59
+ raise Error, "Connection timeout. Max retry exceeded." unless (attempt += 1) < 30 # 30s attempts
60
+
61
+ @listeners[ConnectionListener.name]&.each { _1.on_connection_waiting(self) }
62
+ sleep(1)
63
+ retry
64
+ rescue Error => e
65
+ Sc2.logger.error "#{e.class}: #{e.message}"
66
+ @listeners[ConnectionListener.name]&.each { _1.on_disconnect(self) }
67
+ end
68
+ nil
69
+ end
70
+
71
+ # Closes Connection to client
72
+ # @return [void]
73
+ def close
74
+ @websocket&.close
75
+ @listeners[ConnectionListener.name]&.each { _1.on_disconnect(self) }
76
+ end
77
+
78
+ # Add a listener of specific callback type
79
+ # @param listener [Object]
80
+ # @param klass [Module<Sc2::Connection::ConnectionListener>,Module<Sc2::Connection::StatusListener>]
81
+ def add_listener(listener, klass:)
82
+ @listeners[klass.to_s] ||= []
83
+ @listeners[klass.to_s].push(listener)
84
+ end
85
+
86
+ # Removes a listener of specific callback type
87
+ # @param listener [Object]
88
+ # @param klass [Module<Sc2::Connection::ConnectionListener>,Module<Sc2::Connection::StatusListener>]
89
+ def remove_listener(listener, klass:)
90
+ @listeners[klass.to_s].delete(listener)
91
+ end
92
+
93
+ # ---------------------------------------------------------
94
+ # Sends a request synchronously and returns Api::Response type
95
+ # @return [Api::Response] response
96
+ def send_request(request)
97
+ @scheduler.async do |_task|
98
+ # r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) #debug
99
+ # name = request.is_a?(String) ? request : request.request #debug
100
+ request = request.to_proto unless request.is_a?(String)
101
+ @websocket.send_binary(request)
102
+ response = Api::Response.decode(@websocket.read.to_str)
103
+
104
+ if @status != response.status
105
+ @status = response.status
106
+ @listeners[StatusListener.name]&.each { _1.on_status_change(@status) }
107
+ end
108
+
109
+ # Sc2.logger.debug { response }
110
+ # puts "#{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000} - #{name}" #debug
111
+ response
112
+ end.wait
113
+ rescue EOFError => e
114
+ Sc2.logger.error e
115
+ close
116
+ end
117
+
118
+ # Sends and ignores response.
119
+ # Meant to be used as optimization for RequestStep.
120
+ # No other command sends and ignores.
121
+ # Expects request to be to_proto'd already
122
+ # @return [void]
123
+ def send_request_and_ignore(request)
124
+ @scheduler.async do |_task|
125
+ @websocket.send_binary(request)
126
+ while @websocket.read_frame
127
+ if @websocket.frames.last&.finished?
128
+ @websocket.frames = []
129
+ break
130
+ end
131
+ end
132
+ end.wait
133
+
134
+ nil
135
+ end
136
+
137
+ private
138
+
139
+ attr_writer :status, :listeners
140
+
141
+ # @return [HTTP::Endpoint] websocket url for establishing protobuf connection
142
+ def endpoint
143
+ Async::HTTP::Endpoint.parse("ws://#{@host}:#{@port}/sc2api")
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sc2
4
+ class Client
5
+ # Attributes shared by Configuration and Client
6
+ module ConfigurableOptions
7
+ # @!attribute host
8
+ # Sc2 host param on which to listen for connections, default '0.0.0.0'
9
+ #
10
+ # Launch param: -host 0.0.0.0
11
+ # @return [String,nil]
12
+ attr_accessor :host
13
+
14
+ # @!attribute data_dir
15
+ # Override the path to find the data package.
16
+ #
17
+ # Required if the binary is not in the standard versions folder location.
18
+ #
19
+ # Launch param: -dataDir ../../
20
+ # @return [String,nil]
21
+ attr_accessor :data_dir
22
+
23
+ # @!attribute verbose
24
+ # If set to true, will send param to client.
25
+ #
26
+ # Enables logging of all protocol requests/responses to std::err.
27
+ #
28
+ # Launch param: -verbose
29
+ # @return [Boolean]
30
+ attr_accessor :verbose
31
+
32
+ # @!attribute temp_dir
33
+ # Override the path if you really need to set your own temp dir
34
+ #
35
+ # Implicit default is /tmp/
36
+ #
37
+ # Launch param: -tempDir ../../
38
+ # @return [String,nil]
39
+ attr_accessor :temp_dir
40
+
41
+ # @!attribute egl_path
42
+ # Sets the path the to hardware rendering library.
43
+ #
44
+ # Required for using the rendered interface with hardware rendering
45
+ #
46
+ # Launch param: -eglpath
47
+ #
48
+ # Example: /usr/lib/nvidia-384/libEGL.so
49
+ # @return [String,nil]
50
+ attr_accessor :egl_path
51
+
52
+ # @!attribute osmesa_path
53
+ # Sets the path the to software rendering library.
54
+ #
55
+ # Required for using the rendered interface with software rendering
56
+ #
57
+ # Launch param: -eglpath
58
+ #
59
+ # Example: /usr/lib/x86_64-linux-gnu/libOSMesa.so
60
+ # @return [String,nil]
61
+ attr_accessor :osmesa_path
62
+
63
+ # @!attribute display_mode
64
+ # Launch param: -displayMode
65
+ # @return [0,1,nil] 0 for window, 1 for fullscreen, nil for system default
66
+ attr_accessor :display_mode
67
+ # @!attribute windowwidth
68
+ # pixel width of game window
69
+ # @return [Integer]
70
+ attr_accessor :windowwidth # -windowwidth
71
+ # @!attribute windowheight
72
+ # pixel height of game window
73
+ # @return [Integer]
74
+ attr_accessor :windowheight # -windowheight
75
+ # @!attribute windowx
76
+ # left position of window if windowed
77
+ # @return [Integer]
78
+ attr_accessor :windowx # -windowx
79
+ # @!attribute windowy
80
+ # top position of window if windowed
81
+ # @return [Integer]
82
+ attr_accessor :windowy # -windowy
83
+
84
+ # @!attribute version
85
+ # Version number such as "4.10". Leave blank to use latest
86
+ # @return [String,nil]
87
+ attr_accessor :version
88
+
89
+ # Resets configurable launch options to their defaults
90
+ def load_default_launch_options
91
+ @host = "0.0.0.0" # -listen
92
+ if Paths.wsl?
93
+ @host = "#{Socket.gethostname}.mshome.net"
94
+ elsif Gem.win_platform?
95
+ @host = "127.0.0.1"
96
+ end
97
+
98
+ @display_mode = 0 # -displayMode
99
+ @windowwidth = nil # -windowwidth
100
+ @windowheight = nil # -windowheight
101
+ @windowx = nil # -windowx
102
+ @windowy = nil # -windowy
103
+ @verbose = false # -verbose
104
+
105
+ # Linux
106
+ @data_dir = nil # -dataDir '../../'
107
+ @temp_dir = nil # -tempDir '/tmp/'
108
+ @egl_path = nil # -eglpath '/usr/lib/nvidia-384/libEGL.so'
109
+ @osmesa_path = nil # -osmesapath '/usr/lib/x86_64-linux-gnu/libOSMesa.so'
110
+
111
+ @version = nil # calculates -dataVersion and Base% executable
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client/configurable_options"
4
+
5
+ module Sc2
6
+ # Manages client connection to the Api
7
+ class Client
8
+ include Sc2::Client::ConfigurableOptions
9
+
10
+ # @!attribute port
11
+ # Sc2 port param on which to listen for connections, default is randomly selected<br>
12
+ # Launch param: -port 12345
13
+ # @return [Integer]
14
+ attr_accessor :port
15
+
16
+ # @!attribute base_build
17
+ # Sc2 build number determines where to look for correct executable version binary
18
+ # @return [Integer]
19
+ attr_accessor :base_build
20
+
21
+ # @!attribute data_version
22
+ # Sc2 data param, typically only required when launching older versions and Linux
23
+ # Launch param: -dataVersion "B89B5D6FA7CBF6452E721311BFBC6CB2"
24
+ # @return [String]
25
+ attr_accessor :data_version
26
+
27
+ # Whether the Sc2 process is running or not
28
+ # @return [Boolean]
29
+ def running?
30
+ !!@task&.running?
31
+ end
32
+
33
+ # Initialize new Sc2 client (starts with #launch)
34
+ # @param host [String] to listen on, i.e. "0.0.0.0", "127.0.0.1"
35
+ # @param port [Integer] 5001
36
+ # @param options [Hash] (see Sc2::Client::ConfigurableOptions)
37
+ def initialize(host:, port:, **options)
38
+ raise Error, "Invalid port: #{port}" if port.to_s.empty?
39
+
40
+ load_default_launch_options
41
+ options.each do |key, value|
42
+ instance_variable_set(:"@#{key}", value)
43
+ end
44
+
45
+ # Require these at a minimum
46
+ @port = port
47
+ @host = host
48
+ return if @version.nil?
49
+
50
+ use_version(@version.to_s)
51
+ end
52
+
53
+ # Launches and returns pid or proc
54
+ def launch
55
+ cwd = Paths.exec_working_dir
56
+ @task = Async do |task|
57
+ options = {}
58
+
59
+ if Gem.win_platform?
60
+ options[:new_pgroup] = true
61
+ else
62
+ options[:pgroup] = true
63
+ end
64
+ unless cwd.nil?
65
+ options[:chdir] = cwd
66
+ end
67
+ begin
68
+ ::Async::Process.spawn(command_string, **options)
69
+ rescue
70
+ Sc2.logger.info("Client exited unexpectedly")
71
+ task&.stop
72
+ end
73
+ end
74
+ end
75
+
76
+ # Stops the Sc2 instance<br/>
77
+ # This naturally disconnects attached players too
78
+ # @return [void]
79
+ def stop
80
+ @task&.stop
81
+ end
82
+
83
+ # Reads "base-version" and "data-hash" for corresponding version
84
+ # @example
85
+ # client.base_build => nil
86
+ # client.data_version => nil
87
+ # client.use_version("4.10")
88
+ # client.base_build => 75689
89
+ # client.data_version => "B89B5D6FA7CBF6452E721311BFBC6CB2"
90
+ # @param version [String]
91
+ # return [Array<Integer,String>] tuple base_build and data_version
92
+ def use_version(version)
93
+ found_base_build = nil
94
+ found_data_version = nil
95
+ versions_json.each do |node|
96
+ version_clean = version.gsub(".#{node["base-version"]}", "")
97
+ if version_clean == node["label"]
98
+ found_base_build = node["base-version"]
99
+ found_data_version = node["data-hash"]
100
+ @version = version_clean
101
+ break
102
+ end
103
+ end
104
+ if found_base_build.nil? || found_data_version.nil?
105
+ Sc2.logger.warn "Requested version #{version} not found. Omit to auto-discover latest installed version"
106
+ return false
107
+ end
108
+
109
+ @base_build = found_base_build
110
+ @data_version = found_data_version
111
+ true
112
+ end
113
+
114
+ private
115
+
116
+ # @!attribute task
117
+ # The async task running Sc2. Used when interrupting.
118
+ # @return [Async::Task]
119
+ attr_accessor :task
120
+
121
+ # @private
122
+ # Takes all configuration and Sc2 executable string with arguments
123
+ # @return [String] command to launch Sc2
124
+ def command_string
125
+ command = "\"#{Sc2::Paths.executable(base_build: @base_build)}\" "
126
+ command += " -port #{@port}"
127
+ if Paths.platform == Paths::PF_WSL2
128
+ # For WSL2, always let windows listen on all 0.0.0.0
129
+ # and let the ws connect @host which is the internal bridged windows ip
130
+ command += " -listen 0.0.0.0"
131
+ else
132
+ command += " -listen #{@host}" unless @host.nil?
133
+ end
134
+
135
+ command += " -dataVersion #{@data_version}" unless @data_version.nil?
136
+ command += " -verbose" if @verbose
137
+
138
+ command += " -displayMode #{@display_mode}" unless @display_mode.nil?
139
+ command += " -windowwidth #{@windowwidth}" unless @windowwidth.nil?
140
+ command += " -windowheight #{@windowheight}" unless @windowheight.nil?
141
+ command += " -windowx #{@windowx}" unless @windowx.nil?
142
+ command += " -windowy #{@windowy}" unless @windowy.nil?
143
+
144
+ command += " -dataDir #{@data_dir}" unless @data_dir.nil?
145
+ command += " -tempDir #{@temp_dir}" unless @temp_dir.nil?
146
+ command += " -eglpath #{@egl_path}" unless @egl_path.nil?
147
+ command += " -osmesapath #{@osmesa_path}" unless @osmesa_path.nil?
148
+
149
+ command
150
+ end
151
+
152
+ # @private
153
+ # Reads bundled versions.json
154
+ # @return [JSON] contents of versions.json
155
+ def versions_json
156
+ JSON.load_file(Paths.gem_data_versions_path)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "forwardable"
5
+
6
+ module Sc2
7
+ # Starts, stops and holds reference to clients
8
+ class ClientManager
9
+ include Singleton
10
+
11
+ class << self
12
+ extend Forwardable
13
+ def_delegators :instance, :obtain, :get, :start, :stop
14
+ end
15
+
16
+ # Gets client for player X or starts an instance
17
+ def obtain(player_index)
18
+ client = get(player_index)
19
+ if client.nil? || !client.running?
20
+ client = start(player_index)
21
+ @clients[player_index] = client
22
+ end
23
+ client
24
+ end
25
+
26
+ # Gets Sc2::Client client for player index
27
+ # @param player_index [Integer] normally 0,1
28
+ # @return [Sc2::Connection, nil] running client or nil if not set
29
+ def get(player_index)
30
+ @clients[player_index]
31
+ end
32
+
33
+ # Starts an Sc2 client for player_index. Will stop existing client if present.
34
+ # @param player_index [Integer] normally 0,1
35
+ # @return [Sc2::Client] started client
36
+ def start(player_index)
37
+ existing = @clients[player_index]
38
+ stop(player_index) if !existing.nil? && existing.running?
39
+
40
+ client = Client.new(host: @host, port: @ports[player_index], **Sc2.config.to_h)
41
+ client.launch
42
+ @clients[player_index] = client
43
+ client
44
+ end
45
+
46
+ # Stops client at player index
47
+ # @param player_index [Integer]
48
+ # @return [void]
49
+ def stop(player_index)
50
+ return unless @clients[player_index]
51
+
52
+ @clients[player_index]&.stop
53
+ @clients[player_index] = nil
54
+ end
55
+
56
+ private
57
+
58
+ attr_accessor :clients, :ports
59
+
60
+ def initialize
61
+ super
62
+ @ports = Sc2.config.ports
63
+ @ports = [] unless ports.is_a? Array
64
+ @ports.push(Ports.random_available_port) while @ports.size < 3
65
+
66
+ @host = Sc2.config.host
67
+ @clients = []
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sc2
4
+ # Helps easily locate a map and fetch input for Api::LocalMap
5
+ class MapFile
6
+ # File extension used for maps
7
+ EXTENSION = ".SC2Map"
8
+
9
+ # @!attribute [r] path
10
+ # @return [String] path map location for use in LocalMap.map_path
11
+ attr_reader :path
12
+
13
+ # Accepts a map file name and initializes a local map object
14
+ # @example
15
+ # map = Sc2::Map.new("2000AtmospheresAIE")
16
+ # map = Sc2::Map.new("2000AtmospheresAIE.SC2Map")
17
+ # map = Sc2::Map.new("/absolute/path/to/2000AtmospheresAIE.SC2Map")
18
+ # # If within your Sc2 Maps folder, you have a sub-folder "sc2ai_2022_season3"
19
+ # map = Sc2::Map.new("sc2ai_2022_season3/2000AtmospheresAIE.SC2Map")
20
+ # @param name [String] absolute path or path relative to maps
21
+ def initialize(name)
22
+ raise Error if name.empty?
23
+
24
+ name = "#{name}#{EXTENSION}" unless name.end_with? EXTENSION
25
+
26
+ @path = name.to_s
27
+ return if Pathname(name).absolute?
28
+
29
+ @path = Pathname(Paths.maps_dir).glob("**/#{name}").first.to_s
30
+ if Paths.wsl?
31
+ @path = Paths.wsl_to_win(path: @path)
32
+ end
33
+ end
34
+
35
+ # Returns contents of map file for user with LocalMap.map_data
36
+ # @return [String, nil] contents of file
37
+ def data
38
+ file = Pathname(@path)
39
+ return file.read if file.exist?
40
+
41
+ nil
42
+ end
43
+
44
+ private
45
+
46
+ attr_writer :path
47
+ end
48
+ end