Refactor Minecraft setup process and remove unused classes

- Updated SetupMinecraft task to use a Python script for setup.
- Removed dependencies on the Ripper task and related classes.
- Added new PythonTasks class to handle Python script execution.
- Introduced new scripts for handling Minecraft server setup and HTTP requests.
- Cleaned up unnecessary code and files related to the previous implementation.
This commit is contained in:
2026-05-17 00:53:46 +02:00
parent da41a0951f
commit a145ce6a96
13 changed files with 235 additions and 257 deletions
-2
View File
@@ -24,8 +24,6 @@ subprojects {
tasks.register('setupMinecraft', SetupMinecraft) {
group = 'torchcs'
description = 'Sets up the Minecraft server environment'
dependsOn project(':ripper').tasks.named('jar')
ripperJar.set(project(':ripper').tasks.named('jar', Jar).flatMap { it.archiveFile })
}
tasks.register('configureNativeRelease', Exec) {
-1
View File
@@ -11,5 +11,4 @@ repositories {
}
dependencies {
implementation 'com.google.code.gson:gson:2.11.0'
}
@@ -0,0 +1,70 @@
package torchcs.tasks;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.logging.Logger;
import org.gradle.api.tasks.TaskProvider;
public class PythonTasks {
private final File projectDir;
public PythonTasks(File projectDir) {
this.projectDir = projectDir;
}
public static TaskProvider<Task> CreateTask(Project project, String name, String pythonFile, String... args) {
return project.getTasks().register(name, task -> {
task.doLast(t -> new PythonTasks(project.getProjectDir()).execute(t.getLogger(), pythonFile, args));
});
}
public void execute(Logger logger, String pythonFile, String... args) {
List<String> cmd = new ArrayList<>();
cmd.add(python());
cmd.add("-B");
cmd.add(new File(projectDir, pythonFile).getAbsolutePath());
cmd.add("--root");
cmd.add(projectDir.getAbsolutePath());
cmd.addAll(Arrays.asList(args));
Process process;
try {
process = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
logger.lifecycle(line);
}
int exit = process.waitFor();
if (exit != 0) throw new RuntimeException(pythonFile + " exited with code " + exit);
} catch (InterruptedException e) {
process.destroyForcibly();
Thread.currentThread().interrupt();
throw new RuntimeException("Build cancelled", e);
} catch (IOException e) {
process.destroyForcibly();
throw new RuntimeException(e);
}
}
private static String python() {
return System.getProperty("os.name").toLowerCase().contains("win") ? "python" : "python3";
}
}
@@ -1,107 +1,19 @@
package torchcs.tasks;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.Directory;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.TaskAction;
import com.google.gson.JsonObject;
import torchcs.tasks.common.HttpClientMinecraftApi;
import torchcs.tasks.common.ProjectPath;
public abstract class SetupMinecraft extends DefaultTask {
@InputFile
public abstract RegularFileProperty getRipperJar();
@Inject
public abstract ProjectLayout getLayout();
private ProjectPath projectPath;
@TaskAction
public void run() throws IOException, InterruptedException {
init();
setupMinecraftJava();
}
private void setupMinecraftJava() throws IOException, InterruptedException {
JsonObject metadata = HttpClientMinecraftApi.fetchLatestJavaServerMetadata();
if (metadata == null) {
getLogger().error("Failed to fetch latest Java server metadata.");
return;
}
String downloadUrl = metadata.get("downloads").getAsJsonObject().get("server").getAsJsonObject().get("url").getAsString();
String versionId = metadata.get("id").getAsString();
Directory versionPath = projectPath.getJavaPath().dir(versionId);
if(new File(versionPath.getAsFile(), ".setup-complete").exists()) {
getLogger().lifecycle("Minecraft Java {} is already set up, skipping.", versionId);
return;
}
File marker = new File(versionPath.getAsFile(), ".setup-complete");
versionPath.getAsFile().mkdirs();
File serverJar = versionPath.file("server.jar").getAsFile();
if (!serverJar.exists()) {
getLogger().lifecycle("Downloading Minecraft Java server {}...", versionId);
HttpClientMinecraftApi.downloadFile(downloadUrl, versionPath);
getLogger().lifecycle("Download complete.");
}
getLogger().lifecycle("Decompiling server jar...");
runRipper(serverJar, versionPath.dir("src").getAsFile());
marker.createNewFile();
getLogger().lifecycle("Decompilation complete.");
getLogger().lifecycle("Setup finished successfully.");
}
private void runRipper(File input, File output) throws IOException, InterruptedException {
String java = ProcessHandle.current().info().command()
.orElse(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java");
Process process = new ProcessBuilder(
java,
"-Xmx8g", "-Xms1g",
"-XX:+UseZGC",
"-XX:ConcGCThreads=4",
"-jar", getRipperJar().getAsFile().get().getAbsolutePath(),
input.getAbsolutePath(),
output.getAbsolutePath(),
"-t", String.valueOf(Runtime.getRuntime().availableProcessors())
).redirectErrorStream(true).start();
Thread reader = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
getLogger().lifecycle(line);
}
} catch (IOException ignored) {}
});
reader.start();
int exit = process.waitFor();
reader.join();
if (exit != 0) throw new RuntimeException("Ripper exited with code " + exit);
}
private void init() {
projectPath = new ProjectPath(getLayout());
projectPath.initDirectories();
public void run() {
new PythonTasks(getLayout().getProjectDirectory().getAsFile())
.execute(getLogger(), "scripts/setup_minecraft.py");
}
}
@@ -1,75 +0,0 @@
package torchcs.tasks.common;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.gradle.api.file.Directory;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class HttpClientMinecraftApi {
private static final String BEDROCK_URL = "https://net-secondary.web.minecraft-services.net/api/v1.0/download/links";
private static final String JAVA_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
private static final HttpClient client = HttpClient.newHttpClient();
public static JsonObject fetchBedrock() throws IOException, InterruptedException {
return fetch(BEDROCK_URL);
}
public static JsonObject fetchJava() throws IOException, InterruptedException {
return fetch(JAVA_URL);
}
public static JsonObject fetchLatestJavaServerMetadata() throws IOException, InterruptedException {
JsonObject javaServerMetadata = fetchJava();
String latestVersion = javaServerMetadata.get("latest").getAsJsonObject().get("release").getAsString();
JsonArray versions = javaServerMetadata.getAsJsonArray("versions");
for (var element : versions) {
JsonObject version = element.getAsJsonObject();
String id = version.get("id").getAsString();
if (id.equals(latestVersion)) {
String url = version.get("url").getAsString();
return fetch(url);
}
}
return null;
}
public static void downloadFile(String url, Directory outPath) throws IOException, InterruptedException {
String fileName = url.substring(url.lastIndexOf('/') + 1);
Path destination = outPath.getAsFile().toPath().resolve(fileName);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "TorchCS/1.0")
.GET()
.build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
Files.copy(response.body(), destination, StandardCopyOption.REPLACE_EXISTING);
}
public static JsonObject fetch(String url) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "TorchCS/1.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return JsonParser.parseString(response.body()).getAsJsonObject();
}
}
@@ -1,58 +0,0 @@
package torchcs.tasks.common;
import org.gradle.api.file.Directory;
import org.gradle.api.file.ProjectLayout;
public class ProjectPath {
private final ProjectLayout layout;
public ProjectPath(ProjectLayout layout) {
this.layout = layout;
}
public Directory getBinPath() {
return layout.getProjectDirectory().dir("bin");
}
public Directory getMinecraftPath() {
return getBinPath().dir("minecraft");
}
public Directory getJavaPath() {
return getMinecraftPath().dir("java");
}
public Directory getBedrockPath() {
return getMinecraftPath().dir("bedrock");
}
public void initDirectories() {
Directory path;
path = getBinPath();
if (!path.getAsFile().exists()) {
path.getAsFile().mkdirs();
}
path = getMinecraftPath();
if (!path.getAsFile().exists()) {
path.getAsFile().mkdirs();
}
path = getJavaPath();
if (!path.getAsFile().exists()) {
path.getAsFile().mkdirs();
}
path = getBedrockPath();
if (!path.getAsFile().exists()) {
path.getAsFile().mkdirs();
}
}
}
+1
View File
@@ -0,0 +1 @@
requests
View File
+10
View File
@@ -0,0 +1,10 @@
import os
from pathlib import Path
def _java_executable() -> str:
java_home = os.environ.get("JAVA_HOME")
if java_home:
return str(Path(java_home) / "bin" / "java")
return "java"
+36
View File
@@ -0,0 +1,36 @@
import json
import shutil
import urllib.request
from pathlib import Path
BEDROCK_URL = "https://net-secondary.web.minecraft-services.net/api/v1.0/download/links"
JAVA_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
HEADERS = {"User-Agent": "TorchCS/1.0"}
def fetch(url: str) -> dict:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
def fetch_bedrock() -> dict:
return fetch(BEDROCK_URL)
def fetch_java() -> dict:
return fetch(JAVA_URL)
def fetch_latest_java_server_metadata() -> dict | None:
manifest = fetch_java()
latest_version = manifest["latest"]["release"]
for version in manifest["versions"]:
if version["id"] == latest_version:
return fetch(version["url"])
return None
def download_file(url: str, out_path: str | Path) -> None:
file_name = url.rsplit("/", 1)[-1]
destination = Path(out_path) / file_name
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as response, open(destination, "wb") as out_file:
shutil.copyfileobj(response, out_file)
+87
View File
@@ -0,0 +1,87 @@
import argparse
import logging
import os
import subprocess
import sys
import zipfile
from pathlib import Path
from common.constants import _java_executable
from common.http_client_minecraft import download_file, fetch_latest_java_server_metadata
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
log = logging.getLogger("SetupMinecraft")
def extract_jar(server_jar: Path, version_id: str) -> Path:
extract_dir = server_jar.parent / "extracted"
with zipfile.ZipFile(server_jar) as zf:
zf.extractall(extract_dir)
return extract_dir / f"META-INF/versions/{version_id}/server-{version_id}.jar"
def run_ripper(ripper_jar: Path, input_jar: Path, output_dir: Path) -> None:
java = _java_executable()
cmd = [java, "-Xmx8g", "-Xms1g", "-XX:+UseZGC", "-XX:ConcGCThreads=4", "-jar", str(ripper_jar), str(input_jar), str(output_dir), "-t", str(os.cpu_count())]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for line in process.stdout:
log.info(line.rstrip())
process.wait()
if process.returncode != 0:
raise RuntimeError(f"Ripper exited with code {process.returncode}")
def setup_minecraft_java(ripper_jar: Path, java_path: Path) -> None:
metadata = fetch_latest_java_server_metadata()
if metadata is None:
log.error("Failed to fetch latest Java server metadata.")
return
download_url = metadata["downloads"]["server"]["url"]
version_id = metadata["id"]
version_path = java_path / version_id
if (version_path / ".setup-complete").exists():
log.info("Minecraft Java %s is already set up, skipping.", version_id)
return
version_path.mkdir(parents=True, exist_ok=True)
server_jar = version_path / "server.jar"
if not server_jar.exists():
log.info("Downloading Minecraft Java server %s...", version_id)
download_file(download_url, version_path)
log.info("Download complete.")
log.info("Decompiling server jar...")
inner_jar = extract_jar(server_jar, version_id)
run_ripper(ripper_jar, inner_jar, inner_jar.parent / "src")
(version_path / ".setup-complete").touch()
log.info("Decompilation complete.")
log.info("Setup finished successfully.")
def main() -> None:
parser = argparse.ArgumentParser(description="Set up a Minecraft Java server and decompile it.")
parser.add_argument("--root", required=True, type=Path, help="Project root directory")
args = parser.parse_args()
ripper_jar = args.root / "build" / "gradle" / "ripper" / "libs" / "ripper.jar"
java_path = args.root / "bin" / "minecraft" / "java"
setup_minecraft_java(ripper_jar, java_path)
if __name__ == "__main__":
main()
+5 -30
View File
@@ -4,9 +4,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@@ -56,17 +54,15 @@ public class Ripper {
}
private static void decompile(File inputJar, File outputDir, int threads, boolean verbose) throws IOException {
File targetJar = extractInnerServerJar(inputJar);
int threadCount = threads <= 0 ? Runtime.getRuntime().availableProcessors() : threads;
int totalClasses = countRootClasses(targetJar);
int totalClasses = countRootClasses(inputJar);
int width = String.valueOf(totalClasses).length();
String sep = "".repeat(40 + width * 2);
System.out.println();
System.out.println(" Ripper — Java Decompiler");
System.out.println(" " + sep);
System.out.printf(" Input : %s%n", targetJar.getName());
System.out.printf(" Input : %s%n", inputJar.getName());
System.out.printf(" Output : %s%n", outputDir.getAbsolutePath());
System.out.printf(" Threads : %d%n", threadCount);
System.out.printf(" Classes : ~%d%n", totalClasses);
@@ -93,18 +89,14 @@ public class Ripper {
long start = System.currentTimeMillis();
DirectoryResultSaver saver = new DirectoryResultSaver(outputDir, totalClasses);
BaseDecompiler decompiler = new BaseDecompiler(provider, saver, options, logger);
decompiler.addSource(targetJar);
BaseDecompiler decompiler = new BaseDecompiler(provider, saver, options, logger, new ShutdownCancellationManager());
decompiler.addSource(inputJar);
decompiler.decompileContext();
long elapsed = System.currentTimeMillis() - start;
System.out.println();
System.out.printf(" Done: %d files written in %s%n", saver.getWritten(), formatDuration(elapsed));
System.out.println();
if (!targetJar.equals(inputJar)) {
targetJar.delete();
}
}
private static int countRootClasses(File jar) throws IOException {
@@ -133,21 +125,4 @@ public class Ripper {
System.err.println("[Ripper/ERROR] " + message);
System.exit(1);
}
private static File extractInnerServerJar(File bundlerJar) throws IOException {
try (JarFile jar = new JarFile(bundlerJar)) {
Optional<JarEntry> serverEntry = jar.stream()
.filter(e -> e.getName().startsWith("META-INF/versions/") && e.getName().endsWith(".jar"))
.findFirst();
if (serverEntry.isPresent()) {
File extracted = new File(bundlerJar.getParentFile(), "server-inner.jar");
try (InputStream is = jar.getInputStream(serverEntry.get())) {
Files.copy(is, extracted.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
return extracted;
}
}
return bundlerJar;
}
}
}
@@ -0,0 +1,23 @@
package net.torchcs.ripper;
import org.jetbrains.java.decompiler.main.CancellationManager;
public class ShutdownCancellationManager implements CancellationManager {
private volatile boolean cancelled = false;
public ShutdownCancellationManager() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> cancelled = true));
}
@Override
public void checkCanceled() throws CanceledException {
if (cancelled) throw new CanceledException();
}
@Override
public void startMethod(String className, String methodName) {}
@Override
public void finishMethod(String className, String methodName) {}
}