diff --git a/artifacts-plugin-regression.json b/artifacts-plugin-regression.json index f454ed0..4625d4c 100644 --- a/artifacts-plugin-regression.json +++ b/artifacts-plugin-regression.json @@ -1,6 +1,8 @@ { "passed": true, "checks": [ + "local console help, status and errors produce visible output", + "remote console help produces visible output", "missing export guidance", "empty area failure", "real Paper export without a player", @@ -16,5 +18,5 @@ "blocks": 10112, "chunks": 16, "viewpoints": 24, - "directory": ".cache/plugin-regression-1791186051478" + "directory": ".cache/plugin-regression-1791207258927" } \ No newline at end of file diff --git a/docs/server-plugin.md b/docs/server-plugin.md index f22b612..8797f59 100644 --- a/docs/server-plugin.md +++ b/docs/server-plugin.md @@ -1,8 +1,8 @@ -# Server plugin 0.1.1 +# Server plugin 0.1.2 ## Update and connect -1. Stop the Minecraft server, replace its old MineGeo JAR with `public/tools/plugin/minegeo-plugin-1.21.11.jar`, and restart. Run `/version MineGeo` to verify 0.1.1. +1. Stop the Minecraft server, replace its old MineGeo JAR with `public/tools/plugin/minegeo-plugin-1.21.11.jar`, and restart. Run `version MineGeo` in the console to verify 0.1.2. 2. Open your MineGeo website dashboard, sign in, and generate a plugin token. 3. In the Minecraft server console, configure the website and token. Console commands do not need the leading slash: diff --git a/minecraft/build.gradle b/minecraft/build.gradle index 7891281..d8a2ca7 100644 --- a/minecraft/build.gradle +++ b/minecraft/build.gradle @@ -1 +1 @@ -allprojects { group = 'dev.minegeo'; version = '0.1.1'; repositories { mavenCentral() } } +allprojects { group = 'dev.minegeo'; version = '0.1.2'; repositories { mavenCentral() } } diff --git a/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java b/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java index c82f01a..1b66c05 100644 --- a/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java +++ b/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java @@ -37,7 +37,10 @@ public final class MineGeoPlugin extends JavaPlugin { } @Override public void onDisable(){cancelled.set(true);} @Override public boolean onCommand(CommandSender sender,Command command,String label,String[] args){ - if(!sender.hasPermission("minegeo.admin")){sender.sendMessage("MineGeo: administrator permission required.");return true;} + // Paper may dispatch hosting-panel commands through a remote console sender + // whose permissions differ from the local console. Check permissions here so + // every rejected command receives a visible explanation. + if(!(sender instanceof ConsoleCommandSender||sender instanceof RemoteConsoleCommandSender||sender instanceof Player&&sender.hasPermission("minegeo.admin"))){sender.sendMessage("MineGeo: administrator permission required.");return true;} try { if(args.length==0||args[0].equalsIgnoreCase("help")) {sender.sendMessage(new String[]{"MineGeo commands:","/minegeo website ","/minegeo server — capture server address","/minegeo link ","/minegeo area ","/minegeo export — saves a local export","/minegeo upload — uploads the latest completed export","/minegeo contribute — send up to 12 panoramas to its owner","/minegeo status | cancel | reload"});return true;} switch(args[0].toLowerCase(Locale.ROOT)) { @@ -62,7 +65,7 @@ public final class MineGeoPlugin extends JavaPlugin { }catch(Exception ex){sender.sendMessage("MineGeo: "+ex.getMessage());}return true; } private void requireIdle(){if(busy)throw new IllegalArgumentException("An operation is running. Use /minegeo status or /minegeo cancel.");} - private void report(CommandSender sender,String message){status=message;getLogger().info(message);if(sender instanceof Player&&isEnabled())Bukkit.getScheduler().runTask(this,()->sender.sendMessage("MineGeo: "+message));} + private void report(CommandSender sender,String message){status=message;getLogger().info(message);if(!(sender instanceof ConsoleCommandSender)&&isEnabled())Bukkit.getScheduler().runTask(this,()->sender.sendMessage("MineGeo: "+message));} private void fail(CommandSender sender,String phase,Exception ex){report(sender,cancelled.get()?"Cancelled. Completed exports are kept.":phase+" failed: "+ex.getMessage());if(!cancelled.get())getLogger().log(Level.WARNING,phase+" failure",ex);} private void upload(CommandSender sender,String targetMap)throws Exception { requireIdle();if(latest==null||!Files.isRegularFile(latest.resolve("manifest.json")))throw new IOException("No completed export. Run /minegeo export My World, wait for 'Export complete', then /minegeo upload. Files: "+getDataFolder().toPath().resolve("exports").toAbsolutePath()); diff --git a/minecraft/plugin/src/main/resources/plugin.yml b/minecraft/plugin/src/main/resources/plugin.yml index 92e1f54..2c2d827 100644 --- a/minecraft/plugin/src/main/resources/plugin.yml +++ b/minecraft/plugin/src/main/resources/plugin.yml @@ -1,12 +1,11 @@ name: MineGeo -version: 0.1.1 +version: 0.1.2 main: dev.minegeo.plugin.MineGeoPlugin api-version: '1.21' description: Bounded terrain export for MineGeo panorama guessing commands: minegeo: description: Link, select, export, upload, status or cancel MineGeo maps - permission: minegeo.admin permissions: minegeo.admin: default: op diff --git a/public/tools/plugin/minegeo-plugin-1.21.11.jar b/public/tools/plugin/minegeo-plugin-1.21.11.jar index b568146..ddf965b 100644 Binary files a/public/tools/plugin/minegeo-plugin-1.21.11.jar and b/public/tools/plugin/minegeo-plugin-1.21.11.jar differ diff --git a/scripts/test-plugin.ts b/scripts/test-plugin.ts index d913676..cc24d8f 100644 --- a/scripts/test-plugin.ts +++ b/scripts/test-plugin.ts @@ -1,4 +1,5 @@ import { createServer } from "node:http"; +import { connect } from "node:net"; import { spawn } from "node:child_process"; import { mkdir, copyFile, writeFile, readFile } from "node:fs/promises"; import { createHash } from "node:crypto"; @@ -17,7 +18,7 @@ await copyFile( await writeFile(`${directory}/eula.txt`, "eula=true\n"); await writeFile( `${directory}/server.properties`, - 'server-ip=127.0.0.1\nserver-port=25580\nonline-mode=true\nlevel-type=minecraft:flat\ngenerator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}\ngenerate-structures=false\nview-distance=2\nsimulation-distance=2\nspawn-protection=0\nmax-players=1\n', + 'server-ip=127.0.0.1\nserver-port=25580\nenable-rcon=true\nrcon.port=25581\nrcon.password=minegeo-fixture\nonline-mode=true\nlevel-type=minecraft:flat\ngenerator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}\ngenerate-structures=false\nview-distance=2\nsimulation-distance=2\nspawn-protection=0\nmax-players=1\n', ); let manifest: any, received = Buffer.alloc(0), @@ -164,8 +165,52 @@ const command = async (text: string, pattern: RegExp) => { child.stdin.write(text + "\n"); await pending; }; +const rconCommand = (text: string, expected: RegExp) => new Promise((resolve, reject) => { + const socket = connect(25581, "127.0.0.1"); + let buffer = Buffer.alloc(0); + let authenticated = false; + const timeout = setTimeout(() => finish(new Error(`RCON timed out waiting for ${expected}`)), 10000); + const finish = (error?: Error) => { + clearTimeout(timeout); + socket.destroy(); + if (error) reject(error); + else resolve(); + }; + const packet = (id: number, type: number, body: string) => { + const payload = Buffer.from(body); + const frame = Buffer.alloc(payload.length + 14); + frame.writeInt32LE(payload.length + 10, 0); + frame.writeInt32LE(id, 4); + frame.writeInt32LE(type, 8); + payload.copy(frame, 12); + return frame; + }; + socket.on("connect", () => socket.write(packet(1, 3, "minegeo-fixture"))); + socket.on("error", finish); + socket.on("data", (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + while (buffer.length >= 4 && buffer.length >= buffer.readInt32LE(0) + 4) { + const length = buffer.readInt32LE(0); + const frame = buffer.subarray(0, length + 4); + buffer = buffer.subarray(length + 4); + const id = frame.readInt32LE(4); + const type = frame.readInt32LE(8); + if (id === -1) return finish(new Error("RCON authentication failed")); + if (id === 1 && type === 2 && !authenticated) { + authenticated = true; + socket.write(packet(2, 2, text)); + } else if (id === 2 && expected.test(frame.toString("utf8", 12, length + 2))) { + return finish(); + } + } + }); +}); try { await wait(/Done \(/, 0, 180000); + await command("minegeo help", /MineGeo commands:/); + await command("minegeo status", /MineGeo: Idle/); + await command("minegeo unknown", /Unknown command\. Use \/minegeo help/); + await rconCommand("minegeo help", /MineGeo commands:/); await command("minegeo upload", /No completed export/); await command( "minegeo area world 100000 100000 100032 100032", @@ -266,6 +311,8 @@ try { { passed: true, checks: [ + "local console help, status and errors produce visible output", + "remote console help produces visible output", "missing export guidance", "empty area failure", "real Paper export without a player",