diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3438ad9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Python stuff + +__pycache__/ +build/ +*.pyc diff --git a/.install_files/application-x-mcpimod.png b/.install_files/application-x-mcpimod.png new file mode 100644 index 0000000..79a9542 Binary files /dev/null and b/.install_files/application-x-mcpimod.png differ diff --git a/install_files/icon.png b/.install_files/icon.png similarity index 100% rename from install_files/icon.png rename to .install_files/icon.png diff --git a/.install_files/mcpimod.xml b/.install_files/mcpimod.xml new file mode 100644 index 0000000..fb00ca5 --- /dev/null +++ b/.install_files/mcpimod.xml @@ -0,0 +1,9 @@ + + + + + Minecraft Pi Mod + + + + diff --git a/install_files/minecraft-pe b/.install_files/minecraft-pe similarity index 100% rename from install_files/minecraft-pe rename to .install_files/minecraft-pe diff --git a/install_files/minecraft-pe.bsdiff b/.install_files/minecraft-pe.bsdiff similarity index 100% rename from install_files/minecraft-pe.bsdiff rename to .install_files/minecraft-pe.bsdiff diff --git a/install_files/mcpil.desktop b/.install_files/tk.mcpi.mcpil.desktop similarity index 57% rename from install_files/mcpil.desktop rename to .install_files/tk.mcpi.mcpil.desktop index ffa4575..9a98a1b 100644 --- a/install_files/mcpil.desktop +++ b/.install_files/tk.mcpi.mcpil.desktop @@ -1,9 +1,11 @@ [Desktop Entry] Name=MCPIL Comment=Minecraft Pi Launcher -Exec=$(EXECUTABLE_PATH) -Icon=$(ICON_PATH) +Exec=$(EXECUTABLE_PATH) %f +Icon=mcpil Terminal=false Type=Application Categories=Application;Game; +MimeType=application/x-mcpimod StartupNotify=true +GenericName=Minecraft Pi Launcher diff --git a/README.md b/README.md index f4607b1..5e89357 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,49 @@ A simple launcher for Minecraft: Pi Edition. ## Getting started ### Prerequisites -To use MCPIL you need to have ``Python> = 3.8.x`` pre-installed and root privileges. +To use MCPIL you need to have `Python >= 3.7.x` pre-installed and root privileges. ### Installation To install MCPIL, download or clone the repository: -``` shell +```shell git clone https://github.com/Alvarito050506/MCPIL.git ``` -and then run the file ``install.py``. It will create a desktop file that you can access in the "Games" category. +and then run the `install.py` file. It will create a desktop file that you can access in the "Games" category. ## Features + Switch between Minecraft Pi and PE + Username change + Skin change + Mod load ++ Mod API ++ Mod compilation + World game mode and name change -+ Coming soon: join non-local server ++ Join non-local servers ++ Coming soon: Pi Realms ## Usage -Launch the MCPIL desktop file or run the ``mcpil.py`` file to see the magick! :wink: +Launch the MCPIL desktop file or run the `mcpil.py` file to see the magick! :wink: + +## API +There is an MCPIL API that you can use by importing the `mcpil` module into your Python mod. It exposes the following functions: + +### `def get_user_name()` +Returns the user name of the player. + +### `def get_world_name()` +Returns the name of the current world. + +### `def get_game_mode()` +Returns the game mode of the current world as an interger: + + 0 = Survival + + 1 = Creative + +## Mod compilation +To compile (compress) a Python mod, run the `mcpim.py` file passing the filename of the mod as the first argument. For example: +```shell +./mcpim.py example.py +``` +will produce a `example.mcpi` mod file. ## Thanks To [@Phirel](https://www.minecraftforum.net/members/Phirel) for his Pi2PE (a.k.a. "survival") patch. diff --git a/api/mcpil/__init__.py b/api/mcpil/__init__.py new file mode 100644 index 0000000..c73823e --- /dev/null +++ b/api/mcpil/__init__.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# __init__.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +import sys +import psutil +from os import environ + +def get_user_name(): + return environ.get("MCPIL_USERNAME"); + +def get_world_name(): + mcpi_process = psutil.Process(int(environ.get("MCPIL_PID"))); + return mcpi_process.open_files()[-1].path.split("/")[-2]; + +def get_game_mode(): + world_name = get_world_name(); + world_file = open("/root/.minecraft/games/com.mojang/minecraftWorlds/" + world_name + "/level.dat", "rb"); + world_file.seek(0x16); + game_mode = int.from_bytes(world_file.read(1), "little"); + world_file.close(); + return game_mode; diff --git a/api/setup.py b/api/setup.py new file mode 100644 index 0000000..6a5a804 --- /dev/null +++ b/api/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# setup.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +from distutils.core import setup +from os import chdir, path + +chdir(path.dirname(__file__)); + +setup( + name="mcpil", + version="v0.1.1", + description="MCPI API extensions", + author="Alvarito050506", + packages=["mcpil"], + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)" + ] +); diff --git a/mods/example.py b/example.py similarity index 58% rename from mods/example.py rename to example.py index 472c26e..43294e6 100755 --- a/mods/example.py +++ b/example.py @@ -3,6 +3,7 @@ import sys import time from mcpi import * +from mcpil import * mc = minecraft.Minecraft.create(); @@ -10,7 +11,12 @@ def main(args): mc.setting("world_immutable", True); mc.camera.setFollow(); mc.saveCheckpoint(); - mc.postToChat("Welcome to Minecraft Pi."); + mc.postToChat("Welcome to Minecraft Pi, " + get_user_name() + "."); + mc.postToChat("The name of this awesome world is \"" + get_world_name() + "\"."); + if get_game_mode() == 0: + mc.postToChat("You are in Survival mode."); + else: + mc.postToChat("You are in Creative mode."); time.sleep(5); mc.setting("world_immutable", False); mc.setting("nametags_visible", True); diff --git a/install.py b/install.py old mode 100644 new mode 100755 index 74eba95..50d892a --- a/install.py +++ b/install.py @@ -1,5 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# +# install.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# import subprocess import sys @@ -14,9 +34,11 @@ def main(): if os.geteuid() != 0: sys.stdout.write("Error: You need to have root privileges to run this installer.\n"); + return 1; sys.stdout.write("Installing dependencies... "); subprocess.call(["sudo", "apt-get", "install", "python3-tk", "wget", "bspatch", "-qq"]); + subprocess.call(["sudo", "pip3", "install", "psutil", "-qq"]); sys.stdout.write("OK.\n"); sys.stdout.write("Downloading Minecraft... "); @@ -24,21 +46,30 @@ def main(): sys.stdout.write("OK.\n"); sys.stdout.write("Installing Minecraft... "); - subprocess.call(["bspatch", "/opt/minecraft-pi/minecraft-pi", "/opt/minecraft-pi/minecraft-pe", "install_files/minecraft-pe.bsdiff"]); + subprocess.call(["bspatch", "/opt/minecraft-pi/minecraft-pi", "/opt/minecraft-pi/minecraft-pe", "./.install_files/minecraft-pe.bsdiff"]); subprocess.call(["chmod", "a+x", "/opt/minecraft-pi/minecraft-pe"]); sys.stdout.write("OK.\n"); + sys.stdout.write("Installing API... "); + subprocess.call(["python3", "./api/setup.py", "-q", "install"]); + sys.stdout.write("OK.\n"); + sys.stdout.write("Configuring... "); - desktop_template = open("install_files/mcpil.desktop", "r"); - desktop_file = open("/usr/share/applications/mcpil.desktop", "w"); - desktop_file.write(desktop_template.read().replace("$(EXECUTABLE_PATH)", os.getcwd() + "/mcpil.py").replace("$(ICON_PATH)", os.getcwd() + "/install_files/icon.png")); + desktop_template = open("./.install_files/tk.mcpi.mcpil.desktop", "r"); + desktop_file = open("/usr/share/applications/tk.mcpi.mcpil.desktop", "w"); + desktop_file.write(desktop_template.read().replace("$(EXECUTABLE_PATH)", os.getcwd() + "/mcpil.py").replace("$(ICON_PATH)", os.getcwd() + "/.install_files/icon.png")); desktop_template.close(); desktop_file.close(); - - shutil.copy2("./install_files/minecraft-pe", "/usr/bin/minecraft-pe"); + + shutil.copy2("./.install_files/minecraft-pe", "/usr/bin/minecraft-pe"); subprocess.call(["chmod", "a+x", "/usr/bin/minecraft-pe"]); subprocess.call(["chmod", "a+x", "mcpil.py"]); + + shutil.copy2("./.install_files/icon.png", "/usr/share/pixmaps/mcpil.png"); + shutil.copy2("./.install_files/application-x-mcpimod.png", "/usr/share/pixmaps/application-x-mcpimod.png"); + shutil.copy2("./.install_files/mcpimod.xml", "/usr/share/mime/packages/application-x-mcpimod.xml"); + subprocess.call(["update-mime-database", "/usr/share/mime"]); sys.stdout.write("OK.\n"); return 0; diff --git a/mcpil.py b/mcpil.py index 8f9cdb1..f341e67 100755 --- a/mcpil.py +++ b/mcpil.py @@ -1,12 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# +# mcpil.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# import sys import subprocess import atexit import signal import webbrowser -from os import walk, remove, path, chdir, kill, rename +import psutil +import time +from os import walk, remove, path, chdir, kill, rename, environ, uname, geteuid from tkinter import * from tkinter import ttk from tkinter.filedialog import askopenfilename @@ -22,7 +44,11 @@ def on_select_versions(event): return 0; def launch(): - subprocess.Popen([binaries[current_selection]]); + global mcpi_pid; + mcpi_process = subprocess.Popen([binaries[current_selection]]); + time.sleep(2); + mcpi_pid = mcpi_process.pid + 2; + start_mods(); return 0; def save_settings(): @@ -55,8 +81,9 @@ def on_select_mods(event): delete_button["state"] = DISABLED; return 0; -def install_mod(): - mod_file = askopenfilename(filetypes=[("Python scripts", "*.py")]); +def install_mod(mod_file=None): + if mod_file is None: + mod_file = askopenfilename(filetypes=[("Minecraft Pi Mods", "*.mcpi *.py")]); copy2(mod_file, "./mods/" + path.basename(mod_file)); update_mods(); delete_button["state"] = DISABLED; @@ -78,13 +105,19 @@ def update_mods(): mods.delete(0, END); while i < len(mod_files): - mods.insert(i, mod_files[i]); + mods.insert(i, mod_files[i].replace(".py", "").replace(".mcpi", "")); i += 1; return 0; def start_mods(): global mods_process; - mods_process = subprocess.Popen(["python3", "mcpim.py"]); + mods_env = environ.copy(); + mcpi_file = open("/opt/minecraft-pi/minecraft-pi", "rb"); + mcpi_file.seek(0xfa8ca); + mods_env["MCPIL_USERNAME"] = mcpi_file.read(7).decode("utf-8"); + mcpi_file.close(); + mods_env["MCPIL_PID"] = str(mcpi_pid); + mods_process = subprocess.Popen(["./mcpim.py"], env=mods_env); return 0; def kill_mods(): @@ -93,6 +126,7 @@ def kill_mods(): def change_skin(): skin_file = askopenfilename(filetypes=[("Portable Network Graphics", "*.png")]); + copy2("/opt/minecraft-pi/data/images/mob/char.png", "/opt/minecraft-pi/data/images/mob/char_original.png"); copy2(skin_file, "/opt/minecraft-pi/data/images/mob/char.png"); return 0; @@ -101,16 +135,16 @@ def web_open(event): return 0; def save_world(): - old_worldname = old_worldname_entry.get(); - new_worldname = new_worldname_entry.get(); - world_file = open("/root/.minecraft/games/com.mojang/minecraftWorlds/" + old_worldname + "/level.dat", "rb+"); - new_world = world_file.read().replace(bytes([len(old_worldname)]) + bytes([0]) + bytes(old_worldname.encode()), bytes([len(new_worldname)]) + bytes([0]) + bytes(new_worldname.encode())); + old_world_name = old_worldname_entry.get(); + new_world_name = new_worldname_entry.get(); + world_file = open("/root/.minecraft/games/com.mojang/minecraftWorlds/" + old_world_name + "/level.dat", "rb+"); + new_world = world_file.read().replace(bytes([len(old_world_name)]) + bytes([0]) + bytes(old_world_name.encode()), bytes([len(new_world_name)]) + bytes([0]) + bytes(new_world_name.encode())); world_file.seek(0); world_file.write(new_world); world_file.seek(0x16); world_file.write(bytes([game_mode.get()])); world_file.close(); - rename("/root/.minecraft/games/com.mojang/minecraftWorlds/" + old_worldname, "/root/.minecraft/games/com.mojang/minecraftWorlds/" + new_worldname); + rename("/root/.minecraft/games/com.mojang/minecraftWorlds/" + old_world_name, "/root/.minecraft/games/com.mojang/minecraftWorlds/" + new_world_name); return 0; def set_default_worldname(event): @@ -118,6 +152,20 @@ def set_default_worldname(event): new_worldname_entry.insert(0, old_worldname_entry.get()); return True; +def add_server(): + global proxy_process; + server_addr = server_addr_entry.get(); + server_port = server_port_entry.get(); + proxy_process = subprocess.Popen(["./mcpip.py", server_addr, server_port]); + return 0; + +def kill_proxy(): + try: + kill(proxy_process.pid, signal.SIGTERM); + except NameError: + pass; + return 0; + def play_tab(parent): global description_text; @@ -241,6 +289,37 @@ def worlds_tab(parent): buttons_frame.pack(fill=BOTH, expand=True); return tab; +def servers_tab(parent): + global server_addr_entry; + global server_port_entry; + tab = Frame(parent); + + title = Label(tab, text="Servers"); + title.config(font=("", 24)); + title.pack(); + + server_addr_frame = Frame(tab); + server_addr_text = Label(server_addr_frame, text="Server addres:"); + server_addr_text.pack(side=LEFT, anchor=N, pady=16, padx=8); + + server_addr_entry = Entry(server_addr_frame, width=32); + server_addr_entry.pack(side=RIGHT, anchor=N, pady=16, padx=8); + server_addr_frame.pack(fill=X); + + server_port_frame = Frame(tab); + server_port_text = Label(server_port_frame, text="Server port:"); + server_port_text.pack(side=LEFT, anchor=N, padx=8); + + server_port_entry = Entry(server_port_frame, width=32); + server_port_entry.pack(side=RIGHT, anchor=N, padx=8); + server_port_frame.pack(fill=X); + + buttons_frame = Frame(tab); + start_button = Button(buttons_frame, text="Add server", command=add_server); + start_button.pack(side=RIGHT, anchor=S); + buttons_frame.pack(fill=BOTH, expand=True); + return tab; + def about_tab(parent): tab = Frame(parent); @@ -248,7 +327,7 @@ def about_tab(parent): title.config(font=("", 24)); title.pack(); - version = Label(tab, text="v0.1.1"); + version = Label(tab, text="v0.2.0"); version.config(font=("", 10)); version.pack(); @@ -264,25 +343,36 @@ def about_tab(parent): return tab; def main(args): + if "arm" not in uname()[4] and "aarch" not in uname()[4]: + sys.stdout.write("Error: Minecraft Pi Launcher must run on a Raspberry Pi.\n"); + return 1; + + if geteuid() != 0: + sys.stdout.write("Error: You need to have root privileges to run this program.\n"); + return 1; + global mods_process; chdir(path.dirname(__file__)); window = Tk(); window.title("MCPI Laucher"); window.geometry("480x348"); window.resizable(False, False); - window.iconphoto(True, PhotoImage(file="install_files/icon.png")) + window.iconphoto(True, PhotoImage(file="./.install_files/icon.png")) tabs = ttk.Notebook(window); tabs.add(play_tab(tabs), text="Play"); tabs.add(settings_tab(tabs), text="Settings"); tabs.add(mods_tab(tabs), text="Mods"); tabs.add(worlds_tab(tabs), text="Worlds"); + tabs.add(servers_tab(tabs), text="Servers"); tabs.add(about_tab(tabs), text="About"); tabs.pack(fill=BOTH, expand=True); - copy2("/opt/minecraft-pi/data/images/mob/char.png", "/opt/minecraft-pi/data/images/mob/char_original.png"); - start_mods(); + if len(args) > 1: + install_mod(args[1]); + atexit.register(kill_mods); + atexit.register(kill_proxy); window.mainloop(); return 0; diff --git a/mcpim.py b/mcpim.py old mode 100644 new mode 100755 index ce819b2..ea87e65 --- a/mcpim.py +++ b/mcpim.py @@ -1,11 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# +# mcpim.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# import sys import subprocess import signal import atexit -from os import walk, kill +import zlib +import shutil +import time +from os import walk, kill, environ, remove, path, mkdir from mcpi import * def kill_mods(): @@ -15,7 +38,16 @@ def kill_mods(): i += 1; return 0; -def main(): +def main(args): + if len(args) > 1: + mod_file = open(args[1], "rb"); + mod_code = zlib.compress(mod_file.read()); + mod_file.close(); + mod_file = open(args[1].replace(".py", ".mcpi"), "wb"); + mod_file.write(mod_code); + mod_file.close(); + return 0; + global mods_processes; mods_processes = []; mod_files = []; @@ -28,13 +60,25 @@ def main(): for (_, _, files) in walk("mods"): mod_files.extend(files); while i < len(mod_files): - subprocess.Popen(["python3", "mods/" + mod_files[i]]); + if mod_files[i][-5:] == ".mcpi": + mod_file = open(mod_files[i], "rb"); + mod_code = zlib.decompress(mod_file.read()).decode("utf-8"); + mod_file.close(); + mod_name = "./mods/." + mod_files[i].replace(".mcpi", ".py"); + mod_file = open(mod_name, "w"); + mod_file.write(mod_code); + mod_file.close(); + subprocess.Popen(["python3", mod_name], env=environ); + time.sleep(5); + remove(mod_name); + else: + subprocess.Popen(["python3", "mods/" + mod_files[i]], env=environ); i += 1; atexit.register(kill_mods); j = 1; - except: + except ConnectionRefusedError: pass; return 0; if __name__ == '__main__': - sys.exit(main()); + sys.exit(main(sys.argv)); diff --git a/mcpip.py b/mcpip.py new file mode 100755 index 0000000..77ab906 --- /dev/null +++ b/mcpip.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# mcpip.py +# +# Copyright 2020 Alvarito050506 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + + +import sys +import socket + +def main(args): + ss = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP); + sc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP); + server = (args[1], int(args[2])); + client = ("0.0.0.0", 19134); + sc.setblocking(0); + ss.setblocking(0); + sc.bind(client); + client_addr = None; + + while True: + try: + data, addr = ss.recvfrom(16384); + sc.sendto(data, client_addr); + except BlockingIOError: + pass; + try: + data, addr = sc.recvfrom(16384); + client_addr = addr; + ss.sendto(data, server); + except BlockingIOError: + pass; + return 0; + +if __name__ == '__main__': + sys.exit(main(sys.argv)); diff --git a/mods/example.mcpi b/mods/example.mcpi new file mode 100644 index 0000000..6a6f838 Binary files /dev/null and b/mods/example.mcpi differ diff --git a/screenshot.png b/screenshot.png index 8f6f285..e3f2f98 100644 Binary files a/screenshot.png and b/screenshot.png differ