Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,176 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { decodeNativeWaterPayload } from "./native-water-payload.mjs";
|
||||
|
||||
function putI32(value) {
|
||||
const out = Buffer.alloc(4);
|
||||
out.writeInt32LE(value);
|
||||
return out;
|
||||
}
|
||||
|
||||
function putU32(value) {
|
||||
const out = Buffer.alloc(4);
|
||||
out.writeUInt32LE(value >>> 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function putF32(value) {
|
||||
const out = Buffer.alloc(4);
|
||||
out.writeFloatLE(value);
|
||||
return out;
|
||||
}
|
||||
|
||||
function putCString(value) {
|
||||
const bytes = Buffer.from(`${value}\0`, "utf8");
|
||||
return Buffer.concat([putI32(bytes.length), bytes]);
|
||||
}
|
||||
|
||||
function guidBytes(fill) {
|
||||
return Buffer.alloc(16, fill);
|
||||
}
|
||||
|
||||
function objectBytes(properties = [], version = 2) {
|
||||
const prefix = [guidBytes(0x11), putI32(version), putCString("")];
|
||||
if (version >= 2) {
|
||||
prefix.push(putI32(properties.length));
|
||||
for (const property of properties) {
|
||||
prefix.push(putCString(property.name), putI32(property.type));
|
||||
if (property.type === 2) prefix.push(putI32(property.value));
|
||||
else if (property.type === 8) prefix.push(putF32(property.value));
|
||||
else if (property.type === 12) prefix.push(putCString(property.value));
|
||||
else throw new Error(`Fixture does not implement property type ${property.type}.`);
|
||||
}
|
||||
}
|
||||
return Buffer.concat(prefix);
|
||||
}
|
||||
|
||||
function parcelHeader({ origin = [0, 0, 0], spacing = 1, overlap = 0, width = 3, height = 2 }) {
|
||||
return Buffer.concat([
|
||||
...origin.map(putF32), putF32(spacing), putI32(overlap), putI32(width), putI32(height),
|
||||
]);
|
||||
}
|
||||
|
||||
function heightParcel({ origin = [0, 0, 0], spacing = 1, overlap = 0, width = 3, height = 2, values = [100, 100, 100, 100, 100, 100] } = {}) {
|
||||
const quantized = Buffer.alloc(values.length * 2);
|
||||
values.forEach((value, index) => quantized.writeUInt16LE(value, index * 2));
|
||||
return Buffer.concat([
|
||||
parcelHeader({ origin, spacing, overlap, width, height }),
|
||||
putF32(0), putF32(10), quantized,
|
||||
]);
|
||||
}
|
||||
|
||||
function colorParcel({ origin = [0, 0, 0], spacing = 1, overlap = 0, width = 3, height = 2, values = [0xff112233, 0xff445566, 0xff778899, 0xffaabbcc, 0xffddeeff, 0xff010203] } = {}) {
|
||||
const data = Buffer.alloc(values.length * 4);
|
||||
values.forEach((value, index) => data.writeUInt32LE(value >>> 0, index * 4));
|
||||
return Buffer.concat([parcelHeader({ origin, spacing, overlap, width, height }), data]);
|
||||
}
|
||||
|
||||
function flagsParcel({ origin = [0, 0, 0], spacing = 1, overlap = 0, width = 3, height = 2, values = [0xff, 0xdf, 0x9f, 0xbf, 0x80, 0x00] } = {}) {
|
||||
return Buffer.concat([parcelHeader({ origin, spacing, overlap, width, height }), Buffer.from(values)]);
|
||||
}
|
||||
|
||||
function layerBytes({ version = 3, bodyFill = 0x22, height = heightParcel(), color = colorParcel(), flags = flagsParcel(), translation = [0, 0, 0] } = {}) {
|
||||
const prefix = [guidBytes(0x33), putI32(version), guidBytes(bodyFill)];
|
||||
if (version >= 3) {
|
||||
prefix.push(
|
||||
guidBytes(0x44), putI32(2), objectBytes(),
|
||||
...[1, 1, 1].map(putF32), ...[0, 0, 0, 1].map(putF32), ...translation.map(putF32),
|
||||
putU32(0), putU32(0), putI32(0),
|
||||
);
|
||||
}
|
||||
if (version === 1) return Buffer.concat([...prefix, height, flags]);
|
||||
return Buffer.concat([...prefix, height, color, flags]);
|
||||
}
|
||||
|
||||
function descriptorBytes({ bodyFill = 0x22, version = 5, properties = [] } = {}) {
|
||||
const prefix = [guidBytes(0x55), putI32(version)];
|
||||
if (version >= 2) prefix.push(objectBytes(properties));
|
||||
prefix.push(guidBytes(bodyFill));
|
||||
if (version >= 3) prefix.push(putI32(0));
|
||||
prefix.push(putI32(4)); // Preserve undeclared source value, do not infer its effect.
|
||||
if (version <= 4) {
|
||||
prefix.push(...Array.from({ length: 6 }, () => putF32(0)), putU32(0), putI32(0), putI32(0), putI32(0));
|
||||
prefix.push(putCString(""), putCString(""), putCString(""));
|
||||
}
|
||||
if (version === 4 || version === 5) prefix.push(putU32(0xff000000), putF32(0), putF32(0), putF32(1));
|
||||
return Buffer.concat(prefix);
|
||||
}
|
||||
|
||||
test("native water v3 payload decodes exact dimensions, vertex channels, transform and GUID descriptor", () => {
|
||||
const payload = layerBytes({ translation: [10, 20, 30] });
|
||||
const descriptor = descriptorBytes({ properties: [{ name: "waterDesc:full:bump name", type: 12, value: "texture\\water\\wave8" }] });
|
||||
const decoded = decodeNativeWaterPayload(payload, {
|
||||
layerCount: 1,
|
||||
descriptors: [{ key: "body-row", payload: descriptor }],
|
||||
});
|
||||
const [layer] = decoded.layers;
|
||||
|
||||
assert.equal(decoded.consumedBytes, payload.length);
|
||||
assert.equal(decoded.trailingBytes, 0);
|
||||
assert.equal(layer.version, 3);
|
||||
assert.deepEqual(layer.transform.translation, [10, 20, 30]);
|
||||
for (const value of layer.height.values) assert.ok(Math.abs(value - (100 / 60000 * 10)) < 1e-7);
|
||||
assert.equal(layer.color.values[0], 0xff112233);
|
||||
assert.equal(layer.flags.values[0], 0xff);
|
||||
assert.equal(layer.descriptorJoin.status, "matched");
|
||||
assert.equal(layer.descriptorJoin.descriptors[0].effectType, 4);
|
||||
assert.equal(layer.descriptorJoin.descriptors[0].baseObject.properties[0].value, "texture\\water\\wave8");
|
||||
});
|
||||
|
||||
test("legacy v2 origin migration uses color parcel origin as transform and resets parcel origins", () => {
|
||||
const payload = layerBytes({
|
||||
version: 2,
|
||||
bodyFill: 0x23,
|
||||
height: heightParcel({ origin: [3, 7, 9], overlap: 1, width: 5, height: 4, values: new Array(20).fill(12) }),
|
||||
color: colorParcel({ origin: [50, 60, 70], width: 3, height: 2 }),
|
||||
flags: flagsParcel({ origin: [8, 9, 10], width: 3, height: 2 }),
|
||||
});
|
||||
const decoded = decodeNativeWaterPayload(payload, { layerCount: 1 });
|
||||
const [layer] = decoded.layers;
|
||||
|
||||
assert.deepEqual(layer.transform.translation, [50, 60, 70]);
|
||||
assert.deepEqual(layer.height.origin, [-1, 0, -1]);
|
||||
assert.deepEqual(layer.color.origin, [0, 0, 0]);
|
||||
assert.deepEqual(layer.flags.origin, [0, 0, 0]);
|
||||
});
|
||||
|
||||
test("legacy v1 layer is decoded but explicitly unbuildable because color bytes are absent", () => {
|
||||
const payload = layerBytes({
|
||||
version: 1,
|
||||
bodyFill: 0x24,
|
||||
height: heightParcel(),
|
||||
flags: flagsParcel({ origin: [40, 50, 60] }),
|
||||
});
|
||||
const decoded = decodeNativeWaterPayload(payload, { layerCount: 1 });
|
||||
const [layer] = decoded.layers;
|
||||
|
||||
assert.equal(layer.color.present, false);
|
||||
assert.equal(layer.unbuildableReason, "native v1 water color is not serialized; refusing guessed vertex RGB");
|
||||
assert.deepEqual(layer.transform.translation, [40, 50, 60]);
|
||||
});
|
||||
|
||||
test("descriptor matching is exact and refuses missing or duplicate GUID matches", () => {
|
||||
const payload = layerBytes();
|
||||
const wrongDescriptor = descriptorBytes({ bodyFill: 0x21 });
|
||||
const missing = decodeNativeWaterPayload(payload, { layerCount: 1, descriptors: [{ key: "wrong", payload: wrongDescriptor }] });
|
||||
assert.equal(missing.layers[0].descriptorJoin.status, "missing");
|
||||
const ambiguous = decodeNativeWaterPayload(payload, {
|
||||
layerCount: 1,
|
||||
descriptors: [
|
||||
{ key: "first", payload: descriptorBytes() },
|
||||
{ key: "second", payload: descriptorBytes() },
|
||||
],
|
||||
});
|
||||
assert.equal(ambiguous.layers[0].descriptorJoin.status, "ambiguous");
|
||||
assert.deepEqual(ambiguous.layers[0].descriptorJoin.descriptorKeys, ["first", "second"]);
|
||||
});
|
||||
|
||||
test("decoder requires exact payload and descriptor consumption", () => {
|
||||
assert.throws(() => decodeNativeWaterPayload(Buffer.concat([layerBytes(), Buffer.from([0])]), { layerCount: 1 }), /trailing/);
|
||||
assert.throws(() => decodeNativeWaterPayload(layerBytes().subarray(0, 100), { layerCount: 1 }), /truncated/);
|
||||
assert.throws(() => decodeNativeWaterPayload(layerBytes(), {
|
||||
layerCount: 1,
|
||||
descriptors: [{ key: "descriptor", payload: Buffer.concat([descriptorBytes(), Buffer.from([1])]) }],
|
||||
}), /trailing/);
|
||||
});
|
||||
Reference in new issue
Block a user