factorix 0.6.0 → 0.8.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +3 -0
  4. data/completion/_factorix.bash +15 -1
  5. data/completion/_factorix.fish +15 -7
  6. data/completion/_factorix.zsh +22 -0
  7. data/exe/factorix +17 -0
  8. data/lib/factorix/api/game_download_api.rb +154 -0
  9. data/lib/factorix/api/mod_download_api.rb +10 -5
  10. data/lib/factorix/api/mod_info.rb +1 -1
  11. data/lib/factorix/api/mod_portal_api.rb +6 -49
  12. data/lib/factorix/cache/base.rb +116 -0
  13. data/lib/factorix/cache/entry.rb +25 -0
  14. data/lib/factorix/cache/file_system.rb +137 -57
  15. data/lib/factorix/cache/redis.rb +287 -0
  16. data/lib/factorix/cache/s3.rb +388 -0
  17. data/lib/factorix/cli/commands/cache/evict.rb +17 -22
  18. data/lib/factorix/cli/commands/cache/stat.rb +57 -58
  19. data/lib/factorix/cli/commands/download.rb +150 -0
  20. data/lib/factorix/cli/commands/download_support.rb +1 -6
  21. data/lib/factorix/cli/commands/mod/download.rb +2 -3
  22. data/lib/factorix/cli/commands/mod/edit.rb +1 -4
  23. data/lib/factorix/cli/commands/mod/image/add.rb +1 -4
  24. data/lib/factorix/cli/commands/mod/image/edit.rb +1 -4
  25. data/lib/factorix/cli/commands/mod/image/list.rb +1 -4
  26. data/lib/factorix/cli/commands/mod/install.rb +2 -3
  27. data/lib/factorix/cli/commands/mod/list.rb +3 -3
  28. data/lib/factorix/cli/commands/mod/search.rb +2 -3
  29. data/lib/factorix/cli/commands/mod/show.rb +2 -3
  30. data/lib/factorix/cli/commands/mod/sync.rb +2 -3
  31. data/lib/factorix/cli/commands/mod/update.rb +6 -39
  32. data/lib/factorix/cli/commands/mod/upload.rb +1 -4
  33. data/lib/factorix/cli/commands/portal_support.rb +27 -0
  34. data/lib/factorix/cli.rb +1 -0
  35. data/lib/factorix/container.rb +32 -13
  36. data/lib/factorix/dependency/graph/builder.rb +2 -2
  37. data/lib/factorix/dependency/graph.rb +2 -2
  38. data/lib/factorix/dependency/validation_result.rb +3 -3
  39. data/lib/factorix/errors.rb +3 -0
  40. data/lib/factorix/http/cache_decorator.rb +14 -7
  41. data/lib/factorix/http/cached_response.rb +4 -1
  42. data/lib/factorix/http/client.rb +13 -3
  43. data/lib/factorix/http/response.rb +4 -1
  44. data/lib/factorix/http/retry_decorator.rb +11 -0
  45. data/lib/factorix/info_json.rb +5 -5
  46. data/lib/factorix/portal.rb +3 -2
  47. data/lib/factorix/save_file.rb +2 -2
  48. data/lib/factorix/transfer/downloader.rb +19 -11
  49. data/lib/factorix/version.rb +1 -1
  50. data/lib/factorix.rb +46 -53
  51. data/sig/factorix/api/mod_download_api.rbs +1 -2
  52. data/sig/factorix/cache/base.rbs +28 -0
  53. data/sig/factorix/cache/entry.rbs +14 -0
  54. data/sig/factorix/cache/file_system.rbs +7 -6
  55. data/sig/factorix/cache/redis.rbs +36 -0
  56. data/sig/factorix/cache/s3.rbs +38 -0
  57. data/sig/factorix/errors.rbs +3 -0
  58. data/sig/factorix/portal.rbs +1 -1
  59. metadata +27 -2
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class CLI
5
+ module Commands
6
+ # Download Factorio game files from the official download API
7
+ class Download < Base
8
+ # @!parse
9
+ # # @return [Dry::Logger::Dispatcher]
10
+ # attr_reader :logger
11
+ # # @return [Runtime]
12
+ # attr_reader :runtime
13
+ # # @return [API::GameDownloadAPI]
14
+ # attr_reader :game_download_api
15
+ include Import[:logger, :runtime, :game_download_api]
16
+
17
+ # Platform mapping from Runtime to API platform identifier
18
+ PLATFORM_MAP = {
19
+ "MacOS" => "osx",
20
+ "Linux" => "linux64",
21
+ "Windows" => "win64",
22
+ "WSL" => "win64"
23
+ }.freeze
24
+ private_constant :PLATFORM_MAP
25
+
26
+ desc "Download Factorio game files"
27
+
28
+ argument :version, required: false, default: "latest", desc: "Version (e.g., 2.0.73, latest)"
29
+
30
+ option :build, aliases: ["-b"], default: "alpha", values: API::GameDownloadAPI::BUILDS, desc: "Build type"
31
+ option :platform, aliases: ["-p"], values: API::GameDownloadAPI::PLATFORMS, desc: "Platform (default: auto-detect)"
32
+ option :channel, aliases: ["-c"], default: "stable", values: API::GameDownloadAPI::CHANNELS, desc: "Release channel"
33
+ option :directory, aliases: ["-d"], default: ".", desc: "Download directory"
34
+ option :output, aliases: ["-o"], desc: "Output filename (default: from server)"
35
+
36
+ example [
37
+ " # Download latest stable version (auto-detect platform)",
38
+ "2.0.73 # Download specific version",
39
+ "--build expansion # Download expansion build",
40
+ "--build headless -p linux64 # Download headless server for Linux",
41
+ "--channel experimental # Download experimental release",
42
+ "-o factorio-server.tar.xz # Specify output filename"
43
+ ]
44
+
45
+ # Execute the download command
46
+ #
47
+ # @param version [String] Version to download
48
+ # @param build [String] Build type
49
+ # @param platform [String, nil] Platform (nil for auto-detect)
50
+ # @param channel [String] Release channel
51
+ # @param directory [String] Download directory
52
+ # @param output [String, nil] Output filename
53
+ # @return [void]
54
+ def call(version: "latest", build: "alpha", platform: nil, channel: "stable", directory: ".", output: nil, **)
55
+ platform ||= detect_platform
56
+ resolved_version = resolve_version(version, channel, build)
57
+
58
+ download_dir = Pathname(directory).expand_path
59
+ raise DirectoryNotFoundError, "Download directory does not exist: #{download_dir}" unless download_dir.exist?
60
+
61
+ filename = output || resolve_filename(resolved_version, build, platform)
62
+ output_path = download_dir / filename
63
+
64
+ say "Downloading Factorio #{resolved_version} (#{build}/#{platform})...", prefix: :info
65
+
66
+ download_game(resolved_version, build, platform, output_path)
67
+
68
+ say "Downloaded to #{output_path}", prefix: :success
69
+ end
70
+
71
+ # Detect platform from Runtime
72
+ #
73
+ # @return [String] Platform identifier
74
+ private def detect_platform
75
+ runtime_class = runtime.class.name.split("::").last
76
+ platform = PLATFORM_MAP[runtime_class]
77
+ raise UnsupportedPlatformError, "Cannot auto-detect platform for #{runtime_class}" unless platform
78
+
79
+ logger.debug("Auto-detected platform", platform:)
80
+ platform
81
+ end
82
+
83
+ # Minimum supported major version
84
+ MINIMUM_MAJOR_VERSION = 2
85
+ private_constant :MINIMUM_MAJOR_VERSION
86
+
87
+ # Resolve version, handling "latest" by fetching from API
88
+ #
89
+ # @param version [String] Version or "latest"
90
+ # @param channel [String] Release channel
91
+ # @param build [String] Build type
92
+ # @return [String] Resolved version
93
+ # @raise [InvalidArgumentError] if version is invalid or < 2.0
94
+ private def resolve_version(version, channel, build)
95
+ resolved = if version == "latest"
96
+ v = game_download_api.latest_version(channel:, build:)
97
+ raise InvalidArgumentError, "No #{channel} version available for #{build}" unless v
98
+
99
+ logger.debug("Resolved latest version", channel:, build:, version: v)
100
+ v
101
+ else
102
+ version
103
+ end
104
+
105
+ validate_version!(resolved)
106
+ resolved
107
+ end
108
+
109
+ # Validate version format and minimum version requirement
110
+ #
111
+ # @param version [String] Version string
112
+ # @return [void]
113
+ # @raise [InvalidArgumentError] if version is invalid or < 2.0
114
+ private def validate_version!(version)
115
+ game_version = GameVersion.from_string(version)
116
+
117
+ return if game_version.major >= MINIMUM_MAJOR_VERSION
118
+
119
+ raise InvalidArgumentError, "Version #{version} is not supported. Minimum version is #{MINIMUM_MAJOR_VERSION}.0.0"
120
+ rescue VersionParseError => e
121
+ raise InvalidArgumentError, "Invalid version format: #{e.message}"
122
+ end
123
+
124
+ # Resolve filename by making HEAD request
125
+ #
126
+ # @param version [String] Version
127
+ # @param build [String] Build type
128
+ # @param platform [String] Platform
129
+ # @return [String] Filename
130
+ private def resolve_filename(version, build, platform)
131
+ game_download_api.resolve_filename(version:, build:, platform:)
132
+ end
133
+
134
+ # Download the game with progress tracking
135
+ #
136
+ # @param version [String] Version
137
+ # @param build [String] Build type
138
+ # @param platform [String] Platform
139
+ # @param output_path [Pathname] Output file path
140
+ # @return [void]
141
+ private def download_game(version, build, platform, output_path)
142
+ presenter = Progress::Presenter.new(title: output_path.basename.to_s, output: err)
143
+ handler = Progress::DownloadHandler.new(presenter)
144
+
145
+ game_download_api.download(version:, build:, platform:, output: output_path, handler:)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -97,18 +97,13 @@ module Factorix
97
97
 
