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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/completion/_factorix.bash +202 -0
- data/completion/_factorix.fish +197 -0
- data/completion/_factorix.zsh +376 -0
- data/doc/factorix.1 +377 -0
- data/exe/factorix +20 -0
- data/lib/factorix/api/category.rb +69 -0
- data/lib/factorix/api/image.rb +35 -0
- data/lib/factorix/api/license.rb +71 -0
- data/lib/factorix/api/mod_download_api.rb +66 -0
- data/lib/factorix/api/mod_info.rb +166 -0
- data/lib/factorix/api/mod_management_api.rb +237 -0
- data/lib/factorix/api/mod_portal_api.rb +204 -0
- data/lib/factorix/api/release.rb +49 -0
- data/lib/factorix/api/tag.rb +95 -0
- data/lib/factorix/api.rb +7 -0
- data/lib/factorix/api_credential.rb +54 -0
- data/lib/factorix/application.rb +218 -0
- data/lib/factorix/cache/file_system.rb +307 -0
- data/lib/factorix/cli/commands/backup_support.rb +46 -0
- data/lib/factorix/cli/commands/base.rb +90 -0
- data/lib/factorix/cli/commands/cache/evict.rb +180 -0
- data/lib/factorix/cli/commands/cache/stat.rb +201 -0
- data/lib/factorix/cli/commands/command_wrapper.rb +71 -0
- data/lib/factorix/cli/commands/completion.rb +83 -0
- data/lib/factorix/cli/commands/confirmable.rb +53 -0
- data/lib/factorix/cli/commands/download_support.rb +123 -0
- data/lib/factorix/cli/commands/launch.rb +79 -0
- data/lib/factorix/cli/commands/man.rb +29 -0
- data/lib/factorix/cli/commands/mod/check.rb +99 -0
- data/lib/factorix/cli/commands/mod/disable.rb +188 -0
- data/lib/factorix/cli/commands/mod/download.rb +291 -0
- data/lib/factorix/cli/commands/mod/edit.rb +114 -0
- data/lib/factorix/cli/commands/mod/enable.rb +216 -0
- data/lib/factorix/cli/commands/mod/image/add.rb +47 -0
- data/lib/factorix/cli/commands/mod/image/edit.rb +41 -0
- data/lib/factorix/cli/commands/mod/image/list.rb +74 -0
- data/lib/factorix/cli/commands/mod/install.rb +443 -0
- data/lib/factorix/cli/commands/mod/list.rb +372 -0
- data/lib/factorix/cli/commands/mod/search.rb +134 -0
- data/lib/factorix/cli/commands/mod/settings/dump.rb +88 -0
- data/lib/factorix/cli/commands/mod/settings/restore.rb +101 -0
- data/lib/factorix/cli/commands/mod/show.rb +202 -0
- data/lib/factorix/cli/commands/mod/sync.rb +299 -0
- data/lib/factorix/cli/commands/mod/uninstall.rb +325 -0
- data/lib/factorix/cli/commands/mod/update.rb +222 -0
- data/lib/factorix/cli/commands/mod/upload.rb +90 -0
- data/lib/factorix/cli/commands/path.rb +79 -0
- data/lib/factorix/cli/commands/requires_game_stopped.rb +32 -0
- data/lib/factorix/cli/commands/version.rb +25 -0
- data/lib/factorix/cli.rb +42 -0
- data/lib/factorix/dependency/edge.rb +89 -0
- data/lib/factorix/dependency/entry.rb +124 -0
- data/lib/factorix/dependency/graph/builder.rb +108 -0
- data/lib/factorix/dependency/graph.rb +210 -0
- data/lib/factorix/dependency/list.rb +244 -0
- data/lib/factorix/dependency/mod_version_requirement.rb +73 -0
- data/lib/factorix/dependency/node.rb +60 -0
- data/lib/factorix/dependency/parser.rb +148 -0
- data/lib/factorix/dependency/validation_result.rb +138 -0
- data/lib/factorix/dependency/validator.rb +190 -0
- data/lib/factorix/errors.rb +112 -0
- data/lib/factorix/formatting.rb +56 -0
- data/lib/factorix/game_version.rb +98 -0
- data/lib/factorix/http/cache_decorator.rb +106 -0
- data/lib/factorix/http/cached_response.rb +37 -0
- data/lib/factorix/http/client.rb +187 -0
- data/lib/factorix/http/response.rb +31 -0
- data/lib/factorix/http/retry_decorator.rb +59 -0
- data/lib/factorix/http/retry_strategy.rb +80 -0
- data/lib/factorix/info_json.rb +90 -0
- data/lib/factorix/installed_mod.rb +239 -0
- data/lib/factorix/mod.rb +55 -0
- data/lib/factorix/mod_list.rb +174 -0
- data/lib/factorix/mod_settings.rb +278 -0
- data/lib/factorix/mod_state.rb +34 -0
- data/lib/factorix/mod_version.rb +99 -0
- data/lib/factorix/portal.rb +185 -0
- data/lib/factorix/progress/download_handler.rb +46 -0
- data/lib/factorix/progress/multi_presenter.rb +45 -0
- data/lib/factorix/progress/presenter.rb +67 -0
- data/lib/factorix/progress/presenter_adapter.rb +46 -0
- data/lib/factorix/progress/scan_handler.rb +33 -0
- data/lib/factorix/progress/upload_handler.rb +33 -0
- data/lib/factorix/runtime/base.rb +233 -0
- data/lib/factorix/runtime/linux.rb +32 -0
- data/lib/factorix/runtime/mac_os.rb +53 -0
- data/lib/factorix/runtime/user_configurable.rb +69 -0
- data/lib/factorix/runtime/windows.rb +85 -0
- data/lib/factorix/runtime/wsl.rb +118 -0
- data/lib/factorix/runtime.rb +32 -0
- data/lib/factorix/save_file.rb +178 -0
- data/lib/factorix/ser_des/deserializer.rb +198 -0
- data/lib/factorix/ser_des/serializer.rb +231 -0
- data/lib/factorix/ser_des/signed_integer.rb +63 -0
- data/lib/factorix/ser_des/unsigned_integer.rb +65 -0
- data/lib/factorix/service_credential.rb +127 -0
- data/lib/factorix/transfer/downloader.rb +162 -0
- data/lib/factorix/transfer/uploader.rb +232 -0
- data/lib/factorix/version.rb +6 -0
- data/lib/factorix.rb +38 -0
- data/sig/dry/auto_inject.rbs +15 -0
- data/sig/dry/cli.rbs +19 -0
- data/sig/dry/configurable.rbs +13 -0
- data/sig/dry/core/container.rbs +17 -0
- data/sig/dry/events/publisher.rbs +22 -0
- data/sig/dry/logger.rbs +16 -0
- data/sig/factorix/api/category.rbs +15 -0
- data/sig/factorix/api/image.rbs +15 -0
- data/sig/factorix/api/license.rbs +20 -0
- data/sig/factorix/api/mod_download_api.rbs +18 -0
- data/sig/factorix/api/mod_info.rbs +67 -0
- data/sig/factorix/api/mod_management_api.rbs +25 -0
- data/sig/factorix/api/mod_portal_api.rbs +31 -0
- data/sig/factorix/api/release.rbs +27 -0
- data/sig/factorix/api/tag.rbs +15 -0
- data/sig/factorix/api.rbs +8 -0
- data/sig/factorix/api_credential.rbs +17 -0
- data/sig/factorix/application.rbs +86 -0
- data/sig/factorix/cache/file_system.rbs +35 -0
- data/sig/factorix/cli/commands/base.rbs +13 -0
- data/sig/factorix/cli/commands/cache/evict.rbs +17 -0
- data/sig/factorix/cli/commands/cache/stat.rbs +17 -0
- data/sig/factorix/cli/commands/command_wrapper.rbs +13 -0
- data/sig/factorix/cli/commands/completion/zsh.rbs +15 -0
- data/sig/factorix/cli/commands/confirmable.rbs +12 -0
- data/sig/factorix/cli/commands/download_support.rbs +12 -0
- data/sig/factorix/cli/commands/launch.rbs +15 -0
- data/sig/factorix/cli/commands/mod/check.rbs +18 -0
- data/sig/factorix/cli/commands/mod/disable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/download.rbs +18 -0
- data/sig/factorix/cli/commands/mod/edit.rbs +30 -0
- data/sig/factorix/cli/commands/mod/enable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/image/add.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/edit.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/list.rbs +19 -0
- data/sig/factorix/cli/commands/mod/install.rbs +19 -0
- data/sig/factorix/cli/commands/mod/list.rbs +30 -0
- data/sig/factorix/cli/commands/mod/search.rbs +18 -0
- data/sig/factorix/cli/commands/mod/settings/dump.rbs +17 -0
- data/sig/factorix/cli/commands/mod/settings/restore.rbs +17 -0
- data/sig/factorix/cli/commands/mod/sync.rbs +19 -0
- data/sig/factorix/cli/commands/mod/uninstall.rbs +20 -0
- data/sig/factorix/cli/commands/mod/update.rbs +19 -0
- data/sig/factorix/cli/commands/mod/upload.rbs +24 -0
- data/sig/factorix/cli/commands/path.rbs +18 -0
- data/sig/factorix/cli/commands/requires_game_stopped.rbs +13 -0
- data/sig/factorix/cli/commands/version.rbs +13 -0
- data/sig/factorix/cli.rbs +11 -0
- data/sig/factorix/dependency/edge.rbs +32 -0
- data/sig/factorix/dependency/entry.rbs +30 -0
- data/sig/factorix/dependency/graph/builder.rbs +17 -0
- data/sig/factorix/dependency/graph.rbs +39 -0
- data/sig/factorix/dependency/list.rbs +69 -0
- data/sig/factorix/dependency/mod_version_requirement.rbs +18 -0
- data/sig/factorix/dependency/node.rbs +24 -0
- data/sig/factorix/dependency/parser.rbs +11 -0
- data/sig/factorix/dependency/validation_result.rbs +56 -0
- data/sig/factorix/dependency/validator.rbs +13 -0
- data/sig/factorix/errors.rbs +132 -0
- data/sig/factorix/formatting.rbs +8 -0
- data/sig/factorix/game_version.rbs +24 -0
- data/sig/factorix/http/cache_decorator.rbs +64 -0
- data/sig/factorix/http/client.rbs +55 -0
- data/sig/factorix/http/response.rbs +28 -0
- data/sig/factorix/http/retry_decorator.rbs +44 -0
- data/sig/factorix/http/retry_strategy.rbs +42 -0
- data/sig/factorix/info_json.rbs +19 -0
- data/sig/factorix/installed_mod.rbs +34 -0
- data/sig/factorix/mod.rbs +20 -0
- data/sig/factorix/mod_list.rbs +44 -0
- data/sig/factorix/mod_settings.rbs +47 -0
- data/sig/factorix/mod_state.rbs +18 -0
- data/sig/factorix/mod_version.rbs +23 -0
- data/sig/factorix/portal.rbs +37 -0
- data/sig/factorix/progress/download_handler.rbs +19 -0
- data/sig/factorix/progress/multi_presenter.rbs +15 -0
- data/sig/factorix/progress/presenter.rbs +17 -0
- data/sig/factorix/progress/presenter_adapter.rbs +17 -0
- data/sig/factorix/progress/scan_handler.rbs +16 -0
- data/sig/factorix/progress/upload_handler.rbs +17 -0
- data/sig/factorix/runtime/base.rbs +45 -0
- data/sig/factorix/runtime/linux.rbs +15 -0
- data/sig/factorix/runtime/mac_os.rbs +15 -0
- data/sig/factorix/runtime/user_configurable.rbs +13 -0
- data/sig/factorix/runtime/windows.rbs +23 -0
- data/sig/factorix/runtime/wsl.rbs +19 -0
- data/sig/factorix/runtime.rbs +9 -0
- data/sig/factorix/save_file.rbs +40 -0
- data/sig/factorix/ser_des/deserializer.rbs +49 -0
- data/sig/factorix/ser_des/serializer.rbs +45 -0
- data/sig/factorix/ser_des/signed_integer.rbs +37 -0
- data/sig/factorix/ser_des/unsigned_integer.rbs +37 -0
- data/sig/factorix/service_credential.rbs +19 -0
- data/sig/factorix/transfer/downloader.rbs +15 -0
- data/sig/factorix/transfer/uploader.rbs +21 -0
- data/sig/factorix.rbs +9 -0
- data/sig/tty/progressbar.rbs +18 -0
- metadata +431 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
module Dependency
|
|
7
|
+
# Parser for MOD dependency strings using Parslet
|
|
8
|
+
#
|
|
9
|
+
# This class parses dependency strings from info.json files and converts them
|
|
10
|
+
# into Dependency::Entry objects using a PEG-based parser.
|
|
11
|
+
#
|
|
12
|
+
# @example Parsing various dependency formats
|
|
13
|
+
# parser = Dependency::Parser.new
|
|
14
|
+
#
|
|
15
|
+
# # Required dependency
|
|
16
|
+
# dep1 = parser.parse("base")
|
|
17
|
+
#
|
|
18
|
+
# # Optional dependency with version
|
|
19
|
+
# dep2 = parser.parse("? some-mod >= 1.2.0")
|
|
20
|
+
#
|
|
21
|
+
# # Incompatible MOD
|
|
22
|
+
# dep3 = parser.parse("! bad-mod")
|
|
23
|
+
#
|
|
24
|
+
# # Hidden optional
|
|
25
|
+
# dep4 = parser.parse("(?) hidden-mod")
|
|
26
|
+
#
|
|
27
|
+
# # Load-neutral
|
|
28
|
+
# dep5 = parser.parse("~ neutral-mod")
|
|
29
|
+
class Parser
|
|
30
|
+
# Parslet grammar for dependency strings
|
|
31
|
+
class Grammar < Parslet::Parser
|
|
32
|
+
rule(:space) { match['\s'].repeat(1) }
|
|
33
|
+
rule(:space?) { space.maybe }
|
|
34
|
+
|
|
35
|
+
# Prefix rules (longest first to avoid partial matches)
|
|
36
|
+
rule(:prefix) do
|
|
37
|
+
str("(?)").as(:hidden_optional) |
|
|
38
|
+
str("!").as(:incompatible) |
|
|
39
|
+
str("?").as(:optional) |
|
|
40
|
+
str("~").as(:load_neutral)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# MOD name: starts with alphanumeric, can contain spaces
|
|
44
|
+
# Cannot start with operators or contain only operators
|
|
45
|
+
rule(:mod_name_start) { match["a-zA-Z0-9_-"] }
|
|
46
|
+
rule(:mod_name_char) { match["a-zA-Z0-9_-"] | (space >> match["a-zA-Z0-9_-"].repeat(1)) }
|
|
47
|
+
rule(:mod_name) { (mod_name_start >> mod_name_char.repeat).as(:mod_name) }
|
|
48
|
+
|
|
49
|
+
# Version operators (longest first)
|
|
50
|
+
rule(:operator) do
|
|
51
|
+
(str(">=") | str("<=") | str(">") | str("<") | str("=")).as(:operator)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Version: X.Y.Z or X.Y format
|
|
55
|
+
rule(:version) do
|
|
56
|
+
(match["0-9"].repeat(1) >> str(".") >> match["0-9"].repeat(1) >> (str(".") >> match["0-9"].repeat(1)).maybe).as(:version)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Version requirement: operator space version
|
|
60
|
+
rule(:version_requirement) do
|
|
61
|
+
space? >> operator >> space? >> version
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Complete dependency: [prefix] [space] mod_name [version_requirement]
|
|
65
|
+
rule(:dependency) do
|
|
66
|
+
space? >>
|
|
67
|
+
prefix.maybe.as(:prefix) >>
|
|
68
|
+
space? >>
|
|
69
|
+
mod_name >>
|
|
70
|
+
version_requirement.maybe.as(:requirement) >>
|
|
71
|
+
space?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
root(:dependency)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Transform parsed tree into structured data
|
|
78
|
+
class Transform < Parslet::Transform
|
|
79
|
+
rule(mod_name: simple(:name)) { {mod_name: name.to_s.strip} }
|
|
80
|
+
|
|
81
|
+
rule(version: simple(:ver)) { {version: ver.to_s} }
|
|
82
|
+
|
|
83
|
+
rule(operator: simple(:op), version: simple(:ver)) do
|
|
84
|
+
{operator: op.to_s, version: ver.to_s}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
rule(optional: simple(:_)) { {type: Entry::OPTIONAL} }
|
|
88
|
+
rule(hidden_optional: simple(:_)) { {type: Entry::HIDDEN_OPTIONAL} }
|
|
89
|
+
rule(incompatible: simple(:_)) { {type: Entry::INCOMPATIBLE} }
|
|
90
|
+
rule(load_neutral: simple(:_)) { {type: Entry::LOAD_NEUTRAL} }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def initialize
|
|
94
|
+
@grammar = Grammar.new
|
|
95
|
+
@transform = Transform.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Parse a dependency string into a Dependency::Entry object
|
|
99
|
+
#
|
|
100
|
+
# @param dependency_string [String] Dependency string to parse
|
|
101
|
+
# @return [Entry] Parsed dependency object
|
|
102
|
+
# @raise [DependencyParseError] if the dependency string is invalid
|
|
103
|
+
def parse(dependency_string)
|
|
104
|
+
raise DependencyParseError, "dependency_string cannot be nil or empty" if dependency_string.nil? || dependency_string.empty?
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
tree = @grammar.parse(dependency_string)
|
|
108
|
+
data = @transform.apply(tree)
|
|
109
|
+
|
|
110
|
+
mod_name = data[:mod_name].to_s
|
|
111
|
+
mod = MOD[name: mod_name]
|
|
112
|
+
type = data.dig(:prefix, :type) || Entry::REQUIRED
|
|
113
|
+
version_requirement = build_version_requirement(data[:requirement])
|
|
114
|
+
|
|
115
|
+
Entry[mod:, type:, version_requirement:]
|
|
116
|
+
rescue Parslet::ParseFailed => e
|
|
117
|
+
raise DependencyParseError, parse_error_message(dependency_string, e)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private def build_version_requirement(requirement_data)
|
|
122
|
+
return nil if requirement_data.nil? || requirement_data.empty?
|
|
123
|
+
|
|
124
|
+
operator = requirement_data[:operator]
|
|
125
|
+
version_string = requirement_data[:version]
|
|
126
|
+
|
|
127
|
+
version = MODVersion.from_string(version_string)
|
|
128
|
+
MODVersionRequirement[operator:, version:]
|
|
129
|
+
rescue VersionParseError => e
|
|
130
|
+
# Skip version requirements with out-of-range version components
|
|
131
|
+
Application[:logger].warn("Skipping version requirement '#{version_string}': #{e.message}")
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private def parse_error_message(input, error)
|
|
136
|
+
if input.strip.match?(/^[><=]+/)
|
|
137
|
+
"Invalid dependency format: empty MOD name (input: #{input.inspect})"
|
|
138
|
+
elsif input.match?(/[><=]\s*$/)
|
|
139
|
+
"Invalid dependency format: empty version (input: #{input.inspect})"
|
|
140
|
+
elsif input.match?(/[><=]\s+\S+$/) && !input.match?(/[><=]\s+\d+\.\d+\.\d+/)
|
|
141
|
+
"Invalid version requirement: invalid version format (input: #{input.inspect})"
|
|
142
|
+
else
|
|
143
|
+
"Invalid dependency format (input: #{input.inspect}): #{error.parse_failure_cause.ascii_tree}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
module Dependency
|
|
5
|
+
# Represents the result of MOD dependency validation
|
|
6
|
+
#
|
|
7
|
+
# Holds errors and warnings found during validation.
|
|
8
|
+
class ValidationResult
|
|
9
|
+
Error = Data.define(:type, :message, :mod, :dependency)
|
|
10
|
+
|
|
11
|
+
# Validation error information
|
|
12
|
+
#
|
|
13
|
+
# Represents an error found during dependency validation.
|
|
14
|
+
class Error
|
|
15
|
+
# @!attribute [r] type
|
|
16
|
+
# @return [Symbol] error type
|
|
17
|
+
# @!attribute [r] message
|
|
18
|
+
# @return [String] error message
|
|
19
|
+
# @!attribute [r] mod
|
|
20
|
+
# @return [Factorix::MOD, nil] related MOD
|
|
21
|
+
# @!attribute [r] dependency
|
|
22
|
+
# @return [Factorix::MOD, nil] dependency MOD
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Warning = Data.define(:type, :message, :mod)
|
|
26
|
+
|
|
27
|
+
# Validation warning information
|
|
28
|
+
#
|
|
29
|
+
# Represents a warning found during dependency validation.
|
|
30
|
+
class Warning
|
|
31
|
+
# @!attribute [r] type
|
|
32
|
+
# @return [Symbol] warning type
|
|
33
|
+
# @!attribute [r] message
|
|
34
|
+
# @return [String] warning message
|
|
35
|
+
# @!attribute [r] mod
|
|
36
|
+
# @return [Factorix::MOD, nil] related MOD
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Suggestion = Data.define(:message, :mod, :version)
|
|
40
|
+
|
|
41
|
+
# Validation suggestion information
|
|
42
|
+
#
|
|
43
|
+
# Represents a suggestion for resolving dependency issues.
|
|
44
|
+
class Suggestion
|
|
45
|
+
# @!attribute [r] message
|
|
46
|
+
# @return [String] suggestion message
|
|
47
|
+
# @!attribute [r] mod
|
|
48
|
+
# @return [Factorix::MOD] related MOD
|
|
49
|
+
# @!attribute [r] version
|
|
50
|
+
# @return [Factorix::MODVersion] suggested version
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Error types
|
|
54
|
+
MISSING_DEPENDENCY = :missing_dependency
|
|
55
|
+
public_constant :MISSING_DEPENDENCY
|
|
56
|
+
DISABLED_DEPENDENCY = :disabled_dependency
|
|
57
|
+
public_constant :DISABLED_DEPENDENCY
|
|
58
|
+
VERSION_MISMATCH = :version_mismatch
|
|
59
|
+
public_constant :VERSION_MISMATCH
|
|
60
|
+
CONFLICT = :conflict
|
|
61
|
+
public_constant :CONFLICT
|
|
62
|
+
CIRCULAR_DEPENDENCY = :circular_dependency
|
|
63
|
+
public_constant :CIRCULAR_DEPENDENCY
|
|
64
|
+
|
|
65
|
+
# Warning types
|
|
66
|
+
MOD_IN_LIST_NOT_INSTALLED = :mod_in_list_not_installed
|
|
67
|
+
public_constant :MOD_IN_LIST_NOT_INSTALLED
|
|
68
|
+
MOD_INSTALLED_NOT_IN_LIST = :mod_installed_not_in_list
|
|
69
|
+
public_constant :MOD_INSTALLED_NOT_IN_LIST
|
|
70
|
+
|
|
71
|
+
def initialize
|
|
72
|
+
@errors = []
|
|
73
|
+
@warnings = []
|
|
74
|
+
@suggestions = []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Add an error
|
|
78
|
+
#
|
|
79
|
+
# @param type [Symbol] Error type
|
|
80
|
+
# @param message [String] Error message
|
|
81
|
+
# @param mod [Factorix::MOD, nil] Related MOD
|
|
82
|
+
# @param dependency [Factorix::MOD, nil] Dependency MOD
|
|
83
|
+
# @return [void]
|
|
84
|
+
def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error.new(type:, message:, mod:, dependency:)
|
|
85
|
+
|
|
86
|
+
# Add a warning
|
|
87
|
+
#
|
|
88
|
+
# @param type [Symbol] Warning type
|
|
89
|
+
# @param message [String] Warning message
|
|
90
|
+
# @param mod [Factorix::MOD, nil] Related MOD
|
|
91
|
+
# @return [void]
|
|
92
|
+
def add_warning(type:, message:, mod: nil) = @warnings << Warning.new(type:, message:, mod:)
|
|
93
|
+
|
|
94
|
+
# Add a suggestion
|
|
95
|
+
#
|
|
96
|
+
# @param message [String] Suggestion message
|
|
97
|
+
# @param mod [Factorix::MOD] Related MOD
|
|
98
|
+
# @param version [Factorix::MODVersion] Suggested version
|
|
99
|
+
# @return [void]
|
|
100
|
+
def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion.new(message:, mod:, version:)
|
|
101
|
+
|
|
102
|
+
# Get all errors
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Error>]
|
|
105
|
+
attr_reader :errors
|
|
106
|
+
|
|
107
|
+
# Get all warnings
|
|
108
|
+
#
|
|
109
|
+
# @return [Array<Warning>]
|
|
110
|
+
attr_reader :warnings
|
|
111
|
+
|
|
112
|
+
# Get all suggestions
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<Suggestion>]
|
|
115
|
+
attr_reader :suggestions
|
|
116
|
+
|
|
117
|
+
# Check if there are any errors
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def errors? = !@errors.empty?
|
|
121
|
+
|
|
122
|
+
# Check if there are any warnings
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean]
|
|
125
|
+
def warnings? = !@warnings.empty?
|
|
126
|
+
|
|
127
|
+
# Check if there are any suggestions
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean]
|
|
130
|
+
def suggestions? = !@suggestions.empty?
|
|
131
|
+
|
|
132
|
+
# Check if validation passed (no errors)
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def valid? = !errors?
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
module Dependency
|
|
5
|
+
# Validates MOD dependencies in a graph
|
|
6
|
+
#
|
|
7
|
+
# Performs comprehensive validation of MOD dependencies including:
|
|
8
|
+
# - Required dependencies are installed and enabled
|
|
9
|
+
# - Version requirements are satisfied
|
|
10
|
+
# - No conflicts between enabled MODs
|
|
11
|
+
# - No circular dependencies
|
|
12
|
+
class Validator
|
|
13
|
+
# Initialize validator
|
|
14
|
+
#
|
|
15
|
+
# @param graph [Factorix::Dependency::Graph] The dependency graph
|
|
16
|
+
# @param mod_list [Factorix::MODList] The MOD list
|
|
17
|
+
# @param installed_mods [Array<Factorix::InstalledMOD>] The installed MODs
|
|
18
|
+
def initialize(graph:, mod_list:, installed_mods:)
|
|
19
|
+
@graph = graph
|
|
20
|
+
@mod_list = mod_list
|
|
21
|
+
@installed_mods = installed_mods
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Validate the graph
|
|
25
|
+
#
|
|
26
|
+
# @return [Factorix::Dependency::ValidationResult]
|
|
27
|
+
def validate
|
|
28
|
+
result = ValidationResult.new
|
|
29
|
+
|
|
30
|
+
validate_circular_dependencies(result)
|
|
31
|
+
validate_dependencies(result)
|
|
32
|
+
validate_conflicts(result)
|
|
33
|
+
validate_mod_list(result)
|
|
34
|
+
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private attr_reader :graph, :mod_list, :installed_mods
|
|
39
|
+
|
|
40
|
+
private def validate_circular_dependencies(result)
|
|
41
|
+
return unless graph.cyclic?
|
|
42
|
+
|
|
43
|
+
components = graph.strongly_connected_components
|
|
44
|
+
cycles = components.select {|component| component.size > 1 }
|
|
45
|
+
|
|
46
|
+
cycles.each do |cycle|
|
|
47
|
+
mod_names = cycle.map(&:name).join(" -> ")
|
|
48
|
+
result.add_error(
|
|
49
|
+
type: ValidationResult::CIRCULAR_DEPENDENCY,
|
|
50
|
+
message: "Circular dependency detected: #{mod_names}"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate dependencies for all enabled MODs
|
|
56
|
+
private def validate_dependencies(result)
|
|
57
|
+
graph.nodes.each do |node|
|
|
58
|
+
next unless node.enabled?
|
|
59
|
+
|
|
60
|
+
validate_node_dependencies(node, result)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Validate dependencies for a single node
|
|
65
|
+
private def validate_node_dependencies(node, result)
|
|
66
|
+
graph.edges_from(node.mod).each do |edge|
|
|
67
|
+
next unless edge.required?
|
|
68
|
+
|
|
69
|
+
validate_required_dependency(node, edge, result)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Validate a single required dependency
|
|
74
|
+
private def validate_required_dependency(node, edge, result)
|
|
75
|
+
dependency_node = graph.node(edge.to_mod)
|
|
76
|
+
|
|
77
|
+
unless dependency_node
|
|
78
|
+
result.add_error(
|
|
79
|
+
type: ValidationResult::MISSING_DEPENDENCY,
|
|
80
|
+
message: "MOD '#{node.mod}@#{node.version}' requires '#{edge.to_mod}' which is not installed",
|
|
81
|
+
mod: node.mod,
|
|
82
|
+
dependency: edge.to_mod
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless dependency_node.enabled?
|
|
88
|
+
result.add_error(
|
|
89
|
+
type: ValidationResult::DISABLED_DEPENDENCY,
|
|
90
|
+
message: "MOD '#{node.mod}@#{node.version}' requires '#{edge.to_mod}' which is not enabled",
|
|
91
|
+
mod: node.mod,
|
|
92
|
+
dependency: edge.to_mod
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return if edge.satisfied_by?(dependency_node.version)
|
|
98
|
+
|
|
99
|
+
result.add_error(
|
|
100
|
+
type: ValidationResult::VERSION_MISMATCH,
|
|
101
|
+
message: "MOD '#{node.mod}@#{node.version}' requires '#{edge.to_mod}' version " \
|
|
102
|
+
"#{edge.version_requirement}, but version #{dependency_node.version} is installed",
|
|
103
|
+
mod: node.mod,
|
|
104
|
+
dependency: edge.to_mod
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
check_alternative_versions(edge, result)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Validate that no conflicts exist between enabled MODs
|
|
111
|
+
private def validate_conflicts(result)
|
|
112
|
+
graph.nodes.each do |node|
|
|
113
|
+
next unless node.enabled?
|
|
114
|
+
|
|
115
|
+
validate_node_conflicts(node, result)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Validate conflicts for a single node
|
|
120
|
+
private def validate_node_conflicts(node, result)
|
|
121
|
+
graph.edges_from(node.mod).each do |edge|
|
|
122
|
+
next unless edge.incompatible?
|
|
123
|
+
|
|
124
|
+
conflict_node = graph.node(edge.to_mod)
|
|
125
|
+
next unless conflict_node&.enabled?
|
|
126
|
+
|
|
127
|
+
result.add_error(
|
|
128
|
+
type: ValidationResult::CONFLICT,
|
|
129
|
+
message: "MOD '#{node.mod}@#{node.version}' conflicts with '#{edge.to_mod}@#{conflict_node.version}' but both are enabled",
|
|
130
|
+
mod: node.mod,
|
|
131
|
+
dependency: edge.to_mod
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Validate MOD list consistency
|
|
137
|
+
private def validate_mod_list(result)
|
|
138
|
+
validate_mods_in_list_not_installed(result)
|
|
139
|
+
validate_mods_installed_not_in_list(result)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Warn about MODs in list but not installed
|
|
143
|
+
private def validate_mods_in_list_not_installed(result)
|
|
144
|
+
mod_list.each_mod do |mod|
|
|
145
|
+
next if graph.node?(mod)
|
|
146
|
+
|
|
147
|
+
result.add_warning(
|
|
148
|
+
type: ValidationResult::MOD_IN_LIST_NOT_INSTALLED,
|
|
149
|
+
message: "MOD '#{mod}' in mod-list.json is not installed",
|
|
150
|
+
mod:
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Warn about installed MODs not in list
|
|
156
|
+
private def validate_mods_installed_not_in_list(result)
|
|
157
|
+
graph.nodes.each do |node|
|
|
158
|
+
next if mod_list.exist?(node.mod)
|
|
159
|
+
|
|
160
|
+
result.add_warning(
|
|
161
|
+
type: ValidationResult::MOD_INSTALLED_NOT_IN_LIST,
|
|
162
|
+
message: "MOD '#{node.mod}' is installed but not in mod-list.json",
|
|
163
|
+
mod: node.mod
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Check for alternative installed versions that satisfy a requirement
|
|
169
|
+
#
|
|
170
|
+
# @param edge [Factorix::Dependency::Edge] The dependency edge with version requirement
|
|
171
|
+
# @param result [Factorix::Dependency::ValidationResult] The validation result to add suggestions to
|
|
172
|
+
# @return [void]
|
|
173
|
+
private def check_alternative_versions(edge, result)
|
|
174
|
+
return if installed_mods.empty?
|
|
175
|
+
|
|
176
|
+
alternative_versions = installed_mods.select {|im| im.mod == edge.to_mod }
|
|
177
|
+
|
|
178
|
+
alternative_versions.each do |installed_mod|
|
|
179
|
+
next unless edge.satisfied_by?(installed_mod.version)
|
|
180
|
+
|
|
181
|
+
result.add_suggestion(
|
|
182
|
+
message: "MOD '#{edge.to_mod}' version #{installed_mod.version} is installed and would satisfy this requirement",
|
|
183
|
+
mod: edge.to_mod,
|
|
184
|
+
version: installed_mod.version
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
# Base error class for Factorix
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# =====================================
|
|
8
|
+
# Infrastructure layer errors
|
|
9
|
+
# =====================================
|
|
10
|
+
class InfrastructureError < Error; end
|
|
11
|
+
|
|
12
|
+
# Configuration errors
|
|
13
|
+
class ConfigurationError < InfrastructureError; end
|
|
14
|
+
|
|
15
|
+
# Credential/environment variable errors
|
|
16
|
+
class CredentialError < ConfigurationError; end
|
|
17
|
+
|
|
18
|
+
# Shell/completion errors
|
|
19
|
+
class ShellError < ConfigurationError; end
|
|
20
|
+
|
|
21
|
+
# Directory not found errors
|
|
22
|
+
class DirectoryNotFoundError < ConfigurationError; end
|
|
23
|
+
|
|
24
|
+
# URL related errors
|
|
25
|
+
class URLError < InfrastructureError; end
|
|
26
|
+
|
|
27
|
+
# Platform-specific errors (WSL, etc.)
|
|
28
|
+
class PlatformError < InfrastructureError; end
|
|
29
|
+
|
|
30
|
+
# Unsupported platform errors
|
|
31
|
+
class UnsupportedPlatformError < PlatformError; end
|
|
32
|
+
|
|
33
|
+
# HTTP errors
|
|
34
|
+
class HTTPError < InfrastructureError; end
|
|
35
|
+
|
|
36
|
+
# HTTP client error (4xx) with optional API error details
|
|
37
|
+
class HTTPClientError < HTTPError
|
|
38
|
+
attr_reader :api_error
|
|
39
|
+
attr_reader :api_message
|
|
40
|
+
|
|
41
|
+
def initialize(message=nil, api_error: nil, api_message: nil)
|
|
42
|
+
@api_error = api_error
|
|
43
|
+
@api_message = api_message
|
|
44
|
+
super(message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class HTTPNotFoundError < HTTPClientError; end
|
|
49
|
+
class HTTPServerError < HTTPError; end
|
|
50
|
+
|
|
51
|
+
# Digest verification errors
|
|
52
|
+
class DigestMismatchError < InfrastructureError; end
|
|
53
|
+
|
|
54
|
+
# External command not found
|
|
55
|
+
class CommandNotFoundError < InfrastructureError; end
|
|
56
|
+
|
|
57
|
+
# File format related errors
|
|
58
|
+
class FileFormatError < InfrastructureError; end
|
|
59
|
+
|
|
60
|
+
# Binary format errors
|
|
61
|
+
class BinaryFormatError < FileFormatError; end
|
|
62
|
+
class InvalidLengthError < BinaryFormatError; end
|
|
63
|
+
class UnknownPropertyType < BinaryFormatError; end
|
|
64
|
+
class ExtraDataError < BinaryFormatError; end
|
|
65
|
+
|
|
66
|
+
# MOD settings file errors
|
|
67
|
+
class MODSectionNotFoundError < FileFormatError; end
|
|
68
|
+
|
|
69
|
+
# =====================================
|
|
70
|
+
# Domain layer errors
|
|
71
|
+
# =====================================
|
|
72
|
+
class DomainError < Error; end
|
|
73
|
+
|
|
74
|
+
# MOD errors
|
|
75
|
+
class MODNotFoundError < DomainError; end
|
|
76
|
+
class MODNotOnPortalError < MODNotFoundError; end
|
|
77
|
+
class BundledMODError < DomainError; end
|
|
78
|
+
|
|
79
|
+
# MOD conflict errors
|
|
80
|
+
class MODConflictError < DomainError; end
|
|
81
|
+
|
|
82
|
+
# MOD settings errors
|
|
83
|
+
class MODSettingsError < DomainError; end
|
|
84
|
+
|
|
85
|
+
# Invalid operation errors (e.g., cannot disable base MOD)
|
|
86
|
+
class InvalidOperationError < DomainError; end
|
|
87
|
+
|
|
88
|
+
# CLI argument validation errors
|
|
89
|
+
class InvalidArgumentError < DomainError; end
|
|
90
|
+
|
|
91
|
+
# Version parsing errors
|
|
92
|
+
class VersionParseError < DomainError; end
|
|
93
|
+
|
|
94
|
+
# Dependency parsing errors
|
|
95
|
+
class DependencyParseError < DomainError; end
|
|
96
|
+
|
|
97
|
+
# Dependency graph errors
|
|
98
|
+
class DependencyGraphError < DomainError; end
|
|
99
|
+
|
|
100
|
+
# Dependency errors
|
|
101
|
+
class DependencyError < DomainError; end
|
|
102
|
+
class CircularDependencyError < DependencyError; end
|
|
103
|
+
class DependencyMissingError < DependencyError; end
|
|
104
|
+
class DependencyVersionError < DependencyError; end
|
|
105
|
+
class DependencyViolationError < DependencyError; end
|
|
106
|
+
|
|
107
|
+
# Dependency validation errors
|
|
108
|
+
class ValidationError < DomainError; end
|
|
109
|
+
|
|
110
|
+
# Game state errors
|
|
111
|
+
class GameRunningError < DomainError; end
|
|
112
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
# Formatting utilities for human-readable output
|
|
5
|
+
#
|
|
6
|
+
# This module provides methods for formatting sizes and durations
|
|
7
|
+
# in a human-readable format for CLI output.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# include Factorix::Formatting
|
|
11
|
+
# format_size(1536) # => "1.5 KiB"
|
|
12
|
+
# format_duration(3661) # => "1h 1m"
|
|
13
|
+
module Formatting
|
|
14
|
+
# Format size value for display using binary prefixes (IEC)
|
|
15
|
+
#
|
|
16
|
+
# @param size [Integer, nil] size in bytes
|
|
17
|
+
# @return [String] formatted size ("unlimited" if nil)
|
|
18
|
+
def format_size(size)
|
|
19
|
+
return "unlimited" if size.nil?
|
|
20
|
+
return "0 B" if size == 0
|
|
21
|
+
|
|
22
|
+
units = %w[B KiB MiB GiB TiB]
|
|
23
|
+
unit_index = 0
|
|
24
|
+
value = Float(size)
|
|
25
|
+
|
|
26
|
+
while value >= 1024 && unit_index < units.size - 1
|
|
27
|
+
value /= 1024
|
|
28
|
+
unit_index += 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unit_index == 0 ? "#{size} B" : "#{"%.1f" % value} #{units[unit_index]}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format duration value for display
|
|
35
|
+
#
|
|
36
|
+
# @param seconds [Integer, Float, nil] duration in seconds
|
|
37
|
+
# @return [String] formatted duration ("-" if nil)
|
|
38
|
+
def format_duration(seconds)
|
|
39
|
+
return "-" if seconds.nil?
|
|
40
|
+
|
|
41
|
+
seconds = Integer(seconds)
|
|
42
|
+
return "#{seconds}s" if seconds < 60
|
|
43
|
+
|
|
44
|
+
minutes = seconds / 60
|
|
45
|
+
return "#{minutes}m" if minutes < 60
|
|
46
|
+
|
|
47
|
+
hours = minutes / 60
|
|
48
|
+
remaining_minutes = minutes % 60
|
|
49
|
+
return "#{hours}h #{remaining_minutes}m" if hours < 24
|
|
50
|
+
|
|
51
|
+
days = hours / 24
|
|
52
|
+
remaining_hours = hours % 24
|
|
53
|
+
"#{days}d #{remaining_hours}h"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|