Release v0.1.4 with terrain rescanning and owner-published map updates
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 6m26s

This commit is contained in:
2026-10-28 19:40:53 +05:00
parent 5898ef35a5
commit 658c8974ac
53 changed files with 608 additions and 83 deletions

View File

@@ -25,9 +25,9 @@ public final class ServerAddress {
return host+(port==25565?"":":"+port);
}
public static void requireMatch(String expected,String captured) throws IOException {
String server=normalize(expected);
if(server==null)return;
if(expected==null||expected.isBlank())return;
String actual=normalize(captured);
if(!server.equals(actual))throw new IOException("Wrong capture server: this map requires "+server+", but the export records "+(actual==null?"no server address":actual)+". Join the correct server and capture again. Plugin admins: configure server-address before exporting. The map owner can correct or clear its server address in settings.");
for(String address:expected.split(","))if(java.util.Objects.equals(normalize(address),actual)&&actual!=null)return;
throw new IOException("Wrong capture server: this map requires one of "+expected+", but the export records "+(actual==null?"no server address":actual)+". Join the correct server and capture again.");
}
}

View File

@@ -0,0 +1,48 @@
package dev.minegeo.plugin;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;
import java.util.regex.Pattern;
/** Persists bounded, client-visible surface samples in the plugin terrain format. */
public final class SurfaceTerrain {
private static final Pattern BLOCK_STATE=Pattern.compile("minecraft:[a-z0-9_]+(\\[[a-z0-9_=,]+\\])?");
private final Path directory;
private final Gson gson=new Gson();
private JsonArray current;
public SurfaceTerrain(Path directory){this.directory=directory;}
private Path chunk(String key){return directory.resolve("terrain-chunks").resolve(key.replace(',','_')+".json.gz");}
public boolean has(String key){return Files.isRegularFile(chunk(key));}
public void begin(){current=new JsonArray();}
public void add(int x,int y,int z,String state){
if(current==null)return;
state=state.replace("Block{","").replace("}","");
if(!BLOCK_STATE.matcher(state).matches())return;
JsonArray block=new JsonArray();block.add(x);block.add(y);block.add(z);block.add(state);current.add(block);
}
public void finish(String key) throws IOException {
Path file=chunk(key);Files.createDirectories(file.getParent());Path tmp=file.resolveSibling(file.getFileName()+".tmp");
try(Writer out=new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(tmp)),StandardCharsets.UTF_8)){gson.toJson(current,out);}
Files.move(tmp,file,StandardCopyOption.REPLACE_EXISTING);current=null;
}
public boolean export(Set<String> coverage,int minX,int minZ) throws IOException {
List<String> keys=coverage.stream().filter(this::has).sorted().toList();if(keys.isEmpty())return false;
Path target=directory.resolve("terrain.json.gz"),tmp=directory.resolve("terrain.json.gz.tmp");int blocks=0;
try(JsonWriter writer=new JsonWriter(new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(tmp)),StandardCharsets.UTF_8))){
writer.beginObject().name("bounds").beginObject().name("minX").value(minX).name("minZ").value(minZ).name("maxX").value(minX+512).name("maxZ").value(minZ+512).endObject();
writer.name("blocks").beginArray();
for(String key:keys)try(Reader in=new InputStreamReader(new GZIPInputStream(Files.newInputStream(chunk(key))),StandardCharsets.UTF_8)){
for(JsonElement block:JsonParser.parseReader(in).getAsJsonArray()){gson.toJson(block,writer);blocks++;}
}
writer.endArray().name("coverage").beginArray();for(String key:keys){String[] xy=key.split(",");writer.beginArray().value(Integer.parseInt(xy[0])).value(Integer.parseInt(xy[1])).endArray();}writer.endArray().endObject();
}
if(blocks==0){Files.deleteIfExists(tmp);return false;}
if(Files.size(tmp)>50L*1024*1024){Files.deleteIfExists(tmp);throw new IOException("Terrain snapshot exceeds 50 MiB");}
Files.move(tmp,target,StandardCopyOption.REPLACE_EXISTING);return true;
}
}

View File

@@ -48,7 +48,8 @@ public final class UploadClient {
JsonObject manifest=JsonParser.parseString(Files.readString(manifestFile)).getAsJsonObject();
if(targetMap!=null){
JsonObject serverInfo=api("/contributions/"+targetMap+"/server",null);
ServerAddress.requireMatch(serverInfo.has("serverAddress")&&!serverInfo.get("serverAddress").isJsonNull()?serverInfo.get("serverAddress").getAsString():null,manifest.has("serverAddress")?manifest.get("serverAddress").getAsString():null);
String expected=serverInfo.has("serverAddresses")?java.util.stream.StreamSupport.stream(serverInfo.getAsJsonArray("serverAddresses").spliterator(),false).map(e->e.getAsString()).collect(java.util.stream.Collectors.joining(",")):serverInfo.has("serverAddress")&&!serverInfo.get("serverAddress").isJsonNull()?serverInfo.get("serverAddress").getAsString():null;
ServerAddress.requireMatch(expected,manifest.has("serverAddress")?manifest.get("serverAddress").getAsString():null);
String originalHash=hash((website+"\n"+hash(token.getBytes(StandardCharsets.UTF_8))+"\n"+gson.toJson(manifest)).getBytes(StandardCharsets.UTF_8));
Path selectionFile=directory.resolve(".contribution-"+targetMap+".json");
JsonObject cached=Files.exists(selectionFile)?JsonParser.parseString(Files.readString(selectionFile)).getAsJsonObject():new JsonObject();
@@ -64,7 +65,7 @@ public final class UploadClient {
manifest.add("viewpoints",subset);
if(manifest.get("source").getAsString().equals("fabric")){
Set<String> ids=new HashSet<>();for(JsonElement p:subset)ids.add(p.getAsJsonObject().get("id").getAsString());
JsonArray assets=new JsonArray();for(JsonElement a:manifest.getAsJsonArray("assets")){String path=a.getAsJsonObject().get("path").getAsString();if(path.equals("map.png")||ids.contains(path.split("/")[0]))assets.add(a);}manifest.add("assets",assets);
JsonArray assets=new JsonArray();for(JsonElement a:manifest.getAsJsonArray("assets")){String path=a.getAsJsonObject().get("path").getAsString();if(path.equals("map.png")||path.equals("terrain.json.gz")||ids.contains(path.split("/")[0]))assets.add(a);}manifest.add("assets",assets);
}
cached=new JsonObject();cached.addProperty("hash",originalHash);cached.add("manifest",manifest);save(selectionFile,cached);
}

View File

@@ -1,5 +1,5 @@
name: MineGeo
version: 0.1.3
version: 0.1.4
main: dev.minegeo.plugin.MineGeoPlugin
api-version: '1.21'
description: Bounded terrain export for MineGeo panorama guessing