98
98
  futures = targets.map {|target|
99
99
  Concurrent::Future.execute(executor: pool) do
100
- thread_portal = Container[:portal]
101
- thread_downloader = thread_portal.mod_download_api.downloader
102
-
103
100
  presenter = multi_presenter.register(
104
101
  target[:mod].name,
105
102
  title: target[:release].file_name
106
103
  )
107
104
  handler = Progress::DownloadHandler.new(presenter)
108
105
 
109
- thread_downloader.subscribe(handler)
110
- thread_portal.download_mod(target[:release], target[:output_path])
111
- thread_downloader.unsubscribe(handler)
106
+ portal.download_mod(target[:release], target[:output_path], handler:)
112
107
  end
113
108
  }
114
109
 
@@ -10,14 +10,13 @@ module Factorix
10
10
  # Download MOD files from Factorio MOD Portal
11
11
  class Download < Base
12
12
  include DownloadSupport
13
+ include PortalSupport
13
14
  # @!parse
14
- # # @return [Portal]
15
- # attr_reader :portal
16
15
  # # @return [Dry::Logger::Dispatcher]
17
16
  # attr_reader :logger
18
17
  # # @return [Runtime]
19
18
  # attr_reader :runtime
20
- include Import[:portal, :logger, :runtime]
19
+ include Import[:logger, :runtime]
21
20
 
