UNPKG

mudlet-map-binary-reader

Version:

Reads and writes Mudlet's map binary file (v20), with read-only support for older formats (v16-v19). Can output .js files needed for Mudlet Map Reader.

2,804 lines 69.7 kB
import { buffer } from "qtdatastream-web"; import { QBool, QClass, QDouble, QInt, QString, QUInt, QUserType, Types, qtype } from "qtdatastream-web/types"; import { concat, fromInt8, fromUint16BE } from "qtdatastream-web/bytes"; //#region src/models/qstream-types.ts var QEnum = class extends QClass { static read(buffer) { return buffer.readInt8(); } toBuffer() { return fromInt8(this.__obj); } }; var QUint16 = class extends QClass { static read(buffer) { return buffer.readUInt16BE(); } toBuffer() { return fromUint16BE(this.__obj); } }; /** * Supports RGB only */ var QColor = class extends QClass { static read(buffer) { return { spec: QEnum.read(buffer), alpha: QUint16.read(buffer) >> 8, r: QUint16.read(buffer) >> 8, g: QUint16.read(buffer) >> 8, b: QUint16.read(buffer) >> 8, pad: QUint16.read(buffer) >> 8 }; } toBuffer() { const color = this.__obj; const bufs = []; bufs.push(QEnum.from(color.spec).toBuffer(false)); bufs.push(QUint16.from(color.alpha * 257).toBuffer(false)); bufs.push(QUint16.from(color.r * 257).toBuffer(false)); bufs.push(QUint16.from(color.g * 257).toBuffer(false)); bufs.push(QUint16.from(color.b * 257).toBuffer(false)); bufs.push(QUint16.from((color.pad ?? 0) * 257).toBuffer(false)); return concat(bufs); } }; var QString$1 = class extends QString { toBuffer() { if (this.__obj === "") return QUInt.from(4294967295).toBuffer(); return super.toBuffer(); } }; var QFont = class extends QClass { static read(buffer) { const family = QString$1.read(buffer); const style = QString$1.read(buffer); const pointSize = QDouble.read(buffer); const pixelSize = QInt.read(buffer); const styleHint = QEnum.read(buffer); const styleStrategy = QUint16.read(buffer); buffer.readInt8(); const weight = buffer.readInt8() >>> 0; const fontBits = buffer.readInt8() >>> 0; const stretch = buffer.readUInt16BE(); const extendedFontBits = buffer.readInt8() >>> 0; return { family, style, pointSize, pixelSize, styleHint, styleStrategy, weight, fontBits, stretch, extendedFontBits, letterSpacing: QInt.read(buffer), wordSpacing: QInt.read(buffer), hintingPreference: buffer.readInt8() >>> 0, capital: buffer.readInt8() >>> 0, styleSetting: (fontBits & 1) !== 0, underline: (fontBits & 2) !== 0, overline: (fontBits & 64) !== 0, strikeOut: (fontBits & 4) !== 0, fixedPitch: (fontBits & 8) !== 0, kerning: (fontBits & 16) !== 0, styleOblique: (fontBits & 128) !== 0, ignorePitch: (extendedFontBits & 1) !== 0, letterSpacingIsAbsolute: (extendedFontBits & 2) !== 0 }; } toBuffer() { const f = this.__obj; return concat([ QString$1.from(f.family).toBuffer(), QString$1.from(f.style).toBuffer(), QDouble.from(f.pointSize).toBuffer(), QInt.from(f.pixelSize).toBuffer(), QEnum.from(f.styleHint).toBuffer(), QUint16.from(f.styleStrategy).toBuffer(), new Uint8Array(1), fromInt8(f.weight), fromInt8(f.fontBits), fromUint16BE(f.stretch), fromInt8(f.extendedFontBits), QInt.from(f.letterSpacing).toBuffer(), QInt.from(f.wordSpacing).toBuffer(), fromInt8(f.hintingPreference), fromInt8(f.capital) ]); } }; var QPoint = class extends QClass { static read(buffer) { return [QDouble.read(buffer), QDouble.read(buffer)]; } toBuffer() { const [x, y] = this.__obj; return concat([QDouble.from(x).toBuffer(), QDouble.from(y).toBuffer()]); } }; var QVector = class extends QClass { static read(buffer) { return [ QDouble.read(buffer), QDouble.read(buffer), QDouble.read(buffer) ]; } toBuffer() { const [x, y, z] = this.__obj; return concat([ QDouble.from(x).toBuffer(), QDouble.from(y).toBuffer(), QDouble.from(z).toBuffer() ]); } }; /** * Supports .pngs */ var QPixMap = class extends QClass { static read(buffer) { QUInt.read(buffer); const start = buffer.read_offset; if (buffer.readUInt32BE() !== 2303741511) { buffer.read_offset -= 4; return ""; } while (buffer.readUInt32BE() !== 1229278788) buffer.read_offset -= 3; const end = buffer.read_offset; buffer.read_offset = start; const size = end - start; return buffer.slice(size + 4); } toBuffer() { const data = this.__obj; return concat([QUInt.from(1).toBuffer(), data !== "" ? data : new Uint8Array(0)]); } }; //#endregion //#region src/models/base-types.ts /** * Qt type IDs used across every map version. These are standard Qt * QMetaType ids (QPoint, QFont, QColor, …) and don't change between Mudlet * map versions, so they're registered once and shared by all version models. */ const Types$1 = { ...Types, POINT: 25, FONT: 64, PIXMAP: 65, COLOR: 67, VECTOR: 84 }; let registered = false; /** Register the version-independent Qt types. Idempotent. */ function registerBaseTypes() { if (registered) return; registered = true; qtype(Types$1.POINT)(QPoint); qtype(Types$1.FONT)(QFont); qtype(Types$1.PIXMAP)(QPixMap); qtype(Types$1.COLOR)(QColor); qtype(Types$1.VECTOR)(QVector); } //#endregion //#region src/models/mudlet-types.ts /** Build the per-area label-list container for a given `MudletLabel` type name. */ function createMudletLabels(labelType) { return class MudletLabels extends QClass { static read(buffer) { const areasWithLabelsTotal = QInt.read(buffer); const labels = {}; for (let index = 0; index < areasWithLabelsTotal; index++) { const totalLabels = QInt.read(buffer); const areaId = QInt.read(buffer); labels[areaId] = []; for (let i = 0; i < totalLabels; i++) labels[areaId].push(QUserType.get(labelType).read(buffer)); } return labels; } toBuffer() { const obj = this.__obj; const buffers = []; buffers.push(QInt.from(Object.keys(obj).length).toBuffer()); for (const key of Object.keys(obj)) { const areaId = parseInt(key); buffers.push(QInt.from(obj[areaId].length).toBuffer()); buffers.push(QInt.from(areaId).toBuffer()); for (const label of obj[areaId]) buffers.push(QUserType.get(labelType).from(label).toBuffer(true)); } return concat(buffers); } }; } /** Build the area-table container for a given `MudletArea` type name. */ function createMudletAreas(areaType) { return class MudletAreas extends QClass { static read(buffer) { const areas = {}; const areaSize = QInt.read(buffer); for (let index = 0; index < areaSize; index++) { const id = QInt.read(buffer); areas[id] = QUserType.get(areaType).read(buffer); } return areas; } toBuffer() { const obj = this.__obj; const buffers = []; buffers.push(QInt.from(Object.keys(obj).length).toBuffer()); for (const [key, area] of Object.entries(obj).sort((a, b) => parseInt(a[0]) - parseInt(b[0]))) { buffers.push(QInt.from(parseInt(key)).toBuffer()); buffers.push(QUserType.get(areaType).from(area).toBuffer(true)); } return concat(buffers); } }; } /** Build the room-table container for a given `MudletRoom` type name. */ function createMudletRooms(roomType) { return class MudletRooms extends QClass { static read(buffer) { const rooms = {}; while (buffer.buffer.length > buffer.read_offset) { const id = QInt.read(buffer); rooms[id] = QUserType.get(roomType).read(buffer); } return rooms; } toBuffer() { const obj = this.__obj; const buffers = []; for (const [key, room] of Object.entries(obj).reverse()) { buffers.push(QInt.from(parseInt(key)).toBuffer()); buffers.push(QUserType.get(roomType).from(room).toBuffer(true)); } return concat(buffers); } }; } //#endregion //#region src/models/qstream-containers.ts let customCounter = 1e3; const customMapCache = {}; const customMultiMapCache = {}; const customArrayCache = {}; const customPairCache = {}; function resolveQType(typeOrClass) { if (typeof typeOrClass === "number" || !typeOrClass.qtype) return QClass.types.get(typeOrClass); return typeOrClass; } function mudletSorter(a, b) { if (parseInt(a[0]) === -1) return -1; if (parseInt(b[0]) === -1) return 1; return parseInt(a[0]) - parseInt(b[0]); } function createTypedMultiMap(keyClass, valueClass) { return class QTypedMultiMap extends QClass { static read(buffer) { const map = {}; const count = QUInt.read(buffer); for (let index = 0; index < count; index++) { const key = keyClass.read(buffer); const value = valueClass.read(buffer); if (map[key] === void 0) map[key] = []; map[key].push(value); } return map; } toBuffer() { const bufs = []; const obj = this.__obj; if (obj instanceof Map) { bufs.push(QUInt.from(obj.size).toBuffer()); for (const [key, value] of obj) { bufs.push(keyClass.from(key).toBuffer()); bufs.push(valueClass.from(value).toBuffer()); } } else { let counter = 0; for (const [key, value] of Object.entries(obj).reverse()) for (const item of value) { counter++; bufs.push(keyClass.from(key).toBuffer()); bufs.push(valueClass.from(item).toBuffer()); } bufs.unshift(QUInt.from(counter).toBuffer()); } return concat(bufs); } }; } function createTypedMap(keyClass, valueClass) { return class QTypedMap extends QClass { static read(buffer) { const map = {}; const count = QUInt.read(buffer); for (let index = 0; index < count; index++) { const key = keyClass.read(buffer); map[key] = valueClass.read(buffer); } return map; } toBuffer() { const bufs = []; const obj = this.__obj; if (obj instanceof Map) { bufs.push(QUInt.from(obj.size).toBuffer()); for (const [key, value] of obj) { bufs.push(keyClass.from(key).toBuffer()); bufs.push(valueClass.from(value).toBuffer()); } } else { const entries = Object.entries(obj); bufs.push(QUInt.from(entries.length).toBuffer()); for (const [key, value] of entries.sort(mudletSorter)) { bufs.push(keyClass.from(key).toBuffer()); bufs.push(valueClass.from(value).toBuffer()); } } return concat(bufs); } }; } function createTypedList(valueClass) { return class QTypedList extends QClass { static read(buffer) { const list = []; const count = QUInt.read(buffer); for (let index = 0; index < count; index++) list.push(valueClass.read(buffer)); return list; } toBuffer() { const bufs = []; const arr = this.__obj; bufs.push(QUInt.from(arr.length).toBuffer()); for (const el of arr) bufs.push(valueClass.from(el).toBuffer()); return concat(bufs); } }; } function createTypedPair(first, second) { return class QTypedPair extends QClass { static read(buffer) { return [first.read(buffer), second.read(buffer)]; } toBuffer() { const pair = this.__obj; return concat([first.from(pair[0]).toBuffer(), second.from(pair[1]).toBuffer()]); } }; } function QMultiMap(keyClass, valueClass) { const resolvedKey = resolveQType(keyClass); const resolvedValue = resolveQType(valueClass); const keyStr = String(resolvedKey); const valStr = String(resolvedValue); if (!customMultiMapCache[keyStr]) customMultiMapCache[keyStr] = {}; if (!customMultiMapCache[keyStr][valStr]) { const clazz = createTypedMultiMap(resolvedKey, resolvedValue); const counter = customCounter++; qtype(counter)(clazz); customMultiMapCache[keyStr][valStr] = counter; } return customMultiMapCache[keyStr][valStr]; } function QMap(keyClass, valueClass, _reversed) { const resolvedKey = resolveQType(keyClass); const resolvedValue = resolveQType(valueClass); const keyStr = String(resolvedKey); const valStr = String(resolvedValue); if (!customMapCache[keyStr]) customMapCache[keyStr] = {}; if (!customMapCache[keyStr][valStr]) { const clazz = createTypedMap(resolvedKey, resolvedValue); const counter = customCounter++; qtype(counter)(clazz); customMapCache[keyStr][valStr] = counter; } return customMapCache[keyStr][valStr]; } function QList(valueClass) { const cacheKey = String(valueClass); if (!customArrayCache[cacheKey]) { const clazz = createTypedList(valueClass); const counter = customCounter++; qtype(counter)(clazz); customArrayCache[cacheKey] = counter; } return customArrayCache[cacheKey]; } function QPair(first, second) { const key = `${first.name}#${second.name}`; if (!customPairCache[key]) { const clazz = createTypedPair(first, second); const counter = customCounter++; qtype(counter)(clazz); customPairCache[key] = counter; } return customPairCache[key]; } //#endregion //#region src/models/model-registry.ts const models = /* @__PURE__ */ new Map(); /** Register the model for a format version. A later call for the same version wins. */ function registerMapModel(model) { models.set(model.version, model); } /** Look up the model for a format version, or `undefined` if unsupported. */ function getMapModel(version) { return models.get(version); } /** Every format version that currently has a registered model, ascending. */ function getSupportedVersions() { return [...models.keys()].sort((a, b) => a - b); } //#endregion //#region src/models/legacy.ts registerBaseTypes(); const PEN_STYLE = { "dot line": 3, "dash line": 2, "dash dot line": 4, "dash dot dot line": 5 }; const LEGACY_DIRECTION_KEYS = { N: "n", E: "e", S: "s", W: "w", UP: "up", DOWN: "down", NE: "ne", SE: "se", SW: "sw", NW: "nw", IN: "in", OUT: "out" }; /** Userdata key Mudlet uses to carry the real symbol of a pre-v19 room. */ const FALLBACK_SYMBOL_KEY = "system.fallback_symbol"; /** Lower-case known direction keys, mirroring Mudlet's legacy custom-line load. */ function normalizeDirectionKeys(record) { const out = {}; for (const key of Object.keys(record)) out[LEGACY_DIRECTION_KEYS[key] ?? key] = record[key]; return out; } const DEFAULT_FONT = { family: "Bitstream Vera Sans Mono", style: "", pointSize: 10, pixelSize: -1, styleHint: 0, styleStrategy: 0, weight: 50, fontBits: 0, stretch: 0, extendedFontBits: 0, letterSpacing: 0, wordSpacing: 0, hintingPreference: 0, capital: 0, styleSetting: false, underline: false, overline: false, strikeOut: false, fixedPitch: true, kerning: false, styleOblique: false, ignorePitch: false, letterSpacingIsAbsolute: false }; /** Reads a v16-v18 room symbol: a single signed byte (Qt qint8 char code). */ var QSymbolByte = class extends QClass { static read(buffer) { const code = buffer.readInt8(); return code > 32 ? String.fromCodePoint(code) : ""; } toBuffer() { throw new Error("writing legacy Mudlet map versions is not supported"); } }; /** Reads a pre-v20 custom-line colour: a QList<int> of [r, g, b] -> MudletColor. */ var QColorFromIntList = class extends QClass { static read(buffer) { const count = QUInt.read(buffer); const channels = []; for (let i = 0; i < count; i++) channels.push(QInt.read(buffer)); if (channels.length >= 3) return { spec: 1, alpha: 255, r: channels[0], g: channels[1], b: channels[2], pad: 0 }; return { spec: 1, alpha: 255, r: 255, g: 0, b: 0, pad: 0 }; } toBuffer() { throw new Error("writing legacy Mudlet map versions is not supported"); } }; /** Reads a pre-v20 custom-line style: a QString style-name -> Qt::PenStyle int. */ var QStyleFromString = class extends QClass { static read(buffer) { return PEN_STYLE[QString$1.read(buffer)] ?? 1; } toBuffer() { throw new Error("writing legacy Mudlet map versions is not supported"); } }; /** * Reads the pre-v18 mRoomIdHash: a single legacy "current room" int. The * profile name it was keyed by in C++ isn't in the stream, so the value can't * be reattached meaningfully — we consume the int and surface an empty map. */ var QLegacyRoomId = class extends QClass { static read(buffer) { QInt.read(buffer); return {}; } toBuffer() { throw new Error("writing legacy Mudlet map versions is not supported"); } }; const LEGACY_TYPE = { SYMBOL_BYTE: 300, COLOR_INTLIST: 301, STYLE_STRING: 302, ROOM_ID: 303 }; let legacyTypesRegistered = false; function registerLegacyValueTypes() { if (legacyTypesRegistered) return; legacyTypesRegistered = true; qtype(LEGACY_TYPE.SYMBOL_BYTE)(QSymbolByte); qtype(LEGACY_TYPE.COLOR_INTLIST)(QColorFromIntList); qtype(LEGACY_TYPE.STYLE_STRING)(QStyleFromString); qtype(LEGACY_TYPE.ROOM_ID)(QLegacyRoomId); } const CONFIGS = { 16: { hasMapUserData: false, hasMapFont: false, modernRoomIdHash: false, modernArea: false, stringSymbol: false }, 17: { hasMapUserData: true, hasMapFont: false, modernRoomIdHash: false, modernArea: true, stringSymbol: false }, 18: { hasMapUserData: true, hasMapFont: false, modernRoomIdHash: true, modernArea: true, stringSymbol: false }, 19: { hasMapUserData: true, hasMapFont: true, modernRoomIdHash: true, modernArea: true, stringSymbol: true } }; /** * Backfill header-level (non-room) fields this version's layout doesn't * carry, so the canonical `MudletMapHeader`/`MudletMap` is always fully * populated for downstream consumers. Shared by the full `read` and the * streaming `readHeader`. */ function backfillHeader(header, cfg) { if (!cfg.hasMapUserData) header.mUserData = {}; if (!cfg.hasMapFont) { header.mapSymbolFont = { ...DEFAULT_FONT }; header.mapFontFudgeFactor = 1; header.useOnlyMapFont = false; } if (!cfg.modernArea) for (const value of Object.values(header.areas)) { const area = value; delete area.legacyForZUnused1; delete area.legacyForZUnused2; area.userData = {}; } } /** * Backfill a single room's fields this version's layout doesn't carry. * Shared by the full `read` and the streaming `readRoom`. */ function backfillRoom(room, cfg) { if (!cfg.stringSymbol && room.userData) { const fallback = room.userData[FALLBACK_SYMBOL_KEY]; if (typeof fallback === "string" && fallback.length > 0) room.symbol = fallback; delete room.userData[FALLBACK_SYMBOL_KEY]; } room.customLines = normalizeDirectionKeys(room.customLines); room.customLinesArrow = normalizeDirectionKeys(room.customLinesArrow); room.customLinesColor = normalizeDirectionKeys(room.customLinesColor); room.customLinesStyle = normalizeDirectionKeys(room.customLinesStyle); } /** * Register a read-only MapModel for one of the legacy versions 16-19. Each * version gets its own version-qualified QUserType names and Qt container ids * so the global registry never clobbers another version (including v20). */ function registerLegacyMapModel(version) { const cfg = CONFIGS[version]; if (!cfg) throw new Error(`No legacy config for Mudlet map version ${version}`); registerLegacyValueTypes(); const TYPE = { MAP: `MudletMap@${version}`, HEADER: `MudletMapHeader@${version}`, AREA: `MudletArea@${version}`, ROOM: `MudletRoom@${version}`, LABEL: `MudletLabel@${version}` }; const CONTAINER = { LABELS: version * 10, ROOMS: version * 10 + 1, AREAS: version * 10 + 2 }; qtype(CONTAINER.LABELS)(createMudletLabels(TYPE.LABEL)); qtype(CONTAINER.ROOMS)(createMudletRooms(TYPE.ROOM)); qtype(CONTAINER.AREAS)(createMudletAreas(TYPE.AREA)); const areaFields = [ { rooms: QList(QUInt) }, { zLevels: QList(QInt) }, { mAreaExits: QMultiMap(QInt, QPair(QInt, QInt)) }, { gridMode: Types$1.BOOL }, { max_x: Types$1.INT }, { max_y: Types$1.INT }, { max_z: Types$1.INT }, { min_x: Types$1.INT }, { min_y: Types$1.INT }, { min_z: Types$1.INT }, { span: Types$1.VECTOR } ]; if (cfg.modernArea) areaFields.push({ xmaxForZ: QMap(QInt, QInt) }, { ymaxForZ: QMap(QInt, QInt) }, { xminForZ: QMap(QInt, QInt) }, { yminForZ: QMap(QInt, QInt) }); else areaFields.push({ xmaxForZ: QMap(QInt, QInt) }, { ymaxForZ: QMap(QInt, QInt) }, { legacyForZUnused1: QMap(QInt, QInt) }, { xminForZ: QMap(QInt, QInt) }, { yminForZ: QMap(QInt, QInt) }, { legacyForZUnused2: QMap(QInt, QInt) }); areaFields.push({ pos: Types$1.VECTOR }, { isZone: Types$1.BOOL }, { zoneAreaRef: Types$1.INT }); if (cfg.modernArea) areaFields.push({ userData: QMap(QString$1, QString$1) }); QUserType.register(TYPE.AREA, areaFields); QUserType.register(TYPE.ROOM, [ { area: Types$1.INT }, { x: Types$1.INT }, { y: Types$1.INT }, { z: Types$1.INT }, { north: Types$1.INT }, { northeast: Types$1.INT }, { east: Types$1.INT }, { southeast: Types$1.INT }, { south: Types$1.INT }, { southwest: Types$1.INT }, { west: Types$1.INT }, { northwest: Types$1.INT }, { up: Types$1.INT }, { down: Types$1.INT }, { in: Types$1.INT }, { out: Types$1.INT }, { environment: Types$1.INT }, { weight: Types$1.INT }, { name: Types$1.STRING }, { isLocked: Types$1.BOOL }, { rawSpecialExits: QMultiMap(QUInt, QString$1) }, { symbol: cfg.stringSymbol ? Types$1.STRING : LEGACY_TYPE.SYMBOL_BYTE }, { userData: QMap(QString$1, QString$1) }, { customLines: QMap(QString$1, QList(QPoint)) }, { customLinesArrow: QMap(QString$1, QBool) }, { customLinesColor: QMap(QString$1, QColorFromIntList) }, { customLinesStyle: QMap(QString$1, QStyleFromString) }, { exitLocks: QList(QInt) }, { stubs: QList(QInt) }, { exitWeights: QMap(QString$1, QInt) }, { doors: QMap(QString$1, QInt) } ]); QUserType.register(TYPE.LABEL, [ { id: Types$1.INT }, { pos: Types$1.VECTOR }, { dummy1: Types$1.DOUBLE }, { dummy2: Types$1.DOUBLE }, { size: QPair(QDouble, QDouble) }, { text: Types$1.STRING }, { fgColor: Types$1.COLOR }, { bgColor: Types$1.COLOR }, { pixMap: Types$1.PIXMAP }, { noScaling: Types$1.BOOL }, { showOnTop: Types$1.BOOL } ]); const mapFields = [ { version: Types$1.INT }, { envColors: QMap(QInt, QInt) }, { areaNames: QMap(QInt, QString$1, true) }, { mCustomEnvColors: QMap(QInt, QColor) }, { mpRoomDbHashToRoomId: QMap(QString$1, QUInt) } ]; if (cfg.hasMapUserData) mapFields.push({ mUserData: QMap(QString$1, QString$1) }); if (cfg.hasMapFont) mapFields.push({ mapSymbolFont: Types$1.FONT }, { mapFontFudgeFactor: Types$1.DOUBLE }, { useOnlyMapFont: Types$1.BOOL }); mapFields.push({ areas: CONTAINER.AREAS }); mapFields.push(cfg.modernRoomIdHash ? { mRoomIdHash: QMap(QString$1, QInt) } : { mRoomIdHash: LEGACY_TYPE.ROOM_ID }); mapFields.push({ labels: CONTAINER.LABELS }); QUserType.register(TYPE.HEADER, mapFields); QUserType.register(TYPE.MAP, [...mapFields, { rooms: CONTAINER.ROOMS }]); registerMapModel({ version, read: (rb) => { const map = QUserType.read(rb, TYPE.MAP); backfillHeader(map, cfg); for (const room of Object.values(map.rooms)) backfillRoom(room, cfg); return map; }, write: () => { throw new Error(`Writing Mudlet map version ${version} is not supported (read-only). Mudlet only saves the latest format.`); }, readHeader: (rb) => { const header = QUserType.read(rb, TYPE.HEADER); backfillHeader(header, cfg); return header; }, readRoom: (rb) => { const id = QInt.read(rb); const room = QUserType.get(TYPE.ROOM).read(rb); backfillRoom(room, cfg); return { id, room }; } }); } //#endregion //#region src/models/v16.ts registerLegacyMapModel(16); //#endregion //#region src/models/v17.ts registerLegacyMapModel(17); //#endregion //#region src/models/v18.ts registerLegacyMapModel(18); //#endregion //#region src/models/v19.ts registerLegacyMapModel(19); //#endregion //#region src/models/v20.ts const VERSION = 20; registerBaseTypes(); const TYPE = { MAP: `MudletMap@${VERSION}`, HEADER: `MudletMapHeader@${VERSION}`, AREA: `MudletArea@${VERSION}`, ROOM: `MudletRoom@${VERSION}`, LABEL: `MudletLabel@${VERSION}` }; const CONTAINER = { LABELS: 200, ROOMS: 201, AREAS: 202 }; qtype(CONTAINER.LABELS)(createMudletLabels(TYPE.LABEL)); qtype(CONTAINER.ROOMS)(createMudletRooms(TYPE.ROOM)); qtype(CONTAINER.AREAS)(createMudletAreas(TYPE.AREA)); QUserType.register(TYPE.AREA, [ { rooms: QList(QUInt) }, { zLevels: QList(QInt) }, { mAreaExits: QMultiMap(QInt, QPair(QInt, QInt)) }, { gridMode: Types$1.BOOL }, { max_x: Types$1.INT }, { max_y: Types$1.INT }, { max_z: Types$1.INT }, { min_x: Types$1.INT }, { min_y: Types$1.INT }, { min_z: Types$1.INT }, { span: Types$1.VECTOR }, { xmaxForZ: QMap(QInt, QInt) }, { ymaxForZ: QMap(QInt, QInt) }, { xminForZ: QMap(QInt, QInt) }, { yminForZ: QMap(QInt, QInt) }, { pos: Types$1.VECTOR }, { isZone: Types$1.BOOL }, { zoneAreaRef: Types$1.INT }, { userData: QMap(QString$1, QString$1) } ]); QUserType.register(TYPE.ROOM, [ { area: Types$1.INT }, { x: Types$1.INT }, { y: Types$1.INT }, { z: Types$1.INT }, { north: Types$1.INT }, { northeast: Types$1.INT }, { east: Types$1.INT }, { southeast: Types$1.INT }, { south: Types$1.INT }, { southwest: Types$1.INT }, { west: Types$1.INT }, { northwest: Types$1.INT }, { up: Types$1.INT }, { down: Types$1.INT }, { in: Types$1.INT }, { out: Types$1.INT }, { environment: Types$1.INT }, { weight: Types$1.INT }, { name: Types$1.STRING }, { isLocked: Types$1.BOOL }, { rawSpecialExits: QMultiMap(QUInt, QString$1) }, { symbol: Types$1.STRING }, { userData: QMap(QString$1, QString$1) }, { customLines: QMap(QString$1, QList(QPoint)) }, { customLinesArrow: QMap(QString$1, QBool) }, { customLinesColor: QMap(QString$1, QColor) }, { customLinesStyle: QMap(QString$1, QUInt) }, { exitLocks: QList(QInt) }, { stubs: QList(QInt) }, { exitWeights: QMap(QString$1, QInt) }, { doors: QMap(QString$1, QInt) } ]); QUserType.register(TYPE.LABEL, [ { id: Types$1.INT }, { pos: Types$1.VECTOR }, { dummy1: Types$1.DOUBLE }, { dummy2: Types$1.DOUBLE }, { size: QPair(QDouble, QDouble) }, { text: Types$1.STRING }, { fgColor: Types$1.COLOR }, { bgColor: Types$1.COLOR }, { pixMap: Types$1.PIXMAP }, { noScaling: Types$1.BOOL }, { showOnTop: Types$1.BOOL } ]); const HEADER_FIELDS = [ { version: Types$1.INT }, { envColors: QMap(QInt, QInt) }, { areaNames: QMap(QInt, QString$1, true) }, { mCustomEnvColors: QMap(QInt, QColor) }, { mpRoomDbHashToRoomId: QMap(QString$1, QUInt) }, { mUserData: QMap(QString$1, QString$1) }, { mapSymbolFont: Types$1.FONT }, { mapFontFudgeFactor: Types$1.DOUBLE }, { useOnlyMapFont: Types$1.BOOL }, { areas: CONTAINER.AREAS }, { mRoomIdHash: QMap(QString$1, QInt) }, { labels: CONTAINER.LABELS } ]; QUserType.register(TYPE.HEADER, HEADER_FIELDS); QUserType.register(TYPE.MAP, [...HEADER_FIELDS, { rooms: CONTAINER.ROOMS }]); registerMapModel({ version: VERSION, read: (rb) => QUserType.read(rb, TYPE.MAP), write: (map) => QUserType.get(TYPE.MAP).from(map).toBuffer(true), readHeader: (rb) => QUserType.read(rb, TYPE.HEADER), readRoom: (rb) => { return { id: QInt.read(rb), room: QUserType.get(TYPE.ROOM).read(rb) }; } }); //#endregion //#region src/map-operations.ts const { ReadBuffer } = buffer; const CARDINAL_DIRS = [ "north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest", "up", "down", "in", "out" ]; const DIR_INDEX = { north: 1, northeast: 2, northwest: 3, east: 4, west: 5, south: 6, southeast: 7, southwest: 8, up: 9, down: 10, in: 11, out: 12 }; const DIR_OTHER = 13; /** * Rebuild every area's `mAreaExits` from the current room graph. This field * is a cache Mudlet's autowalker/pathfinder uses to navigate between zones, * and mirrors TArea::determineAreaExits in Mudlet's C++ source. Any editor * that mutates rooms / exits and re-saves must regenerate it — otherwise the * cache goes stale and Mudlet's routing breaks. */ function rebuildAreaExits(map) { for (const areaId in map.areas) { if (!Object.hasOwn(map.areas, areaId)) continue; map.areas[areaId].mAreaExits = {}; } for (const idStr in map.rooms) { if (!Object.hasOwn(map.rooms, idStr)) continue; const roomId = Number(idStr); const room = map.rooms[roomId]; const srcArea = map.areas[room.area]; if (!srcArea) continue; const entries = []; for (const dir of CARDINAL_DIRS) { const targetId = room[dir]; if (!targetId || targetId < 1) continue; const target = map.rooms[targetId]; if (!target || target.area === room.area) continue; entries.push([targetId, DIR_INDEX[dir]]); } const specials = room.mSpecialExits ?? {}; const specialNames = Object.keys(specials).sort(); for (const name of specialNames) { const targetId = specials[name]; if (!targetId || targetId < 1) continue; const target = map.rooms[targetId]; if (!target || target.area === room.area) continue; entries.push([targetId, DIR_OTHER]); } if (entries.length > 0) srcArea.mAreaExits[roomId] = entries; } } /** * Rebuild every area's spatial-extent cache (zLevels, min/max x/y/z, and the * per-Z xminForZ/xmaxForZ/yminForZ/ymaxForZ maps) from the current room graph. * Mirrors Mudlet's TArea::calcSpan in src/TArea.cpp. * * Two quirks inherited from C++: * 1. y is negated when stored on the area. room.y is plain, but * area.min_y / max_y / yminForZ / ymaxForZ all hold `-room.y`. * 2. span and pos are NOT recomputed. calcSpan never touches them, and * the TArea.h header flags them both as effectively dead. We leave * them as-is so round-tripping an untouched map stays byte-identical. * 3. For an area with zero valid rooms, the per-Z maps and zLevels are * cleared but min/max are preserved — again, matching C++. */ function rebuildAreaExtents(map) { for (const areaIdStr in map.areas) { if (!Object.hasOwn(map.areas, areaIdStr)) continue; const area = map.areas[areaIdStr]; area.xminForZ = {}; area.xmaxForZ = {}; area.yminForZ = {}; area.ymaxForZ = {}; area.zLevels = []; let first = true; for (const roomId of area.rooms) { const room = map.rooms[roomId]; if (!room) continue; const x = room.x; const y = -room.y; const z = room.z; if (first) { area.min_x = x; area.max_x = x; area.min_y = y; area.max_y = y; area.min_z = z; area.max_z = z; area.zLevels.push(z); area.xminForZ[z] = x; area.xmaxForZ[z] = x; area.yminForZ[z] = y; area.ymaxForZ[z] = y; first = false; continue; } if (!area.zLevels.includes(z)) area.zLevels.push(z); if (area.xminForZ[z] === void 0 || x < area.xminForZ[z]) area.xminForZ[z] = x; if (x < area.min_x) area.min_x = x; if (area.xmaxForZ[z] === void 0 || x > area.xmaxForZ[z]) area.xmaxForZ[z] = x; if (x > area.max_x) area.max_x = x; if (area.yminForZ[z] === void 0 || y < area.yminForZ[z]) area.yminForZ[z] = y; if (y < area.min_y) area.min_y = y; if (y > area.max_y) area.max_y = y; if (area.ymaxForZ[z] === void 0 || y > area.ymaxForZ[z]) area.ymaxForZ[z] = y; if (z < area.min_z) area.min_z = z; if (z > area.max_z) area.max_z = z; } if (area.zLevels.length > 1) area.zLevels.sort((a, b) => a - b); } } /** * Populate each `room.hash` from the inverse of `map.mpRoomDbHashToRoomId`. * The binary format doesn't carry a per-room hash field — the hash lives * only in the map-level index. Copying it onto the room on load lets * editors treat `room.hash` as the source of truth, and lets * {@link rebuildRoomHashIndex} rebuild the index cleanly on save. * * NOTE: this is specifically about the content hash index * (`mpRoomDbHashToRoomId` — mirrors Mudlet's `TRoomDB::hashToRoomID`). * The similarly-named `mRoomIdHash` is something else entirely: it maps * Mudlet profile name → player's current room ID. Do not touch it here. */ function populateRoomHashes(map) { for (const hash in map.mpRoomDbHashToRoomId) { if (!Object.hasOwn(map.mpRoomDbHashToRoomId, hash)) continue; const roomId = map.mpRoomDbHashToRoomId[hash]; const room = map.rooms[roomId]; if (room) room.hash = hash; } } /** * Rebuild `map.mpRoomDbHashToRoomId` from the `hash` field on each room. * Mirrors Mudlet's `TRoomDB::hashToRoomID`. Callers can freely mutate * `room.hash` (or add/remove rooms) between load and save; this helper * keeps the on-disk index consistent. * * Rooms with `hash === undefined`, `''`, or `null` are skipped. If two * rooms declare the same non-empty hash (should not happen in a sane * map) the later iteration wins and a warning is logged — the underlying * data is already inconsistent at that point. * * NOTE: does not touch `mRoomIdHash`. That's the per-profile player * cursor (profile name → room ID) and is not derivable from rooms. */ function rebuildRoomHashIndex(map) { map.mpRoomDbHashToRoomId = {}; for (const idStr in map.rooms) { if (!Object.hasOwn(map.rooms, idStr)) continue; const hash = map.rooms[idStr].hash; if (typeof hash !== "string" || hash.length === 0) continue; const roomId = Number(idStr); if (Object.hasOwn(map.mpRoomDbHashToRoomId, hash)) { const prev = map.mpRoomDbHashToRoomId[hash]; console.warn(`[mudlet-map-binary-reader] duplicate room hash "${hash}" on rooms ${prev} and ${roomId}; last wins`); } map.mpRoomDbHashToRoomId[hash] = roomId; } } /** * Hydrate a single room's `mSpecialExits` / `mSpecialExitLocks` fields by * parsing the `rawSpecialExits` layout Qt stores on disk. */ function hydrateRoomSpecialExits(room) { room.mSpecialExits = {}; room.mSpecialExitLocks = []; for (const key in room.rawSpecialExits) { if (!Object.hasOwn(room.rawSpecialExits, key)) continue; for (const ex of room.rawSpecialExits[key]) if (ex.startsWith("0")) room.mSpecialExits[ex.substring(1)] = parseInt(key); else if (ex.startsWith("1")) { room.mSpecialExits[ex.substring(1)] = parseInt(key); room.mSpecialExitLocks.push(parseInt(key)); } else room.mSpecialExits[ex] = parseInt(key); } } /** * Hydrate the `mSpecialExits` / `mSpecialExitLocks` fields on every room * by parsing the `rawSpecialExits` layout Qt stores on disk. */ function hydrateSpecialExits(map) { for (const roomId in map.rooms) { if (!Object.hasOwn(map.rooms, roomId)) continue; hydrateRoomSpecialExits(map.rooms[roomId]); } } /** * Inverse of {@link hydrateSpecialExits}: repacks `mSpecialExits` / * `mSpecialExitLocks` back into the `rawSpecialExits` layout Qt expects * before serialising. */ function dehydrateSpecialExits(map) { for (const roomId in map.rooms) { const room = map.rooms[roomId]; const rawSpecialExits = {}; for (const exit in room.mSpecialExits) { if (!Object.hasOwn(room.mSpecialExits, exit)) continue; const exRoomId = room.mSpecialExits[exit]; if (rawSpecialExits[exRoomId] === void 0) rawSpecialExits[exRoomId] = []; const locked = room.mSpecialExitLocks.indexOf(exRoomId) > -1; rawSpecialExits[exRoomId].push((locked ? "1" : "0") + exit); } room.rawSpecialExits = rawSpecialExits; } } /** * Read just the leading version int, on a throwaway buffer, so we can pick the * right version model before parsing the rest of the stream. */ function readMapVersion(buf) { return QInt.read(new ReadBuffer(buf)); } /** * Parse a Mudlet binary map from an in-memory buffer. Environment- * independent: no `fs`, no file path. Callers in Node can pass * `fs.readFileSync(path)`; callers in the browser can pass a `Buffer` * constructed from a `File` / `ArrayBuffer`. * * The format version (the first int in the stream) selects the model. An * unsupported version fails fast with a clear error rather than silently * mis-parsing a layout it doesn't match (reading a v16 map as v20 desyncs the * stream and dies with an opaque "Invalid array length"). */ function readMapFromBuffer$1(buf) { const version = readMapVersion(buf); const model = getMapModel(version); if (!model) throw new Error(`Unsupported Mudlet map version ${version}. Supported version(s): ${getSupportedVersions().join(", ")}.`); const map = model.read(new ReadBuffer(buf)); hydrateSpecialExits(map); populateRoomHashes(map); return map; } /** * Stream a Mudlet binary map room-by-room without ever holding the whole * room graph in memory. Decodes the (small) header sections eagerly — areas, * labels, colours, names — then walks the trailing rooms blob, invoking * `onRoom(id, room)` for each room and discarding it as soon as the callback * returns. Peak memory is therefore `buffer + one room`, not the multi-GB * fully-materialised object graph that {@link readMapFromBuffer} builds. * * This is the building block for chunking/packing very large maps (260 MB+) * that cannot be loaded whole. The format is strictly sequential with no * index, so this still reads every byte once — but it does not accumulate. * Supported for every version {@link readMapFromBuffer} supports (v16-v20); * an unsupported version throws the same error `readMapFromBuffer` would. * * Each emitted room has its `mSpecialExits` / `mSpecialExitLocks` hydrated, * matching {@link readMapFromBuffer}. The per-room content `hash` is NOT set * (it lives in the header's `mpRoomDbHashToRoomId` index, returned here so a * caller can resolve hashes itself if needed). * * `onHeader`, if given, is invoked with the decoded header *before* the room * loop begins. The rooms section itself is unframed (no count), but every * area's `rooms` id-list is in the header, so a caller can sum them there to * learn the total room count up front (e.g. for a progress bar / preallocation). * * @returns the map header (everything except `rooms`). */ function streamRooms$1(buf, onRoom, onHeader) { const version = readMapVersion(buf); const model = getMapModel(version); if (!model) throw new Error(`Unsupported Mudlet map version ${version}. Supported version(s): ${getSupportedVersions().join(", ")}.`); const rb = new ReadBuffer(buf); const header = model.readHeader(rb); onHeader?.(header); while (rb.read_offset < rb.buffer.length) { const { id, room } = model.readRoom(rb); hydrateRoomSpecialExits(room); onRoom(id, room); } return header; } /** * Serialise a map model to a Mudlet binary buffer. Environment- * independent: no `fs`. Node callers persist with * `fs.writeFileSync(path, writeMapToBuffer(map))`; browser callers can * hand it to a `Blob` or HTTP response. */ function writeMapToBuffer$1(map) { const model = getMapModel(map.version); if (!model) throw new Error(`Cannot write Mudlet map: unsupported version ${map.version}. Supported version(s): ${getSupportedVersions().join(", ")}.`); dehydrateSpecialExits(map); rebuildAreaExits(map); rebuildAreaExtents(map); rebuildRoomHashIndex(map); return model.write(map); } //#endregion //#region mudlet-colors.json var mudlet_colors_default = { light_gray: [ 211, 211, 211 ], ansi_027: [ 0, 95, 255 ], pale_turquoise: [ 175, 238, 238 ], ansi_178: [ 215, 175, 0 ], purple: [ 160, 32, 240 ], ansi_079: [ 95, 215, 175 ], ansi_047: [ 0, 255, 95 ], PaleGreen: [ 152, 251, 152 ], ansi_041: [ 0, 215, 95 ], sky_blue: [ 135, 206, 235 ], light_goldenrod_yellow: [ 250, 250, 210 ], OrangeRed: [ 255, 69, 0 ], ansi_185: [ 215, 215, 95 ], ansi_014: [ 0, 255, 255 ], OliveDrab: [ 107, 142, 35 ], PapayaWhip: [ 255, 239, 213 ], chocolate: [ 210, 105, 30 ], cornflower_blue: [ 100, 149, 237 ], ansi_160: [ 215, 0, 0 ], ansi_black: [ 0, 0, 0 ], ansi_058: [ 95, 95, 0 ], gold: [ 255, 215, 0 ], lawn_green: [ 124, 252, 0 ], ansi_036: [ 0, 175, 135 ], grey: [ 190, 190, 190 ], NavajoWhite: [ 255, 222, 173 ], ansi_008: [ 128, 128, 128 ], ansi_204: [ 255, 95, 135 ], sandy_brown: [ 244, 164, 96 ], ansi_024: [ 0, 95, 135 ], ansi_048: [ 0, 255, 135 ], MediumSpringGreen: [ 0, 250, 154 ], DarkOliveGreen: [ 85, 107, 47 ], ansi_244: [ 128, 128, 128 ], ansi_012: [ 0, 0, 255 ], ansi_025: [ 0, 95, 175 ], light_salmon: [ 255, 160, 122 ], ansi_182: [ 215, 175, 215 ], ForestGreen: [ 34, 139, 34 ], ansi_194: [ 215, 255, 215 ], burlywood: [ 222, 184, 135 ], BlanchedAlmond: [ 255, 235, 205 ], ansi_131: [ 175, 95, 95 ], HotPink: [ 255, 105, 180 ], dark_sea_green: [ 143, 188, 143 ], MediumSlateBlue: [ 123, 104, 238 ], LightGrey: [ 211, 211, 211 ], dark_violet: [ 148, 0, 211 ], saddle_brown: [ 139, 69, 19 ], medium_orchid: [ 186, 85, 211 ], ansi_yellow: [ 128, 128, 0 ], ansi_230: [ 255, 255, 215 ], ansi_238: [ 68, 68, 68 ], MintCream: [ 245, 255, 250 ], ansi_188: [ 215, 215, 215 ], pale_green: [ 152, 251, 152 ], pale_goldenrod: [ 238, 232, 170 ], ansi_164: [ 215, 0, 215 ], mint_cream: [ 245, 255, 250 ], violet_red: [ 208, 32, 144 ], ansi_215: [ 255, 175, 95 ], ansi_248: [ 168, 168, 168 ], ansi_163: [ 215, 0, 175 ], ansi_023: [ 0, 95, 95 ], ansi_044: [ 0, 215, 215 ], spring_green: [ 0, 255, 127 ], orange: [ 255, 165, 0 ], LightCyan: [ 224, 255, 255 ], ansi_143: [ 175, 175, 95 ], LightSeaGreen: [ 32, 178, 170 ], salmon: [ 250, 128, 114 ], LightSteelBlue: [ 176, 196, 222 ], ansi_000: [ 0, 0, 0 ], indian_red: [ 205, 92, 92 ], ansi_144: [ 175, 175, 135 ], light_steel_blue: [ 176, 196, 222 ], ansi_251: [ 198, 198, 198 ], ansi_090: [ 135, 0, 135 ], dark_green: [ 0, 100, 0 ], ansi_064: [ 95, 135, 0 ], ghost_white: [ 248, 248, 255 ], ansi_016: [ 0, 0, 0 ], gray: [ 190, 190, 190 ], ansi_127: [ 175, 0, 175 ], ansi_222: [ 255, 215, 135 ], DarkViolet: [ 148, 0, 211 ], ansi_098: [ 135, 95, 215 ], old_lace: [ 253, 245, 230 ], maroon: [ 176, 48, 96 ], snow: [ 255, 250, 250 ], ansi_094: [ 135, 95, 0 ], ansi_050: [ 0, 255, 215 ], ansi_139: [ 175, 135, 175 ], ansi_171: [ 215, 95, 255 ], MediumTurquoise: [ 72, 209, 204 ], blanched_almond: [ 255, 235, 205 ], ansi_087: [ 95, 255, 255 ], LightBlue: [ 173, 216, 230 ], seashell: [ 255, 245, 238 ], ansi_111: [ 135, 175, 255 ], ansi_013: [ 255, 0, 255 ], ansi_light_red: [ 255, 0, 0 ], blue: [ 0, 0, 255 ], dark_slate_grey: [ 47, 79, 79 ], LightGray: [ 211, 211, 211 ], ansi_121: [ 135, 255, 175 ], light_blue: [ 173, 216, 230 ], ansi_119: [ 135, 255, 95 ], DarkSalmon: [ 233, 150, 122 ], ansi_211: [ 255, 135, 175 ], ansi_214: [ 255, 175, 0 ], ansi_077: [ 95, 215, 95 ], floral_white: [ 255, 250, 240 ], ansiCyan: [ 0, 128, 128 ], ansi_086: [ 95, 255, 215 ], ansi_002: [ 0, 128, 0 ], ansi_156: [ 175, 255, 135 ], ansi_042: [ 0, 215, 135 ], SaddleBrown: [ 139, 69, 19 ], ansi_199: [ 255, 0, 175 ], honeydew: [ 240, 255, 240 ], LightSlateGrey: [ 119, 136, 153 ], ansi_217: [ 255, 175, 175 ], tomato: [ 255, 99, 71 ], ansi_184: [ 215, 215, 0 ], forest_green: [ 34, 139, 34 ], ansi_212: [ 255, 135, 215 ], LightSlateBlue: [ 132, 112, 255 ], light_slate_gray: [ 119, 136, 153 ], ansi_light_black: [ 128, 128, 128 ], PaleVioletRed: [ 219, 112, 147 ], LightGoldenrod: [ 238, 221, 130 ], light_slate_blue: [ 132, 112, 255 ], medium_purple: [ 147, 112, 219 ], ansi_175: [ 215, 135, 175 ], ansi_183: [ 215, 175, 255 ], PaleGoldenrod: [ 238, 232, 170 ], ansi_234: [ 28, 28, 28 ], ansi_129: [ 175, 0, 255 ], red: [ 255, 0, 0 ], ansi_010: [ 0, 255, 0 ], ansi_176: [ 215, 135, 215 ], ansi_magenta: [ 128, 0, 128 ], ansi_001: [ 128, 0, 0 ], lavender: [ 230, 230, 250 ], green_yellow: [ 173, 255, 47 ], ansi_046: [ 0, 255, 0 ], dark_olive_green: [ 85, 107, 47 ], ansi_068: [ 95, 135, 215 ], midnight_blue: [ 25, 25, 112 ], ansi_104: [ 135, 135, 215 ], moccasin: [ 255, 228, 181 ], DarkOrange: [ 255, 140, 0 ], ansi_017: [ 0, 0, 95 ], NavyBlue: [ 0, 0, 128 ], papaya_whip: [ 255, 239, 213 ], ansi_240: [ 88, 88, 88 ], light_sea_green: [ 32, 178, 170 ], ansi_109: [ 135, 175, 175 ], ansi_126: [ 175, 0, 135 ], ansi_168: [ 215, 95, 135 ], ansi_005: [ 128, 0, 128 ], black: [ 0, 0, 0 ], ansi_009: [ 255, 0, 0 ], yellow: [ 255, 255, 0 ], light_slate_grey: [ 119, 136, 153 ], goldenrod: [ 218, 165, 32 ], lavender_blush: [ 255, 240, 245 ], ansi_227: [ 255, 255, 95 ], ansi_053: [ 95, 0, 95 ], ansiWhite: [ 192, 192, 192 ], ansi_197: [ 255, 0, 95 ], GreenYellow: [ 173, 255, 47 ], magenta: [ 255, 0, 255 ], ansi_100: [ 135, 135, 0 ], ansi_145: [ 175, 175, 175 ], ansi_140: [ 175, 135, 215 ], ansi_202: [ 255, 95, 0 ], MediumAquamarine: [ 102, 205, 170 ], ansi_235: [ 38, 38, 38 ], thistle: [ 216, 191, 216 ], ansi_162: [ 215, 0, 135 ], ansi_066: [ 95, 135, 135 ], ansi_221: [ 255, 215, 95 ], ansi_097: [ 135, 95, 175 ], ansi_236: [ 48, 48, 48 ], ansi_102: [ 135, 135, 135 ], ansi_011: [ 255, 255, 0 ], ansi_029: [ 0, 135, 95 ], ansi_146: [ 175, 175, 215 ], ansi_223: [ 255, 215, 175 ], ansi_070: [ 95, 175, 0 ], ansi_231: [ 255, 255, 255 ], ansi_233: [ 18, 18, 18 ], ansi_055: [ 95, 0, 175 ], antique_white: [ 250, 235, 215 ], ansi_115: [ 135, 215, 175 ], ansi_133: [ 175, 95, 175 ], ansi_red: [ 128, 0, 0 ], ansi_255: [ 238, 238, 238 ], PaleTurquoise: [ 175, 238, 238 ], ansi_037: [ 0, 175, 175 ], ansi_063: [ 95, 95, 255 ], ansi_242: [ 108, 108, 108 ], ansi_191: [ 215, 255, 95 ], DarkGoldenrod: [ 184, 134, 11 ], ansi_071: [ 95, 175, 95 ], ansi_228: [ 255, 255, 135 ], SpringGreen: [ 0, 255, 127 ], ansi_093: [ 135, 0, 255 ], dark_khaki: [ 189, 183, 107 ], SlateBlue: [ 106, 90, 205 ], ansiRed: [ 128, 0, 0 ], ansi_153: [ 175, 215, 255 ], ansi_167: [ 215, 95, 95 ], ansi_018: [ 0, 0, 135 ], ansi_033: [ 0, 135, 255 ], ansi_022: [ 0, 95, 0 ], alice_blue: [ 240, 248, 255 ], ansi_241: [ 98, 98, 98 ], DarkSlateGray: [ 47, 79, 79 ], ansi_035: [ 0, 175, 95 ], slate_blue: [ 106, 90, 205 ], ansi_237: [ 58, 58, 58 ], bisque: [ 255, 228, 196 ], AntiqueWhite: [ 250, 235, 215 ], IndianRed: [ 205, 92, 92 ], ansi_white: [ 192, 192, 192 ], ansi_148: [ 175, 215, 0 ], light_goldenrod: [ 238, 221, 130 ], ansi_blue: [ 0, 0, 128 ], ansi_253: [ 218, 218, 218 ], ansiLightMagenta: [ 255, 0, 255 ], DimGrey: [ 105, 105, 105 ], ansi_103: [ 135, 135, 175 ], ansi_172: [ 215, 135, 0 ], LightSlateGray: [ 119, 136, 153 ], ansi_038: [ 0, 175, 215 ], lime_green: [ 50, 205, 50 ], deep_sky_blue: [ 0, 191, 255 ], ansiYellow: [ 128, 128, 0 ], misty_rose: [ 255, 228, 225 ], rosy_brown: [ 188, 143, 143 ], ansi_245: [ 138, 138, 138 ], DimGray: [ 105, 105, 105 ], ansi_light_cyan: [ 0, 255, 255 ], ansi_light_green: [ 0, 255, 0 ], ansi_207: [ 255, 95, 255 ], white_smoke: [ 245, 245, 245 ], dark_slate_gray: [ 47, 79, 79 ], ansi_141: [ 175, 135, 255 ], DeepSkyBlue: [ 0, 191, 255 ], ansi_213: [ 255, 135, 255 ], gainsboro: [ 220, 220, 220 ], medium_blue: [ 0, 0, 205 ], ansi_193: [ 215, 255, 175 ], MediumPurple: [ 147, 112, 219 ], ansi_089: [ 135, 0, 95 ], ansi_198: [ 255, 0, 135 ], ansi_166: [ 215, 95, 0 ], firebrick: [ 178, 34, 34 ], VioletRed: [ 208, 32, 144 ], ansi_065: [ 95, 135, 95 ], DarkSeaGreen: [ 143, 188, 143 ], ansi_122: [ 135, 255, 215 ], navy: [ 0, 0, 128 ], cornsilk: [ 255, 248, 220 ], ansi_189: [ 215, 215, 255 ], LightPink: [ 255, 182, 193 ], LightSkyBlue: [ 135, 206, 250 ], ansi_060: [ 95, 95, 135 ], ansi_088: [ 135, 0, 0 ], DarkSlateGrey: [ 47, 79, 79 ], pink: [ 255, 192, 203 ], medium_violet_red: [ 199, 21, 133 ], peru: [ 205, 133, 63 ], ansi_021: [ 0, 0, 255 ], ansi_200: [ 255, 0, 215 ], ansi_028: [ 0, 135, 0 ], ansi_069: [ 95, 135, 255 ], ansi_142: [ 175, 175, 0 ], ansi_052: [ 95, 0, 0 ], beige: [ 245, 245, 220 ], ansi_007: [ 192, 192, 192 ], deep_pink: [ 255, 20, 147 ], medium_turquoise: [ 72, 209, 204 ], ansi_161: [ 215, 0, 95 ], RoyalBlue: [ 65, 105, 225 ], ansi_135: [ 175, 95, 255 ], ansi_020: [ 0, 0, 215 ], ansi_136: [ 175, 135, 0 ], ansi_225: [ 255, 215, 255 ], ansi_249: [ 178, 178, 178 ], DarkTurquoise: [ 0, 206, 209 ], ansi_light_white: [ 255, 255, 255 ], GhostWhite: [ 248, 248, 255 ], ansi_123: [ 135, 255, 255 ], ansi_208: [ 255, 135, 0 ], ansi_062: [ 95, 95, 215 ], ansi_015: [ 255, 255, 255 ], ansi_125: [ 175, 0, 95 ], LightGoldenrodYellow: [ 250, 250, 210 ], DarkKhaki: [ 189, 183, 107 ], ansi_232: [ 8, 8, 8 ], ansi_239: [ 78, 78, 78 ], ansi_219: [ 255, 175, 255 ], ansi_083: [ 95, 255, 95 ], ansi_061: [ 95, 95, 175 ], ansi_075: [ 95, 175, 255 ], linen: [ 250, 240, 230 ], light_coral: [ 240, 128, 128 ], AliceBlue: [ 240, 248, 255 ], ansi_059: [ 95, 95, 95 ], LightSalmon: [ 255, 160, 122 ], khaki: [ 240, 230, 140 ], ansi_073: [ 95, 175, 175 ], ansi_201: [ 255, 0, 255 ], ansi_128: [ 175, 0, 215 ], ansiBlue: [ 0, 0, 128 ], hot_pink: [ 255, 105, 180 ], LemonChiffon: [ 255, 250, 205 ], OldLace: [ 253, 245, 23