sc2ai 0.0.0.pre → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/data/data.json +1 -0
  3. data/data/data_readable.json +22946 -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 +37900 -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 +1951 -0
  23. data/lib/sc2ai/api/buff_id.rb +316 -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 +82 -0
  27. data/lib/sc2ai/api/tech_tree_data.rb +2342 -0
  28. data/lib/sc2ai/api/unit_type_id.rb +2074 -0
  29. data/lib/sc2ai/api/upgrade_id.rb +312 -0
  30. data/lib/sc2ai/cli/cli.rb +177 -0
  31. data/lib/sc2ai/cli/ladderzip.rb +173 -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. metadata +353 -9
@@ -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