From 7c14c2463b8b432d4020e65b32ea301de8466878 Mon Sep 17 00:00:00 2001
From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
Date: Sat, 21 Mar 2026 15:17:18 +0000
Subject: [PATCH 01/70] Main upload with 1000 lines
---
main.py | 554 +++++++++++++--
scripts/Launcher.hta | 33 +
scripts/Launcher.ps1 | 125 ++++
scripts/Launcher.targets | 44 ++
scripts/Launcher.vbs | 178 +++++
scripts/PortableMCLoader.cs | 135 ++++
scripts/portablemc.py | 1317 +++++++++++++++++++++++++++++++++++
7 files changed, 2319 insertions(+), 67 deletions(-)
create mode 100644 scripts/Launcher.hta
create mode 100644 scripts/Launcher.ps1
create mode 100644 scripts/Launcher.targets
create mode 100644 scripts/Launcher.vbs
create mode 100644 scripts/PortableMCLoader.cs
create mode 100644 scripts/portablemc.py
diff --git a/main.py b/main.py
index c9bd6bc..9e1e0cb 100644
--- a/main.py
+++ b/main.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
-"""
+r"""
Cross‑platform bootstrap script for embedded Python 3.14 (Windows only).
-Attempts to download the native portablemc binary, falls back to pip.
+All files are stored in %LOCALAPPDATA%\PortableMC.
Run with any Python (e.g., system 3.11) to set up and launch the launcher.
"""
@@ -14,6 +14,7 @@
import shutil
import platform
import ssl
+import winreg
from pathlib import Path
# --- Windows-only guard ---
@@ -22,13 +23,33 @@
sys.exit(1)
# --- Configuration ---
-EMBEDDED_DIR = Path(__file__).parent / "python"
+# Use AppData for all downloads and runtime files
+APPDATA = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
+BASE_DIR = APPDATA / "PortableMC"
+EMBEDDED_DIR = BASE_DIR / "python"
EMBEDDED_PYTHON = EMBEDDED_DIR / "python.exe"
PYTHON_VERSION = "3.14.3"
PYTHON_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip"
GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
BASE_PACKAGES = ["flask", "flask-socketio", "psutil", "ansi2html", "certifi"]
-PORTABLEMC_BIN_DIR = Path(__file__).parent / "portablemc_bin"
+PORTABLEMC_BIN_DIR = BASE_DIR / "portablemc_bin"
+
+# Default game settings
+DEFAULT_USERNAME = "CubeUniform840"
+DEFAULT_SERVER_IP = "77.103.184.72"
+DEFAULT_JVM_OPTS = "-Xmx3G -Xms3G"
+
+# Paths
+ROOT_DIR = Path(__file__).parent
+SCRIPTS_DIR = ROOT_DIR / "Scripts"
+SCRIPTS_DIR.mkdir(exist_ok=True)
+MSBUILD_PATH = r"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe"
+
+# Launcher files inside the Scripts folder
+TARGETS_FILE = SCRIPTS_DIR / "Launcher.targets"
+LAUNCHER_VBS = SCRIPTS_DIR / "Launcher.vbs"
+LAUNCHER_PS1 = SCRIPTS_DIR / "Launcher.ps1"
+PORTABLEMC_PY = SCRIPTS_DIR / "portablemc.py"
# --- OS and architecture detection (for portablemc binary) ---
SYSTEM = platform.system().lower()
@@ -71,37 +92,123 @@ def get_portablemc_url():
filename = f"portablemc-5.0.2-{os_name}-{arch}.{ext}"
return base + filename
+# --- Data functions ---
+def prepare_user_data():
+ """Move static folder and game files to BASE_DIR if not already present."""
+ base_dir = BASE_DIR
+ root_dir = ROOT_DIR
+
+ # Static folder
+ src_static = root_dir / "static"
+ dst_static = base_dir / "static"
+ if src_static.exists() and src_static.is_dir() and not dst_static.exists():
+ try:
+ shutil.copytree(src_static, dst_static)
+ print("✅ Static folder moved to %LOCALAPPDATA%\\PortableMC\\static")
+ except Exception as e:
+ print(f"⚠️ Failed to copy static folder: {e}")
+ elif dst_static.exists():
+ print("ℹ️ Static folder already exists in %LOCALAPPDATA%\\PortableMC")
+
+ # Game files
+ for filename in ["servers.dat", "options.txt"]:
+ src = root_dir / filename
+ dst = base_dir / filename
+ if src.exists() and src.is_file() and not dst.exists():
+ try:
+ shutil.copy2(src, dst)
+ print(f"✅ {filename} moved to %LOCALAPPDATA%\\PortableMC")
+ except Exception as e:
+ print(f"⚠️ Failed to copy {filename}: {e}")
+ elif dst.exists():
+ print(f"ℹ️ {filename} already exists in %LOCALAPPDATA%\\PortableMC")
+
+# --- Junction functions ---
+def create_junction(source, target):
+ """
+ Create a junction from source to target.
+ Safely removes any existing target before creating the junction.
+ Returns True if a junction was created, False otherwise (fallback to regular directory).
+ """
+ source_path = Path(source).resolve()
+ target_path = Path(target).resolve()
+
+ # Ensure source exists (create if missing, so junction has a target)
+ source_path.mkdir(parents=True, exist_ok=True)
+
+ # Remove existing target if it exists
+ if target_path.exists():
+ if target_path.is_symlink():
+ # This is a junction (or a symlink) – remove only the junction itself
+ try:
+ os.rmdir(str(target_path)) # works for junctions
+ print(f"Removed existing junction: {target_path}")
+ except Exception as e:
+ print(f"Warning: Could not remove junction {target_path}: {e}")
+ elif target_path.is_dir():
+ # Regular directory – delete its contents but not the source folder
+ if target_path == source_path:
+ print(f"Target {target_path} is the same as source; skipping removal.")
+ else:
+ shutil.rmtree(str(target_path), ignore_errors=True)
+ print(f"Removed existing directory: {target_path}")
+ else:
+ target_path.unlink()
+
+ # Try to create junction
+ try:
+ subprocess.run(
+ ["cmd", "/c", "mklink", "/J", str(target_path), str(source_path)],
+ check=True, capture_output=True, text=True
+ )
+ print(f"✅ Junction created: {target_path} -> {source_path}")
+ return True
+ except subprocess.CalledProcessError as e:
+ print(f"⚠️ Could not create junction (falling back to regular directory): {e.stderr}")
+ target_path.mkdir(parents=True, exist_ok=True)
+ return False
+
+def ensure_junctions():
+ r"""Ensure mods and resourcepacks directories exist in %LOCALAPPDATA%\PortableMC."""
+ base_dir = BASE_DIR
+ base_dir.mkdir(parents=True, exist_ok=True)
+
+ create_junction(ROOT_DIR / "mods", base_dir / "mods")
+ create_junction(ROOT_DIR / "resourcepacks", base_dir / "resourcepacks")
+
# --- Helper functions ---
def ensure_embedded_python():
+ """Download and extract embedded Python to BASE_DIR if missing."""
+ BASE_DIR.mkdir(parents=True, exist_ok=True)
if EMBEDDED_PYTHON.exists():
print("✅ Embedded Python already exists.")
return True
print("📥 Downloading embedded Python...")
- # Try with verification first
+ zip_path = BASE_DIR / "python-embed.zip"
try:
- urllib.request.urlretrieve(PYTHON_URL, "python-embed.zip")
+ urllib.request.urlretrieve(PYTHON_URL, zip_path)
except Exception as e:
print(f"⚠️ First download attempt failed: {e}")
print("📥 Retrying with SSL verification disabled (insecure)...")
try:
- # Create an unverified SSL context
ssl_context = ssl._create_unverified_context()
with urllib.request.urlopen(PYTHON_URL, context=ssl_context) as response:
- with open("python-embed.zip", 'wb') as out_file:
+ with open(zip_path, 'wb') as out_file:
out_file.write(response.read())
except Exception as e2:
print(f"❌ Failed to download Python even with unverified SSL: {e2}")
return False
print("📦 Extracting...")
- with zipfile.ZipFile("python-embed.zip", "r") as zip_ref:
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(EMBEDDED_DIR)
- os.remove("python-embed.zip")
+ zip_path.unlink()
print("✅ Embedded Python ready.")
return True
def fix_pth_file():
+ """Enable site-packages in embedded Python's ._pth file."""
pth_files = list(EMBEDDED_DIR.glob("*._pth"))
if not pth_files:
print("❌ No ._pth file found; cannot enable site-packages.")
@@ -130,14 +237,16 @@ def test_embedded_python():
print(f"❌ Embedded Python test failed: {result.stderr}")
return False
except OSError as e:
- # Common error: WinError 1260 (group policy block)
print(f"❌ Cannot run embedded Python (blocked by group policy?): {e}")
return False
except Exception as e:
print(f"❌ Embedded Python test error: {e}")
return False
-def download_get_pip():
+def download_get_pip(python_exe=None):
+ """Download get-pip.py into the embedded Python directory."""
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
pip_script = EMBEDDED_DIR / "get-pip.py"
if pip_script.exists():
print("✅ get-pip.py already present.")
@@ -145,7 +254,6 @@ def download_get_pip():
print("📥 Downloading get-pip.py (ignoring SSL cert for this request)...")
try:
- # Create an unverified SSL context to bypass certificate errors
ssl_context = ssl._create_unverified_context()
with urllib.request.urlopen(GET_PIP_URL, context=ssl_context) as response:
with open(pip_script, 'wb') as out_file:
@@ -156,13 +264,15 @@ def download_get_pip():
return None
return pip_script
-def run_pip_command(args, isolated=True):
+def run_pip_command(args, isolated=True, python_exe=None):
+ """Run a pip command with the given Python executable."""
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
env = os.environ.copy()
if isolated:
env["PYTHONNOUSERSITE"] = "1"
env["PYTHONPATH"] = ""
- cmd = [str(EMBEDDED_PYTHON)] + args
- # SAFE: args are static or come from trusted BASE_PACKAGES
+ cmd = [str(python_exe)] + args
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ Pip command failed: {' '.join(args)}")
@@ -171,14 +281,17 @@ def run_pip_command(args, isolated=True):
print(result.stdout)
return True
-def install_pip():
- pip_script = download_get_pip()
+def install_pip(python_exe=None):
+ """Install pip into the embedded Python environment."""
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
+ pip_script = download_get_pip(python_exe)
if not pip_script:
return False
env = os.environ.copy()
env["PYTHONNOUSERSITE"] = "1"
env["PYTHONPATH"] = ""
- cmd = [str(EMBEDDED_PYTHON), str(pip_script),
+ cmd = [str(python_exe), str(pip_script),
"--trusted-host=files.pythonhosted.org",
"--trusted-host=pypi.org"]
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
@@ -189,24 +302,29 @@ def install_pip():
print("✅ pip installed.")
return True
-def install_base_packages():
+def install_base_packages(python_exe=None):
+ """Install the base packages (flask, etc.) into the given Python."""
print("📦 Installing base packages...")
- if not run_pip_command(["-m", "pip", "install", "--upgrade", "pip"], isolated=True):
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
+ if not run_pip_command(["-m", "pip", "install", "--upgrade", "pip"], isolated=True, python_exe=python_exe):
print("⚠️ Pip upgrade failed, continuing anyway.")
for pkg in BASE_PACKAGES:
print(f" Installing {pkg}...")
- if not run_pip_command(["-m", "pip", "install", pkg], isolated=True):
+ if not run_pip_command(["-m", "pip", "install", pkg], isolated=True, python_exe=python_exe):
print(f"❌ Failed to install {pkg}.")
return False
return True
-def get_certifi_path():
+def get_certifi_path(python_exe=None):
"""Return the path to certifi's CA bundle, or None if certifi not installed."""
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
try:
result = subprocess.run(
- [str(EMBEDDED_PYTHON), "-c", "import certifi; print(certifi.where())"],
+ [str(python_exe), "-c", "import certifi; print(certifi.where())"],
capture_output=True, text=True, check=True,
- env={"PYTHONNOUSERSITE": "1"} # ensure isolation
+ env={"PYTHONNOUSERSITE": "1"}
)
path = result.stdout.strip()
if path and Path(path).exists():
@@ -216,13 +334,14 @@ def get_certifi_path():
return None
def download_portablemc_binary():
+ """Download and extract the native portablemc binary into BASE_DIR."""
url = get_portablemc_url()
if not url:
return False
print(f"📥 Downloading portablemc from {url}...")
- archive_path = Path(__file__).parent / "portablemc_download"
+ BASE_DIR.mkdir(parents=True, exist_ok=True)
+ archive_path = BASE_DIR / "portablemc_download"
try:
- # Attempt with verification first
urllib.request.urlretrieve(url, archive_path)
except Exception as e:
print(f"⚠️ First download attempt failed: {e}")
@@ -249,7 +368,7 @@ def download_portablemc_binary():
return False
finally:
archive_path.unlink(missing_ok=True)
- # Move all extracted files to the root of PORTABLEMC_BIN_DIR
+ # Flatten subdirectories
for item in PORTABLEMC_BIN_DIR.iterdir():
if item.is_dir():
for sub in item.iterdir():
@@ -258,7 +377,7 @@ def download_portablemc_binary():
print(f"✅ portablemc binary extracted to {PORTABLEMC_BIN_DIR}")
return True
-def test_portablemc():
+def test_portablemc(python_exe=None):
"""Check if portablemc is available (binary or module). Returns 'binary' or 'module' or None."""
# Try binary first
exe_name = "portablemc.exe" if SYSTEM == "windows" else "portablemc"
@@ -278,10 +397,12 @@ def test_portablemc():
print("⏱️ portablemc binary check timed out or not found, trying module.")
# Try module
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
env = os.environ.copy()
env["PYTHONNOUSERSITE"] = "1"
env["PYTHONPATH"] = ""
- cmd = [str(EMBEDDED_PYTHON), "-m", "portablemc", "--help"]
+ cmd = [str(python_exe), "-m", "portablemc", "--help"]
try:
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=5)
if result.returncode == 0:
@@ -291,45 +412,133 @@ def test_portablemc():
print("⏱️ portablemc module check timed out, assuming not available.")
return None
-def ensure_portablemc():
+def ensure_portablemc(python_exe=None):
"""Make portablemc available – try binary, fallback to pip. Returns method string or None."""
- method = test_portablemc()
+ method = test_portablemc(python_exe)
if method:
return method
if download_portablemc_binary():
- method = test_portablemc()
+ method = test_portablemc(python_exe)
if method:
return method
print("⚠️ Binary download failed, falling back to pip.")
print("📦 Installing portablemc via pip...")
- if run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True):
- method = test_portablemc()
+ if run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True, python_exe=python_exe):
+ method = test_portablemc(python_exe)
if method:
return method
return None
-def launch_launcher(method):
- launcher_script = Path(__file__).parent / "portablemc.py"
+# --- System Python detection ---
+def get_system_python():
+ """Find a system Python 3.x executable, preferring 3.11 or higher.
+ Returns the path to a usable Python interpreter, or None.
+ """
+ candidates = []
+
+ # Add the current interpreter (if it's not the embedded Python we're trying to set up)
+ current = Path(sys.executable)
+ if current.exists() and current != EMBEDDED_PYTHON:
+ try:
+ result = subprocess.run([str(current), "--version"], capture_output=True, text=True, timeout=2)
+ if result.returncode == 0 and "Python 3" in result.stdout:
+ candidates.append(current)
+ except:
+ pass
+
+ # From registry (both HKCU and HKLM)
+ for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
+ try:
+ key = winreg.OpenKey(hive, r"Software\Python\PythonCore")
+ except FileNotFoundError:
+ continue
+ i = 0
+ while True:
+ try:
+ ver = winreg.EnumKey(key, i)
+ if ver.startswith("3."):
+ install_path = winreg.QueryValue(key, f"{ver}\\InstallPath")
+ candidates.append(Path(install_path) / "python.exe")
+ i += 1
+ except WindowsError:
+ break
+ winreg.CloseKey(key)
+
+ # Also check common locations (including all versions from 3.9 to 3.14)
+ versions = ["3.14", "3.13", "3.12", "3.11", "3.10", "3.9"]
+ common_paths = []
+ for ver in versions:
+ # Standard install locations
+ common_paths.append(f"C:\\Python{ver.replace('.', '')}\\python.exe")
+ common_paths.append(f"C:\\Users\\{os.environ.get('USERNAME')}\\AppData\\Local\\Programs\\Python\\Python{ver.replace('.', '')}\\python.exe")
+ common_paths.append(f"C:\\Program Files\\Python{ver.replace('.', '')}\\python.exe")
+ common_paths.append(f"C:\\Program Files (x86)\\Python{ver.replace('.', '')}\\python.exe")
+ # Add Sysnative and System32 for potential 64-bit Python from WOW64
+ common_paths.append(r"C:\Windows\Sysnative\python.exe")
+ common_paths.append(r"C:\Windows\System32\python.exe")
+
+ for p in common_paths:
+ candidates.append(Path(p))
+
+ # Also search PATH
+ for dir in os.environ.get("PATH", "").split(os.pathsep):
+ candidates.append(Path(dir) / "python.exe")
+ candidates.append(Path(dir) / "python3.exe")
+
+ # Remove duplicates and check existence and version
+ seen = set()
+ valid = []
+ for p in candidates:
+ if p.exists() and p not in seen:
+ seen.add(p)
+ try:
+ result = subprocess.run([str(p), "--version"], capture_output=True, text=True, timeout=2)
+ if result.returncode == 0 and "Python 3" in result.stdout:
+ version_str = result.stdout.strip().split()[1] # e.g., "3.11.5"
+ valid.append((version_str, p))
+ except:
+ pass
+
+ if not valid:
+ return None
+
+ # Sort by version descending (higher version first)
+ valid.sort(key=lambda x: x[0], reverse=True)
+ best = valid[0][1]
+ print(f"Selected system Python: {best}")
+ return best
+
+# --- Launcher functions ---
+def launch_launcher(method, python_exe=None):
+ """Launch portablemc.py with the given Python executable."""
+ launcher_script = PORTABLEMC_PY
if not launcher_script.exists():
print("❌ portablemc.py not found in the same directory.")
return False
+ if python_exe is None:
+ python_exe = EMBEDDED_PYTHON
+
env = os.environ.copy()
+ # Ensure paths for binaries (portablemc binary may be in PORTABLEMC_BIN_DIR)
paths = [str(EMBEDDED_DIR), str(EMBEDDED_DIR / "Scripts"), str(PORTABLEMC_BIN_DIR)]
env["PATH"] = os.pathsep.join(paths) + os.pathsep + env.get("PATH", "")
env["__compat_layer"] = "runasinvoker"
- env["PYTHONHOME"] = str(EMBEDDED_DIR)
+ env["LAUNCHER_ROOT"] = str(ROOT_DIR)
+ # For embedded Python, set PYTHONHOME; for system Python, leave it unset
+ if python_exe == EMBEDDED_PYTHON:
+ env["PYTHONHOME"] = str(EMBEDDED_DIR)
env["CLICOLOR_FORCE"] = "1"
env["PYTHONNOUSERSITE"] = "1"
env["PYTHONPATH"] = ""
env["PORTABLEMC_METHOD"] = method
- cert_path = get_certifi_path()
+ cert_path = get_certifi_path(python_exe)
if cert_path:
env["SSL_CERT_FILE"] = cert_path
env["REQUESTS_CA_BUNDLE"] = cert_path
- cmd = [str(EMBEDDED_PYTHON), str(launcher_script)]
+ cmd = [str(python_exe), str(launcher_script)]
print(f"🚀 Launching: {' '.join(cmd)}")
try:
subprocess.run(cmd, env=env, check=True)
@@ -340,40 +549,251 @@ def launch_launcher(method):
print("⏹️ Interrupted by user.")
return True
-def main():
- print("=== Embedded Python Bootstrap (Windows only) ===")
+def run_web_launcher():
+ """Attempt to use embedded Python; if blocked, fall back to system Python."""
+ print("\n=== Bootstrapping environment for web launcher ===\n")
+ # Move static folders and game files before launch
+ prepare_user_data()
+ # Try embedded Python
if not ensure_embedded_python():
- sys.exit(1)
- if not fix_pth_file():
- sys.exit(1)
- if not test_embedded_python():
- print("❌ Embedded Python cannot be executed. Please check group policy or run without embedded isolation.")
- print(" You may try to run portablemc.py directly with your system Python after installing dependencies:")
- print(" pip install flask flask-socketio psutil ansi2html portablemc")
- sys.exit(1)
+ print("⚠️ Could not set up embedded Python. Will try system Python.")
+ else:
+ if not fix_pth_file():
+ print("⚠️ Failed to fix .pth file for embedded Python.")
+ if test_embedded_python():
+ print("✅ Embedded Python is usable.")
+ # Setup pip and packages with embedded Python
+ env_check = os.environ.copy()
+ env_check["PYTHONNOUSERSITE"] = "1"
+ env_check["PYTHONPATH"] = ""
+ pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"],
+ env=env_check, capture_output=True, text=True)
+ if pip_check.returncode != 0:
+ print("📦 pip not found, installing...")
+ if not install_pip(EMBEDDED_PYTHON):
+ return False
+ else:
+ print(f"✅ pip already installed: {pip_check.stdout.strip()}")
+
+ if not install_base_packages(EMBEDDED_PYTHON):
+ return False
+
+ method = ensure_portablemc(EMBEDDED_PYTHON)
+ if not method:
+ return False
+
+ print("✅ Setup complete. Launching portablemc.py with embedded Python...")
+ return launch_launcher(method, EMBEDDED_PYTHON)
+
+ # Embedded Python failed; fall back to system Python
+ print("\n⚠️ Embedded Python not usable. Trying system Python...")
+ sys_python = get_system_python()
+ if not sys_python:
+ print("❌ No system Python found. Cannot proceed.")
+ return False
+
+ # Create a user-specific directory for Python packages (to avoid admin rights)
+ user_appdata = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
+ launcher_python_dir = user_appdata / "PythonLauncher"
+ launcher_python_dir.mkdir(exist_ok=True)
+
+ # Set up environment to use this directory for site-packages
+ env = os.environ.copy()
+ env["PYTHONUSERBASE"] = str(launcher_python_dir)
+ env["PYTHONNOUSERSITE"] = "0"
+ env["PYTHONPATH"] = ""
+ # Install packages with system Python
+ print("📦 Installing required packages with system Python...")
+ for pkg in BASE_PACKAGES + ["portablemc"]:
+ print(f" Installing {pkg}...")
+ cmd = [str(sys_python), "-m", "pip", "install", "--user", pkg]
+ result = subprocess.run(cmd, env=env, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"❌ Failed to install {pkg}: {result.stderr}")
+ return False
+ print(f" ✅ {pkg} installed.")
+
+ # Test portablemc with system Python
+ method = test_portablemc(sys_python)
+ if not method:
+ print("❌ portablemc not available even after installation.")
+ return False
- # Check pip
- env_check = os.environ.copy()
- env_check["PYTHONNOUSERSITE"] = "1"
- env_check["PYTHONPATH"] = ""
- pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"],
- env=env_check, capture_output=True, text=True)
- if pip_check.returncode != 0:
- print("📦 pip not found, installing...")
- if not install_pip():
- sys.exit(1)
+ print("✅ Setup complete. Launching portablemc.py with system Python...")
+ # Launch portablemc.py with system Python, using the same environment
+ return launch_launcher(method, sys_python)
+
+def run_msbuild_launcher():
+ print("\n=== Launching via MSBuild ===\n")
+ # Move static folders and game files before launch
+ prepare_user_data()
+ if not os.path.isfile(MSBUILD_PATH):
+ print(f"❌ MSBuild not found at {MSBUILD_PATH}")
+ return False
+ if not TARGETS_FILE.exists():
+ print(f"❌ Launcher.targets not found at {TARGETS_FILE}")
+ return False
+
+ env = os.environ.copy()
+ env["__COMPAT_LAYER"] = "RUNASINVOKER"
+
+ cmd = [
+ MSBUILD_PATH,
+ str(TARGETS_FILE),
+ f"/p:Username={DEFAULT_USERNAME}",
+ f"/p:ServerIp={DEFAULT_SERVER_IP}",
+ f'/p:JvmOpts="{DEFAULT_JVM_OPTS}"'
+ ]
+ print(f"Executing: {' '.join(cmd)}")
+ try:
+ subprocess.run(cmd, env=env, check=True)
+ except subprocess.CalledProcessError as e:
+ print(f"❌ MSBuild failed with exit code {e.returncode}")
+ return False
+ except KeyboardInterrupt:
+ print("⏹️ Interrupted by user.")
+ return True
+
+def run_cli_launcher():
+ """Launch portablemc in CLI mode using the embedded Python (or system Python fallback)."""
+ print("\n=== Bootstrapping environment for CLI launcher ===\n")
+ # Move static folders and game files before launch
+ prepare_user_data()
+ # Try embedded Python first
+ if not ensure_embedded_python():
+ print("⚠️ Could not set up embedded Python. Will try system Python.")
else:
- print(f"✅ pip already installed: {pip_check.stdout.strip()}")
+ if not fix_pth_file():
+ print("⚠️ Failed to fix .pth file for embedded Python.")
+ if test_embedded_python():
+ # Setup embedded Python (pip + portablemc only)
+ env_check = os.environ.copy()
+ env_check["PYTHONNOUSERSITE"] = "1"
+ env_check["PYTHONPATH"] = ""
+ pip_check = subprocess.run([str(EMBEDDED_PYTHON), "-m", "pip", "--version"],
+ env=env_check, capture_output=True, text=True)
+ if pip_check.returncode != 0:
+ print("📦 pip not found, installing...")
+ if not install_pip(EMBEDDED_PYTHON):
+ return False
+ else:
+ print(f"✅ pip already installed: {pip_check.stdout.strip()}")
+
+ print("📦 Installing portablemc via pip...")
+ if not run_pip_command(["-m", "pip", "install", "portablemc"], isolated=True, python_exe=EMBEDDED_PYTHON):
+ print("❌ Failed to install portablemc.")
+ return False
+
+ method = test_portablemc(EMBEDDED_PYTHON)
+ if not method:
+ return False
+
+ # Create Junctions for mods and resourcepacks
+ ensure_junctions()
+
+ # Build CLI arguments (module syntax)
+ cmd = [
+ str(EMBEDDED_PYTHON), "-m", "portablemc",
+ "--main-dir", ".",
+ "start",
+ "--server", DEFAULT_SERVER_IP,
+ "--jvm-args", DEFAULT_JVM_OPTS,
+ "fabric:",
+ "-u", DEFAULT_USERNAME
+ ]
+ env = os.environ.copy()
+ env["__COMPAT_LAYER"] = "RUNASINVOKER"
+ env["PYTHONNOUSERSITE"] = "1"
+ env["PYTHONPATH"] = ""
+ env["LAUNCHER_ROOT"] = str(ROOT_DIR) # for completeness
+ print(f"🚀 Launching: {' '.join(cmd)}")
+ try:
+ subprocess.run(cmd, env=env, cwd=BASE_DIR, check=True)
+ except subprocess.CalledProcessError as e:
+ print(f"❌ CLI launcher exited with error: {e}")
+ return False
+ except KeyboardInterrupt:
+ print("⏹️ Interrupted by user.")
+ return True
- if not install_base_packages():
- sys.exit(1)
+ # Fallback to system Python
+ sys_python = get_system_python()
+ if not sys_python:
+ print("❌ No system Python found. Cannot proceed.")
+ return False
- method = ensure_portablemc()
+ user_appdata = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
+ launcher_python_dir = user_appdata / "PythonLauncher"
+ launcher_python_dir.mkdir(exist_ok=True)
+
+ env = os.environ.copy()
+ env["PYTHONUSERBASE"] = str(launcher_python_dir)
+ env["PYTHONNOUSERSITE"] = "0"
+ env["PYTHONPATH"] = ""
+
+ # Install portablemc with system Python
+ print("📦 Installing portablemc with system Python...")
+ cmd = [str(sys_python), "-m", "pip", "install", "--user", "portablemc"]
+ result = subprocess.run(cmd, env=env, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"❌ Failed to install portablemc: {result.stderr}")
+ return False
+
+ method = test_portablemc(sys_python)
if not method:
+ print("❌ portablemc not available after installation.")
+ return False
+
+ # Create Junctions for mods and resourcepacks
+ ensure_junctions()
+
+ # Build CLI arguments (module syntax)
+ cmd = [
+ str(sys_python), "-m", "portablemc",
+ "--main-dir", ".",
+ "start",
+ "--server", DEFAULT_SERVER_IP,
+ "--jvm-args", DEFAULT_JVM_OPTS,
+ "fabric:",
+ "-u", DEFAULT_USERNAME
+ ]
+ env["__COMPAT_LAYER"] = "RUNASINVOKER"
+ env["LAUNCHER_ROOT"] = str(ROOT_DIR)
+ print(f"🚀 Launching: {' '.join(cmd)}")
+ try:
+ subprocess.run(cmd, env=env, cwd=BASE_DIR, check=True)
+ except subprocess.CalledProcessError as e:
+ print(f"❌ CLI launcher exited with error: {e}")
+ return False
+ except KeyboardInterrupt:
+ print("⏹️ Interrupted by user.")
+ return True
+
+def main():
+ # Display menu
+ print("\n" + "=" * 60)
+ print(" Minecraft Launcher – Choose a Method")
+ print("=" * 60)
+ print("1) Web launcher (portablemc.py) – requires setup, runs in browser")
+ print("2) MSBuild launcher – uses Launcher.targets (Microsoft‑signed binaries)")
+ print("3) CLI launcher – runs portablemc directly in terminal")
+ print("q) Quit")
+ choice = input("\nEnter choice (1/2/3/q): ").strip()
+
+ if choice == "1":
+ success = run_web_launcher()
+ elif choice == "2":
+ success = run_msbuild_launcher()
+ elif choice == "3":
+ success = run_cli_launcher()
+ elif choice.lower() == "q":
+ print("Exiting.")
+ sys.exit(0)
+ else:
+ print("Invalid choice. Exiting.")
sys.exit(1)
- print("✅ Setup complete. Launching portablemc.py...")
- launch_launcher(method)
+ sys.exit(0 if success else 1)
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file
diff --git a/scripts/Launcher.hta b/scripts/Launcher.hta
new file mode 100644
index 0000000..f6e0f0c
--- /dev/null
+++ b/scripts/Launcher.hta
@@ -0,0 +1,33 @@
+
+
+
+PortableMC Launcher Test
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/Launcher.ps1 b/scripts/Launcher.ps1
new file mode 100644
index 0000000..b57b72b
--- /dev/null
+++ b/scripts/Launcher.ps1
@@ -0,0 +1,125 @@
+$env:__COMPAT_LAYER = "RUNASINVOKER"
+
+param(
+ [string]$Username,
+ [string]$ServerIp,
+ [string]$JvmOpts
+)
+
+# Fallback to positional arguments if needed
+if (-not $JvmOpts -and $args.Count -ge 0) {
+ $JvmOpts = $args[0]
+}
+
+Write-Host "DEBUG: Username = '$Username'"
+Write-Host "DEBUG: ServerIp = '$ServerIp'"
+Write-Host "DEBUG: JvmOpts = '$JvmOpts'"
+if (-not $JvmOpts) {
+ Write-Host "ERROR: JvmOpts is empty. Cannot proceed."
+ exit 1
+}
+
+# Determine script location and base directories
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$rootDir = Split-Path -Parent $scriptDir
+$baseDir = Join-Path $env:LOCALAPPDATA "PortableMC"
+$binDir = Join-Path $baseDir "portablemc_bin"
+$exePath = Join-Path $binDir "portablemc.exe"
+
+# --- Function to create a junction, removing any existing folder/link first ---
+function Ensure-Junction {
+ param(
+ [string]$Source,
+ [string]$Target
+ )
+ # Create source folder if it doesn't exist (so junction has a target)
+ if (-not (Test-Path $Source)) {
+ New-Item -ItemType Directory -Path $Source -Force | Out-Null
+ Write-Host "Created source folder: $Source"
+ }
+ # If target already exists, remove it (whether it's a folder or a junction)
+ if (Test-Path $Target) {
+ Remove-Item -Path $Target -Recurse -Force
+ Write-Host "Removed existing target: $Target"
+ }
+ Write-Host "Creating junction: $Target -> $Source"
+ cmd /c mklink /J "$Target" "$Source" 2>&1 | Out-Null
+ if ($LASTEXITCODE -eq 0) {
+ Write-Host "Junction created successfully."
+ } else {
+ Write-Host "Failed to create junction. Ensure source exists and you're on the same drive."
+ }
+}
+
+# --- Ensure the base directory exists ---
+if (-not (Test-Path $baseDir)) {
+ New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
+}
+
+# --- Download portablemc.exe if missing (with SSL fallback) ---
+if (-not (Test-Path $exePath)) {
+ Write-Host "portablemc.exe not found. Downloading to $binDir..."
+ $url = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/portablemc-5.0.2-windows-x86_64-msvc.zip"
+ $zipPath = Join-Path $baseDir "portablemc.zip"
+ try {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
+ } catch {
+ Write-Host "First download attempt failed: $_"
+ Write-Host "Retrying with SSL verification disabled..."
+ try {
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
+ Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
+ } catch {
+ Write-Host "Download failed. Exiting."
+ exit 1
+ }
+ }
+ Write-Host "Extracting..."
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $binDir)
+ Remove-Item $zipPath
+
+ # Flatten subdirectories
+ Get-ChildItem $binDir -Directory | ForEach-Object {
+ Get-ChildItem $_.FullName -File | Move-Item -Destination $binDir -Force
+ Remove-Item $_.FullName -Recurse
+ }
+}
+
+# --- Create junctions for mods and resourcepacks (force recreation) ---
+Ensure-Junction -Source (Join-Path $rootDir "mods") -Target (Join-Path $baseDir "mods")
+Ensure-Junction -Source (Join-Path $rootDir "resourcepacks") -Target (Join-Path $baseDir "resourcepacks")
+
+# --- Process JVM options and launch ---
+$jvmArgs = $JvmOpts -replace ' ', ','
+$arguments = "--main-dir . start --join-server $ServerIp --jvm-arg=$jvmArgs fabric: -u $Username"
+
+Write-Host "Launching: $exePath $arguments"
+Write-Host "Working directory: $baseDir"
+
+$env:__COMPAT_LAYER = "RUNASINVOKER"
+
+$tempOut = Join-Path $env:TEMP "portablemc_out.txt"
+$tempErr = Join-Path $env:TEMP "portablemc_err.txt"
+$process = Start-Process -FilePath $exePath -ArgumentList $arguments -WorkingDirectory $baseDir -NoNewWindow -PassThru -RedirectStandardOutput $tempOut -RedirectStandardError $tempErr
+
+$timeout = 30
+$elapsed = 0
+while ($elapsed -lt $timeout) {
+ if ($process.HasExited) { break }
+ Start-Sleep -Seconds 1
+ $elapsed++
+}
+
+if ($process.HasExited) {
+ Write-Host "Process exited with code $($process.ExitCode)"
+ if (Test-Path $tempOut) { Get-Content $tempOut }
+ if (Test-Path $tempErr) { Get-Content $tempErr }
+ Remove-Item $tempOut, $tempErr -Force -ErrorAction SilentlyContinue
+ exit $process.ExitCode
+} else {
+ Write-Host "Process still running after $timeout seconds detaching."
+ Remove-Item $tempOut, $tempErr -Force -ErrorAction SilentlyContinue
+ exit 0
+}
\ No newline at end of file
diff --git a/scripts/Launcher.targets b/scripts/Launcher.targets
new file mode 100644
index 0000000..4b22e40
--- /dev/null
+++ b/scripts/Launcher.targets
@@ -0,0 +1,44 @@
+
+
+
+ $(TEMP)
+ $(TempDir)\PortableMCLoader.cs
+ $(TempDir)\PortableMCLoader.exe
+ $(MSBuildProjectDirectory)\PortableMCLoader.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/Launcher.vbs b/scripts/Launcher.vbs
new file mode 100644
index 0000000..d94a239
--- /dev/null
+++ b/scripts/Launcher.vbs
@@ -0,0 +1,178 @@
+' Launcher.vbs
+' Usage: cscript Launcher.vbs
+' Stores all files in %LOCALAPPDATA%\PortableMC and forces junction recreation for mods/resourcepacks.
+
+Set args = WScript.Arguments
+If args.Count < 3 Then
+ WScript.Echo "Usage: cscript Launcher.vbs Username ServerIp JvmOpts"
+ WScript.Quit 1
+End If
+
+username = args(0)
+serverIp = args(1)
+jvmOpts = args(2)
+
+Set fso = CreateObject("Scripting.FileSystemObject")
+Set shell = CreateObject("WScript.Shell")
+shell.Environment("PROCESS")("__COMPAT_LAYER") = "RUNASINVOKER"
+
+' --- Get script directory ---
+scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
+' Root directory is one level up
+rootDir = fso.GetParentFolderName(scriptDir)
+
+' --- Base directory in %LOCALAPPDATA% ---
+localAppData = shell.ExpandEnvironmentStrings("%LOCALAPPDATA%")
+baseDir = fso.BuildPath(localAppData, "PortableMC")
+If Not fso.FolderExists(baseDir) Then fso.CreateFolder(baseDir)
+
+binDir = fso.BuildPath(baseDir, "portablemc_bin")
+exePath = fso.BuildPath(binDir, "portablemc.exe")
+
+' --- Helper to create a junction, removing any existing folder/link first ---
+Sub CreateJunction(source, target)
+ ' Ensure source folder exists (create if missing)
+ If Not fso.FolderExists(source) Then fso.CreateFolder(source)
+ ' If target already exists, delete it (whether folder or junction)
+ If fso.FolderExists(target) Then
+ fso.DeleteFolder target, True
+ WScript.Echo "Removed existing target: " & target
+ End If
+ WScript.Echo "Creating junction: " & target & " -> " & source
+ ' Use cmd /c mklink /J (target must not exist before)
+ cmd = "cmd /c mklink /J """ & target & """ """ & source & """"
+ shell.Run cmd, 0, True
+ ' Junction creation doesn't set exit code easily; assume success if target now exists
+ If fso.FolderExists(target) Then
+ WScript.Echo "Junction created successfully."
+ Else
+ WScript.Echo "Failed to create junction. Ensure source exists and you're on the same drive."
+ End If
+End Sub
+
+' --- Download function with SSL fallback ---
+Function DownloadFile(url, destPath)
+ On Error Resume Next
+ Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
+ http.Open "GET", url, False
+ http.SetTimeouts 10000, 10000, 10000, 10000
+ http.Send
+ If Err.Number = 0 And http.Status = 200 Then
+ Dim binaryData
+ binaryData = http.ResponseBody
+ Set stream = CreateObject("ADODB.Stream")
+ stream.Type = 1
+ stream.Open
+ stream.Write binaryData
+ stream.SaveToFile destPath, 2
+ stream.Close
+ DownloadFile = True
+ Exit Function
+ End If
+ ' Fallback to XMLHTTP with SSL ignore
+ Set xmlHttp = CreateObject("MSXML2.ServerXMLHTTP.6.0")
+ xmlHttp.setOption 2, 13056
+ xmlHttp.Open "GET", url, False
+ xmlHttp.Send
+ If Err.Number = 0 And xmlHttp.Status = 200 Then
+ Set stream = CreateObject("ADODB.Stream")
+ stream.Type = 1
+ stream.Open
+ stream.Write xmlHttp.ResponseBody
+ stream.SaveToFile destPath, 2
+ stream.Close
+ DownloadFile = True
+ Else
+ WScript.Echo "Download failed: " & Err.Description
+ DownloadFile = False
+ End If
+ On Error Goto 0
+End Function
+
+Sub ExtractZip(zipPath, destDir)
+ If Not fso.FolderExists(destDir) Then fso.CreateFolder(destDir)
+ Set zip = CreateObject("Shell.Application").NameSpace(zipPath)
+ Set dest = CreateObject("Shell.Application").NameSpace(destDir)
+ dest.CopyHere zip.Items, 284
+ WScript.Sleep 5000
+End Sub
+
+' --- Main download and extraction ---
+If Not fso.FileExists(exePath) Then
+ WScript.Echo "portablemc.exe not found. Downloading..."
+ url = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/portablemc-5.0.2-windows-x86_64-msvc.zip"
+ zipPath = fso.BuildPath(baseDir, "portablemc.zip")
+ If Not DownloadFile(url, zipPath) Then
+ WScript.Echo "Download failed. Exiting."
+ WScript.Quit 1
+ End If
+ Set file = fso.GetFile(zipPath)
+ WScript.Echo "Download complete. Size: " & file.Size & " bytes"
+ WScript.Echo "Extracting to: " & binDir
+ ExtractZip zipPath, binDir
+ fso.DeleteFile zipPath, True
+
+ ' Flatten subdirectories
+ If fso.FolderExists(binDir) Then
+ Set folder = fso.GetFolder(binDir)
+ For Each subFolder In folder.SubFolders
+ For Each file In subFolder.Files
+ destFile = fso.BuildPath(binDir, file.Name)
+ If fso.FileExists(destFile) Then fso.DeleteFile destFile, True
+ file.Move destFile
+ Next
+ subFolder.Delete True
+ Next
+ End If
+End If
+
+If Not fso.FileExists(exePath) Then
+ WScript.Echo "ERROR: portablemc.exe still not found at " & exePath
+ WScript.Quit 1
+End If
+
+' --- Create junctions for mods and resourcepacks (force recreation) ---
+CreateJunction fso.BuildPath(rootDir, "mods"), fso.BuildPath(baseDir, "mods")
+CreateJunction fso.BuildPath(rootDir, "resourcepacks"), fso.BuildPath(baseDir, "resourcepacks")
+
+' --- Build command line ---
+jvmArgs = Replace(jvmOpts, " ", ",")
+arguments = "--main-dir . start --join-server " & serverIp & " --jvm-arg=" & jvmArgs & " fabric: -u " & username
+
+WScript.Echo "Launching: " & exePath & " " & arguments
+WScript.Echo "Working directory: " & baseDir
+
+' Set environment variable to run as invoker
+shell.Environment("PROCESS")("__COMPAT_LAYER") = "RUNASINVOKER"
+
+' Change to base directory and launch
+shell.CurrentDirectory = baseDir
+Set proc = shell.Exec("""" & exePath & """ " & arguments)
+
+' Wait up to 30 seconds
+Dim startTime, line
+startTime = Timer
+Do While Timer - startTime < 30
+ While Not proc.StdOut.AtEndOfStream
+ WScript.StdOut.WriteLine proc.StdOut.ReadLine
+ Wend
+ While Not proc.StdErr.AtEndOfStream
+ WScript.StdErr.WriteLine proc.StdErr.ReadLine
+ Wend
+ If proc.Status <> 0 Then Exit Do
+ WScript.Sleep 200
+Loop
+
+If proc.Status = 0 Then
+ WScript.Echo "Process still running – detaching."
+ WScript.Quit 0
+Else
+ While Not proc.StdOut.AtEndOfStream
+ WScript.StdOut.WriteLine proc.StdOut.ReadLine
+ Wend
+ While Not proc.StdErr.AtEndOfStream
+ WScript.StdErr.WriteLine proc.StdErr.ReadLine
+ Wend
+ WScript.Echo "Process exited with code " & proc.ExitCode
+ WScript.Quit proc.ExitCode
+End If
\ No newline at end of file
diff --git a/scripts/PortableMCLoader.cs b/scripts/PortableMCLoader.cs
new file mode 100644
index 0000000..0806e5c
--- /dev/null
+++ b/scripts/PortableMCLoader.cs
@@ -0,0 +1,135 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Diagnostics;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ if (args.Length < 3)
+ {
+ Console.WriteLine("Usage: PortableMCLoader.exe Username ServerIp JvmOpts");
+ Environment.Exit(1);
+ }
+ string username = args[0];
+ string serverIp = args[1];
+ string jvmOpts = args[2];
+
+ string baseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PortableMC");
+ Directory.CreateDirectory(baseDir);
+ string binDir = Path.Combine(baseDir, "portablemc_bin");
+ string exePath = Path.Combine(binDir, "portablemc.exe");
+
+ if (!File.Exists(exePath))
+ {
+ Console.WriteLine("portablemc.exe not found. Downloading...");
+ string url = "https://github.com/mindstorm38/portablemc/releases/download/v5.0.2/portablemc-5.0.2-windows-x86_64-msvc.zip";
+ string zipPath = Path.Combine(baseDir, "portablemc.zip");
+ try
+ {
+ using (WebClient client = new WebClient())
+ {
+ client.DownloadFile(url, zipPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Download failed: " + ex.Message);
+ Console.WriteLine("Retrying with SSL disabled...");
+ try
+ {
+ ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true;
+ using (WebClient client = new WebClient())
+ {
+ client.DownloadFile(url, zipPath);
+ }
+ }
+ catch (Exception ex2)
+ {
+ Console.WriteLine("Download failed even with SSL disabled: " + ex2.Message);
+ Environment.Exit(1);
+ }
+ }
+
+ Console.WriteLine("Extracting...");
+ ZipFile.ExtractToDirectory(zipPath, binDir);
+ File.Delete(zipPath);
+ foreach (string dir in Directory.GetDirectories(binDir))
+ {
+ foreach (string file in Directory.GetFiles(dir))
+ {
+ string dest = Path.Combine(binDir, Path.GetFileName(file));
+ if (File.Exists(dest)) File.Delete(dest);
+ File.Move(file, dest);
+ }
+ Directory.Delete(dir, true);
+ }
+ Console.WriteLine("Extraction complete.");
+ }
+
+ string scriptDir = Directory.GetCurrentDirectory();
+ CreateJunction(Path.Combine(scriptDir, "mods"), Path.Combine(baseDir, "mods"));
+ CreateJunction(Path.Combine(scriptDir, "resourcepacks"), Path.Combine(baseDir, "resourcepacks"));
+
+ string jvmArgs = jvmOpts.Replace(' ', ',');
+ string arguments = string.Format("--main-dir . start --join-server {0} --jvm-arg={1} fabric: -u {2}",
+ serverIp, jvmArgs, username);
+
+ Console.WriteLine(string.Format("Launching: {0} {1}", exePath, arguments));
+ Console.WriteLine(string.Format("Working directory: {0}", baseDir));
+
+ Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RUNASINVOKER");
+
+ ProcessStartInfo psi = new ProcessStartInfo
+ {
+ FileName = exePath,
+ Arguments = arguments,
+ WorkingDirectory = baseDir,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using (Process proc = Process.Start(psi))
+ {
+ proc.OutputDataReceived += (s, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
+ proc.ErrorDataReceived += (s, e) => { if (e.Data != null) Console.Error.WriteLine(e.Data); };
+ proc.BeginOutputReadLine();
+ proc.BeginErrorReadLine();
+
+ if (proc.WaitForExit(30000))
+ {
+ Console.WriteLine(string.Format("Process exited with code {0}", proc.ExitCode));
+ Environment.Exit(proc.ExitCode);
+ }
+ else
+ {
+ Console.WriteLine("Process still running – detaching.");
+ Environment.Exit(0);
+ }
+ }
+ }
+
+ static void CreateJunction(string source, string target)
+ {
+ if (!Directory.Exists(source))
+ Directory.CreateDirectory(source);
+
+ if (Directory.Exists(target))
+ {
+ Directory.Delete(target, true);
+ Console.WriteLine(string.Format("Removed existing target: {0}", target));
+ }
+
+ Console.WriteLine(string.Format("Creating junction: {0} -> {1}", target, source));
+ Process.Start("cmd", string.Format("/c mklink /J \"{0}\" \"{1}\"", target, source)).WaitForExit();
+
+ if (Directory.Exists(target))
+ Console.WriteLine("Junction created successfully.");
+ else
+ Console.WriteLine("Failed to create junction.");
+ }
+}
\ No newline at end of file
diff --git a/scripts/portablemc.py b/scripts/portablemc.py
new file mode 100644
index 0000000..4b73d2e
--- /dev/null
+++ b/scripts/portablemc.py
@@ -0,0 +1,1317 @@
+from pathlib import Path
+from flask import Flask, request, render_template_string, jsonify, Response
+from flask_socketio import SocketIO, emit
+import subprocess
+import sys
+import socket
+import time
+import re
+import signal
+import shutil
+import threading
+import logging
+import os
+import json
+import importlib.util
+
+# Determine base directory (where Minecraft files are stored)
+APPDATA = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
+BASE_DIR = APPDATA / "PortableMC"
+BASE_DIR.mkdir(parents=True, exist_ok=True)
+
+# Flask app with static folder in BASE_DIR
+app = Flask(__name__, static_folder=str(BASE_DIR / "static"))
+socketio = SocketIO(app, cors_allowed_origins="*")
+
+def escape_html(s):
+ """Escape only &, <, > for safe innerHTML – leaves quotes and apostrophes untouched."""
+ return s.replace('&', '&').replace('<', '<').replace('>', '>')
+
+# --- CONFIGURATION ---
+VALID_USERNAME_REGEX = re.compile(r'^[a-zA-Z0-9_]{3,16}$')
+FORBIDDEN_LIST = ["CubeUniform840", "Admin", "Owner"]
+PASS_KEY = "1234"
+SERVER_IP = "77.103.184.72"
+# SERVER_IP = "eu.chickencraft.nl"
+JVM_OPTS = "-Xmx3G -Xms3G -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M -XX:+AlwaysPreTouch -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC"
+
+# Thread-safe process tracking
+active_processes = {}
+processes_lock = threading.Lock()
+
+# Track connected clients
+connected_clients = 0
+clients_lock = threading.Lock()
+logging.basicConfig(level=logging.INFO)
+
+# Path of root of Launcher
+LAUNCHER_ROOT = os.environ.get("LAUNCHER_ROOT")
+if LAUNCHER_ROOT is None:
+ LAUNCHER_ROOT = str(Path(__file__).parent.parent)
+ROOT_DIR = Path(LAUNCHER_ROOT)
+
+# Optional psutil for process tree killing
+try:
+ import psutil
+ PSUTIL_AVAILABLE = True
+except ImportError:
+ psutil = None
+ PSUTIL_AVAILABLE = False
+ logging.warning(
+ "psutil not installed; process tree killing is limited. "
+ "Install psutil for full cleanup."
+ )
+
+# Fallbacks when certain libraries are restricted/corrupted/disabled
+try:
+ from ansi2html import Ansi2HTMLConverter
+ ansi_converter = Ansi2HTMLConverter(dark_bg=True, inline=True)
+ use_server_conversion = True
+except ImportError:
+ # ansi2html not installed – fallback to raw ANSI
+ use_server_conversion = False
+ logging.warning("ansi2html not installed; client will handle ANSI conversion.")
+
+# --- HTML TEMPLATE (with 4-space indented JavaScript) ---
+HTML_TEMPLATE = r"""
+
+
+
+
+ Minecraft - Java Edition
+
+
+
+
+
+
+
+ Ambient Mode
+
+
+
+

+
+
+
+
Minecraft Java 1.21.11
+
+
+ Flask: Connected
+
+ {% if error %}
{{ error }}
{% endif %}
+
+
+
+
+
+
SYSTEM_CORE // LIVE_LOGS
+
+
+
+
+ PORTABLE_MC // APEX_V6.5
+
+
+
+
+
+
+
+
+
+
+"""
+
+# --- JUNCTIONS ---
+def create_junction(source, target):
+ """
+ Create a junction from source to target.
+ Safely removes any existing target before creating the junction.
+ Returns True if a junction was created, False otherwise (fallback to regular directory).
+ """
+ source_path = Path(source).resolve()
+ target_path = Path(target).resolve()
+
+ # Ensure source exists (create if missing, so junction has a target)
+ source_path.mkdir(parents=True, exist_ok=True)
+
+ # Remove existing target if it exists
+ if target_path.exists():
+ if target_path.is_symlink():
+ # This is a junction (or a symlink) – remove only the junction itself
+ try:
+ os.rmdir(str(target_path)) # works for junctions
+ print(f"Removed existing junction: {target_path}")
+ except Exception as e:
+ print(f"Warning: Could not remove junction {target_path}: {e}")
+ elif target_path.is_dir():
+ # Regular directory – delete its contents but not the source folder
+ if target_path == source_path:
+ print(f"Target {target_path} is the same as source; skipping removal.")
+ else:
+ shutil.rmtree(str(target_path), ignore_errors=True)
+ print(f"Removed existing directory: {target_path}")
+ else:
+ target_path.unlink()
+
+ # Try to create junction
+ try:
+ subprocess.run(
+ ["cmd", "/c", "mklink", "/J", str(target_path), str(source_path)],
+ check=True, capture_output=True, text=True
+ )
+ print(f"✅ Junction created: {target_path} -> {source_path}")
+ return True
+ except subprocess.CalledProcessError as e:
+ print(f"⚠️ Could not create junction (falling back to regular directory): {e.stderr}")
+ target_path.mkdir(parents=True, exist_ok=True)
+ return False
+
+def ensure_junctions():
+ r"""Ensure mods and resourcepacks directories exist in %LOCALAPPDATA%\PortableMC."""
+ base_dir = BASE_DIR
+ base_dir.mkdir(parents=True, exist_ok=True)
+
+ create_junction(ROOT_DIR / "mods", base_dir / "mods")
+ create_junction(ROOT_DIR / "resourcepacks", base_dir / "resourcepacks")
+
+# --- ROUTES ---
+@socketio.on('connect')
+def handle_connect():
+ global connected_clients
+ with clients_lock:
+ connected_clients += 1
+ client_id = request.sid
+ print(f'Client connected: {client_id} (Total: {connected_clients})')
+ emit('status', {'core': 'online', 'minecraft': 'checking'})
+
+@socketio.on('disconnect')
+def handle_disconnect():
+ global connected_clients
+ with clients_lock:
+ connected_clients -= 1
+ client_id = request.sid
+ print(f'Client disconnected: {client_id} (Total: {connected_clients})')
+
+@socketio.on('ping_minecraft')
+def handle_ping_minecraft():
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(1.5)
+ s.connect((SERVER_IP, 25565))
+ s.close()
+ emit('minecraft_status', {'online': True})
+ except Exception:
+ emit('minecraft_status', {'online': False})
+
+@app.route("/ping")
+def ping():
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(1.5)
+ s.connect((SERVER_IP, 25565))
+ s.close()
+ return jsonify(online=True)
+ except Exception:
+ return jsonify(online=False), 200
+
+@app.route("/stream")
+def stream():
+ # --- Create junctions for mods and resourcepacks (like .vbs/.ps1) ---
+ ensure_junctions() # This will create junctions in %LOCALAPPDATA%\PortableMC
+
+ # --- PORTABLEMC AVAILABILITY CHECK ---
+ def is_portablemc_available():
+ if shutil.which("portablemc"):
+ return True
+ return importlib.util.find_spec("portablemc") is not None
+
+ if not is_portablemc_available():
+ def error_gen():
+ lines = [
+ "\x1b[91m[!] PORTABLEMC NOT FOUND\x1b[0m",
+ "\x1b[93mPlease install it via 'pip install portablemc'.\x1b[0m"
+ ]
+ for line in lines:
+ payload = json.dumps({'type': 'ansi', 'content': line})
+ yield f"data: {payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return Response(error_gen(), mimetype="text/event-stream")
+
+ # --- GET USER INPUT ---
+ user = request.args.get("username", "Player").strip()
+ password = request.args.get("password", "")
+
+ # --- VALIDATIONS (JSON wrapped) ---
+ if not user:
+ def error_gen():
+ payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] USERNAME REQUIRED\x1b[0m"})
+ yield f"data: {payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return Response(error_gen(), mimetype="text/event-stream")
+
+ if not VALID_USERNAME_REGEX.match(user):
+ def error_gen():
+ lines = [
+ "\x1b[91m[!] INVALID USERNAME\x1b[0m",
+ "\x1b[93mUsername must be 3-16 characters and only letters, numbers, or underscore.\x1b[0m"
+ ]
+ for line in lines:
+ payload = json.dumps({'type': 'ansi', 'content': line})
+ yield f"data: {payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return Response(error_gen(), mimetype="text/event-stream")
+
+ user_lower = user.lower()
+ forbidden_lower = [name.lower() for name in FORBIDDEN_LIST]
+ if user_lower in forbidden_lower and password != PASS_KEY:
+ def error_gen():
+ payload = json.dumps({'type': 'ansi', 'content': "\x1b[91m[!] ACCESS DENIED – INVALID SECURE_KEY\x1b[0m"})
+ yield f"data: {payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return Response(error_gen(), mimetype="text/event-stream")
+
+ # --- PREVENT MULTIPLE LAUNCHES (thread-safe) ---
+ with processes_lock:
+ if user in active_processes and active_processes[user].poll() is None:
+ lines = [
+ "\x1b[91m[!] CORE BUSY\x1b[0m",
+ "\x1b[93mAnother Minecraft instance is already running.\x1b[0m",
+ "\x1b[90mPlease close the game before launching again.\x1b[0m"
+ ]
+ def error_gen():
+ for line in lines:
+ payload = json.dumps({'type': 'ansi', 'content': line})
+ yield f"data: {payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return Response(error_gen(), mimetype="text/event-stream")
+ if user in active_processes:
+ del active_processes[user]
+
+ # --- BUILD COMMAND (with binary vs module adaptation) ---
+ using_binary = os.environ.get("PORTABLEMC_METHOD") == "binary"
+
+ # Launcher command – tied directly to the method, not PATH
+ if using_binary:
+ launcher_cmd = ["portablemc"]
+ else:
+ launcher_cmd = [sys.executable, "-m", "portablemc"]
+
+ # Determine base directory for portablemc data (same as the launcher uses)
+ local_appdata = os.environ.get("LOCALAPPDATA", os.path.join(os.path.expanduser("~"), "AppData", "Local"))
+ base_dir = os.path.join(local_appdata, "PortableMC")
+
+ # Global arguments (before 'start')
+ global_args = ["--main-dir", base_dir]
+ if not using_binary:
+ # Python module supports these extras
+ global_args.extend(["--timeout", "60", "--output", "human-color"])
+
+ # Start‑specific arguments
+ start_args = []
+ if using_binary:
+ # Binary uses --join-server
+ start_args.extend(["--join-server", SERVER_IP])
+ else:
+ # Python module uses --server
+ start_args.extend(["--server", SERVER_IP])
+
+ # JVM arguments
+ if using_binary:
+ # Binary expects --jvm-arg=OPT1,OPT2,OPT3
+ jvm_opts_combined = ",".join(JVM_OPTS.split())
+ start_args.append(f"--jvm-arg={jvm_opts_combined}")
+ else:
+ # Python module accepts a single --jvm-args string
+ start_args.extend(["--jvm-args", JVM_OPTS])
+
+ # Version (fabric:)
+ start_args.append("fabric:")
+
+ # Username
+ start_args.extend(["-u", user])
+
+ # Custom Java path (supported by both)
+ java_exe = "java.exe" if os.name == "nt" else "java"
+ java_bin = Path.cwd() / "jvm" / "java-runtime-delta" / "bin" / java_exe
+ if java_bin.exists():
+ # Insert --jvm and its path right after the server arguments
+ # Current start_args: [server args, jvm args, version, -u user]
+ # We want to insert before the version, so after the server args.
+ # Server args are two elements, so index 2.
+ start_args[2:2] = ["--jvm", str(java_bin)]
+
+ # Full command
+ cmd = launcher_cmd + global_args + ["start"] + start_args
+
+ # --- DISCONNECT DETECTION ---
+ closed_event = threading.Event()
+ progress_re = re.compile(r"(\d+/\d+)")
+
+ def generate():
+ process = None
+ got_generator_exit = False
+ try:
+ with processes_lock:
+ if user in active_processes and active_processes[user].poll() is None:
+ busy_payload = json.dumps({'type': 'ansi', 'content': '\x1b[91m[!] CORE BUSY\x1b[0m'})
+ yield f"data: {busy_payload}\n\n"
+ yield "data: CLOSE\n\n"
+ return
+
+ startupinfo = None
+ if os.name == 'nt':
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = subprocess.SW_HIDE
+
+ # SAFE: shell=False, all arguments are separate list elements.
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ shell=False,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ bufsize=1,
+ creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
+ startupinfo=startupinfo
+ )
+ active_processes[user] = process
+ logging.info(f"Started process for {user} with PID {process.pid}")
+
+ last_progress = ""
+ last_send_time = time.perf_counter()
+ update_interval = 0.15
+ ok_reached = False
+
+ while True:
+ if closed_event.is_set():
+ disc_msg = json.dumps({'type': 'ansi', 'content': "\x1b[91m[SYSTEM] CONNECTION CLOSED\x1b[0m"})
+ try:
+ yield f"data: {disc_msg}\n\n"
+ except (BrokenPipeError, OSError, GeneratorExit):
+ pass
+ break
+
+ line = process.stdout.readline()
+ if not line:
+ if process.poll() is not None:
+ break
+ continue
+
+ raw_line = line.rstrip('\n')
+ now = time.perf_counter()
+
+ if not ok_reached and "[ OK ]" in raw_line:
+ ok_reached = True
+
+ progress_match = progress_re.search(raw_line)
+
+ if use_server_conversion and ansi_converter:
+ try:
+ safe_line = escape_html(raw_line)
+ html_line = ansi_converter.convert(safe_line, full=False).strip()
+ payload = json.dumps({'type': 'html', 'content': html_line})
+ except Exception as e:
+ logging.error(f"Conversion failed: {e}, sending raw ANSI")
+ payload = json.dumps({'type': 'ansi', 'content': raw_line})
+ else:
+ payload = json.dumps({'type': 'ansi', 'content': raw_line})
+
+ try:
+ if progress_match and not ok_reached:
+ current_file = progress_match.group(1)
+ if current_file != last_progress and (now - last_send_time) > update_interval:
+ yield f"data: {payload}\n\n"
+ last_progress = current_file
+ last_send_time = now
+ else:
+ yield f"data: {payload}\n\n"
+ except (BrokenPipeError, OSError, GeneratorExit) as e:
+ if isinstance(e, GeneratorExit):
+ got_generator_exit = True
+ break
+
+ except FileNotFoundError as e:
+ if not closed_event.is_set():
+ try:
+ payload = json.dumps({'type': 'ansi', 'content': f"\x1b[91m[SYSTEM] Launcher not found: {str(e)}\x1b[0m"})
+ yield f"data: {payload}\n\n"
+ except Exception:
+ pass
+ except Exception as e:
+ if not closed_event.is_set():
+ try:
+ payload = json.dumps({'type': 'ansi', 'content': f"[SYSTEM ERROR] {str(e)}"})
+ yield f"data: {payload}\n\n"
+ except Exception:
+ pass
+ finally:
+ if process:
+ process.stdout.close()
+ kill_process_tree(process)
+ with processes_lock:
+ if user in active_processes:
+ del active_processes[user]
+ logging.info(f"Removed process entry for {user}")
+
+ if not got_generator_exit:
+ try:
+ ended_msg = json.dumps({'type': 'ansi', 'content': "\x1b[90m[SYSTEM] SESSION ENDED\x1b[0m"})
+ tip_msg = json.dumps({'type': 'ansi', 'content': "\x1b[34m[TIP] Click the console to return to login.\x1b[0m"})
+ yield f"data: {ended_msg}\n\n"
+ yield f"data: {tip_msg}\n\n"
+ yield "data: CLOSE\n\n"
+ except GeneratorExit:
+ pass
+ except Exception:
+ pass
+
+ response = Response(generate(), mimetype="text/event-stream")
+ response.call_on_close(closed_event.set)
+ return response
+
+@app.route("/")
+def home():
+ return render_template_string(HTML_TEMPLATE, forbidden_list=FORBIDDEN_LIST)
+
+def kill_process_tree(proc):
+ """Kill a process and all its children. Falls back to simple kill if psutil missing."""
+ if proc.poll() is not None:
+ logging.debug(f"Process {proc.pid} already dead")
+ return
+ if PSUTIL_AVAILABLE:
+ try:
+ parent = psutil.Process(proc.pid)
+ children = parent.children(recursive=True)
+ for child in children:
+ try:
+ child.kill()
+ except psutil.NoSuchProcess:
+ pass
+ parent.kill()
+ logging.info(f"Killed process tree for PID {proc.pid}")
+ except psutil.NoSuchProcess:
+ logging.debug(f"Process {proc.pid} already gone")
+ else:
+ try:
+ proc.terminate()
+ proc.wait(timeout=2)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ logging.info(f"Terminated process PID {proc.pid} (psutil unavailable)")
+
+def graceful_shutdown(sig, frame):
+ logging.info("SHUTTING DOWN CORE...")
+ with processes_lock:
+ for user, proc in list(active_processes.items()):
+ kill_process_tree(proc)
+ sys.exit(0)
+
+signal.signal(signal.SIGINT, graceful_shutdown)
+
+if __name__ == "__main__":
+ try:
+ socketio.run(app, port=5000, debug=False, allow_unsafe_werkzeug=True)
+ except KeyboardInterrupt:
+ graceful_shutdown(None, None)
\ No newline at end of file
From b212f384473ec94c7bfbea3c7ad01bac6c69db3b Mon Sep 17 00:00:00 2001
From: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
Date: Sat, 21 Mar 2026 15:18:12 +0000
Subject: [PATCH 02/70] Update and rename portablemc.py to
scrtips/portablemc.py
---
portablemc.py => scrtips/portablemc.py | 77 +++++++++++++++++++++++++-
1 file changed, 74 insertions(+), 3 deletions(-)
rename portablemc.py => scrtips/portablemc.py (91%)
diff --git a/portablemc.py b/scrtips/portablemc.py
similarity index 91%
rename from portablemc.py
rename to scrtips/portablemc.py
index 035b83c..645fc93 100644
--- a/portablemc.py
+++ b/scrtips/portablemc.py
@@ -10,12 +10,17 @@
import shutil
import threading
import logging
-import traceback
import os
import json
import importlib.util
-app = Flask(__name__)
+# Determine base directory (where Minecraft files are stored)
+APPDATA = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
+BASE_DIR = APPDATA / "PortableMC"
+BASE_DIR.mkdir(parents=True, exist_ok=True)
+
+# Flask app with static folder in BASE_DIR
+app = Flask(__name__, static_folder=str(BASE_DIR / "static"))
socketio = SocketIO(app, cors_allowed_origins="*")
def escape_html(s):
@@ -39,6 +44,12 @@ def escape_html(s):
clients_lock = threading.Lock()
logging.basicConfig(level=logging.INFO)
+# Path of root of Launcher
+LAUNCHER_ROOT = os.environ.get("LAUNCHER_ROOT")
+if LAUNCHER_ROOT is None:
+ LAUNCHER_ROOT = str(Path(__file__).parent.parent)
+ROOT_DIR = Path(LAUNCHER_ROOT)
+
# Optional psutil for process tree killing
try:
import psutil
@@ -909,6 +920,59 @@ def escape_html(s):