22
21
  desc "Download MOD files from Factorio MOD Portal"
23
22
 
@@ -6,10 +6,7 @@ module Factorix
6
6
  module MOD
7
7
  # Edit MOD metadata on Factorio MOD Portal
8
8
  class Edit < Base
9
- # @!parse
10
- # # @return [Portal]
11
- # attr_reader :portal
12
- include Import[:portal]
9
+ include PortalSupport
13
10
 
14
11
  desc "Edit MOD metadata on Factorio MOD Portal"
15
12
 
@@ -7,10 +7,7 @@ module Factorix
7
7
  module Image
8
8
  # Add an image to a MOD on Factorio MOD Portal
9
9
  class Add < Base
10
- # @!parse
11
- # # @return [Portal]
12
- # attr_reader :portal
13
- include Import[:portal]
10
+ include PortalSupport
14
11
 
15
12
  desc "Add an image to a MOD"
16
13
 
@@ -7,10 +7,7 @@ module Factorix
7
7
  module Image
8
8
  # Edit MOD's image list on Factorio MOD Portal
9
9
  class Edit < Base
10
- # @!parse
11
- # # @return [Portal]
12
- # attr_reader :portal
13
- include Import[:portal]
10
+ include PortalSupport
14
11
 
15
12
  desc "Edit MOD's image list (reorder/remove images)"
