├── README
├── UNLICENSE
└── minekiss
/README:
--------------------------------------------------------------------------------
1 | WARNING: This project has been archived as I stopped playing Minecraft. It has
2 | been a fun, albeit very broken, little project. Obviously you can still fork it
3 | and do whatever you want with the project, being in the public domain.
4 |
5 | minekiss is a shitty Minecraft launcher written in POSIX shell.
6 |
7 | Yes, you heard that right. This should in theory only need a POSIX compliant
8 | shell, some of it utilities (awk, sed, head etc.), curl, jq and Java of course.
9 |
10 | Right now it's still hardcoded to download Linux native libraries, doesn't
11 | support Microsoft accounts yet and there's still quite a lot of stuff to do.
12 | Look for "TODO" comments in the script.
13 |
14 | It's usable though, and I unironically use it to run Minecraft.
15 |
16 | It works with normal FabricMC version definitions. Just specify the data dir as
17 | a destination for its installer, disabling profile creation if you don't want
18 | junk files since they'll be unused.
19 |
20 | I guess this will make more sense when KISS Linux will have decent Java support.
21 |
22 | This software is under the Unlicense, which means that you can do anything you
23 | want with it, no strings attached or attribution required.
24 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/minekiss:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 | #
3 | # minekiss - a shitty Minecraft launcher written entirely* in POSIX shell
4 | # inspired from kiss.
5 | #
6 | # This is free and unencumbered software released into the public domain.
7 | #
8 | # Anyone is free to copy, modify, publish, use, compile, sell, or
9 | # distribute this software, either in source code form or as a compiled
10 | # binary, for any purpose, commercial or non-commercial, and by any
11 | # means.
12 | #
13 | # In jurisdictions that recognize copyright laws, the author or authors
14 | # of this software dedicate any and all copyright interest in the
15 | # software to the public domain. We make this dedication for the benefit
16 | # of the public at large and to the detriment of our heirs and
17 | # successors. We intend this dedication to be an overt act of
18 | # relinquishment in perpetuity of all present and future rights to this
19 | # software under copyright law.
20 | #
21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24 | # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 | # OTHER DEALINGS IN THE SOFTWARE.
28 | #
29 | # For more information, please refer to
30 |
31 | # TODO: Make a version inherit everything from its parent when appropiate
32 | # TODO: Validating -> Checking?
33 | # TODO: More settings (eg. minekiss root, prompts etc)
34 | # TODO: Properly manage missing accounts and invalid tokens
35 | # TODO: Have an idea of what to do with multiple downloading instances
36 | # TODO: Properly parse all library and jvm rules
37 | # TODO: Add some more error handling
38 | # TODO: Properly manage pre-1.6 assets
39 | # TODO: Make the converted formats cleaner (tab separated?)
40 | # TODO: Remove all platform-specific stuff, detect the current OS and set all rules properly.
41 |
42 | # Bruh
43 | export LC_ALL="C"
44 |
45 | previous_stty="$(stty -g)"
46 |
47 | abort() {
48 | stty "$previous_stty"
49 | printf "\n"
50 |
51 | error "Aborted."
52 | }
53 |
54 | cleanup() {
55 | if [ "$MINEKISS_DEBUG" = "" ]
56 | then
57 | is_directory_empty "$MINEKISS_TEMP_DIR" || info "Cleaning up..."
58 |
59 | rm -rf "$MINEKISS_TEMP_DIR"
60 | fi
61 | }
62 |
63 | error() {
64 | # This variable is intended to be parsed as a format string.
65 | # shellcheck disable=2059
66 | printf -- "$error_format" "$1" >&2
67 | exit 1
68 | }
69 |
70 | info() {
71 | # This variable is intended to be parsed as a format string.
72 | # shellcheck disable=2059
73 | printf -- "$info_format" "$1"
74 | }
75 |
76 | warning() {
77 | # This variable is intended to be parsed as a format string.
78 | # shellcheck disable=2059
79 | printf -- "$warning_format" "$1" >&2
80 | }
81 |
82 | prompt() {
83 | # This variable is intended to be parsed as a format string.
84 | # shellcheck disable=2059
85 | printf "$prompt_format" "$1"
86 |
87 | # Not quoting the second argument allows leaving it empty without extra logic.
88 | # shellcheck disable=SC2086
89 | read -r $2
90 | }
91 |
92 | prompt_hidden() {
93 | # We don't want to write the password to the terminal.
94 | stty -echo
95 |
96 | # This variable is intended to be parsed as a format string.
97 | # shellcheck disable=2059
98 | printf "$prompt_format" "$1"
99 |
100 | # Not quoting the second argument allows leaving it empty without extra logic.
101 | # shellcheck disable=SC2086
102 | read -r $2
103 |
104 | stty echo
105 | printf "\n"
106 | }
107 |
108 | is_directory_empty() {
109 | # This implmentation avoids ls and passes shellcheck through globbing.
110 | directory_path="$1"
111 | set -- "$1"/*
112 | test "$*" = "$directory_path/*"
113 | }
114 |
115 | get_relative_resource_path() {
116 | printf "%s\n" "$(printf "%s" "$1" | head -c 2)/$1"
117 | }
118 |
119 | get_resource_url() {
120 | printf "%s\n" "$MINEKISS_RESOURCES_URL/$(get_relative_resource_path "$1")"
121 | }
122 |
123 | get_library_index_from_metadata_file() {
124 | # Format: [path] [sha1sum] [url]
125 | # Apparently we can avoid the whole "exclude" rule as it seems to do pretty
126 | # marginal stuff for now.
127 | jq -r ".libraries[]
128 | | select((has(\"rules\") | not) or .rules[0].action == \"allow\" and .rules[0].os.name != \"osx\")
129 | | .downloads.classifiers.\"natives-linux\" // .downloads.artifact // empty
130 | | \"$MINEKISS_LIBRARIES_DIR/\" + .path + \" \" + .url + \" \" + .sha1" "$1"
131 |
132 | # Maven library support
133 | jq -r '.libraries[] | select(has("name") and has("url")) | .name + " " + .url' "$1" | while read -r package repo_url
134 | do
135 | relative_library_path="$(printf "%s" "$package" | {
136 | IFS=":" read -r namespace name version
137 | printf "%s\n" "$(printf "%s" "$namespace" | tr "." "/")/$name/$version/$name-$version.jar"
138 | })"
139 |
140 | local_library_path="$MINEKISS_LIBRARIES_DIR/$relative_library_path"
141 | remote_library_path="$repo_url/$relative_library_path"
142 | library_sha1="$(cat "$local_library_path.sha1" 2> /dev/null)"
143 |
144 | printf "%s\n" "$local_library_path $remote_library_path $library_sha1"
145 | done
146 | }
147 |
148 | get_unformatted_game_arguments_from_metadata_file() {
149 | jq -r 'if .arguments.game != null
150 | then .arguments.game[] | strings
151 | else .minecraftArguments // empty
152 | end' "$1" | tr "\n" " "
153 | }
154 |
155 | token_refresh() {
156 | curl -s -H "Content-Type: application/json" -d \
157 | "{
158 | \"accessToken\": \"$1\",
159 | \"clientToken\": \"$(cat "$userdata_dir/client_id")\"
160 | }" "$MINEKISS_AUTH_SERVER_URL/refresh" | jq -r "
161 | if has(\"accessToken\")
162 | then .accessToken
163 | else \"\" | halt_error(1)
164 | end"
165 | return
166 | }
167 |
168 | token_is_valid() {
169 | curl -s -f -H "Content-Type: application/json" -d \
170 | "{
171 | \"accessToken\":\"$1\",
172 | \"clientToken\":\"$(cat "$userdata_dir/client_id")\"
173 | }" "$MINEKISS_AUTH_SERVER_URL/validate"
174 | }
175 |
176 | token_invalidate() {
177 | curl -s -f -H "Content-Type: application/json" -d \
178 | "{
179 | \"accessToken\": \"$1\",
180 | \"clientToken\": \"$(cat "$userdata_dir/client_id")\"
181 | }" "$MINEKISS_AUTH_SERVER_URL/invalidate"
182 | }
183 |
184 | request_account_authentication() {
185 | curl -s --fail-with-body -H "Content-Type: application/json" -d @- "$MINEKISS_AUTH_SERVER_URL"/authenticate <<- EOF
186 | {
187 | "agent": {"name": "Minecraft", "version": 1},
188 | "username": "$1",
189 | "password": "$2",
190 | "clientToken": "$(cat "$userdata_dir/client_id")"
191 | }
192 | EOF
193 | }
194 |
195 | # NOTE: This as of now just returns the only select profile (if any) as i'll
196 | # still have to implement Microsoft login properly when it becomes more
197 | # widespread. This will suffice for now.
198 | #
199 | # FORMAT: [ACCESS TOKEN] [PROFILE NAME] [PROFILE UUID]
200 | parse_account_authentication_response() {
201 | jq -r \
202 | 'if has("accessToken")
203 | then
204 | if has("selectedProfile")
205 | then .accessToken + " " + (.selectedProfile | .name + " " + .id )
206 | else "No Minecraft profile found." | halt_error(2)
207 | end
208 | else "Authentication failed: " + .errorMessage + "\n" | halt_error(1)
209 | end'
210 | }
211 |
212 | account_authenticate() {
213 | request_account_authentication "$1" "$2" | parse_account_authentication_response
214 | }
215 |
216 | account_write() {
217 | email_address="$1"
218 | auth_token="$2"
219 | profile_name="$3"
220 | profile_uuid="$4"
221 |
222 | account_dir="$userdata_dir/accounts/$email_address"
223 |
224 | umask 077
225 | mkdir -p "$account_dir/profiles"
226 | printf "%s\n" "$auth_token" > "$account_dir/auth_token"
227 | printf "%s\n" "$profile_name" > "$account_dir/profiles/$profile_uuid"
228 | printf "%s\n" "$profile_uuid" > "$account_dir/profiles/selected_uuid"
229 | printf "%s\n" "$email_address" > "$userdata_dir/selected_account"
230 | }
231 |
232 | account_login() {
233 | if [ ! "$1" ]
234 | then
235 | account="$(cat "$userdata_dir/selected_account")"
236 | else
237 | account="$1"
238 | fi
239 |
240 | if [ "$account" = "" ]
241 | then
242 | prompt "username:" account
243 | printf "%s\n" "$account" > "$userdata_dir/selected_account"
244 | fi
245 |
246 | password="$2"
247 |
248 | while true
249 | do
250 | prompt_hidden "Password for $account:" password
251 | account_authenticate "$account" "$password" | if read -r auth_token profile_name profile_uuid
252 | then account_write "$account" "$auth_token" "$profile_name" "$profile_uuid"
253 | else return 1 # We're inside a subshell due to the pipe
254 | fi && break
255 | done
256 |
257 | info "Authenticated successfully."
258 | }
259 |
260 | account_list() {
261 | is_directory_empty "$userdata_dir/accounts/" || printf "%s\n" "$userdata_dir/accounts/"*/ 2>/dev/null | while read -r account
262 | do
263 | info "$(basename "$account")"
264 | done
265 | }
266 |
267 | account_logout() {
268 | if [ "$1" ]
269 | then
270 | account="$1"
271 | else
272 | account="$(cat "$userdata_dir/selected_account")"
273 | fi
274 |
275 | account_path="$userdata_dir/accounts/$account"
276 |
277 | [ -d "$account_path" ] || error "Account not found."
278 |
279 | token_invalidate "$(cat "$account_path/auth_token")"
280 | rm -rf "$account_path"
281 | }
282 |
283 | account_select() {
284 | if [ "$1" ]
285 | then
286 | printf "%s\n" "$1" > "$userdata_dir/selected_account"
287 | else
288 | selected_account="$(cat "$userdata_dir/selected_account")"
289 |
290 | if [ "$selected_account" ]
291 | then
292 | info "Selected account: \"$selected_account\""
293 | else
294 | info "No default account selected."
295 | fi
296 | fi
297 | }
298 |
299 | parse_version_id() {
300 | version=${1:-latest}
301 |
302 | case "$version" in
303 | latest) version="$(jq -r '.latest.release' "$version_manifest_file")" ;;
304 | latest-snapshot) version="$(jq -r '.latest.snapshot' "$version_manifest_file")";;
305 | esac
306 |
307 | version_dir="$MINEKISS_VERSIONS_DIR/$version"
308 | version_client_file="$version_dir/$version.jar"
309 | version_metadata_file="$version_dir/$version.json"
310 | version_metadata_url="$(jq -r ".versions[] | select(.id == \"$version\").url" "$version_manifest_file")"
311 | }
312 |
313 | convert_version_metadata() {
314 | # This could surely be optimized into one single call and parsed with simpler
315 | # tools but it'll do for now.
316 | version_id="$(jq -r ".id // empty" "$1")"
317 | converted_version_path="$MINEKISS_TEMP_DIR/converted_versions/$version_id"
318 |
319 | if is_directory_empty "$converted_version_path"
320 | then
321 | mkdir -p "$converted_version_path"
322 |
323 | jq -r ".inheritsFrom // empty" "$1" > "$converted_version_path/inherits_from"
324 | jq -r ".downloads.client.url // empty" "$1" > "$converted_version_path/client_url"
325 | jq -r ".downloads.client.sha1 // empty" "$1" > "$converted_version_path/client_sha1"
326 | jq -r ".assetIndex.id // empty" "$1" > "$converted_version_path/assetindex_id"
327 | jq -r ".assetIndex.url // empty" "$1" > "$converted_version_path/assetindex_url"
328 | jq -r ".assetIndex.sha1 // empty" "$1" > "$converted_version_path/assetindex_sha1"
329 | printf "%s\n" "$MINEKISS_ASSETS_DIR/indexes/$(cat "$converted_version_path/assetindex_id").json" > "$converted_version_path/assetindex_path"
330 | fi
331 |
332 | printf "%s\n" "$converted_version_path"
333 | }
334 |
335 | fetch_latest_manifest() {
336 | info "Fetching the latest version manifest..."
337 |
338 | if ! curl -s --create-dirs "$MINEKISS_MANIFEST_URL" -o "$version_manifest_file"
339 | then
340 | if [ ! -f "$version_manifest_file" ]
341 | then
342 | error "Unable to fetch the version manifest file and no cached version found, aborting!"
343 | else
344 | warning "Unable to fetch the latest manifest! Offline mode enabled!"
345 | MINEKISS_OFFLINE=1
346 | fi
347 | fi
348 | }
349 |
350 | download() {
351 | MINEKISS_INTEGRITY=1
352 |
353 | parse_version_id "$1"
354 |
355 | if [ "$version_metadata_url" ]
356 | then
357 | # Metadata
358 | info "Validating $version's metadata..."
359 |
360 | # The SHA1 is for some reason only encoded into the version metadata path.
361 | # URL format: https://launchermeta.mojang.com/v1/packages/SHA/VERSION.json
362 | version_metadata_sha1="$(basename "$(dirname "$version_metadata_url")")"
363 |
364 | if ! printf "%s %s\n" "$version_metadata_sha1" "$version_metadata_file" | sha1sum -c 1>/dev/null 2>&1
365 | then
366 | if [ ! "$MINEKISS_OFFLINE" ]
367 | then
368 | info "Downloading \"$(basename "$version_metadata_file")\"..."
369 | curl -s "$version_metadata_url" --create-dirs -o "$version_metadata_file"
370 | else
371 | if [ -f "$version_metadata_file" ]
372 | then
373 | warning "Integrity check for \"$(basename "$version_metadata_file")\" failed! Unable to download it due to missing internet connection."
374 | MINEKISS_INTEGRITY=0
375 | else
376 | error "Unable to download the version metadata file."
377 | fi
378 | fi
379 | fi
380 | fi
381 |
382 | [ -f "$version_metadata_file" ] || error "Version not found!"
383 |
384 | converted_metadata_path="$(convert_version_metadata "$version_metadata_file")"
385 | version_inherits="$(cat "$converted_metadata_path/inherits_from")"
386 | version_client_url="$(cat "$converted_metadata_path/client_url")"
387 | version_client_sha1="$(cat "$converted_metadata_path/client_sha1")"
388 | asset_index_id="$(cat "$converted_metadata_path/assetindex_id")"
389 | asset_index_url="$(cat "$converted_metadata_path/assetindex_url")"
390 | asset_index_sha1="$(cat "$converted_metadata_path/assetindex_sha1")"
391 | asset_index_file="$(cat "$converted_metadata_path/assetindex_path")"
392 |
393 | if [ "$version_inherits" ]
394 | then
395 | info "Validating parent version $version_inherits..."
396 | (download "$version_inherits")
397 |
398 | # Downloading will alredy have converted its metadata
399 | converted_inherited_version_metadata_path="$MINEKISS_TEMP_DIR/converted_versions/$version_inherits"
400 |
401 | inherited_version_path="$MINEKISS_VERSIONS_DIR/$version_inherits"
402 | inherited_version_asset_index_id="$(cat "$converted_inherited_version_metadata_path/assetindex_id")"
403 | inherited_version_asset_index_file="$(cat "$converted_inherited_version_metadata_path/assetindex_path")"
404 |
405 | mkdir -p "$version_dir/lib"
406 | ln -sf "$inherited_version_path/$version_inherits.jar" "$version_dir/$version.jar"
407 | ln -sf "$inherited_version_path"/lib/* "$version_dir/lib"
408 | fi
409 |
410 | if [ "$version_client_url" ] && [ "$version_client_sha1" ]
411 | then
412 | # Client
413 | info "Validating $version's client..."
414 |
415 | if ! printf "%s %s\n" "$version_client_sha1" "$version_client_file" | sha1sum -c 1>/dev/null 2>&1
416 | then
417 | if [ "$MINEKISS_OFFLINE" = "" ]
418 | then
419 | info "Downloading \"$(basename "$version_client_file")\"..."
420 | curl -s --create-dirs "$version_client_url" -o "$version_client_file"
421 | else
422 | warning "Integrity check for \"$(basename "$asset_index_file")\" failed! Unable to download it due to missing internet connection."
423 | MINEKISS_INTEGRITY=0
424 | fi
425 | fi
426 | fi
427 |
428 | if [ "$asset_index_url" ] && [ "$asset_index_sha1" ] && [ "$asset_index_id" ]
429 | then
430 | # Asset index
431 | info "Validating $version's asset index..."
432 |
433 | if ! printf "%s %s\n" "$asset_index_sha1" "$asset_index_file" | sha1sum -c 1>/dev/null 2>&1
434 | then
435 | if [ "$MINEKISS_OFFLINE" ]
436 | then
437 | warning "Integrity check for \"$(basename "$asset_index_file")\" failed! Unable to download it due to missing internet connection."
438 | MINEKISS_INTEGRITY=0
439 | else
440 | info "Downloading \"$(basename "$asset_index_file")\"..."
441 |
442 | curl -s "$asset_index_url" --create-dirs -o "$asset_index_file"
443 | fi
444 | fi
445 |
446 | if [ -f "$asset_index_file" ]
447 | then
448 | # TODO: Add support for the older asset indexes.
449 | info "Validating $asset_index_id's assets..."
450 |
451 | if [ "$asset_index_id" = "pre-1.6" ]
452 | then
453 | error "Pre 1.6 version detected! Download stopped as legacy asset managing is not supported yet."
454 | fi
455 |
456 | converted_asset_index="$MINEKISS_TEMP_DIR/converted_asset_index"
457 | failed_assets="$MINEKISS_TEMP_DIR/failed_assets"
458 |
459 | # Le epic conversion: [le hash] [le actual path] [le game path] [le asset url]
460 | # This allows us to use the shell's own word splitting as a blazingly fast line
461 | # by line "parser" for our intermediate format. Maybe we could use FIFOs but I
462 | # think that would just make things more complicated for nothing.
463 | jq -r ".virtual as \$virtual
464 | | .objects | to_entries[]
465 | | .value.hash
466 | + \" \"
467 | + (if \$virtual | not
468 | then
469 | \"$MINEKISS_ASSETS_DIR/objects/\" + .value.hash[0:2]+ \"/\" + .value.hash
470 | else
471 | \"$MINEKISS_ASSETS_DIR/virtual/$asset_index_id\" + \"/\" + .key
472 | end)
473 | + \" \" + .key + \" \"
474 | + \"$MINEKISS_RESOURCES_URL/\" + .value.hash[0:2] + \"/\" + .value.hash" "$asset_index_file" > "$converted_asset_index"
475 |
476 | awk '{print $1 " " $2}' "$converted_asset_index" | sha1sum -c 2> /dev/null | grep -i failed | cut -d ":" -f 1 > "$failed_assets"
477 |
478 | # It's easier for now to read with unused variables. I might switch to
479 | # tab-separated files one day.
480 | # shellcheck disable=SC2034
481 | [ "$MINEKISS_OFFLINE" ] && grep -f "$failed_assets" "$converted_asset_index" | while read -r asset_sha1 asset_file_path asset_game_path asset_url
482 | do
483 | warning "Integrity check for \"$$asset_game_path\" failed! Unable to download it due to missing internet connection."
484 | done
485 |
486 | # We set MINEKISS_INTEGRITY only once if there are any broken assets.
487 | if grep -q . "$failed_assets"
488 | then
489 | [ "$MINEKISS_OFFLINE" ] && MINEKISS_INTEGRITY=0
490 |
491 | unique_asset_index="$MINEKISS_TEMP_DIR/unique_asset_index"
492 | # Apparently the asset index might contain some duplicates, so we remove them here.
493 | awk '!seen[$1]++' "$converted_asset_index" > "$unique_asset_index"
494 |
495 | grep -f "$failed_assets" "$unique_asset_index" \
496 | | awk '{print "url " $4 "\n" "output " $2 "\n"}' | curl --create-dirs -K - -s -w "%{url}\n" | while read -r url
497 | do
498 | info "Downloaded \"$(grep -F "$url" "$unique_asset_index" | awk '{print $3}')\"."
499 | done
500 | fi
501 | fi
502 | else
503 | [ -f "$inherited_version_asset_index_file" ] || warning "No asset index for version $version found!"
504 | fi
505 |
506 | # Libraries
507 | info "Validating $version's libraries..."
508 |
509 | converted_library_index="$MINEKISS_TEMP_DIR/converted_library_index"
510 | failed_libs="$MINEKISS_TEMP_DIR/failed_libs"
511 |
512 | get_library_index_from_metadata_file "$version_metadata_file" > "$converted_library_index"
513 |
514 | awk '{print $3 " " $1}' "$converted_library_index" | sha1sum -c 2> /dev/null | grep FAILED | cut -d ":" -f 1 > "$failed_libs"
515 |
516 | # We look for clearly invalid checksums by looking at their length.
517 | awk '{if (length($3) != 40) print $1}' "$converted_library_index" >> "$failed_libs"
518 |
519 | # We set MINEKISS_INTEGRITY only once if there are any broken library files.
520 | if [ "$MINEKISS_OFFLINE" ] && grep -q . "$failed_libs"
521 | then
522 | MINEKISS_INTEGRITY=0
523 | fi
524 |
525 | # TODO: Avoid grep | awk
526 | grep -f "$failed_libs" "$converted_library_index" | awk '{print $1 " " $2}' | while read -r library_file library_url
527 | do
528 | library_file_name="$(basename "$library_file")"
529 |
530 | if [ "$MINEKISS_OFFLINE" ]
531 | then
532 | warning "Integrity check for \"$library_file_name\" failed! Unable to download it due to missing internet connection."
533 | # We already set MINEKISS_INTEGRITY above.
534 | continue
535 | fi
536 |
537 | info "Downloading \"$library_file_name\"..."
538 |
539 | curl -s -f "$library_url" --create-dirs -o "$library_file"
540 | curl -s -f "$library_url.sha1" --create-dirs -o "$library_file.sha1"
541 | done
542 |
543 | info "Validating $version's native libraries..."
544 |
545 | mkdir -p "$version_dir/lib"
546 | grep "native" "$converted_library_index" | while read -r library_file library_sha1 library_url
547 | do
548 | library_file_name="$(basename "$library_file")"
549 | library_directory="$(dirname "$library_file")"
550 |
551 | mkdir -p "$library_directory/native"
552 |
553 | for native_library_file_name in $(zipinfo -1 "$library_file" -x 'META-INF/*' '*.git' '*.sha1' 2> /dev/null)
554 | do
555 | # We fetch the first matching element and work on that, otherwise any other
556 | # similarly named file might get stuck there forever.
557 | checksummed_native_library_file="$(printf "%s " "$library_directory/native/$native_library_file_name".* | cut -d " " -f 1)"
558 | checksummed_native_library_file_name="$(basename "$checksummed_native_library_file")"
559 |
560 | native_library_file="$library_directory/native/$native_library_file_name"
561 | native_library_file_sha1="${checksummed_native_library_file_name##*.}"
562 | native_library_file_current_sha1="$(sha1sum "$checksummed_native_library_file" 2> /dev/null | cut -d ' ' -f 1)"
563 | native_library_link="$version_dir/lib/$native_library_file_name"
564 |
565 | # We steer off a bit from the traditional native library management by
566 | # storing each decompressed library into a folder with its checksum
567 | # appended to its name. We then parse it to get its original checksum
568 | # and compare it.
569 | # Thanks to Dylan Araps for this great idea.
570 | if [ "$native_library_file_current_sha1" != "$native_library_file_sha1" ]
571 | then
572 | rm -f "$checksummed_native_library_file"
573 | info "Extracting \"$native_library_file_name\"..."
574 |
575 | (
576 | cd "$library_directory/native" || exit
577 | unzip -qqo "$library_file" "$native_library_file_name"
578 | )
579 |
580 | native_library_file_sha1="$(sha1sum "$native_library_file" 2> /dev/null | cut -d ' ' -f 1)"
581 | checksummed_native_library_file="$native_library_file.$native_library_file_sha1"
582 | mv "$native_library_file" "$checksummed_native_library_file"
583 | fi
584 |
585 | [ -f "$native_library_link" ] || ln -sf "$checksummed_native_library_file" "$native_library_link"
586 | done
587 | done
588 | }
589 |
590 | start_version() {
591 | parse_version_id "$1"
592 |
593 | # TODO: Maybe decide stuff with the dedicated status page? It'd be a mess to
594 | # support custom URLs though.
595 | if curl -s -f "$MINEKISS_AUTH_SERVER_URL" > /dev/null && [ ! "$MINEKISS_OFFLINE" ]
596 | then
597 | # TODO: Handle missing accounts.
598 |
599 | # Authentication
600 | selected_account="$(cat "$userdata_dir/selected_account")"
601 | selected_account_dir="$userdata_dir/accounts/$selected_account"
602 | selected_uuid="$(cat "$selected_account_dir/profiles/selected_uuid")"
603 | selected_username="$(cat "$selected_account_dir/profiles/$selected_uuid")"
604 | auth_token="$(cat "$selected_account_dir/auth_token")"
605 |
606 | if ! token_is_valid "$auth_token"
607 | then
608 | warning "Invalid auth token: access required."
609 | account_login "$selected_account"
610 | fi
611 | else
612 | # Ugh, there has to be a better way of doing this
613 | selected_account='""'
614 | selected_account_dir='""'
615 | selected_uuid='""'
616 | selected_username='""'
617 |
618 | warning "Unable to connect to the authentication server, offline mode enabled!"
619 | MINEKISS_OFFLINE=1
620 | prompt "Offline username:" selected_username
621 | fi
622 |
623 | download "$version"
624 |
625 | [ "$MINEKISS_INTEGRITY" = "0" ] && prompt "Validation failed, attempt running the game anyways? [Enter|^C]:" answer
626 |
627 | classpath="$(get_library_index_from_metadata_file "$version_metadata_file" | awk '{print $1}' | tr "\n" ":")"
628 | classpath="$classpath$version_client_file"
629 |
630 | version_main_class="$(jq -r ".mainClass" "$version_metadata_file")"
631 |
632 | # todo: find a way to manage stuff like resolution which is behind a rule.
633 | # todo: format all arguments.
634 | game_arguments="$(get_unformatted_game_arguments_from_metadata_file "$version_metadata_file")"
635 |
636 | if [ "$version_inherits" ]
637 | then
638 | classpath="$classpath:$(get_library_index_from_metadata_file "$inherited_version_path/$version_inherits.json" | awk '{print $1}' | tr "\n" ":")"
639 | asset_index_id="${asset_index_id:-$inherited_version_asset_index_id}"
640 | inherited_version_game_arguments="$(get_unformatted_game_arguments_from_metadata_file "$inherited_version_path/$version_inherits.json")"
641 | game_arguments="$inherited_version_game_arguments $game_arguments"
642 | fi
643 |
644 | # I have no idea how ${user_properties} is supposed to work
645 | game_arguments="$(printf "%s\n" "$game_arguments" | sed \
646 | -e "s|\${version_name}|$version|" \
647 | -e "s|\${assets_root}|$MINEKISS_ASSETS_DIR|" \
648 | -e "s|\${assets_index_name}|$asset_index_id|" \
649 | -e "s|\${version_type}|$(jq -r .type "$version_metadata_file")|" \
650 | -e "s|\${game_directory}|$MINEKISS_GAME_DIRECTORY|" \
651 | -e "s|\${user_properties}|{}|" \
652 | -e "s|\${auth_uuid}|$selected_uuid|" \
653 | -e "s|\${auth_access_token}|$auth_token|" \
654 | -e "s|\${auth_player_name}|$selected_username|")"
655 |
656 | # Legacy argument formatting for compatibility.
657 | game_arguments="$(printf "%s\n" "$game_arguments" \
658 | | sed "s|\${auth_session}|$auth_token|")"
659 |
660 | # Virtual assets support
661 | if jq -e .virtual "$MINEKISS_ASSETS_DIR/indexes/$asset_index_id.json" > /dev/null
662 | then
663 | game_arguments="$(printf "%s\n" "$game_arguments" \
664 | | sed "s|\${game_assets}|$MINEKISS_ASSETS_DIR/virtual/$asset_index_id|")"
665 | fi
666 |
667 | if [ "$JAVA_HOME" ]
668 | then
669 | java="$JAVA_HOME/bin/java"
670 | else
671 | java="java"
672 | fi
673 |
674 | # TODO: Read metadata from the instance to choose the right Java version.
675 | # TODO: Format the JVM's arguments.
676 | #
677 | # We need word splitting because the arguments are dynamically specified
678 | # in the version manifest.
679 | # shellcheck disable=SC2086
680 | "$java" -Djava.library.path="$MINEKISS_VERSIONS_DIR/$version/lib" -cp "$classpath" $version_main_class $game_arguments
681 | }
682 |
683 | list_versions() {
684 | # Printing with the format itself allows us to avoid a whole subshell.
685 | #
686 | # info_format is intended to be parsed as a format string.
687 | # shellcheck disable=2059
688 | is_directory_empty "$MINEKISS_VERSIONS_DIR" || printf -- "$info_format" "$MINEKISS_VERSIONS_DIR"/*
689 | }
690 |
691 | # Configuration
692 |
693 | XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
694 |
695 | userdata_dir="$XDG_DATA_HOME/minekiss"
696 |
697 | MINEKISS_MANIFEST_URL="${MINEKISS_MANIFEST_URL:-"https://launchermeta.mojang.com/mc/game/version_manifest.json"}"
698 | MINEKISS_RESOURCES_URL="${MINEKISS_RESOURCES_URL:-"https://resources.download.minecraft.net"}"
699 | MINEKISS_AUTH_SERVER_URL="${MINEKISS_AUTH_SERVER_URL:-"https://authserver.mojang.com"}"
700 | MINEKISS_ASSETS_DIR="${MINEKISS_ASSETS_DIR:-"$userdata_dir/assets"}"
701 | MINEKISS_LIBRARIES_DIR="${MINEKISS_LIBRARIES_DIR:-"$userdata_dir/libraries"}"
702 | MINEKISS_VERSIONS_DIR="${MINEKISS_VERSIONS_DIR:-"$userdata_dir/versions"}"
703 | MINEKISS_GAME_DIRECTORY="${MINEKISS_GAME_DIRECTORY:-"."}"
704 | MINEKISS_TEMP_DIR="${MINEKISS_TEMP_DIR:-"/tmp/minekiss"}/$$"
705 |
706 | version_manifest_file="$userdata_dir/version_manifest.json"
707 |
708 | mkdir -p "$MINEKISS_TEMP_DIR"
709 | mkdir -p "$userdata_dir"
710 |
711 | [ -f "$userdata_dir/client_id" ] || (umask 077; uuidgen > "$userdata_dir/client_id")
712 |
713 | [ -f "$userdata_dir/selected_account" ] || "$userdata_dir/selected_account"
714 |
715 | if [ -t 1 ]
716 | then
717 | bold_modifier="$(tput bold)"
718 | red_color="$(tput setaf 1)"
719 | green_color="$(tput setaf 2)"
720 | yellow_color="$(tput setaf 3)"
721 | reset_color="$(tput sgr0)"
722 |
723 | if [ "$KISS_STYLE" ]
724 | then
725 | error_format="$bold_modifier$yellow_color"ERROR"$reset_color"' %s\n'
726 | warning_format="$bold_modifier$yellow_color"WARNING"$reset_color"' %s\n'
727 | info_format="$bold_modifier$yellow_color->$reset_color"' %s\n'
728 | prompt_format='%s '
729 | else
730 | error_format="$bold_modifier$red_color"XX"$reset_color"' %s\n'
731 | warning_format="$bold_modifier$yellow_color"!!"$reset_color"' %s\n'
732 | info_format="$bold_modifier$green_color->$reset_color"' %s\n'
733 | prompt_format="$bold_modifier$green_color<-$reset_color"' %s '
734 | fi
735 | else
736 | error_format="%s\n"
737 | warning_format="%s\n"
738 | info_format="%s\n"
739 | prompt_format="%s\n"
740 | fi
741 |
742 |
743 |
744 | trap abort INT
745 | trap cleanup EXIT
746 |
747 | case $1 in
748 | download | d) fetch_latest_manifest && download "$2"; exit ;;
749 | start | s) fetch_latest_manifest && start_version "$2"; exit ;;
750 | versions | v) list_versions; exit ;;
751 |
752 | authenticate | a) account_login "$2"; exit ;;
753 | logout | l) account_logout "$2"; exit ;;
754 | select | se) account_select "$2" ; exit ;;
755 | accounts | ac) account_list; exit ;;
756 | esac
757 |
758 | if [ "$1" ]
759 | then
760 | error "Unrecognized command."
761 | else
762 | executable_name="$(basename "$0")"
763 | info "$executable_name [d|s|v] [version]"
764 | info "download Verify and download a Minecraft version into the cache directory"
765 | info "start Start a Minecraft version into the current directory"
766 | info "versions List all currently downloaded versions"
767 | info
768 | info "$executable_name [a|l|se|ac] [account]"
769 | info "authenticate Authenticate or refresh an account"
770 | info "logout Log out off an account"
771 | info "select Select an account as the default"
772 | info "accounts List all available accounts"
773 | fi
774 | exit 0
775 |
--------------------------------------------------------------------------------