mmi 0.1.3 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 542fe675b42d32312f06e4953aa52b44a2a3e7ff906d25f797aedffb35624576
4
- data.tar.gz: 5bba31fccf041f7745cc66cf2069b6a86baf3fe5a35a3fbba49ad447689dd6a6
3
+ metadata.gz: 54e9d7ed91e3ac72f8c78ba1d9bb550de696e87b134e4bd1f5db393e99e7d7d2
4
+ data.tar.gz: 55df81f836288de4d0166c03488b7a3362992e6fc76304ccba2dc085758b1bb8
5
5
  SHA512:
6
- metadata.gz: b9d50f19d7272034f6786a15349809993659cf5f7298cf968c7f267f0ca599839d1be2e136807e22b1656e34218574321a2265325e81af1ec094034cd873e421
7
- data.tar.gz: 8ce3dfe58ea7884529f7692aaf86eb30f1d31d9813b123e4362f4e61397b679ffe3a718ecef14e7a8851d14ac019155fed7b50cfd54cacf0c15a7de484d52c4b
6
+ metadata.gz: 2c9e1954062c87ea12251b23103fee16b141a9c1f78ca88199c70441649090c16f5bb09481e5d6b158903f6553c201a754608346b12dbc208400fa973eebab73
7
+ data.tar.gz: '0481cffce09c9bf0439c1b739cb53cfeba1779a994c4b0ee203f45c3c17becd0164cc8c4b6c7400da01e26605e4e2065c4eeb141f0c6054fef06722f7bd7def2'
data/README.adoc CHANGED
@@ -13,14 +13,29 @@ The application is currently CLI-only, but should work on Linux, MacOS and Windo
13
13
 
14
14
  toc::[]
15
15
 
16
- = Installation
16
+ == Installation
17
17
 
18
- == Prerequisites
18
+ === Prerequisites
19
19
 
20
- To run the application, Ruby version 2.5+ has to be installed.
21
- For maximum compatibility, use version 3.0+.
20
+ It is recommended to use MMI with Ruby 3.0 or higher.
21
+ While lower versions might work, further development is expected to ignore any specifics that may render those versions unusable.
22
22
 
23
- == Steps
23
+ === Steps
24
+
25
+ ==== Installation through rubygems.org
26
+
27
+ [example]
28
+ ====
29
+ *Disclaimer:* While MMI releases are published on rubygems.org, they might not reflect current development.
30
+ If something does not work, try the manual installation.
31
+ ====
32
+
33
+ [source,bash]
34
+ ----
35
+ $ gem install mmi
36
+ ----
37
+
38
+ ==== Manual installation
24
39
 
25
40
  Clone the repository and navigate into its root directory.
26
41
 
@@ -33,27 +48,32 @@ $ hg clone https://github.com/expeehaa/mmi.git
33
48
  $ cd mmi
34
49
  ----
35
50
 
36
- Install gem dependencies.
51
+ Install gem dependencies using bundler
37
52
 
38
53
  [source,bash]
39
54
  ----
40
55
  $ bundle install
41
56
  ----
42
57
 
43
- If you get an error that the lockfile requires `bundler` version 2.0+, install it first.
58
+ If you get an error saying the lockfile requires `bundler` version 2.0+, install it first.
44
59
 
45
60
  [source,bash]
46
61
  ----
47
62
  $ gem install bundler
48
63
  ----
49
64
 
50
- = Basic usage
65
+ Install the gem.
51
66
 
52
- In the repository root directory, run `exe/mmi`.
67
+ [source,bash]
68
+ ----
69
+ $ rake install
70
+ ----
71
+
72
+ == Basic usage
53
73
 
54
74
  [source,text]
55
75
  ----
56
- Syntax: exe/mmi <config file> [action=install]
76
+ Syntax: mmi <config file> [action=install]
57
77
 
58
78
  where action is one of
59
79
  install - Install the modloader and assets as specified in the config file.
@@ -62,30 +82,48 @@ Syntax: exe/mmi <config file> [action=install]
62
82
  ----