16
13
 
@@ -7,10 +7,7 @@ module Factorix
7
7
  module Image
8
8
  # List images for a MOD on Factorio MOD Portal
9
9
  class List < Base
10
- # @!parse
11
- # # @return [Portal]
12
- # attr_reader :portal
13
- include Import[:portal]
10
+ include PortalSupport
14
11
 
15
12
  desc "List images for a MOD"
16
13
 
@@ -14,15 +14,14 @@ module Factorix
14
14
  backup_support!
15
15
 
16
16
  include DownloadSupport
17
+ include PortalSupport
17
18
 
18
19
  # @!parse
19
- # # @return [Portal]
20
- # attr_reader :portal
21
20
  # # @return [Dry::Logger::Dispatcher]
22
21
  # attr_reader :logger
23
22
  # # @return [Factorix::Runtime]
24
23
  # attr_reader :runtime
25
- include Import[:portal, :logger, :runtime]
24
+ include Import[:logger, :runtime]
26
25
 
27
26
  desc "Install MOD(s) from Factorio MOD Portal (downloads to MOD directory and enables)"
28
27
 
@@ -147,7 +147,7 @@ module Factorix
147
147
  enabled = mod_list.exist?(mod) && mod_list.enabled?(mod)
148
148
  error = error_map[mod.name]
149
149
 
150
- MODInfo.new(name: mod.name, version: display_version, enabled:, error:, latest_version: nil)
150
+ MODInfo[name: mod.name, version: display_version, enabled:, error:, latest_version: nil]
151
151
  }
152
152
  end
153
153
 
@@ -256,13 +256,13 @@ module Factorix
256
256
  private def fetch_latest_version_for_mod(info)
257
257
  portal_info = mod_portal_api.get_mod(info.name)
258
258
  latest = portal_info[:releases]&.map {|r| MODVersion.from_string(r[:version]) }&.max
259
- MODInfo.new(
259
+ MODInfo[
260
260
  name: info.name,
261
261
  version: info.version,
262
262
  enabled: info.enabled,
263
263
  error: info.error,
264
264
  latest_version: latest
265
- )
265
+ ]
266
266
  rescue MODNotOnPortalError
267
267
  logger.debug("MOD not found on portal", mod: info.name)
268
268
  info
@@ -8,12 +8,11 @@ module Factorix
8
8
  module MOD
9
9
  # Search MODs on Factorio MOD Portal
10
10
  class Search < Base
11
+ include PortalSupport
11
12
  # @!parse
12
- # # @return [Portal]
13
- # attr_reader :portal
14
13
  # # @return [Runtime]
15
14
  # attr_reader :runtime
16
- include Import[:portal, :runtime]
15
+ include Import[:runtime]
17
16
 
18
17
  desc "Search MOD(s) on Factorio MOD Portal"
19
18
 
@@ -20,11 +20,10 @@ module Factorix
20
20
  INCOMPATIBLE_MOD_STYLE = TIntMe[:red]
21
21
  private_constant :INCOMPATIBLE_MOD_STYLE
22
22
  # @!parse
23
- # # @return [Portal]
24
- # attr_reader :portal
25
23
  # # @return [Runtime]
26
24
  # attr_reader :runtime
27
- include Import[:portal, :runtime]
25
+ include Import[:runtime]
26
+ include PortalSupport
28
27
 
