From 9cba61198231a13e183dbed4ff4aee3589b59c7c Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Fri, 25 Sep 2020 12:43:53 -0400 Subject: [PATCH] Initial Commit --- .dockerignore | 4 + .gitignore | 4 + Dockerfile | 25 ++++ README.md | 23 ++++ build.sh | 5 + cmake/toolchain.cmake | 5 + core/CMakeLists.txt | 14 +++ core/include/libcore/libcore.h | 37 ++++++ core/src/bcm_host.c | 39 ++++++ core/src/core.c | 44 +++++++ core/src/launcher.c | 87 +++++++++++++ mods/CMakeLists.txt | 19 +++ mods/src/compat.c | 204 +++++++++++++++++++++++++++++++ mods/src/readdir.c | 23 ++++ mods/src/touch.c | 8 ++ run.sh | 10 ++ scripts/build-libpng12.sh | 15 +++ scripts/build-mods.sh | 19 +++ scripts/download-minecraft-pi.sh | 8 ++ 19 files changed, 593 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 build.sh create mode 100644 cmake/toolchain.cmake create mode 100644 core/CMakeLists.txt create mode 100644 core/include/libcore/libcore.h create mode 100644 core/src/bcm_host.c create mode 100644 core/src/core.c create mode 100644 core/src/launcher.c create mode 100644 mods/CMakeLists.txt create mode 100644 mods/src/compat.c create mode 100644 mods/src/readdir.c create mode 100644 mods/src/touch.c create mode 100755 run.sh create mode 100755 scripts/build-libpng12.sh create mode 100755 scripts/build-mods.sh create mode 100755 scripts/download-minecraft-pi.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..26df1758 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.gitignore +Dockerfile +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0055243e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/minecraft-pi +/libpng +/core/build +/mods/build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5660af89 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM arm64v8/debian:bullseye + +RUN dpkg --add-architecture armhf + +RUN apt-get update +RUN apt-get upgrade -y + +RUN apt-get install -y libglvnd-dev:armhf libsdl1.2-dev:armhf libx11-dev:armhf build-essential zlib1g-dev:armhf git cmake curl gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf gdb + +RUN ln -s /usr/lib/arm-linux-gnueabihf/libGLESv2.so.2 /usr/lib/libGLESv2.so +RUN ln -s /usr/lib/arm-linux-gnueabihf/libEGL.so.1 /usr/lib/libEGL.so + +ADD . /app + +WORKDIR /app + +RUN ./scripts/download-minecraft-pi.sh + +RUN ./scripts/build-mods.sh + +RUN ./scripts/build-libpng12.sh + +WORKDIR /app/minecraft-pi + +ENTRYPOINT ./launcher diff --git a/README.md b/README.md new file mode 100644 index 00000000..db2edf66 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Minecraft: Pi Edition For Docker + +## Dependencies +```sh +# Required For Hardware Acceleration +sudo apt install virgl-server + +# Required For ARM Support +sudo docker run --rm --privileged multiarch/qemu-user-static --reset -p yes +``` + +## Tutorial +```sh +virgl_test_server & +PID="$!" + +sudo docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp/.virgl_test:/tmp/.virgl_test -v ~/.minecraft-pi:/root/.minecraft -e DISPLAY=unix${DISPLAY} thebrokenrail/minecraft-pi + +kill "${PID}" +``` + +## Tweaks +The included version of Minecraft: Pi Ediiton is slightly modified so it uses the old touchscreen UI. diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..fe52941d --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +sudo docker build --tag thebrokenrail/minecraft-pi:latest . diff --git a/cmake/toolchain.cmake b/cmake/toolchain.cmake new file mode 100644 index 00000000..db2566cb --- /dev/null +++ b/cmake/toolchain.cmake @@ -0,0 +1,5 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) +set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 00000000..9775adb1 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(core) + +add_compile_options(-Wall -Wextra -Werror) + +include_directories(include) + +add_library(core SHARED src/core.c) +target_link_libraries(core dl) + +add_library(bcm_host SHARED src/bcm_host.c) + +add_executable(launcher src/launcher.c) diff --git a/core/include/libcore/libcore.h b/core/include/libcore/libcore.h new file mode 100644 index 00000000..bfeff27f --- /dev/null +++ b/core/include/libcore/libcore.h @@ -0,0 +1,37 @@ +#ifndef LIBLOADER_H + +#define LIBLOADER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define HOOK(name, return_type, args) \ + typedef return_type (*name##_t)args; \ + static name##_t real_##name = NULL; \ + \ + __attribute__((__unused__)) static void ensure_##name() { \ + if (!real_##name) { \ + dlerror(); \ + real_##name = (name##_t) dlsym(RTLD_NEXT, #name); \ + if (!real_##name) { \ + fprintf(stderr, "Error Resolving Symbol: "#name": %s\n", dlerror()); \ + exit(1); \ + } \ + } \ + }; \ + \ + __attribute__((__used__)) return_type name args + +void overwrite(void *start, void *target); +void patch(void *start, unsigned char patch[]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/src/bcm_host.c b/core/src/bcm_host.c new file mode 100644 index 00000000..9d4d7357 --- /dev/null +++ b/core/src/bcm_host.c @@ -0,0 +1,39 @@ +#include + +void bcm_host_init(void) { +} + +void bcm_host_deinit(void) { +} + +int32_t graphics_get_display_size(__attribute__((unused)) const uint16_t display_number, __attribute__((unused)) uint32_t *width, __attribute__((unused)) uint32_t *height) { + return -1; +} + +unsigned bcm_host_get_peripheral_address(void) { + return 0x20000000; +} + +unsigned bcm_host_get_peripheral_size(void) { + return 0x01000000; +} + +unsigned bcm_host_get_sdram_address(void) { + return 0x40000000; +} + +uint32_t vc_dispmanx_display_open(__attribute__((unused)) uint32_t device) { + return 0; +} + +uint32_t vc_dispmanx_element_add(__attribute__((unused)) uint32_t update, __attribute__((unused)) uint32_t display, __attribute__((unused)) int32_t layer, __attribute__((unused)) const void *dest_rect, __attribute__((unused)) uint32_t src, __attribute__((unused)) const void *src_rect, __attribute__((unused)) uint32_t protection, __attribute__((unused)) void *alpha, __attribute__((unused)) void *clamp, __attribute__((unused)) uint32_t transform) { + return 0; +} + +uint32_t vc_dispmanx_update_start(__attribute__((unused)) int32_t priority) { + return 0; +} + +int vc_dispmanx_update_submit_sync(__attribute__((unused)) uint32_t update) { + return 0; +} diff --git a/core/src/core.c b/core/src/core.c new file mode 100644 index 00000000..0ea3da14 --- /dev/null +++ b/core/src/core.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +#include + +#define PREPARE_PATCH(start) \ + size_t page_size = sysconf(_SC_PAGESIZE); \ + uintptr_t end = ((uintptr_t) start) + 1; \ + uintptr_t page_start = ((uintptr_t) start) & -page_size; \ + mprotect((void *) page_start, end - page_start, PROT_READ | PROT_WRITE); \ + \ + unsigned char *data = (unsigned char *) start; \ + int thumb = ((size_t) start) & 1; \ + if (thumb) { \ + data--; \ + } \ + fprintf(stderr, "Patching - original: %i %i %i %i %i\n", data[0], data[1], data[2], data[3], data[4]); + +#define END_PATCH() \ + fprintf(stderr, "Patching - result: %i %i %i %i %i\n", data[0], data[1], data[2], data[3], data[4]); \ + \ + mprotect((void *) page_start, end - page_start, PROT_READ | PROT_EXEC); + +void overwrite(void *start, void *target) { + PREPARE_PATCH(start); + if (thumb) { + unsigned char patch[4] = {0xdf, 0xf8, 0x00, 0xf0}; + memcpy(data, patch, 4); + } else { + unsigned char patch[4] = {0x04, 0xf0, 0x1f, 0xe5}; + memcpy(data, patch, 4); + } + memcpy(&data[4], &target, sizeof (int)); + END_PATCH(); +} + +void patch(void *start, unsigned char patch[]) { + PREPARE_PATCH(start); + memcpy(data, patch, 4); + END_PATCH(); +} diff --git a/core/src/launcher.c b/core/src/launcher.c new file mode 100644 index 00000000..8849c7e1 --- /dev/null +++ b/core/src/launcher.c @@ -0,0 +1,87 @@ +#define _FILE_OFFSET_BITS 64 +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include + +static int starts_with(const char *s, const char *t) { + return strncmp(s, t, strlen(t)) == 0; +} + +static int ends_with(const char *s, const char *t) { + size_t slen = strlen(s); + size_t tlen = strlen(t); + if (tlen > slen) return 1; + return strcmp(s + slen - tlen, t) == 0; +} + +#define MODS_FOLDER "./mods/" + +static void set_and_print_env(char *name, char *value) { + fprintf(stderr, "Set %s = %s\n", name, value); + setenv(name, value, 1); +} + +static char *get_env_safe(const char *name) { + char *ret = getenv(name); + return ret != NULL ? ret : ""; +} + +int main(__attribute__((unused)) int argc, char *argv[]) { + fprintf(stderr, "Configuring Game...\n"); + + char *ld_path = NULL; + char *cwd = getcwd(NULL, 0); + asprintf(&ld_path, "%s:/usr/arm-linux-gnueabihf/lib:%s", cwd, get_env_safe("LD_LIBRARY_PATH")); + free(cwd); + set_and_print_env("LD_LIBRARY_PATH", ld_path); + free(ld_path); + + char *ld_preload = NULL; + asprintf(&ld_preload, "%s", get_env_safe("LD_PRELOAD")); + int folder_name_length = strlen(MODS_FOLDER); + DIR *dp = opendir(MODS_FOLDER); + if (dp != NULL) { + struct dirent *entry = NULL; + errno = 0; + while (1) { + entry = readdir(dp); + if (entry != NULL) { + if (starts_with(entry->d_name, "lib") && ends_with(entry->d_name, ".so")) { + int name_length = strlen(entry->d_name); + int total_length = folder_name_length + name_length; + char name[total_length + 1]; + + for (int i = 0; i < folder_name_length; i++) { + name[i] = MODS_FOLDER[i]; + } + for (int i = 0; i < name_length; i++) { + name[folder_name_length + i] = entry->d_name[i]; + } + + name[total_length] = '\0'; + + asprintf(&ld_preload, "%s:%s", name, ld_preload); + } + } else if (errno != 0) { + fprintf(stderr, "Error Reading Directory: %s\n", strerror(errno)); + exit(1); + } else { + break; + } + } + } else { + fprintf(stderr, "Error Opening Directory: %s\n", strerror(errno)); + exit(1); + } + closedir(dp); + set_and_print_env("LD_PRELOAD", ld_preload); + free(ld_preload); + + fprintf(stderr, "Starting Game...\n"); + return execve("./minecraft-pi", argv, environ); +} diff --git a/mods/CMakeLists.txt b/mods/CMakeLists.txt new file mode 100644 index 00000000..2288d7c2 --- /dev/null +++ b/mods/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(mods) + +add_compile_options(-Wall -Wextra -Werror) + +add_subdirectory(../core core) +include_directories(../core/include) + +add_library(compat SHARED src/compat.c) +target_link_libraries(compat core SDL EGL GLESv1_CM GLESv2 X11 dl) +# Force GLESv1 Link +target_link_options(compat PRIVATE "-Wl,--no-as-needed") + +add_library(touch SHARED src/touch.c) +target_link_libraries(touch core dl) + +add_library(readdir SHARED src/readdir.c) +target_link_libraries(readdir core) diff --git a/mods/src/compat.c b/mods/src/compat.c new file mode 100644 index 00000000..313c550a --- /dev/null +++ b/mods/src/compat.c @@ -0,0 +1,204 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#include + +static Display *x11_display; +static EGLDisplay egl_display; +static Window x11_window; +static Window x11_root_window; +static EGLConfig egl_config; +static int window_loaded = 0; +static EGLContext egl_context; +static EGLSurface egl_surface; + +HOOK(eglGetDisplay, EGLDisplay, (__attribute__((unused)) NativeDisplayType native_display)) { + // Handled In ensure_x11_window() + return 0; +} + +// Get Reference To X Window +static void ensure_x11_window() { + if (!window_loaded) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + SDL_GetWMInfo(&info); + + x11_display = info.info.x11.display; + x11_window = info.info.x11.window; + x11_root_window = RootWindow(x11_display, DefaultScreen(x11_display)); + ensure_eglGetDisplay(); + egl_display = (*real_eglGetDisplay)(x11_display); + + window_loaded = 1; + } +} + +// Handled In SDL_WM_SetCaption +HOOK(eglInitialize, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLint *major, __attribute__((unused)) EGLint *minor)) { + return EGL_TRUE; +} + +// Handled In SDL_WM_SetCaption +HOOK(eglChooseConfig, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLint const *attrib_list, __attribute__((unused)) EGLConfig *configs, __attribute__((unused)) EGLint config_size, __attribute__((unused)) EGLint *num_config)) { + return EGL_TRUE; +} + +// Handled In SDL_WM_SetCaption +HOOK(eglBindAPI, EGLBoolean, (__attribute__((unused)) EGLenum api)) { + return EGL_TRUE; +} + +// Handled In SDL_WM_SetCaption +HOOK(eglCreateContext, EGLContext, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLConfig config, __attribute__((unused)) EGLContext share_context, __attribute__((unused)) EGLint const *attrib_list)) { + return 0; +} + +// Handled In SDL_WM_SetCaption +HOOK(eglCreateWindowSurface, EGLSurface, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLConfig config, __attribute__((unused)) NativeWindowType native_window, __attribute__((unused)) EGLint const *attrib_list)) { + return 0; +} + +// Handled In SDL_WM_SetCaption +HOOK(eglMakeCurrent, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLSurface draw, __attribute__((unused)) EGLSurface read, __attribute__((unused)) EGLContext context)) { + return EGL_TRUE; +} + +// Handled In SDL_Quit +HOOK(eglDestroySurface, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLSurface surface)) { + return EGL_TRUE; +} + +// Handled In SDL_Quit +HOOK(eglDestroyContext, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLContext context)) { + return EGL_TRUE; +} + +// Handled In SDL_Quit +HOOK(eglTerminate, EGLBoolean, (__attribute__((unused)) EGLDisplay display)) { + return EGL_TRUE; +} + +// Handled In SDL_WM_SetCaption +HOOK(SDL_SetVideoMode, SDL_Surface *, (__attribute__((unused)) int width, __attribute__((unused)) int height, __attribute__((unused)) int bpp, __attribute__((unused)) Uint32 flags)) { + // Return Value Is Only Used For A NULL-Check + return (SDL_Surface *) 1; +} + +// EGL Config +EGLint const set_attrib_list[] = { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_DEPTH_SIZE, 16, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE +}; + +// Init EGL +HOOK(SDL_WM_SetCaption, void, (const char *title, const char *icon)) { + ensure_SDL_SetVideoMode(); + (*real_SDL_SetVideoMode)(848, 480, 32, 16); + + ensure_SDL_WM_SetCaption(); + (*real_SDL_WM_SetCaption)(title, icon); + + ensure_x11_window(); + + ensure_eglInitialize(); + (*real_eglInitialize)(egl_display, NULL, NULL); + + EGLint number_of_config; + ensure_eglChooseConfig(); + (*real_eglChooseConfig)(egl_display, set_attrib_list, &egl_config, 1, &number_of_config); + + ensure_eglBindAPI(); + (*real_eglBindAPI)(EGL_OPENGL_ES_API); + + ensure_eglCreateContext(); + egl_context = (*real_eglCreateContext)(egl_display, egl_config, EGL_NO_CONTEXT, NULL); + + ensure_eglCreateWindowSurface(); + egl_surface = (*real_eglCreateWindowSurface)(egl_display, egl_config, x11_window, NULL); + + ensure_eglMakeCurrent(); + (*real_eglMakeCurrent)(egl_display, egl_surface, egl_surface, egl_context); + + eglSwapInterval(egl_display, 1); +} + +HOOK(eglSwapBuffers, EGLBoolean, (__attribute__((unused)) EGLDisplay display, __attribute__((unused)) EGLSurface surface)) { + ensure_x11_window(); + + ensure_eglSwapBuffers(); + EGLBoolean ret = (*real_eglSwapBuffers)(egl_display, egl_surface); + + return ret; +} + +HOOK(SDL_PollEvent, int, (SDL_Event *event)) { + ensure_SDL_PollEvent(); + int ret = (*real_SDL_PollEvent)(event); + + // Resize EGL + if (event != NULL && event->type == SDL_VIDEORESIZE) { + ensure_SDL_SetVideoMode(); + (*real_SDL_SetVideoMode)(event->resize.w, event->resize.h, 32, 16); + + // OpenGL state modification for resizing + glViewport(0, 0, event->resize.w, event->resize.h); + glMatrixMode(GL_PROJECTION); + glOrthox(0, event->resize.w, 0, event->resize.h, -1, 1); + glLoadIdentity(); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glClear(GL_COLOR_BUFFER_BIT); + glLoadIdentity(); + } + + return ret; +} + +// Terminate EGL +HOOK(SDL_Quit, void, ()) { + ensure_SDL_Quit(); + (*real_SDL_Quit)(); + + ensure_eglDestroyContext(); + (*real_eglDestroyContext)(egl_display, egl_context); + ensure_eglDestroySurface(); + (*real_eglDestroySurface)(egl_display, egl_surface); + ensure_eglTerminate(); + (*real_eglTerminate)(egl_display); +} + +HOOK(XTranslateCoordinates, int, (Display *display, Window src_w, Window dest_w, int src_x, int src_y, int *dest_x_return, int *dest_y_return, Window *child_return)) { + ensure_XTranslateCoordinates(); + if (window_loaded) { + return (*real_XTranslateCoordinates)(x11_display, x11_window, x11_root_window, src_x, src_y, dest_x_return, dest_y_return, child_return); + } else { + return (*real_XTranslateCoordinates)(display, src_w, dest_w, src_x, src_y, dest_x_return, dest_y_return, child_return); + } +} + +HOOK(XGetWindowAttributes, int, (Display *display, Window w, XWindowAttributes *window_attributes_return)) { + ensure_XGetWindowAttributes(); + if (window_loaded) { + return (*real_XGetWindowAttributes)(x11_display, x11_window, window_attributes_return); + } else { + return (*real_XGetWindowAttributes)(display, w, window_attributes_return); + } +} + +#include + +// Use VirGL +__attribute__((constructor)) static void init() { + setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1); + setenv("GALLIUM_DRIVER", "virpipe", 1); +} diff --git a/mods/src/readdir.c b/mods/src/readdir.c new file mode 100644 index 00000000..84668b2c --- /dev/null +++ b/mods/src/readdir.c @@ -0,0 +1,23 @@ +#define _GNU_SOURCE +#define __USE_LARGEFILE64 + +#include +#include +#include + +#include + +#define FILENAME_SIZE 256 + +HOOK(readdir, struct dirent *, (DIR *dirp)) { + struct dirent64 *original = readdir64(dirp); + if (original == NULL) { + return NULL; + } + static struct dirent new; + for (int i = 0; i < FILENAME_SIZE; i++) { + new.d_name[i] = original->d_name[i]; + } + new.d_type = original->d_type; + return &new; +} diff --git a/mods/src/touch.c b/mods/src/touch.c new file mode 100644 index 00000000..6364cd72 --- /dev/null +++ b/mods/src/touch.c @@ -0,0 +1,8 @@ +#include + +__attribute__((constructor)) static void init() { + unsigned char patch_data[4] = {0x01, 0x00, 0x50, 0xe3}; + patch((void *) 0x292fc, patch_data); + //unsigned char patch_data_2[4] = {0x00, 0x30, 0xa0, 0xe3}; + //patch((void *) 0x3d9b8, patch_data_2); +} diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..4d155cf9 --- /dev/null +++ b/run.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +virgl_test_server & +PID="$!" + +sudo docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -v /tmp/.virgl_test:/tmp/.virgl_test -v ~/.minecraft-pi:/root/.minecraft -e DISPLAY=unix${DISPLAY} thebrokenrail/minecraft-pi + +kill "${PID}" diff --git a/scripts/build-libpng12.sh b/scripts/build-libpng12.sh new file mode 100755 index 00000000..75599991 --- /dev/null +++ b/scripts/build-libpng12.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +git clone --depth 1 https://git.code.sf.net/p/libpng/code libpng -b libpng12 + +cd libpng + +./configure --host arm-linux-gnueabihf --prefix /usr/arm-linux-gnueabihf + +make -j$(nproc) +make install + +cd ../ +rm -rf libpng diff --git a/scripts/build-mods.sh b/scripts/build-mods.sh new file mode 100755 index 00000000..5c17b8a0 --- /dev/null +++ b/scripts/build-mods.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +cd mods + +mkdir build +cd build + +cmake -DCMAKE_TOOLCHAIN_FILE=../../cmake/toolchain.cmake .. +make -j$(nproc) + +cd ../../ + +mkdir minecraft-pi/mods +cp mods/build/lib*.so minecraft-pi/mods + +cp mods/build/core/lib*.so minecraft-pi +cp mods/build/core/launcher minecraft-pi diff --git a/scripts/download-minecraft-pi.sh b/scripts/download-minecraft-pi.sh new file mode 100755 index 00000000..c1503ee3 --- /dev/null +++ b/scripts/download-minecraft-pi.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +URL="https://www.minecraft.net/content/dam/minecraft/edition-pi/minecraft-pi-0.1.1.tar.gz" + +mkdir minecraft-pi +curl "${URL}" | tar -xz --strip-components 1 -C minecraft-pi