factorix 0.5.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 (202) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +105 -0
  5. data/completion/_factorix.bash +202 -0
  6. data/completion/_factorix.fish +197 -0
  7. data/completion/_factorix.zsh +376 -0
  8. data/doc/factorix.1 +377 -0
  9. data/exe/factorix +20 -0
  10. data/lib/factorix/api/category.rb +69 -0
  11. data/lib/factorix/api/image.rb +35 -0
  12. data/lib/factorix/api/license.rb +71 -0
  13. data/lib/factorix/api/mod_download_api.rb +66 -0
  14. data/lib/factorix/api/mod_info.rb +166 -0
  15. data/lib/factorix/api/mod_management_api.rb +237 -0
  16. data/lib/factorix/api/mod_portal_api.rb +204 -0
  17. data/lib/factorix/api/release.rb +49 -0
  18. data/lib/factorix/api/tag.rb +95 -0
  19. data/lib/factorix/api.rb +7 -0
  20. data/lib/factorix/api_credential.rb +54 -0
  21. data/lib/factorix/application.rb +218 -0
  22. data/lib/factorix/cache/file_system.rb +307 -0
  23. data/lib/factorix/cli/commands/backup_support.rb +46 -0
  24. data/lib/factorix/cli/commands/base.rb +90 -0
  25. data/lib/factorix/cli/commands/cache/evict.rb +180 -0
  26. data/lib/factorix/cli/commands/cache/stat.rb +201 -0
  27. data/lib/factorix/cli/commands/command_wrapper.rb +71 -0
  28. data/lib/factorix/cli/commands/completion.rb +83 -0
  29. data/lib/factorix/cli/commands/confirmable.rb +53 -0
  30. data/lib/factorix/cli/commands/download_support.rb +123 -0
  31. data/lib/factorix/cli/commands/launch.rb +79 -0
  32. data/lib/factorix/cli/commands/man.rb +29 -0
  33. data/lib/factorix/cli/commands/mod/check.rb +99 -0
  34. data/lib/factorix/cli/commands/mod/disable.rb +188 -0
  35. data/lib/factorix/cli/commands/mod/download.rb +291 -0
  36. data/lib/factorix/cli/commands/mod/edit.rb +114 -0
  37. data/lib/factorix/cli/commands/mod/enable.rb +216 -0
  38. data/lib/factorix/cli/commands/mod/image/add.rb +47 -0
  39. data/lib/factorix/cli/commands/mod/image/edit.rb +41 -0
  40. data/lib/factorix/cli/commands/mod/image/list.rb +74 -0
  41. data/lib/factorix/cli/commands/mod/install.rb +443 -0
  42. data/lib/factorix/cli/commands/mod/list.rb +372 -0
  43. data/lib/factorix/cli/commands/mod/search.rb +134 -0
  44. data/lib/factorix/cli/commands/mod/settings/dump.rb +88 -0
  45. data/lib/factorix/cli/commands/mod/settings/restore.rb +101 -0
  46. data/lib/factorix/cli/commands/mod/show.rb +202 -0
  47. data/lib/factorix/cli/commands/mod/sync.rb +299 -0
  48. data/lib/factorix/cli/commands/mod/uninstall.rb +325 -0
  49. data/lib/factorix/cli/commands/mod/update.rb +222 -0
  50. data/lib/factorix/cli/commands/mod/upload.rb +90 -0
  51. data/lib/factorix/cli/commands/path.rb +79 -0
  52. data/lib/factorix/cli/commands/requires_game_stopped.rb +32 -0
  53. data/lib/factorix/cli/commands/version.rb +25 -0
  54. data/lib/factorix/cli.rb +42 -0
  55. data/lib/factorix/dependency/edge.rb +89 -0
  56. data/lib/factorix/dependency/entry.rb +124 -0
  57. data/lib/factorix/dependency/graph/builder.rb +108 -0
  58. data/lib/factorix/dependency/graph.rb +210 -0
  59. data/lib/factorix/dependency/list.rb +244 -0
  60. data/lib/factorix/dependency/mod_version_requirement.rb +73 -0
  61. data/lib/factorix/dependency/node.rb +60 -0
  62. data/lib/factorix/dependency/parser.rb +148 -0
  63. data/lib/factorix/dependency/validation_result.rb +138 -0
  64. data/lib/factorix/dependency/validator.rb +190 -0
  65. data/lib/factorix/errors.rb +112 -0
  66. data/lib/factorix/formatting.rb +56 -0
  67. data/lib/factorix/game_version.rb +98 -0
  68. data/lib/factorix/http/cache_decorator.rb +106 -0
  69. data/lib/factorix/http/cached_response.rb +37 -0
  70. data/lib/factorix/http/client.rb +187 -0
  71. data/lib/factorix/http/response.rb +31 -0
  72. data/lib/factorix/http/retry_decorator.rb +59 -0
  73. data/lib/factorix/http/retry_strategy.rb +80 -0
  74. data/lib/factorix/info_json.rb +90 -0
  75. data/lib/factorix/installed_mod.rb +239 -0
  76. data/lib/factorix/mod.rb +55 -0
  77. data/lib/factorix/mod_list.rb +174 -0
  78. data/lib/factorix/mod_settings.rb +278 -0
  79. data/lib/factorix/mod_state.rb +34 -0
  80. data/lib/factorix/mod_version.rb +99 -0
  81. data/lib/factorix/portal.rb +185 -0
  82. data/lib/factorix/progress/download_handler.rb +46 -0
  83. data/lib/factorix/progress/multi_presenter.rb +45 -0
  84. data/lib/factorix/progress/presenter.rb +67 -0
  85. data/lib/factorix/progress/presenter_adapter.rb +46 -0
  86. data/lib/factorix/progress/scan_handler.rb +33 -0
  87. data/lib/factorix/progress/upload_handler.rb +33 -0
  88. data/lib/factorix/runtime/base.rb +233 -0
  89. data/lib/factorix/runtime/linux.rb +32 -0
  90. data/lib/factorix/runtime/mac_os.rb +53 -0
  91. data/lib/factorix/runtime/user_configurable.rb +69 -0
  92. data/lib/factorix/runtime/windows.rb +85 -0
  93. data/lib/factorix/runtime/wsl.rb +118 -0
  94. data/lib/factorix/runtime.rb +32 -0
  95. data/lib/factorix/save_file.rb +178 -0
  96. data/lib/factorix/ser_des/deserializer.rb +198 -0
  97. data/lib/factorix/ser_des/serializer.rb +231 -0
  98. data/lib/factorix/ser_des/signed_integer.rb +63 -0
  99. data/lib/factorix/ser_des/unsigned_integer.rb +65 -0
  100. data/lib/factorix/service_credential.rb +127 -0
  101. data/lib/factorix/transfer/downloader.rb +162 -0
  102. data/lib/factorix/transfer/uploader.rb +232 -0
  103. data/lib/factorix/version.rb +6 -0
  104. data/lib/factorix.rb +38 -0
  105. data/sig/dry/auto_inject.rbs +15 -0
  106. data/sig/dry/cli.rbs +19 -0
  107. data/sig/dry/configurable.rbs +13 -0
  108. data/sig/dry/core/container.rbs +17 -0
  109. data/sig/dry/events/publisher.rbs +22 -0
  110. data/sig/dry/logger.rbs +16 -0
  111. data/sig/factorix/api/category.rbs +15 -0
  112. data/sig/factorix/api/image.rbs +15 -0
  113. data/sig/factorix/api/license.rbs +20 -0
  114. data/sig/factorix/api/mod_download_api.rbs +18 -0
  115. data/sig/factorix/api/mod_info.rbs +67 -0
  116. data/sig/factorix/api/mod_management_api.rbs +25 -0
  117. data/sig/factorix/api/mod_portal_api.rbs +31 -0
  118. data/sig/factorix/api/release.rbs +27 -0
  119. data/sig/factorix/api/tag.rbs +15 -0
  120. data/sig/factorix/api.rbs +8 -0
  121. data/sig/factorix/api_credential.rbs +17 -0
  122. data/sig/factorix/application.rbs +86 -0
  123. data/sig/factorix/cache/file_system.rbs +35 -0
  124. data/sig/factorix/cli/commands/base.rbs +13 -0
  125. data/sig/factorix/cli/commands/cache/evict.rbs +17 -0
  126. data/sig/factorix/cli/commands/cache/stat.rbs +17 -0
  127. data/sig/factorix/cli/commands/command_wrapper.rbs +13 -0
  128. data/sig/factorix/cli/commands/completion/zsh.rbs +15 -0
  129. data/sig/factorix/cli/commands/confirmable.rbs +12 -0
  130. data/sig/factorix/cli/commands/download_support.rbs +12 -0
  131. data/sig/factorix/cli/commands/launch.rbs +15 -0
  132. data/sig/factorix/cli/commands/mod/check.rbs +18 -0
  133. data/sig/factorix/cli/commands/mod/disable.rbs +20 -0
  134. data/sig/factorix/cli/commands/mod/download.rbs +18 -0
  135. data/sig/factorix/cli/commands/mod/edit.rbs +30 -0
  136. data/sig/factorix/cli/commands/mod/enable.rbs +20 -0
  137. data/sig/factorix/cli/commands/mod/image/add.rbs +19 -0
  138. data/sig/factorix/cli/commands/mod/image/edit.rbs +19 -0
  139. data/sig/factorix/cli/commands/mod/image/list.rbs +19 -0
  140. data/sig/factorix/cli/commands/mod/install.rbs +19 -0
  141. data/sig/factorix/cli/commands/mod/list.rbs +30 -0
  142. data/sig/factorix/cli/commands/mod/search.rbs +18 -0
  143. data/sig/factorix/cli/commands/mod/settings/dump.rbs +17 -0
  144. data/sig/factorix/cli/commands/mod/settings/restore.rbs +17 -0
  145. data/sig/factorix/cli/commands/mod/sync.rbs +19 -0
  146. data/sig/factorix/cli/commands/mod/uninstall.rbs +20 -0
  147. data/sig/factorix/cli/commands/mod/update.rbs +19 -0
  148. data/sig/factorix/cli/commands/mod/upload.rbs +24 -0
  149. data/sig/factorix/cli/commands/path.rbs +18 -0
  150. data/sig/factorix/cli/commands/requires_game_stopped.rbs +13 -0
  151. data/sig/factorix/cli/commands/version.rbs +13 -0
  152. data/sig/factorix/cli.rbs +11 -0
  153. data/sig/factorix/dependency/edge.rbs +32 -0
  154. data/sig/factorix/dependency/entry.rbs +30 -0
  155. data/sig/factorix/dependency/graph/builder.rbs +17 -0
  156. data/sig/factorix/dependency/graph.rbs +39 -0
  157. data/sig/factorix/dependency/list.rbs +69 -0
  158. data/sig/factorix/dependency/mod_version_requirement.rbs +18 -0
  159. data/sig/factorix/dependency/node.rbs +24 -0
  160. data/sig/factorix/dependency/parser.rbs +11 -0
  161. data/sig/factorix/dependency/validation_result.rbs +56 -0
  162. data/sig/factorix/dependency/validator.rbs +13 -0
  163. data/sig/factorix/errors.rbs +132 -0
  164. data/sig/factorix/formatting.rbs +8 -0
  165. data/sig/factorix/game_version.rbs +24 -0
  166. data/sig/factorix/http/cache_decorator.rbs +64 -0
  167. data/sig/factorix/http/client.rbs +55 -0
  168. data/sig/factorix/http/response.rbs +28 -0
  169. data/sig/factorix/http/retry_decorator.rbs +44 -0
  170. data/sig/factorix/http/retry_strategy.rbs +42 -0
  171. data/sig/factorix/info_json.rbs +19 -0
  172. data/sig/factorix/installed_mod.rbs +34 -0
  173. data/sig/factorix/mod.rbs +20 -0
  174. data/sig/factorix/mod_list.rbs +44 -0
  175. data/sig/factorix/mod_settings.rbs +47 -0
  176. data/sig/factorix/mod_state.rbs +18 -0
  177. data/sig/factorix/mod_version.rbs +23 -0
  178. data/sig/factorix/portal.rbs +37 -0
  179. data/sig/factorix/progress/download_handler.rbs +19 -0
  180. data/sig/factorix/progress/multi_presenter.rbs +15 -0
  181. data/sig/factorix/progress/presenter.rbs +17 -0
  182. data/sig/factorix/progress/presenter_adapter.rbs +17 -0
  183. data/sig/factorix/progress/scan_handler.rbs +16 -0
  184. data/sig/factorix/progress/upload_handler.rbs +17 -0
  185. data/sig/factorix/runtime/base.rbs +45 -0
  186. data/sig/factorix/runtime/linux.rbs +15 -0
  187. data/sig/factorix/runtime/mac_os.rbs +15 -0
  188. data/sig/factorix/runtime/user_configurable.rbs +13 -0
  189. data/sig/factorix/runtime/windows.rbs +23 -0
  190. data/sig/factorix/runtime/wsl.rbs +19 -0
  191. data/sig/factorix/runtime.rbs +9 -0
  192. data/sig/factorix/save_file.rbs +40 -0
  193. data/sig/factorix/ser_des/deserializer.rbs +49 -0
  194. data/sig/factorix/ser_des/serializer.rbs +45 -0
  195. data/sig/factorix/ser_des/signed_integer.rbs +37 -0
  196. data/sig/factorix/ser_des/unsigned_integer.rbs +37 -0
  197. data/sig/factorix/service_credential.rbs +19 -0
  198. data/sig/factorix/transfer/downloader.rbs +15 -0
  199. data/sig/factorix/transfer/uploader.rbs +21 -0
  200. data/sig/factorix.rbs +9 -0
  201. data/sig/tty/progressbar.rbs +18 -0
  202. metadata +431 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-progressbar"