29
28
  desc "Show MOD details from Factorio MOD Portal"
30
29
 
@@ -14,15 +14,14 @@ module Factorix
14
14
  backup_support!
15
15
 
16
16
  include DownloadSupport
17
+ include PortalSupport
17
18
 
18
19
  # @!parse
19
- # # @return [Portal]
20
- # attr_reader :portal
21
20
  # # @return [Dry::Logger::Dispatcher]
22
21
  # attr_reader :logger
23
22
  # # @return [Factorix::Runtime]
24
23
  # attr_reader :runtime
25
- include Import[:portal, :logger, :runtime]
24
+ include Import[:logger, :runtime]
26
25
 
27
26
  desc "Sync MOD states and startup settings from a save file"
28
27
 
@@ -13,14 +13,14 @@ module Factorix
13
13
  require_game_stopped!
14
14
  backup_support!
15
15
 
16
+ include PortalSupport
17
+ include DownloadSupport
16
18
  # @!parse
17
- # # @return [Portal]
18
- # attr_reader :portal
19
19
  # # @return [Dry::Logger::Dispatcher]
20
20
  # attr_reader :logger
21
21
  # # @return [Factorix::Runtime]
22
22
  # attr_reader :runtime
23
- include Import[:portal, :logger, :runtime]
23
+ include Import[:logger, :runtime]
24
24
 
25
25
  desc "Update MOD(s) to their latest versions"
26
26
 
@@ -141,7 +141,7 @@ module Factorix
141
141
  mod:,
142
142
  mod_info:,
143
143
  current_version:,
144
- latest_release:,
144
+ release: latest_release,
145
145
  output_path: runtime.mod_dir / latest_release.file_name
146
146
  }
147
147
  rescue MODNotOnPortalError
@@ -156,7 +156,7 @@ module Factorix
156
156
  private def show_plan(targets)
157
157
  say "Planning to update #{targets.size} MOD(s):", prefix: :info
158
158
  targets.each do |target|
159
- say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:latest_release].version}"
159
+ say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:release].version}"
160
160
  end
161
161
  end
162
162
 
@@ -176,46 +176,13 @@ module Factorix
176
176
  current_enabled = mod_list.enabled?(mod)
177
177
  mod_list.remove(mod)
178
178
  mod_list.add(mod, enabled: current_enabled)
179
- say "Updated #{mod} to #{target[:latest_release].version}", prefix: :success
179
+ say "Updated #{mod} to #{target[:release].version}", prefix: :success
180
180
  else
181
181
  mod_list.add(mod, enabled: true)
182
182
  say "Added #{mod} to mod-list.json", prefix: :success
183
183
  end
184
184
  end
185
185
  end
186
-
187
- # Download MODs in parallel
188
- #
189
- # @param targets [Array<Hash>] Update targets
190
- # @param jobs [Integer] Number of parallel jobs
191
- # @return [void]
192
- private def download_mods(targets, jobs)
193
- multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads", output: err)
194
-
195
- pool = Concurrent::FixedThreadPool.new(jobs)
196
-
197
- futures = targets.map {|target|
198
- Concurrent::Future.execute(executor: pool) do
199
- thread_portal = Container[:portal]
200
- thread_downloader = thread_portal.mod_download_api.downloader
201
-
202
- presenter = multi_presenter.register(
203
- target[:mod].name,
204
- title: target[:latest_release].file_name
205
- )
206
- handler = Progress::DownloadHandler.new(presenter)
207
-
208
- thread_downloader.subscribe(handler)
209
- thread_portal.download_mod(target[:latest_release], target[:output_path])
210
- thread_downloader.unsubscribe(handler)
211
- end
212
- }
213
-
214
- futures.each(&:wait!)
215
- ensure
216
- pool&.shutdown
217
- pool&.wait_for_termination
218
- end
219
186
  end
220
187
  end
221
188
  end
@@ -6,10 +6,7 @@ module Factorix
6
6
  module MOD
7
7
  # Upload MOD to Factorio MOD Portal (handles both new and update)
8
8
  class Upload < Base
