mapsyncer-paper: server side of MapSyncer for Xaero's World Map, discovery-limited

Paper 26.2 plugin speaking the stock MapSyncer client mod's protocol.
Renders Xaero region zips from the world's region files, but only for chunks
players have actually been sent (PlayerChunkLoadEvent), seeded once from
InhabitedTime. Shared or per-player visibility, background render cycle,
per-player streaming with hash/timestamp skipping, Gitea Actions release
workflow.

Vendors the MCA parser and Xaero writer from upstream MapSyncer (GPL-3.0),
see NOTICE.md.

Claude-Session: https://claude.ai/code/session_011FePLXwBsCGLTzaSkk1Z6V
This commit is contained in:
2026-09-07 00:04:43 +03:00
commit 7fb3a82482
67 changed files with 10934 additions and 0 deletions
@@ -0,0 +1,70 @@
package com.mapsyncer.mca;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* 快速检测 MCA 是否含有可转换的 chunk 数据(不解析 NBT)。
* 用于跳过 0 字节或仅含 location/timestamp 表头的空 region 文件。
*/
public final class McaContentProbe {
private static final Logger LOGGER = LoggerFactory.getLogger(McaContentProbe.class);
private static final int SECTOR_SIZE = 4096;
private static final int CHUNKS_PER_REGION = 32;
/** 空 MCA 仅含 location + timestamp 两扇区 */
private static final long HEADER_ONLY_SIZE = (long) SECTOR_SIZE * 2;
private McaContentProbe() {}
/**
* @return true 若文件存在且 location 表中至少有一个有效 chunk slot
*/
public static boolean hasAnyChunk(Path mcaPath) {
if (mcaPath == null || !Files.exists(mcaPath)) {
return false;
}
try {
long size = Files.size(mcaPath);
if (size == 0) {
return false;
}
if (size <= HEADER_ONLY_SIZE) {
return false;
}
} catch (IOException e) {
LOGGER.debug("Cannot stat MCA {}: {}", mcaPath, e.getMessage());
return false;
}
try (RandomAccessFile raf = new RandomAccessFile(mcaPath.toFile(), "r")) {
if (raf.length() < HEADER_ONLY_SIZE) {
return false;
}
for (int localX = 0; localX < CHUNKS_PER_REGION; localX++) {
for (int localZ = 0; localZ < CHUNKS_PER_REGION; localZ++) {
int index = (localX + localZ * CHUNKS_PER_REGION) * 4;
raf.seek(index);
int b0 = raf.readUnsignedByte();
int b1 = raf.readUnsignedByte();
int b2 = raf.readUnsignedByte();
int offsetSectors = (b0 << 16) | (b1 << 8) | b2;
int sectorCount = raf.readUnsignedByte();
if (offsetSectors > 0 && sectorCount > 0) {
return true;
}
}
}
return false;
} catch (IOException e) {
LOGGER.debug("MCA chunk probe failed for {}: {}", mcaPath, e.getMessage());
return false;
}
}
}