149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
import { z } from "zod";
|
|
export const boundsSchema = z
|
|
.object({
|
|
minX: z.number().int(),
|
|
minZ: z.number().int(),
|
|
maxX: z.number().int(),
|
|
maxZ: z.number().int(),
|
|
})
|
|
.refine(
|
|
(b) =>
|
|
b.maxX > b.minX &&
|
|
b.maxZ > b.minZ &&
|
|
b.maxX - b.minX <= 2048 &&
|
|
b.maxZ - b.minZ <= 2048,
|
|
"Bounds must be positive and at most 2048 blocks per side",
|
|
);
|
|
export const pointSchema = z.object({
|
|
x: z.number().finite(),
|
|
y: z.number().finite(),
|
|
z: z.number().finite(),
|
|
});
|
|
export const faces = ["px", "nx", "py", "ny", "pz", "nz"] as const;
|
|
export const datasetSchema = z
|
|
.object({
|
|
format: z.literal("minegeo-dataset/v1"),
|
|
minecraftVersion: z.literal("1.21.11"),
|
|
dimension: z.literal("minecraft:overworld"),
|
|
source: z.enum(["plugin", "fabric"]),
|
|
name: z.string().min(3).max(80),
|
|
description: z.string().max(2000).default(""),
|
|
serverName: z.string().min(2).max(80),
|
|
serverAddress: z.string().max(255).optional(),
|
|
bounds: boundsSchema,
|
|
north: z.literal("-z"),
|
|
viewpoints: z
|
|
.array(
|
|
pointSchema.extend({ id: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) }),
|
|
)
|
|
.min(5)
|
|
.max(100),
|
|
// Retain the capture's locations while viewpoints holds the saved selection.
|
|
availableViewpoints: z
|
|
.array(
|
|
pointSchema.extend({ id: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) }),
|
|
)
|
|
.min(5)
|
|
.max(100)
|
|
.optional(),
|
|
assets: z
|
|
.array(
|
|
z.object({
|
|
path: z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9_./-]+$/)
|
|
.refine((s) => !s.includes("..") && !s.startsWith("/")),
|
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
bytes: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(50 * 1024 * 1024),
|
|
}),
|
|
)
|
|
.max(4096),
|
|
})
|
|
.superRefine((d, ctx) => {
|
|
const paths = new Set(d.assets.map((asset) => asset.path));
|
|
const requiredPaths =
|
|
d.source === "plugin"
|
|
? ["terrain.json.gz"]
|
|
: [
|
|
"map.png",
|
|
...(d.availableViewpoints ?? d.viewpoints).flatMap((point) =>
|
|
faces.map((face) => `${point.id}/${face}.png`),
|
|
),
|
|
];
|
|
for (const path of requiredPaths) {
|
|
if (!paths.has(path))
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: `Missing required export asset: ${path}`,
|
|
});
|
|
}
|
|
if (new Set(d.viewpoints.map((p) => p.id)).size !== d.viewpoints.length)
|
|
ctx.addIssue({ code: "custom", message: "Viewpoint IDs must be unique" });
|
|
if (d.availableViewpoints) {
|
|
const ids = new Set(d.availableViewpoints.map((p) => p.id));
|
|
if (
|
|
ids.size !== d.availableViewpoints.length ||
|
|
d.viewpoints.some((p) => !ids.has(p.id))
|
|
)
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Selection must use unique captured locations",
|
|
});
|
|
}
|
|
if (new Set(d.assets.map((a) => a.path)).size !== d.assets.length)
|
|
ctx.addIssue({ code: "custom", message: "Asset paths must be unique" });
|
|
for (const p of [...d.viewpoints, ...(d.availableViewpoints ?? [])])
|
|
if (
|
|
p.x < d.bounds.minX ||
|
|
p.x > d.bounds.maxX ||
|
|
p.z < d.bounds.minZ ||
|
|
p.z > d.bounds.maxZ
|
|
)
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Viewpoint outside map bounds",
|
|
});
|
|
});
|
|
export type DatasetManifest = z.infer<typeof datasetSchema>;
|
|
export type Bounds = z.infer<typeof boundsSchema>;
|
|
export type Point = z.infer<typeof pointSchema>;
|
|
export const difficulties = {
|
|
easy: { seconds: 180, zoom: true, label: "Easy" },
|
|
normal: { seconds: 90, zoom: true, label: "Normal" },
|
|
hard: { seconds: 30, zoom: false, label: "Hard" },
|
|
} as const;
|
|
export type Difficulty = keyof typeof difficulties;
|
|
export function scoreGuess(
|
|
a: Pick<Point, "x" | "z">,
|
|
b: Pick<Point, "x" | "z">,
|
|
bounds: Bounds,
|
|
) {
|
|
const distance = Math.hypot(a.x - b.x, a.z - b.z);
|
|
return {
|
|
distance,
|
|
score:
|
|
distance <= 1
|
|
? 5000
|
|
: Math.round(
|
|
5000 *
|
|
Math.exp(
|
|
(-5 * distance) /
|
|
Math.hypot(
|
|
bounds.maxX - bounds.minX,
|
|
bounds.maxZ - bounds.minZ,
|
|
),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
export type TerrainBlock = [number, number, number, string, string?];
|
|
export type TerrainSnapshot = {
|
|
bounds: Bounds;
|
|
blocks: TerrainBlock[];
|
|
coverage: [number, number][];
|
|
};
|