9
- # @!parse
10
- # # @return [Portal]
11
- # attr_reader :portal
12
- include Import[:portal]
9
+ include PortalSupport
13
10
 
14
11
  desc "Upload MOD to Factorio MOD Portal (handles both new and update)"
15
12
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class CLI
5
+ module Commands
6
+ # Provides lazy Portal resolution for CLI commands
7
+ #
8
+ # This module defers Portal dependency resolution until first use,
9
+ # allowing configuration to be loaded before cache backends are resolved.
10
+ #
11
+ # @example
12
+ # class Show < Base
13
+ # include PortalSupport
14
+ #
15
+ # def call(mod_name:, **)
16
+ # mod_info = portal.get_mod(mod_name)
17
+ # end
18
+ # end
19
+ module PortalSupport
20
+ # Lazily resolve Portal from Container
21
+ #
22
+ # @return [Portal] the portal instance
23
+ private def portal = @portal ||= Container[:portal]
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/factorix/cli.rb CHANGED
@@ -17,6 +17,7 @@ module Factorix
17
17
  register "man", Commands::Man
18
18
  register "launch", Commands::Launch
19
19
  register "path", Commands::Path
20
+ register "download", Commands::Download
20
21
  register "completion", Commands::Completion
21
22
  register "mod check", Commands::MOD::Check
22
23
  register "mod list", Commands::MOD::List
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/core"
4
+ require "dry/inflector"
4
5
  require "dry/logger"
5
6
 
6
7
  module Factorix
@@ -13,12 +14,28 @@ module Factorix
13
14
  class Container
14
15
  extend Dry::Core::Container::Mixin
15
16
 
16
- # Some items are registered with memoize: false to support independent event handlers
17
+ INFLECTOR = Dry::Inflector.new do |inflections|
18
+ inflections.uncountable("redis")
19
+ end
20
+ private_constant :INFLECTOR
21
+
22
+ # Build a cache instance from configuration.
23
+ #
24
+ # @param cache_type [Symbol] cache type (:download, :api, :info_json)
25
+ # @param config [Dry::Configurable::Config] cache configuration
26
+ # @return [Cache::Base] cache instance
27
+ def self.build_cache(cache_type, config)
28
+ backend = config.backend
29
+ backend_config = config.public_send(backend).to_h
30
+ backend_class = Cache.const_get(INFLECTOR.classify(backend.to_s))
31
+ backend_class.new(cache_type:, ttl: config.ttl, **backend_config)
32
+ end
33
+ private_class_method :build_cache
34
+
35
+ # :downloader is registered with memoize: false to support independent event handlers
17
36
  # for each parallel download task (e.g., progress tracking).
18
- # Items registered with memoize: false:
19
- # - :downloader (event handlers for progress tracking)
20
- # - :mod_download_api (contains :downloader)
21
- # - :portal (contains :mod_download_api)
37
+ # MODDownloadAPI resolves :downloader lazily per download call, allowing
38
+ # :mod_download_api and :portal to be safely memoized.
22
39
 
23
40
  # Register runtime detector
24
41
  register(:runtime, memoize: true) do
@@ -48,20 +65,17 @@ module Factorix
48
65
 
49
66
  # Register download cache
50
67
  register(:download_cache, memoize: true) do
51
- c = Factorix.config.cache.download
52
- Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
68
+ build_cache(:download, Factorix.config.cache.download)
53
69
  end
54
70
 
55
71
  # Register API cache (with compression for JSON responses)
56
72
  register(:api_cache, memoize: true) do
57
- c = Factorix.config.cache.api
58
- Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
73
+ build_cache(:api, Factorix.config.cache.api)
59
74
  end
60
75
 
61
76
  # Register info.json cache (for MOD metadata from ZIP files)
62
77
  register(:info_json_cache, memoize: true) do
63
- c = Factorix.config.cache.info_json
64
- Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
78
+ build_cache(:info_json, Factorix.config.cache.info_json)
65
79
  end
66
80
 
67
81
  # Register base HTTP client
@@ -118,10 +132,15 @@ module Factorix
118
132
  end
