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,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Nauvisian
6
+ class ModSettings
7
+ DEFAULT_MOD_SETTINGS_PATH = Nauvisian.platform.mod_directory / "mod-settings.dat"
8
+ private_constant :DEFAULT_MOD_SETTINGS_PATH
9
+
10
+ def self.load(from=DEFAULT_MOD_SETTINGS_PATH)
11
+ File.open(from, "rb") do |stream|
12
+ des = Nauvisian::Deserializer.new(stream)
13
+ version = des.read_version64
14
+ _unused = des.read_bool
15
+ properties = des.read_property_tree
16
+ new(version:, properties:)
17
+ end
18
+ end
19
+
20
+ def initialize(version:, properties:)
21
+ @version = version
22
+ @properties = properties
23
+ end
24
+
25
+ def save(to=DEFAULT_MOD_SETTINGS_PATH)
26
+ File.open(to, "wb") do |stream|
27
+ ser = Nauvisian::Serializer.new(stream)
28
+ ser.write_version64(@version)
29
+ ser.write_bool(false)
30
+ ser.write_property_tree(@properties)
31
+ end
32
+ end
33
+
34
+ def [](key)
35
+ @properties[key]
36
+ end
37
+
38
+ def []=(key, properties)
39
+ @properties[key] = properties
40
+ end
41
+
42
+ def to_json(*args)
43
+ JSON.generate(@properties.merge(version: @version), *args)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "rbconfig"
5
+
6
+ module Nauvisian
7
+ class Platform
8
+ def self.platform
9
+ host_os = RbConfig::CONFIG["host_os"]
10
+ case host_os
11
+ when /\bdarwin\d*\b/
12
+ MacOS.new
13
+ when /\blinux\z/
14
+ Linux.new
15
+ when /\b(?:cygwin|mswin|mingw|bccwin|wince|emx)\b/
16
+ Windows.new
17
+ else
18
+ raise UnsupportedPlatform, host_os
19
+ end
20
+ end
21
+
22
+ def mod_directory = user_data_directory / "mods"
23
+
24
+ def save_directory = user_data_directory / "saves"
25
+
26
+ def script_output_directory = user_data_directory / "script-output"
27
+
28
+ # Returns the directory which holds user data
29
+ def user_data_directory
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def application_directory
34
+ APPLICATON_DIRECTORIES.find(&:directory?)
35
+ end
36
+
37
+ def home_directory
38
+ Pathname("~").expand_path.freeze
39
+ end
40
+
41
+ class MacOS < self
42
+ def user_data_directory
43
+ Pathname("~/Library/Application Support/Factorio").expand_path.freeze
44
+ end
45
+
46
+ APPLICATON_DIRECTORIES = [
47
+ Pathname("~/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents").freeze,
48
+ Pathname("/Applications/factorio.app/Contents").freeze
49
+ ].freeze
50
+ private_constant :APPLICATON_DIRECTORIES
51
+ end
52
+
53
+ class Linux < self
54
+ def user_data_directory
55
+ Pathname("~/.factorio").expand_path.freeze
56
+ end
57
+
58
+ def application_directory
59
+ Pathname("~/.factorio").expand_path.freeze
60
+ end
61
+ end
62
+
63
+ class Windows < self
64
+ def user_data_directory
65
+ (Pathname(ENV.fetch("APPDATA")).expand_path / "Factorio").freeze
66
+ end
67
+
68
+ def home_directory
69
+ Pathname(ENV.fetch("USERPROFILE")).expand_path.freeze
70
+ end
71
+
72
+ APPLICATON_DIRECTORIES = [].freeze
73
+ APPLICATON_DIRECTORIES << Pathname("#{ENV.fetch("PROGRAMFILES(x86)")}\\Steam\\steamapps\\common\\Factorio").freeze if ENV.key?("PROGRAMFILES(x86)")
74
+ APPLICATON_DIRECTORIES << Pathname("#{ENV.fetch("PROGRAMFILES")}\\Factorio").freeze if ENV.key?("PROGRAMFILES")
75
+ private_constant :APPLICATON_DIRECTORIES
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-progressbar"
4
+
5
+ module Nauvisian
6
+ module Progress
7
+ class Bar
8
+ FORMAT = "%t|%B|%J%%|"
9
+ private_constant :FORMAT
10
+
11
+ def initialize(release)
12
+ @progress_bar = ProgressBar.create(title: "⚙ %s" % release.file_name, format: FORMAT)
13
+ end
14
+
15
+ def progress=(progress)
16
+ @progress_bar.progress = progress
17
+ end
18
+
19
+ def total=(total)
20
+ @progress_bar.total = total
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ module Progress
5
+ class Null
6
+ def initialize(_release)
7
+ # do nothing
8
+ end
9
+
10
+ def progress=(_progress)
11
+ # do nothing
12
+ end
13
+
14
+ def total=(_total)
15
+ # do nothing
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "progress/bar"
4
+ require_relative "progress/null"
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ require "zip"
6
+
7
+ module Nauvisian
8
+ Save = Data.define(:version, :mods, :startup_settings)
9
+
10
+ class Save
11
+ LEVEL_FILE_NAMES = %w(level.dat0 level-init.dat).freeze
12
+ private_constant :LEVEL_FILE_NAMES
13
+
14
+ LEVEL_FILE_NAMES_GLOB = File.join("*", "level{.dat0,-init.dat}")
15
+ private_constant :LEVEL_FILE_NAMES_GLOB
16
+
17
+ def self.load(zip_path)
18
+ stream = stream(zip_path)
19
+ des = Nauvisian::Deserializer.new(stream)
20
+
21
+ new(**populate(des))
22
+ end
23
+
24
+ class << self
25
+ private :new
26
+
27
+ private def stream(zip_path)
28
+ Zip::File.open(zip_path) do |zip_file|
29
+ candidate_entries = zip_file.glob(LEVEL_FILE_NAMES_GLOB)
30
+ LEVEL_FILE_NAMES.each do |file_name|
31
+ candidate_entries.each do |entry|
32
+ next unless File.basename(entry.name) == file_name
33
+
34
+ stream = entry.get_input_stream
35
+ # ZLIB Compressed Data Format Specification version 3.3
36
+ # 2.2 Data Format https://www.rfc-editor.org/rfc/rfc1950#section-2.2
37
+ cmf = stream.read(1).unpack1("C")
38
+ stream.rewind
39
+ # 32K window, deflate
40
+ return cmf == 0x78 ? StringIO.new(Zlib.inflate(stream.read)) : stream # level.dat0 : level-init.dat
41
+ end
42
+ end
43
+ raise Errno::ENOENT, "level.dat0 or level-init.dat not found"
44
+ end
45
+ end
46
+
47
+ private def populate(des)
48
+ version = des.read_version64
49
+ raise Nauvisian::UnsupportedVersion if version < Nauvisian::Version64[1, 0, 0, 0]
50
+
51
+ des.read_u8 # skip a byte
52
+
53
+ # Some values are out of concern
54
+ _campaign = des.read_str
55
+ _level_name = des.read_str
56
+ _base_mod = des.read_str
57
+ _difficulty = des.read_u8
58
+ _finished = des.read_bool
59
+ _player_won = des.read_bool
60
+ _next_level = des.read_str
61
+ _can_continue = des.read_bool
62
+ _finished_but_continuing = des.read_bool
63
+ _saving_replay = des.read_bool
64
+ _allow_non_admin_debug_options = des.read_bool
65
+ _loaded_from = des.read_version24
66
+ _loaded_from_build = des.read_u16
67
+ _allowed_commands = des.read_u8
68
+
69
+ mods = read_mods(des)
70
+
71
+ _unknown_4_bytes = des.read_bytes(4) # example: fPK\t (0x66 0x50 0x4B 0x09)
72
+ startup_settings = des.read_property_tree
73
+
74
+ {version:, mods:, startup_settings:}
75
+ end
76
+
77
+ private def read_mod(des)
78
+ mod = Nauvisian::Mod[name: des.read_str.freeze]
79
+ version = des.read_version24
80
+ _crc = des.read_u32.freeze
81
+ [mod, version]
82
+ end
83
+
84
+ private def read_mods(des) = Array.new(des.read_optim_u32) { read_mod(des) }.to_h.freeze
85
+ end
86
+ end
87
+ end
88
+
89
+ require_relative "deserializer"
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ class Serializer
5
+ def initialize(stream)
6
+ raise ArgumentError, "can't read from the given argument" unless stream.respond_to?(:write)
7
+
8
+ @stream = stream
9
+ end
10
+
11
+ def write_bytes(data)
12
+ raise ArgumentError if data.nil?
13
+ return if data.empty?
14
+
15
+ @stream.write(data)
16
+ end
17
+
18
+ def write_u8(uint8) = write_bytes([uint8].pack("C"))
19
+ def write_u16(uint16) = write_bytes([uint16].pack("v"))
20
+ def write_u32(uint32) = write_bytes([uint32].pack("V"))
21
+
22
+ # https://wiki.factorio.com/Data_types#Space_Optimized
23
+ def write_optim_u16(uint16)
24
+ if uint16 < 0xFF
25
+ write_u8(uint16 & 0xFF)
26
+ else
27
+ write_u8(0xFF)
28
+ write_u16(uint16)
29
+ end
30
+ end
31
+
32
+ # https://wiki.factorio.com/Data_types#Space_Optimized
33
+ def write_optim_u32(uint32)
34
+ if uint32 < 0xFF
35
+ write_u8(uint32 & 0xFF)
36
+ else
37
+ write_u8(0xFF)
38
+ write_u32(uint32)
39
+ end
40
+ end
41
+
42
+ # def read_u16_tuple(length) = Array.new(length) { read_u16 }
43
+ # def read_optim_tuple(bit_size, length) = Array.new(length) { read_optim(bit_size) }
44
+
45
+ def write_bool(bool) = write_u8(bool ? 0x01 : 0x00)
46
+
47
+ def write_str(str)
48
+ write_optim_u32(str.length)
49
+ write_bytes(str.b)
50
+ end
51
+
52
+ # https://wiki.factorio.com/Property_tree#String
53
+ def write_str_property(str)
54
+ if str.empty?
55
+ write_bool(true)
56
+ else
57
+ write_bool(false)
58
+ write_str(str)
59
+ end
60
+ end
61
+
62
+ # https://wiki.factorio.com/Property_tree#Number
63
+ def write_double(dbl) = write_bytes([dbl].pack("d"))
64
+
65
+ def write_version64(v64) = v64.to_a.each {|u16| write_u16(u16) }
66
+
67
+ def write_version24(v24) = v24.to_a.each {|u16| write_optim_u16(u16) }
68
+
69
+ # https://wiki.factorio.com/Property_tree#List
70
+ def write_list(list)
71
+ write_optim_u32(list.size)
72
+ list.each {|e| write_property_tree(e) }
73
+ end
74
+
75
+ # https://wiki.factorio.com/Property_tree#Dictionary
76
+ def write_dictionary(dict)
77
+ write_u32(dict.size)
78
+ dict.each do |(key, value)|
79
+ write_str_property(key)
80
+ write_property_tree(value)
81
+ end
82
+ end
83
+
84
+ def write_property_tree(obj)
85
+ case obj
86
+ in true | false => bool
87
+ write_u8(1)
88
+ write_bool(false)
89
+ write_bool(bool)
90
+ in Float => dbl
91
+ write_u8(2)
92
+ write_bool(false)
93
+ write_double(dbl)
94
+ in String => str
95
+ write_u8(3)
96
+ write_bool(false)
97
+ write_str_property(str)
98
+ in Array => list
99
+ write_u8(4)
100
+ write_bool(false)
101
+ write_list(list)
102
+ in Hash => dict
103
+ write_u8(5)
104
+ write_bool(false)
105
+ write_dictionary(dict)
106
+ else
107
+ raise Nauvisian::UnknownPropertyType, obj.class
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Nauvisian
6
+ module URI
7
+ class S3 < ::URI::Generic
8
+ DEFAULT_PORT = nil
9
+ private_constant :DEFAULT_PORT
10
+
11
+ def key = path.delete_prefix("/").freeze
12
+
13
+ def key=(key)
14
+ self.path = "/#{key}"
15
+ end
16
+
17
+ alias bucket host
18
+ end
19
+ end
20
+ end
21
+
22
+ URI.register_scheme "S3", Nauvisian::URI::S3
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ VERSION = "0.1.0"
5
+ public_constant :VERSION
6
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ class Version24
5
+ include Comparable
6
+
7
+ UINT8_MAX = (2**8) - 1
8
+ private_constant :UINT8_MAX
9
+
10
+ def initialize(*args)
11
+ case args
12
+ in [String] if /\A(\d+)\.(\d+)\.(\d+)\z/ =~ args[0]
13
+ @version = [Integer($1), Integer($2), Integer($3)]
14
+ in [Integer, Integer, Integer] if args.all? {|e| e.is_a?(Numeric) && e.integer? && e.between?(0, UINT8_MAX) }
15
+ @version = args
16
+ else
17
+ raise ArgumentError, "Expect version string or 3-tuple: %p" % [args]
18
+ end
19
+ @version.freeze
20
+ freeze
21
+ end
22
+
23
+ class << self
24
+ alias [] new
25
+ end
26
+
27
+ protected attr_reader :version
28
+
29
+ def to_s = "%d.%d.%d" % @version
30
+ def to_a = @version.dup.freeze
31
+ def <=>(other) = @version <=> other.version
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nauvisian
4
+ class Version64
5
+ include Comparable
6
+
7
+ UINT16_MAX = (2**16) - 1
8
+ private_constant :UINT16_MAX
9
+
10
+ def initialize(*args)
11
+ case args
12
+ in [String] if /\A(\d+)\.(\d+)\.(\d+)(?:-(\d+))?\z/ =~ args[0]
13
+ @version = [Integer($1), Integer($2), Integer($3), $4.nil? ? 0 : Integer($4)]
14
+ in [Integer, Integer, Integer, Integer] if args.all? {|e| e.is_a?(Numeric) && e.integer? && e.between?(0, UINT16_MAX) }
15
+ @version = args
16
+ else
17
+ raise ArgumentError, "Expect version string or 4-tuple: %p" % [args]
18
+ end
19
+ @version.freeze
20
+ freeze
21
+ end
22
+
23
+ class << self
24
+ alias [] new
25
+ end
26
+
27
+ protected attr_reader :version
28
+
29
+ def to_s = "%d.%d.%d-%d" % @version
30
+ def to_a = @version.dup.freeze
31
+ def <=>(other) = @version <=> other.version
32
+ end
33
+ end
data/lib/nauvisian.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ require_relative "nauvisian/error"
6
+
7
+ require_relative "nauvisian/api"
8
+ require_relative "nauvisian/credential"
9
+ require_relative "nauvisian/deserializer"
10
+ require_relative "nauvisian/downloader"
11
+ require_relative "nauvisian/mod"
12
+ require_relative "nauvisian/platform"
13
+ require_relative "nauvisian/progress"
14
+ require_relative "nauvisian/save"
15
+ require_relative "nauvisian/serializer"
16
+ require_relative "nauvisian/version"
17
+ require_relative "nauvisian/version24"
18
+ require_relative "nauvisian/version64"
19
+
20
+ module Nauvisian
21
+ def self.inflector
22
+ @inflector ||= Dry::Inflector.new
23
+ end
24
+
25
+ def self.platform
26
+ @platform ||= Nauvisian::Platform.platform
27
+ end
28
+ end
29
+
30
+ # some class must be loaded after definine Nauvisian::Platform.platform
31
+ require_relative "nauvisian/cache"
32
+ require_relative "nauvisian/mod_list"
33
+ require_relative "nauvisian/mod_settings"
data/nauvisian.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/nauvisian/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "nauvisian"
7
+ spec.version = Nauvisian::VERSION
8
+ spec.authors = ["OZAWA Sakuro"]
9
+ spec.email = ["10973+sakuro@users.noreply.github.com"]
10
+
11
+ spec.summary = "A library for managing Factorio MODs"
12
+ spec.description = <<~DESC
13
+ Nauvisian is a ruby library for the management of Factorio MODs.
14
+
15
+ It comes with a CLI.
16
+ DESC
17
+ spec.homepage = "https://github.com/sakuro/nauvisian"
18
+ spec.license = "MIT"
19
+ spec.required_ruby_version = ">= 3.2.0"
20
+
21
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+
23
+ spec.metadata["homepage_uri"] = spec.homepage
24
+ spec.metadata["source_code_uri"] = "https://github.com/sakuro/nauvisian.git"
25
+ spec.metadata["changelog_uri"] = "https://github.com/sakuro/nauvisian/blob/main/CHANGELOG.md"
26
+ spec.metadata["rubygems_mfa_required"] = "true"
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(__dir__) do
31
+ %x[git ls-files -z].split("\x0").reject do |f|
32
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
33
+ end
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{\Aexe/}) {|f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency "aws-sdk-s3", "~> 1.0"
40
+ spec.add_dependency "dry-cli", "~> 1.0"
41
+ spec.add_dependency "dry-inflector", "~> 1.0"
42
+ spec.add_dependency "rack", "~> 3.0"
43
+ spec.add_dependency "ruby-progressbar", "~> 1.11"
44
+ spec.add_dependency "rubyzip", "~> 2.3"
45
+ end