4
+
5
+ module Factorix
6
+ module Progress
7
+ # Progress presenter implementation
8
+ #
9
+ # This class provides a simple progress presentation interface using tty-progressbar.
10
+ class Presenter
11
+ # Create a new progress presenter
12
+ #
13
+ # @param title [String] title of the progress presenter
14
+ # @param output [IO] output stream for the progress presenter
15
+ def initialize(title: "Progress", output: $stderr)
16
+ @title = title
17
+ @output = output
18
+ @tty_bar = nil
19
+ end
20
+
21
+ # Start the progress presentation with a specific total
22
+ #
23
+ # @param total [Integer] total size/count for progress tracking
24
+ # @param format [String] progress presenter format string (default: generic format)
25
+ # @return [void]
26
+ def start(total: nil, format: nil)
27
+ format ||= total.nil? ? "#{@title} [:bar] :current" : "#{@title} [:bar] :percent :current/:total"
28
+ @tty_bar = TTY::ProgressBar.new(
29
+ format,
30
+ total:,
31
+ output: @output,
32
+ frequency: 1, # Always update (important for testing with StringIO)
33
+ force: true # Force output even when not a TTY
34
+ )
35
+ end
36
+
37
+ # Update the progress presenter to a specific value
38
+ #
39
+ # @param current [Integer] current progress value
40
+ # @return [void]
41
+ def update(current=nil)
42
+ if current
43
+ @tty_bar&.current = current
44
+ else
45
+ # For indeterminate progress, just advance
46
+ @tty_bar&.advance
47
+ end
48
+ end
49
+
50
+ # Increase the total count dynamically
51
+ #
52
+ # @param increment [Integer] amount to add to current total
53
+ # @return [void]
54
+ def increase_total(increment)
55
+ return unless @tty_bar
56
+
57
+ current_total = @tty_bar.total || 0
58
+ @tty_bar.update(total: current_total + increment)
59
+ end
60
+
61
+ # Mark the progress presenter as finished
62
+ #
63
+ # @return [void]
64
+ def finish = @tty_bar&.finish
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Progress
5
+ # Adapter to make TTY::ProgressBar compatible with Presenter interface
6
+ #
7
+ # This adapter wraps a TTY::ProgressBar instance and provides the same
8
+ # interface as Progress::Presenter, allowing them to be used interchangeably.
9
+ class PresenterAdapter
10
+ # Create a new presenter adapter
11
+ #
12
+ # @param tty_bar [TTY::ProgressBar] the progress bar to adapt
13
+ # @param mutex [Mutex] mutex for thread-safe operations
14
+ def initialize(tty_bar, mutex)
15
+ @tty_bar = tty_bar
16
+ @mutex = mutex
17
+ @started = false
18
+ end
19
+
20
+ # Start the progress presentation
21
+ #
22
+ # @param total [Integer] total size/count for progress tracking
23
+ # @param format [String, nil] format string (ignored, already set in TTY::ProgressBar)
24
+ # @return [void]
25
+ def start(total:, format: nil)
26
+ _ = format # Acknowledge unused parameter
27
+ @mutex.synchronize do
28
+ @tty_bar.update(total:) if total
29
+ @tty_bar.start unless @started
30
+ @started = true
31
+ end
32
+ end
33
+
34
+ # Update the progress to a specific value
35
+ #
36
+ # @param current [Integer] current progress value
37
+ # @return [void]
38
+ def update(current) = @mutex.synchronize { @tty_bar.current = current }
39
+
40
+ # Mark the progress as finished
41
+ #
42
+ # @return [void]
43
+ def finish = @mutex.synchronize { @tty_bar.finish }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Progress
5
+ # Scan event handler for progress presenters
6
+ #
7
+ # This class listens to scan events and updates a progress presenter accordingly.
8
+ class ScanHandler
9
+ # Create a new scan handler
10
+ #
11
+ # @param presenter [Presenter, PresenterAdapter] progress presenter to update
12
+ def initialize(presenter) = @presenter = presenter
13
+
14
+ # Handle scan started event
15
+ #
16
+ # @param event [Dry::Events::Event] event with total payload
17
+ # @return [void]
18
+ def on_scan_started(event) = @presenter.start(total: event[:total])
19
+
20
+ # Handle scan progress event
21
+ #
22
+ # @param event [Dry::Events::Event] event with current payload
23
+ # @return [void]
24
+ def on_scan_progress(event) = @presenter.update(event[:current])
25
+
26
+ # Handle scan completed event
27
+ #
28
+ # @param event [Dry::Events::Event] event with total payload
29
+ # @return [void]
30
+ def on_scan_completed(_event) = @presenter.finish
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Progress
5
+ # Upload event handler for progress presenters
6
+ #
7
+ # This class listens to upload events and updates a progress presenter accordingly.
8
+ class UploadHandler
9
+ # Create a new upload handler
10
+ #
11
+ # @param presenter [Presenter, PresenterAdapter] progress presenter to update
12
+ def initialize(presenter) = @presenter = presenter
13
+
14
+ # Handle upload started event
15
+ #
16
+ # @param event [Dry::Events::Event] event with total_size payload
17
+ # @return [void]
18
+ def on_upload_started(event) = @presenter.start(total: event[:total_size], format: "Uploading [:bar] :percent :byte/:total_byte")
19
+
20
+ # Handle upload progress event
21
+ #
22
+ # @param event [Dry::Events::Event] event with current_size payload
23
+ # @return [void]
24
+ def on_upload_progress(event) = @presenter.update(event[:current_size])
25
+
26
+ # Handle upload completed event
27
+ #
28
+ # @param event [Dry::Events::Event] event with total_size payload
29
+ # @return [void]
30
+ def on_upload_completed(_event) = @presenter.finish
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Factorix
6
+ class Runtime
7
+ # Abstract base class for platform-specific runtime environments
8
+ #
9
+ # This class defines the interface that all platform-specific runtime
10
+ # implementations must provide. It provides common implementations for
11
+ # derived paths and XDG Base Directory specification support.
12
+ class Base
13
+ prepend UserConfigurable
14
+
15
+ # Hook called when Base is subclassed
16
+ #
17
+ # Automatically prepends UserConfigurable to all subclasses to ensure
18
+ # configuration support is available across all platform implementations.
19
+ #
20
+ # @param subclass [Class] the subclass being created
21
+ # @return [void]
22
+ def self.inherited(subclass)
23
+ super
24
+ subclass.prepend(UserConfigurable)
25
+ end
26
+
27
+ # Get the Factorio executable path
28
+ #
29
+ # Returns the path to the Factorio executable file.
30
+ # The exact location varies by platform and installation method (Steam, GOG, etc.).
31
+ #
32
+ # @return [Pathname] the Factorio executable path
33
+ # @raise [NotImplementedError] if not implemented by the platform
34
+ def executable_path = raise NotImplementedError, "#{self.class}#executable_path is not implemented"
35
+
36
+ # Get the Factorio user directory path
37
+ #
38
+ # This directory contains user-specific Factorio data including mods,
39
+ # saves, configuration files, and player data.
40
+ #
41
+ # @return [Pathname] the Factorio user directory
42
+ # @raise [NotImplementedError] if not implemented by the platform
43
+ def user_dir = raise NotImplementedError, "#{self.class}#user_dir is not implemented"
44
+
45
+ # Get the Factorio data directory path
46
+ #
47
+ # This directory contains the base game data and built-in expansion MODs.
48
+ # Each MOD (base, space-age, etc.) has its own subdirectory with info.json.
49
+ #
50
+ # @return [Pathname] the Factorio data directory
51
+ # @raise [NotImplementedError] if not implemented by the platform
52
+ def data_dir = raise NotImplementedError, "#{self.class}#data_dir is not implemented"
53
+
54
+ # Get the MOD directory path of Factorio
55
+ #
56
+ # This directory contains all installed MODs and MOD configuration files
57
+ # such as mod-list.json and mod-settings.dat.
58
+ #
59
+ # @return [Pathname] the MOD directory of Factorio
60
+ def mod_dir = user_dir + "mods"
61
+
62
+ # Get the save directory path of Factorio
63
+ #
64
+ # This directory contains all save game files.
65
+ #
66
+ # @return [Pathname] the save directory of Factorio
67
+ def save_dir = user_dir + "saves"
68
+
69
+ # Get the script-output directory path of Factorio
70
+ #
71
+ # This directory contains output files generated by Factorio Lua scripts.
72
+ #
73
+ # @return [Pathname] the script-output directory of Factorio
74
+ def script_output_dir = user_dir + "script-output"
75
+
76
+ # Get the path of the mod-list.json file
77
+ #
78
+ # This file contains the list of installed MODs and their enabled/disabled states.
79
+ #
80
+ # @return [Pathname] the path of the mod-list.json file
81
+ def mod_list_path = mod_dir + "mod-list.json"
82
+
83
+ # Get the path of the mod-settings.dat file
84
+ #
85
+ # This file contains the MOD settings for startup, runtime-global, and runtime-per-user.
86
+ #
87
+ # @return [Pathname] the path of the mod-settings.dat file
88
+ def mod_settings_path = mod_dir + "mod-settings.dat"
89
+
90
+ # Get the path of the player-data.json file
91
+ #
92
+ # This file contains player-specific data including authentication credentials,
93
+ # preferences, and game statistics.
94
+ #
95
+ # @return [Pathname] the path of the player-data.json file
96
+ def player_data_path = user_dir + "player-data.json"
97
+
98
+ # Get the path of the current Factorio log file
99
+ #
100
+ # This file contains the log output from the current Factorio session.
101
+ #
102
+ # @return [Pathname] the path of the current log file
103
+ def current_log_path = user_dir + "factorio-current.log"
104
+
105
+ # Get the path of the previous Factorio log file
106
+ #
107
+ # This file contains the log output from the previous Factorio session.
108
+ #
109
+ # @return [Pathname] the path of the previous log file
110
+ def previous_log_path = user_dir + "factorio-previous.log"
111
+
112
+ # Get the XDG cache home directory
113
+ #
114
+ # Returns the base directory for user-specific cache data according to
115
+ # the XDG Base Directory Specification. On platforms that don't follow
116
+ # XDG conventions, this returns the platform-appropriate equivalent.
117
+ #
118
+ # @return [Pathname] the XDG cache home directory
119
+ def xdg_cache_home_dir = Pathname(ENV.fetch("XDG_CACHE_HOME") { default_cache_home_dir })
120
+
121
+ # Get the XDG config home directory
122
+ #
123
+ # Returns the base directory for user-specific configuration files according to
124
+ # the XDG Base Directory Specification. On platforms that don't follow
125
+ # XDG conventions, this returns the platform-appropriate equivalent.
126
+ #
127
+ # @return [Pathname] the XDG config home directory
128
+ def xdg_config_home_dir = Pathname(ENV.fetch("XDG_CONFIG_HOME") { default_config_home_dir })
129
+
130
+ # Get the XDG data home directory
131
+ #
132
+ # Returns the base directory for user-specific data files according to
133
+ # the XDG Base Directory Specification. On platforms that don't follow
134
+ # XDG conventions, this returns the platform-appropriate equivalent.
135
+ #
136
+ # @return [Pathname] the XDG data home directory
137
+ def xdg_data_home_dir = Pathname(ENV.fetch("XDG_DATA_HOME") { default_data_home_dir })
138
+
139
+ # Get the XDG state home directory
140
+ #
141
+ # Returns the base directory for user-specific state files (logs, history, etc.)
142
+ # according to the XDG Base Directory Specification. On platforms that don't follow
143
+ # XDG conventions, this returns the platform-appropriate equivalent.
144
+ #
145
+ # @return [Pathname] the XDG state home directory
146
+ def xdg_state_home_dir = Pathname(ENV.fetch("XDG_STATE_HOME") { default_state_home_dir })
147
+
148
+ # Get the Factorix cache directory
149
+ #
150
+ # Returns the directory where Factorix stores its cache data.
151
+ #
152
+ # @return [Pathname] the Factorix cache directory
153
+ def factorix_cache_dir = xdg_cache_home_dir / "factorix"
154
+
155
+ # Get the Factorix configuration file path
156
+ #
157
+ # Returns the path to the Factorix configuration file.
158
+ #
159
+ # @return [Pathname] the Factorix configuration file path
160
+ def factorix_config_path = xdg_config_home_dir / "factorix" / "config.rb"
161
+
162
+ # Get the Factorix log file path
163
+ #
164
+ # Returns the path to the Factorix log file.
165
+ #
166
+ # @return [Pathname] the Factorix log file path
167
+ def factorix_log_path = xdg_state_home_dir / "factorix" / "factorix.log"
168
+
169
+ # Get the Factorio lock file path
170
+ #
171
+ # The lock file is created by Factorio when the game is running
172
+ # and is used to detect if the game is already running.
173
+ #
174
+ # @return [Pathname] the lock file path
175
+ def lock_path = user_dir / ".lock"
176
+
177
+ # Check if Factorio is currently running
178
+ #
179
+ # Because the game becomes daemonized on launch, we cannot use the process ID
180
+ # or process groups to check if the game is running. Instead, we check the
181
+ # existence of the lock file.
182
+ #
183
+ # @return [Boolean] true if the game is running, false otherwise
184
+ def running? = lock_path.exist?
185
+
186
+ # Launch Factorio with the specified options
187
+ #
188
+ # @param args [Array<String>] command-line arguments to pass to Factorio
189
+ # @param async [Boolean] whether to launch asynchronously (default: true)
190
+ # @return [void]
191
+ # @raise [RuntimeError] if the game is already running
192
+ def launch(*, async: true)
193
+ if async
194
+ spawn([executable_path.to_s, "factorio"], *, out: IO::NULL)
195
+ else
196
+ system([executable_path.to_s, "factorio"], *)
197
+ end
198
+ end
199
+
200
+ # Get the default cache home directory for this platform
201
+ #
202
+ # This method should be overridden by platform-specific subclasses
203
+ # to provide appropriate defaults.
204
+ #
205
+ # @return [Pathname] the default cache home directory
206
+ private def default_cache_home_dir = Pathname(Dir.home) + ".cache"
207
+
208
+ # Get the default config home directory for this platform
209
+ #
210
+ # This method should be overridden by platform-specific subclasses
211
+ # to provide appropriate defaults.
212
+ #
213
+ # @return [Pathname] the default config home directory
214
+ private def default_config_home_dir = Pathname(Dir.home) + ".config"
215
+
216
+ # Get the default data home directory for this platform
217
+ #
218
+ # This method should be overridden by platform-specific subclasses
219
+ # to provide appropriate defaults.
220
+ #
221
+ # @return [Pathname] the default data home directory
222
+ private def default_data_home_dir = Pathname(Dir.home) + ".local/share"
223
+
224
+ # Get the default state home directory for this platform
225
+ #
226
+ # This method should be overridden by platform-specific subclasses
227
+ # to provide appropriate defaults.
228
+ #
229
+ # @return [Pathname] the default state home directory
230
+ private def default_state_home_dir = Pathname(Dir.home) + ".local/state"
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class Runtime
5
+ # Linux runtime environment
6
+ #
7
+ # Provides default paths for Steam installation on Linux.
8
+ # Users with non-Steam installations (standalone, Flatpak, Snap, etc.)
9
+ # should configure paths via the configuration file.
10
+ # See Runtime::UserConfigurable for configuration instructions.
11
+ class Linux < Base
12
+ # Get the Factorio executable path
13
+ #
14
+ # Returns the default Steam installation path on Linux.
15
+ #
16
+ # @return [Pathname] the Factorio executable path
17
+ def executable_path = Pathname(Dir.home) + ".steam/steam/steamapps/common/Factorio/bin/x64/factorio"
18
+
19
+ # Get the Factorio user directory path
20
+ #
21
+ # @return [Pathname] the Factorio user directory
22
+ def user_dir = Pathname(Dir.home) + ".factorio"
23
+
24
+ # Get the Factorio data directory path
25
+ #
26
+ # This directory contains the base game data and built-in expansion MODs.
27
+ #
28
+ # @return [Pathname] the Factorio data directory
29
+ def data_dir = Pathname(Dir.home) + ".steam/steam/steamapps/common/Factorio/data"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class Runtime
5
+ # macOS runtime environment
6
+ #
7
+ # This implementation assumes Factorio is installed via Steam.
8
+ # For other installation methods (GOG, itch.io, standalone), users should
9
+ # configure the installation path in the Factorix configuration file.
10
+ class MacOS < Base
11
+ # Get the Factorio executable path
12
+ #
13
+ # Returns the default Steam installation path on macOS.
14
+ #
15
+ # @return [Pathname] the Factorio executable path
16
+ def executable_path = Pathname(Dir.home) + "Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/MacOS/factorio"
17
+
18
+ # Get the Factorio user directory path
19
+ #
20
+ # @return [Pathname] the Factorio user directory
21
+ def user_dir = Pathname(Dir.home) + "Library/Application Support/factorio"
22
+
23
+ # Get the Factorio data directory path
24
+ #
25
+ # This directory contains the base game data and built-in expansion MODs.
26
+ #
27
+ # @return [Pathname] the Factorio data directory
28
+ def data_dir = Pathname(Dir.home) + "Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/data"
29
+
30
+ # Get the Factorix log file path
31
+ #
32
+ # Returns the path to the Factorix log file using macOS convention.
33
+ #
34
+ # @return [Pathname] the Factorix log file path
35
+ def factorix_log_path = Pathname(Dir.home) + "Library/Logs/factorix/factorix.log"
36
+
37
+ # Get the default cache home directory for macOS
38
+ #
39
+ # @return [Pathname] the default cache home directory
40
+ private def default_cache_home_dir = Pathname(Dir.home) + "Library/Caches"
41
+
42
+ # Get the default config home directory for macOS
43
+ #
44
+ # @return [Pathname] the default config home directory
45
+ private def default_config_home_dir = Pathname(Dir.home) + "Library/Application Support"
46
+
47
+ # Get the default data home directory for macOS
48
+ #
49
+ # @return [Pathname] the default data home directory
50
+ private def default_data_home_dir = Pathname(Dir.home) + "Library/Application Support"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class Runtime
5
+ # Provides user-configurable path overrides for Runtime
6
+ #
7
+ # This module is prepended to Runtime::Base to allow users to override
8
+ # auto-detected paths via configuration. When a configured path is available,
9
+ # it is used instead of platform-specific auto-detection.
10
+ #
11
+ # Configuration is done via Application.config:
12
+ #
13
+ # @example Configure paths in config file
14
+ # Factorix::Application.configure do |config|
15
+ # config.runtime.executable_path = "/opt/factorio/bin/x64/factorio"
16
+ # config.runtime.user_dir = "/home/user/.factorio"
17
+ # end
18
+ #
19
+ # All path resolution decisions are logged at DEBUG level.
20
+ module UserConfigurable
21
+ # Get the Factorio executable path
22
+ #
23
+ # Returns the configured executable path if available, otherwise falls back
24
+ # to platform-specific auto-detection.
25
+ #
26
+ # @return [Pathname] the Factorio executable path
27
+ # @raise [ConfigurationError] if auto-detection is not supported and no configuration is provided
28
+ def executable_path = configurable_path(:executable_path, example_path: "/path/to/factorio") { super }
29
+
30
+ # Get the Factorio user directory path
31
+ #
32
+ # Returns the configured user_dir if available, otherwise falls back
33
+ # to platform-specific auto-detection.
34
+ #
35
+ # @return [Pathname] the Factorio user directory
36
+ # @raise [ConfigurationError] if auto-detection is not supported and no configuration is provided
37
+ def user_dir = configurable_path(:user_dir, example_path: "/path/to/factorio/user/dir") { super }
38
+
39
+ # Get the Factorio data directory path
40
+ #
41
+ # Returns the configured data_dir if available, otherwise falls back
42
+ # to platform-specific auto-detection.
43
+ #
44
+ # @return [Pathname] the Factorio data directory
45
+ # @raise [ConfigurationError] if auto-detection is not supported and no configuration is provided
46
+ def data_dir = configurable_path(:data_dir, example_path: "/path/to/factorio/data") { super }
47
+
48
+ private def configurable_path(name, example_path:)
49
+ if (configured = Application.config.runtime.public_send(name))
50
+ Application[:logger].debug("Using configured #{name}", path: configured.to_s)
51
+ configured
52
+ else
53
+ Application[:logger].debug("No configuration for #{name}, using auto-detection")
54
+ yield.tap {|path| Application[:logger].debug("Auto-detected #{name}", path: path.to_s) }
55
+ end
56
+ rescue NotImplementedError => e
57
+ Application[:logger].error("Auto-detection failed and no configuration provided", error: e.message)
58
+ raise ConfigurationError, <<~MESSAGE
59
+ #{name} not configured and auto-detection is not supported for this platform.
60
+ Please configure it in #{Application[:runtime].factorix_config_path}:
61
+
62
+ Factorix::Application.configure do |config|
63
+ config.runtime.#{name} = "#{example_path}"
64
+ end
65
+ MESSAGE
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class Runtime
5
+ # Windows runtime environment
6
+ #
7
+ # This implementation uses Windows environment variables (APPDATA, LOCALAPPDATA)
8
+ # to locate Factorio directories. It assumes Factorio is installed via Steam.
9
+ # For other installation methods (GOG, itch.io, standalone), users should
10
+ # configure the installation path in the Factorix configuration file.
11
+ class Windows < Base
12
+ # Initialize Windows runtime environment
13
+ #
14
+ # @param path [WindowsPath] the path provider (for dependency injection)
15
+ def initialize(path: WindowsPath.new)
16
+ super()
17
+ @path = path
18
+ end
19
+
20
+ # Get the Factorio executable path
21
+ #
22
+ # Returns the default Steam installation path on Windows.
23
+ #
24
+ # @return [Pathname] the Factorio executable path
25
+ def executable_path = path.program_files_x86.join("Steam/steamapps/common/Factorio/bin/x64/factorio.exe")
26
+
27
+ # Get the Factorio user directory path
28
+ #
29
+ # @return [Pathname] the Factorio user directory
30
+ def user_dir = path.app_data.join("Factorio")
31
+
32
+ # Get the Factorio data directory path
33
+ #
34
+ # This directory contains the base game data and built-in expansion MODs.
35
+ #
36
+ # @return [Pathname] the Factorio data directory
37
+ def data_dir = path.program_files_x86.join("Steam/steamapps/common/Factorio/data")
38
+
39
+ private attr_reader :path
40
+
41
+ # Get the default cache home directory for Windows
42
+ #
43
+ # @return [Pathname] the default cache home directory
44
+ private def default_cache_home_dir = path.local_app_data
45
+
46
+ # Get the default config home directory for Windows
47
+ #
48
+ # @return [Pathname] the default config home directory
49
+ private def default_config_home_dir = path.app_data
50
+
51
+ # Get the default data home directory for Windows
52
+ #
53
+ # @return [Pathname] the default data home directory
54
+ private def default_data_home_dir = path.local_app_data
55
+
56
+ # Windows-specific path provider
57
+ class WindowsPath
58
+ # Get the Program Files (x86) directory path
59
+ #
60
+ # @return [Pathname] the Program Files (x86) directory
61
+ def program_files_x86 = Pathname(convert_separator(ENV.fetch("ProgramFiles(x86)")))
62
+
63
+ # Get the AppData directory path
64
+ #
65
+ # @return [Pathname] the AppData directory
66
+ def app_data = Pathname(convert_separator(ENV.fetch("APPDATA")))
67
+
68
+ # Get the Local AppData directory path
69
+ #
70
+ # @return [Pathname] the Local AppData directory
71
+ def local_app_data = Pathname(convert_separator(ENV.fetch("LOCALAPPDATA")))
72
+
73
+ # Convert Windows path separators to forward slashes for aesthetics and consistency
74
+ #
75
+ # While Ruby accepts both separators, normalizing to forward slashes prevents
76
+ # mixing backslashes and forward slashes when concatenating paths with Pathname#+,
77
+ # which improves readability.
78
+ #
79
+ # @param path_string [String] the path string with backslashes
80
+ # @return [String] the path string with forward slashes
81
+ private def convert_separator(path_string) = path_string.tr(File::ALT_SEPARATOR, File::SEPARATOR)
82
+ end
83
+ end
84
+ end
85
+ end