mirror of
https://github.com/ceratic/project_vollidioten_mod.git
synced 2026-05-14 00:16:47 +02:00
implement GitHub resource pack auto-updater with HTTP server and configuration
This commit is contained in:
46
README.md
46
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:
|
||||
|
||||
@@ -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<String> lines = Files.readAllLines(serverProps);
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user