nauvisian 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +29 -0
- data/.rubocop_todo.yml +26 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +141 -0
- data/LICENSE.txt +21 -0
- data/README.md +55 -0
- data/Rakefile +15 -0
- data/exe/nvsn +7 -0
- data/lib/nauvisian/api.rb +68 -0
- data/lib/nauvisian/cache/file_system.rb +55 -0
- data/lib/nauvisian/cache.rb +3 -0
- data/lib/nauvisian/cli/commands/mod/disable.rb +32 -0
- data/lib/nauvisian/cli/commands/mod/download.rb +34 -0
- data/lib/nauvisian/cli/commands/mod/enable.rb +32 -0
- data/lib/nauvisian/cli/commands/mod/info.rb +39 -0
- data/lib/nauvisian/cli/commands/mod/installed.rb +33 -0
- data/lib/nauvisian/cli/commands/mod/latest.rb +30 -0
- data/lib/nauvisian/cli/commands/mod/settings/dump.rb +30 -0
- data/lib/nauvisian/cli/commands/mod/versions.rb +31 -0
- data/lib/nauvisian/cli/commands/save/mod/list.rb +35 -0
- data/lib/nauvisian/cli/commands/save/mod/sync.rb +99 -0
- data/lib/nauvisian/cli/download_helper.rb +35 -0
- data/lib/nauvisian/cli/lister.rb +66 -0
- data/lib/nauvisian/cli/message_helper.rb +18 -0
- data/lib/nauvisian/cli.rb +36 -0
- data/lib/nauvisian/credential.rb +23 -0
- data/lib/nauvisian/deserializer.rb +96 -0
- data/lib/nauvisian/downloader.rb +57 -0
- data/lib/nauvisian/error.rb +16 -0
- data/lib/nauvisian/mod/detail.rb +15 -0
- data/lib/nauvisian/mod/release.rb +13 -0
- data/lib/nauvisian/mod.rb +20 -0
- data/lib/nauvisian/mod_list.rb +71 -0
- data/lib/nauvisian/mod_settings.rb +46 -0
- data/lib/nauvisian/platform.rb +78 -0
- data/lib/nauvisian/progress/bar.rb +24 -0
- data/lib/nauvisian/progress/null.rb +19 -0
- data/lib/nauvisian/progress.rb +4 -0
- data/lib/nauvisian/save.rb +89 -0
- data/lib/nauvisian/serializer.rb +111 -0
- data/lib/nauvisian/uri/s3.rb +22 -0
- data/lib/nauvisian/version.rb +6 -0
- data/lib/nauvisian/version24.rb +33 -0
- data/lib/nauvisian/version64.rb +33 -0
- data/lib/nauvisian.rb +33 -0
- data/nauvisian.gemspec +45 -0
- metadata +184 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../../message_helper"
|
4
|
+
|
5
|
+
module Nauvisian
|
6
|
+
module CLI
|
7
|
+
module Commands
|
8
|
+
module Mod
|
9
|
+
module Settings
|
10
|
+
class Dump < Dry::CLI::Command
|
11
|
+
include MessageHelper
|
12
|
+
|
13
|
+
desc "Dump MOD settings"
|
14
|
+
option :mod_directory, desc: "Directory where MODs are installed", required: false, default: Nauvisian.platform.mod_directory.to_s
|
15
|
+
|
16
|
+
def call(**options)
|
17
|
+
mod_directory = Pathname(options[:mod_directory])
|
18
|
+
mod_settings_path = mod_directory / "mod-settings.dat"
|
19
|
+
settings = Nauvisian::ModSettings.load(mod_settings_path)
|
20
|
+
puts settings.to_json
|
21
|
+
rescue => e
|
22
|
+
message(e)
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../message_helper"
|
4
|
+
|
5
|
+
module Nauvisian
|
6
|
+
module CLI
|
7
|
+
module Commands
|
8
|
+
module Mod
|
9
|
+
class Versions < Dry::CLI::Command
|
10
|
+
include MessageHelper
|
11
|
+
|
12
|
+
desc "List available versions of MOD"
|
13
|
+
argument :mod, desc: "Target MOD", required: true
|
14
|
+
|
15
|
+
def call(mod:, **)
|
16
|
+
api = Nauvisian::API.new
|
17
|
+
mod = Nauvisian::Mod[name: mod]
|
18
|
+
releases = api.releases(mod).sort_by(&:released_at)
|
19
|
+
|
20
|
+
releases.each do |release|
|
21
|
+
puts release.version
|
22
|
+
end
|
23
|
+
rescue => e
|
24
|
+
message(e)
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../../lister"
|
4
|
+
require_relative "../../../message_helper"
|
5
|
+
|
6
|
+
module Nauvisian
|
7
|
+
module CLI
|
8
|
+
module Commands
|
9
|
+
module Save
|
10
|
+
module Mod
|
11
|
+
class List < Dry::CLI::Command
|
12
|
+
include MessageHelper
|
13
|
+
|
14
|
+
desc "List MODs used in the given save"
|
15
|
+
argument :file, desc: "Save file of a Factorio game", required: true
|
16
|
+
option :format, default: "plain", values: Nauvisian::CLI::Lister.all, desc: "Output format"
|
17
|
+
|
18
|
+
def call(file:, **options)
|
19
|
+
file_path = Pathname(file)
|
20
|
+
save = Nauvisian::Save.load(file_path)
|
21
|
+
mods = save.mods.sort
|
22
|
+
rows = mods.map {|mod, version| {"Name" => mod.name, "Version" => version} }
|
23
|
+
|
24
|
+
lister = Nauvisian::CLI::Lister.for(options[:format].to_sym).new(%w(Name Version))
|
25
|
+
lister.list(rows)
|
26
|
+
rescue => e
|
27
|
+
message(e)
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../../download_helper"
|
4
|
+
require_relative "../../../message_helper"
|
5
|
+
|
6
|
+
module Nauvisian
|
7
|
+
module CLI
|
8
|
+
module Commands
|
9
|
+
module Save
|
10
|
+
module Mod
|
11
|
+
class Sync < Dry::CLI::Command
|
12
|
+
include DownloadHelper
|
13
|
+
include MessageHelper
|
14
|
+
|
15
|
+
desc "Synchronize MODs and settings with the given save"
|
16
|
+
argument :save_file, desc: "Save file of a Factorio game", required: true
|
17
|
+
|
18
|
+
option :mod_directory, desc: "Directory where MODs are installed", required: false, default: Nauvisian.platform.mod_directory.to_s
|
19
|
+
option :exact, desc: "Use exact version", type: :boolean, default: false
|
20
|
+
option :verbose, desc: "Print extra information", type: :boolean, default: false
|
21
|
+
|
22
|
+
def call(save_file:, **options)
|
23
|
+
save_file_path = Pathname(save_file)
|
24
|
+
save = Nauvisian::Save.load(save_file_path)
|
25
|
+
mods_in_save = save.mods.sort # [[mod, version]]
|
26
|
+
|
27
|
+
options[:mod_directory] = Pathname(options[:mod_directory])
|
28
|
+
existing_mods = ExistingMods.new(**options)
|
29
|
+
|
30
|
+
downloader = Nauvisian::Downloader.new(credential: find_credential, progress: options[:verbose] ? Nauvisian::Progress::Bar : Nauvisian::Progress::Null)
|
31
|
+
|
32
|
+
mods_in_save.each do |mod, version|
|
33
|
+
next if mod.base?
|
34
|
+
|
35
|
+
release = existing_mods.release_to_download(mod, version)
|
36
|
+
next unless release
|
37
|
+
|
38
|
+
downloader.download(release, options[:mod_directory] / release.file_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
list = Nauvisian::ModList.new(mods_in_save.map {|mod, _version| [mod, true] })
|
42
|
+
list.save(options[:mod_directory] / "mod-list.json")
|
43
|
+
|
44
|
+
settings = Nauvisian::ModSettings.load(options[:mod_directory] / "mod-settings.dat")
|
45
|
+
settings["startup"] = save.startup_settings
|
46
|
+
settings.save(options[:mod_directory] / "mod-settings.dat")
|
47
|
+
rescue => e
|
48
|
+
message(e)
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
|
52
|
+
class ExistingMods
|
53
|
+
include DownloadHelper
|
54
|
+
include MessageHelper
|
55
|
+
|
56
|
+
def initialize(mod_directory:, exact:, verbose:)
|
57
|
+
@exact = exact
|
58
|
+
@verbose = verbose
|
59
|
+
|
60
|
+
zips = mod_directory.glob("*.zip")
|
61
|
+
directories = mod_directory.entries.select(&:directory?)
|
62
|
+
# [[mod, [version...]]
|
63
|
+
@mods = [*zips, *directories].filter_map {|path|
|
64
|
+
/(?<name>.*)_(?<version>\d+\.\d+\.\d+)(?:\.zip|$)\z/ =~ path.basename.to_s && [Nauvisian::Mod[name:], Nauvisian::Version24[version]]
|
65
|
+
}.group_by(&:first).transform_values {|v| v.map(&:last) }.to_a
|
66
|
+
end
|
67
|
+
|
68
|
+
def exact? = @exact
|
69
|
+
def verbose? = @verbose
|
70
|
+
|
71
|
+
def release_to_download(mod, version)
|
72
|
+
message "⚙ Checking #{mod.name} #{version} ... "
|
73
|
+
|
74
|
+
# TODO: Take version requirement into consideration
|
75
|
+
case @mods
|
76
|
+
in [*, [^mod, [*, ^version, *]], *]
|
77
|
+
message "✓ Exact version (#{version}) exists, nothing to do"
|
78
|
+
in [*, [^mod, [*versions]], *]
|
79
|
+
if exact?
|
80
|
+
message "📥 some versions are installed but exact version (#{version}) is requested"
|
81
|
+
find_release(mod, version:)
|
82
|
+
elsif versions.all? {|v| v < version }
|
83
|
+
message "📥 all versions are older than the version in the save (#{version}), downloading the latest"
|
84
|
+
find_release(mod)
|
85
|
+
else
|
86
|
+
message "✓ newer version exists, nothing to do"
|
87
|
+
end
|
88
|
+
else
|
89
|
+
message "📥 MOD is not installed"
|
90
|
+
exact? ? find_release(mod, version:) : find_release(mod)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
module CLI
|
5
|
+
module DownloadHelper
|
6
|
+
private def find_credential(**credential_options)
|
7
|
+
case credential_options
|
8
|
+
in {user:, token:}
|
9
|
+
return Nauvisian::Credential[user:, token:]
|
10
|
+
in {user:}
|
11
|
+
puts "User is specified, but token is missing"
|
12
|
+
exit 1
|
13
|
+
in {token:}
|
14
|
+
puts "Token is specified, but user is missing"
|
15
|
+
exit 1
|
16
|
+
else
|
17
|
+
Nauvisian::Credential.from_env
|
18
|
+
end
|
19
|
+
rescue KeyError
|
20
|
+
Nauvisian::Credential.from_player_data_file
|
21
|
+
end
|
22
|
+
|
23
|
+
private def find_release(mod, version: nil)
|
24
|
+
api = Nauvisian::API.new
|
25
|
+
releases = api.releases(mod)
|
26
|
+
if version.nil?
|
27
|
+
releases.max_by(&:released_at)
|
28
|
+
else
|
29
|
+
if_none = -> { puts "Version: #{version} not found"; exit 1 }
|
30
|
+
releases.find(if_none) {|release| release.version == version }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Nauvisian
|
7
|
+
module CLI
|
8
|
+
class Lister
|
9
|
+
@listers = {}
|
10
|
+
|
11
|
+
def self.for(format)
|
12
|
+
@listers.fetch(format)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
@listers.keys.sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.inherited(subclass)
|
20
|
+
demodulized = Nauvisian.inflector.demodulize(subclass.name)
|
21
|
+
underscored = Nauvisian.inflector.underscore(demodulized)
|
22
|
+
@listers[underscored.to_sym] = subclass
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(headers)
|
28
|
+
@headers = headers
|
29
|
+
end
|
30
|
+
|
31
|
+
class CSV < self
|
32
|
+
def list(rows)
|
33
|
+
CSV(headers: @headers, write_headers: true) do |out|
|
34
|
+
rows.each do |row|
|
35
|
+
out << row.values_at(*@headers)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Gfm < self
|
42
|
+
def list(rows)
|
43
|
+
puts @headers.join("|")
|
44
|
+
puts Array.new(@headers.length, "-").join("|")
|
45
|
+
rows.each do |row|
|
46
|
+
puts row.values_at(*@headers).join("|")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Plain < self
|
52
|
+
def list(rows)
|
53
|
+
rows.each do |row|
|
54
|
+
puts row.values_at(*@headers).join(" ")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Json < self
|
60
|
+
def list(rows)
|
61
|
+
puts rows.to_json
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
module CLI
|
5
|
+
module MessageHelper
|
6
|
+
private def message(exception_or_message)
|
7
|
+
case exception_or_message
|
8
|
+
in Exception
|
9
|
+
puts exception_or_message.message
|
10
|
+
in String
|
11
|
+
puts exception_or_message
|
12
|
+
else
|
13
|
+
raise TypeError, "must be Exception or String: %p" % message_or_exception
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/cli"
|
4
|
+
|
5
|
+
require "nauvisian"
|
6
|
+
|
7
|
+
require_relative "cli/commands/mod/disable"
|
8
|
+
require_relative "cli/commands/mod/download"
|
9
|
+
require_relative "cli/commands/mod/enable"
|
10
|
+
require_relative "cli/commands/mod/info"
|
11
|
+
require_relative "cli/commands/mod/installed"
|
12
|
+
require_relative "cli/commands/mod/latest"
|
13
|
+
require_relative "cli/commands/mod/settings/dump"
|
14
|
+
require_relative "cli/commands/mod/versions"
|
15
|
+
require_relative "cli/commands/save/mod/list"
|
16
|
+
require_relative "cli/commands/save/mod/sync"
|
17
|
+
|
18
|
+
module Nauvisian
|
19
|
+
module CLI
|
20
|
+
module Commands
|
21
|
+
extend Dry::CLI::Registry
|
22
|
+
|
23
|
+
register "mod disable", Nauvisian::CLI::Commands::Mod::Disable
|
24
|
+
register "mod enable", Nauvisian::CLI::Commands::Mod::Enable
|
25
|
+
register "mod download", Nauvisian::CLI::Commands::Mod::Download
|
26
|
+
register "mod info", Nauvisian::CLI::Commands::Mod::Info
|
27
|
+
register "mod installed", Nauvisian::CLI::Commands::Mod::Installed
|
28
|
+
register "mod latest", Nauvisian::CLI::Commands::Mod::Latest
|
29
|
+
register "mod versions", Nauvisian::CLI::Commands::Mod::Versions
|
30
|
+
register "mod settings dump", Nauvisian::CLI::Commands::Mod::Settings::Dump
|
31
|
+
|
32
|
+
register "save mod list", Nauvisian::CLI::Commands::Save::Mod::List
|
33
|
+
register "save mod sync", Nauvisian::CLI::Commands::Save::Mod::Sync
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Nauvisian
|
6
|
+
Credential = Data.define(:username, :token)
|
7
|
+
|
8
|
+
class Credential
|
9
|
+
class << self
|
10
|
+
private :new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_env
|
14
|
+
# NOTE: values of ENV are already frozen
|
15
|
+
self[username: ENV.fetch("FACTORIO_SERVICE_USERNAME"), token: ENV.fetch("FACTORIO_SERVICE_TOKEN")]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_player_data_file(player_data_file_path: Nauvisian.platform.user_data_directory / "player-data.json")
|
19
|
+
data = JSON.load_file(player_data_file_path)
|
20
|
+
self[username: data["service-username"].freeze, token: data["service-token"].freeze]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
class Deserializer
|
5
|
+
def initialize(stream)
|
6
|
+
raise ArgumentError, "can't read from the given argument" unless stream.respond_to?(:read)
|
7
|
+
|
8
|
+
@stream = stream
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_bytes(length)
|
12
|
+
raise ArgumentError, "nil length" if length.nil?
|
13
|
+
raise ArgumentError, "negative length" if length.negative?
|
14
|
+
return +"" if length.zero?
|
15
|
+
|
16
|
+
bytes = @stream.read(length)
|
17
|
+
raise EOFError if bytes.nil? || bytes.size < length
|
18
|
+
|
19
|
+
bytes
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_u8 = read_bytes(1).unpack1("C")
|
23
|
+
def read_u16 = read_bytes(2).unpack1("v")
|
24
|
+
def read_u32 = read_bytes(4).unpack1("V")
|
25
|
+
|
26
|
+
# https://wiki.factorio.com/Data_types#Space_Optimized
|
27
|
+
def read_optim_u16
|
28
|
+
byte = read_u8
|
29
|
+
byte == 0xFF ? read_u16 : byte
|
30
|
+
end
|
31
|
+
|
32
|
+
# https://wiki.factorio.com/Data_types#Space_Optimized
|
33
|
+
def read_optim_u32
|
34
|
+
byte = read_u8
|
35
|
+
byte == 0xFF ? read_u32 : byte
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_u16_tuple(length) = Array.new(length) { read_u16 }
|
39
|
+
def read_optim_tuple(bit_size, length) = Array.new(length) { read_optim(bit_size) }
|
40
|
+
|
41
|
+
def read_bool = read_u8 != 0
|
42
|
+
|
43
|
+
def read_str
|
44
|
+
length = read_optim_u32
|
45
|
+
read_bytes(length).force_encoding(Encoding::UTF_8)
|
46
|
+
end
|
47
|
+
|
48
|
+
# https://wiki.factorio.com/Property_tree#String
|
49
|
+
def read_str_property = read_bool ? "" : read_str
|
50
|
+
|
51
|
+
# https://wiki.factorio.com/Property_tree#Number
|
52
|
+
def read_double = read_bytes(8).unpack1("d")
|
53
|
+
|
54
|
+
# Assumed: method arguments are evaluated from left to right but...
|
55
|
+
# https://stackoverflow.com/a/36212870/16014712
|
56
|
+
|
57
|
+
def read_version64 = Nauvisian::Version64[read_u16, read_u16, read_u16, read_u16]
|
58
|
+
|
59
|
+
def read_version24 = Nauvisian::Version24[read_optim_u16, read_optim_u16, read_optim_u16]
|
60
|
+
|
61
|
+
# https://wiki.factorio.com/Property_tree#List
|
62
|
+
def read_list
|
63
|
+
length = read_optim_u32
|
64
|
+
Array(length) { read_property_tree }
|
65
|
+
end
|
66
|
+
|
67
|
+
# https://wiki.factorio.com/Property_tree#Dictionary
|
68
|
+
def read_dictionary
|
69
|
+
length = read_u32
|
70
|
+
length.times.each_with_object({}) do |_i, dict|
|
71
|
+
key = read_str_property
|
72
|
+
dict[key] = read_property_tree
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_property_tree
|
77
|
+
type = read_u8
|
78
|
+
_any_type_flag = read_bool
|
79
|
+
|
80
|
+
case type
|
81
|
+
when 1
|
82
|
+
read_bool
|
83
|
+
when 2
|
84
|
+
read_double
|
85
|
+
when 3
|
86
|
+
read_str_property
|
87
|
+
when 4
|
88
|
+
read_list
|
89
|
+
when 5
|
90
|
+
read_dictionary
|
91
|
+
else
|
92
|
+
raise Nauvisian::UnknownPropertyType, type
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
require "open-uri"
|
5
|
+
|
6
|
+
require "rack/utils"
|
7
|
+
|
8
|
+
module Nauvisian
|
9
|
+
class Downloader
|
10
|
+
def initialize(credential:, progress: Nauvisian::Progress::Null)
|
11
|
+
@credential = credential
|
12
|
+
@progress_class = progress
|
13
|
+
@cache = Nauvisian::Cache::FileSystem.new(name: "download", ttl: Float::INFINITY)
|
14
|
+
end
|
15
|
+
|
16
|
+
def download(release, output_path)
|
17
|
+
with_error_handling(release) do
|
18
|
+
@progress = @progress_class.new(release)
|
19
|
+
url = release.download_url.dup
|
20
|
+
url.query = Rack::Utils.build_nested_query(@credential.to_h)
|
21
|
+
data = @cache.fetch(url) { get(url) }
|
22
|
+
File.binwrite(output_path, data)
|
23
|
+
raise Nauvisian::DigestMismatch unless Digest::SHA1.file(output_path) == release.sha1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private def with_error_handling(release)
|
28
|
+
yield
|
29
|
+
rescue OpenURI::HTTPError => e
|
30
|
+
case e.io.status
|
31
|
+
in ["404", _]
|
32
|
+
raise Nauvisian::ModNotFound, release.mod
|
33
|
+
else
|
34
|
+
raise Nauvisian::Error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private def get(url)
|
39
|
+
url.open(content_length_proc: method(:set_total), progress_proc: method(:update_progress)) do |io|
|
40
|
+
case io.content_type
|
41
|
+
when "application/octet-stream"
|
42
|
+
return io.read
|
43
|
+
else # login requested
|
44
|
+
raise Nauvisian::AuthError, io.status[1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private def set_total(total) # rubocop:disable Naming/AccessorMethodName
|
50
|
+
@progress.total = total if total
|
51
|
+
end
|
52
|
+
|
53
|
+
private def update_progress(progress)
|
54
|
+
@progress.progress = progress
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ModNotFound < Error
|
7
|
+
def initialize(mod) = super "Mod not found: #{mod.name}"
|
8
|
+
end
|
9
|
+
|
10
|
+
class AuthError < Error; end
|
11
|
+
class APIError < Error; end
|
12
|
+
class DigestMismatch < Error; end
|
13
|
+
class UnsupportedPlatform < Error; end
|
14
|
+
class UnsupportedVersion < Error; end
|
15
|
+
class UnknownPropertyType < Error; end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
class Mod
|
5
|
+
Detail = Data.define(:downloads_count, :name, :owner, :summary, :title, :category, :created_at, :description)
|
6
|
+
|
7
|
+
class Detail
|
8
|
+
def url = (URI("https://mods.factorio.com/mod/") + name).freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
private :new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nauvisian
|
4
|
+
Mod = Data.define(:name) do
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
def base?
|
8
|
+
name == "base"
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s = name
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
(base? && (other.base? ? 0 : -1)) || (other.base? ? 1 : name.casecmp(other.name))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require_relative "mod/detail"
|
20
|
+
require_relative "mod/release"
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Nauvisian
|
6
|
+
class ModList
|
7
|
+
DEFAULT_MOD_LIST_PATH = Nauvisian.platform.mod_directory / "mod-list.json"
|
8
|
+
private_constant :DEFAULT_MOD_LIST_PATH
|
9
|
+
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
def self.load(from=DEFAULT_MOD_LIST_PATH)
|
13
|
+
raw_data = JSON.parse(File.read(from), symbolize_names: true)
|
14
|
+
new(raw_data[:mods].to_h {|e| [Mod[name: e[:name]], e[:enabled]] })
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(mods={})
|
18
|
+
@mods = {Nauvisian::Mod[name: "base"] => true}
|
19
|
+
mods.each do |mod, enabled|
|
20
|
+
next if mod.base?
|
21
|
+
|
22
|
+
@mods[mod] = enabled
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def save(to=DEFAULT_MOD_LIST_PATH)
|
27
|
+
File.write(to, JSON.pretty_generate({mods: @mods.map {|mod, enabled| {name: mod.name, enabled:} }}))
|
28
|
+
end
|
29
|
+
|
30
|
+
def each
|
31
|
+
return @mods.to_enum unless block_given?
|
32
|
+
|
33
|
+
@mods.each do |mod, enabled|
|
34
|
+
yield(mod, enabled)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add(mod, enabled: nil)
|
39
|
+
raise ArgumentError, "Can't disable the base mod" if mod.base? && enabled == false
|
40
|
+
|
41
|
+
@mods[mod] = enabled.nil? ? true : enabled
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove(mod)
|
45
|
+
raise ArgumentError, "Can't remove the base mod" if mod.base?
|
46
|
+
|
47
|
+
@mods.delete(mod)
|
48
|
+
end
|
49
|
+
|
50
|
+
def exist?(mod) = @mods.key?(mod)
|
51
|
+
|
52
|
+
def enabled?(mod)
|
53
|
+
raise Nauvisian::ModNotFound, mod unless exist?(mod)
|
54
|
+
|
55
|
+
@mods[mod]
|
56
|
+
end
|
57
|
+
|
58
|
+
def enable(mod)
|
59
|
+
raise Nauvisian::ModNotFound, mod unless exist?(mod)
|
60
|
+
|
61
|
+
@mods[mod] = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def disable(mod)
|
65
|
+
raise ArgumentError, "Can't disalbe the base mod" if mod.base?
|
66
|
+
raise Nauvisian::ModNotFound, mod unless exist?(mod)
|
67
|
+
|
68
|
+
@mods[mod] = false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|