63
83
 
64
84
  The script currently does not support any arguments besides the ones listed above.
65
- Trying to get help by running `exe/mmi --help` or similar will not work.
85
+ Trying to get help by running `mmi --help` or similar will not work.
66
86
 
67
- = Example configs
87
+ == Example configs
68
88
 
69
89
  Example configuration files can be found in `examples/`.
70
- They are completely working and download and install files into `tmp/` so that your `.minecraft` folder will stay unharmed.
90
+ They can be parsed by the MMI version they come with.
91
+ Example configurations download and install files into `tmp/` so that your `.minecraft` folder will stay unharmed.
71
92
 
93
+ The files are intended to provide an overview of the different features of MMI.
94
+ They are not intended to create a working modded Minecraft environment and will therefore not be updated for each new Minecraft version.
72
95
 
73
- = TODOs
96
+ == TODOs
74
97
 
75
98
  * Improve argument parsing.
76
99
  * Write tests.
77
100
  * Add command to interactively create a config file.
78
101
  * Be able to update/change modloader interactively.
79
102
  * Be able to remove assets interactively.
80
- * Think about advising everyone to use another file extension than `.yaml` (e.g. `.mmi`, `.mmiconf`).
103
+ * Think about advising users to use another file extension than `.yaml` (e.g. `.mmi`, `.mmiconf`).
81
104
  * Try to make a Java-executable `.jar` file using `warbler` and `JRuby`.
82
105
 
83
- = Contributing
106
+ == Versioning
107
+
108
+ MMI uses semantic versioning with major, minor and patch versions.
109
+
110
+ Versions are coupled to the configuration file specification.
111
+ This means, within the same major version, every release must be backwards-compatible regarding config file interpreting and MMI releases with a lower minor version must still be able to interpret a config file with a higher minor version (although they are allowed and expected to not support some configurations).
112
+
113
+ An exception to this rule are all releases with the major version `0`.
114
+ There, within the same minor version, backwards compatibility can be expected, but different minor versions will (most likely) not be compatible.
115
+
116
+ == Contributing
117
+
118
+ Contributions are very welcome, as well as feature/bug requests.
119
+
120
+ In case you want to implement a feature, it would be best to create an issue beforehand so that we can discuss it.
121
+ This reduces the chance of a pull request being rejected.
84
122
 
85
- Contributions are very welcome.
86
123
  If you intend to create a pull request, please choose a branch name describing its changes (i.e. not some generic term like `master` or `fix`, but e.g. `fix_typo` or `add_interactive_update_command`).
87
124
 
88
125
  Please be aware that I have a very specific code style and am interested in keeping it in this repository.
89
126
  Some basic rules are specified in the `.editorconfig` file, which your editor should support.
90
- Other code style rules may have to be exactly specified in the future.
91
- When in doubt, take a look at the existing code.
127
+ Additionally, Rubocop has been configured using the existing code and every commit should pass Rubocop’s inspection.
128
+ There most likely are a lot of Rubocop rules that are currently not triggered and therefore not configured.
129
+ In such a case, either try to derive a style from existing code or feel free to ask.
data/exe/mmi CHANGED
@@ -19,25 +19,25 @@ def processor(file)
19
19
  end
20
20
  end
21
21
 
22
- if ARGV.length>0
22
+ if !ARGV.empty?
23
23
  file = ARGV.shift
24
24
  action_param = ARGV.shift
25
25
  action = action_param.nil? ? :install : action_param.to_sym
26
26
 
27
27
  case action
28
- when :install
29
- processor(file).install
30
- when :validate
31
- processor(file)
32
-
33
- puts 'File is valid.'
34
- when :update
35
- Mmi::Interactive.update(processor(file), file)
36
- else
37
- STDERR.puts "Unknown action: #{action_param.inspect}."
38
- Kernel.exit 1
28
+ when :install
29
+ processor(file).install
30
+ when :validate
31
+ processor(file)
32
+
33
+ puts 'File is valid.'
34
+ when :update
35
+ Mmi::Interactive.update(processor(file), file)
36
+ else
37
+ warn "Unknown action: #{action_param.inspect}."
38
+ Kernel.exit 1
39
39
  end
