Add SCUM current-service map asset contract
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"key": "scum-current-service-coordinate-map",
|
||||
"mapVersion": "scum-current-service-coordinate-map-v1",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"authorization": {
|
||||
"redistribution": "first-party-generated-coordinate-metadata",
|
||||
"baseMapArtwork": "not-packaged",
|
||||
"renderingAvailability": "unavailable-until-authorized-base-map"
|
||||
},
|
||||
"worldBounds": {
|
||||
"minX": -901009.4375,
|
||||
"minY": -883992.5625,
|
||||
"maxX": 612580.0625,
|
||||
"maxY": 615977.375
|
||||
},
|
||||
"image": {
|
||||
"width": 4096,
|
||||
"height": 4096
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"key": "players",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "player",
|
||||
"label": "Players",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
},
|
||||
{
|
||||
"key": "vehicles",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "vehicle",
|
||||
"label": "Vehicles",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
},
|
||||
{
|
||||
"key": "flags",
|
||||
"capability": "positions.read",
|
||||
"subjectType": "flag",
|
||||
"label": "Flags",
|
||||
"sourceQueryKey": "scum-positions-read"
|
||||
}
|
||||
],
|
||||
"transformAssetPath": "assets/scum-live/map/current-service-transform.json"
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"key": "scum-current-service-coordinate-transform",
|
||||
"transformVersion": "scum-current-service-transform-v1",
|
||||
"mapAssetKey": "scum-current-service-coordinate-map",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"coordinateSystem": "current-service-observed-entity-envelope",
|
||||
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
|
||||
"worldBounds": {
|
||||
"minX": -901009.4375,
|
||||
"minY": -883992.5625,
|
||||
"maxX": 612580.0625,
|
||||
"maxY": 615977.375
|
||||
},
|
||||
"image": {
|
||||
"width": 4096,
|
||||
"height": 4096
|
||||
},
|
||||
"axisMapping": {
|
||||
"x": "left-to-right",
|
||||
"y": "bottom-to-top-inverted-for-image-y"
|
||||
},
|
||||
"formula": {
|
||||
"pixelX": "((x - minX) / (maxX - minX)) * (width - 1)",
|
||||
"pixelY": "((maxY - y) / (maxY - minY)) * (height - 1)"
|
||||
},
|
||||
"validation": {
|
||||
"rejectNonFinite": true,
|
||||
"rejectOutOfBounds": true,
|
||||
"acceptBoundaryPoints": true
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
"name": "observed-minimum-corner",
|
||||
"world": { "x": -901009.4375, "y": -883992.5625 },
|
||||
"pixel": { "x": 0, "y": 4095 }
|
||||
},
|
||||
{
|
||||
"name": "observed-maximum-corner",
|
||||
"world": { "x": 612580.0625, "y": 615977.375 },
|
||||
"pixel": { "x": 4095, "y": 0 }
|
||||
},
|
||||
{
|
||||
"name": "observed-center",
|
||||
"world": { "x": -144214.6875, "y": -134007.59375 },
|
||||
"pixel": { "x": 2047.5, "y": 2047.5 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -558,6 +558,28 @@
|
||||
"timeoutMs": 2000,
|
||||
"maxResultBytes": 262144
|
||||
}
|
||||
],
|
||||
"mapAssets": [
|
||||
{
|
||||
"key": "scum-current-service-coordinate-map",
|
||||
"adapterVersion": "scum-live-data-v1",
|
||||
"assetPath": "assets/scum-live/map/current-service-map.json",
|
||||
"digest": "sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b",
|
||||
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
|
||||
"metadataSchemaRef": "schemas/scum-live/map-metadata.schema.json",
|
||||
"transformAssetPath": "assets/scum-live/map/current-service-transform.json",
|
||||
"transformDigest": "sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478",
|
||||
"worldBounds": {
|
||||
"minX": -901009.4375,
|
||||
"minY": -883992.5625,
|
||||
"maxX": 612580.0625,
|
||||
"maxY": 615977.375
|
||||
},
|
||||
"image": {
|
||||
"width": 4096,
|
||||
"height": 4096
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
@@ -653,6 +675,14 @@
|
||||
{
|
||||
"path": "assets/scum-live/queries/positions-read.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "assets/scum-live/map/current-service-map.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "assets/scum-live/map/current-service-transform.json",
|
||||
"mode": 384
|
||||
}
|
||||
],
|
||||
"productionLifecycle": {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMCurrentServiceMapMetadata",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["key", "mapVersion", "adapterVersion", "requiredSchemaFingerprint", "sourceEvidence", "authorization", "worldBounds", "image", "layers", "transformAssetPath"],
|
||||
"properties": {
|
||||
"key": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]{0,119}$" },
|
||||
"mapVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
|
||||
"adapterVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
|
||||
"requiredSchemaFingerprint": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
|
||||
"sourceEvidence": { "type": "string", "minLength": 1, "maxLength": 240 },
|
||||
"authorization": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["redistribution", "baseMapArtwork", "renderingAvailability"],
|
||||
"properties": {
|
||||
"redistribution": { "const": "first-party-generated-coordinate-metadata" },
|
||||
"baseMapArtwork": { "const": "not-packaged" },
|
||||
"renderingAvailability": { "const": "unavailable-until-authorized-base-map" }
|
||||
}
|
||||
},
|
||||
"worldBounds": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["minX", "minY", "maxX", "maxY"],
|
||||
"properties": { "minX": { "type": "number" }, "minY": { "type": "number" }, "maxX": { "type": "number" }, "maxY": { "type": "number" } }
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["width", "height"],
|
||||
"properties": { "width": { "type": "integer", "minimum": 1 }, "height": { "type": "integer", "minimum": 1 } }
|
||||
},
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 8,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["key", "capability", "subjectType", "label", "sourceQueryKey"],
|
||||
"properties": {
|
||||
"key": { "enum": ["players", "vehicles", "flags"] },
|
||||
"capability": { "const": "positions.read" },
|
||||
"subjectType": { "enum": ["player", "vehicle", "flag"] },
|
||||
"label": { "type": "string", "minLength": 1, "maxLength": 40 },
|
||||
"sourceQueryKey": { "const": "scum-positions-read" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"transformAssetPath": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,240}$" }
|
||||
}
|
||||
}
|
||||
@@ -1150,6 +1150,77 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
}
|
||||
if (sha256FileDigest(absoluteAssetPath) !== expectedDigest) errors.push(`${digestLocation}: digest does not match packaged asset content`);
|
||||
};
|
||||
const readSCUMJSONAsset = (assetPath: string | undefined, assetLocation: string): Record<string, unknown> | undefined => {
|
||||
if (!manifestDir || !assetPath || !isSafeRelativePathRef(assetPath)) return undefined;
|
||||
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
|
||||
if (!fs.existsSync(absoluteAssetPath) || !fs.statSync(absoluteAssetPath).isFile()) return undefined;
|
||||
try {
|
||||
const value = readJson(absoluteAssetPath);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
errors.push(`${assetLocation}: asset content must be a JSON object`);
|
||||
return undefined;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
} catch {
|
||||
errors.push(`${assetLocation}: asset content must be valid JSON`);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const mapBoundsMatch = (candidate: unknown, bounds: MapAsset["worldBounds"]): boolean => {
|
||||
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate) || !bounds) return false;
|
||||
const value = candidate as { minX?: unknown; minY?: unknown; maxX?: unknown; maxY?: unknown };
|
||||
return value.minX === bounds.minX && value.minY === bounds.minY && value.maxX === bounds.maxX && value.maxY === bounds.maxY;
|
||||
};
|
||||
const mapImageMatch = (candidate: unknown, image: MapAsset["image"]): boolean => {
|
||||
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate) || !image) return false;
|
||||
const value = candidate as { width?: unknown; height?: unknown };
|
||||
return value.width === image.width && value.height === image.height;
|
||||
};
|
||||
const requireSCUMMapAssetJSONCompatibility = (mapAsset: MapAsset, itemLocation: string): void => {
|
||||
const metadata = readSCUMJSONAsset(mapAsset.assetPath, `${itemLocation}.assetPath`);
|
||||
if (metadata) {
|
||||
if (metadata.key !== mapAsset.key) errors.push(`${itemLocation}.assetPath: metadata key must match the map asset declaration`);
|
||||
if (!adapterPattern.test(String(metadata.mapVersion ?? ""))) errors.push(`${itemLocation}.assetPath: metadata mapVersion must be a bounded version`);
|
||||
if (metadata.adapterVersion !== mapAsset.adapterVersion) errors.push(`${itemLocation}.assetPath: metadata adapterVersion must match the map asset declaration`);
|
||||
if (metadata.requiredSchemaFingerprint !== mapAsset.requiredSchemaFingerprint) errors.push(`${itemLocation}.assetPath: metadata schema fingerprint must match the map asset declaration`);
|
||||
if (metadata.transformAssetPath !== mapAsset.transformAssetPath) errors.push(`${itemLocation}.assetPath: metadata transformAssetPath must match the map asset declaration`);
|
||||
if (!mapBoundsMatch(metadata.worldBounds, mapAsset.worldBounds)) errors.push(`${itemLocation}.assetPath: metadata worldBounds must match the map asset declaration`);
|
||||
if (!mapImageMatch(metadata.image, mapAsset.image)) errors.push(`${itemLocation}.assetPath: metadata image dimensions must match the map asset declaration`);
|
||||
const layers = metadata.layers;
|
||||
if (!Array.isArray(layers) || layers.length === 0) {
|
||||
errors.push(`${itemLocation}.assetPath: metadata must declare at least one map layer`);
|
||||
} else {
|
||||
const layerKeys = new Set<string>();
|
||||
for (const [layerIndex, layer] of layers.entries()) {
|
||||
const layerLocation = `${itemLocation}.assetPath.layers[${layerIndex}]`;
|
||||
if (!layer || typeof layer !== "object" || Array.isArray(layer)) {
|
||||
errors.push(`${layerLocation}: layer metadata must be an object`);
|
||||
continue;
|
||||
}
|
||||
const candidate = layer as { key?: unknown; capability?: unknown; subjectType?: unknown; sourceQueryKey?: unknown };
|
||||
const layerKey = String(candidate.key ?? "");
|
||||
if (layerKeys.has(layerKey)) errors.push(`${layerLocation}.key: duplicate layer key ${layerKey}`);
|
||||
layerKeys.add(layerKey);
|
||||
if (!logicalKeyPattern.test(layerKey)) errors.push(`${layerLocation}.key: must be a safe logical key`);
|
||||
if (candidate.capability !== "positions.read") errors.push(`${layerLocation}.capability: map layers must use positions.read`);
|
||||
if (!["player", "vehicle", "flag"].includes(String(candidate.subjectType ?? ""))) errors.push(`${layerLocation}.subjectType: must be player, vehicle, or flag`);
|
||||
if (candidate.sourceQueryKey !== "scum-positions-read") errors.push(`${layerLocation}.sourceQueryKey: must reference the packaged positions query`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const transform = readSCUMJSONAsset(mapAsset.transformAssetPath, `${itemLocation}.transformAssetPath`);
|
||||
if (transform) {
|
||||
if (transform.mapAssetKey !== mapAsset.key) errors.push(`${itemLocation}.transformAssetPath: transform mapAssetKey must match the map asset declaration`);
|
||||
if (!adapterPattern.test(String(transform.transformVersion ?? ""))) errors.push(`${itemLocation}.transformAssetPath: transformVersion must be a bounded version`);
|
||||
if (transform.adapterVersion !== mapAsset.adapterVersion) errors.push(`${itemLocation}.transformAssetPath: transform adapterVersion must match the map asset declaration`);
|
||||
if (transform.requiredSchemaFingerprint !== mapAsset.requiredSchemaFingerprint) errors.push(`${itemLocation}.transformAssetPath: transform schema fingerprint must match the map asset declaration`);
|
||||
if (!mapBoundsMatch(transform.worldBounds, mapAsset.worldBounds)) errors.push(`${itemLocation}.transformAssetPath: transform worldBounds must match the map asset declaration`);
|
||||
if (!mapImageMatch(transform.image, mapAsset.image)) errors.push(`${itemLocation}.transformAssetPath: transform image dimensions must match the map asset declaration`);
|
||||
const validation = transform.validation as { rejectNonFinite?: unknown; rejectOutOfBounds?: unknown; acceptBoundaryPoints?: unknown } | undefined;
|
||||
if (validation?.rejectNonFinite !== true || validation?.rejectOutOfBounds !== true || validation?.acceptBoundaryPoints !== true) errors.push(`${itemLocation}.transformAssetPath.validation: must reject non-finite/out-of-bounds coordinates and accept boundary points`);
|
||||
if (!Array.isArray(transform.fixtures) || transform.fixtures.length === 0) errors.push(`${itemLocation}.transformAssetPath.fixtures: transform must include known-point fixtures`);
|
||||
}
|
||||
};
|
||||
const requireUniqueAssetKeys = (assets: Asset[] | undefined, collection: string): void => {
|
||||
const seenKeys = new Set<string>();
|
||||
for (const [index, asset] of (assets ?? []).entries()) {
|
||||
@@ -1255,6 +1326,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
const bounds = mapAsset.worldBounds;
|
||||
if (!bounds || !Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY) || !Number.isFinite(bounds.maxX) || !Number.isFinite(bounds.maxY) || (bounds.minX ?? 0) >= (bounds.maxX ?? 0) || (bounds.minY ?? 0) >= (bounds.maxY ?? 0)) errors.push(`${itemLocation}.worldBounds: must define finite increasing bounds`);
|
||||
if (!Number.isInteger(mapAsset.image?.width) || (mapAsset.image?.width ?? 0) < 1 || !Number.isInteger(mapAsset.image?.height) || (mapAsset.image?.height ?? 0) < 1) errors.push(`${itemLocation}.image: width and height must be positive integers`);
|
||||
requireSCUMMapAssetJSONCompatibility(mapAsset, itemLocation);
|
||||
}
|
||||
requireUniqueAssetKeys(liveData.giftCatalogs, "giftCatalogs");
|
||||
for (const [index, catalog] of (liveData.giftCatalogs ?? []).entries()) {
|
||||
|
||||
@@ -120,6 +120,23 @@ type SCUMSQLiteQueryAsset = {
|
||||
};
|
||||
};
|
||||
|
||||
type SCUMMapBounds = { minX: number; minY: number; maxX: number; maxY: number };
|
||||
type SCUMMapImage = { width: number; height: number };
|
||||
type SCUMMapTransformFixture = { name: string; world: { x: number; y: number }; pixel: { x: number; y: number } };
|
||||
type SCUMMapTransformAsset = {
|
||||
key?: string;
|
||||
transformVersion?: string;
|
||||
mapAssetKey?: string;
|
||||
adapterVersion?: string;
|
||||
requiredSchemaFingerprint?: string;
|
||||
worldBounds: SCUMMapBounds;
|
||||
image: SCUMMapImage;
|
||||
validation?: { rejectNonFinite?: boolean; rejectOutOfBounds?: boolean; acceptBoundaryPoints?: boolean };
|
||||
fixtures?: SCUMMapTransformFixture[];
|
||||
};
|
||||
|
||||
type SCUMMapProjection = { ok: true; pixel: { x: number; y: number } } | { ok: false; reason: "non-finite" | "out-of-bounds" };
|
||||
|
||||
type JSONSchemaObject = {
|
||||
type?: unknown;
|
||||
additionalProperties?: boolean;
|
||||
@@ -146,6 +163,14 @@ function sha256FixtureDigest(fixtureDir: string, relativePath: string): string {
|
||||
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(path.join(fixtureDir, relativePath))).digest("hex")}`;
|
||||
}
|
||||
|
||||
function projectSCUMMapCoordinate(transform: SCUMMapTransformAsset, world: { x: number; y: number }): SCUMMapProjection {
|
||||
const { minX, minY, maxX, maxY } = transform.worldBounds;
|
||||
const { width, height } = transform.image;
|
||||
if (![world.x, world.y, minX, minY, maxX, maxY, width, height].every(Number.isFinite)) return { ok: false, reason: "non-finite" };
|
||||
if (world.x < minX || world.x > maxX || world.y < minY || world.y > maxY) return { ok: false, reason: "out-of-bounds" };
|
||||
return { ok: true, pixel: { x: ((world.x - minX) / (maxX - minX)) * (width - 1), y: ((maxY - world.y) / (maxY - minY)) * (height - 1) } };
|
||||
}
|
||||
|
||||
function listProductionGoFiles(directory: string): string[] {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const target = path.join(directory, entry.name);
|
||||
@@ -384,6 +409,58 @@ describe("plugin manifest validation", () => {
|
||||
for (const statement of allowedReadBoundaries) expect(statement).not.toMatch(unsafeSCUMSQLiteReadStatementPattern);
|
||||
});
|
||||
|
||||
it("packages SCUM map metadata and tested coordinate transform assets", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration };
|
||||
const assetPaths = new Set(manifest.assetFiles?.map((file) => file.path) ?? []);
|
||||
const mapAssets = manifest.scumLiveData.mapAssets ?? [];
|
||||
expect(mapAssets.map((asset) => asset.key)).toEqual(["scum-current-service-coordinate-map"]);
|
||||
const mapAsset = mapAssets[0];
|
||||
expect(mapAsset).toMatchObject({ adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, metadataSchemaRef: "schemas/scum-live/map-metadata.schema.json", image: { width: 4096, height: 4096 } });
|
||||
expect([...assetPaths]).toEqual(expect.arrayContaining([mapAsset.assetPath, mapAsset.transformAssetPath]));
|
||||
|
||||
const mapAssetPath = path.join(pluginDir, mapAsset.assetPath);
|
||||
const transformAssetPath = path.join(pluginDir, mapAsset.transformAssetPath);
|
||||
expect(`sha256:${crypto.createHash("sha256").update(fs.readFileSync(mapAssetPath)).digest("hex")}`).toBe(mapAsset.digest);
|
||||
expect(`sha256:${crypto.createHash("sha256").update(fs.readFileSync(transformAssetPath)).digest("hex")}`).toBe(mapAsset.transformDigest);
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(mapAssetPath, "utf8")) as Record<string, any>;
|
||||
const metadataSchema = JSON.parse(fs.readFileSync(path.join(pluginDir, mapAsset.metadataSchemaRef), "utf8"));
|
||||
const validateMetadata = new Ajv2020({ allErrors: true }).compile(metadataSchema);
|
||||
if (!validateMetadata(metadata)) throw new Error(`SCUM map metadata schema failed: ${JSON.stringify(validateMetadata.errors)}`);
|
||||
expect(metadata).toMatchObject({ key: mapAsset.key, adapterVersion: mapAsset.adapterVersion, requiredSchemaFingerprint: currentSCUMSchemaFingerprint, transformAssetPath: mapAsset.transformAssetPath, worldBounds: mapAsset.worldBounds, image: mapAsset.image });
|
||||
expect(metadata.authorization).toMatchObject({ redistribution: "first-party-generated-coordinate-metadata", baseMapArtwork: "not-packaged", renderingAvailability: "unavailable-until-authorized-base-map" });
|
||||
expect(metadata.layers.map((layer: { key: string; subjectType: string }) => `${layer.key}:${layer.subjectType}`)).toEqual(["players:player", "vehicles:vehicle", "flags:flag"]);
|
||||
|
||||
const transform = JSON.parse(fs.readFileSync(transformAssetPath, "utf8")) as SCUMMapTransformAsset;
|
||||
expect(transform).toMatchObject({ mapAssetKey: mapAsset.key, adapterVersion: mapAsset.adapterVersion, requiredSchemaFingerprint: currentSCUMSchemaFingerprint, worldBounds: mapAsset.worldBounds, image: mapAsset.image, validation: { rejectNonFinite: true, rejectOutOfBounds: true, acceptBoundaryPoints: true } });
|
||||
expect(transform.fixtures?.map((fixture) => fixture.name)).toEqual(["observed-minimum-corner", "observed-maximum-corner", "observed-center"]);
|
||||
for (const fixture of transform.fixtures ?? []) {
|
||||
const projection = projectSCUMMapCoordinate(transform, fixture.world);
|
||||
expect(projection.ok).toBe(true);
|
||||
if (projection.ok) {
|
||||
expect(projection.pixel.x).toBeCloseTo(fixture.pixel.x, 6);
|
||||
expect(projection.pixel.y).toBeCloseTo(fixture.pixel.y, 6);
|
||||
}
|
||||
}
|
||||
for (const world of [{ x: Number.NaN, y: 0 }, { x: Number.POSITIVE_INFINITY, y: 0 }, { x: 0, y: Number.NEGATIVE_INFINITY }]) expect(projectSCUMMapCoordinate(transform, world)).toEqual({ ok: false, reason: "non-finite" });
|
||||
for (const world of [{ x: mapAsset.worldBounds.minX - 1, y: mapAsset.worldBounds.minY }, { x: mapAsset.worldBounds.maxX + 1, y: mapAsset.worldBounds.maxY }, { x: mapAsset.worldBounds.minX, y: mapAsset.worldBounds.minY - 1 }, { x: mapAsset.worldBounds.maxX, y: mapAsset.worldBounds.maxY + 1 }]) expect(projectSCUMMapCoordinate(transform, world)).toEqual({ ok: false, reason: "out-of-bounds" });
|
||||
});
|
||||
|
||||
it("rejects SCUM map transform adapter and schema incompatibility", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => {
|
||||
const mapAsset = manifest.scumLiveData.mapAssets[0];
|
||||
const transformPath = mapAsset.transformAssetPath;
|
||||
const transform = JSON.parse(fs.readFileSync(path.join(fixtureDir, transformPath), "utf8"));
|
||||
transform.adapterVersion = "scum-live-data-v2";
|
||||
transform.requiredSchemaFingerprint = `sha256:${"0".repeat(64)}`;
|
||||
writeFixtureJSON(fixtureDir, transformPath, transform);
|
||||
mapAsset.transformDigest = sha256FixtureDigest(fixtureDir, transformPath);
|
||||
});
|
||||
expect(errors.some((error) => error.includes("transform adapterVersion must match the map asset declaration"))).toBe(true);
|
||||
expect(errors.some((error) => error.includes("transform schema fingerprint must match the map asset declaration"))).toBe(true);
|
||||
});
|
||||
|
||||
it("requires SCUM schema probe targets to have generated Run workspace data targets", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
manifest.runtimeProfiles.dataTargets = [];
|
||||
@@ -413,7 +490,11 @@ describe("plugin manifest validation", () => {
|
||||
];
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => {
|
||||
manifest.assetFiles = [...manifest.assetFiles, ...assetPaths.map((assetPath) => ({ path: assetPath, mode: 384 }))];
|
||||
for (const assetPath of assetPaths) writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
|
||||
for (const assetPath of [assetPaths[0], assetPaths[1], assetPaths[2], assetPaths[3], assetPaths[6]]) writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
|
||||
const mapBounds = { minX: -100000, minY: -100000, maxX: 100000, maxY: 100000 };
|
||||
const mapImage = { width: 4096, height: 4096 };
|
||||
writeFixtureJSON(fixtureDir, assetPaths[4], { key: "island-map", mapVersion: "island-map-v1", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, sourceEvidence: "fixture", authorization: { redistribution: "first-party-generated-coordinate-metadata", baseMapArtwork: "not-packaged", renderingAvailability: "unavailable-until-authorized-base-map" }, worldBounds: mapBounds, image: mapImage, layers: [{ key: "players", capability: "positions.read", subjectType: "player", label: "Players", sourceQueryKey: "scum-positions-read" }], transformAssetPath: assetPaths[5] });
|
||||
writeFixtureJSON(fixtureDir, assetPaths[5], { key: "island-transform", transformVersion: "island-transform-v1", mapAssetKey: "island-map", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, worldBounds: mapBounds, image: mapImage, validation: { rejectNonFinite: true, rejectOutOfBounds: true, acceptBoundaryPoints: true }, fixtures: [{ name: "origin", world: { x: 0, y: 0 }, pixel: { x: 2047.5, y: 2047.5 } }] });
|
||||
const digest = (assetPath: string) => sha256FixtureDigest(fixtureDir, assetPath);
|
||||
manifest.scumLiveData = {
|
||||
...manifest.scumLiveData,
|
||||
|
||||
Reference in New Issue
Block a user