From e0dbca8d51006ea8adce74fe026c54fb0c8de572 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Tue, 30 Dec 2025 00:17:46 +0100 Subject: [PATCH] implement GitHub resource pack auto-updater with HTTP server and configuration --- README.md | 46 +++- .../GithubResourcePackUpdater.java | 226 ++++++++++++++++++ .../Projectvollidioten.java | 18 ++ .../config/GithubResourcePackConfig.java | 40 ++++ .../http/ResourcePackHttpServer.java | 101 ++++++++ src/main/resources/fabric.mod.json | 6 +- 6 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ceratic/projectvollidioten/GithubResourcePackUpdater.java create mode 100644 src/main/java/ceratic/projectvollidioten/config/GithubResourcePackConfig.java create mode 100644 src/main/java/ceratic/projectvollidioten/http/ResourcePackHttpServer.java diff --git a/README.md b/README.md index 0cf4f6c..3288414 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Projectvollidioten -A Minecraft Fabric mod that logs player data on join and leave events to an external API. +A Minecraft Fabric mod that provides comprehensive player data logging and automatic resource pack management via GitHub integration. ## Features -- Logs player join and leave events +### Player Data Logging +- Logs player join and leave events to external API - Captures detailed player statistics including: - Health and max health - Hunger level @@ -15,6 +16,15 @@ A Minecraft Fabric mod that logs player data on join and leave events to an exte - Asynchronous API communication to prevent server lag - Pretty-printed JSON logging for debugging +### Automatic Resource Pack Updates +- **GitHub Integration**: Automatically monitors GitHub releases for new resource packs +- **HTTP Server**: Built-in HTTP server (port 25585) to serve resource packs locally +- **Smart Updates**: Downloads and installs new resource pack versions automatically +- **Server Configuration**: Automatically updates `server.properties` with correct URLs and SHA1 hashes +- **Player Notifications**: Broadcasts resource pack availability to all players +- **Periodic Checking**: Configurable interval checking (default: every 60 minutes) +- **Secure Serving**: Only serves configured resource pack files for security + ## Requirements - Minecraft 1.21.11 @@ -30,10 +40,42 @@ A Minecraft Fabric mod that logs player data on join and leave events to an exte ## Configuration +### Player Data Logging The mod sends data to a hardcoded API endpoint: `http://localhost:3000/api/data` To change the API endpoint, modify the `API_URL` constant in `PlayerDataLogger.java`. +### Resource Pack Auto-Updater +The resource pack updater is configured via `config/projectvollidioten/resourcepack.json`: + +```json +{ + "githubOwner": "your-username", + "githubRepo": "your-repo-name", + "resourcePackFileName": "my-pack.zip", + "checkIntervalMinutes": 60 +} +``` + +**Configuration Options:** +- `githubOwner`: Your GitHub username or organization name +- `githubRepo`: The repository name containing the resource pack releases +- `resourcePackFileName`: The exact filename of the resource pack ZIP in GitHub releases +- `checkIntervalMinutes`: How often to check for updates (default: 60 minutes) + +**Setup Instructions:** +1. Create a GitHub repository for your resource packs +2. Upload resource pack ZIP files as release assets +3. Update the configuration file with your repository details +4. The mod will automatically download and serve new versions +5. Players will be notified when new packs are available + +**Important Notes:** +- The built-in HTTP server runs on port 25585 by default +- Resource packs are stored in `server-resource-packs/` directory +- Only the configured filename can be served for security +- SHA1 hashes are automatically computed and updated in server.properties + ## API Format The mod sends POST requests with JSON payloads in the following format: diff --git a/src/main/java/ceratic/projectvollidioten/GithubResourcePackUpdater.java b/src/main/java/ceratic/projectvollidioten/GithubResourcePackUpdater.java new file mode 100644 index 0000000..157c185 --- /dev/null +++ b/src/main/java/ceratic/projectvollidioten/GithubResourcePackUpdater.java @@ -0,0 +1,226 @@ +package ceratic.projectvollidioten; + +import ceratic.projectvollidioten.config.GithubResourcePackConfig; +import ceratic.projectvollidioten.http.ResourcePackHttpServer; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipFile; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; + +import static ceratic.projectvollidioten.config.GithubResourcePackConfig.loadOrCreate; + +public class GithubResourcePackUpdater { + private ResourcePackHttpServer httpServer; + private final MinecraftServer server; + private final Path configDir; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private GithubResourcePackConfig config; + private String latestVersionTag = ""; + private String downloadUrl = ""; + + public GithubResourcePackUpdater(MinecraftServer server) { + this.server = server; + this.configDir = server.getRunDirectory().resolve("config/projectvollidioten"); + this.config = loadOrCreate(configDir); + } + + public void start() { + // Start HTTP server FIRST (so we have URL ready) + try { + httpServer = new ResourcePackHttpServer(server, config.resourcePackFileName, 25585); // Use port 25585 for now + httpServer.start(); + } catch (IOException e) { + System.err.println("[GithubRP] ❌ Failed to start HTTP server: " + e.getMessage()); + } + + // Schedule periodic checks + long interval = config.checkIntervalMinutes; + scheduler.scheduleAtFixedRate(this::checkForUpdate, 0, interval, TimeUnit.MINUTES); + System.out.println("[GithubRP] Auto-updater started. Checking every " + interval + " minutes."); + } + + public void stop() { + if (httpServer != null) httpServer.stop(); + scheduler.shutdown(); + System.out.println("[GithubRP] Auto-updater stopped."); + } + + private void checkForUpdate() { + try { + String apiUrl = String.format( + "https://api.github.com/repos/%s/%s/releases/latest", + config.githubOwner, config.githubRepo + ); + + HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection(); + conn.setRequestProperty("User-Agent", "FabricMod-GithubRPUpdater"); + conn.setConnectTimeout(10_000); + conn.setReadTimeout(15_000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + System.err.println("[GithubRP] Failed to fetch release: HTTP " + responseCode); + conn.disconnect(); + return; + } + + JsonObject release = JsonParser.parseReader(new InputStreamReader(conn.getInputStream())).getAsJsonObject(); + conn.disconnect(); + + String tag = release.get("tag_name").getAsString(); + if (tag.equals(latestVersionTag)) { + // Already up to date + return; + } + + JsonArray assets = release.getAsJsonArray("assets"); + for (var assetElem : assets) { + JsonObject asset = assetElem.getAsJsonObject(); + String name = asset.get("name").getAsString(); + if (name.equals(config.resourcePackFileName)) { + downloadUrl = asset.get("browser_download_url").getAsString(); + downloadAndInstallResourcePack(downloadUrl, tag); + latestVersionTag = tag; + return; + } + } + + System.err.println("[GithubRP] Release " + tag + " found, but no asset named '" + config.resourcePackFileName + "'"); + } catch (Exception e) { + System.err.println("[GithubRP] Error checking for update: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void downloadAndInstallResourcePack(String downloadUrl, String tag) throws IOException { + Path packsDir = server.getRunDirectory().resolve("server-resource-packs"); + Files.createDirectories(packsDir); + + Path tempFile = Files.createTempFile("github-rp-", ".zip"); + Path targetFile = packsDir.resolve(config.resourcePackFileName); + + try { + // Download + System.out.println("[GithubRP] Downloading new resource pack: " + tag); + HttpURLConnection conn = (HttpURLConnection) new URL(downloadUrl).openConnection(); + conn.setRequestProperty("User-Agent", "FabricMod-GithubRPUpdater"); + try (InputStream in = conn.getInputStream()) { + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + // Validate ZIP (basic check) + try (ZipFile ignored = new ZipFile(tempFile.toFile())) { + // OK if no exception + } catch (Exception e) { + throw new IOException("Downloaded file is not a valid ZIP: " + e.getMessage()); + } + + // Replace old pack + Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + updateServerPropertiesWithHttpUrl(targetFile); // ✅ Now uses local HTTP URL! + + // Update server.properties to point to the new pack + // updateServerProperties(targetFile); + + // Update server.properties if needed (optional — Fabric handles reload via packet) + if (server instanceof DedicatedServer dedicated) { + // Force reload of resource packs (Fabric supports dynamic reload) + // We’ll push updated pack hash to clients on next join via login packet + System.out.println("[GithubRP] Resource pack updated to " + tag + ". New clients will get it."); + } + } finally { + try { Files.deleteIfExists(tempFile); } catch (IOException ignored) {} + } + } + +private void updateServerPropertiesWithHttpUrl(Path packPath) { + try { + String sha1 = computeSHA1(packPath); + if (sha1 == null) return; + + String url = (httpServer != null) + ? httpServer.getPackUrl() + : "http://127.0.0.1:25585/resourcepacks/" + config.resourcePackFileName; + + // Update server.properties + Path serverProps = server.getRunDirectory().resolve("server.properties"); + if (!Files.exists(serverProps)) return; + + List lines = Files.readAllLines(serverProps); + List newLines = new ArrayList<>(); + boolean foundRp = false, foundSha = false; + + for (String line : lines) { + String t = line.trim(); + if (t.startsWith("resource-pack=")) { + line = "resource-pack=" + url + (line.contains("#") ? " " + line.substring(line.indexOf('#')) : ""); + foundRp = true; + } else if (t.startsWith("resource-pack-sha1=")) { + line = "resource-pack-sha1=" + sha1 + (line.contains("#") ? " " + line.substring(line.indexOf('#')) : ""); + foundSha = true; + } + newLines.add(line); + } + + if (!foundRp) newLines.add("resource-pack=" + url); + if (!foundSha) newLines.add("resource-pack-sha1=" + sha1); + + Path tmp = serverProps.resolveSibling("server.properties.tmp"); + Files.write(tmp, newLines); + Files.move(tmp, serverProps, StandardCopyOption.REPLACE_EXISTING); + + System.out.println("[GithubRP] ✅ server.properties updated:"); + System.out.println(" URL: " + url); + System.out.println(" SHA1: " + sha1.substring(0, 8) + "..."); + + // Optional: Broadcast to all players + server.execute(() -> { + String shortUrl = url.replace("http://", "").replace("https://", ""); + server.getPlayerManager().getPlayerList().forEach(p -> { + p.sendMessage(net.minecraft.text.Text.literal("[GithubRP] 🌐 Pack served at " + shortUrl), false); + }); + }); + + } catch (Exception e) { + System.err.println("[GithubRP] Failed to update server.properties with HTTP URL: " + e.getMessage()); + } +} + +private String computeSHA1(Path file) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + try (InputStream in = Files.newInputStream(file)) { + byte[] buffer = new byte[8192]; + int n; + while ((n = in.read(buffer)) != -1) { + digest.update(buffer, 0, n); + } + } + byte[] hash = digest.digest(); + StringBuilder hex = new StringBuilder(); + for (byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } +} + +} diff --git a/src/main/java/ceratic/projectvollidioten/Projectvollidioten.java b/src/main/java/ceratic/projectvollidioten/Projectvollidioten.java index 9aa51c1..8b739a6 100644 --- a/src/main/java/ceratic/projectvollidioten/Projectvollidioten.java +++ b/src/main/java/ceratic/projectvollidioten/Projectvollidioten.java @@ -1,7 +1,9 @@ package ceratic.projectvollidioten; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +16,8 @@ public class Projectvollidioten implements ModInitializer { // That way, it's clear which mod wrote info, warnings, and errors. public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + private GithubResourcePackUpdater updater; + @Override public void onInitialize() { // This code runs as soon as Minecraft is in a mod-load-ready state. @@ -22,6 +26,9 @@ public class Projectvollidioten implements ModInitializer { LOGGER.info("Hello Fabric world!"); + ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStart); + ServerLifecycleEvents.SERVER_STOPPING.register(this::onServerStop); + // Event: Player Join ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { PlayerDataLogger.sendPlayerData(handler.getPlayer(), server, "JOIN"); @@ -32,4 +39,15 @@ public class Projectvollidioten implements ModInitializer { PlayerDataLogger.sendPlayerData(handler.getPlayer(), server, "LEAVE"); }); } + + private void onServerStart(MinecraftServer server) { + updater = new GithubResourcePackUpdater(server); + updater.start(); + } + + private void onServerStop(MinecraftServer server) { + if (updater != null) { + updater.stop(); + } + } } diff --git a/src/main/java/ceratic/projectvollidioten/config/GithubResourcePackConfig.java b/src/main/java/ceratic/projectvollidioten/config/GithubResourcePackConfig.java new file mode 100644 index 0000000..f43acf6 --- /dev/null +++ b/src/main/java/ceratic/projectvollidioten/config/GithubResourcePackConfig.java @@ -0,0 +1,40 @@ +package ceratic.projectvollidioten.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; + +public class GithubResourcePackConfig { + public String githubOwner = "your-username"; + public String githubRepo = "your-repo"; + public String resourcePackFileName = "my-pack.zip"; + public int checkIntervalMinutes = 60; // check every hour + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final String CONFIG_NAME = "resourcepack.json"; + + public static GithubResourcePackConfig loadOrCreate(Path configDir) { + Path configFile = configDir.resolve(CONFIG_NAME); + try { + if (Files.exists(configFile)) { + try (Reader reader = Files.newBufferedReader(configFile)) { + return GSON.fromJson(reader, GithubResourcePackConfig.class); + } + } else { + // Create default config + GithubResourcePackConfig defaultConfig = new GithubResourcePackConfig(); + Files.createDirectories(configDir); + try (Writer writer = Files.newBufferedWriter(configFile)) { + GSON.toJson(defaultConfig, writer); + } + return defaultConfig; + } + } catch (Exception e) { + System.err.println("[GithubRP] Failed to load config, using defaults: " + e.getMessage()); + return new GithubResourcePackConfig(); + } + } +} \ No newline at end of file diff --git a/src/main/java/ceratic/projectvollidioten/http/ResourcePackHttpServer.java b/src/main/java/ceratic/projectvollidioten/http/ResourcePackHttpServer.java new file mode 100644 index 0000000..b27c48b --- /dev/null +++ b/src/main/java/ceratic/projectvollidioten/http/ResourcePackHttpServer.java @@ -0,0 +1,101 @@ +package ceratic.projectvollidioten.http; + +import ceratic.projectvollidioten.config.GithubResourcePackConfig; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import net.minecraft.server.MinecraftServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResourcePackHttpServer { + private final HttpServer server; + private final Path resourcePacksDir; + private final String expectedFileName; + private final int port; + + public ResourcePackHttpServer(MinecraftServer mcServer, String expectedFileName, int port) throws IOException { + this.resourcePacksDir = mcServer.getRunDirectory().resolve("server-resource-packs"); + this.expectedFileName = expectedFileName; + this.port = port; + + Files.createDirectories(resourcePacksDir); + + this.server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0); + this.server.createContext("/resourcepacks", new ResourcePackHandler()); + this.server.setExecutor(null); // Uses default executor + } + + public void start() { + server.start(); + System.out.println("[GithubRP] 🌐 HTTP server started on http://0.0.0.0:" + port + "/resourcepacks/"); + } + + public void stop() { + server.stop(1); + System.out.println("[GithubRP] 🌐 HTTP server stopped."); + } + + // Returns URL clients should use — e.g. http://192.168.1.100:25585/resourcepacks/my-pack.zip + public String getPackUrl() { + // Try to get LAN IP (optional: replace with public IP if port-forwarded) + String host = "127.0.0.1"; // fallback + try { + java.net.InetAddress addr = java.net.InetAddress.getLocalHost(); + if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress()) { + host = addr.getHostAddress(); + } + } catch (Exception ignored) {} + + return String.format("http://%s:%d/resourcepacks/%s", host, port, expectedFileName); + } + + private class ResourcePackHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String path = exchange.getRequestURI().getPath(); + String targetFile = path.substring("/resourcepacks/".length()); + + // Security: only allow configured pack filename + if (!targetFile.equals(expectedFileName)) { + sendResponse(exchange, 403, "Forbidden: unknown resource pack"); + return; + } + + Path packFile = resourcePacksDir.resolve(targetFile); + if (!Files.exists(packFile) || !Files.isRegularFile(packFile)) { + sendResponse(exchange, 404, "Resource pack not found"); + return; + } + + // Set headers + exchange.getResponseHeaders().set("Content-Type", "application/zip"); + exchange.getResponseHeaders().set("Content-Length", String.valueOf(Files.size(packFile))); + exchange.getResponseHeaders().set("Cache-Control", "public, max-age=3600"); + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + + // Send file + exchange.sendResponseHeaders(200, 0); // chunked + try (OutputStream os = exchange.getResponseBody(); + var fis = Files.newInputStream(packFile)) { + fis.transferTo(os); + } catch (IOException e) { + System.err.println("[GithubRP] Client disconnected during pack download."); + } + } + + private void sendResponse(HttpExchange exchange, int code, String msg) throws IOException { + byte[] data = msg.getBytes(java.nio.charset.StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(code, data.length); + exchange.getResponseBody().write(data); + exchange.close(); + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index e1a5e9f..7186c41 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,13 +3,13 @@ "id": "projectvollidioten", "version": "${version}", "name": "projectvollidioten", - "description": "This is an example description! Tell everyone what your mod is about!", + "description": "A Minecraft Fabric mod that provides comprehensive player data logging and automatic resource pack management via GitHub integration.", "authors": [ "Me!" ], "contact": { - "homepage": "https://fabricmc.net/", - "sources": "https://github.com/FabricMC/fabric-example-mod" + "homepage": "https://vollidioten.ceraticsoft.de/", + "sources": "https://github.com/ceratic/projekt_vollidion_mod" }, "license": "CC0-1.0", "icon": "assets/projectvollidioten/icon.png",