40
40
  else
41
41
  puts 'No file given.'
42
42
  Kernel.exit 1
43
- end
43
+ end
@@ -1,41 +1,54 @@
1
+ require 'mmi/option_attributes'
1
2
  require 'mmi/source/github'
3
+ require 'mmi/source/modrinth'
4
+ require 'mmi/source/url'
2
5
 
3
6
  module Mmi
4
7
  class AssetsProcessor
5
- attr_reader :profile_dir
6
- attr_reader :assets
8
+ include OptionAttributes
7
9
 
8
- def initialize(profile_dir, assets)
9
- @profile_dir = profile_dir
10
-
11
- assets ||= []
10
+ opt_accessor(:profile_dir) { Mmi.minecraft_dir }
11
+ opt_accessor(:items ) { [] }
12
+
13
+ attr_reader :parsed_items
14
+
15
+ def initialize(options)
16
+ @options = options
12
17
 
13
- if assets.is_a?(Array)
14
- @assets = assets.map.with_index do |asset, index|
18
+ parse!
19
+ end
20
+
21
+ def parse!
22
+ if self.items.is_a?(Array)
23
+ @parsed_items = self.items.map.with_index do |asset, index|
15
24
  source = asset['source']
16
25
 
17
26
  if source
18
27
  type = source['type']
19
28
 
20
29
  case type
