├── 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 | --------------------------------------------------------------------------------