← Files ModRetro Chromatic PluginARCHIVED FILE
dist/dialogue.js
28 KB · Oct 2, 2026 · 00:37 UTC
import { readFile, realpath, stat } from "node:fs/promises";
import path from "node:path";
import { PNG } from "pngjs";
import { GameStudioProjectError, } from "./project.js";
import { PROJECT_RESPONSE_LIMITS } from "./project-access.js";
import { loadProjectSnapshot } from "./project-snapshot.js";
const TILE_SIZE = 8;
const SCREEN_WIDTH = 160;
const SCREEN_HEIGHT = 144;
const DEFAULT_MAX_WIDTH = 128;
const DEFAULT_VISIBLE_LINES = 3;
const AVATAR_RESERVED_WIDTH = 24;
const MAX_FONT_BYTES = 4 * 1024 * 1024;
const MAX_FONT_DIMENSION = 1024;
const MAX_DIAGNOSTICS = 64;
const MAX_EVENTS = 2048;
const MAX_PAGES = 4096;
const DEFAULT_PROJECTED_EVENTS = 50;
const DEFAULT_PROJECTED_PAGES = 50;
const CONTROL_TOKEN = /\$[^$\r\n]+\$|@[^@\r\n]+@|\{\{[^{}\r\n]+\}\}|\\[A-Za-z][A-Za-z0-9]*(?:\[[^\]\r\n]*\])?/gu;
const indexedFontCache = new WeakMap();
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isInsideRoot(root, candidate) {
const relative = path.relative(root, candidate);
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
}
async function readBoundedProjectFile(projectRoot, resourcePath) {
const candidate = path.resolve(projectRoot, resourcePath);
if (!isInsideRoot(projectRoot, candidate)) {
throw new GameStudioProjectError("PATH_OUTSIDE_PROJECT", `Resource is outside the project: ${resourcePath}`, resourcePath);
}
let canonical;
try {
canonical = await realpath(candidate);
}
catch (error) {
throw new GameStudioProjectError("RESOURCE_NOT_FOUND", `Project resource does not exist: ${resourcePath}`, resourcePath);
}
if (!isInsideRoot(projectRoot, canonical)) {
throw new GameStudioProjectError("PATH_OUTSIDE_PROJECT", `Resource resolves outside the project: ${resourcePath}`, resourcePath);
}
const info = await stat(canonical);
if (!info.isFile() || info.size > MAX_FONT_BYTES) {
throw new GameStudioProjectError("INVALID_RESOURCE", `Resource is not a bounded regular file: ${resourcePath}`, resourcePath);
}
return readFile(canonical);
}
function decodeFontImage(bytes, resourcePath) {
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
throw new GameStudioProjectError("INVALID_FONT", `Font is not a valid PNG: ${resourcePath}`, resourcePath);
}
const width = bytes.readUInt32BE(16);
const height = bytes.readUInt32BE(20);
if (width < TILE_SIZE || height < TILE_SIZE || width > MAX_FONT_DIMENSION || height > MAX_FONT_DIMENSION || width % TILE_SIZE !== 0 || height % TILE_SIZE !== 0) {
throw new GameStudioProjectError("INVALID_FONT", `Font atlas dimensions must be bounded multiples of eight: ${resourcePath}`, resourcePath);
}
try {
return PNG.sync.read(bytes);
}
catch (error) {
throw new GameStudioProjectError("INVALID_FONT", `Could not decode font atlas ${resourcePath}: ${error instanceof Error ? error.message : String(error)}`, resourcePath);
}
}
function isMagenta(image, x, y) {
const offset = (image.width * y + x) * 4;
return image.data[offset] === 255 && image.data[offset + 1] === 0 && image.data[offset + 2] === 255 && (image.data[offset + 3] ?? 0) > 0;
}
function measureGlyph(image, index) {
const columns = image.width / TILE_SIZE;
const startX = (index % columns) * TILE_SIZE;
const startY = Math.floor(index / columns) * TILE_SIZE;
let width = TILE_SIZE;
while (width > 0) {
let padding = true;
for (let y = 0; y < TILE_SIZE; y += 1) {
if (!isMagenta(image, startX + width - 1, startY + y)) {
padding = false;
break;
}
}
if (!padding)
break;
width -= 1;
}
return Math.max(width, 1);
}
function addMappings(mapping, source, glyphCount) {
if (!isObject(source))
return;
for (const [key, value] of Object.entries(source)) {
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value < glyphCount) {
mapping.set(key, value);
}
else if (typeof value === "string" && /^\d+$/u.test(key)) {
const index = Number(key);
if (index >= 0 && index < glyphCount)
mapping.set(value, index);
}
}
}
export async function loadDialogueFont(inventory, requestedFontId, access) {
const fontId = requestedFontId ?? (typeof inventory.settings.defaultFontId === "string" ? inventory.settings.defaultFontId : "");
const fonts = inventory.assets.filter((asset) => asset.type === "font");
const asset = fontId ? fonts.find((candidate) => candidate.id === fontId) : fonts.length === 1 ? fonts[0] : undefined;
if (!asset) {
throw new GameStudioProjectError("FONT_NOT_FOUND", fontId ? `The selected font does not exist: ${fontId}` : "The project has no unambiguous selected font");
}
const mappingPath = asset.resourcePath.replace(/\.png$/iu, ".json");
const indexedAccess = access;
const pngDigest = indexedAccess?.authoredFile?.(asset.resourcePath)?.sha256;
const sidecarDigest = asset.metadataPath ? indexedAccess?.authoredFile?.(asset.metadataPath)?.sha256 : undefined;
const mappingDigest = indexedAccess?.authoredFile?.(mappingPath)?.sha256;
const cacheKey = pngDigest === undefined
? undefined
: JSON.stringify([asset.id, fontId, pngDigest, sidecarDigest ?? null, mappingDigest ?? null]);
if (access && cacheKey) {
const cached = indexedFontCache.get(access)?.get(cacheKey);
if (cached)
return cached;
}
const image = decodeFontImage(await readBoundedProjectFile(inventory.projectRoot, asset.resourcePath), asset.resourcePath);
const glyphCount = (image.width / TILE_SIZE) * (image.height / TILE_SIZE);
const glyphWidths = Array.from({ length: glyphCount }, (_, index) => measureGlyph(image, index));
const mapping = new Map();
for (let index = 0; index < glyphCount; index += 1) {
mapping.set(String.fromCodePoint(index + 32), index);
}
addMappings(mapping, asset.mapping, glyphCount);
try {
const parsed = JSON.parse((await readBoundedProjectFile(inventory.projectRoot, mappingPath)).toString("utf8"));
if (isObject(parsed))
addMappings(mapping, parsed.mapping, glyphCount);
}
catch (error) {
if (!(error instanceof GameStudioProjectError) || error.code !== "RESOURCE_NOT_FOUND")
throw error;
}
const font = {
id: asset.id,
name: asset.name,
variableWidth: glyphWidths.some((width) => width !== TILE_SIZE),
image,
glyphWidths,
mapping,
resourcePath: asset.resourcePath,
};
if (access && cacheKey) {
let fonts = indexedFontCache.get(access);
if (!fonts) {
fonts = new Map();
indexedFontCache.set(access, fonts);
}
fonts.set(cacheKey, font);
if (fonts.size > 32)
fonts.delete(fonts.keys().next().value);
}
return font;
}
function isDialogueCommand(command) {
return command === "EVENT_TEXT";
}
function collectResourceEvents(resource, sceneId, owner, requestedScriptKey, destination) {
const visit = (values, scriptKey) => {
for (const candidate of values) {
if (!isObject(candidate))
continue;
const args = isObject(candidate.args) ? candidate.args : {};
if (isDialogueCommand(candidate.command) && (typeof args.text === "string" || (Array.isArray(args.text) && args.text.every((page) => typeof page === "string")))) {
if (destination.length >= MAX_EVENTS)
throw new GameStudioProjectError("DIALOGUE_LIMIT_EXCEEDED", `Dialogue analysis is limited to ${MAX_EVENTS} events`);
destination.push({
id: typeof candidate.id === "string" ? candidate.id : "",
command: candidate.command,
sceneId,
...owner,
scriptKey,
resourcePath: resource.resourcePath,
text: args.text,
avatarId: typeof args.avatarId === "string" ? args.avatarId : "",
position: args.position === "top" ? "top" : "bottom",
});
}
if (isObject(candidate.children)) {
for (const branch of Object.values(candidate.children)) {
if (Array.isArray(branch))
visit(branch, scriptKey);
}
}
}
};
for (const [key, value] of Object.entries(resource)) {
if (!Array.isArray(value) || (key !== "script" && !key.endsWith("Script")) || (requestedScriptKey && key !== requestedScriptKey))
continue;
visit(value, key);
}
}
export function collectDialogueEvents(inventory, input = {}, access) {
if (input.actorId && input.triggerId)
throw new GameStudioProjectError("INVALID_DIALOGUE_TARGET", "A dialogue target cannot select both an actor and a trigger");
let scenes = inventory.scenes;
if (input.sceneId) {
const indexedScene = access?.scene(input.sceneId);
scenes = indexedScene ? [indexedScene] : scenes.filter((scene) => scene.id === input.sceneId);
if (scenes.length === 0)
throw new GameStudioProjectError("SCENE_NOT_FOUND", `No scene exists with id ${input.sceneId}`);
}
const result = [];
let ownerFound = !input.actorId && !input.triggerId;
for (const scene of scenes) {
if (!input.actorId && !input.triggerId)
collectResourceEvents(scene, scene.id, {}, input.scriptKey, result);
for (const actor of scene.actors) {
if (input.triggerId || (input.actorId && actor.id !== input.actorId))
continue;
if (input.actorId)
ownerFound = true;
collectResourceEvents(actor, scene.id, { actorId: actor.id }, input.scriptKey, result);
}
for (const trigger of scene.triggers) {
if (input.actorId || (input.triggerId && trigger.id !== input.triggerId))
continue;
if (input.triggerId)
ownerFound = true;
collectResourceEvents(trigger, scene.id, { triggerId: trigger.id }, input.scriptKey, result);
}
}
if (!ownerFound) {
throw new GameStudioProjectError(input.actorId ? "ACTOR_NOT_FOUND" : "TRIGGER_NOT_FOUND", `The requested dialogue resource does not exist: ${input.actorId ?? input.triggerId}`);
}
const selected = input.eventId ? result.filter((event) => event.id === input.eventId) : result;
if (input.eventId && selected.length === 0)
throw new GameStudioProjectError("EVENT_NOT_FOUND", `No dialogue event exists with id ${input.eventId}`);
return selected;
}
function boundedPositiveInteger(value, fallback, maximum, name) {
if (value === undefined)
return fallback;
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
throw new GameStudioProjectError("INVALID_DIALOGUE_LAYOUT", `${name} must be an integer between 1 and ${maximum}`);
return value;
}
function measureLine(font, text) {
const tokens = [];
const visibleText = text.replace(CONTROL_TOKEN, (token) => {
tokens.push(token);
return "";
});
let width = 0;
const missing = [];
for (const character of visibleText) {
const index = font.mapping.get(character);
if (index === undefined) {
missing.push(character);
width += TILE_SIZE;
}
else {
width += font.glyphWidths[index] ?? TILE_SIZE;
}
}
return { width, tokens, missing };
}
function wrapLine(font, line, maxWidth) {
if (measureLine(font, line).width <= maxWidth)
return [line];
const pieces = line.match(/[^\s]+|[\t ]+/gu) ?? [line];
let current = "";
let pending = "";
const lines = [];
for (const piece of pieces) {
if (/^[\t ]+$/u.test(piece)) {
pending += piece;
continue;
}
if (current && measureLine(font, `${current}${pending}${piece}`).width > maxWidth) {
lines.push(current);
current = piece;
}
else {
current += pending + piece;
}
pending = "";
}
if (pending)
current += pending;
if (current || lines.length === 0)
lines.push(current);
return lines;
}
function transformPages(font, pages, maxWidth, maxVisibleLines, wrap, paginate) {
const result = [];
for (const page of pages) {
const lines = page.split("\n").flatMap((line) => wrap ? wrapLine(font, line, maxWidth) : [line]);
if (paginate && lines.length > maxVisibleLines) {
for (let index = 0; index < lines.length; index += maxVisibleLines)
result.push(lines.slice(index, index + maxVisibleLines).join("\n"));
}
else {
result.push(lines.join("\n"));
}
}
return result;
}
function verifyAvatar(inventory, avatarId, access) {
if (!avatarId)
return undefined;
const lookup = access?.asset({ assetId: avatarId });
const avatar = lookup?.status === "unique" && lookup.asset.type === "avatar"
? lookup.asset
: inventory.assets.find((asset) => asset.type === "avatar" && asset.id === avatarId);
if (!avatar)
throw new GameStudioProjectError("AVATAR_NOT_FOUND", `The selected avatar does not exist: ${avatarId}`);
if (!inventory.assets.includes(avatar))
inventory.assets.push(avatar);
return avatar;
}
function focusedDialogueInventory(access, input) {
const inspection = access.projectInspection();
const indexedAccess = access;
const authoredSettings = indexedAccess.authoredFile?.("project/settings.gbsres")?.json;
const descriptorPath = path.relative(access.projectRoot, access.projectPath).split(path.sep).join("/");
const descriptor = indexedAccess.authoredFile?.(descriptorPath)?.json;
const legacySettings = isObject(descriptor?.settings) ? descriptor.settings : undefined;
const settings = indexedAccess.settings?.() ?? authoredSettings ?? legacySettings ?? inspection.settings;
let scenes;
if (input.sceneId) {
const scene = access.scene(input.sceneId);
scenes = scene ? [scene] : [];
}
else if (input.actorId || input.triggerId) {
const ownerType = input.actorId ? "actor" : "trigger";
const ownerId = input.actorId ?? input.triggerId;
const owner = access.resource({ type: ownerType, id: ownerId });
if (owner) {
const ownerDirectory = owner.resourcePath.replace(/\/(?:actors|triggers)\/[^/]+$/u, "");
scenes = access.resourcesByKind("scene")
.filter((scene) => scene.resourcePath.startsWith(`${ownerDirectory}/`));
}
else {
scenes = [];
}
}
else if (input.text !== undefined && input.eventId === undefined) {
scenes = [];
}
else {
scenes = [...access.resourcesByKind("scene")];
}
const assets = [];
const selectedFontId = input.fontId ?? (typeof settings.defaultFontId === "string" ? settings.defaultFontId : "");
if (selectedFontId) {
const font = access.asset({ assetId: selectedFontId });
if (font.status === "unique" && font.asset.type === "font")
assets.push(font.asset);
}
else {
for (const candidate of access.resourcesByKind("asset")) {
const asset = candidate;
if (asset.type === "font")
assets.push(asset);
}
}
for (const scene of scenes) {
if (typeof scene.backgroundId !== "string" || !scene.backgroundId)
continue;
const background = access.asset({ assetId: scene.backgroundId });
if (background.status === "unique" && background.asset.type === "background" && !assets.includes(background.asset)) {
assets.push(background.asset);
}
}
const uiPaletteId = settings.defaultUIPaletteId;
const uiPalette = typeof uiPaletteId === "string" ? access.palette(uiPaletteId) : undefined;
return {
projectPath: inspection.projectPath,
projectRoot: inspection.projectRoot,
descriptor: descriptor ? { ...descriptor } : {},
format: inspection.format,
version: inspection.version,
name: inspection.name,
author: inspection.author,
settings: { ...settings },
scenes,
actors: scenes.flatMap((scene) => scene.actors),
triggers: scenes.flatMap((scene) => scene.triggers),
palettes: uiPalette ? [uiPalette] : [],
assets,
diagnostics: [],
counts: {
scenes: inspection.counts.scenes,
actors: inspection.counts.actors,
triggers: inspection.counts.triggers,
palettes: inspection.counts.palettes,
assets: inspection.counts.assets,
},
};
}
export async function prepareDialogue(projectPath, input = {}, access) {
if (access)
await access.ensureFresh();
const inventory = access
? focusedDialogueInventory(access, input)
: (await loadProjectSnapshot(projectPath)).inventory;
const font = await loadDialogueFont(inventory, input.fontId, access);
const matching = input.text !== undefined && !input.eventId && !input.sceneId && !input.actorId && !input.triggerId
? []
: collectDialogueEvents(inventory, input, access);
const hasInlineText = input.text !== undefined;
let events;
if (hasInlineText) {
const selected = input.eventId ? matching[0] : undefined;
events = [{
id: selected?.id ?? "",
command: input.eventCommand ?? selected?.command ?? "EVENT_TEXT",
sceneId: selected?.sceneId ?? input.sceneId ?? "",
...(selected?.actorId ?? input.actorId ? { actorId: selected?.actorId ?? input.actorId } : {}),
...(selected?.triggerId ?? input.triggerId ? { triggerId: selected?.triggerId ?? input.triggerId } : {}),
scriptKey: selected?.scriptKey ?? input.scriptKey ?? "script",
resourcePath: selected?.resourcePath ?? "",
text: input.text,
avatarId: input.avatarId ?? selected?.avatarId ?? "",
position: selected?.position ?? "bottom",
}];
}
else {
events = matching.filter((event) => !input.eventCommand || event.command === input.eventCommand);
}
if (input.wrap !== undefined && input.wrap !== "none" && input.wrap !== "word") {
throw new GameStudioProjectError("INVALID_DIALOGUE_LAYOUT", "wrap must be none or word");
}
if (input.profile !== undefined && !["auto", "conservative"].includes(input.profile)) {
throw new GameStudioProjectError("INVALID_DIALOGUE_LAYOUT", "profile must be auto or conservative");
}
const visibleLines = boundedPositiveInteger(input.maxVisibleLines, DEFAULT_VISIBLE_LINES, 16, "maxVisibleLines");
const baseWidth = boundedPositiveInteger(input.maxWidthPx, DEFAULT_MAX_WIDTH, SCREEN_WIDTH, "maxWidthPx");
const anyAvatar = Boolean(input.avatarId || events.some((event) => event.avatarId));
if (input.avatarId)
verifyAvatar(inventory, input.avatarId, access);
const diagnostics = [];
const diagnosticCounts = {};
const pageSummaries = [];
const eventSummaries = [];
let totalLines = 0;
let maximumMeasuredLinePx = 0;
let confidence = "conservative";
let suggestion;
const report = (diagnostic) => {
diagnosticCounts[diagnostic.code] = (diagnosticCounts[diagnostic.code] ?? 0) + 1;
if (diagnostics.length < MAX_DIAGNOSTICS)
diagnostics.push(diagnostic);
};
for (const event of events) {
const avatarId = input.avatarId ?? event.avatarId;
verifyAvatar(inventory, avatarId, access);
const maxWidthPx = Math.max(1, baseWidth - (avatarId ? AVATAR_RESERVED_WIDTH : 0));
const originalPages = Array.isArray(event.text) ? [...event.text] : [event.text];
if (originalPages.length + pageSummaries.length > MAX_PAGES)
throw new GameStudioProjectError("DIALOGUE_LIMIT_EXCEEDED", `Dialogue analysis is limited to ${MAX_PAGES} pages`);
const explicitTransformation = input.wrap === "word" || input.paginate === true;
const analyzedPages = explicitTransformation ? transformPages(font, originalPages, maxWidthPx, visibleLines, input.wrap === "word", input.paginate === true) : originalPages;
if ((input.includeSuggestion || explicitTransformation) && events.length === 1) {
const suggestedPages = explicitTransformation ? analyzedPages : transformPages(font, originalPages, maxWidthPx, visibleLines, true, true);
const changed = suggestedPages.length !== originalPages.length || suggestedPages.some((page, index) => page !== originalPages[index]);
suggestion = {
text: Array.isArray(event.text) || suggestedPages.length !== 1 ? suggestedPages : suggestedPages[0],
pages: suggestedPages,
changed,
};
}
eventSummaries.push({
id: event.id,
command: event.command,
sceneId: event.sceneId,
...(event.actorId ? { actorId: event.actorId } : {}),
...(event.triggerId ? { triggerId: event.triggerId } : {}),
scriptKey: event.scriptKey,
pageCount: analyzedPages.length,
resourcePath: event.resourcePath,
});
for (let pageIndex = 0; pageIndex < analyzedPages.length; pageIndex += 1) {
const page = analyzedPages[pageIndex];
const lines = page.split("\n");
let maximumPageLinePx = 0;
if (lines.length > visibleLines) {
report({ severity: "error", code: "PAGE_OVERFLOW", message: `Page ${pageIndex + 1} has ${lines.length} visible lines; the selected profile allows ${visibleLines}`, ...(event.id ? { eventId: event.id } : {}), pageIndex, ...(event.resourcePath ? { resourcePath: event.resourcePath } : {}) });
}
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const measured = measureLine(font, lines[lineIndex]);
maximumPageLinePx = Math.max(maximumPageLinePx, measured.width);
maximumMeasuredLinePx = Math.max(maximumMeasuredLinePx, measured.width);
totalLines += 1;
if (measured.width > maxWidthPx) {
report({ severity: "error", code: "LINE_OVERFLOW", message: `Line ${lineIndex + 1} measures ${measured.width}px, exceeding the ${maxWidthPx}px dialogue box`, ...(event.id ? { eventId: event.id } : {}), pageIndex, lineIndex, measuredWidthPx: measured.width, maxWidthPx, ...(event.resourcePath ? { resourcePath: event.resourcePath } : {}) });
}
if (measured.tokens.length > 0) {
confidence = "uncertain";
report({ severity: "warning", code: "UNSUPPORTED_CONTROL_TOKEN", message: `Runtime interpolation or control-token width is not statically verified: ${measured.tokens.join(", ")}`, ...(event.id ? { eventId: event.id } : {}), pageIndex, lineIndex, ...(event.resourcePath ? { resourcePath: event.resourcePath } : {}) });
}
if (measured.missing.length > 0) {
confidence = "uncertain";
report({ severity: "warning", code: "GLYPH_NOT_FOUND", message: `The selected font does not map ${Array.from(new Set(measured.missing)).join(", ")}; an eight-pixel fallback was measured`, ...(event.id ? { eventId: event.id } : {}), pageIndex, lineIndex, ...(event.resourcePath ? { resourcePath: event.resourcePath } : {}) });
}
}
pageSummaries.push({
...(event.id ? { eventId: event.id } : {}),
command: event.command,
...(event.sceneId ? { sceneId: event.sceneId } : {}),
...(event.actorId ? { actorId: event.actorId } : {}),
...(event.triggerId ? { triggerId: event.triggerId } : {}),
pageIndex,
text: page,
lineCount: lines.length,
maximumMeasuredLinePx: maximumPageLinePx,
avatarPresent: Boolean(avatarId),
...(event.resourcePath ? { resourcePath: event.resourcePath } : {}),
});
}
}
const diagnosticCount = Object.values(diagnosticCounts).reduce((sum, count) => sum + count, 0);
const analysis = {
valid: (diagnosticCounts.LINE_OVERFLOW ?? 0) === 0 && (diagnosticCounts.PAGE_OVERFLOW ?? 0) === 0,
confidence,
font: { id: font.id, name: font.name, variableWidth: font.variableWidth },
layout: { screenWidth: SCREEN_WIDTH, screenHeight: SCREEN_HEIGHT, maxWidthPx: Math.max(1, baseWidth - (anyAvatar ? AVATAR_RESERVED_WIDTH : 0)), maxVisibleLines: visibleLines, avatarPresent: anyAvatar },
eventCount: events.length,
pageCount: pageSummaries.length,
lineCount: totalLines,
maximumMeasuredLinePx,
events: eventSummaries,
pages: pageSummaries,
diagnostics,
diagnosticCount,
diagnosticCounts,
...(suggestion ? { suggestion } : {}),
};
return { inventory, font, events, analysis };
}
function projectDialogueAnalysis(analysis, input) {
const requestedEventLimit = input.eventLimit;
const requestedPageLimit = input.pageLimit;
if (requestedEventLimit !== undefined && (!Number.isSafeInteger(requestedEventLimit) || requestedEventLimit < 1 || requestedEventLimit > MAX_EVENTS)) {
throw new GameStudioProjectError("INVALID_DIALOGUE_LAYOUT", `eventLimit must be an integer between 1 and ${MAX_EVENTS}`);
}
if (requestedPageLimit !== undefined && (!Number.isSafeInteger(requestedPageLimit) || requestedPageLimit < 1 || requestedPageLimit > MAX_PAGES)) {
throw new GameStudioProjectError("INVALID_DIALOGUE_LAYOUT", `pageLimit must be an integer between 1 and ${MAX_PAGES}`);
}
let eventLimit = requestedEventLimit ?? analysis.events.length;
let pageLimit = requestedPageLimit ?? analysis.pages.length;
let projected = {
...analysis,
events: analysis.events.slice(0, eventLimit),
pages: analysis.pages.slice(0, pageLimit),
};
let byteSize = Buffer.byteLength(JSON.stringify(projected));
if (byteSize > PROJECT_RESPONSE_LIMITS.semanticQueryBytes) {
eventLimit = Math.min(eventLimit, DEFAULT_PROJECTED_EVENTS);
pageLimit = Math.min(pageLimit, DEFAULT_PROJECTED_PAGES);
}
while (true) {
const truncated = eventLimit < analysis.eventCount || pageLimit < analysis.pageCount;
projected = {
...analysis,
events: analysis.events.slice(0, eventLimit),
pages: analysis.pages.slice(0, pageLimit),
...(truncated ? {
returnedEventCount: Math.min(eventLimit, analysis.eventCount),
returnedPageCount: Math.min(pageLimit, analysis.pageCount),
truncated: true,
} : {}),
};
byteSize = Buffer.byteLength(JSON.stringify(projected));
if (byteSize <= PROJECT_RESPONSE_LIMITS.semanticQueryBytes)
return projected;
if (pageLimit > 1)
pageLimit = Math.max(1, Math.floor(pageLimit / 2));
else if (eventLimit > 1)
eventLimit = Math.max(1, Math.floor(eventLimit / 2));
else {
throw new GameStudioProjectError("DIALOGUE_RESPONSE_TOO_LARGE", `A requested dialogue event or page exceeds the ${PROJECT_RESPONSE_LIMITS.semanticQueryBytes}-byte response limit; select a smaller event or page`);
}
}
}
export async function analyzeDialogue(projectPath, input = {}, access) {
const analysis = (await prepareDialogue(projectPath, input, access)).analysis;
return access || input.eventLimit !== undefined || input.pageLimit !== undefined
? projectDialogueAnalysis(analysis, input)
: analysis;
}
//# sourceMappingURL=dialogue.js.mapSHA-256: f10d37f911ced0dab31abf665d5f53984322e58b02d348eb1abea14739dd46cd