21
- when 'github'
22
- Source::Github.new(source)
23
- else
24
- raise Mmi::InvalidAttributeError, %Q{Invalid "source.type" in asset #{index.inspect}: #{type.inspect}}
30
+ when 'github'
31
+ Source::Github.new(source)
32
+ when 'modrinth'
33
+ Source::Modrinth.new(source)
34
+ when 'url'
35
+ Source::Url.new(source)
36
+ else
37
+ raise Mmi::InvalidAttributeError, %Q(Invalid "source.type" in asset #{index.inspect}: #{type.inspect})
25
38
  end
26
39
  else
27
- raise Mmi::MissingAttributeError, %Q{Missing "source" in asset #{index.inspect}.}
40
+ raise Mmi::MissingAttributeError, %Q(Missing "source" in asset #{index.inspect}.)
28
41
  end
29
42
  end
30
43
  else
31
- raise Mmi::InvalidAttributeError, %Q{Invalid "assets": expected Array or nothing, got #{self.assets.inspect}.}
44
+ raise Mmi::InvalidAttributeError, %Q(Invalid "assets": expected Array or nothing, got #{self.items.inspect}.)
32
45
  end
33
46
  end
34
47
 
35
48
  def install
36
- assets.each do |asset|
49
+ self.parsed_items.each do |asset|
37
50
  asset.install(self.profile_dir)
38
51
  end
39
52
  end
40
53
  end
41
- end
54
+ end
@@ -0,0 +1,59 @@
1
+ require 'digest'
2
+ require 'fileutils'
3
+ require 'open-uri'
4
+
5
+ module Mmi
6
+ module CachedDownload
7
+ class << self
8
+ def open_cached(path, sha512: nil)
9
+ if File.exist?(path)
10
+ File.open(path).tap do |f|
11
+ if !sha512 || sha512 == Digest::SHA512.hexdigest(f.read)
12
+ f.seek(0)
13
+ f
14
+ else
15
+ nil
16
+ end
17
+ end
18
+ else
19
+ nil
20
+ end
21
+ end
22
+
23
+ def download(uri, sha512: nil)
24
+ URI.parse(uri).open.tap do |stream|
25
+ if sha512
26
+ actual_sha512 = Digest::SHA512.hexdigest(stream.read)
27
+
28
+ if sha512 == actual_sha512
29
+ stream.seek(0)
30
+ else
31
+ Mmi.fail! "Expected download to have SHA512 sum #{expected_hexdigest.inspect} but received #{actual_sha512.inspect}."
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def download_cached(uri, download_path, sha512_uri: nil)
38
+ ensure_cache_dir_exists!
39
+
40
+ expected_hexdigest = URI.parse(sha512_uri).open.read
41
+ cached_file = open_cached(download_path, sha512: expected_hexdigest)
42
+
43
+ if !cached_file
44
+ stream = download(uri, sha512: expected_hexdigest)
45
+
46
+ IO.copy_stream(stream, download_path)
47
+ else
48
+ Mmi.info "Using cached version of #{uri.inspect} from #{download_path.inspect}."
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def ensure_cache_dir_exists!
55
+ FileUtils.mkdir_p(Mmi.cache_dir)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ require 'octokit'
2
+
3
+ module Mmi
4
+ module GithubApi
5
+ class << self
6
+ def client
7
+ @client ||= Octokit::Client.new
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,208 @@
1
+ require 'cli/ui'
2
+
3
+ require 'mmi/github_api'
4
+ require 'mmi/source/github'
5
+
6
+ module Mmi
7
+ module Interactive
8
+ module Assets
9
+ def update_assets
10
+ loop do
11
+ assets = processor.parsed_assets.parsed_items
12
+
13
+ choice = CLI::UI::Prompt.ask('Which asset do you want to change?') do |handler|
14
+ assets.each do |asset|
15
+ handler.option(asset.display_name) do
16
+ asset
17
+ end
18
+ end
19
+
20
+ handler.option('add', &:to_sym)
21
+ handler.option('quit', &:to_sym)
22
+ end
23
+
24
+ case choice
25
+ when :quit
26
+ break
27
+ when :add
28
+ add_asset
29
+ else
30
+ update_asset_version(choice)
31
+ end
32
+ end
33
+ end
34
+
35
+ def add_asset
36
+ source_type = CLI::UI::Prompt.ask('Choose a source type.') do |handler|
37
+ %w[
38
+ github
39
+ modrinth
40
+ url
41
+ ].each do |type|
42
+ handler.option(type, &:to_sym)
43
+ end
44
+
45
+ handler.option('quit', &:to_sym)
46
+ end
47
+
48
+ opts, source =
49
+ case source_type
50
+ when :quit
51
+ return false
52
+ when :github
53
+ options = {
54
+ 'source' => {
55
+ 'type' => 'github',
56
+ 'asset_id' => 0,
57
+ },
58
+ }
59
+
60
+ options['source']['owner' ] = CLI::UI::Prompt.ask('Who is the owner of the source repository?').strip
61
+ options['source']['repo' ] = CLI::UI::Prompt.ask('What is the name of the source repository?').strip
62
+ options['source']['install_dir'] = CLI::UI::Prompt.ask('In which directory should the asset be placed?', default: 'mods').strip
63
+ options['source']['filename' ] = CLI::UI::Prompt.ask('Under which filename should the asset be saved? (leave empty for release asset name)', allow_empty: true).strip.then do |filename|
64
+ filename == '' ? nil : filename
65
+ end
66
+
67
+ options['source'].compact!
68
+
69
+ [options, Mmi::Source::Github.new(options['source'])]
70
+ when :modrinth
71
+ options = {
72
+ 'source' => {
73
+ 'type' => 'modrinth',
74
+ 'version' => '0',
75
+ 'version_file' => '0',
76
+ },
77
+ }
78
+
79
+ options['source']['name' ] = CLI::UI::Prompt.ask('What is the name of the mod in the Modrinth URL?').strip
80
+ options['source']['install_dir'] = CLI::UI::Prompt.ask('In which directory should the asset be placed?', default: 'mods').strip
81
+ options['source']['filename' ] = CLI::UI::Prompt.ask('Under which filename should the asset be saved? (leave empty for release asset name)', allow_empty: true).strip.then do |filename|
82
+ filename == '' ? nil : filename
83
+ end
84
+
85
+ options['source'].compact!
86
+
87
+ [options, Mmi::Source::Modrinth.new(options['source'])]
88
+ when :url
89
+ options = {
90
+ 'source' => {
91
+ 'type' => 'url',
92
+ 'url' => '',
93
+ },
94
+ }
95
+
96
+ options['source']['install_dir'] = CLI::UI::Prompt.ask('In which directory should the asset be placed?', default: 'mods').strip
97
+ options['source']['filename' ] = CLI::UI::Prompt.ask('Under which filename should the asset be saved? (leave empty for release asset name)', allow_empty: true).strip.then do |filename|
98
+ filename == '' ? nil : filename
99
+ end
100
+
101
+ options['source'].compact!
102
+
103
+ [options, Mmi::Source::Url.new(options['source'])]
104
+ end
105
+
106
+ if update_asset_version(source)
107
+ self.processor.parsed_assets.items.push(opts)
108
+ self.processor.parsed_assets.parsed_items.push(source)
109
+
110
+ true
111
+ else
112
+ CLI::UI.puts('Aborting asset addition. No change will be made.', color: CLI::UI::Color::RED)
113
+
114
+ false
115
+ end
116
+ end
117
+
118
+ def update_asset_version(asset)
119
+ case asset
120
+ when Mmi::Source::Github
121
+ github_releases = Mmi::GithubApi.client.releases("#{asset.owner}/#{asset.repo}")
122
+
123
+ github_release = CLI::UI::Prompt.ask('Choose a release.') do |handler|
124
+ github_releases.select do |release|
125
+ release.assets.any?
126
+ end.each do |release|
127
+ handler.option(release.name) do
128
+ release
129
+ end
130
+ end
131
+
132
+ handler.option('quit', &:to_sym)
133
+ end
134
+
135
+ case github_release
136
+ when :quit
137
+ false
138
+ else
139
+ release_asset = CLI::UI::Prompt.ask('Choose an asset.') do |handler|
140
+ github_release.assets.each do |a|
141
+ handler.option(a.name) do
142
+ a
143
+ end
144
+ end
145
+
146
+ handler.option('quit', &:to_sym)
147
+ end
148
+
149
+ case release_asset
150
+ when :quit
151
+ false
152
+ else
153
+ asset.release = nil
154
+ asset.file = nil
155
+
156
+ asset.asset_id = release_asset.id
157
+
158
+ true
159
+ end
160
+ end
161
+ when Mmi::Source::Modrinth
162
+ mod_version = CLI::UI::Prompt.ask('Choose a version.') do |handler|
163
+ asset.cached_mod_versions.select do |version|
164
+ version['files'].any?
165
+ end.each do |version|
166
+ handler.option("#{version['name']} (for game versions #{version['game_versions'].join('/')})") do
167
+ version
168
+ end
169
+ end
170
+
171
+ handler.option('quit', &:to_sym)
172
+ end
173
+
174
+ case mod_version
175
+ when :quit
176
+ false
177
+ else
178
+ version_file = CLI::UI::Prompt.ask('Choose a version file.') do |handler|
179
+ mod_version['files'].each do |file|
180
+ handler.option(file['filename']) do
181
+ file
182
+ end
183
+ end
184
+
185
+ handler.option('quit', &:to_sym)
186
+ end
187
+
188
+ case version_file
189
+ when :quit
190
+ false
191
+ else
192
+ asset.version = mod_version['name']
193
+ asset.version_file = version_file['filename']
194
+
195
+ true
196
+ end
197
+ end
198
+ when Mmi::Source::Url
199
+ asset.url = CLI::UI::Prompt.ask('What is the URL of the file that should be downloaded?', default: asset.url).strip
200
+ else
201
+ CLI::UI.puts('This asset cannot be updated.', color: CLI::UI::Color::RED)
202
+
203
+ false
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,139 @@
1
+ require 'cli/ui'
2
+
3
+ require 'mmi/modloader/fabric'
4
+ require 'mmi/modloader/none'
5
+
6
+ module Mmi
7
+ module Interactive
8
+ module Modloader
9
+ def update_modloader
10
+ choice = CLI::UI::Prompt.ask('What do you want to do?') do |handler|
11
+ [
12
+ (["Update current modloader #{self.processor.modloader['name']}", :update_current] unless self.processor.parsed_modloader.is_a?(Mmi::Modloader::None)),
13
+ ['quit', :quit ],
14
+ ].each do |name, result|
15
+ handler.option(name) do
16
+ result
17
+ end
18
+ end
19
+ end
20
+
21
+ case choice
22
+ when :update_current
23
+ update_modloader_current
24
+ when :quit
25
+ # Pass.
26
+ else
27
+ raise 'Consider yourself lucky, you found a bug.'
28
+ end
29
+ end
30
+
31
+ def update_modloader_current
32
+ ml = self.processor.parsed_modloader
33
+
34
+ case ml
35
+ when Mmi::Modloader::None
36
+ CLI::UI.puts('There is currently no modloader set, please choose one first.', color: CLI::UI::Color::RED)
37
+ when Mmi::Modloader::Fabric
38
+ loop do
39
+ choice = CLI::UI::Prompt.ask('What do you want to update?') do |handler|
40
+ [
41
+ ['Installer version', :version ],
42
+ ['Minecraft version', :mc_version ],
43
+ ['Download Minecraft?', :download_mc ],
44
+ ['Install directory', :install_dir ],
45
+ ['Install type', :install_type],
46
+ ['quit', :quit ],
47
+ ].each do |name, result|
48
+ handler.option(name) do
49
+ result
50
+ end
51
+ end
52
+ end
53
+
54
+ case choice
55
+ when :version
56
+ choice2 = CLI::UI::Prompt.ask('Choose a fabric installer version') do |handler|
57
+ ml.available_versions.sort.reverse.each do |v|
58
+ handler.option(v, &:itself)
59
+ end
60
+
61
+ handler.option('quit', &:to_sym)
62
+ end
63
+
64
+ case choice2
65
+ when :quit
66
+ # Pass.
67
+ else
68
+ ml.version = choice2
69
+ end
70
+ when :mc_version
71
+ choice2 =
72
+ begin
73
+ CLI::UI::Prompt.ask('Which Minecraft version do you need?')
74
+ rescue Interrupt
75
+ :quit
76
+ end
77
+
78
+ case choice2
79
+ when :quit
80
+ # Pass.
81
+ else
82
+ ml.mcversion = choice2
83
+ end
84
+ when :download_mc
85
+ choice2 =
86
+ begin
87
+ CLI::UI::Prompt.confirm('Download minecraft when installing the modloader? (Ctrl+C for no change)', default: ml.download_mc)
88
+ rescue Interrupt
89
+ :quit
90
+ end
91
+
92
+ case choice2
93
+ when :quit
94
+ # Pass.
95
+ else
96
+ ml.download_mc = choice2
97
+ end
98
+ when :install_dir
99
+ choice2 =
100
+ begin
101
+ CLI::UI::Prompt.ask('In which directory should the modloader be installed? ', is_file: true)
102
+ rescue Interrupt
103
+ :quit
104
+ end
105
+
106
+ case choice2
107
+ when :quit
108
+ # Pass.
109
+ else
110
+ ml.install_dir = choice2.strip.empty? ? nil : choice2
111
+ end
112
+ when :install_type
113
+ choice2 = CLI::UI::Prompt.ask('What type of installation do you want?') do |handler|
114
+ ml.allowed_install_types.each do |type|
115
+ handler.option(type, &:itself)
116
+ end
117
+
118
+ handler.option('quit', &:to_sym)
119
+ end
120
+
121
+ case choice2
122
+ when :quit
123
+ # Pass.
124
+ else
125
+ ml.install_type = choice2
126
+ end
127
+ when :quit
128
+ break
129
+ else
130
+ raise 'Consider yourself lucky, you found a bug.'
131
+ end
132
+ end
133
+ else
134
+ CLI::UI.puts("Modloader #{self.processor.modloader['name']} is not supported.", color: CLI::UI::Color::RED)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end