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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Factorix
6
+ class CLI
7
+ module Commands
8
+ # Display Factorio and Factorix paths
9
+ #
10
+ # This command outputs all paths managed by the runtime environment.
11
+ #
12
+ # @example
13
+ # $ factorix path
14
+ # executable_path /path/to/factorio
15
+ # user_dir /path/to/user
16
+ # ...
17
+ class Path < Base
18
+ # Mapping from path type keys to runtime method names
19
+ PATH_TYPES = {
20
+ "executable_path" => :executable_path,
21
+ "data_dir" => :data_dir,
22
+ "user_dir" => :user_dir,
23
+ "mod_dir" => :mod_dir,
24
+ "save_dir" => :save_dir,
25
+ "script_output_dir" => :script_output_dir,
26
+ "mod_list_path" => :mod_list_path,
27
+ "mod_settings_path" => :mod_settings_path,
28
+ "player_data_path" => :player_data_path,
29
+ "lock_path" => :lock_path,
30
+ "current_log_path" => :current_log_path,
31
+ "previous_log_path" => :previous_log_path,
32
+ "factorix_cache_dir" => :factorix_cache_dir,
33
+ "factorix_config_path" => :factorix_config_path,
34
+ "factorix_log_path" => :factorix_log_path
35
+ }.freeze
36
+ private_constant :PATH_TYPES
37
+
38
+ # @!parse
39
+ # # @return [Runtime::Base]
40
+ # attr_reader :runtime
41
+ # # @return [Dry::Logger::Dispatcher]
42
+ # attr_reader :logger
43
+ include Import[:runtime, :logger]
44
+
45
+ desc "Display Factorio and Factorix paths"
46
+
47
+ example [
48
+ " # Display paths in table format",
49
+ "--json # Display paths in JSON format"
50
+ ]
51
+
52
+ option :json, type: :flag, default: false, desc: "Output in JSON format"
53
+
54
+ # Execute the path command
55
+ #
56
+ # @param json [Boolean] output in JSON format
57
+ # @return [void]
58
+ def call(json:, **)
59
+ logger.debug("Displaying all paths")
60
+
61
+ result = PATH_TYPES.transform_values {|method_name| runtime.public_send(method_name).to_s }
62
+
63
+ if json
64
+ puts JSON.pretty_generate(result)
65
+ else
66
+ output_table(result)
67
+ end
68
+ end
69
+
70
+ private def output_table(result)
71
+ key_width = result.keys.map(&:length).max
72
+ result.each do |key, value|
73
+ puts "%-#{key_width}s %s" % [key, value]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class CLI
5
+ module Commands
6
+ # Mixin for commands that require the game to be stopped
7
+ #
8
+ # This module provides automatic validation that the game is not running
9
+ # before executing commands that modify MOD installation state or
10
+ # mod-list.json/mod-settings.dat files.
11
+ #
12
+ # Prepend this module in commands that should not run while the game is active
13
+ # (e.g., install, uninstall, enable, disable)
14
+ module RequiresGameStopped
15
+ # Wrapper for command call that checks game state
16
+ # @param options [Hash] command options passed to the original call method
17
+ # @return [void]
18
+ def call(**options)
19
+ check_game_stopped
20
+ super
21
+ end
22
+
23
+ private def check_game_stopped
24
+ return unless runtime.running?
25
+
26
+ logger.error("Operation blocked: game is running")
27
+ raise GameRunningError, "Cannot perform this operation while Factorio is running. Please stop the game and try again."
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ class CLI
5
+ module Commands
6
+ # Display Factorix version
7
+ #
8
+ # This command outputs the current version of the Factorix gem.
9
+ #
10
+ # @example
11
+ # $ factorix version
12
+ # 0.1.0
13
+ class Version < Base
14
+ desc "Display Factorix version"
15
+
16
+ # Execute the version command
17
+ #
18
+ # Outputs the current version of the Factorix gem to stdout.
19
+ #
20
+ # @return [void]
21
+ def call(**) = puts VERSION
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ module Factorix
6
+ # Command-line interface for Factorix
7
+ #
8
+ # This class serves as the registry for all CLI commands using dry-cli.
9
+ # Commands are registered with their names and mapped to command classes.
10
+ #
11
+ # @example Running the CLI
12
+ # Dry::CLI.new(Factorix::CLI).call
13
+ class CLI
14
+ extend Dry::CLI::Registry
15
+
16
+ register "version", Commands::Version
17
+ register "man", Commands::Man
18
+ register "launch", Commands::Launch
19
+ register "path", Commands::Path
20
+ register "completion", Commands::Completion
21
+ register "mod check", Commands::MOD::Check
22
+ register "mod list", Commands::MOD::List
23
+ register "mod show", Commands::MOD::Show
24
+ register "mod enable", Commands::MOD::Enable
25
+ register "mod disable", Commands::MOD::Disable
26
+ register "mod install", Commands::MOD::Install
27
+ register "mod uninstall", Commands::MOD::Uninstall
28
+ register "mod update", Commands::MOD::Update
29
+ register "mod download", Commands::MOD::Download
30
+ register "mod upload", Commands::MOD::Upload
31
+ register "mod edit", Commands::MOD::Edit
32
+ register "mod search", Commands::MOD::Search
33
+ register "mod sync", Commands::MOD::Sync
34
+ register "mod image list", Commands::MOD::Image::List
35
+ register "mod image add", Commands::MOD::Image::Add
36
+ register "mod image edit", Commands::MOD::Image::Edit
37
+ register "mod settings dump", Commands::MOD::Settings::Dump
38
+ register "mod settings restore", Commands::MOD::Settings::Restore
39
+ register "cache stat", Commands::Cache::Stat
40
+ register "cache evict", Commands::Cache::Evict
41
+ end
42
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Dependency
5
+ Edge = Data.define(:from_mod, :to_mod, :type, :version_requirement)
6
+
7
+ # Represents a dependency edge in the dependency graph
8
+ #
9
+ # Each edge represents a dependency relationship from one MOD (dependent)
10
+ # to another MOD (dependency). The edge type indicates the nature of the
11
+ # relationship (required, optional, incompatible, etc.).
12
+ class Edge
13
+ # @!attribute [r] from_mod
14
+ # @return [Factorix::MOD] MOD object (the dependent)
15
+ # @!attribute [r] to_mod
16
+ # @return [Factorix::MOD] MOD object (the dependency)
17
+ # @!attribute [r] type
18
+ # @return [Symbol] dependency type
19
+ # @!attribute [r] version_requirement
20
+ # @return [MODVersionRequirement, nil] version requirement
21
+
22
+ # Dependency types (from Factorix::Dependency::Entry)
23
+ REQUIRED = Entry::REQUIRED
24
+ public_constant :REQUIRED
25
+ OPTIONAL = Entry::OPTIONAL
26
+ public_constant :OPTIONAL
27
+ HIDDEN_OPTIONAL = Entry::HIDDEN_OPTIONAL
28
+ public_constant :HIDDEN_OPTIONAL
29
+ INCOMPATIBLE = Entry::INCOMPATIBLE
30
+ public_constant :INCOMPATIBLE
31
+ LOAD_NEUTRAL = Entry::LOAD_NEUTRAL
32
+ public_constant :LOAD_NEUTRAL
33
+
34
+ # @param from_mod [Factorix::MOD] The dependent MOD
35
+ # @param to_mod [Factorix::MOD] The dependency MOD
36
+ # @param type [Symbol] The dependency type (:required, :optional, :hidden, :incompatible, :load_neutral)
37
+ # @param version_requirement [MODVersionRequirement, nil] Version requirement (optional)
38
+ def initialize(from_mod:, to_mod:, type:, version_requirement: nil) = super
39
+
40
+ # Check if this is a required dependency
41
+ #
42
+ # @return [Boolean]
43
+ def required? = type == REQUIRED
44
+
45
+ # Check if this is an optional dependency
46
+ #
47
+ # @return [Boolean]
48
+ def optional? = type == OPTIONAL || type == HIDDEN_OPTIONAL
49
+
50
+ # Check if this is a hidden optional dependency
51
+ #
52
+ # @return [Boolean]
53
+ def hidden_optional? = type == HIDDEN_OPTIONAL
54
+
55
+ # Check if this is an incompatibility relationship
56
+ #
57
+ # @return [Boolean]
58
+ def incompatible? = type == INCOMPATIBLE
59
+
60
+ # Check if this is a load-neutral dependency
61
+ #
62
+ # @return [Boolean]
63
+ def load_neutral? = type == LOAD_NEUTRAL
64
+
65
+ # Check if the given version satisfies this edge's version requirement
66
+ #
67
+ # @param version [Factorix::MODVersion] The version to check
68
+ # @return [Boolean] true if satisfied or no requirement exists
69
+ def satisfied_by?(version)
70
+ return true unless version_requirement
71
+
72
+ version_requirement.satisfied_by?(version)
73
+ end
74
+
75
+ # String representation of the edge
76
+ #
77
+ # @return [String]
78
+ def to_s
79
+ requirement_str = version_requirement ? " #{version_requirement}" : ""
80
+ "#{from_mod} --[#{type}#{requirement_str}]--> #{to_mod}"
81
+ end
82
+
83
+ # Detailed inspection string
84
+ #
85
+ # @return [String]
86
+ def inspect = "#<#{self.class.name} #{self}>"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Dependency
5
+ # Define Entry as an immutable data class
6
+ Entry = Data.define(:mod, :type, :version_requirement)
7
+
8
+ # Represents a single MOD dependency
9
+ #
10
+ # This class encapsulates a MOD dependency with its type (required, optional, etc.)
11
+ # and optional version requirement.
12
+ #
13
+ # @example Creating dependencies
14
+ # # Required dependency on base MOD
15
+ # base_mod = MOD[name: "base"]
16
+ # dep1 = Dependency::Entry[mod: base_mod, type: :required, version_requirement: nil]
17
+ #
18
+ # # Optional dependency with version requirement
19
+ # some_mod = MOD[name: "some-mod"]
20
+ # requirement = MODVersionRequirement[operator: ">=", version: MODVersion.from_string("1.2.0")]
21
+ # dep2 = Dependency::Entry[mod: some_mod, type: :optional, version_requirement: requirement]
22
+ #
23
+ # # Incompatible MOD
24
+ # bad_mod = MOD[name: "bad-mod"]
25
+ # dep3 = Dependency::Entry[mod: bad_mod, type: :incompatible, version_requirement: nil]
26
+ class Entry
27
+ # @!attribute [r] mod
28
+ # @return [MOD] The dependent MOD
29
+ # @!attribute [r] type
30
+ # @return [Symbol] Type of dependency (:required, :optional, :hidden, :incompatible, :load_neutral)
31
+ # @!attribute [r] version_requirement
32
+ # @return [MODVersionRequirement, nil] Version requirement (nil if no requirement)
33
+
34
+ # Dependency type constants
35
+ REQUIRED = :required
36
+ public_constant :REQUIRED
37
+ OPTIONAL = :optional
38
+ public_constant :OPTIONAL
39
+ HIDDEN_OPTIONAL = :hidden
40
+ public_constant :HIDDEN_OPTIONAL
41
+ INCOMPATIBLE = :incompatible
42
+ public_constant :INCOMPATIBLE
43
+ LOAD_NEUTRAL = :load_neutral
44
+ public_constant :LOAD_NEUTRAL
45
+
46
+ VALID_TYPES = [REQUIRED, OPTIONAL, HIDDEN_OPTIONAL, INCOMPATIBLE, LOAD_NEUTRAL].freeze
47
+ private_constant :VALID_TYPES
48
+
49
+ # Create a new Entry
50
+ #
51
+ # @param mod [MOD] The dependent MOD
52
+ # @param type [Symbol] Type of dependency (:required, :optional, :hidden, :incompatible, :load_neutral)
53
+ # @param version_requirement [MODVersionRequirement, nil] Version requirement (nil if no requirement)
54
+ # @return [Entry]
55
+ # @raise [ArgumentError] if mod is not a MOD instance
56
+ # @raise [ArgumentError] if type is not valid
57
+ # @raise [ArgumentError] if version_requirement is not nil or MODVersionRequirement
58
+ def initialize(mod:, type:, version_requirement: nil)
59
+ unless mod.is_a?(MOD)
60
+ raise ArgumentError, "mod must be a MOD instance, got #{mod.class}"
61
+ end
62
+
63
+ unless VALID_TYPES.include?(type)
64
+ raise ArgumentError, "Invalid dependency type: #{type}. Must be one of: #{VALID_TYPES.join(", ")}"
65
+ end
66
+
67
+ if version_requirement && !version_requirement.is_a?(MODVersionRequirement)
68
+ raise ArgumentError, "version_requirement must be a MODVersionRequirement or nil, got #{version_requirement.class}"
69
+ end
70
+
71
+ super
72
+ end
73
+
74
+ # Check if this is a required dependency
75
+ #
76
+ # @return [Boolean] true if dependency is required
77
+ def required? = type == REQUIRED
78
+
79
+ # Check if this is an optional dependency (including hidden optional)
80
+ #
81
+ # @return [Boolean] true if dependency is optional or hidden optional
82
+ def optional? = type == OPTIONAL || type == HIDDEN_OPTIONAL
83
+
84
+ # Check if this is an incompatible (conflicting) dependency
85
+ #
86
+ # @return [Boolean] true if dependency is incompatible
87
+ def incompatible? = type == INCOMPATIBLE
88
+
89
+ # Check if this dependency does not affect load order
90
+ #
91
+ # @return [Boolean] true if dependency is load-neutral
92
+ def load_neutral? = type == LOAD_NEUTRAL
93
+
94
+ # Check if a given version satisfies this dependency's version requirement
95
+ #
96
+ # @param version [MODVersion] Version to check
97
+ # @return [Boolean] true if version requirement is satisfied, or true if no requirement exists
98
+ def satisfied_by?(version)
99
+ return true unless version_requirement
100
+
101
+ version_requirement.satisfied_by?(version)
102
+ end
103
+
104
+ # Return string representation of the dependency
105
+ #
106
+ # @return [String] String representation (e.g., "? some-mod >= 1.2.0")
107
+ def to_s
108
+ result = case type
109
+ when REQUIRED then ""
110
+ when OPTIONAL then "? "
111
+ when HIDDEN_OPTIONAL then "(?) "
112
+ when INCOMPATIBLE then "! "
113
+ when LOAD_NEUTRAL then "~ "
114
+ else
115
+ raise ArgumentError, "Unexpected dependency type: #{type}"
116
+ end
117
+
118
+ result += mod.name
119
+ result += " #{version_requirement}" if version_requirement
120
+ result
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Factorix
4
+ module Dependency
5
+ class Graph
6
+ # Builds a dependency graph from installed MODs and MOD list
7
+ #
8
+ # The Builder constructs a Graph by:
9
+ # 1. Creating nodes for all installed MODs
10
+ # 2. Setting enabled state from mod-list.json
11
+ # 3. Creating edges from dependency information in info.json
12
+ class Builder
13
+ # Build a dependency graph from current state
14
+ #
15
+ # @param installed_mods [Array<Factorix::InstalledMOD>] Installed MODs from MOD directory
16
+ # @param mod_list [Factorix::MODList] MOD list from mod-list.json
17
+ # @return [Factorix::Dependency::Graph] The constructed graph
18
+ def self.build(installed_mods:, mod_list:) = new(installed_mods:, mod_list:).build
19
+
20
+ # @param installed_mods [Array<Factorix::InstalledMOD>] Installed MODs
21
+ # @param mod_list [Factorix::MODList] MOD list
22
+ def initialize(installed_mods:, mod_list:)
23
+ @installed_mods = installed_mods
24
+ @mod_list = mod_list
25
+ end
26
+
27
+ # Build the graph
28
+ #
29
+ # @return [Factorix::Dependency::Graph] The constructed graph
30
+ def build
31
+ graph = Graph.new
32
+
33
+ unique_mods = @installed_mods.map(&:mod)
34
+ unique_mods.uniq!
35
+
36
+ unique_mods.each do |mod|
37
+ add_node_for_mod(graph, mod)
38
+ end
39
+
40
+ # Only active versions contribute edges
41
+ active_versions = graph.nodes.to_h {|node| [node.mod, node.version] }
42
+
43
+ @installed_mods.each do |installed_mod|
44
+ next unless active_versions[installed_mod.mod] == installed_mod.version
45
+
46
+ add_edges_for_dependencies(graph, installed_mod)
47
+ end
48
+
49
+ graph
50
+ end
51
+
52
+ private def add_node_for_mod(graph, mod)
53
+ version = select_version_for_mod(mod)
54
+ enabled = mod_enabled?(mod)
55
+
56
+ node = Node.new(mod:, version:, enabled:, installed: true)
57
+ graph.add_node(node)
58
+ end
59
+
60
+ # Select which version to use for a MOD
61
+ #
62
+ # @param mod [Factorix::MOD] The MOD
63
+ # @return [Factorix::MODVersion] The selected version
64
+ private def select_version_for_mod(mod)
65
+ if @mod_list.exist?(mod)
66
+ specified_version = @mod_list.version(mod)
67
+ if specified_version
68
+ installed_with_version = @installed_mods.find {|im| im.mod == mod && im.version == specified_version }
69
+ return specified_version if installed_with_version
70
+ end
71
+ end
72
+
73
+ versions_for_mod = @installed_mods.select {|im| im.mod == mod }
74
+ versions_for_mod.max_by(&:version).version
75
+ end
76
+
77
+ # Add edges for a MOD's dependencies
78
+ #
79
+ # @param graph [Factorix::Dependency::Graph] The graph to add to
80
+ # @param installed_mod [Factorix::InstalledMOD] The installed MOD
81
+ # @return [void]
82
+ private def add_edges_for_dependencies(graph, installed_mod)
83
+ from_mod = installed_mod.mod
84
+ dependencies = installed_mod.info.dependencies || []
85
+
86
+ dependencies.each do |dependency|
87
+ # Skip only base MOD (always available and cannot be disabled)
88
+ # Expansion MODs can be disabled, so they must be validated
89
+ next if dependency.mod.base?
90
+
91
+ edge = Edge.new(from_mod:, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement)
92
+ graph.add_edge(edge)
93
+ end
94
+ end
95
+
96
+ # Check if a MOD is enabled in the MOD list
97
+ #
98
+ # @param mod [Factorix::MOD] The MOD to check
99
+ # @return [Boolean] true if enabled, false otherwise
100
+ private def mod_enabled?(mod)
101
+ return false unless @mod_list.exist?(mod)
102
+
103
+ @mod_list.enabled?(mod)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end