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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Factorix
|
|
7
|
+
class Runtime
|
|
8
|
+
# WSL (Windows Subsystem for Linux) runtime environment
|
|
9
|
+
#
|
|
10
|
+
# This implementation inherits from Windows and converts Windows paths
|
|
11
|
+
# to WSL paths. It retrieves Windows environment variables via PowerShell
|
|
12
|
+
# in a single batch operation and converts paths using native Ruby code.
|
|
13
|
+
class WSL < Windows
|
|
14
|
+
# Initialize WSL runtime environment
|
|
15
|
+
#
|
|
16
|
+
# @param path [WSLPath] the path provider (for dependency injection)
|
|
17
|
+
def initialize(path: WSLPath.new) = super
|
|
18
|
+
|
|
19
|
+
# WSL-specific path provider
|
|
20
|
+
#
|
|
21
|
+
# This class fetches Windows environment variables via PowerShell in one batch operation and converts Windows paths to WSL paths.
|
|
22
|
+
class WSLPath
|
|
23
|
+
# Default WSL mount root for Windows drives
|
|
24
|
+
MOUNT_ROOT = "/mnt"
|
|
25
|
+
private_constant :MOUNT_ROOT
|
|
26
|
+
|
|
27
|
+
# PowerShell script to fetch Windows environment variables
|
|
28
|
+
POWERSHELL_SCRIPT = <<~POWERSHELL
|
|
29
|
+
[pscustomobject]@{
|
|
30
|
+
"ProgramFiles(x86)" = ${Env:ProgramFiles(x86)};
|
|
31
|
+
"APPDATA" = ${Env:APPDATA};
|
|
32
|
+
"LOCALAPPDATA" = ${Env:LOCALAPPDATA}
|
|
33
|
+
} | ConvertTo-Json -Compress
|
|
34
|
+
POWERSHELL
|
|
35
|
+
private_constant :POWERSHELL_SCRIPT
|
|
36
|
+
|
|
37
|
+
POWERSHELL_FALLBACK_PATHS = %w[
|
|
38
|
+
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe
|
|
39
|
+
/mnt/c/Windows/system32/WindowsPowerShell/v1.0/powershell.exe
|
|
40
|
+
].freeze
|
|
41
|
+
private_constant :POWERSHELL_FALLBACK_PATHS
|
|
42
|
+
|
|
43
|
+
# Initialize the path provider
|
|
44
|
+
#
|
|
45
|
+
# Sets up the mutex for thread-safe lazy initialization
|
|
46
|
+
def initialize
|
|
47
|
+
@mutex = Mutex.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get the Program Files (x86) directory path (WSL-converted)
|
|
51
|
+
#
|
|
52
|
+
# @return [Pathname] the Program Files (x86) directory
|
|
53
|
+
def program_files_x86 = @program_files_x86 ||= Pathname(convert_windows_to_wsl(windows_paths["ProgramFiles(x86)"]))
|
|
54
|
+
|
|
55
|
+
# Get the AppData directory path (WSL-converted)
|
|
56
|
+
#
|
|
57
|
+
# @return [Pathname] the AppData directory
|
|
58
|
+
def app_data = @app_data ||= Pathname(convert_windows_to_wsl(windows_paths["APPDATA"]))
|
|
59
|
+
|
|
60
|
+
# Get the Local AppData directory path (WSL-converted)
|
|
61
|
+
#
|
|
62
|
+
# @return [Pathname] the Local AppData directory
|
|
63
|
+
def local_app_data = @local_app_data ||= Pathname(convert_windows_to_wsl(windows_paths["LOCALAPPDATA"]))
|
|
64
|
+
|
|
65
|
+
# Fetch and cache all Windows environment variables
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash] the environment variables as a hash
|
|
68
|
+
private def windows_paths
|
|
69
|
+
return @windows_paths if @windows_paths
|
|
70
|
+
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
@windows_paths ||= fetch_windows_paths_via_powershell
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Fetch Windows environment variables via PowerShell
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash] the environment variables as a hash
|
|
79
|
+
# @raise [PlatformError] if PowerShell execution fails
|
|
80
|
+
private def fetch_windows_paths_via_powershell
|
|
81
|
+
ps = find_powershell_exe
|
|
82
|
+
|
|
83
|
+
stdout, status = Open3.capture2(ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", POWERSHELL_SCRIPT)
|
|
84
|
+
|
|
85
|
+
raise PlatformError, "PowerShell execution failed: #{status}" unless status.success?
|
|
86
|
+
|
|
87
|
+
JSON.parse(stdout.encode("UTF-8", invalid: :replace, undef: :replace))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert Windows path to WSL path
|
|
91
|
+
#
|
|
92
|
+
# @param windows_path [String] Windows-style path to convert
|
|
93
|
+
# @return [String] equivalent WSL path
|
|
94
|
+
# @raise [PlatformError] if the path format is invalid
|
|
95
|
+
private def convert_windows_to_wsl(windows_path)
|
|
96
|
+
raise PlatformError, "Invalid Windows path: #{windows_path}" unless windows_path =~ %r{\A([A-Za-z]):[\\/]?(.*)\z}
|
|
97
|
+
|
|
98
|
+
drive = $1.downcase
|
|
99
|
+
path = $2.tr("\\", "/")
|
|
100
|
+
result = "#{MOUNT_ROOT}/#{drive}/#{path}"
|
|
101
|
+
# Normalize: collapse multiple slashes and remove trailing slash
|
|
102
|
+
result.squeeze("/").delete_suffix("/")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find powershell.exe executable
|
|
106
|
+
#
|
|
107
|
+
# @return [String] path to powershell.exe
|
|
108
|
+
# @raise [PlatformError] if powershell.exe is not found
|
|
109
|
+
private def find_powershell_exe
|
|
110
|
+
return "powershell.exe" if system("which", "powershell.exe", out: File::NULL, err: File::NULL)
|
|
111
|
+
|
|
112
|
+
POWERSHELL_FALLBACK_PATHS.find {|path| File.exist?(path) } ||
|
|
113
|
+
raise(PlatformError, "powershell.exe not found in PATH or default locations")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
# Factorio runtime environment abstraction
|
|
5
|
+
#
|
|
6
|
+
# This class provides a factory method to detect the current platform
|
|
7
|
+
# and return the appropriate runtime environment instance.
|
|
8
|
+
class Runtime
|
|
9
|
+
# Detect the current platform and return the appropriate runtime
|
|
10
|
+
#
|
|
11
|
+
# @return [Runtime::Base] the runtime environment for the current platform
|
|
12
|
+
# @raise [UnsupportedPlatformError] if the platform is not supported
|
|
13
|
+
def self.detect
|
|
14
|
+
case RUBY_PLATFORM
|
|
15
|
+
when /darwin/
|
|
16
|
+
MacOS.new
|
|
17
|
+
when /mingw|mswin/
|
|
18
|
+
Windows.new
|
|
19
|
+
when /linux/
|
|
20
|
+
wsl? ? WSL.new : Linux.new
|
|
21
|
+
else
|
|
22
|
+
raise UnsupportedPlatformError, "Platform is not supported: #{RUBY_PLATFORM}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if running on WSL
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if running on WSL, false otherwise
|
|
29
|
+
def self.wsl? = File.exist?("/proc/version") && /microsoft/i.match?(File.read("/proc/version"))
|
|
30
|
+
private_class_method :wsl?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zip"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module Factorix
|
|
7
|
+
SaveFile = Data.define(:version, :mods, :startup_settings)
|
|
8
|
+
|
|
9
|
+
# Data structure for Factorio save file information
|
|
10
|
+
#
|
|
11
|
+
# SaveFile provides functionality to extract MOD information and startup settings
|
|
12
|
+
# from Factorio save files (.zip format containing level.dat0 or level-init.dat).
|
|
13
|
+
class SaveFile
|
|
14
|
+
# @!attribute [r] version
|
|
15
|
+
# @return [Factorix::GameVersion] Game version from the save file
|
|
16
|
+
# @!attribute [r] mods
|
|
17
|
+
# @return [Hash<String, Factorix::MODState>] Hash of MOD name to MODState
|
|
18
|
+
# @!attribute [r] startup_settings
|
|
19
|
+
# @return [Factorix::MODSettings::Section] Startup settings section
|
|
20
|
+
|
|
21
|
+
# Level file names to search for, in priority order
|
|
22
|
+
LEVEL_FILE_NAMES = %w[level.dat0 level-init.dat].freeze
|
|
23
|
+
private_constant :LEVEL_FILE_NAMES
|
|
24
|
+
|
|
25
|
+
# Load a save file and extract MOD information and settings
|
|
26
|
+
#
|
|
27
|
+
# @param path [Pathname] Path to the save file (.zip)
|
|
28
|
+
# @return [SaveFile] Extracted save file data
|
|
29
|
+
# @raise [FileFormatError] If save file or level file not found
|
|
30
|
+
# @raise [Factorix::Error] If save file format is invalid
|
|
31
|
+
def self.load(path) = Parser.new(path).parse
|
|
32
|
+
|
|
33
|
+
# Internal parser for save files
|
|
34
|
+
class Parser
|
|
35
|
+
# Initialize a new Parser instance
|
|
36
|
+
#
|
|
37
|
+
# @param path [Pathname] Path to the save file
|
|
38
|
+
def initialize(path) = @path = path
|
|
39
|
+
|
|
40
|
+
# Parse the save file and return extracted data
|
|
41
|
+
#
|
|
42
|
+
# @return [SaveFile] Extracted save file data
|
|
43
|
+
# @raise [FileFormatError] If save file or level file not found
|
|
44
|
+
def parse
|
|
45
|
+
open_level_file do |stream|
|
|
46
|
+
deserializer = SerDes::Deserializer.new(stream)
|
|
47
|
+
parse_save_header(deserializer)
|
|
48
|
+
skip_unknown_bytes(deserializer)
|
|
49
|
+
parse_startup_settings(deserializer)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
SaveFile.new(version: @version, mods: @mods, startup_settings: @startup_settings)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private def open_level_file
|
|
56
|
+
Zip::File.open(@path) do |zip_file|
|
|
57
|
+
LEVEL_FILE_NAMES.each do |file_name|
|
|
58
|
+
entry = find_level_entry(zip_file, file_name)
|
|
59
|
+
next unless entry
|
|
60
|
+
|
|
61
|
+
stream = entry.get_input_stream
|
|
62
|
+
return yield decompress_if_needed(stream)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
raise FileFormatError, "level.dat0 or level-init.dat not found in #{@path}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Find a level file entry in the zip file
|
|
70
|
+
#
|
|
71
|
+
# @param zip_file [Zip::File] The zip file
|
|
72
|
+
# @param file_name [String] The level file name to search for
|
|
73
|
+
# @return [Zip::Entry, nil] The entry if found, nil otherwise
|
|
74
|
+
private def find_level_entry(zip_file, file_name) = zip_file.glob("*/#{file_name}").first
|
|
75
|
+
|
|
76
|
+
# Decompress stream if it's zlib compressed
|
|
77
|
+
#
|
|
78
|
+
# @param stream [IO] The input stream
|
|
79
|
+
# @return [IO] Decompressed stream or original stream
|
|
80
|
+
private def decompress_if_needed(stream)
|
|
81
|
+
# Read CMF (Compression Method and Flags) byte
|
|
82
|
+
cmf = stream.read(1)
|
|
83
|
+
return StringIO.new("") if cmf.nil?
|
|
84
|
+
|
|
85
|
+
stream.rewind
|
|
86
|
+
|
|
87
|
+
# CMF = 0x78 indicates zlib compression (DEFLATE with 32K window)
|
|
88
|
+
if cmf.unpack1("C") == 0x78
|
|
89
|
+
StringIO.new(Zlib::Inflate.inflate(stream.read))
|
|
90
|
+
else
|
|
91
|
+
stream
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Parse save header to extract game version and MOD list
|
|
96
|
+
#
|
|
97
|
+
# @param deserializer [Factorix::SerDes::Deserializer] The deserializer
|
|
98
|
+
# @return [void]
|
|
99
|
+
private def parse_save_header(deserializer)
|
|
100
|
+
# Read game version
|
|
101
|
+
@version = deserializer.read_game_version
|
|
102
|
+
|
|
103
|
+
# Skip 1 byte after version
|
|
104
|
+
deserializer.read_u8
|
|
105
|
+
|
|
106
|
+
# Skip fields we don't need
|
|
107
|
+
deserializer.read_str # campaign
|
|
108
|
+
deserializer.read_str # level_name
|
|
109
|
+
deserializer.read_str # base_mod
|
|
110
|
+
deserializer.read_u8 # difficulty
|
|
111
|
+
deserializer.read_bool # finished
|
|
112
|
+
deserializer.read_bool # player_won
|
|
113
|
+
deserializer.read_str # next_level
|
|
114
|
+
deserializer.read_bool # can_continue
|
|
115
|
+
deserializer.read_bool # finished_but_continuing
|
|
116
|
+
deserializer.read_bool # saving_replay
|
|
117
|
+
deserializer.read_bool # allow_non_admin_debug_options
|
|
118
|
+
deserializer.read_mod_version # loaded_from (MODVersion, not GameVersion)
|
|
119
|
+
deserializer.read_u16 # loaded_from_build
|
|
120
|
+
deserializer.read_u8 # allowed_commands
|
|
121
|
+
|
|
122
|
+
# Additional fields before the MOD list
|
|
123
|
+
# These fields' purposes are not yet fully understood
|
|
124
|
+
deserializer.read_bool # Unknown boolean field
|
|
125
|
+
deserializer.read_u32 # Unknown u32 field
|
|
126
|
+
deserializer.read_bool # Unknown boolean field
|
|
127
|
+
|
|
128
|
+
# Read MOD list
|
|
129
|
+
parse_mods(deserializer)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Parse MOD list from save header
|
|
133
|
+
#
|
|
134
|
+
# @param deserializer [Factorix::SerDes::Deserializer] The deserializer
|
|
135
|
+
# @return [void]
|
|
136
|
+
private def parse_mods(deserializer)
|
|
137
|
+
mods_count = deserializer.read_optim_u32
|
|
138
|
+
@mods = {}
|
|
139
|
+
|
|
140
|
+
mods_count.times do
|
|
141
|
+
name = deserializer.read_str
|
|
142
|
+
version = deserializer.read_mod_version
|
|
143
|
+
_crc = deserializer.read_u32
|
|
144
|
+
|
|
145
|
+
# All MODs in save file are enabled
|
|
146
|
+
@mods[name] = MODState.new(enabled: true, version:)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Skip unknown 4 bytes after MOD list
|
|
151
|
+
#
|
|
152
|
+
# @param deserializer [Factorix::SerDes::Deserializer] The deserializer
|
|
153
|
+
# @return [void]
|
|
154
|
+
private def skip_unknown_bytes(deserializer) = deserializer.read_bytes(4)
|
|
155
|
+
|
|
156
|
+
# Parse startup settings from save file
|
|
157
|
+
#
|
|
158
|
+
# @param deserializer [Factorix::SerDes::Deserializer] The deserializer
|
|
159
|
+
# @return [void]
|
|
160
|
+
private def parse_startup_settings(deserializer)
|
|
161
|
+
raw_settings = deserializer.read_property_tree
|
|
162
|
+
|
|
163
|
+
# Create a new Section and populate it
|
|
164
|
+
@startup_settings = MODSettings::Section.new("startup")
|
|
165
|
+
|
|
166
|
+
return unless raw_settings.is_a?(Hash)
|
|
167
|
+
|
|
168
|
+
raw_settings.each do |key, value_hash|
|
|
169
|
+
# Extract the actual value from the {"value" => X} hash
|
|
170
|
+
next unless value_hash.is_a?(Hash)
|
|
171
|
+
|
|
172
|
+
@startup_settings[key] = value_hash["value"]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
private_constant :Parser
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
module SerDes
|
|
5
|
+
# Deserialize data from binary format
|
|
6
|
+
#
|
|
7
|
+
# This class provides methods to deserialize various data types from Factorio's
|
|
8
|
+
# binary file format, following the specifications documented in the Factorio wiki.
|
|
9
|
+
class Deserializer
|
|
10
|
+
# @!parse
|
|
11
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
12
|
+
# attr_reader :logger
|
|
13
|
+
include Import[:logger]
|
|
14
|
+
|
|
15
|
+
# Create a new Deserializer instance
|
|
16
|
+
#
|
|
17
|
+
# @param stream [IO] An IO-like object that responds to #read
|
|
18
|
+
# @param logger [Dry::Logger::Dispatcher] optional logger
|
|
19
|
+
# @raise [ArgumentError] If the stream doesn't respond to #read
|
|
20
|
+
def initialize(stream, logger: nil)
|
|
21
|
+
super(logger:)
|
|
22
|
+
raise ArgumentError, "can't read from the given argument" unless stream.respond_to?(:read)
|
|
23
|
+
|
|
24
|
+
@stream = stream
|
|
25
|
+
logger.debug "Initializing Deserializer"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read raw bytes from the stream
|
|
29
|
+
#
|
|
30
|
+
# @param length [Integer] Number of bytes to read
|
|
31
|
+
# @raise [Factorix::InvalidLengthError] If length is nil or negative
|
|
32
|
+
# @raise [EOFError] If end of file is reached before reading length bytes
|
|
33
|
+
# @return [String] Binary data read
|
|
34
|
+
def read_bytes(length)
|
|
35
|
+
raise InvalidLengthError, "nil length" if length.nil?
|
|
36
|
+
raise InvalidLengthError, "negative length #{length}" if length.negative?
|
|
37
|
+
return +"" if length.zero?
|
|
38
|
+
|
|
39
|
+
bytes = @stream.read(length)
|
|
40
|
+
if bytes.nil? || bytes.size < length
|
|
41
|
+
logger.debug("Unexpected EOF", requested_bytes: length)
|
|
42
|
+
raise EOFError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
bytes
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Read an unsigned 8-bit integer
|
|
49
|
+
#
|
|
50
|
+
# @return [Integer] 8-bit unsigned integer
|
|
51
|
+
def read_u8 = read_bytes(1).unpack1("C")
|
|
52
|
+
|
|
53
|
+
# Read an unsigned 16-bit integer
|
|
54
|
+
#
|
|
55
|
+
# @return [Integer] 16-bit unsigned integer
|
|
56
|
+
def read_u16 = read_bytes(2).unpack1("v")
|
|
57
|
+
|
|
58
|
+
# Read an unsigned 32-bit integer
|
|
59
|
+
#
|
|
60
|
+
# @return [Integer] 32-bit unsigned integer
|
|
61
|
+
def read_u32 = read_bytes(4).unpack1("V")
|
|
62
|
+
|
|
63
|
+
# Read a space-optimized 16-bit unsigned integer
|
|
64
|
+
#
|
|
65
|
+
# @see https://wiki.factorio.com/Data_types#Space_Optimized
|
|
66
|
+
# @return [Integer] 16-bit unsigned integer
|
|
67
|
+
def read_optim_u16
|
|
68
|
+
byte = read_u8
|
|
69
|
+
byte == 0xFF ? read_u16 : byte
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Read a space-optimized 32-bit unsigned integer
|
|
73
|
+
#
|
|
74
|
+
# @see https://wiki.factorio.com/Data_types#Space_Optimized
|
|
75
|
+
# @return [Integer] 32-bit unsigned integer
|
|
76
|
+
def read_optim_u32
|
|
77
|
+
byte = read_u8
|
|
78
|
+
byte == 0xFF ? read_u32 : byte
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Read a tuple of 16-bit unsigned integers
|
|
82
|
+
#
|
|
83
|
+
# @param length [Integer] Number of integers to read
|
|
84
|
+
# @return [Array<Integer>] Array of 16-bit unsigned integers
|
|
85
|
+
def read_u16_tuple(length) = Array.new(length) { read_u16 }
|
|
86
|
+
|
|
87
|
+
# Read a boolean value
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] Boolean value
|
|
90
|
+
def read_bool = read_u8 != 0
|
|
91
|
+
|
|
92
|
+
# Read a string
|
|
93
|
+
#
|
|
94
|
+
# @return [String] String read
|
|
95
|
+
def read_str
|
|
96
|
+
length = read_optim_u32
|
|
97
|
+
read_bytes(length).force_encoding(Encoding::UTF_8)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Read a string property
|
|
101
|
+
#
|
|
102
|
+
# @see https://wiki.factorio.com/Property_tree#String
|
|
103
|
+
# @return [String] String property
|
|
104
|
+
def read_str_property = read_bool ? "" : read_str
|
|
105
|
+
|
|
106
|
+
# Read a double-precision floating point number
|
|
107
|
+
#
|
|
108
|
+
# @see https://wiki.factorio.com/Property_tree#Number
|
|
109
|
+
# @return [Float] Double-precision floating point number
|
|
110
|
+
def read_double = read_bytes(8).unpack1("d")
|
|
111
|
+
|
|
112
|
+
# Read a GameVersion object
|
|
113
|
+
#
|
|
114
|
+
# @return [GameVersion] GameVersion object
|
|
115
|
+
def read_game_version = GameVersion.from_numbers(read_u16, read_u16, read_u16, read_u16)
|
|
116
|
+
|
|
117
|
+
# Read a MODVersion object
|
|
118
|
+
#
|
|
119
|
+
# @return [MODVersion] MODVersion object
|
|
120
|
+
def read_mod_version = MODVersion.from_numbers(read_optim_u16, read_optim_u16, read_optim_u16)
|
|
121
|
+
|
|
122
|
+
# Read a signed long integer (8 bytes)
|
|
123
|
+
#
|
|
124
|
+
# @return [Integer] Signed long integer
|
|
125
|
+
def read_long = read_bytes(8).unpack1("q<")
|
|
126
|
+
|
|
127
|
+
# Read an unsigned long integer (8 bytes)
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer] Unsigned long integer
|
|
130
|
+
def read_unsigned_long = read_bytes(8).unpack1("Q<")
|
|
131
|
+
|
|
132
|
+
# Read a dictionary
|
|
133
|
+
#
|
|
134
|
+
# @see https://wiki.factorio.com/Property_tree#Dictionary
|
|
135
|
+
# @return [Hash] Dictionary of key-value pairs
|
|
136
|
+
def read_dictionary
|
|
137
|
+
length = read_u32
|
|
138
|
+
logger.debug("Reading dictionary", length:)
|
|
139
|
+
length.times.each_with_object({}) do |_i, dict|
|
|
140
|
+
key = read_str_property
|
|
141
|
+
dict[key] = read_property_tree
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Read a list
|
|
146
|
+
# This type is identical to dictionary
|
|
147
|
+
#
|
|
148
|
+
# @see https://wiki.factorio.com/Property_tree#List
|
|
149
|
+
alias read_list read_dictionary
|
|
150
|
+
|
|
151
|
+
# Read a property tree
|
|
152
|
+
#
|
|
153
|
+
# @raise [Factorix::UnknownPropertyType] If the property type is not supported
|
|
154
|
+
# @return [Object] Object read
|
|
155
|
+
def read_property_tree
|
|
156
|
+
type = read_u8
|
|
157
|
+
_any_type_flag = read_bool
|
|
158
|
+
logger.debug("Reading property tree", type:)
|
|
159
|
+
|
|
160
|
+
case type
|
|
161
|
+
when 0
|
|
162
|
+
# Handle type 0 - None (null value)
|
|
163
|
+
#
|
|
164
|
+
# @see https://wiki.factorio.com/Property_tree
|
|
165
|
+
nil
|
|
166
|
+
when 1
|
|
167
|
+
read_bool
|
|
168
|
+
when 2
|
|
169
|
+
read_double
|
|
170
|
+
when 3
|
|
171
|
+
read_str_property
|
|
172
|
+
when 4
|
|
173
|
+
read_list
|
|
174
|
+
when 5
|
|
175
|
+
read_dictionary
|
|
176
|
+
when 6
|
|
177
|
+
# Handle type 6 - Signed integer
|
|
178
|
+
#
|
|
179
|
+
# @see https://wiki.factorio.com/Property_tree
|
|
180
|
+
SignedInteger.new(read_long)
|
|
181
|
+
when 7
|
|
182
|
+
# Handle type 7 - Unsigned integer
|
|
183
|
+
#
|
|
184
|
+
# @see https://wiki.factorio.com/Property_tree
|
|
185
|
+
UnsignedInteger.new(read_unsigned_long)
|
|
186
|
+
else
|
|
187
|
+
logger.debug("Unknown property type", type:)
|
|
188
|
+
raise UnknownPropertyType, "Unknown property type: #{type}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if the stream is at EOF
|
|
193
|
+
#
|
|
194
|
+
# @return [Boolean] True if at end of file, false otherwise
|
|
195
|
+
def eof? = @stream.eof?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|