mmi 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 542fe675b42d32312f06e4953aa52b44a2a3e7ff906d25f797aedffb35624576
4
- data.tar.gz: 5bba31fccf041f7745cc66cf2069b6a86baf3fe5a35a3fbba49ad447689dd6a6
3
+ metadata.gz: b06278de132f8f2d3579987335931bdafe56470aef8641b7157b78922d12bd99
4
+ data.tar.gz: 046a5c00f73f2ae5151411572978ef1af74662136d72a82ad30e2570e12c0523
5
5
  SHA512:
6
- metadata.gz: b9d50f19d7272034f6786a15349809993659cf5f7298cf968c7f267f0ca599839d1be2e136807e22b1656e34218574321a2265325e81af1ec094034cd873e421
7
- data.tar.gz: 8ce3dfe58ea7884529f7692aaf86eb30f1d31d9813b123e4362f4e61397b679ffe3a718ecef14e7a8851d14ac019155fed7b50cfd54cacf0c15a7de484d52c4b
6
+ metadata.gz: 252ff8dcf6dfc7ece06c47300903e4bd3cf50ede5c7b82b8bcaf8ef7bdbd63b35f72a9a02d553c050e2a81fd17941523d7814435eb3c34e55f0071e6a073d1c5
7
+ data.tar.gz: 57127da3df65211b4b4b1afa2899f404f95da5a9c14ba4067da24749ae5261ec6d0003d60e66d7f9eafcf3ad7b577b19b89600ef174e3d35276e64c79c5eb960
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