Add Minecraft 26.x support and version Java artifacts
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<Header />
|
||||
@@ -33,7 +35,9 @@ export default async function Downloads() {
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{t("Minecraft Java 1.21.11 + Fabric Loader + Fabric API")}
|
||||
{t(
|
||||
"Minecraft Java 1.21.11, 26.1 & 26.2 + Fabric Loader + Fabric API",
|
||||
)}
|
||||
</li>
|
||||
<li>{t("Open the capture panel with G")}</li>
|
||||
<li>{t("Link your account through a browser pairing code")}</li>
|
||||
@@ -48,24 +52,35 @@ export default async function Downloads() {
|
||||
</li>
|
||||
<li>{t("Contributions check the recorded server address")}</li>
|
||||
</ul>
|
||||
{fabric ? (
|
||||
{versions.map((version) =>
|
||||
existsSync(
|
||||
`public/tools/mod/minegeo-fabric-${version}-${release}.jar`,
|
||||
) ? (
|
||||
<a
|
||||
key={version}
|
||||
className="button primary"
|
||||
href="/tools/mod/minegeo-fabric-1.21.11.jar"
|
||||
href={`/tools/mod/minegeo-fabric-${version}-${release}.jar`}
|
||||
download
|
||||
style={{ marginRight: 8, marginBottom: 8 }}
|
||||
>
|
||||
<Download size={16} /> {t("Download Fabric mod")}
|
||||
<Download size={16} /> {t("Download Fabric mod")} · {version}
|
||||
</a>
|
||||
) : (
|
||||
<p className="muted">
|
||||
{t("The Fabric build is not available yet.")}
|
||||
<p className="muted" key={version}>
|
||||
{version}: {t("The Fabric build is not available yet.")}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
<p className="muted" style={{ fontSize: 11, marginTop: 20 }}>
|
||||
{t(
|
||||
"Install the JAR in your Minecraft mods folder alongside Fabric API.",
|
||||
)}
|
||||
</p>
|
||||
<p className="muted">
|
||||
{t(
|
||||
"Java 21 for 1.21.11; Java 25 for 26.1 and 26.2. Both tools connect to minegeo.vercel.app automatically.",
|
||||
)}
|
||||
</p>
|
||||
</article>
|
||||
<article className="tool-card">
|
||||
<Server size={34} />
|
||||
@@ -77,7 +92,7 @@ export default async function Downloads() {
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>{t("Bukkit/Paper 1.21.11 · Java 21")}</li>
|
||||
<li>{t("Bukkit/Paper 1.21.11, 26.1 & 26.2")}</li>
|
||||
<li>{t("Select a bounded area and export existing chunks")}</li>
|
||||
<li>{t("Choose up to 24 random, safe surface viewpoints")}</li>
|
||||
<li>
|
||||
@@ -90,11 +105,7 @@ export default async function Downloads() {
|
||||
<li>{t("Vanilla blocks/fluids, simplified lighting")}</li>
|
||||
</ul>
|
||||
{plugin ? (
|
||||
<a
|
||||
className="button primary"
|
||||
href="/tools/plugin/minegeo-plugin-1.21.11.jar"
|
||||
download
|
||||
>
|
||||
<a className="button primary" href={pluginPath} download>
|
||||
<Download size={16} /> {t("Download server plugin")}
|
||||
</a>
|
||||
) : (
|
||||
@@ -104,7 +115,7 @@ export default async function Downloads() {
|
||||
)}
|
||||
<pre>
|
||||
{
|
||||
"/minegeo website https://your-minegeo-site.example\n/minegeo server play.your-server.example\n/minegeo link <dashboard token>\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 <dashboard token>\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"
|
||||
}
|
||||
</pre>
|
||||
<p className="muted">
|
||||
@@ -117,10 +128,9 @@ export default async function Downloads() {
|
||||
)}
|
||||
</p>
|
||||
<p className="muted" style={{ fontSize: 11 }}>
|
||||
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.",
|
||||
)}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
"directory": ".cache/plugin-regression-1791360575134"
|
||||
}
|
||||
@@ -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 <token copied from the dashboard>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
19
minecraft/fabric-modern/build.gradle
Normal file
19
minecraft/fabric-modern/build.gradle
Normal file
@@ -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" }
|
||||
@@ -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;}}
|
||||
}
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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()<minimum){message("Capture at least "+minimum+" locations before uploading.");return;}uploading=true;background.execute(()->{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;}});}
|
||||
}
|
||||
@@ -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<Point> points=new ArrayList<>();public final Set<String> 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<minX+512&&z>=minZ&&z<minZ+512;}
|
||||
public void scan(Minecraft c){
|
||||
if(scanning==null){int px=Math.floorDiv(c.player.getBlockX(),16),pz=Math.floorDiv(c.player.getBlockZ(),16);outer:for(int radius=0;radius<=12;radius++)for(int dz=-radius;dz<=radius;dz++)for(int dx=-radius;dx<=radius;dx++){if(Math.max(Math.abs(dx),Math.abs(dz))!=radius)continue;int cx=px+dx,cz=pz+dz;String key=cx+","+cz;if(coverage.contains(key)||!contains(cx*16,cz*16))continue;if(c.level.getChunk(cx,cz,ChunkStatus.FULL,false)==null)continue;scanning=key;chunkX=cx;chunkZ=cz;cursor=0;break outer;}}
|
||||
if(scanning==null)return;if(c.level.getChunk(chunkX,chunkZ,ChunkStatus.FULL,false)==null){scanning=null;return;}
|
||||
var pos=new BlockPos.MutableBlockPos();for(int n=0;n<64&&cursor<256;n++,cursor++){int x=chunkX*16+(cursor%16),z=chunkZ*16+(cursor/16),y=c.level.getHeight(Heightmap.Types.WORLD_SURFACE,x,z)-1;pos.set(x,y,z);int color=c.level.getBlockState(pos).getMapColor(c.level,pos).col;image.setRGB(x-minX,z-minZ,0xff000000|color);}
|
||||
if(cursor==256){coverage.add(scanning);scanning=null;}
|
||||
}
|
||||
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",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);}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.minegeo.fabric.mixin;
|
||||
import dev.minegeo.fabric.Capture;
|
||||
import net.minecraft.client.Camera;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
@Mixin(Camera.class)
|
||||
public abstract class CameraMixin {
|
||||
@Shadow protected abstract void setRotation(float yaw,float pitch);
|
||||
@Inject(method="update",at=@At("TAIL")) private void direction(CallbackInfo ci){if(Capture.active())setRotation(Capture.yaw(),Capture.pitch());}
|
||||
@Inject(method="getFov",at=@At("HEAD"),cancellable=true) private void fov(CallbackInfoReturnable<Float> ci){if(Capture.active())ci.setReturnValue(90f);}
|
||||
}
|
||||
@@ -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();}
|
||||
}
|
||||
@@ -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<Integer> ci){if(Capture.active())ci.setReturnValue(Capture.SIZE);}
|
||||
}
|
||||
11
minecraft/fabric-modern/src/main/resources/fabric.mod.json
Normal file
11
minecraft/fabric-modern/src/main/resources/fabric.mod.json
Normal file
@@ -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": "*"}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"required":true,"package":"dev.minegeo.fabric.mixin","compatibilityLevel":"JAVA_25","client":["GameRendererMixin","WindowMixin","${targetMixin}","CameraMixin"],"injectors":{"defaultRequire":1}}
|
||||
@@ -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;}
|
||||
}
|
||||
|
||||
@@ -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()<minimum){message("Capture at least "+minimum+" locations before uploading.");return;}uploading=true;background.execute(()->{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()<minimum){message("Capture at least "+minimum+" locations before uploading.");return;}uploading=true;background.execute(()->{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;}});}
|
||||
}
|
||||
|
||||
@@ -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);}
|
||||
}
|
||||
|
||||
4
minecraft/fabric261/build.gradle
Normal file
4
minecraft/fabric261/build.gradle
Normal file
@@ -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'
|
||||
@@ -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);}
|
||||
}
|
||||
@@ -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<RenderTarget> ci){if(Capture.active()&&Capture.framebuffer()!=null)ci.setReturnValue(Capture.framebuffer());}
|
||||
}
|
||||
4
minecraft/fabric262/build.gradle
Normal file
4
minecraft/fabric262/build.gradle
Normal file
@@ -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'
|
||||
@@ -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);}
|
||||
}
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 <http(s)://address:port>","/minegeo server <hostname[:port]> — capture server address","/minegeo link <dashboard token>","/minegeo area <world> <minX> <minZ> <maxX> <maxZ>","/minegeo export <map name> — saves a local export","/minegeo upload — uploads the latest completed export","/minegeo contribute <map ID> — add panoramas (24 owner, 12 guests)","/minegeo status | cancel | reload"});return true;}
|
||||
if(args.length==0||args[0].equalsIgnoreCase("help")) {sender.sendMessage(new String[]{"MineGeo commands:","/minegeo website — show the built-in MineGeo service","/minegeo server <hostname[:port]> — capture server address","/minegeo link <dashboard token>","/minegeo area <world> <minX> <minZ> <maxX> <maxZ>","/minegeo export <map name> — saves a local export","/minegeo upload — uploads the latest completed export","/minegeo contribute <map ID> — add panoramas (24 owner, 12 guests)","/minegeo status | cancel | reload"});return true;}
|
||||
switch(args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "area" -> {
|
||||
requireIdle();if(args.length!=6)throw new IllegalArgumentException("Usage: /minegeo area <world> <minX> <minZ> <maxX> <maxZ>");
|
||||
@@ -54,7 +54,7 @@ public final class MineGeoPlugin extends JavaPlugin {
|
||||
case "status" -> {sender.sendMessage("MineGeo: "+status);if(latest!=null)sender.sendMessage("Latest complete export: "+latest.toAbsolutePath());}
|
||||
case "cancel" -> {cancelled.set(true);sender.sendMessage(busy?"MineGeo cancellation requested. Completed exports are kept.":"MineGeo: no operation is running.");}
|
||||
case "reload" -> {requireIdle();reloadConfig();sender.sendMessage("MineGeo configuration reloaded.");}
|
||||
case "website" -> {requireIdle();if(args.length!=2)throw new IllegalArgumentException("Usage: /minegeo website <http(s)://address:port>");new UploadClient(args[1],"");getConfig().set("website",args[1]);saveConfig();sender.sendMessage("MineGeo website saved: "+args[1]);}
|
||||
case "website" -> {if(args.length!=1)throw new IllegalArgumentException("The MineGeo website is fixed to "+UploadClient.WEBSITE+". No URL configuration is needed.");sender.sendMessage("MineGeo website: "+UploadClient.WEBSITE);}
|
||||
case "server" -> {requireIdle();if(args.length!=2)throw new IllegalArgumentException("Usage: /minegeo server <hostname[:port]>");String address=ServerAddress.normalize(args[1]);getConfig().set("server-address",address);saveConfig();sender.sendMessage("Capture server address saved: "+address+". Export again to record it.");}
|
||||
case "link" -> {requireIdle();if(args.length!=2||args[1].isBlank())throw new IllegalArgumentException("Usage: /minegeo link <dashboard token>");getConfig().set("token",args[1]);saveConfig();sender.sendMessage("MineGeo contributor token saved.");}
|
||||
case "export" -> export(sender,args.length>1?String.join(" ",Arrays.copyOfRange(args,1,args.length)):"Overworld Explorer");
|
||||
@@ -69,8 +69,8 @@ public final class MineGeoPlugin extends JavaPlugin {
|
||||
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());
|
||||
String website=getConfig().getString("website"),token=getConfig().getString("token");if(token==null||token.isBlank())throw new IOException("Create a token in the website dashboard, then /minegeo link <token>.");
|
||||
UploadClient client=new UploadClient(website,token);Path directory=latest;busy=true;cancelled.set(false);status="Uploading "+directory.getFileName();
|
||||
String website=UploadClient.WEBSITE,token=getConfig().getString("token");if(token==null||token.isBlank())throw new IOException("Create a token in the website dashboard, then /minegeo link <token>.");
|
||||
UploadClient client=new UploadClient(token);Path directory=latest;busy=true;cancelled.set(false);status="Uploading "+directory.getFileName();
|
||||
Bukkit.getScheduler().runTaskAsynchronously(this,()->{try{client.upload(directory,targetMap,message->status=message,cancelled::get);report(sender,"Upload complete. Open "+website+"/dashboard to preview and submit "+(targetMap==null?"the private map for moderator review.":"the panoramas to the map owner."));}catch(Exception ex){fail(sender,"Upload",ex);}finally{busy=false;}});
|
||||
sender.sendMessage("MineGeo upload started. /minegeo status shows progress; retry /minegeo upload after a connection failure.");
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public final class MineGeoPlugin extends JavaPlugin {
|
||||
List<int[]> chosen=candidates.subList(0,Math.min(desired,candidates.size()));
|
||||
for(int i=0;i<chosen.size();i++){int[] p=chosen.get(i);JsonObject point=new JsonObject();point.addProperty("id","p"+i);point.addProperty("x",p[0]+.5);point.addProperty("y",p[1]+1.62);point.addProperty("z",p[2]+.5);points.add(point);}
|
||||
if(Files.size(terrain)>50L*1024*1024)throw new IOException("Compressed export exceeds 50 MiB. Select a smaller area.");
|
||||
JsonObject manifest=new JsonObject();manifest.addProperty("format","minegeo-dataset/v1");manifest.addProperty("minecraftVersion","1.21.11");manifest.addProperty("dimension","minecraft:overworld");manifest.addProperty("source","plugin");manifest.addProperty("name",name);manifest.addProperty("description","Vanilla surface terrain exported from "+serverName);manifest.addProperty("serverName",serverName);if(serverAddress!=null)manifest.addProperty("serverAddress",serverAddress);manifest.addProperty("north","-z");manifest.add("bounds",bounds);manifest.add("viewpoints",points);
|
||||
JsonObject manifest=new JsonObject();manifest.addProperty("format","minegeo-dataset/v1");manifest.addProperty("minecraftVersion",Bukkit.getBukkitVersion().split("[-+]",2)[0].replaceFirst("[.]build[.].*$",""));manifest.addProperty("dimension","minecraft:overworld");manifest.addProperty("source","plugin");manifest.addProperty("name",name);manifest.addProperty("description","Vanilla surface terrain exported from "+serverName);manifest.addProperty("serverName",serverName);if(serverAddress!=null)manifest.addProperty("serverAddress",serverAddress);manifest.addProperty("north","-z");manifest.add("bounds",bounds);manifest.add("viewpoints",points);
|
||||
JsonArray assets=new JsonArray();JsonObject asset=new JsonObject();asset.addProperty("path","terrain.json.gz");asset.addProperty("bytes",Files.size(terrain));MessageDigest digest=MessageDigest.getInstance("SHA-256");try(InputStream input=Files.newInputStream(terrain)){byte[] buffer=new byte[65536];int read;while((read=input.read(buffer))!=-1)digest.update(buffer,0,read);}asset.addProperty("sha256",HexFormat.of().formatHex(digest.digest()));assets.add(asset);manifest.add("assets",assets);
|
||||
// The manifest is the completion marker. Partial folders never become upload candidates.
|
||||
if(cancelled.get())throw new IOException("Cancelled");Files.writeString(out.resolve("manifest.json.tmp"),gson.toJson(manifest));Files.move(out.resolve("manifest.json.tmp"),out.resolve("manifest.json"),StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
@@ -12,6 +12,8 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** Shared TUS client. State is saved beside each completed local dataset. */
|
||||
public final class UploadClient {
|
||||
public static final String WEBSITE="https://minegeo.vercel.app";
|
||||
public UploadClient(String token){this(WEBSITE,token);}
|
||||
private final String website,token;
|
||||
// Next's HTTP server does not support Java's cleartext HTTP/2 upgrade handshake.
|
||||
private final HttpClient client=HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).connectTimeout(Duration.ofSeconds(30)).build();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
website: 'http://localhost:3000'
|
||||
# The MineGeo service URL is built in: https://minegeo.vercel.app
|
||||
token: ''
|
||||
server-name: 'My Minecraft server'
|
||||
# Public Minecraft connection address, matching the address used by Fabric players.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: MineGeo
|
||||
version: 0.1.2
|
||||
version: 0.1.3
|
||||
main: dev.minegeo.plugin.MineGeoPlugin
|
||||
api-version: '1.21'
|
||||
description: Bounded terrain export for MineGeo panorama guessing
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pluginManagement { repositories { maven { url = 'https://maven.fabricmc.net/' }; gradlePluginPortal(); mavenCentral() } }
|
||||
rootProject.name = 'minegeo-tools'
|
||||
include 'plugin', 'fabric'
|
||||
include 'plugin', 'fabric', 'fabric261', 'fabric262'
|
||||
|
||||
BIN
public/tools/mod/minegeo-fabric-1.21.11-0.1.3.jar
Normal file
BIN
public/tools/mod/minegeo-fabric-1.21.11-0.1.3.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/tools/mod/minegeo-fabric-26.1-0.1.3.jar
Normal file
BIN
public/tools/mod/minegeo-fabric-26.1-0.1.3.jar
Normal file
Binary file not shown.
BIN
public/tools/mod/minegeo-fabric-26.1.jar
Normal file
BIN
public/tools/mod/minegeo-fabric-26.1.jar
Normal file
Binary file not shown.
BIN
public/tools/mod/minegeo-fabric-26.2-0.1.3.jar
Normal file
BIN
public/tools/mod/minegeo-fabric-26.2-0.1.3.jar
Normal file
Binary file not shown.
BIN
public/tools/mod/minegeo-fabric-26.2.jar
Normal file
BIN
public/tools/mod/minegeo-fabric-26.2.jar
Normal file
Binary file not shown.
BIN
public/tools/plugin/minegeo-plugin-0.1.3.jar
Normal file
BIN
public/tools/plugin/minegeo-plugin-0.1.3.jar
Normal file
Binary file not shown.
Binary file not shown.
@@ -3,17 +3,17 @@ import { createServer } from "node:http";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { resolve, sep } from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { assetsRoot, prepareAssets } from "../scripts/assets.mjs";
|
||||
import { prepareAssets } from "../scripts/assets.mjs";
|
||||
import { assertTerrainImage } from "./image-validation.mjs";
|
||||
/** @param {import('../shared/protocol').TerrainSnapshot} snapshot
|
||||
* @param {Array<{id:string,x:number,y:number,z:number}>} viewpoints
|
||||
* @param {string} output
|
||||
* @param {{size?:number,signal?:AbortSignal,onProgress?:(event:{percent:number,message:string})=>Promise<void>}} [options] */
|
||||
* @param {{size?:number,minecraftVersion?:string,signal?:AbortSignal,onProgress?:(event:{percent:number,message:string})=>Promise<void>}} [options] */
|
||||
export async function renderSnapshot(
|
||||
snapshot,
|
||||
viewpoints,
|
||||
output,
|
||||
{ size = 1024, signal, onProgress } = {},
|
||||
{ size = 1024, minecraftVersion = "1.21.11", signal, onProgress } = {},
|
||||
) {
|
||||
const start = Date.now();
|
||||
if (signal?.aborted) throw new Error("Rendering cancelled");
|
||||
@@ -21,7 +21,7 @@ export async function renderSnapshot(
|
||||
percent: 22,
|
||||
message: "Checking Minecraft models and textures",
|
||||
});
|
||||
await prepareAssets();
|
||||
const assetsRoot = await prepareAssets(minecraftVersion);
|
||||
await mkdir(output, { recursive: true });
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
@@ -207,9 +207,7 @@ export async function renderSnapshot(
|
||||
if (map.data.startsWith("data:image/webp;"))
|
||||
await writeFile(`${output}/map.webp`, mapBytes);
|
||||
else
|
||||
await sharp(mapBytes)
|
||||
.webp({ quality: 92 })
|
||||
.toFile(`${output}/map.webp`);
|
||||
await sharp(mapBytes).webp({ quality: 92 }).toFile(`${output}/map.webp`);
|
||||
stats.map = { width: map.width, height: map.height, cameraY: 320 };
|
||||
await report({
|
||||
percent: 90,
|
||||
|
||||
@@ -2,7 +2,11 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { unzipSync } from "fflate";
|
||||
export const assetsRoot = ".cache/minecraft/1.21.11";
|
||||
const completionMarker = `${assetsRoot}/.minegeo-assets.json`;
|
||||
export function assetsDirectory(version = "1.21.11") {
|
||||
if (!/^(?:1\.21\.11|26\.[12](?:\.\d+)*)$/.test(version))
|
||||
throw new Error("Unsupported Minecraft asset version");
|
||||
return `.cache/minecraft/${version}`;
|
||||
}
|
||||
const requiredAssets = [
|
||||
"blockstates/grass_block.json",
|
||||
"blockstates/stone.json",
|
||||
@@ -14,12 +18,14 @@ const requiredAssets = [
|
||||
"textures/block/stone.png",
|
||||
"textures/block/water_still.png",
|
||||
];
|
||||
async function assetsReady() {
|
||||
async function assetsReady(minecraftVersion) {
|
||||
const assetsRoot = assetsDirectory(minecraftVersion);
|
||||
const completionMarker = `${assetsRoot}/.minegeo-assets.json`;
|
||||
try {
|
||||
const marker = JSON.parse(await readFile(completionMarker, "utf8"));
|
||||
if (
|
||||
marker.format !== "minegeo-assets/v2" ||
|
||||
marker.minecraftVersion !== "1.21.11" ||
|
||||
marker.minecraftVersion !== minecraftVersion ||
|
||||
!marker.files
|
||||
)
|
||||
return false;
|
||||
@@ -39,10 +45,12 @@ async function assetsReady() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export async function prepareAssets() {
|
||||
if (await assetsReady()) return;
|
||||
export async function prepareAssets(minecraftVersion = "1.21.11") {
|
||||
const assetsRoot = assetsDirectory(minecraftVersion);
|
||||
const completionMarker = `${assetsRoot}/.minegeo-assets.json`;
|
||||
if (await assetsReady(minecraftVersion)) return assetsRoot;
|
||||
console.log(
|
||||
"Preparing Minecraft 1.21.11 models and textures for terrain rendering…",
|
||||
`Preparing Minecraft ${minecraftVersion} models and textures for terrain rendering…`,
|
||||
);
|
||||
const manifest = await (
|
||||
await fetch(
|
||||
@@ -50,13 +58,14 @@ export async function prepareAssets() {
|
||||
{ signal: AbortSignal.timeout(60000) },
|
||||
)
|
||||
).json();
|
||||
const entry = manifest.versions.find((v) => v.id === "1.21.11");
|
||||
if (!entry) throw new Error("Official Minecraft 1.21.11 metadata missing");
|
||||
const entry = manifest.versions.find((v) => v.id === minecraftVersion);
|
||||
if (!entry)
|
||||
throw new Error(`Official Minecraft ${minecraftVersion} metadata missing`);
|
||||
const version = await (
|
||||
await fetch(entry.url, { signal: AbortSignal.timeout(60000) })
|
||||
).json();
|
||||
await mkdir(".cache/minecraft", { recursive: true });
|
||||
const jarPath = ".cache/minecraft/client-1.21.11.jar";
|
||||
const jarPath = `.cache/minecraft/client-${minecraftVersion}.jar`;
|
||||
let jar;
|
||||
try {
|
||||
jar = await readFile(jarPath);
|
||||
@@ -101,16 +110,17 @@ export async function prepareAssets() {
|
||||
completionMarker,
|
||||
JSON.stringify({
|
||||
format: "minegeo-assets/v2",
|
||||
minecraftVersion: "1.21.11",
|
||||
minecraftVersion,
|
||||
clientSha1: version.downloads.client.sha1,
|
||||
files: Object.keys(entries).length,
|
||||
}),
|
||||
);
|
||||
if (!(await assetsReady()))
|
||||
if (!(await assetsReady(minecraftVersion)))
|
||||
throw new Error(
|
||||
"Minecraft rendering assets are incomplete or corrupt. Run npm run setup to repair them.",
|
||||
);
|
||||
console.log(
|
||||
`Prepared ${Object.keys(entries).length} verified vanilla model/texture assets.`,
|
||||
);
|
||||
return assetsRoot;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, copyFile } from "node:fs/promises";
|
||||
import { mkdir, copyFile, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
const windows = process.platform === "win32";
|
||||
const command = windows ? "gradlew.bat" : "sh";
|
||||
const targets = process.argv.slice(2);
|
||||
if (!targets.some((target) => !target.startsWith("-")))
|
||||
targets.unshift(":plugin:build", ":fabric:build");
|
||||
targets.unshift(
|
||||
":plugin:build",
|
||||
":fabric:build",
|
||||
":fabric261:build",
|
||||
":fabric262:build",
|
||||
);
|
||||
const version = (await readFile("minecraft/build.gradle", "utf8")).match(
|
||||
/version = '([^']+)'/,
|
||||
)?.[1];
|
||||
if (!version) throw new Error("Minecraft tools release version is missing");
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, windows ? targets : ["./gradlew", ...targets], {
|
||||
cwd: "minecraft",
|
||||
@@ -18,15 +28,28 @@ await new Promise((resolve, reject) => {
|
||||
code === 0 ? resolve() : reject(new Error(`Gradle exited ${code}`)),
|
||||
);
|
||||
});
|
||||
for (const name of ["plugin", "fabric"]) {
|
||||
const directory = `public/tools/${name === "fabric" ? "mod" : "plugin"}`;
|
||||
const file = `minegeo-${name}-1.21.11.jar`,
|
||||
path = `minecraft/${name}/build/libs/${file}`;
|
||||
if (existsSync(path)) {
|
||||
for (const [name, kind, minecraftVersion] of [
|
||||
["plugin", "plugin", "1.21.11"],
|
||||
["fabric", "fabric", "1.21.11"],
|
||||
["fabric261", "fabric", "26.1"],
|
||||
["fabric262", "fabric", "26.2"],
|
||||
]) {
|
||||
const directory = `public/tools/${kind === "fabric" ? "mod" : "plugin"}`;
|
||||
const file = `minegeo-${kind}-${minecraftVersion}.jar`;
|
||||
const buildRoot = process.env.MINEGEO_JAVA_BUILD_ROOT
|
||||
? path.join(process.env.MINEGEO_JAVA_BUILD_ROOT, name)
|
||||
: `minecraft/${name}/build`;
|
||||
const artifact = path.join(buildRoot, "libs", file);
|
||||
if (existsSync(artifact)) {
|
||||
await mkdir(directory, { recursive: true });
|
||||
await copyFile(path, `${directory}/${file}`);
|
||||
await copyFile(artifact, `${directory}/${file}`);
|
||||
const releaseFile =
|
||||
kind === "plugin"
|
||||
? `minegeo-plugin-${version}.jar`
|
||||
: `minegeo-fabric-${minecraftVersion}-${version}.jar`;
|
||||
await copyFile(artifact, `${directory}/${releaseFile}`);
|
||||
console.log(
|
||||
`Updated ${directory}/${file}. Include this JAR in your next commit.`,
|
||||
`Updated ${directory}/${releaseFile} and its compatibility download.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ if (
|
||||
throw new Error(
|
||||
"This draft contains accepted contributions. Upload a new capture instead of reprocessing mixed media.",
|
||||
);
|
||||
if (dataset.source === "plugin") await prepareAssets();
|
||||
if (dataset.source === "plugin")
|
||||
await prepareAssets(input.manifest.minecraftVersion);
|
||||
|
||||
// This maintenance command runs with the worker stopped. Compare-and-set
|
||||
// guards reject intervening publication, ownership of a job, or another repair.
|
||||
|
||||
205
scripts/test-minecraft-versions.mjs
Normal file
205
scripts/test-minecraft-versions.mjs
Normal file
@@ -0,0 +1,205 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import path from "node:path";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
|
||||
// Disposable servers listen only on loopback. No fixture uploads or tokens are
|
||||
// sent to MineGeo. Keep exports/logs locally as compatibility evidence.
|
||||
const root = path.resolve(".cache/minecraft-compat");
|
||||
const java = process.env.JAVA_HOME
|
||||
? path.join(
|
||||
process.env.JAVA_HOME,
|
||||
"bin",
|
||||
process.platform === "win32" ? "java.exe" : "java",
|
||||
)
|
||||
: "java";
|
||||
async function serverJar(version) {
|
||||
await mkdir(root, { recursive: true });
|
||||
const file = `${root}/paper-${version}.jar`;
|
||||
if (version === "1.21.11" && !existsSync(file))
|
||||
await copyFile(".cache/paper-proof/paper.jar", file);
|
||||
if (existsSync(file)) return file;
|
||||
const response = await fetch(
|
||||
`https://fill.papermc.io/v3/projects/paper/versions/${version}/builds`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "MineGeo/0.1.3 local compatibility verification",
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const builds = await response.json();
|
||||
const download = (builds.find((b) => b.channel === "STABLE") ?? builds[0])
|
||||
.downloads["server:default"];
|
||||
const bytes = Buffer.from(await (await fetch(download.url)).arrayBuffer());
|
||||
assert.equal(
|
||||
createHash("sha256").update(bytes).digest("hex"),
|
||||
download.checksums.sha256,
|
||||
);
|
||||
await writeFile(file, bytes);
|
||||
return file;
|
||||
}
|
||||
function monitor(child, label) {
|
||||
let output = "";
|
||||
const waiters = new Set();
|
||||
let ended = false;
|
||||
const record = (bytes) => {
|
||||
output += stripVTControlCharacters(bytes.toString());
|
||||
for (const w of waiters)
|
||||
if (w.pattern.test(output.slice(w.start))) {
|
||||
clearTimeout(w.timer);
|
||||
waiters.delete(w);
|
||||
w.resolve();
|
||||
}
|
||||
};
|
||||
child.stdout.on("data", record);
|
||||
child.stderr.on("data", record);
|
||||
const exit = new Promise((resolve) =>
|
||||
child.on("exit", (code) => {
|
||||
ended = true;
|
||||
for (const w of waiters) {
|
||||
clearTimeout(w.timer);
|
||||
w.reject(new Error(`${label} exited ${code}: ${output.slice(-5000)}`));
|
||||
}
|
||||
waiters.clear();
|
||||
resolve(code);
|
||||
}),
|
||||
);
|
||||
return {
|
||||
get output() {
|
||||
return output;
|
||||
},
|
||||
exit,
|
||||
wait(pattern, start = output.length, timeout = 180000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pattern.test(output.slice(start))) return resolve();
|
||||
if (ended)
|
||||
return reject(
|
||||
new Error(`${label} already exited: ${output.slice(-5000)}`),
|
||||
);
|
||||
const w = {
|
||||
pattern,
|
||||
start,
|
||||
resolve,
|
||||
reject,
|
||||
timer: setTimeout(() => {
|
||||
waiters.delete(w);
|
||||
reject(
|
||||
new Error(
|
||||
`${label} timed out for ${pattern}: ${output.slice(-5000)}`,
|
||||
),
|
||||
);
|
||||
}, timeout),
|
||||
};
|
||||
waiters.add(w);
|
||||
});
|
||||
},
|
||||
async command(command, pattern) {
|
||||
const ready = this.wait(pattern);
|
||||
child.stdin.write(command + "\n");
|
||||
await ready;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (process.argv.includes("--fabric"))
|
||||
throw new Error(
|
||||
"This check runs PaperMC servers only; Minecraft clients are disabled.",
|
||||
);
|
||||
const selected = process.argv
|
||||
.find((arg) => arg.startsWith("--version="))
|
||||
?.split("=")[1];
|
||||
const matrix = ["1.21.11", "26.1.2", "26.2"].filter(
|
||||
(v) => !selected || v === selected,
|
||||
);
|
||||
for (const version of matrix) {
|
||||
const directory = `${root}/plugin-${version}-${Date.now()}`;
|
||||
await mkdir(`${directory}/plugins`, { recursive: true });
|
||||
await copyFile(await serverJar(version), `${directory}/paper.jar`);
|
||||
await copyFile(
|
||||
"public/tools/plugin/minegeo-plugin-0.1.3.jar",
|
||||
`${directory}/plugins/minegeo.jar`,
|
||||
);
|
||||
await writeFile(`${directory}/eula.txt`, "eula=true\n");
|
||||
await writeFile(
|
||||
`${directory}/server.properties`,
|
||||
[
|
||||
"server-ip=127.0.0.1",
|
||||
`server-port=25582`,
|
||||
"online-mode=false",
|
||||
"level-type=minecraft:flat",
|
||||
'generator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}',
|
||||
"generate-structures=false",
|
||||
"view-distance=3",
|
||||
"simulation-distance=2",
|
||||
"max-players=1",
|
||||
"spawn-protection=0",
|
||||
].join("\n") + "\n",
|
||||
);
|
||||
const server = spawn(java, ["-Xmx1536M", "-jar", "paper.jar", "--nogui"], {
|
||||
cwd: directory,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
const logs = monitor(server, `Paper ${version}`);
|
||||
try {
|
||||
await logs.wait(/Done \(/, 0);
|
||||
console.log(`Paper ${version} started.`);
|
||||
|
||||
await logs.command("version MineGeo", /MineGeo version 0\.1\.3/);
|
||||
await logs.command(
|
||||
"minegeo website",
|
||||
/MineGeo website: https:\/\/minegeo\.vercel\.app/,
|
||||
);
|
||||
await logs.command(
|
||||
"minegeo website http://127.0.0.1:9999",
|
||||
/website is fixed to https:\/\/minegeo\.vercel\.app/,
|
||||
);
|
||||
await logs.command(
|
||||
"minegeo server play.compat.example",
|
||||
/Capture server address saved/,
|
||||
);
|
||||
await logs.command("minegeo area world -16 -16 16 16", /area selected/);
|
||||
await logs.command("forceload add -16 -16 15 15", /Marked|already marked/);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
await logs.command(
|
||||
"minegeo export Compatibility World",
|
||||
/Export complete:/,
|
||||
);
|
||||
const relative = (
|
||||
await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8")
|
||||
).trim();
|
||||
const manifest = JSON.parse(
|
||||
await readFile(
|
||||
`${directory}/plugins/MineGeo/${relative}/manifest.json`,
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
assert.equal(manifest.minecraftVersion, version);
|
||||
assert.equal(manifest.viewpoints.length, 24);
|
||||
const terrain = JSON.parse(
|
||||
gunzipSync(
|
||||
await readFile(
|
||||
`${directory}/plugins/MineGeo/${relative}/terrain.json.gz`,
|
||||
),
|
||||
).toString(),
|
||||
);
|
||||
assert.ok(terrain.blocks.length > 0);
|
||||
await logs.command(
|
||||
"minegeo upload",
|
||||
/Create a token in the website dashboard/,
|
||||
);
|
||||
console.log(
|
||||
`PLUGIN ${version} PASSED: fixed endpoint, commands, real export, correct version, ${terrain.blocks.length} blocks.`,
|
||||
);
|
||||
} finally {
|
||||
server.stdin.write("stop\n");
|
||||
const timeout = setTimeout(() => server.kill(), 20000);
|
||||
await logs.exit;
|
||||
clearTimeout(timeout);
|
||||
await writeFile(`${directory}/server.log`, logs.output);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createServer } from "node:http";
|
||||
import { connect } from "node:net";
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, execFile } from "node:child_process";
|
||||
import { mkdir, copyFile, writeFile, readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { datasetSchema } from "../shared/protocol";
|
||||
|
||||
// Disposable, loopback-only Paper test. No game client is launched.
|
||||
@@ -102,8 +104,50 @@ const api = createServer(async (req, res) => {
|
||||
await new Promise<void>((r) => api.listen(0, "127.0.0.1", r));
|
||||
await writeFile(
|
||||
`${directory}/plugins/MineGeo/config.yml`,
|
||||
`website: 'http://127.0.0.1:${(api.address() as any).port}'\ntoken: 'fixture-token'\nserver-name: 'Regression world'\nserver-address: 'play.regression.example.com'\nchunks-per-tick: 2\nviewpoints: 24\nmax-area-size: 2048\n`,
|
||||
`website: 'http://127.0.0.1:${(api.address() as any).port}'\ntoken: ''\nserver-name: 'Regression world'\nserver-address: 'play.regression.example.com'\nchunks-per-tick: 2\nviewpoints: 24\nmax-area-size: 2048\n`,
|
||||
);
|
||||
// Exercise the shared upload transport against the loopback mock separately.
|
||||
// The deployed plugin always uses its hardcoded production endpoint.
|
||||
const execute = promisify(execFile);
|
||||
let exportDir: string;
|
||||
const pluginJar = path.resolve("public/tools/plugin/minegeo-plugin-0.1.3.jar");
|
||||
await execute("javac", [
|
||||
"-cp",
|
||||
pluginJar,
|
||||
"-d",
|
||||
directory,
|
||||
"tests/java/UploadSmoke.java",
|
||||
]);
|
||||
await writeFile(
|
||||
directory + "/fixture-auth.json",
|
||||
JSON.stringify({ token: "fixture-token" }),
|
||||
);
|
||||
const upload = async (
|
||||
target: string | null,
|
||||
expected: RegExp,
|
||||
fails = false,
|
||||
) => {
|
||||
const args = [
|
||||
"-cp",
|
||||
directory + path.delimiter + pluginJar,
|
||||
"UploadSmoke",
|
||||
"http://127.0.0.1:" + (api.address() as any).port,
|
||||
exportDir,
|
||||
directory + "/fixture-auth.json",
|
||||
];
|
||||
if (target) args.push(target);
|
||||
let output = "",
|
||||
code = 0;
|
||||
try {
|
||||
const result = await execute("java", args, { windowsHide: true });
|
||||
output = result.stdout + result.stderr;
|
||||
} catch (error: any) {
|
||||
output = error.stdout + error.stderr;
|
||||
code = error.code;
|
||||
}
|
||||
assert.equal(code === 0, !fails, output);
|
||||
assert.match(output, expected);
|
||||
};
|
||||
const child = spawn("java", ["-Xmx1536M", "-jar", "paper.jar", "--nogui"], {
|
||||
cwd: directory,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
@@ -165,11 +209,15 @@ const command = async (text: string, pattern: RegExp) => {
|
||||
child.stdin.write(text + "\n");
|
||||
await pending;
|
||||
};
|
||||
const rconCommand = (text: string, expected: RegExp) => new Promise<void>((resolve, reject) => {
|
||||
const rconCommand = (text: string, expected: RegExp) =>
|
||||
new Promise<void>((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 timeout = setTimeout(
|
||||
() => finish(new Error(`RCON timed out waiting for ${expected}`)),
|
||||
10000,
|
||||
);
|
||||
const finish = (error?: Error) => {
|
||||
clearTimeout(timeout);
|
||||
socket.destroy();
|
||||
@@ -199,7 +247,10 @@ const rconCommand = (text: string, expected: RegExp) => new Promise<void>((resol
|
||||
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))) {
|
||||
} else if (
|
||||
id === 2 &&
|
||||
expected.test(frame.toString("utf8", 12, length + 2))
|
||||
) {
|
||||
return finish();
|
||||
}
|
||||
}
|
||||
@@ -227,7 +278,7 @@ try {
|
||||
let relative = (
|
||||
await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8")
|
||||
).trim();
|
||||
const exportDir = `${directory}/plugins/MineGeo/${relative}`;
|
||||
exportDir = `${directory}/plugins/MineGeo/${relative}`;
|
||||
const local = datasetSchema.parse(
|
||||
JSON.parse(await readFile(`${exportDir}/manifest.json`, "utf8")),
|
||||
);
|
||||
@@ -243,44 +294,46 @@ try {
|
||||
(p) => p.x >= -32 && p.x < 32 && p.z >= -32 && p.z < 32,
|
||||
),
|
||||
);
|
||||
await command("minegeo upload", /Upload failed: Upload paused/);
|
||||
await command("minegeo upload", /Upload complete\. Open/);
|
||||
await upload(null, /Upload paused/, true);
|
||||
await upload(null, /queueing panorama processing|Already uploaded/);
|
||||
assert.equal(creates, 1);
|
||||
assert.equal(finalizes, 1);
|
||||
assert.equal(heads, 1);
|
||||
await command("minegeo upload", /Upload complete\. Open/);
|
||||
await upload(null, /queueing panorama processing|Already uploaded/);
|
||||
assert.equal(finalizes, 1);
|
||||
received = Buffer.alloc(0);
|
||||
expectedServer = "wrong.example.com";
|
||||
await command(
|
||||
"minegeo contribute 00000000-0000-4000-8000-000000000024",
|
||||
await upload(
|
||||
"00000000-0000-4000-8000-000000000024",
|
||||
/Wrong capture server/,
|
||||
true,
|
||||
);
|
||||
assert.equal(creates, 1, "Mismatch must stop before creating a draft");
|
||||
expectedServer = "PLAY.REGRESSION.EXAMPLE.COM.:25565";
|
||||
await command(
|
||||
"minegeo contribute 00000000-0000-4000-8000-000000000024",
|
||||
/Upload complete\. Open/,
|
||||
await upload(
|
||||
"00000000-0000-4000-8000-000000000024",
|
||||
/queueing panorama processing|Already uploaded/,
|
||||
);
|
||||
assert.equal(manifest.contributionTo, "00000000-0000-4000-8000-000000000024");
|
||||
assert.equal(manifest.viewpoints.length, 12);
|
||||
assert.equal(creates, 2);
|
||||
expectedServer = "wrong.example.com";
|
||||
await command(
|
||||
"minegeo contribute 00000000-0000-4000-8000-000000000024",
|
||||
await upload(
|
||||
"00000000-0000-4000-8000-000000000024",
|
||||
/Wrong capture server/,
|
||||
true,
|
||||
);
|
||||
assert.equal(creates, 2, "A cached selection must recheck the server");
|
||||
expectedServer = null;
|
||||
await command(
|
||||
"minegeo contribute 00000000-0000-4000-8000-000000000024",
|
||||
/Upload complete\. Open/,
|
||||
await upload(
|
||||
"00000000-0000-4000-8000-000000000024",
|
||||
/queueing panorama processing|Already uploaded/,
|
||||
);
|
||||
assert.equal(creates, 2, "Cleared server permits an idempotent retry");
|
||||
assert.equal(finalizes, 2);
|
||||
await command(
|
||||
"minegeo contribute 00000000-0000-4000-8000-000000000024",
|
||||
/Upload complete\. Open/,
|
||||
await upload(
|
||||
"00000000-0000-4000-8000-000000000024",
|
||||
/queueing panorama processing|Already uploaded/,
|
||||
);
|
||||
assert.equal(creates, 2);
|
||||
await command("minegeo export Random Positions", /Export complete:/);
|
||||
@@ -321,7 +374,7 @@ try {
|
||||
"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",
|
||||
],
|
||||
|
||||
@@ -7,7 +7,8 @@ import { assertTerrainImage } from "../renderer/image-validation.mjs";
|
||||
|
||||
// Exercise the product's rendering worker without launching a Minecraft server
|
||||
// or driving the website. A cached real export can also be supplied as argv[2].
|
||||
const output = ".cache/renderer-regression";
|
||||
let minecraftVersion = process.argv[3] || "1.21.11";
|
||||
const output = `.cache/renderer-regression-${minecraftVersion}`;
|
||||
const snapshot = {
|
||||
bounds: { minX: -24, minZ: -24, maxX: 24, maxZ: 24 },
|
||||
coverage: [],
|
||||
@@ -69,11 +70,12 @@ let points = [
|
||||
[0.5, 18.5],
|
||||
].map(([x, z], i) => ({ id: `p${i}`, x, y: 66.62, z }));
|
||||
let source = "grass, dirt, water, tree and building regression fixture";
|
||||
if (process.argv[2]) {
|
||||
if (process.argv[2] && process.argv[2] !== "--fixture") {
|
||||
const directory = process.argv[2];
|
||||
const manifest = JSON.parse(
|
||||
await readFile(`${directory}/manifest.json`, "utf8"),
|
||||
);
|
||||
minecraftVersion = manifest.minecraftVersion;
|
||||
terrain = JSON.parse(
|
||||
gunzipSync(await readFile(`${directory}/terrain.json.gz`)).toString(),
|
||||
);
|
||||
@@ -81,7 +83,10 @@ if (process.argv[2]) {
|
||||
source = "cached Paper export (no server started)";
|
||||
}
|
||||
await mkdir(output, { recursive: true });
|
||||
const report = await renderSnapshot(terrain, points, output, { size: 256 });
|
||||
const report = await renderSnapshot(terrain, points, output, {
|
||||
size: 256,
|
||||
minecraftVersion,
|
||||
});
|
||||
assert.ok(report.meshes > 0, "terrain must produce meshes");
|
||||
assert.ok(report.emittedFaces > 0, "terrain must produce faces");
|
||||
assert.ok(report.hiddenFaces > 0, "interior terrain faces must be culled");
|
||||
@@ -106,12 +111,16 @@ assert.ok(
|
||||
"blocks without explicit default properties must appear on the aerial map",
|
||||
);
|
||||
const mapPixel = (x, z) => {
|
||||
const pixelX = Math.floor(((x - snapshot.bounds.minX + 0.5) / 48) * mapInfo.width);
|
||||
const pixelY = Math.floor(((z - snapshot.bounds.minZ + 0.5) / 48) * mapInfo.height);
|
||||
const pixelX = Math.floor(
|
||||
((x - snapshot.bounds.minX + 0.5) / 48) * mapInfo.width,
|
||||
);
|
||||
const pixelY = Math.floor(
|
||||
((z - snapshot.bounds.minZ + 0.5) / 48) * mapInfo.height,
|
||||
);
|
||||
const offset = (pixelY * mapInfo.width + pixelX) * mapInfo.channels;
|
||||
return mapPixels.subarray(offset, offset + 3);
|
||||
};
|
||||
if (!process.argv[2]) {
|
||||
if (!process.argv[2] || process.argv[2] === "--fixture") {
|
||||
const shadedGrass = mapPixel(10, 9);
|
||||
const sunlitGrass = mapPixel(10, -9);
|
||||
assert.ok(
|
||||
|
||||
@@ -664,5 +664,9 @@
|
||||
"Room not found": "Комната не найдена",
|
||||
"Join this room to play": "Присоединитесь к комнате, чтобы играть",
|
||||
"Wait for the round to finish": "Дождитесь завершения раунда",
|
||||
"Players can only be removed before the match starts": "Удалять игроков можно только до начала игры"
|
||||
"Players can only be removed before the match starts": "Удалять игроков можно только до начала игры",
|
||||
"Minecraft Java 1.21.11, 26.1 & 26.2 + Fabric Loader + Fabric API": "Minecraft Java 1.21.11, 26.1 и 26.2 + Fabric Loader + Fabric API",
|
||||
"Bukkit/Paper 1.21.11, 26.1 & 26.2": "Bukkit/Paper 1.21.11, 26.1 и 26.2",
|
||||
"Java 21 for 1.21.11; Java 25 for 26.1 and 26.2. Both tools connect to minegeo.vercel.app automatically.": "Java 21 для 1.21.11; Java 25 для 26.1 и 26.2. Оба инструмента автоматически подключаются к minegeo.vercel.app.",
|
||||
"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.": "Поместите JAR в plugins/ и перезапустите сервер. Укажите server-name и server-address в plugins/MineGeo/config.yml, затем выполните /minegeo reload. Разрешение: minegeo.admin. Один JAR плагина поддерживает все три версии."
|
||||
}
|
||||
|
||||
@@ -41,7 +41,12 @@ export function panoramaPath(
|
||||
export const datasetSchema = z
|
||||
.object({
|
||||
format: z.literal("minegeo-dataset/v1"),
|
||||
minecraftVersion: z.literal("1.21.11"),
|
||||
minecraftVersion: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?:1\.21\.11|26\.[12](?:\.\d+)*)$/,
|
||||
"Unsupported Minecraft version",
|
||||
),
|
||||
dimension: z.literal("minecraft:overworld"),
|
||||
source: z.enum(["plugin", "fabric"]),
|
||||
name: z.string().min(3).max(80),
|
||||
|
||||
@@ -46,6 +46,21 @@ test("dataset requires five unique in-bounds viewpoints and safe paths", () => {
|
||||
assets: [{ path: "terrain.json.gz", sha256: "a".repeat(64), bytes: 100 }],
|
||||
};
|
||||
assert.equal(datasetSchema.safeParse(base).success, true);
|
||||
for (const minecraftVersion of ["26.1", "26.1.1", "26.1.2", "26.2"])
|
||||
assert.equal(
|
||||
datasetSchema.safeParse({ ...base, minecraftVersion }).success,
|
||||
true,
|
||||
);
|
||||
for (const minecraftVersion of [
|
||||
"26.3",
|
||||
"1.21.10",
|
||||
"26.2-snapshot",
|
||||
"../secrets",
|
||||
])
|
||||
assert.equal(
|
||||
datasetSchema.safeParse({ ...base, minecraftVersion }).success,
|
||||
false,
|
||||
);
|
||||
assert.equal(datasetSchema.safeParse({ ...base, assets: [] }).success, false);
|
||||
assert.equal(
|
||||
datasetSchema.safeParse({ ...base, source: "fabric" }).success,
|
||||
|
||||
@@ -37,7 +37,11 @@ const jar = zipSync(
|
||||
);
|
||||
const checksum = createHash("sha1").update(jar).digest("hex");
|
||||
|
||||
async function isolatedCache(t: TestContext, corruptDownload = false) {
|
||||
async function isolatedCache(
|
||||
t: TestContext,
|
||||
corruptDownload = false,
|
||||
minecraftVersion = "1.21.11",
|
||||
) {
|
||||
const previous = process.cwd();
|
||||
const cacheParent = resolve(".cache");
|
||||
await mkdir(cacheParent, { recursive: true });
|
||||
@@ -54,7 +58,9 @@ async function isolatedCache(t: TestContext, corruptDownload = false) {
|
||||
requests.push(url);
|
||||
if (url.includes("version_manifest_v2"))
|
||||
return Response.json({
|
||||
versions: [{ id: "1.21.11", url: "https://fixture.invalid/version" }],
|
||||
versions: [
|
||||
{ id: minecraftVersion, url: "https://fixture.invalid/version" },
|
||||
],
|
||||
});
|
||||
if (url.endsWith("/version"))
|
||||
return Response.json({
|
||||
@@ -91,6 +97,27 @@ test("missing vanilla cache is prepared completely and subsequent runs work offl
|
||||
assert.deepEqual(requests, []);
|
||||
});
|
||||
|
||||
test("26.x assets use distinct verified caches and reject unsupported versions", async (t) => {
|
||||
await isolatedCache(t, false, "26.2");
|
||||
const directory = await prepareAssets("26.2");
|
||||
assert.equal(directory, ".cache/minecraft/26.2");
|
||||
const marker = JSON.parse(
|
||||
await readFile(`${directory}/.minegeo-assets.json`, "utf8"),
|
||||
);
|
||||
assert.equal(marker.minecraftVersion, "26.2");
|
||||
await assert.rejects(readFile(`${assetsRoot}/.minegeo-assets.json`), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
await assert.rejects(
|
||||
prepareAssets("../secrets"),
|
||||
/Unsupported Minecraft asset version/,
|
||||
);
|
||||
await assert.rejects(
|
||||
prepareAssets("26.3"),
|
||||
/Unsupported Minecraft asset version/,
|
||||
);
|
||||
});
|
||||
|
||||
test("an interrupted extraction with only a blockstate cannot masquerade as complete", async (t) => {
|
||||
await isolatedCache(t);
|
||||
await mkdir(`${assetsRoot}/assets/minecraft/blockstates`, {
|
||||
|
||||
@@ -189,6 +189,7 @@ async function processJob(job: {
|
||||
output,
|
||||
{
|
||||
size: Number(process.env.MINEGEO_FACE_SIZE || 1024),
|
||||
minecraftVersion: manifest.minecraftVersion,
|
||||
signal: abort.signal,
|
||||
onProgress: ({ percent, message }) => progress(percent, message),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user