aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <yuuta@yuuta.moe>2021-12-27 00:31:39 -0800
committerTrumeet <yuuta@yuuta.moe>2021-12-27 00:31:39 -0800
commit5d004c12d05bf6aba067c194acc7f8ab5f0af62f (patch)
tree3462925ca504cd13cf35dcb62c83515056d66bb6
downloadminecraft-pacman-5d004c12d05bf6aba067c194acc7f8ab5f0af62f.tar
minecraft-pacman-5d004c12d05bf6aba067c194acc7f8ab5f0af62f.tar.gz
minecraft-pacman-5d004c12d05bf6aba067c194acc7f8ab5f0af62f.tar.bz2
minecraft-pacman-5d004c12d05bf6aba067c194acc7f8ab5f0af62f.zip
First Commit
-rw-r--r--.gitignore10
-rw-r--r--README.md49
-rw-r--r--assets/.gitignore41
-rw-r--r--assets/PKGBUILD35
-rw-r--r--gen.c636
-rwxr-xr-xlauncher34
-rw-r--r--mc/.gitignore6
-rw-r--r--mc/PKGBUILD52
l---------mc/launcher.gen1
-rwxr-xr-xswitch6
10 files changed, 870 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..836d5cf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+/launcher.gen
+PKGBUILD.gen
+src/
+pkg/
+*.zst
+*.log
+*.jar
+gen
+*.xml
+logs/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c1c6825
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# Arch packages for Minecraft
+
+This repository contains build scripts (PKGBUILDs and their generation scripts) to use Pacman as a Minecraft version manager.
+
+Packages are separated into `minecraft-xxx` and `minecraft-assets-xxx`, because the later is huge and takes a long to package, and is also optional for some users.
+
+I made a generator in C to fetch version JSONs and convert them into partial PKGBUILDs and launcher.gen files.
+
+This repository does not contain any launchers. Instead, it exposes some variables from the manifest to launchers through launcher.gen files. Launchers should source them to run the game (see `launcher` as an example).
+
+## makepkg
+
+1. Compile the generator and generate `PKGBUILD.gen` and `launcher.gen`: `./switch 1.18.1`
+
+2. Bulid and install the Minecraft package: `cd mc; makepkg`
+
+3. (Optional) Build and install the assets package: `cd assets; makepkg`
+
+Whenever you need to build a different Minecraft version, do the steps again (changing the arguments of `./switch` will cause it to generate new build files).
+
+## Tested versions
+
+1.18
+
+1.17
+
+1.13
+
+Basic testing for 1.14, 1.15 and 1.16 (no assets)
+
+## TODO
+
+Fabric support
+
+Forge support
+
+## Known issues
+
+Assets folders cannot be shared across versions (i.e. you must have a dedicated assets folder for each asset version).
+
+Library compatibility issues with version <= 1.12: Inconsistency detected by ld.so: dl-lookup.c: 105: check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!
+
+## License
+
+Thanks to my friends' help.
+
+Minecraft itself is a proprietary software. Its files subject to its EULA and are not included in this repository.
+
+The build scripts are licensed under GPL v2.
diff --git a/assets/.gitignore b/assets/.gitignore
new file mode 100644
index 0000000..c476ae8
--- /dev/null
+++ b/assets/.gitignore
@@ -0,0 +1,41 @@
+src/
+pkg/
+*.zst
+*.log
+*.json
+a*
+b*
+c*
+d*
+e*
+f*
+g*
+h*
+i*
+j*
+k*
+l*
+m*
+n*
+o*
+p*
+q*
+r*
+s*
+t*
+u*
+v*
+w*
+x*
+y*
+z*
+0*
+1*
+2*
+3*
+4*
+5*
+6*
+7*
+8*
+9*
diff --git a/assets/PKGBUILD b/assets/PKGBUILD
new file mode 100644
index 0000000..ed58a15
--- /dev/null
+++ b/assets/PKGBUILD
@@ -0,0 +1,35 @@
+# Maintainer: YuutaW <YuutaW@ymc.moe>
+. ../PKGBUILD.gen
+pkgname=minecraft-assets-${_ASSET_ID}
+pkgver=${_ASSET_ID//-/_}
+pkgrel=1
+epoch=
+pkgdesc="Minecraft assets ${_ASSET_ID}"
+arch=(any)
+url="https://minecraft.net/"
+license=('custom')
+install=
+source=($_MC_ASSET_URL $_MC_ASSET_INDEX_URL)
+sha1sums=($_MC_ASSET_SHA1 $_MC_ASSET_INDEX_SHA1)
+noextract=($_MC_ASSET $_MC_ASSET_INDEX)
+
+_dir=/usr/share/minecraft/assets/$pkgver
+
+package() {
+ _dir="$pkgdir/$_dir"
+ mkdir -p $_dir/
+ cd $_dir/
+ mkdir objects
+ cd objects
+ for asset in `echo $_MC_ASSET`; do
+ local _ha="${asset:0:2}"
+ mkdir -p ./$_ha
+ cp $srcdir/$asset ./$_ha/
+ done
+ cd ..
+ mkdir indexes
+ cd indexes
+ for index in `echo $_MC_ASSET_INDEX`; do
+ cp $srcdir/$index ./
+ done
+}
diff --git a/gen.c b/gen.c
new file mode 100644
index 0000000..48f4141
--- /dev/null
+++ b/gen.c
@@ -0,0 +1,636 @@
+/**
+ * Author: YuutaW <YuutaW@YMC.MOE>
+ *
+ * CFLAGS=-std=c99 -I/usr/local/include/ -D_POSIX_C_SOURCE=200809L
+ * LDFLAGS=-ljson-c -lcurl
+ *
+ * License: GPLv2
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <err.h>
+#include <sysexits.h>
+#include <curl/curl.h>
+#include <stdlib.h>
+#include <json-c/json_object.h>
+#include <json-c/json_tokener.h>
+#include <json-c/json.h>
+
+enum source_type {
+ jar,
+ client,
+ extract_jar,
+ log4j,
+ asset,
+ asset_index
+};
+
+struct source {
+ struct source *next;
+ char *id;
+ char *hash;
+ char *url;
+ enum source_type type;
+};
+
+static struct source *source_fist = NULL;
+
+static char *out_pkgbuild = "PKGBUILD.gen";
+static FILE *pkgbuild = NULL;
+static char *out_launcher = "launcher.gen";
+static FILE *launcher = NULL;
+static char *version = NULL;
+static CURL *curl = NULL;
+static json_object *json = NULL;
+static json_tokener *tok = NULL;
+static char *all_version_manifest_url = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json";
+static char *version_manifest_url = NULL;
+static char *assets_url = NULL;
+
+static void cleanup(void) {
+ if (pkgbuild) fclose(pkgbuild);
+ if (launcher) fclose(launcher);
+ if (curl) {
+ curl_easy_cleanup(curl);
+ curl_global_cleanup();
+ }
+ if (tok) json_tokener_free(tok);
+ if (json) {
+ json_object_put(json);
+ json = NULL;
+ }
+ struct source *s = source_fist;
+ while (s) {
+ struct source *s1 = s->next;
+ free(s->hash);
+ free(s->id);
+ free(s->url);
+ free(s);
+ s = s1;
+ }
+}
+
+static void get(const char *url) {
+ /* The url may belong to the json. Use it first. */
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ if (json) {
+ json_object_put(json);
+ json = NULL;
+ }
+ CURLcode res = curl_easy_perform(curl);
+ if (res == CURLE_WRITE_ERROR) {
+ cleanup();
+ exit(1);
+ }
+ if (res) {
+ cleanup();
+ errx(res, "Cannot GET %s: %s",
+ url,
+ curl_easy_strerror(res));
+ }
+ /* No worries for IDE condition warning: it will be set in the callback function. */
+ if (!json) {
+ cleanup();
+ errx(1, "Cannot parse response JSON: "
+ "The stream ends without a full JSON.");
+ }
+}
+
+static FILE *try_fopen(const char *path) {
+ FILE *f = fopen(path, "w+");
+ if (!f) {
+ cleanup();
+ err(errno, "Cannot open %s", path);
+ }
+ return f;
+}
+
+static size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp) {
+ json = json_tokener_parse_ex(tok,
+ buffer,
+ (int) (size * nmemb));
+ enum json_tokener_error e;
+ if (!json && (e = json_tokener_get_error(tok)) != json_tokener_continue) {
+ /* Not sure if it is safe to call curl_easy_cleanup() within the callback.
+ * Thus, delegate exit to the caller.
+ * We will lose the return code here. */
+ fprintf(stderr, "Cannot parse response JSON: %s",
+ json_tokener_error_desc(e));
+ /* Not necessary, but just to satisfy me. */
+ json_tokener_reset(tok);
+ return 0;
+ }
+ return size * nmemb;
+}
+
+static void src(const char *hash, const char *url, const enum source_type type) {
+ char *id = strrchr(url, '/');
+ if (!id) {
+ cleanup();
+ errx(1, "URL %s is not valid.", url);
+ }
+ /* Remove leading / */
+ id ++;
+ struct source *s = malloc(sizeof(struct source));
+ if (!s) {
+ cleanup();
+ err(errno, "Cannot allocate memory. Requested %lu bytes.",
+ sizeof(struct source));
+ }
+ s->next = source_fist;
+ s->id = calloc(strlen(id) + 1, sizeof(char));
+ if (!s->id) {
+ free(s);
+ cleanup();
+ err(errno, "Cannot allocate memory");
+ }
+ strcpy(s->id, id);
+ s->hash = calloc(strlen(hash) + 1, sizeof(char));
+ if (!s->hash) {
+ free(s->id);
+ free(s);
+ cleanup();
+ err(errno, "Cannot allocate memory");
+ }
+ strcpy(s->hash, hash);
+ s->url = calloc(strlen(url) + 1, sizeof(char));
+ if (!s->url) {
+ free(s->id);
+ free(s->hash);
+ free(s);
+ cleanup();
+ err(errno, "Cannot allocate memory");
+ }
+ strcpy(s->url, url);
+
+ s->type = type;
+
+ source_fist = s;
+}
+
+static char classpath_set = 0;
+static char libraries_set = 0;
+
+static void append(FILE *stream, const char *key, const char *value) {
+ if (!classpath_set &&
+ !strcmp("JVM_ARGS", key) &&
+ !strcmp("-cp", value)) {
+ classpath_set = 1;
+ }
+ if (!libraries_set &&
+ !strcmp("JVM_ARGS", key) &&
+ !strncmp("-Djava.library.path", value, 19)) {
+ libraries_set = 1;
+ }
+ fprintf(stream, "%s=\"$%s %s\"\n",
+ key,
+ key,
+ value);
+}
+
+static void set(FILE *stream, const char *key, const char *value) {
+ fprintf(stream, "%s=\"%s\"\n",
+ key,
+ value);
+}
+
+static void parse_artifact(json_object *artifact, const enum source_type type) {
+ json_object *obj;
+ if (!json_object_object_get_ex(artifact, "sha1", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: library doesn't have sha1.");
+ }
+ const char *sha1 = json_object_get_string(obj);
+ if (!json_object_object_get_ex(artifact, "url", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: library doesn't have url.");
+ }
+ const char *url = json_object_get_string(obj);
+ src(sha1, url, type);
+}
+
+static char check_rules(json_object *rules) {
+ char allow = 0;
+ unsigned int rules_len = json_object_array_length(rules);
+ for (unsigned int j = 0; j < rules_len; j ++) {
+ const json_object *rule = json_object_array_get_idx(rules, j);
+ json_object *obj2;
+ if (json_object_object_get_ex(rule, "os", &obj2)) {
+ json_object *obj3;
+ if (json_object_object_get_ex(obj2, "name", &obj3)) {
+ /* Definitely not Unix, bro? */
+ continue;
+ }
+ if (json_object_object_get_ex(obj2, "arch", &obj3) &&
+ !strcmp("x86", json_object_get_string(obj3))) {
+ allow = 2;
+ break;
+ }
+ continue;
+ }
+ if (!json_object_object_get_ex(rule, "action", &obj2)) {
+ cleanup();
+ errx(1, "Rule doesn't have an action.");
+ }
+ /* Although the value seems to be always allow,
+ * a double check seems satisfying. */
+ if (!strcmp("allow", json_object_get_string(obj2))) {
+ allow = 1;
+ break;
+ }
+ }
+ return allow;
+}
+
+static const char *fetch_version_manifest(void) {
+ get(all_version_manifest_url);
+ json_object *obj;
+ if (!version) {
+ if (!json_object_object_get_ex(json, "latest", &obj)) {
+ cleanup();
+ errx(1, "Invalid version_manifest_v2.json: No latest object.");
+ }
+ if (!json_object_object_get_ex(obj, "release", &obj)) {
+ cleanup();
+ errx(1, "Invalid version_manifest_v2.json: No release object.");
+ }
+ version = (char *) json_object_get_string(obj);
+ }
+ if (!json_object_object_get_ex(json, "versions", &obj)) {
+ cleanup();
+ errx(1, "Invalid version_manifest_v2.json: No versions[] object.");
+ }
+ unsigned int len = json_object_array_length(obj);
+ for (unsigned int i = 0; i < len; i ++) {
+ const json_object *item = json_object_array_get_idx(obj, i);
+ json_object *obj1;
+ if (!json_object_object_get_ex(item, "id", &obj1)) {
+ cleanup();
+ errx(1, "Version doesn't have an id object.");
+ }
+ if (strcmp(version, json_object_get_string(obj1)) != 0) continue;
+ if (!json_object_object_get_ex(item, "url", &obj1)) {
+ cleanup();
+ errx(1, "Invalid version_manifest_v2.json: no URL.");
+ }
+ return json_object_get_string(obj1);
+ }
+ cleanup();
+ errx(1, "Version %s is not found.", version);
+}
+
+static void parse_arguments(struct json_object *obj) {
+ json_object *args;
+ if (!json_object_object_get_ex(obj, "game", &args)) {
+ cleanup();
+ errx(1, "Invalid version.json: No game[] object.");
+ }
+ unsigned int args_len = json_object_array_length(args);
+ for (unsigned int i = 0; i < args_len; i ++) {
+ json_object *item = json_object_array_get_idx(args, i);
+ if (json_object_is_type(item, json_type_string)) {
+ append(launcher, "MC_ARGS", json_object_get_string(item));
+ continue;
+ }
+ if (!json_object_is_type(item, json_type_object)) {
+ cleanup();
+ errx(1, "Unknown Argument type: it must be a string or object.");
+ }
+ /* Currently, we don't support --demo, --width and --height. */
+ json_object *val;
+ if (!json_object_object_get_ex(item, "value", &val)) {
+ cleanup();
+ errx(1, "Argument doesn't have a value object.");
+ }
+ if (json_object_is_type(val, json_type_string)) {
+ fprintf(stderr, "Skipped unsupported game argument: %s\n",
+ json_object_get_string(val));
+ } else if (json_object_is_type(val, json_type_array)) {
+ fprintf(stderr, "Skipped unsupported game argument: ");
+ unsigned int len = json_object_array_length(val);
+ for (unsigned int j = 0; j < len; j ++) {
+ fprintf(stderr, "%s ",
+ json_object_get_string(
+ json_object_array_get_idx(val, j)
+ ));
+ }
+ fprintf(stderr, "\n");
+ }
+ }
+ if (!json_object_object_get_ex(obj, "jvm", &args)) {
+ cleanup();
+ errx(1, "Invalid version.json: No jvm[] object.");
+ }
+ args_len = json_object_array_length(args);
+ for (unsigned int i = 0; i < args_len; i ++) {
+ json_object *item = json_object_array_get_idx(args, i);
+ if (json_object_is_type(item, json_type_string)) {
+ append(launcher, "JVM_ARGS", json_object_get_string(item));
+ continue;
+ }
+ if (!json_object_is_type(item, json_type_object)) {
+ cleanup();
+ errx(1, "Unknown JVM Argument type: it must be a string or object.");
+ }
+ json_object *value;
+ unsigned int value_len = 1;
+ if (!json_object_object_get_ex(item, "value", &value)) {
+ cleanup();
+ errx(1, "JVM Argument doesn't have a value object.");
+ }
+ if (json_object_is_type(value, json_type_array)) {
+ value_len = json_object_array_length(value);
+ }
+ json_object *rules;
+ if (json_object_object_get_ex(item, "rules", &rules)) {
+ const char rule = check_rules(rules);
+ if (!rule) continue;
+ if (rule == 2) {
+ if (json_object_is_type(value, json_type_string)) {
+ append(launcher, "JVM_ARGS_X86", json_object_get_string(value));
+ } else {
+ for (unsigned int j = 0; j < value_len; j ++) {
+ append(launcher, "JVM_ARGS_X86",
+ json_object_get_string(
+ json_object_array_get_idx(value, j)
+ ));
+ }
+ }
+ } else {
+ if (json_object_is_type(value, json_type_string)) {
+ append(launcher, "JVM_ARGS", json_object_get_string(value));
+ } else {
+ for (unsigned int j = 0; j < value_len; j ++) {
+ append(launcher, "JVM_ARGS",
+ json_object_get_string(
+ json_object_array_get_idx(value, j)
+ ));
+ }
+ }
+ }
+ }
+ }
+}
+
+static void fetch_version(const char *url) {
+ /* URL is no longer valid. */
+ get(url);
+ json_object *obj;
+ if (!json_object_object_get_ex(json, "mainClass", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No mainClass object.");
+ }
+ set(launcher, "MAIN_CLASS", json_object_get_string(obj));
+ if (!json_object_object_get_ex(json, "id", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No id object.");
+ }
+ set(launcher, "ID", json_object_get_string(obj));
+ set(pkgbuild, "_MC_ID", json_object_get_string(obj));
+ if (json_object_object_get_ex(json, "javaVersion", &obj)) {
+ if (!json_object_object_get_ex(obj, "majorVersion", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No majorVersion object.");
+ }
+ fprintf(pkgbuild, "_JAVA_VERSION=%d\n",
+ json_object_get_int(obj));
+ }
+ if (!json_object_object_get_ex(json, "libraries", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No libraries[] object.");
+ }
+ unsigned int libs_len = json_object_array_length(obj);
+ for (unsigned int i = 0; i < libs_len; i ++) {
+ const json_object *item = json_object_array_get_idx(obj, i);
+ json_object *obj1;
+ if (json_object_object_get_ex(item, "rules", &obj1)) {
+ if (!check_rules(obj1)) continue;
+ }
+ if (!json_object_object_get_ex(item, "downloads", &obj1)) {
+ cleanup();
+ errx(1, "Library doesn't have downloads object.");
+ }
+ json_object *artifact;
+ json_object *natives;
+ json_object *classifiers;
+ /* Prefer natives in case they both exist.
+ * It seems that if they both exist, there must be a standalone artifact before it.
+ */
+ if (json_object_object_get_ex(item, "natives", &natives) &&
+ json_object_object_get_ex(obj1, "classifiers", &classifiers)) {
+ if (!json_object_object_get_ex(natives, "linux", &obj1))
+ continue;
+ if (!json_object_object_get_ex(classifiers,
+ json_object_get_string(obj1),
+ &obj1))
+ continue;
+ parse_artifact(obj1, extract_jar);
+ } else if (json_object_object_get_ex(obj1, "artifact", &artifact)) {
+ parse_artifact(artifact,
+ json_object_object_get_ex(item, "extract", &obj1) ? extract_jar
+ : jar);
+ } else {
+ cleanup();
+ errx(1, "Library doesn't have an artifact or natives and classifiers object.");
+ }
+ }
+ if (!json_object_object_get_ex(json, "downloads", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No downloads object.");
+ }
+ if (!json_object_object_get_ex(obj, "client", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No client object.");
+ }
+ parse_artifact(obj, client);
+
+ if (!json_object_object_get_ex(json, "assetIndex", &obj)) {
+ cleanup();
+ errx(1, "Invalid version.json: No assetIndex object.");
+ }
+ json_object *obj1;
+ if (!json_object_object_get_ex(obj, "url", &obj1)) {
+ cleanup();
+ errx(1, "Invalid version.json: assetIndex does not have the url object.");
+ }
+ assets_url = (char *) json_object_get_string(obj1);
+ if (!json_object_object_get_ex(obj, "sha1", &obj1)) {
+ cleanup();
+ errx(1, "Invalid version.json: assetIndex does not have the sha1 object.");
+ }
+ src(json_object_get_string(obj1), assets_url, asset_index);
+ if (!json_object_object_get_ex(obj, "id", &obj1)) {
+ cleanup();
+ errx(1, "Invalid version.json: assetIndex does not have the id object.");
+ }
+ set(pkgbuild, "_ASSET_ID", json_object_get_string(obj1));
+ set(launcher, "ASSET_ID", json_object_get_string(obj1));
+ set(launcher, "assets_index_name", json_object_get_string(obj1));
+
+ if (json_object_object_get_ex(json, "arguments", &obj)) {
+ parse_arguments(obj);
+ } else if (json_object_object_get_ex(json, "minecraftArguments", &obj)) {
+ append(launcher, "MC_ARGS", json_object_get_string(obj));
+ } else {
+ cleanup();
+ errx(1, "Invalid version.json: No arguments or minecraftArguments object.");
+ }
+ if (json_object_object_get_ex(json, "logging", &obj)) {
+ if (json_object_object_get_ex(obj, "client", &obj)) {
+ json_object *file;
+ json_object *arg;
+ if (!json_object_object_get_ex(obj, "file", &file)) {
+ cleanup();
+ errx(1, "Logging doesn't have the file object.");
+ }
+ if (!json_object_object_get_ex(obj, "argument", &arg)) {
+ cleanup();
+ errx(1, "Logging doesn't have the argument object.");
+ }
+ json_object *id;
+ if (!json_object_object_get_ex(file, "id", &id)) {
+ cleanup();
+ errx(1, "Logging file doesn't have the id object.");
+ }
+ set(pkgbuild, "_LOG4J_FILE", json_object_get_string(id));
+ set(launcher, "LOG4J_FILE", json_object_get_string(id));
+ parse_artifact(file, log4j);
+ /* Hope Mojang won't change this. */
+ if (strcmp("-Dlog4j.configurationFile=${path}",
+ json_object_get_string(arg)) != 0) {
+ fprintf(stderr,
+ "Unsupported Log4J Configuration: %s",
+ (char *) json_object_get_string(arg));
+ } else {
+ append(launcher, "JVM_ARGS", "-Dlog4j.configurationFile=LOG4J_XML_PATH");
+ }
+ }
+ }
+}
+
+static void fetch_assets(void) {
+ get(assets_url);
+ json_object *obj;
+ if (!json_object_object_get_ex(json, "objects", &obj)) {
+ cleanup();
+ errx(1, "Invalid assets JSON: no objects object.");
+ }
+ char url[85];
+ json_object_object_foreach(obj, key, val) {
+ json_object *hash;
+ if (!json_object_object_get_ex(val, "hash", &hash)) {
+ cleanup();
+ errx(1, "Invalid assets JSON: no hash object.");
+ }
+ const char *hash_str = json_object_get_string(hash);
+ if (strlen(hash_str) != 40) {
+ cleanup();
+ errx(1, "Invalid length of hash.");
+ }
+ sprintf(url, "https://resources.download.minecraft.net/%c%c/%s",
+ hash_str[0],
+ hash_str[1],
+ hash_str);
+ src(hash_str, url, asset);
+ }
+}
+
+static void out(const char *tag, enum source_type type) {
+ char tag_fin[64];
+ sprintf(tag_fin, "_MC_%s=\"", tag);
+ struct source *s = source_fist;
+ fprintf(pkgbuild, "%s", tag_fin);
+ while (s) {
+ if (s->type == type) fprintf(pkgbuild, "%s\n", s->id);
+ s = s->next;
+ }
+ fprintf(pkgbuild, "\"\n");
+
+ sprintf(tag_fin, "_MC_%s_SHA1=\"", tag);
+ s = source_fist;
+ fprintf(pkgbuild, "%s", tag_fin);
+ while (s) {
+ if (s->type == type) fprintf(pkgbuild, "%s\n", s->hash);
+ s = s->next;
+ }
+ fprintf(pkgbuild, "\"\n");
+
+ sprintf(tag_fin, "_MC_%s_URL=\"", tag);
+ s = source_fist;
+ fprintf(pkgbuild, "%s", tag_fin);
+ while (s) {
+ if (s->type == type) fprintf(pkgbuild, "%s\n", s->url);
+ s = s->next;
+ }
+ fprintf(pkgbuild, "\"\n");
+}
+
+int main(int argc, char **argv) {
+ int opt;
+ while ((opt = getopt(argc, argv, "o:O:v:m:M:")) != -1) {
+ switch (opt) {
+ case 'o':
+ out_pkgbuild = optarg;
+ break;
+ case 'O':
+ out_launcher = optarg;
+ break;
+ case 'v':
+ version = optarg;
+ break;
+ case 'm':
+ version_manifest_url = optarg;
+ break;
+ case 'M':
+ all_version_manifest_url = optarg;
+ break;
+ default:
+ errx(EX_USAGE, "-o %s -O %s -v %s",
+ out_pkgbuild,
+ out_launcher,
+ version);
+ }
+ }
+ pkgbuild = try_fopen(out_pkgbuild);
+ launcher = try_fopen(out_launcher);
+ CURLcode res = curl_global_init(CURL_GLOBAL_SSL);
+ if (res) {
+ cleanup();
+ errx(res, "Cannot initialize libcurl global: %s",
+ curl_easy_strerror(res));
+ }
+ if (!(curl = curl_easy_init())) {
+ curl_global_cleanup();
+ cleanup();
+ errx(1, "Cannot initialize libcurl.");
+ }
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
+ tok = json_tokener_new();
+ if (!tok) {
+ cleanup();
+ errx(1, "Cannot initialize JSON tok.");
+ }
+ fetch_version(version_manifest_url == NULL ? fetch_version_manifest() : version_manifest_url);
+ if (assets_url) fetch_assets();
+
+ out("CLIENT", client);
+ out("JAR", jar);
+ out("EXTRACT_JAR", extract_jar);
+ out("ASSET", asset);
+ out("LOG4J", log4j);
+ out("ASSET_INDEX", asset_index);
+ if (!classpath_set) {
+ /* Just for compatibility for old versions. */
+ append(launcher, "JVM_ARGS", "-cp ${classpath}");
+ }
+ if (!libraries_set) {
+ /* Just for compatibility for old versions. */
+ append(launcher, "JVM_ARGS", "-Djava.library.path=${natives_directory}");
+ }
+
+ cleanup();
+ return 0;
+}
diff --git a/launcher b/launcher
new file mode 100755
index 0000000..255f6b3
--- /dev/null
+++ b/launcher
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Example launcher
+set -ex
+VERSION=$1
+if test -z "$VERSION"; then
+ echo "Usage: launcher <version>"
+ exit 64
+fi
+
+# Implement your Yggdrasil client here.
+export auth_player_name=Steve
+export auth_uuid=12345
+export auth_access_token=12345
+
+export JVM_ARGS="-Xmx8192m -Duser.home=null"
+export version_name=xxx
+export game_directory=~/Minecraft
+export launcher_name=Shell
+export launcher_version=Alex
+export user_type=mojang
+export version_type=release
+export assets_root=$game_directory/assets/$VERSION
+
+source /usr/share/java/minecraft/$VERSION/launcher.gen
+
+mkdir -p $assets_root
+mkdir -p $assets_root/skin
+ln -s /usr/share/minecraft/assets/$assets_index_name/objects $assets_root/objects || true
+ln -s /usr/share/minecraft/assets/$assets_index_name/indexes $assets_root/indexes || true
+
+exec /usr/lib/jvm/java-17-openjdk/bin/java \
+ $JVM_ARGS \
+ $MAIN_CLASS \
+ $MC_ARGS
diff --git a/mc/.gitignore b/mc/.gitignore
new file mode 100644
index 0000000..10dc547
--- /dev/null
+++ b/mc/.gitignore
@@ -0,0 +1,6 @@
+src/
+pkg/
+*.zst
+*.log
+*.jar
+*.xml
diff --git a/mc/PKGBUILD b/mc/PKGBUILD
new file mode 100644
index 0000000..e300a5e
--- /dev/null
+++ b/mc/PKGBUILD
@@ -0,0 +1,52 @@
+# Maintainer: YuutaW <YuutaW@ymc.moe>
+. ../PKGBUILD.gen
+pkgname=minecraft-${_MC_ID}
+pkgver=${_MC_ID//-/_}
+pkgrel=1
+epoch=
+pkgdesc="Minecraft ${_MC_ID}"
+arch=(x86_64)
+url="https://minecraft.net/"
+license=('custom')
+depends=("java-environment>=$_JAVA_VERSION")
+makedepends=()
+optdepends=("minecraft-assets-${_ASSET_ID//-/_}: Minecraft assets (built-in sounds, extra resource packs, etc)")
+install=
+source=(launcher.gen ${_MC_ID}.jar::$_MC_CLIENT_URL $_MC_JAR_URL $_MC_EXTRACT_JAR_URL $_MC_LOG4J_URL)
+sha1sums=('SKIP' $_MC_CLIENT_SHA1 $_MC_JAR_SHA1 $_MC_EXTRACT_JAR_SHA1 $_MC_LOG4J_SHA1)
+noextract=(launcher.gen ${_MC_ID}.jar $_MC_JAR $_MC_EXTRACT_JAR $_MC_LOG4J)
+# Prevents lwjgl from complaining sha mismatches
+options=(!strip)
+
+_dir=/usr/share/java/minecraft/$pkgver/
+
+prepare() {
+ cd $srcdir
+ echo "natives_directory=\"${_dir}native/\"" > launcher.gen.add
+ printf "classpath=\"" >> launcher.gen.add
+ for jar in `echo $_MC_JAR`; do
+ printf "%s/%s:" $_dir $jar >> launcher.gen.add
+ done
+ echo "$_dir/${_MC_ID}.jar\"" >> launcher.gen.add
+
+ sed -e "s/LOG4J_XML_PATH/\/usr\/share\/java\/minecraft\/$pkgver\/$_LOG4J_FILE/" $srcdir/launcher.gen > launcher.gen.mod
+
+ cat launcher.gen.mod >> launcher.gen.add
+}
+
+package() {
+ cd $srcdir
+ _dir="$pkgdir/$_dir"
+ mkdir -p $_dir
+ cp $_MC_JAR ${_MC_ID}.jar $_dir
+ if test -n "$_MC_LOG4J"; then
+ cp $_MC_LOG4J $_dir
+ fi
+ cp launcher.gen.add $_dir/launcher.gen
+ mkdir -p $_dir/native/
+ cd $_dir/native/
+ for jar in `echo $_MC_EXTRACT_JAR`; do
+ bsdtar -xf $srcdir/$jar
+ done
+ rm -rf META-INF
+}
diff --git a/mc/launcher.gen b/mc/launcher.gen
new file mode 120000
index 0000000..a30c677
--- /dev/null
+++ b/mc/launcher.gen
@@ -0,0 +1 @@
+../launcher.gen \ No newline at end of file
diff --git a/switch b/switch
new file mode 100755
index 0000000..41fa57b
--- /dev/null
+++ b/switch
@@ -0,0 +1,6 @@
+#!/bin/sh
+if test -z "$1"; then
+ echo "Usage: switch <Minecraft version>"
+fi
+cc -D_POSIX_C_SOURCE=200809L -std=c99 -ljson-c -lcurl -o gen gen.c
+./gen -v "$1"