nauvisian 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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