119
133
 
120
134
  # Register MOD Download API client
121
- register(:mod_download_api, memoize: false) do
135
+ register(:mod_download_api, memoize: true) do
122
136
  API::MODDownloadAPI.new
123
137
  end
124
138
 
139
+ # Register Game Download API client
140
+ register(:game_download_api, memoize: true) do
141
+ API::GameDownloadAPI.new
142
+ end
143
+
125
144
  # Register API credential (for MOD upload/management)
126
145
  register(:api_credential, memoize: true) { APICredential.load }
127
146
 
@@ -134,7 +153,7 @@ module Factorix
134
153
  end
135
154
 
136
155
  # Register portal (high-level API wrapper)
137
- register(:portal, memoize: false) do
156
+ register(:portal, memoize: true) do
138
157
  Portal.new
139
158
  end
140
159
  end
@@ -53,7 +53,7 @@ module Factorix
53
53
  version = select_version_for_mod(mod)
54
54
  enabled = mod_enabled?(mod)
55
55
 
56
- node = Node.new(mod:, version:, enabled:, installed: true)
56
+ node = Node[mod:, version:, enabled:, installed: true]
57
57
  graph.add_node(node)
58
58
  end
59
59
 
@@ -88,7 +88,7 @@ module Factorix
88
88
  # Expansion MODs can be disabled, so they must be validated
89
89
  next if dependency.mod.base?
90
90
 
91
- edge = Edge.new(from_mod:, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement)
91
+ edge = Edge[from_mod:, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
92
92
  graph.add_edge(edge)
93
93
  end
94
94
  end
@@ -75,7 +75,7 @@ module Factorix
75
75
  return
76
76
  end
77
77
 
78
- node = Node.new(mod:, version: release.version, enabled: false, installed: false, operation:)
78
+ node = Node[mod:, version: release.version, enabled: false, installed: false, operation:]
79
79
  add_node(node)
80
80
 
81
81
  dependencies = release.info_json[:dependencies] || []
@@ -85,7 +85,7 @@ module Factorix
85
85
  dependency = parser.parse(dep_string)
86
86
  next if dependency.mod.base?
87
87
 
88
- edge = Edge.new(from_mod: mod, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement)
88
+ edge = Edge[from_mod: mod, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
89
89
 
90
90
  add_edge(edge)
91
91
  end
@@ -81,7 +81,7 @@ module Factorix
81
81
  # @param mod [Factorix::MOD, nil] Related MOD
82
82
  # @param dependency [Factorix::MOD, nil] Dependency MOD
83
83
  # @return [void]
84
- def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error.new(type:, message:, mod:, dependency:)
84
+ def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error[type:, message:, mod:, dependency:]
85
85
 
86
86
  # Add a warning
87
87
  #
@@ -89,7 +89,7 @@ module Factorix
89
89
  # @param message [String] Warning message
90
90
  # @param mod [Factorix::MOD, nil] Related MOD
91
91
  # @return [void]
92
- def add_warning(type:, message:, mod: nil) = @warnings << Warning.new(type:, message:, mod:)
92
+ def add_warning(type:, message:, mod: nil) = @warnings << Warning[type:, message:, mod:]
93
93
 
94
94
  # Add a suggestion
95
95
  #
@@ -97,7 +97,7 @@ module Factorix
97
97
  # @param mod [Factorix::MOD] Related MOD
98
98
  # @param version [Factorix::MODVersion] Suggested version
99
99
  # @return [void]
100
- def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion.new(message:, mod:, version:)
100
+ def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion[message:, mod:, version:]
101
101
 
102
102
  # Get all errors
103
103
  #
@@ -48,6 +48,9 @@ module Factorix
48
48
  class HTTPNotFoundError < HTTPClientError; end
49
49
  class HTTPServerError < HTTPError; end
50
50
 
51
+ # Cache lock timeout errors
52
+ class LockTimeoutError < InfrastructureError; end
53
+
51
54
  # Digest verification errors
52
55
  class DigestMismatchError < InfrastructureError; end
53
56