nauvisian 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +29 -0
  4. data/.rubocop_todo.yml +26 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +26 -0
  7. data/Gemfile.lock +141 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +55 -0
  10. data/Rakefile +15 -0
  11. data/exe/nvsn +7 -0
  12. data/lib/nauvisian/api.rb +68 -0
  13. data/lib/nauvisian/cache/file_system.rb +55 -0
  14. data/lib/nauvisian/cache.rb +3 -0
  15. data/lib/nauvisian/cli/commands/mod/disable.rb +32 -0
  16. data/lib/nauvisian/cli/commands/mod/download.rb +34 -0
  17. data/lib/nauvisian/cli/commands/mod/enable.rb +32 -0
  18. data/lib/nauvisian/cli/commands/mod/info.rb +39 -0
  19. data/lib/nauvisian/cli/commands/mod/installed.rb +33 -0
  20. data/lib/nauvisian/cli/commands/mod/latest.rb +30 -0
  21. data/lib/nauvisian/cli/commands/mod/settings/dump.rb +30 -0
  22. data/lib/nauvisian/cli/commands/mod/versions.rb +31 -0
  23. data/lib/nauvisian/cli/commands/save/mod/list.rb +35 -0
  24. data/lib/nauvisian/cli/commands/save/mod/sync.rb +99 -0
  25. data/lib/nauvisian/cli/download_helper.rb +35 -0
  26. data/lib/nauvisian/cli/lister.rb +66 -0
  27. data/lib/nauvisian/cli/message_helper.rb +18 -0
  28. data/lib/nauvisian/cli.rb +36 -0
  29. data/lib/nauvisian/credential.rb +23 -0
  30. data/lib/nauvisian/deserializer.rb +96 -0
  31. data/lib/nauvisian/downloader.rb +57 -0
  32. data/lib/nauvisian/error.rb +16 -0
  33. data/lib/nauvisian/mod/detail.rb +15 -0
  34. data/lib/nauvisian/mod/release.rb +13 -0
  35. data/lib/nauvisian/mod.rb +20 -0
  36. data/lib/nauvisian/mod_list.rb +71 -0
  37. data/lib/nauvisian/mod_settings.rb +46 -0
  38. data/lib/nauvisian/platform.rb +78 -0
  39. data/lib/nauvisian/progress/bar.rb +24 -0
  40. data/lib/nauvisian/progress/null.rb +19 -0
  41. data/lib/nauvisian/progress.rb +4 -0
  42. data/lib/nauvisian/save.rb +89 -0
  43. data/lib/nauvisian/serializer.rb +111 -0
  44. data/lib/nauvisian/uri/s3.rb +22 -0
  45. data/lib/nauvisian/version.rb +6 -0
  46. data/lib/nauvisian/version24.rb +33 -0
  47. data/lib/nauvisian/version64.rb +33 -0
  48. data/lib/nauvisian.rb +33 -0
  49. data/nauvisian.gemspec +45 -0
  50. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ class Mod
5
+ Release = Data.define(:mod, :download_url, :file_name, :released_at, :version, :sha1)
6
+
7
+ class Release
8
+ class << self
9
+ private :new
10
+ end
11
+ end
12
+ end
13
+ 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