factorix 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +3 -0
- data/completion/_factorix.bash +15 -1
- data/completion/_factorix.fish +15 -7
- data/completion/_factorix.zsh +22 -0
- data/lib/factorix/api/game_download_api.rb +154 -0
- data/lib/factorix/api/mod_info.rb +1 -1
- data/lib/factorix/cache/file_system.rb +2 -2
- data/lib/factorix/cache/redis.rb +2 -2
- data/lib/factorix/cache/s3.rb +2 -2
- data/lib/factorix/cli/commands/download.rb +150 -0
- data/lib/factorix/cli/commands/mod/list.rb +3 -3
- data/lib/factorix/cli.rb +1 -0
- data/lib/factorix/container.rb +5 -0
- data/lib/factorix/dependency/graph/builder.rb +2 -2
- data/lib/factorix/dependency/graph.rb +2 -2
- data/lib/factorix/dependency/validation_result.rb +3 -3
- data/lib/factorix/http/cache_decorator.rb +9 -2
- data/lib/factorix/http/cached_response.rb +4 -1
- data/lib/factorix/http/client.rb +13 -3
- data/lib/factorix/http/response.rb +4 -1
- data/lib/factorix/http/retry_decorator.rb +11 -0
- data/lib/factorix/save_file.rb +2 -2
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc75b1211b8487fb9f93c19751c2086415385d32356282dfd3d71ef4158fdd1
|
|
4
|
+
data.tar.gz: 8d55640b9704c518ec15660cf17604b6f89b96356bee3044e5e663d28afebca0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 638164922741c6f7225125b1c1f676c21adc5253e7ab1e40a99c0c6baef3f1e3d554097a254cf3cc4cb0ffc4e055c9153c4a37132222216ea00097cbf24f044c
|
|
7
|
+
data.tar.gz: dfc7c5bce1df717c321f2189d78486a0443d640e7c32571dd0038b07f34c7be5da68b72e5aa12c9ed1de59ca55531b37937a4cc9194d15aea88ff6a834d6cd95
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-02-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `download` command to download Factorio game files from the official Download API (#51)
|
|
8
|
+
- Supports alpha, expansion, demo, and headless builds
|
|
9
|
+
- Auto-detects platform (Windows, Linux, macOS, WSL)
|
|
10
|
+
- Resolves latest version from stable/experimental channels
|
|
11
|
+
- Add `head` method to HTTP client and decorators (`Client`, `RetryDecorator`, `CacheDecorator`)
|
|
12
|
+
|
|
3
13
|
## [0.7.0] - 2026-01-24
|
|
4
14
|
|
|
5
15
|
### Added
|
data/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Factorix simplifies Factorio MOD management by providing:
|
|
|
20
20
|
- **Settings Management**: Export/import MOD settings in JSON format
|
|
21
21
|
- **MOD Portal Integration**: Upload new MODs or update existing ones, edit metadata
|
|
22
22
|
- **Game Control**: Launch Factorio from the command line
|
|
23
|
+
- **Game Download**: Download Factorio game files (alpha, expansion, demo, headless)
|
|
23
24
|
- **Cross-platform Support**: Works on Windows, Linux, macOS, and WSL
|
|
24
25
|
|
|
25
26
|
## Requirements
|
|
@@ -38,6 +39,8 @@ export FACTORIO_API_KEY=your_api_key_here
|
|
|
38
39
|
|
|
39
40
|
API key is not required for downloading, installing, or managing local MODs.
|
|
40
41
|
|
|
42
|
+
For downloading the game itself (`factorix download`), service credentials are required. These are automatically loaded from `player-data.json` if you have logged into Factorio, or you can set `FACTORIO_USERNAME` and `FACTORIO_TOKEN` environment variables.
|
|
43
|
+
|
|
41
44
|
## Configuration
|
|
42
45
|
|
|
43
46
|
### Path Configuration
|
data/completion/_factorix.bash
CHANGED
|
@@ -17,7 +17,7 @@ _factorix() {
|
|
|
17
17
|
local confirmable_opts="-y --yes"
|
|
18
18
|
|
|
19
19
|
# Top-level commands
|
|
20
|
-
local commands="version man launch path mod cache completion"
|
|
20
|
+
local commands="version man launch path download mod cache completion"
|
|
21
21
|
|
|
22
22
|
# mod subcommands
|
|
23
23
|
local mod_commands="check list show enable disable install uninstall update download upload edit search sync image settings"
|
|
@@ -52,6 +52,20 @@ _factorix() {
|
|
|
52
52
|
fi
|
|
53
53
|
return
|
|
54
54
|
;;
|
|
55
|
+
download)
|
|
56
|
+
if [[ "$cur" == -* ]]; then
|
|
57
|
+
COMPREPLY=($(compgen -W "$global_opts -b --build -p --platform -c --channel -d --directory -o --output" -- "$cur"))
|
|
58
|
+
elif [[ "$prev" == "--build" ]] || [[ "$prev" == "-b" ]]; then
|
|
59
|
+
COMPREPLY=($(compgen -W "alpha expansion demo headless" -- "$cur"))
|
|
60
|
+
elif [[ "$prev" == "--platform" ]] || [[ "$prev" == "-p" ]]; then
|
|
61
|
+
COMPREPLY=($(compgen -W "win64 win64-manual osx linux64" -- "$cur"))
|
|
62
|
+
elif [[ "$prev" == "--channel" ]] || [[ "$prev" == "-c" ]]; then
|
|
63
|
+
COMPREPLY=($(compgen -W "stable experimental" -- "$cur"))
|
|
64
|
+
elif [[ "$prev" == "--directory" ]] || [[ "$prev" == "-d" ]]; then
|
|
65
|
+
COMPREPLY=($(compgen -d -- "$cur"))
|
|
66
|
+
fi
|
|
67
|
+
return
|
|
68
|
+
;;
|
|
55
69
|
cache)
|
|
56
70
|
if [[ $cword -eq 2 ]]; then
|
|
57
71
|
COMPREPLY=($(compgen -W "$cache_commands" -- "$cur"))
|
data/completion/_factorix.fish
CHANGED
|
@@ -73,13 +73,14 @@ complete -c factorix -l log-level -d 'Set log level' -xa 'debug info warn error
|
|
|
73
73
|
complete -c factorix -s q -l quiet -d 'Suppress non-essential output'
|
|
74
74
|
|
|
75
75
|
# Top-level commands
|
|
76
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
|
|
77
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
|
|
78
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
|
|
79
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
|
|
80
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
81
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
82
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
76
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
|
|
77
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
|
|
78
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
|
|
79
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
|
|
80
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a download -d 'Download Factorio game files'
|
|
81
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a mod -d 'MOD management commands'
|
|
82
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a cache -d 'Cache management commands'
|
|
83
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a completion -d 'Generate shell completion script'
|
|
83
84
|
|
|
84
85
|
# launch options
|
|
85
86
|
complete -c factorix -n "__factorix_using_command launch" -s w -l wait -d 'Wait for the game to finish'
|
|
@@ -90,6 +91,13 @@ complete -c factorix -n "__factorix_using_command path" -l json -d 'Output in JS
|
|
|
90
91
|
# completion shell argument
|
|
91
92
|
complete -c factorix -n "__factorix_using_command completion" -a 'zsh bash fish' -d 'Shell type'
|
|
92
93
|
|
|
94
|
+
# download options
|
|
95
|
+
complete -c factorix -n "__factorix_using_command download" -s b -l build -d 'Build type' -xa 'alpha expansion demo headless'
|
|
96
|
+
complete -c factorix -n "__factorix_using_command download" -s p -l platform -d 'Platform' -xa 'win64 win64-manual osx linux64'
|
|
97
|
+
complete -c factorix -n "__factorix_using_command download" -s c -l channel -d 'Release channel' -xa 'stable experimental'
|
|
98
|
+
complete -c factorix -n "__factorix_using_command download" -s d -l directory -d 'Download directory' -ra '(__fish_complete_directories)'
|
|
99
|
+
complete -c factorix -n "__factorix_using_command download" -s o -l output -d 'Output filename' -r
|
|
100
|
+
|
|
93
101
|
# cache subcommands
|
|
94
102
|
complete -c factorix -n "__factorix_using_command cache" -a stat -d 'Display cache statistics'
|
|
95
103
|
complete -c factorix -n "__factorix_using_command cache" -a evict -d 'Evict cache entries'
|
data/completion/_factorix.zsh
CHANGED
|
@@ -39,6 +39,7 @@ _factorix() {
|
|
|
39
39
|
'man:Display the Factorix manual page'
|
|
40
40
|
'launch:Launch Factorio game'
|
|
41
41
|
'path:Display Factorio and Factorix paths'
|
|
42
|
+
'download:Download Factorio game files'
|
|
42
43
|
'mod:MOD management commands'
|
|
43
44
|
'cache:Cache management commands'
|
|
44
45
|
'completion:Generate shell completion script'
|
|
@@ -61,6 +62,9 @@ _factorix() {
|
|
|
61
62
|
$global_opts \
|
|
62
63
|
'--json[Output in JSON format]'
|
|
63
64
|
;;
|
|
65
|
+
download)
|
|
66
|
+
_factorix_download
|
|
67
|
+
;;
|
|
64
68
|
completion)
|
|
65
69
|
_factorix_completion
|
|
66
70
|
;;
|
|
@@ -88,6 +92,24 @@ _factorix_completion() {
|
|
|
88
92
|
'1:shell:(zsh bash fish)'
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
_factorix_download() {
|
|
96
|
+
local -a global_opts
|
|
97
|
+
global_opts=(
|
|
98
|
+
'(-c --config-path)'{-c,--config-path}'[Path to configuration file]:config file:_files'
|
|
99
|
+
'--log-level[Set log level]:level:(debug info warn error fatal)'
|
|
100
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
_arguments \
|
|
104
|
+
$global_opts \
|
|
105
|
+
'(-b --build)'{-b,--build}'[Build type]:build:(alpha expansion demo headless)' \
|
|
106
|
+
'(-p --platform)'{-p,--platform}'[Platform]:platform:(win64 win64-manual osx linux64)' \
|
|
107
|
+
'(-c --channel)'{-c,--channel}'[Release channel]:channel:(stable experimental)' \
|
|
108
|
+
'(-d --directory)'{-d,--directory}'[Download directory]:directory:_files -/' \
|
|
109
|
+
'(-o --output)'{-o,--output}'[Output filename]:filename:' \
|
|
110
|
+
'1:version:'
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
_factorix_mod() {
|
|
92
114
|
local context state state_descr line
|
|
93
115
|
typeset -A opt_args
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Factorix
|
|
7
|
+
module API
|
|
8
|
+
# API client for downloading Factorio game files
|
|
9
|
+
#
|
|
10
|
+
# Corresponds to: https://wiki.factorio.com/Download_API
|
|
11
|
+
class GameDownloadAPI
|
|
12
|
+
# @!parse
|
|
13
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
14
|
+
# attr_reader :logger
|
|
15
|
+
# # @return [HTTP::Client]
|
|
16
|
+
# attr_reader :client
|
|
17
|
+
include Import[:logger, client: :api_http_client]
|
|
18
|
+
|
|
19
|
+
# Base URL for game downloads
|
|
20
|
+
DOWNLOAD_BASE_URL = "https://www.factorio.com"
|
|
21
|
+
private_constant :DOWNLOAD_BASE_URL
|
|
22
|
+
|
|
23
|
+
# Base URL for API endpoints
|
|
24
|
+
API_BASE_URL = "https://factorio.com"
|
|
25
|
+
private_constant :API_BASE_URL
|
|
26
|
+
|
|
27
|
+
# Valid build types
|
|
28
|
+
BUILDS = %w[alpha expansion demo headless].freeze
|
|
29
|
+
public_constant :BUILDS
|
|
30
|
+
|
|
31
|
+
# Valid platforms
|
|
32
|
+
PLATFORMS = %w[win64 win64-manual osx linux64].freeze
|
|
33
|
+
public_constant :PLATFORMS
|
|
34
|
+
|
|
35
|
+
# Valid release channels
|
|
36
|
+
CHANNELS = %w[stable experimental].freeze
|
|
37
|
+
public_constant :CHANNELS
|
|
38
|
+
|
|
39
|
+
# Initialize with thread-safe credential loading
|
|
40
|
+
#
|
|
41
|
+
# @param args [Hash] dependency injection arguments
|
|
42
|
+
def initialize(...)
|
|
43
|
+
super
|
|
44
|
+
@service_credential_mutex = Mutex.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetch latest release information
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash{Symbol => Hash}] Hash containing stable and experimental release info
|
|
50
|
+
# @example Response format
|
|
51
|
+
# {
|
|
52
|
+
# stable: { alpha: "2.0.28", expansion: "2.0.28", headless: "2.0.28" },
|
|
53
|
+
# experimental: { alpha: "2.0.29", expansion: "2.0.29", headless: "2.0.29" }
|
|
54
|
+
# }
|
|
55
|
+
def latest_releases
|
|
56
|
+
logger.debug "Fetching latest releases"
|
|
57
|
+
uri = URI.join(API_BASE_URL, "/api/latest-releases")
|
|
58
|
+
response = client.get(uri)
|
|
59
|
+
JSON.parse((+response.body).force_encoding(Encoding::UTF_8), symbolize_names: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get the latest version for a specific channel and build
|
|
63
|
+
#
|
|
64
|
+
# @param channel [String] Release channel (stable, experimental)
|
|
65
|
+
# @param build [String] Build type (alpha, expansion, demo, headless)
|
|
66
|
+
# @return [String, nil] Version string or nil if not available
|
|
67
|
+
def latest_version(channel:, build:)
|
|
68
|
+
releases = latest_releases
|
|
69
|
+
releases.dig(channel.to_sym, build.to_sym)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Resolve the download filename by making a HEAD request
|
|
73
|
+
#
|
|
74
|
+
# @param version [String] Game version (e.g., "2.0.28")
|
|
75
|
+
# @param build [String] Build type (alpha, expansion, demo, headless)
|
|
76
|
+
# @param platform [String] Platform (win64, win64-manual, osx, linux64)
|
|
77
|
+
# @return [String] Filename extracted from final redirect URL
|
|
78
|
+
# @raise [ArgumentError] if build or platform is invalid
|
|
79
|
+
def resolve_filename(version:, build:, platform:)
|
|
80
|
+
validate_build!(build)
|
|
81
|
+
validate_platform!(platform)
|
|
82
|
+
|
|
83
|
+
uri = build_download_uri(version, build, platform)
|
|
84
|
+
response = client.head(uri)
|
|
85
|
+
File.basename(response.uri.path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Download the game to the specified output path
|
|
89
|
+
#
|
|
90
|
+
# @param version [String] Game version (e.g., "2.0.28")
|
|
91
|
+
# @param build [String] Build type (alpha, expansion, demo, headless)
|
|
92
|
+
# @param platform [String] Platform (win64, win64-manual, osx, linux64)
|
|
93
|
+
# @param output [Pathname] Output file path
|
|
94
|
+
# @param handler [Object, nil] Event handler for download progress (optional)
|
|
95
|
+
# @return [void]
|
|
96
|
+
# @raise [ArgumentError] if build or platform is invalid
|
|
97
|
+
def download(version:, build:, platform:, output:, handler: nil)
|
|
98
|
+
validate_build!(build)
|
|
99
|
+
validate_platform!(platform)
|
|
100
|
+
|
|
101
|
+
uri = build_download_uri(version, build, platform)
|
|
102
|
+
downloader = Container[:downloader]
|
|
103
|
+
downloader.subscribe(handler) if handler
|
|
104
|
+
begin
|
|
105
|
+
downloader.download(uri, output)
|
|
106
|
+
ensure
|
|
107
|
+
downloader.unsubscribe(handler) if handler
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build the download URI with authentication
|
|
112
|
+
#
|
|
113
|
+
# @param version [String] Game version
|
|
114
|
+
# @param build [String] Build type
|
|
115
|
+
# @param platform [String] Platform
|
|
116
|
+
# @return [URI::HTTPS] Complete download URI with credentials
|
|
117
|
+
private def build_download_uri(version, build, platform)
|
|
118
|
+
path = "/get-download/#{version}/#{build}/#{platform}"
|
|
119
|
+
uri = URI.join(DOWNLOAD_BASE_URL, path)
|
|
120
|
+
params = {username: service_credential.username, token: service_credential.token}
|
|
121
|
+
uri.query = URI.encode_www_form(params)
|
|
122
|
+
uri
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private def service_credential
|
|
126
|
+
return @service_credential if defined?(@service_credential)
|
|
127
|
+
|
|
128
|
+
@service_credential_mutex.synchronize do
|
|
129
|
+
@service_credential ||= Container[:service_credential]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Validate build type
|
|
134
|
+
#
|
|
135
|
+
# @param build [String] Build type to validate
|
|
136
|
+
# @raise [ArgumentError] if build type is invalid
|
|
137
|
+
private def validate_build!(build)
|
|
138
|
+
return if BUILDS.include?(build)
|
|
139
|
+
|
|
140
|
+
raise ArgumentError, "Invalid build type: #{build}. Valid types: #{BUILDS.join(", ")}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Validate platform
|
|
144
|
+
#
|
|
145
|
+
# @param platform [String] Platform to validate
|
|
146
|
+
# @raise [ArgumentError] if platform is invalid
|
|
147
|
+
private def validate_platform!(platform)
|
|
148
|
+
return if PLATFORMS.include?(platform)
|
|
149
|
+
|
|
150
|
+
raise ArgumentError, "Invalid platform: #{platform}. Valid platforms: #{PLATFORMS.join(", ")}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -152,7 +152,7 @@ module Factorix
|
|
|
152
152
|
|
|
153
153
|
# Filter detail_fields to only include keys that Detail.new accepts
|
|
154
154
|
# Exclude deprecated fields like github_path
|
|
155
|
-
detail = Detail
|
|
155
|
+
detail = Detail[**detail_fields.slice(*DETAIL_ALLOWED_KEYS)] if all_required_detail_fields?(detail_fields)
|
|
156
156
|
|
|
157
157
|
super(name:, title:, owner:, summary:, downloads_count:, category:, score:, thumbnail:, latest_release:, releases:, detail:)
|
|
158
158
|
end
|
|
@@ -273,11 +273,11 @@ module Factorix
|
|
|
273
273
|
|
|
274
274
|
logical_key = JSON.parse(metadata_path.read)["logical_key"]
|
|
275
275
|
age = Time.now - path.mtime
|
|
276
|
-
entry = Entry
|
|
276
|
+
entry = Entry[
|
|
277
277
|
size: path.size,
|
|
278
278
|
age:,
|
|
279
279
|
expired: @ttl ? age > @ttl : false
|
|
280
|
-
|
|
280
|
+
]
|
|
281
281
|
|
|
282
282
|
yield logical_key, entry
|
|
283
283
|
end
|
data/lib/factorix/cache/redis.rb
CHANGED
|
@@ -223,11 +223,11 @@ module Factorix
|
|
|
223
223
|
logical_key = logical_key_from_data_key(data_k)
|
|
224
224
|
meta = @redis.hgetall(meta_key(logical_key))
|
|
225
225
|
|
|
226
|
-
entry = Entry
|
|
226
|
+
entry = Entry[
|
|
227
227
|
size: meta["size"] ? Integer(meta["size"], 10) : 0,
|
|
228
228
|
age: meta["created_at"] ? Time.now.to_i - Integer(meta["created_at"], 10) : 0,
|
|
229
229
|
expired: false # Redis handles expiry natively
|
|
230
|
-
|
|
230
|
+
]
|
|
231
231
|
|
|
232
232
|
yield logical_key, entry
|
|
233
233
|
end
|
data/lib/factorix/cache/s3.rb
CHANGED
|
@@ -360,11 +360,11 @@ module Factorix
|
|
|
360
360
|
age = Time.now - obj.last_modified
|
|
361
361
|
expired = check_expired_from_head_response(resp)
|
|
362
362
|
|
|
363
|
-
entry = Entry
|
|
363
|
+
entry = Entry[
|
|
364
364
|
size: obj.size,
|
|
365
365
|
age:,
|
|
366
366
|
expired:
|
|
367
|
-
|
|
367
|
+
]
|
|
368
368
|
|
|
369
369
|
[logical_key, entry]
|
|
370
370
|
rescue Aws::S3::Errors::NotFound
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Download Factorio game files from the official download API
|
|
7
|
+
class Download < Base
|
|
8
|
+
# @!parse
|
|
9
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
10
|
+
# attr_reader :logger
|
|
11
|
+
# # @return [Runtime]
|
|
12
|
+
# attr_reader :runtime
|
|
13
|
+
# # @return [API::GameDownloadAPI]
|
|
14
|
+
# attr_reader :game_download_api
|
|
15
|
+
include Import[:logger, :runtime, :game_download_api]
|
|
16
|
+
|
|
17
|
+
# Platform mapping from Runtime to API platform identifier
|
|
18
|
+
PLATFORM_MAP = {
|
|
19
|
+
"MacOS" => "osx",
|
|
20
|
+
"Linux" => "linux64",
|
|
21
|
+
"Windows" => "win64",
|
|
22
|
+
"WSL" => "win64"
|
|
23
|
+
}.freeze
|
|
24
|
+
private_constant :PLATFORM_MAP
|
|
25
|
+
|
|
26
|
+
desc "Download Factorio game files"
|
|
27
|
+
|
|
28
|
+
argument :version, required: false, default: "latest", desc: "Version (e.g., 2.0.73, latest)"
|
|
29
|
+
|
|
30
|
+
option :build, aliases: ["-b"], default: "alpha", values: API::GameDownloadAPI::BUILDS, desc: "Build type"
|
|
31
|
+
option :platform, aliases: ["-p"], values: API::GameDownloadAPI::PLATFORMS, desc: "Platform (default: auto-detect)"
|
|
32
|
+
option :channel, aliases: ["-c"], default: "stable", values: API::GameDownloadAPI::CHANNELS, desc: "Release channel"
|
|
33
|
+
option :directory, aliases: ["-d"], default: ".", desc: "Download directory"
|
|
34
|
+
option :output, aliases: ["-o"], desc: "Output filename (default: from server)"
|
|
35
|
+
|
|
36
|
+
example [
|
|
37
|
+
" # Download latest stable version (auto-detect platform)",
|
|
38
|
+
"2.0.73 # Download specific version",
|
|
39
|
+
"--build expansion # Download expansion build",
|
|
40
|
+
"--build headless -p linux64 # Download headless server for Linux",
|
|
41
|
+
"--channel experimental # Download experimental release",
|
|
42
|
+
"-o factorio-server.tar.xz # Specify output filename"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Execute the download command
|
|
46
|
+
#
|
|
47
|
+
# @param version [String] Version to download
|
|
48
|
+
# @param build [String] Build type
|
|
49
|
+
# @param platform [String, nil] Platform (nil for auto-detect)
|
|
50
|
+
# @param channel [String] Release channel
|
|
51
|
+
# @param directory [String] Download directory
|
|
52
|
+
# @param output [String, nil] Output filename
|
|
53
|
+
# @return [void]
|
|
54
|
+
def call(version: "latest", build: "alpha", platform: nil, channel: "stable", directory: ".", output: nil, **)
|
|
55
|
+
platform ||= detect_platform
|
|
56
|
+
resolved_version = resolve_version(version, channel, build)
|
|
57
|
+
|
|
58
|
+
download_dir = Pathname(directory).expand_path
|
|
59
|
+
raise DirectoryNotFoundError, "Download directory does not exist: #{download_dir}" unless download_dir.exist?
|
|
60
|
+
|
|
61
|
+
filename = output || resolve_filename(resolved_version, build, platform)
|
|
62
|
+
output_path = download_dir / filename
|
|
63
|
+
|
|
64
|
+
say "Downloading Factorio #{resolved_version} (#{build}/#{platform})...", prefix: :info
|
|
65
|
+
|
|
66
|
+
download_game(resolved_version, build, platform, output_path)
|
|
67
|
+
|
|
68
|
+
say "Downloaded to #{output_path}", prefix: :success
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Detect platform from Runtime
|
|
72
|
+
#
|
|
73
|
+
# @return [String] Platform identifier
|
|
74
|
+
private def detect_platform
|
|
75
|
+
runtime_class = runtime.class.name.split("::").last
|
|
76
|
+
platform = PLATFORM_MAP[runtime_class]
|
|
77
|
+
raise UnsupportedPlatformError, "Cannot auto-detect platform for #{runtime_class}" unless platform
|
|
78
|
+
|
|
79
|
+
logger.debug("Auto-detected platform", platform:)
|
|
80
|
+
platform
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Minimum supported major version
|
|
84
|
+
MINIMUM_MAJOR_VERSION = 2
|
|
85
|
+
private_constant :MINIMUM_MAJOR_VERSION
|
|
86
|
+
|
|
87
|
+
# Resolve version, handling "latest" by fetching from API
|
|
88
|
+
#
|
|
89
|
+
# @param version [String] Version or "latest"
|
|
90
|
+
# @param channel [String] Release channel
|
|
91
|
+
# @param build [String] Build type
|
|
92
|
+
# @return [String] Resolved version
|
|
93
|
+
# @raise [InvalidArgumentError] if version is invalid or < 2.0
|
|
94
|
+
private def resolve_version(version, channel, build)
|
|
95
|
+
resolved = if version == "latest"
|
|
96
|
+
v = game_download_api.latest_version(channel:, build:)
|
|
97
|
+
raise InvalidArgumentError, "No #{channel} version available for #{build}" unless v
|
|
98
|
+
|
|
99
|
+
logger.debug("Resolved latest version", channel:, build:, version: v)
|
|
100
|
+
v
|
|
101
|
+
else
|
|
102
|
+
version
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
validate_version!(resolved)
|
|
106
|
+
resolved
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate version format and minimum version requirement
|
|
110
|
+
#
|
|
111
|
+
# @param version [String] Version string
|
|
112
|
+
# @return [void]
|
|
113
|
+
# @raise [InvalidArgumentError] if version is invalid or < 2.0
|
|
114
|
+
private def validate_version!(version)
|
|
115
|
+
game_version = GameVersion.from_string(version)
|
|
116
|
+
|
|
117
|
+
return if game_version.major >= MINIMUM_MAJOR_VERSION
|
|
118
|
+
|
|
119
|
+
raise InvalidArgumentError, "Version #{version} is not supported. Minimum version is #{MINIMUM_MAJOR_VERSION}.0.0"
|
|
120
|
+
rescue VersionParseError => e
|
|
121
|
+
raise InvalidArgumentError, "Invalid version format: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Resolve filename by making HEAD request
|
|
125
|
+
#
|
|
126
|
+
# @param version [String] Version
|
|
127
|
+
# @param build [String] Build type
|
|
128
|
+
# @param platform [String] Platform
|
|
129
|
+
# @return [String] Filename
|
|
130
|
+
private def resolve_filename(version, build, platform)
|
|
131
|
+
game_download_api.resolve_filename(version:, build:, platform:)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Download the game with progress tracking
|
|
135
|
+
#
|
|
136
|
+
# @param version [String] Version
|
|
137
|
+
# @param build [String] Build type
|
|
138
|
+
# @param platform [String] Platform
|
|
139
|
+
# @param output_path [Pathname] Output file path
|
|
140
|
+
# @return [void]
|
|
141
|
+
private def download_game(version, build, platform, output_path)
|
|
142
|
+
presenter = Progress::Presenter.new(title: output_path.basename.to_s, output: err)
|
|
143
|
+
handler = Progress::DownloadHandler.new(presenter)
|
|
144
|
+
|
|
145
|
+
game_download_api.download(version:, build:, platform:, output: output_path, handler:)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -147,7 +147,7 @@ module Factorix
|
|
|
147
147
|
enabled = mod_list.exist?(mod) && mod_list.enabled?(mod)
|
|
148
148
|
error = error_map[mod.name]
|
|
149
149
|
|
|
150
|
-
MODInfo
|
|
150
|
+
MODInfo[name: mod.name, version: display_version, enabled:, error:, latest_version: nil]
|
|
151
151
|
}
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -256,13 +256,13 @@ module Factorix
|
|
|
256
256
|
private def fetch_latest_version_for_mod(info)
|
|
257
257
|
portal_info = mod_portal_api.get_mod(info.name)
|
|
258
258
|
latest = portal_info[:releases]&.map {|r| MODVersion.from_string(r[:version]) }&.max
|
|
259
|
-
MODInfo
|
|
259
|
+
MODInfo[
|
|
260
260
|
name: info.name,
|
|
261
261
|
version: info.version,
|
|
262
262
|
enabled: info.enabled,
|
|
263
263
|
error: info.error,
|
|
264
264
|
latest_version: latest
|
|
265
|
-
|
|
265
|
+
]
|
|
266
266
|
rescue MODNotOnPortalError
|
|
267
267
|
logger.debug("MOD not found on portal", mod: info.name)
|
|
268
268
|
info
|
data/lib/factorix/cli.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Factorix
|
|
|
17
17
|
register "man", Commands::Man
|
|
18
18
|
register "launch", Commands::Launch
|
|
19
19
|
register "path", Commands::Path
|
|
20
|
+
register "download", Commands::Download
|
|
20
21
|
register "completion", Commands::Completion
|
|
21
22
|
register "mod check", Commands::MOD::Check
|
|
22
23
|
register "mod list", Commands::MOD::List
|
data/lib/factorix/container.rb
CHANGED
|
@@ -136,6 +136,11 @@ module Factorix
|
|
|
136
136
|
API::MODDownloadAPI.new
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Register Game Download API client
|
|
140
|
+
register(:game_download_api, memoize: true) do
|
|
141
|
+
API::GameDownloadAPI.new
|
|
142
|
+
end
|
|
143
|
+
|
|
139
144
|
# Register API credential (for MOD upload/management)
|
|
140
145
|
register(:api_credential, memoize: true) { APICredential.load }
|
|
141
146
|
|
|
@@ -53,7 +53,7 @@ module Factorix
|
|
|
53
53
|
version = select_version_for_mod(mod)
|
|
54
54
|
enabled = mod_enabled?(mod)
|
|
55
55
|
|
|
56
|
-
node = Node
|
|
56
|
+
node = Node[mod:, version:, enabled:, installed: true]
|
|
57
57
|
graph.add_node(node)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@ module Factorix
|
|
|
88
88
|
# Expansion MODs can be disabled, so they must be validated
|
|
89
89
|
next if dependency.mod.base?
|
|
90
90
|
|
|
91
|
-
edge = Edge
|
|
91
|
+
edge = Edge[from_mod:, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
|
|
92
92
|
graph.add_edge(edge)
|
|
93
93
|
end
|
|
94
94
|
end
|
|
@@ -75,7 +75,7 @@ module Factorix
|
|
|
75
75
|
return
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
node = Node
|
|
78
|
+
node = Node[mod:, version: release.version, enabled: false, installed: false, operation:]
|
|
79
79
|
add_node(node)
|
|
80
80
|
|
|
81
81
|
dependencies = release.info_json[:dependencies] || []
|
|
@@ -85,7 +85,7 @@ module Factorix
|
|
|
85
85
|
dependency = parser.parse(dep_string)
|
|
86
86
|
next if dependency.mod.base?
|
|
87
87
|
|
|
88
|
-
edge = Edge
|
|
88
|
+
edge = Edge[from_mod: mod, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
|
|
89
89
|
|
|
90
90
|
add_edge(edge)
|
|
91
91
|
end
|
|
@@ -81,7 +81,7 @@ module Factorix
|
|
|
81
81
|
# @param mod [Factorix::MOD, nil] Related MOD
|
|
82
82
|
# @param dependency [Factorix::MOD, nil] Dependency MOD
|
|
83
83
|
# @return [void]
|
|
84
|
-
def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error
|
|
84
|
+
def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error[type:, message:, mod:, dependency:]
|
|
85
85
|
|
|
86
86
|
# Add a warning
|
|
87
87
|
#
|
|
@@ -89,7 +89,7 @@ module Factorix
|
|
|
89
89
|
# @param message [String] Warning message
|
|
90
90
|
# @param mod [Factorix::MOD, nil] Related MOD
|
|
91
91
|
# @return [void]
|
|
92
|
-
def add_warning(type:, message:, mod: nil) = @warnings << Warning
|
|
92
|
+
def add_warning(type:, message:, mod: nil) = @warnings << Warning[type:, message:, mod:]
|
|
93
93
|
|
|
94
94
|
# Add a suggestion
|
|
95
95
|
#
|
|
@@ -97,7 +97,7 @@ module Factorix
|
|
|
97
97
|
# @param mod [Factorix::MOD] Related MOD
|
|
98
98
|
# @param version [Factorix::MODVersion] Suggested version
|
|
99
99
|
# @return [void]
|
|
100
|
-
def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion
|
|
100
|
+
def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion[message:, mod:, version:]
|
|
101
101
|
|
|
102
102
|
# Get all errors
|
|
103
103
|
#
|
|
@@ -56,7 +56,7 @@ module Factorix
|
|
|
56
56
|
if cached_body
|
|
57
57
|
logger.debug("Cache hit", uri: uri.to_s)
|
|
58
58
|
publish("cache.hit", url: uri.to_s)
|
|
59
|
-
return CachedResponse.new(cached_body)
|
|
59
|
+
return CachedResponse.new(cached_body, uri:)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
logger.debug("Cache miss", uri: uri.to_s)
|
|
@@ -68,7 +68,7 @@ module Factorix
|
|
|
68
68
|
cached_body = cache.read(cache_key)
|
|
69
69
|
if cached_body
|
|
70
70
|
publish("cache.hit", url: uri.to_s)
|
|
71
|
-
return CachedResponse.new(cached_body)
|
|
71
|
+
return CachedResponse.new(cached_body, uri:)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
response = client.get(uri, headers:)
|
|
@@ -94,6 +94,13 @@ module Factorix
|
|
|
94
94
|
# @return [Response] response object
|
|
95
95
|
def post(uri, body:, headers: {}, content_type: nil) = client.post(uri, body:, headers:, content_type:)
|
|
96
96
|
|
|
97
|
+
# Execute a HEAD request (never cached)
|
|
98
|
+
#
|
|
99
|
+
# @param uri [URI::HTTPS] target URI
|
|
100
|
+
# @param headers [Hash<String, String>] request headers
|
|
101
|
+
# @return [Response] response object
|
|
102
|
+
def head(uri, headers: {}) = client.head(uri, headers:)
|
|
103
|
+
|
|
97
104
|
private def with_temporary_file
|
|
98
105
|
temp_file = Tempfile.new("http_cache")
|
|
99
106
|
yield temp_file
|
|
@@ -12,12 +12,15 @@ module Factorix
|
|
|
12
12
|
attr_reader :body
|
|
13
13
|
attr_reader :code
|
|
14
14
|
attr_reader :headers
|
|
15
|
+
attr_reader :uri
|
|
15
16
|
|
|
16
17
|
# @param body [String] cached response body
|
|
17
|
-
|
|
18
|
+
# @param uri [URI, nil] original request URI (not stored in cache, always nil)
|
|
19
|
+
def initialize(body, uri: nil)
|
|
18
20
|
@body = body
|
|
19
21
|
@code = 200
|
|
20
22
|
@headers = {"content-type" => ["application/octet-stream"]}
|
|
23
|
+
@uri = uri
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
# Always returns true for cached responses
|
data/lib/factorix/http/client.rb
CHANGED
|
@@ -58,6 +58,13 @@ module Factorix
|
|
|
58
58
|
# @return [Response] response object
|
|
59
59
|
def get(uri, headers: {}, &) = request(:get, uri, headers:, &)
|
|
60
60
|
|
|
61
|
+
# Execute a HEAD request
|
|
62
|
+
#
|
|
63
|
+
# @param uri [URI::HTTPS] target URI
|
|
64
|
+
# @param headers [Hash<String, String>] request headers
|
|
65
|
+
# @return [Response] response object
|
|
66
|
+
def head(uri, headers: {}) = request(:head, uri, headers:)
|
|
67
|
+
|
|
61
68
|
# Execute a POST request
|
|
62
69
|
#
|
|
63
70
|
# @param uri [URI::HTTPS] target URI
|
|
@@ -86,18 +93,20 @@ module Factorix
|
|
|
86
93
|
result
|
|
87
94
|
end
|
|
88
95
|
|
|
89
|
-
private def handle_response(response,
|
|
96
|
+
private def handle_response(response, method, uri, redirect_count, &block)
|
|
90
97
|
case response
|
|
91
98
|
when Net::HTTPSuccess, Net::HTTPPartialContent
|
|
92
99
|
yield(response) if block
|
|
93
|
-
Response.new(response)
|
|
100
|
+
Response.new(response, uri:)
|
|
94
101
|
|
|
95
102
|
when Net::HTTPRedirection
|
|
96
103
|
location = response["Location"]
|
|
97
104
|
redirect_url = URI(location)
|
|
98
105
|
logger.info("Following redirect", location: mask_credentials(redirect_url))
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
# HEAD stays HEAD, others become GET (standard redirect behavior)
|
|
108
|
+
redirect_method = method == :head ? :head : :get
|
|
109
|
+
perform_request(redirect_method, redirect_url, redirect_count: redirect_count + 1, headers: {}, body: nil, &block)
|
|
101
110
|
|
|
102
111
|
when Net::HTTPNotFound
|
|
103
112
|
api_error, api_message = parse_api_error(response)
|
|
@@ -139,6 +148,7 @@ module Factorix
|
|
|
139
148
|
private def build_request(method, uri, headers:, body:)
|
|
140
149
|
request = case method
|
|
141
150
|
when :get then Net::HTTP::Get.new(uri)
|
|
151
|
+
when :head then Net::HTTP::Head.new(uri)
|
|
142
152
|
when :post then Net::HTTP::Post.new(uri)
|
|
143
153
|
when :put then Net::HTTP::Put.new(uri)
|
|
144
154
|
when :delete then Net::HTTP::Delete.new(uri)
|
|
@@ -8,13 +8,16 @@ module Factorix
|
|
|
8
8
|
attr_reader :body
|
|
9
9
|
attr_reader :headers
|
|
10
10
|
attr_reader :raw_response
|
|
11
|
+
attr_reader :uri
|
|
11
12
|
|
|
12
13
|
# @param net_http_response [Net::HTTPResponse] Raw Net::HTTP response
|
|
13
|
-
|
|
14
|
+
# @param uri [URI, nil] Final URI after following redirects
|
|
15
|
+
def initialize(net_http_response, uri: nil)
|
|
14
16
|
@code = Integer(net_http_response.code, 10)
|
|
15
17
|
@body = net_http_response.body
|
|
16
18
|
@headers = net_http_response.to_hash
|
|
17
19
|
@raw_response = net_http_response
|
|
20
|
+
@uri = uri
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
# Check if response is successful (2xx)
|
|
@@ -54,6 +54,17 @@ module Factorix
|
|
|
54
54
|
client.post(uri, body:, headers:, content_type:)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
+
|
|
58
|
+
# Execute a HEAD request with retry
|
|
59
|
+
#
|
|
60
|
+
# @param uri [URI::HTTPS] target URI
|
|
61
|
+
# @param headers [Hash<String, String>] request headers
|
|
62
|
+
# @return [Response] response object
|
|
63
|
+
def head(uri, headers: {})
|
|
64
|
+
retry_strategy.with_retry do
|
|
65
|
+
client.head(uri, headers:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
end
|
data/lib/factorix/save_file.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Factorix
|
|
|
49
49
|
parse_startup_settings(deserializer)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
SaveFile
|
|
52
|
+
SaveFile[version: @version, mods: @mods, startup_settings: @startup_settings]
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
private def open_level_file
|
|
@@ -143,7 +143,7 @@ module Factorix
|
|
|
143
143
|
_crc = deserializer.read_u32
|
|
144
144
|
|
|
145
145
|
# All MODs in save file are enabled
|
|
146
|
-
@mods[name] = MODState
|
|
146
|
+
@mods[name] = MODState[enabled: true, version:]
|
|
147
147
|
end
|
|
148
148
|
end
|
|
149
149
|
|
data/lib/factorix/version.rb
CHANGED
data/lib/factorix.rb
CHANGED
|
@@ -129,6 +129,7 @@ module Factorix
|
|
|
129
129
|
"installed_mod" => "InstalledMOD",
|
|
130
130
|
"mac_os" => "MacOS",
|
|
131
131
|
"mod" => "MOD",
|
|
132
|
+
"game_download_api" => "GameDownloadAPI",
|
|
132
133
|
"mod_download_api" => "MODDownloadAPI",
|
|
133
134
|
"mod_info" => "MODInfo",
|
|
134
135
|
"mod_management_api" => "MODManagementAPI",
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: factorix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OZAWA Sakuro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -226,6 +226,7 @@ files:
|
|
|
226
226
|
- lib/factorix.rb
|
|
227
227
|
- lib/factorix/api.rb
|
|
228
228
|
- lib/factorix/api/category.rb
|
|
229
|
+
- lib/factorix/api/game_download_api.rb
|
|
229
230
|
- lib/factorix/api/image.rb
|
|
230
231
|
- lib/factorix/api/license.rb
|
|
231
232
|
- lib/factorix/api/mod_download_api.rb
|
|
@@ -248,6 +249,7 @@ files:
|
|
|
248
249
|
- lib/factorix/cli/commands/command_wrapper.rb
|
|
249
250
|
- lib/factorix/cli/commands/completion.rb
|
|
250
251
|
- lib/factorix/cli/commands/confirmable.rb
|
|
252
|
+
- lib/factorix/cli/commands/download.rb
|
|
251
253
|
- lib/factorix/cli/commands/download_support.rb
|
|
252
254
|
- lib/factorix/cli/commands/launch.rb
|
|
253
255
|
- lib/factorix/cli/commands/man.rb
|