From dac8650ac19b2b147de0d4830317706e2b885c9c Mon Sep 17 00:00:00 2001
From: pavel444-byte
Date: Wed, 7 Oct 2026 13:18:31 +0500
Subject: [PATCH] Add Minecraft 26.x support and version Java artifacts
---
app/downloads/page.tsx | 62 +++---
artifacts-plugin-regression.json | 6 +-
docs/server-plugin.md | 13 +-
docs/shared-assets.md | 38 +++-
minecraft/build.gradle | 5 +-
minecraft/fabric-modern/build.gradle | 19 ++
.../main/java/dev/minegeo/fabric/Capture.java | 37 ++++
.../dev/minegeo/fabric/CaptureScreen.java | 22 ++
.../dev/minegeo/fabric/MineGeoClient.java | 37 ++++
.../dev/minegeo/fabric/SurfaceDataset.java | 34 +++
.../dev/minegeo/fabric/mixin/CameraMixin.java | 14 ++
.../fabric/mixin/GameRendererMixin.java | 14 ++
.../dev/minegeo/fabric/mixin/WindowMixin.java | 10 +
.../src/main/resources/fabric.mod.json | 11 +
.../src/main/resources/minegeo.mixins.json | 1 +
.../dev/minegeo/fabric/CaptureScreen.java | 11 +-
.../dev/minegeo/fabric/MineGeoClient.java | 11 +-
.../dev/minegeo/fabric/SurfaceDataset.java | 2 +-
minecraft/fabric261/build.gradle | 4 +
.../java/dev/minegeo/fabric/ClientCompat.java | 13 ++
.../fabric/mixin/MinecraftTargetMixin.java | 12 +
minecraft/fabric262/build.gradle | 4 +
.../java/dev/minegeo/fabric/ClientCompat.java | 14 ++
.../fabric/mixin/GameRendererTargetMixin.java | 16 ++
.../gradle/wrapper/gradle-wrapper.properties | 2 +-
.../dev/minegeo/plugin/MineGeoPlugin.java | 12 +-
.../java/dev/minegeo/plugin/UploadClient.java | 2 +
.../plugin/src/main/resources/config.yml | 2 +-
.../plugin/src/main/resources/plugin.yml | 2 +-
minecraft/settings.gradle | 2 +-
.../mod/minegeo-fabric-1.21.11-0.1.3.jar | Bin 0 -> 39338 bytes
public/tools/mod/minegeo-fabric-1.21.11.jar | Bin 39117 -> 39338 bytes
.../tools/mod/minegeo-fabric-26.1-0.1.3.jar | Bin 0 -> 38789 bytes
public/tools/mod/minegeo-fabric-26.1.jar | Bin 0 -> 38789 bytes
.../tools/mod/minegeo-fabric-26.2-0.1.3.jar | Bin 0 -> 38867 bytes
public/tools/mod/minegeo-fabric-26.2.jar | Bin 0 -> 38867 bytes
public/tools/plugin/minegeo-plugin-0.1.3.jar | Bin 0 -> 341003 bytes
.../tools/plugin/minegeo-plugin-1.21.11.jar | Bin 340746 -> 341003 bytes
renderer/render.mjs | 12 +-
scripts/assets.mjs | 32 ++-
scripts/java-build.mjs | 41 +++-
scripts/reprocess.mjs | 3 +-
scripts/test-minecraft-versions.mjs | 205 ++++++++++++++++++
scripts/test-plugin.ts | 169 ++++++++++-----
scripts/test-renderer.mjs | 21 +-
shared/locales/ru.json | 6 +-
shared/protocol.ts | 7 +-
tests/protocol.test.ts | 15 ++
tests/renderer-assets.test.ts | 31 ++-
worker/main.ts | 1 +
50 files changed, 813 insertions(+), 162 deletions(-)
create mode 100644 minecraft/fabric-modern/build.gradle
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/Capture.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/CaptureScreen.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/MineGeoClient.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/CameraMixin.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/GameRendererMixin.java
create mode 100644 minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/WindowMixin.java
create mode 100644 minecraft/fabric-modern/src/main/resources/fabric.mod.json
create mode 100644 minecraft/fabric-modern/src/main/resources/minegeo.mixins.json
create mode 100644 minecraft/fabric261/build.gradle
create mode 100644 minecraft/fabric261/src/main/java/dev/minegeo/fabric/ClientCompat.java
create mode 100644 minecraft/fabric261/src/main/java/dev/minegeo/fabric/mixin/MinecraftTargetMixin.java
create mode 100644 minecraft/fabric262/build.gradle
create mode 100644 minecraft/fabric262/src/main/java/dev/minegeo/fabric/ClientCompat.java
create mode 100644 minecraft/fabric262/src/main/java/dev/minegeo/fabric/mixin/GameRendererTargetMixin.java
create mode 100644 public/tools/mod/minegeo-fabric-1.21.11-0.1.3.jar
create mode 100644 public/tools/mod/minegeo-fabric-26.1-0.1.3.jar
create mode 100644 public/tools/mod/minegeo-fabric-26.1.jar
create mode 100644 public/tools/mod/minegeo-fabric-26.2-0.1.3.jar
create mode 100644 public/tools/mod/minegeo-fabric-26.2.jar
create mode 100644 public/tools/plugin/minegeo-plugin-0.1.3.jar
create mode 100644 scripts/test-minecraft-versions.mjs
diff --git a/app/downloads/page.tsx b/app/downloads/page.tsx
index b731e16..3a858b0 100644
--- a/app/downloads/page.tsx
+++ b/app/downloads/page.tsx
@@ -3,11 +3,13 @@ import { Header, Footer } from "@/components/Shell";
import { Blocks, Server, Download, ExternalLink } from "lucide-react";
import { existsSync } from "node:fs";
import Link from "next/link";
+const release = "0.1.3";
+const versions = ["1.21.11", "26.1", "26.2"];
export const dynamic = "force-dynamic";
export default async function Downloads() {
const { t } = await getI18n();
- const fabric = existsSync("public/tools/mod/minegeo-fabric-1.21.11.jar"),
- plugin = existsSync("public/tools/plugin/minegeo-plugin-1.21.11.jar");
+ const pluginPath = `/tools/plugin/minegeo-plugin-${release}.jar`;
+ const plugin = existsSync(`public${pluginPath}`);
return (
<>
@@ -33,7 +35,9 @@ export default async function Downloads() {
-
- {t("Minecraft Java 1.21.11 + Fabric Loader + Fabric API")}
+ {t(
+ "Minecraft Java 1.21.11, 26.1 & 26.2 + Fabric Loader + Fabric API",
+ )}
- {t("Open the capture panel with G")}
- {t("Link your account through a browser pairing code")}
@@ -48,24 +52,35 @@ export default async function Downloads() {
- {t("Contributions check the recorded server address")}
- {fabric ? (
-
- {t("Download Fabric mod")}
-
- ) : (
-
- {t("The Fabric build is not available yet.")}
-
+ {versions.map((version) =>
+ existsSync(
+ `public/tools/mod/minegeo-fabric-${version}-${release}.jar`,
+ ) ? (
+
+ {t("Download Fabric mod")} · {version}
+
+ ) : (
+
+ {version}: {t("The Fabric build is not available yet.")}
+
+ ),
)}
{t(
"Install the JAR in your Minecraft mods folder alongside Fabric API.",
)}
+
+ {t(
+ "Java 21 for 1.21.11; Java 25 for 26.1 and 26.2. Both tools connect to minegeo.vercel.app automatically.",
+ )}
+
@@ -77,7 +92,7 @@ export default async function Downloads() {
)}
- - {t("Bukkit/Paper 1.21.11 · Java 21")}
+ - {t("Bukkit/Paper 1.21.11, 26.1 & 26.2")}
- {t("Select a bounded area and export existing chunks")}
- {t("Choose up to 24 random, safe surface viewpoints")}
-
@@ -90,11 +105,7 @@ export default async function Downloads() {
- {t("Vanilla blocks/fluids, simplified lighting")}
{plugin ? (
-
+
{t("Download server plugin")}
) : (
@@ -104,7 +115,7 @@ export default async function Downloads() {
)}
{
- "/minegeo website https://your-minegeo-site.example\n/minegeo server play.your-server.example\n/minegeo link \n/minegeo area world -64 -64 64 64\n/minegeo export My Overworld\n/minegeo status\n# Wait for Export complete, then:\n/minegeo upload"
+ "/minegeo server play.your-server.example\n/minegeo link \n/minegeo area world -64 -64 64 64\n/minegeo export My Overworld\n/minegeo status\n# Wait for Export complete, then:\n/minegeo upload"
}
@@ -117,10 +128,9 @@ export default async function Downloads() {
)}
- Put the JAR in plugins/ and restart. Set website, server-name and
- server-address in plugins/MineGeo/config.yml, then run /minegeo
- reload. HTTP LAN addresses are supported for testing. Permission:
- minegeo.admin.
+ {t(
+ "Put the JAR in plugins/ and restart. Set server-name and server-address in plugins/MineGeo/config.yml, then run /minegeo reload. Permission: minegeo.admin. The same plugin JAR supports all three versions.",
+ )}
diff --git a/artifacts-plugin-regression.json b/artifacts-plugin-regression.json
index 4625d4c..2c2a95c 100644
--- a/artifacts-plugin-regression.json
+++ b/artifacts-plugin-regression.json
@@ -11,12 +11,12 @@
"repeated upload is idempotent",
"cancellation preserves completed export",
"24 random positions change on the next export",
- "contribution command caps at 12 and retries idempotently",
+ "shared upload client caps contributions at 12 and retries idempotently",
"wrong server blocked before upload, including cached retry",
"owner override/default port and cleared address supported"
],
"blocks": 10112,
"chunks": 16,
"viewpoints": 24,
- "directory": ".cache/plugin-regression-1791207258927"
-}
\ No newline at end of file
+ "directory": ".cache/plugin-regression-1791360575134"
+}
diff --git a/docs/server-plugin.md b/docs/server-plugin.md
index 8797f59..fd3e1de 100644
--- a/docs/server-plugin.md
+++ b/docs/server-plugin.md
@@ -1,18 +1,17 @@
-# Server plugin 0.1.2
+# Server plugin 0.1.3
## 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` in the console to verify 0.1.2.
+1. Stop the Minecraft server, replace its old MineGeo JAR with `public/tools/plugin/minegeo-plugin-0.1.3.jar`, and restart. Run `version MineGeo` in the console to verify 0.1.3.
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:
+3. In the Minecraft server console, configure the Minecraft server address and contributor token. Console commands do not need the leading slash:
```text
-minegeo website https://your-minegeo-domain.example
minegeo server play.your-minecraft-server.example
minegeo link
```
-For LAN testing the website may be `http://192.168.1.50:3000`; use your actual address. `localhost` means the computer running Minecraft, which may be different from the Mac running the website. HTTP is supported for testing; use HTTPS when transmitting contributor tokens over a public connection.
+The website URL is built in: `https://minegeo.vercel.app`. Existing `website` settings are removed on startup; no URL configuration is needed. `minegeo website` displays the fixed endpoint. Account tokens remain private local configuration.
`minegeo server` sets the Minecraft connection hostname/IP, including a non-default port if needed. It is separate from the MineGeo website URL. Use the address players enter in Minecraft, or set `server-address` in `config.yml` and reload before exporting. The plugin records it in each export. Contributions to checked maps reject a missing or different capture address; old exports need to be recreated after configuring it. The map owner can override or clear the expected address in panorama settings. See [contribution server checks](panorama-contributions.md#contribution-server-check).
@@ -67,7 +66,7 @@ If the worker stops renewing its lease, the page warns that it has stopped repor
- An interrupted transfer resumes with `minegeo upload`. Progress is saved in `.upload-state.json` beside the export. Already received exports are not submitted twice.
- `minegeo cancel` cancels capture or stops upload at the next transfer boundary. Completed exports are kept. HTTP requests already in flight can take up to their timeout to stop.
-- `minegeo reload` reloads `config.yml` after you edit `website`, `server-name`, `chunks-per-tick`, or other settings.
+- `minegeo reload` reloads `config.yml` after you edit `server-name`, `chunks-per-tick`, or other settings.
- `minegeo status` reports the active stage and the last completed export folder.
- `minegeo help` lists commands.
@@ -96,4 +95,4 @@ To regenerate an existing unpublished draft after pulling this fix:
The maintenance command prepares plugin rendering assets, keeps the original raw export, clears the previous preview metadata and queues a fresh render. It refuses published revisions and active jobs. Inspect the regenerated private draft and submit it for review again. If the export itself is invalid, create and upload a corrected export instead.
-Expected limits: Minecraft 1.21.11 / Java 21, Overworld only, five or more safe viewpoints, at most 2048 blocks per selected side and the worker's terrain/file-size limits. A large selection may need to be split into smaller maps.
+Expected limits: Minecraft 1.21.11 / Java 21, or Minecraft 26.1 and 26.2 / Java 25, Overworld only, five or more safe viewpoints, at most 2048 blocks per selected side and the worker's terrain/file-size limits. A large selection may need to be split into smaller maps.
diff --git a/docs/shared-assets.md b/docs/shared-assets.md
index 691e750..fe38335 100644
--- a/docs/shared-assets.md
+++ b/docs/shared-assets.md
@@ -3,16 +3,22 @@
Commit these website assets together with the source code:
- `public/images/example-map.webp` — homepage illustration and map-card fallback.
-- `public/tools/mod/minegeo-fabric-1.21.11.jar` — Fabric mod download.
-- `public/tools/plugin/minegeo-plugin-1.21.11.jar` — server plugin download.
+- `public/tools/mod/minegeo-fabric-1.21.11-0.1.3.jar` — Java 21 Fabric mod.
+- `public/tools/mod/minegeo-fabric-26.1-0.1.3.jar` — Java 25 Fabric mod for 26.1.
+- `public/tools/mod/minegeo-fabric-26.2-0.1.3.jar` — Java 25 Fabric mod for 26.2.
+- `public/tools/plugin/minegeo-plugin-0.1.3.jar` — Bukkit/Paper plugin for all three.
-Next.js serves these directly at `/images/example-map.webp`,
-`/tools/mod/minegeo-fabric-1.21.11.jar`, and
-`/tools/plugin/minegeo-plugin-1.21.11.jar`. They are deliberately tracked, so a
-fresh checkout on macOS has the homepage image and both download buttons without
+Next.js serves these directly under `/images/` and `/tools/`. The download page
+uses versioned filenames to avoid stale downloads. Unversioned 1.21.11 URLs
+remain updated compatibility copies. These assets are deliberately tracked, so a
+fresh checkout on macOS has the homepage image and all download buttons without
building Minecraft tools or running the renderer proof.
-After changing the Java source, run `npm run java:build` with Java 21 installed.
+After changing the Java source, run `npm run java:build` with Java 25 selected
+as `JAVA_HOME`, and Java 21 available for the older mod and plugin toolchains.
+Gradle builds the older Yarn-based mod and both separate Mojang-named 26.x mods.
+On Windows/OneDrive, set `MINEGEO_JAVA_BUILD_ROOT` to a local temporary directory
+to keep generated Gradle output outside synced folders.
The build copies the new JARs into `public/tools/`; commit those updated files too.
Gradle caches, intermediate build output, and generated test panoramas remain
ignored. The JARs contain Java bytecode and work across Windows, macOS, and Linux.
@@ -20,3 +26,21 @@ ignored. The JARs contain Java bytecode and work across Windows, macOS, and Linu
On the Mac, pull the commit, run `npm install`, and restart the development
server. For a production server, run `npm run build` and then `npm start`.
Keep that machine's Supabase credentials in its own `.env.local`.
+
+Both tools connect to `https://minegeo.vercel.app` without URL setup. The mod
+still stores its contributor token and selected map locally; the plugin retains
+its contributor token, Minecraft connection address, and export settings.
+
+The website accepts 1.21.11 and 26.1/26.2 capture manifests. The updated worker
+selects and verifies vanilla models/textures for each export's version. Restart
+the rendering worker on its existing host after updating these source files;
+Vercel deploys only the website and JAR downloads, not that separate worker.
+
+Verification commands:
+
+Select Java 25 in `JAVA_HOME` for the Paper matrix. It checks 1.21.11, the
+maintained 26.1.2 release of the 26.1 family, and 26.2 without launching a game
+client. Fabric builds are compile-checked; client capture was not runtime-tested.
+
+- `node scripts/test-minecraft-versions.mjs`: local Paper command/export matrix.
+- `node scripts/test-renderer.mjs --fixture 26.1` (or `26.2`): versioned rendering.
diff --git a/minecraft/build.gradle b/minecraft/build.gradle
index d8a2ca7..abb2dd7 100644
--- a/minecraft/build.gradle
+++ b/minecraft/build.gradle
@@ -1 +1,4 @@
-allprojects { group = 'dev.minegeo'; version = '0.1.2'; repositories { mavenCentral() } }
+allprojects { group = 'dev.minegeo'; version = '0.1.3'; repositories { mavenCentral() } }
+if (System.getenv('MINEGEO_JAVA_BUILD_ROOT')) {
+ subprojects { layout.buildDirectory = new File(System.getenv('MINEGEO_JAVA_BUILD_ROOT'), project.name) }
+}
diff --git a/minecraft/fabric-modern/build.gradle b/minecraft/fabric-modern/build.gradle
new file mode 100644
index 0000000..10f9f9b
--- /dev/null
+++ b/minecraft/fabric-modern/build.gradle
@@ -0,0 +1,19 @@
+dependencies {
+ minecraft "com.mojang:minecraft:${minecraftVersion}"
+ implementation 'net.fabricmc:fabric-loader:0.19.5'
+ implementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}"
+ implementation 'com.google.code.gson:gson:2.11.0'
+}
+java { toolchain { languageVersion = JavaLanguageVersion.of(25) }; withSourcesJar() }
+sourceSets.main.java.srcDirs '../fabric-modern/src/main/java', '../plugin/src/main/java'
+sourceSets.main.java.exclude 'dev/minegeo/plugin/MineGeoPlugin.java'
+sourceSets.main.resources.srcDir '../fabric-modern/src/main/resources'
+processResources {
+ inputs.property 'version', project.version
+ inputs.property 'minecraftVersion', minecraftVersion
+ inputs.property 'targetMixin', minecraftVersion == '26.1' ? 'MinecraftTargetMixin' : 'GameRendererTargetMixin'
+ filesMatching('fabric.mod.json') { expand version: project.version, minecraftVersion: minecraftVersion }
+ filesMatching('minegeo.mixins.json') { expand targetMixin: minecraftVersion == '26.1' ? 'MinecraftTargetMixin' : 'GameRendererTargetMixin' }
+}
+base { archivesName = "minegeo-fabric-${minecraftVersion}" }
+jar { archiveFileName = "minegeo-fabric-${minecraftVersion}.jar" }
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/Capture.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/Capture.java
new file mode 100644
index 0000000..0dd3a8a
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/Capture.java
@@ -0,0 +1,37 @@
+package dev.minegeo.fabric;
+import net.minecraft.client.Minecraft;
+import com.mojang.blaze3d.pipeline.*;
+import net.minecraft.client.CameraType;
+import net.minecraft.client.Screenshot;
+import net.minecraft.world.phys.Vec3;
+import java.nio.file.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Changes the render camera only: no movement or rotation packets are sent to a server. */
+public final class Capture {
+ public static final int SIZE=1024;
+ public static final String[] FACES={"px","nx","py","ny","pz","nz"};
+ private static final float[] YAW={-90,90,0,0,0,180},PITCH={0,0,-90,90,0,0};
+ private static boolean active;private static RenderTarget buffer;private static Path output;
+ private static Vec3 position;private static Object world;private static int face,settle;
+ private static boolean hud,bobbing;private static CameraType perspective;
+ private static final AtomicBoolean pending=new AtomicBoolean();
+ public static boolean active(){return active;}public static RenderTarget framebuffer(){return buffer;}
+ public static float yaw(){return YAW[Math.min(face,5)];}public static float pitch(){return PITCH[Math.min(face,5)];}
+ public static void start(Path directory)throws Exception {
+ var c=Minecraft.getInstance();if(active)throw new IllegalStateException("Capture already running");
+ if(c.player==null||c.level==null||!c.level.dimension().identifier().toString().equals("minecraft:overworld"))throw new IllegalStateException("Join an Overworld first");
+ if(c.player.isUnderWater())throw new IllegalStateException("Capture from a dry surface location");
+ output=directory;Files.createDirectories(output);position=new Vec3(c.player.getX(),c.player.getY(),c.player.getZ());world=c.level;
+ hud=ClientCompat.hudHidden(c);bobbing=c.options.bobView().get();perspective=c.options.getCameraType();
+ try {buffer=ClientCompat.target();face=0;settle=0;pending.set(false);active=true;ClientCompat.hudHidden(c,true);c.options.bobView().set(false);c.options.setCameraType(CameraType.FIRST_PERSON);c.gameRenderer.resize(SIZE,SIZE);}
+ catch(Throwable e){cancel("Capture setup failed: "+e.getMessage());throw e;}
+ }
+ public static void beforeFrame(){if(!active)return;var c=Minecraft.getInstance();if(c.player==null||c.level!=world||ClientCompat.screen(c)!=null||new Vec3(c.player.getX(),c.player.getY(),c.player.getZ()).distanceToSqr(position)>.0004)cancel("Capture cancelled. Stand still until all six views are saved.");}
+ public static void afterFrame(){if(!active||pending.get()||++settle<4)return;pending.set(true);final int index=face;final Path directory=output;
+ try{Screenshot.takeScreenshot(buffer,image->{try(image){if(active&&output.equals(directory)){image.writeToFile(directory.resolve(FACES[index]+".png"));var c=Minecraft.getInstance();c.execute(()->{if(!active||!output.equals(directory))return;face++;settle=0;pending.set(false);if(face==6){restore();MineGeoClient.INSTANCE.savedCapture(directory);}});}}catch(Exception e){Minecraft.getInstance().execute(()->cancel("Capture failed: "+e.getMessage()));}});}
+ catch(Exception e){cancel("Capture failed: "+e.getMessage());}
+ }
+ public static void cancel(String reason){if(active||buffer!=null){restore();MineGeoClient.INSTANCE.message(reason);}}
+ private static void restore(){var c=Minecraft.getInstance();active=false;pending.set(false);ClientCompat.hudHidden(c,hud);c.options.bobView().set(bobbing);if(perspective!=null)c.options.setCameraType(perspective);c.gameRenderer.resize(c.getWindow().getWidth(),c.getWindow().getHeight());if(buffer!=null){buffer.destroyBuffers();buffer=null;}}
+}
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/CaptureScreen.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/CaptureScreen.java
new file mode 100644
index 0000000..9e8a747
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/CaptureScreen.java
@@ -0,0 +1,22 @@
+package dev.minegeo.fabric;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.client.gui.GuiGraphicsExtractor;
+import net.minecraft.client.gui.components.*;
+import net.minecraft.network.chat.Component;
+
+public final class CaptureScreen extends Screen {
+ private EditBox name,target;
+ public CaptureScreen(){super(Component.literal("MineGeo Capture"));}
+ @Override protected void init(){var mod=MineGeoClient.INSTANCE;int x=width/2-130;
+ name=new EditBox(font,x,92,260,20,Component.literal("Map name"));name.setMaxLength(80);name.setValue(mod.dataset==null?"My Minecraft map":mod.dataset.name);addRenderableWidget(name);
+ target=new EditBox(font,x,126,260,20,Component.literal("Add to map ID (optional)"));target.setMaxLength(36);target.setValue(mod.targetMap);addRenderableWidget(target);
+ addRenderableWidget(Button.builder(Component.literal("Capture panorama"),b->{mod.targetMap=target.getValue().trim();mod.capture();}).bounds(x,154,126,20).build());
+ addRenderableWidget(Button.builder(Component.literal("Preview files"),b->{if(mod.dataset!=null)net.minecraft.util.Util.getPlatform().openPath(mod.dataset.directory);}).bounds(x+134,154,126,20).build());
+ addRenderableWidget(Button.builder(Component.literal("Link account"),b->{mod.targetMap=target.getValue().trim();mod.pair();}).bounds(x,182,126,20).build());
+ addRenderableWidget(Button.builder(Component.literal("Upload / resume"),b->{mod.targetMap=target.getValue().trim();mod.upload(name.getValue());}).bounds(x+134,182,126,20).build());
+ addRenderableWidget(Button.builder(Component.literal("New map at this location"),b->{mod.newMap();resize(width,height);}).bounds(x,210,260,20).build());
+ addRenderableWidget(Button.builder(Component.literal("Close"),b->onClose()).bounds(x,238,260,20).build());
+ }
+ @Override public void extractRenderState(GuiGraphicsExtractor context,int mouseX,int mouseY,float delta){super.extractRenderState(context,mouseX,mouseY,delta);int x=width/2-130;context.centeredText(font,title,width/2,20,0xff8cdf6b);context.text(font,"MineGeo service",x,38,0xffffffff);context.text(font,MineGeoClient.website,x,54,0xff8cdf6b);context.text(font,"Map name",x,80,0xffffffff);context.text(font,"Add to map ID (blank = new map)",x,114,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.centeredText(font,Component.literal(counts),width/2,270,0xffffffff);int y=292;for(var line:font.split(Component.literal(mod.status),260)){context.text(font,line,x,y,0xffd5d5d5);y+=10;}}
+ @Override public boolean isPauseScreen(){return false;}
+}
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/MineGeoClient.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/MineGeoClient.java
new file mode 100644
index 0000000..870c08b
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/MineGeoClient.java
@@ -0,0 +1,37 @@
+package dev.minegeo.fabric;
+import dev.minegeo.plugin.UploadClient;
+import com.google.gson.*;
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.KeyMapping;
+import com.mojang.blaze3d.platform.InputConstants;
+import net.minecraft.resources.Identifier;
+import net.minecraft.network.chat.Component;
+import org.lwjgl.glfw.GLFW;
+import java.nio.file.*;
+import java.util.concurrent.*;
+
+public final class MineGeoClient implements ClientModInitializer {
+ public static MineGeoClient INSTANCE;
+ public final ExecutorService background=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"MineGeo upload");t.setDaemon(true);return t;});
+ public final Gson gson=new GsonBuilder().setPrettyPrinting().create();
+ public static final String website=UploadClient.WEBSITE;
+ public volatile String token="",status="Explore normally to collect surface coverage.";
+ public volatile String targetMap="";
+ public volatile boolean uploading=false;public SurfaceDataset dataset;private SurfaceDataset.Point pending;
+ private Path config;
+ @Override public void onInitializeClient(){INSTANCE=this;config=Minecraft.getInstance().gameDirectory.toPath().resolve("config/minegeo.json");try{if(Files.exists(config)){var j=JsonParser.parseString(Files.readString(config)).getAsJsonObject();token=j.get("token").getAsString();if(j.has("targetMap"))targetMap=j.get("targetMap").getAsString();}}catch(Exception e){message("Could not read MineGeo configuration");}
+ var key=KeyMappingHelper.registerKeyMapping(new KeyMapping("key.minegeo.open",InputConstants.Type.KEYSYM,GLFW.GLFW_KEY_G,KeyMapping.Category.register(Identifier.fromNamespaceAndPath("minegeo","capture"))));
+ ClientTickEvents.END_CLIENT_TICK.register(c->{while(key.consumeClick()){if(Capture.active())Capture.cancel("Capture cancelled");else ClientCompat.screen(c,new CaptureScreen());}if(c.level!=null&&c.player!=null&&c.level.dimension().identifier().toString().equals("minecraft:overworld")){try{if(dataset==null||!dataset.server.equals(server()))dataset=SurfaceDataset.open(server());if(!Capture.active()&&!uploading)dataset.scan(c);}catch(Exception e){message("Map collection: "+e.getMessage());}}});
+ }
+ public String server(){var c=Minecraft.getInstance();return c.getCurrentServer()!=null?c.getCurrentServer().ip:"Singleplayer";}
+ public void saveConfig()throws Exception{Files.createDirectories(config.getParent());var j=new JsonObject();j.addProperty("token",token);j.addProperty("targetMap",targetMap);Files.writeString(config,gson.toJson(j));}
+ public void message(String text){status=text;var c=Minecraft.getInstance();if(c.player!=null)ClientCompat.message(c,Component.literal("[MineGeo] "+text));}
+ public void capture(){try{if(dataset==null)throw new IllegalStateException("Join an Overworld and explore first");var c=Minecraft.getInstance();if(!dataset.contains(c.player.getX(),c.player.getZ()))throw new IllegalStateException("Outside this map's 512-block boundary. Start a new map here.");int limit=24;if(dataset.points.size()>=limit)throw new IllegalStateException("This capture already contains "+limit+" locations");pending=new SurfaceDataset.Point("p"+java.util.UUID.randomUUID().toString().replace("-","").substring(0,12),c.player.getX(),c.player.getEyeY(),c.player.getZ());ClientCompat.screen(c,null);Capture.start(dataset.directory.resolve(pending.id()));message("Capturing six views. Stand still; press G to cancel.");}catch(Exception e){message(e.getMessage());}}
+ public void savedCapture(Path path){try{if(pending==null||!path.getFileName().toString().equals(pending.id()))return;dataset.points.add(pending);dataset.save();message("Panorama saved locally ("+dataset.points.size()+" total). Press G for preview or upload.");}catch(Exception e){message("Saving capture failed: "+e.getMessage());}finally{pending=null;}}
+ public void newMap(){try{if(uploading||Capture.active())throw new IllegalStateException("Finish the current operation first");dataset=SurfaceDataset.create(server());message("New 512 × 512 map started. Older maps remain in .minecraft/minegeo.");}catch(Exception e){message(e.getMessage());}}
+ public void pair(){background.execute(()->{try{saveConfig();var uploader=new UploadClient("");var pair=uploader.beginPairing();String code=pair.get("deviceCode").getAsString();String link=pair.get("verificationUri").getAsString();Minecraft.getInstance().execute(()->{message("Approve code "+pair.get("userCode").getAsString()+" at "+link);net.minecraft.util.Util.getPlatform().openUri(link);});for(int i=0;i<120;i++){Thread.sleep(5000);var response=uploader.pollPairing(code);if(response.get("status").getAsString().equals("approved")){token=response.get("token").getAsString();saveConfig();Minecraft.getInstance().execute(()->message("Account linked. You can upload maps now."));return;}}throw new IllegalStateException("Pairing expired. Try linking again.");}catch(Exception e){Minecraft.getInstance().execute(()->message("Pairing: "+e.getMessage()));}});}
+ public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;final String target=targetMap.trim();if(!target.isBlank()&&(Minecraft.getInstance().getCurrentServer()==null||selected==null||!java.util.Objects.equals(dev.minegeo.plugin.ServerAddress.normalize(server()),dev.minegeo.plugin.ServerAddress.normalize(selected.server)))){message("Join the server this capture belongs to before contributing.");return;}int minimum=target.isBlank()?5:1;if(selected==null||selected.points.size(){try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(token).upload(selected.directory,target.isBlank()?null:target, progress -> status=progress, () -> false);Minecraft.getInstance().execute(()->message("Upload received. Preview your selection and submit it in your dashboard."));}catch(Exception e){Minecraft.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});}
+}
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/SurfaceDataset.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
new file mode 100644
index 0000000..be0d32f
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
@@ -0,0 +1,34 @@
+package dev.minegeo.fabric;
+import com.google.gson.*;
+import net.minecraft.client.Minecraft;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.levelgen.Heightmap;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.nio.file.*;
+import java.security.MessageDigest;
+import java.util.*;
+import java.awt.image.BufferedImage;
+import javax.imageio.ImageIO;
+
+/** Reads only chunks already delivered to the client; never requests or generates chunks. */
+public final class SurfaceDataset {
+ public record Point(String id,double x,double y,double z){}
+ public final Path directory;public final String server;public String name="My Minecraft map";
+ public final List points=new ArrayList<>();public final Set coverage=new HashSet<>();
+ private final BufferedImage image=new BufferedImage(512,512,BufferedImage.TYPE_INT_ARGB);
+ private int minX,minZ,cursor;private String scanning;private int chunkX,chunkZ;
+ private SurfaceDataset(Path directory,String server,int x,int z){this.directory=directory;this.server=server;minX=x;minZ=z;for(int iz=0;iz<512;iz++)for(int ix=0;ix<512;ix++)image.setRGB(ix,iz,((ix/16+iz/16)%2==0)?0xff4c493d:0xff3b3932);}
+ public static SurfaceDataset create(String server)throws Exception{var c=Minecraft.getInstance();var root=c.gameDirectory.toPath().resolve("minegeo");Files.createDirectories(root);var dataset=new SurfaceDataset(root.resolve(UUID.randomUUID().toString()),server,(Math.floorDiv(c.player.getBlockX(),16)-16)*16,(Math.floorDiv(c.player.getBlockZ(),16)-16)*16);Files.createDirectories(dataset.directory);dataset.saveLocal();return dataset;}
+ public static SurfaceDataset open(String server)throws Exception{Path root=Minecraft.getInstance().gameDirectory.toPath().resolve("minegeo");if(Files.exists(root)){try(var dirs=Files.list(root)){for(Path path:dirs.sorted(Comparator.reverseOrder()).toList()){Path state=path.resolve("local.json");if(!Files.exists(state))continue;var j=JsonParser.parseString(Files.readString(state)).getAsJsonObject();if(!j.get("server").getAsString().equals(server))continue;var d=new SurfaceDataset(path,server,j.get("minX").getAsInt(),j.get("minZ").getAsInt());d.name=j.get("name").getAsString();for(var p:j.getAsJsonArray("viewpoints")){var v=p.getAsJsonObject();d.points.add(new Point(v.get("id").getAsString(),v.get("x").getAsDouble(),v.get("y").getAsDouble(),v.get("z").getAsDouble()));}for(var k:j.getAsJsonArray("coverage"))d.coverage.add(k.getAsString());if(Files.exists(path.resolve("map.png"))){var saved=ImageIO.read(path.resolve("map.png").toFile());d.image.getGraphics().drawImage(saved,0,0,null);}return d;}}}return create(server);}
+ public boolean contains(double x,double z){return x>=minX&&x=minZ&&z ci){if(Capture.active())ci.setReturnValue(90f);}
+}
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/GameRendererMixin.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/GameRendererMixin.java
new file mode 100644
index 0000000..6247bab
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/GameRendererMixin.java
@@ -0,0 +1,14 @@
+package dev.minegeo.fabric.mixin;
+import dev.minegeo.fabric.Capture;
+import net.minecraft.client.renderer.GameRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+@Mixin(GameRenderer.class)
+public class GameRendererMixin {
+ @Inject(method="update",at=@At("HEAD")) private void before(CallbackInfo ci){Capture.beforeFrame();}
+ @Inject(method="render",at=@At("RETURN")) private void after(CallbackInfo ci){Capture.afterFrame();}
+ @Inject(method={"renderItemInHand","bobView","bobHurt"},at=@At("HEAD"),cancellable=true) private void clean(CallbackInfo ci){if(Capture.active())ci.cancel();}
+}
diff --git a/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/WindowMixin.java b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/WindowMixin.java
new file mode 100644
index 0000000..9f431e2
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/java/dev/minegeo/fabric/mixin/WindowMixin.java
@@ -0,0 +1,10 @@
+package dev.minegeo.fabric.mixin;
+import dev.minegeo.fabric.Capture;
+import com.mojang.blaze3d.platform.Window;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.*;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+@Mixin(Window.class)
+public class WindowMixin {
+ @Inject(method={"getWidth","getHeight"},at=@At("HEAD"),cancellable=true) private void square(CallbackInfoReturnable ci){if(Capture.active())ci.setReturnValue(Capture.SIZE);}
+}
diff --git a/minecraft/fabric-modern/src/main/resources/fabric.mod.json b/minecraft/fabric-modern/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000..cbd1290
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/resources/fabric.mod.json
@@ -0,0 +1,11 @@
+{
+ "schemaVersion": 1,
+ "id": "minegeo",
+ "version": "${version}",
+ "name": "MineGeo Capture",
+ "description": "Capture six-face Minecraft panoramas and surface maps, then upload them to MineGeo.",
+ "environment": "client",
+ "entrypoints": {"client": ["dev.minegeo.fabric.MineGeoClient"]},
+ "mixins": ["minegeo.mixins.json"],
+ "depends": {"fabricloader": ">=0.19.5", "minecraft": "~${minecraftVersion}", "java": ">=25", "fabric-api": "*"}
+}
diff --git a/minecraft/fabric-modern/src/main/resources/minegeo.mixins.json b/minecraft/fabric-modern/src/main/resources/minegeo.mixins.json
new file mode 100644
index 0000000..31efc4e
--- /dev/null
+++ b/minecraft/fabric-modern/src/main/resources/minegeo.mixins.json
@@ -0,0 +1 @@
+{"required":true,"package":"dev.minegeo.fabric.mixin","compatibilityLevel":"JAVA_25","client":["GameRendererMixin","WindowMixin","${targetMixin}","CameraMixin"],"injectors":{"defaultRequire":1}}
diff --git a/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java b/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java
index d69c069..bc27ecf 100644
--- a/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java
+++ b/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java
@@ -5,19 +5,18 @@ import net.minecraft.client.gui.widget.*;
import net.minecraft.text.Text;
public final class CaptureScreen extends Screen {
- private TextFieldWidget website,name,target;
+ private TextFieldWidget name,target;
public CaptureScreen(){super(Text.literal("MineGeo Capture"));}
@Override protected void init(){var mod=MineGeoClient.INSTANCE;int x=width/2-130;
- website=new TextFieldWidget(textRenderer,x,50,260,20,Text.literal("MineGeo website"));website.setMaxLength(200);website.setText(mod.website);addDrawableChild(website);
name=new TextFieldWidget(textRenderer,x,92,260,20,Text.literal("Map name"));name.setMaxLength(80);name.setText(mod.dataset==null?"My Minecraft map":mod.dataset.name);addDrawableChild(name);
target=new TextFieldWidget(textRenderer,x,126,260,20,Text.literal("Add to map ID (optional)"));target.setMaxLength(36);target.setText(mod.targetMap);addDrawableChild(target);
- addDrawableChild(ButtonWidget.builder(Text.literal("Capture panorama"),b->{mod.website=website.getText();mod.targetMap=target.getText().trim();mod.capture();}).dimensions(x,154,126,20).build());
+ addDrawableChild(ButtonWidget.builder(Text.literal("Capture panorama"),b->{mod.targetMap=target.getText().trim();mod.capture();}).dimensions(x,154,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Preview files"),b->{if(mod.dataset!=null)net.minecraft.util.Util.getOperatingSystem().open(mod.dataset.directory.toFile());}).dimensions(x+134,154,126,20).build());
- addDrawableChild(ButtonWidget.builder(Text.literal("Link account"),b->{mod.website=website.getText();mod.targetMap=target.getText().trim();mod.pair();}).dimensions(x,182,126,20).build());
- addDrawableChild(ButtonWidget.builder(Text.literal("Upload / resume"),b->{mod.website=website.getText();mod.targetMap=target.getText().trim();mod.upload(name.getText());}).dimensions(x+134,182,126,20).build());
+ addDrawableChild(ButtonWidget.builder(Text.literal("Link account"),b->{mod.targetMap=target.getText().trim();mod.pair();}).dimensions(x,182,126,20).build());
+ addDrawableChild(ButtonWidget.builder(Text.literal("Upload / resume"),b->{mod.targetMap=target.getText().trim();mod.upload(name.getText());}).dimensions(x+134,182,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("New map at this location"),b->{mod.newMap();init(width,height);}).dimensions(x,210,260,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Close"),b->close()).dimensions(x,238,260,20).build());
}
- @Override public void render(DrawContext context,int mouseX,int mouseY,float delta){super.render(context,mouseX,mouseY,delta);int x=width/2-130;context.drawCenteredTextWithShadow(textRenderer,title,width/2,20,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"Website URL (HTTP for LAN testing)",x,38,0xffffffff);context.drawTextWithShadow(textRenderer,"Map name",x,80,0xffffffff);context.drawTextWithShadow(textRenderer,"Add to map ID (blank = new map)",x,114,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.drawCenteredTextWithShadow(textRenderer,Text.literal(counts),width/2,270,0xffffffff);int y=292;for(var line:textRenderer.wrapLines(Text.literal(mod.status),260)){context.drawTextWithShadow(textRenderer,line,x,y,0xffd5d5d5);y+=10;}}
+ @Override public void render(DrawContext context,int mouseX,int mouseY,float delta){super.render(context,mouseX,mouseY,delta);int x=width/2-130;context.drawCenteredTextWithShadow(textRenderer,title,width/2,20,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"MineGeo service",x,38,0xffffffff);context.drawTextWithShadow(textRenderer,MineGeoClient.website,x,54,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"Map name",x,80,0xffffffff);context.drawTextWithShadow(textRenderer,"Add to map ID (blank = new map)",x,114,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.drawCenteredTextWithShadow(textRenderer,Text.literal(counts),width/2,270,0xffffffff);int y=292;for(var line:textRenderer.wrapLines(Text.literal(mod.status),260)){context.drawTextWithShadow(textRenderer,line,x,y,0xffd5d5d5);y+=10;}}
@Override public boolean shouldPause(){return false;}
}
diff --git a/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java b/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java
index ec4b642..cc53fc1 100644
--- a/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java
+++ b/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java
@@ -17,20 +17,21 @@ public final class MineGeoClient implements ClientModInitializer {
public static MineGeoClient INSTANCE;
public final ExecutorService background=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"MineGeo upload");t.setDaemon(true);return t;});
public final Gson gson=new GsonBuilder().setPrettyPrinting().create();
- public volatile String website="http://localhost:3000",token="",status="Explore normally to collect surface coverage.";
+ public static final String website=UploadClient.WEBSITE;
+ public volatile String token="",status="Explore normally to collect surface coverage.";
public volatile String targetMap="";
public volatile boolean uploading=false;public SurfaceDataset dataset;private SurfaceDataset.Point pending;
private Path config;
- @Override public void onInitializeClient(){INSTANCE=this;config=MinecraftClient.getInstance().runDirectory.toPath().resolve("config/minegeo.json");try{if(Files.exists(config)){var j=JsonParser.parseString(Files.readString(config)).getAsJsonObject();website=j.get("website").getAsString();token=j.get("token").getAsString();if(j.has("targetMap"))targetMap=j.get("targetMap").getAsString();}}catch(Exception e){message("Could not read MineGeo configuration");}
+ @Override public void onInitializeClient(){INSTANCE=this;config=MinecraftClient.getInstance().runDirectory.toPath().resolve("config/minegeo.json");try{if(Files.exists(config)){var j=JsonParser.parseString(Files.readString(config)).getAsJsonObject();token=j.get("token").getAsString();if(j.has("targetMap"))targetMap=j.get("targetMap").getAsString();}}catch(Exception e){message("Could not read MineGeo configuration");}
var key=KeyBindingHelper.registerKeyBinding(new KeyBinding("key.minegeo.open",InputUtil.Type.KEYSYM,GLFW.GLFW_KEY_G,KeyBinding.Category.create(Identifier.of("minegeo","capture"))));
ClientTickEvents.END_CLIENT_TICK.register(c->{while(key.wasPressed()){if(Capture.active())Capture.cancel("Capture cancelled");else c.setScreen(new CaptureScreen());}if(c.world!=null&&c.player!=null&&c.world.getRegistryKey().getValue().toString().equals("minecraft:overworld")){try{if(dataset==null||!dataset.server.equals(server()))dataset=SurfaceDataset.open(server());if(!Capture.active()&&!uploading)dataset.scan(c);}catch(Exception e){message("Map collection: "+e.getMessage());}}CaptureFixture.tick(c);});
}
public String server(){var c=MinecraftClient.getInstance();return c.getCurrentServerEntry()!=null?c.getCurrentServerEntry().address:"Singleplayer";}
- public void saveConfig()throws Exception{Files.createDirectories(config.getParent());var j=new JsonObject();j.addProperty("website",website);j.addProperty("token",token);j.addProperty("targetMap",targetMap);Files.writeString(config,gson.toJson(j));}
+ public void saveConfig()throws Exception{Files.createDirectories(config.getParent());var j=new JsonObject();j.addProperty("token",token);j.addProperty("targetMap",targetMap);Files.writeString(config,gson.toJson(j));}
public void message(String text){status=text;var c=MinecraftClient.getInstance();if(c.player!=null)c.player.sendMessage(Text.literal("[MineGeo] "+text),false);}
public void capture(){try{if(dataset==null)throw new IllegalStateException("Join an Overworld and explore first");var c=MinecraftClient.getInstance();if(!dataset.contains(c.player.getX(),c.player.getZ()))throw new IllegalStateException("Outside this map's 512-block boundary. Start a new map here.");int limit=24;if(dataset.points.size()>=limit)throw new IllegalStateException("This capture already contains "+limit+" locations");pending=new SurfaceDataset.Point("p"+java.util.UUID.randomUUID().toString().replace("-","").substring(0,12),c.player.getX(),c.player.getEyeY(),c.player.getZ());c.setScreen(null);Capture.start(dataset.directory.resolve(pending.id()));message("Capturing six views. Stand still; press G to cancel.");}catch(Exception e){message(e.getMessage());}}
public void savedCapture(Path path){try{if(pending==null||!path.getFileName().toString().equals(pending.id()))return;dataset.points.add(pending);dataset.save();message("Panorama saved locally ("+dataset.points.size()+" total). Press G for preview or upload.");}catch(Exception e){message("Saving capture failed: "+e.getMessage());}finally{pending=null;}}
public void newMap(){try{if(uploading||Capture.active())throw new IllegalStateException("Finish the current operation first");dataset=SurfaceDataset.create(server());message("New 512 × 512 map started. Older maps remain in .minecraft/minegeo.");}catch(Exception e){message(e.getMessage());}}
- public void pair(){background.execute(()->{try{saveConfig();var uploader=new UploadClient(website,"");var pair=uploader.beginPairing();String code=pair.get("deviceCode").getAsString();String link=pair.get("verificationUri").getAsString();MinecraftClient.getInstance().execute(()->{message("Approve code "+pair.get("userCode").getAsString()+" at "+link);net.minecraft.util.Util.getOperatingSystem().open(link);});for(int i=0;i<120;i++){Thread.sleep(5000);var response=uploader.pollPairing(code);if(response.get("status").getAsString().equals("approved")){token=response.get("token").getAsString();saveConfig();MinecraftClient.getInstance().execute(()->message("Account linked. You can upload maps now."));return;}}throw new IllegalStateException("Pairing expired. Try linking again.");}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Pairing: "+e.getMessage()));}});}
- public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;final String target=targetMap.trim();if(!target.isBlank()&&(MinecraftClient.getInstance().getCurrentServerEntry()==null||selected==null||!java.util.Objects.equals(dev.minegeo.plugin.ServerAddress.normalize(server()),dev.minegeo.plugin.ServerAddress.normalize(selected.server)))){message("Join the server this capture belongs to before contributing.");return;}int minimum=target.isBlank()?5:1;if(selected==null||selected.points.size(){try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(website,token).upload(selected.directory,target.isBlank()?null:target, progress -> status=progress, () -> false);MinecraftClient.getInstance().execute(()->message("Upload received. Preview your selection and submit it in your dashboard."));}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});}
+ public void pair(){background.execute(()->{try{saveConfig();var uploader=new UploadClient("");var pair=uploader.beginPairing();String code=pair.get("deviceCode").getAsString();String link=pair.get("verificationUri").getAsString();MinecraftClient.getInstance().execute(()->{message("Approve code "+pair.get("userCode").getAsString()+" at "+link);net.minecraft.util.Util.getOperatingSystem().open(link);});for(int i=0;i<120;i++){Thread.sleep(5000);var response=uploader.pollPairing(code);if(response.get("status").getAsString().equals("approved")){token=response.get("token").getAsString();saveConfig();MinecraftClient.getInstance().execute(()->message("Account linked. You can upload maps now."));return;}}throw new IllegalStateException("Pairing expired. Try linking again.");}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Pairing: "+e.getMessage()));}});}
+ public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;final String target=targetMap.trim();if(!target.isBlank()&&(MinecraftClient.getInstance().getCurrentServerEntry()==null||selected==null||!java.util.Objects.equals(dev.minegeo.plugin.ServerAddress.normalize(server()),dev.minegeo.plugin.ServerAddress.normalize(selected.server)))){message("Join the server this capture belongs to before contributing.");return;}int minimum=target.isBlank()?5:1;if(selected==null||selected.points.size(){try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(token).upload(selected.directory,target.isBlank()?null:target, progress -> status=progress, () -> false);MinecraftClient.getInstance().execute(()->message("Upload received. Preview your selection and submit it in your dashboard."));}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});}
}
diff --git a/minecraft/fabric/src/main/java/dev/minegeo/fabric/SurfaceDataset.java b/minecraft/fabric/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
index edca438..1ab53bc 100644
--- a/minecraft/fabric/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
+++ b/minecraft/fabric/src/main/java/dev/minegeo/fabric/SurfaceDataset.java
@@ -29,6 +29,6 @@ public final class SurfaceDataset {
}
private JsonObject state(){var g=MineGeoClient.INSTANCE.gson;var j=new JsonObject();j.addProperty("server",server);j.addProperty("name",name);j.addProperty("minX",minX);j.addProperty("minZ",minZ);j.add("viewpoints",g.toJsonTree(points));j.add("coverage",g.toJsonTree(coverage));return j;}
private void saveLocal()throws Exception{Files.createDirectories(directory);Files.writeString(directory.resolve("local.json"),MineGeoClient.INSTANCE.gson.toJson(state()));ImageIO.write(image,"png",directory.resolve("map.png").toFile());}
- public void save()throws Exception{saveLocal();var j=new JsonObject();j.addProperty("format","minegeo-dataset/v1");j.addProperty("minecraftVersion","1.21.11");j.addProperty("dimension","minecraft:overworld");j.addProperty("north","-z");j.addProperty("source","fabric");j.addProperty("name",name);j.addProperty("description","Player-captured surface map. Checkerboard areas have not been explored.");j.addProperty("serverName",server);if(!server.equals("Singleplayer"))j.addProperty("serverAddress",dev.minegeo.plugin.ServerAddress.normalize(server));var b=new JsonObject();b.addProperty("minX",minX);b.addProperty("maxX",minX+512);b.addProperty("minZ",minZ);b.addProperty("maxZ",minZ+512);j.add("bounds",b);j.add("viewpoints",MineGeoClient.INSTANCE.gson.toJsonTree(points));var chunks=new JsonArray();for(String key:coverage){var v=new JsonArray();for(String number:key.split(","))v.add(Integer.parseInt(number));chunks.add(v);}j.add("coverage",chunks);var assets=new JsonArray();addAsset(assets,"map.png");for(Point p:points)for(String face:Capture.FACES)addAsset(assets,p.id()+"/"+face+".png");j.add("assets",assets);Files.writeString(directory.resolve("manifest.json"),MineGeoClient.INSTANCE.gson.toJson(j));}
+ public void save()throws Exception{saveLocal();var j=new JsonObject();j.addProperty("format","minegeo-dataset/v1");j.addProperty("minecraftVersion",net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("minecraft").orElseThrow().getMetadata().getVersion().getFriendlyString());j.addProperty("dimension","minecraft:overworld");j.addProperty("north","-z");j.addProperty("source","fabric");j.addProperty("name",name);j.addProperty("description","Player-captured surface map. Checkerboard areas have not been explored.");j.addProperty("serverName",server);if(!server.equals("Singleplayer"))j.addProperty("serverAddress",dev.minegeo.plugin.ServerAddress.normalize(server));var b=new JsonObject();b.addProperty("minX",minX);b.addProperty("maxX",minX+512);b.addProperty("minZ",minZ);b.addProperty("maxZ",minZ+512);j.add("bounds",b);j.add("viewpoints",MineGeoClient.INSTANCE.gson.toJsonTree(points));var chunks=new JsonArray();for(String key:coverage){var v=new JsonArray();for(String number:key.split(","))v.add(Integer.parseInt(number));chunks.add(v);}j.add("coverage",chunks);var assets=new JsonArray();addAsset(assets,"map.png");for(Point p:points)for(String face:Capture.FACES)addAsset(assets,p.id()+"/"+face+".png");j.add("assets",assets);Files.writeString(directory.resolve("manifest.json"),MineGeoClient.INSTANCE.gson.toJson(j));}
private void addAsset(JsonArray array,String file)throws Exception{Path p=directory.resolve(file);var j=new JsonObject();j.addProperty("path",file);j.addProperty("bytes",Files.size(p));j.addProperty("sha256",HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(p))));array.add(j);}
}
diff --git a/minecraft/fabric261/build.gradle b/minecraft/fabric261/build.gradle
new file mode 100644
index 0000000..ca32739
--- /dev/null
+++ b/minecraft/fabric261/build.gradle
@@ -0,0 +1,4 @@
+plugins { id 'net.fabricmc.fabric-loom' version '1.16.3' }
+ext.minecraftVersion = '26.1'
+ext.fabricApiVersion = '0.145.1+26.1'
+apply from: '../fabric-modern/build.gradle'
diff --git a/minecraft/fabric261/src/main/java/dev/minegeo/fabric/ClientCompat.java b/minecraft/fabric261/src/main/java/dev/minegeo/fabric/ClientCompat.java
new file mode 100644
index 0000000..cde371a
--- /dev/null
+++ b/minecraft/fabric261/src/main/java/dev/minegeo/fabric/ClientCompat.java
@@ -0,0 +1,13 @@
+package dev.minegeo.fabric;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import com.mojang.blaze3d.pipeline.*;
+final class ClientCompat {
+ static boolean hudHidden(Minecraft c){return c.options.hideGui;}
+ static void hudHidden(Minecraft c,boolean hidden){c.options.hideGui=hidden;}
+ static Screen screen(Minecraft c){return c.screen;}
+ static void screen(Minecraft c,Screen screen){c.setScreen(screen);}
+ static void message(Minecraft c,Component message){c.gui.getChat().addClientSystemMessage(message);}
+ static RenderTarget target(){return new TextureTarget("MineGeo capture",Capture.SIZE,Capture.SIZE,true);}
+}
diff --git a/minecraft/fabric261/src/main/java/dev/minegeo/fabric/mixin/MinecraftTargetMixin.java b/minecraft/fabric261/src/main/java/dev/minegeo/fabric/mixin/MinecraftTargetMixin.java
new file mode 100644
index 0000000..d1f8829
--- /dev/null
+++ b/minecraft/fabric261/src/main/java/dev/minegeo/fabric/mixin/MinecraftTargetMixin.java
@@ -0,0 +1,12 @@
+package dev.minegeo.fabric.mixin;
+import dev.minegeo.fabric.Capture;
+import net.minecraft.client.Minecraft;
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.*;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+@Mixin(Minecraft.class)
+public class MinecraftTargetMixin {
+ @Inject(method="getMainRenderTarget",at=@At("HEAD"),cancellable=true)
+ private void square(CallbackInfoReturnable ci){if(Capture.active()&&Capture.framebuffer()!=null)ci.setReturnValue(Capture.framebuffer());}
+}
diff --git a/minecraft/fabric262/build.gradle b/minecraft/fabric262/build.gradle
new file mode 100644
index 0000000..0f5678e
--- /dev/null
+++ b/minecraft/fabric262/build.gradle
@@ -0,0 +1,4 @@
+plugins { id 'net.fabricmc.fabric-loom' version '1.16.3' }
+ext.minecraftVersion = '26.2'
+ext.fabricApiVersion = '0.161.0+26.2'
+apply from: '../fabric-modern/build.gradle'
diff --git a/minecraft/fabric262/src/main/java/dev/minegeo/fabric/ClientCompat.java b/minecraft/fabric262/src/main/java/dev/minegeo/fabric/ClientCompat.java
new file mode 100644
index 0000000..e3b52eb
--- /dev/null
+++ b/minecraft/fabric262/src/main/java/dev/minegeo/fabric/ClientCompat.java
@@ -0,0 +1,14 @@
+package dev.minegeo.fabric;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import com.mojang.blaze3d.pipeline.*;
+import com.mojang.blaze3d.GpuFormat;
+final class ClientCompat {
+ static boolean hudHidden(Minecraft c){return c.gui.hud.isHidden();}
+ static void hudHidden(Minecraft c,boolean hidden){if(c.gui.hud.isHidden()!=hidden)c.gui.hud.toggle();}
+ static Screen screen(Minecraft c){return c.gui.screen();}
+ static void screen(Minecraft c,Screen screen){c.gui.setScreen(screen);}
+ static void message(Minecraft c,Component message){c.gui.hud.getChat().addClientSystemMessage(message);}
+ static RenderTarget target(){return new TextureTarget("MineGeo capture",Capture.SIZE,Capture.SIZE,true,GpuFormat.RGBA8_UNORM);}
+}
diff --git a/minecraft/fabric262/src/main/java/dev/minegeo/fabric/mixin/GameRendererTargetMixin.java b/minecraft/fabric262/src/main/java/dev/minegeo/fabric/mixin/GameRendererTargetMixin.java
new file mode 100644
index 0000000..107898a
--- /dev/null
+++ b/minecraft/fabric262/src/main/java/dev/minegeo/fabric/mixin/GameRendererTargetMixin.java
@@ -0,0 +1,16 @@
+package dev.minegeo.fabric.mixin;
+import dev.minegeo.fabric.Capture;
+import net.minecraft.client.renderer.GameRenderer;
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.injection.*;
+@Mixin(GameRenderer.class)
+public class GameRendererTargetMixin {
+ @Shadow @Final private RenderTarget mainRenderTarget;
+ // 26.2 moved the framebuffer into GameRenderer. Redirect reads through the
+ // game's rendering abstraction; keep the actual window target intact.
+ @Redirect(method="*",at=@At(value="FIELD",target="Lnet/minecraft/client/renderer/GameRenderer;mainRenderTarget:Lcom/mojang/blaze3d/pipeline/RenderTarget;",opcode=180))
+ private RenderTarget square(GameRenderer renderer){return Capture.active()&&Capture.framebuffer()!=null?Capture.framebuffer():mainRenderTarget;}
+}
diff --git a/minecraft/gradle/wrapper/gradle-wrapper.properties b/minecraft/gradle/wrapper/gradle-wrapper.properties
index 23449a2..dbc3ce4 100644
--- a/minecraft/gradle/wrapper/gradle-wrapper.properties
+++ b/minecraft/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
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 b32a41a..83b0057 100644
--- a/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java
+++ b/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java
@@ -26,7 +26,7 @@ public final class MineGeoPlugin extends JavaPlugin {
private final Gson gson=new GsonBuilder().setPrettyPrinting().create();
private volatile Path latest;
@Override public void onEnable(){
- saveDefaultConfig();Objects.requireNonNull(getCommand("minegeo")).setExecutor(this);
+ saveDefaultConfig();if(getConfig().isSet("website")){getConfig().set("website",null);saveConfig();}Objects.requireNonNull(getCommand("minegeo")).setExecutor(this);
try {
Files.createDirectories(getDataFolder().toPath().resolve("exports"));
Path root=getDataFolder().toPath().toAbsolutePath().normalize(),pointer=root.resolve("latest-export.txt");
@@ -42,7 +42,7 @@ public final class MineGeoPlugin extends JavaPlugin {
// 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