From c7f5646e6234a7c49f7d276e445bbe453ff7d6d8 Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 20 Apr 2026 20:07:48 -0400 Subject: [PATCH] feat: add GFF type schema definitions for visual editors --- packages/backend/src/gff/are.schema.ts | 106 ++++++++++++++++++ packages/backend/src/gff/dlg.schema.ts | 138 ++++++++++++++++++++++++ packages/backend/src/gff/schema.ts | 53 +++++++++ packages/backend/src/gff/utc.schema.ts | 144 +++++++++++++++++++++++++ packages/backend/src/gff/uti.schema.ts | 89 +++++++++++++++ packages/backend/src/gff/utm.schema.ts | 62 +++++++++++ packages/backend/src/gff/utp.schema.ts | 116 ++++++++++++++++++++ packages/backend/src/routes/editor.ts | 12 +++ 8 files changed, 720 insertions(+) create mode 100644 packages/backend/src/gff/are.schema.ts create mode 100644 packages/backend/src/gff/dlg.schema.ts create mode 100644 packages/backend/src/gff/schema.ts create mode 100644 packages/backend/src/gff/utc.schema.ts create mode 100644 packages/backend/src/gff/uti.schema.ts create mode 100644 packages/backend/src/gff/utm.schema.ts create mode 100644 packages/backend/src/gff/utp.schema.ts diff --git a/packages/backend/src/gff/are.schema.ts b/packages/backend/src/gff/are.schema.ts new file mode 100644 index 0000000..79620de --- /dev/null +++ b/packages/backend/src/gff/are.schema.ts @@ -0,0 +1,106 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "are", + displayName: "Area", + categories: ["Identity", "Dimensions", "Lighting"], + fields: [ + { + label: "Tag", + displayName: "Tag", + type: GffFieldType.CExoString, + category: "Identity", + }, + { + label: "ResRef", + displayName: "ResRef", + type: GffFieldType.ResRef, + category: "Identity", + }, + { + label: "Name", + displayName: "Name", + type: GffFieldType.CExoLocString, + category: "Identity", + }, + { + label: "Flags", + displayName: "Area Flags", + type: GffFieldType.Dword, + category: "Identity", + description: "Bitmask: bit 0 = Interior, bit 1 = Underground, bit 2 = Natural", + }, + { + label: "Width", + displayName: "Width", + type: GffFieldType.Int, + category: "Dimensions", + description: "Area width in tiles", + }, + { + label: "Height", + displayName: "Height", + type: GffFieldType.Int, + category: "Dimensions", + description: "Area height in tiles", + }, + { + label: "Tileset", + displayName: "Tileset", + type: GffFieldType.ResRef, + category: "Dimensions", + description: "Tileset resref from tilesets.2da", + }, + { + label: "SunAmbientColor", + displayName: "Sun Ambient Color", + type: GffFieldType.Dword, + category: "Lighting", + description: "Ambient light color during daytime (BGR integer)", + }, + { + label: "SunDiffuseColor", + displayName: "Sun Diffuse Color", + type: GffFieldType.Dword, + category: "Lighting", + description: "Directional light color during daytime (BGR integer)", + }, + { + label: "SunFogColor", + displayName: "Sun Fog Color", + type: GffFieldType.Dword, + category: "Lighting", + }, + { + label: "MoonAmbientColor", + displayName: "Moon Ambient Color", + type: GffFieldType.Dword, + category: "Lighting", + }, + { + label: "MoonDiffuseColor", + displayName: "Moon Diffuse Color", + type: GffFieldType.Dword, + category: "Lighting", + }, + { + label: "MoonFogColor", + displayName: "Moon Fog Color", + type: GffFieldType.Dword, + category: "Lighting", + }, + { + label: "IsNight", + displayName: "Is Night", + type: GffFieldType.Byte, + category: "Lighting", + }, + { + label: "SkyBox", + displayName: "Sky Box", + type: GffFieldType.Byte, + category: "Lighting", + description: "Skybox index from skyboxes.2da", + }, + ], +}; diff --git a/packages/backend/src/gff/dlg.schema.ts b/packages/backend/src/gff/dlg.schema.ts new file mode 100644 index 0000000..1f4abae --- /dev/null +++ b/packages/backend/src/gff/dlg.schema.ts @@ -0,0 +1,138 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "dlg", + displayName: "Dialog", + categories: ["Properties", "Entries", "Replies"], + fields: [ + { + label: "DelayEntry", + displayName: "Entry Delay", + type: GffFieldType.Dword, + category: "Properties", + description: "Delay in milliseconds before NPC lines appear", + }, + { + label: "DelayReply", + displayName: "Reply Delay", + type: GffFieldType.Dword, + category: "Properties", + description: "Delay in milliseconds before PC reply options appear", + }, + { + label: "PreventZoomIn", + displayName: "Prevent Zoom In", + type: GffFieldType.Byte, + category: "Properties", + description: "Disables camera zoom during conversation", + }, + { + label: "NumWords", + displayName: "Word Count", + type: GffFieldType.Dword, + category: "Properties", + editable: false, + }, + { + label: "EndConverAbort", + displayName: "On Abort Script", + type: GffFieldType.ResRef, + category: "Properties", + description: "Script fired when the player aborts the conversation", + }, + { + label: "EndConversation", + displayName: "On End Script", + type: GffFieldType.ResRef, + category: "Properties", + description: "Script fired when the conversation ends normally", + }, + { + label: "EntryList", + displayName: "NPC Entries", + type: GffFieldType.List, + category: "Entries", + description: "Lines spoken by NPCs", + fields: [ + { + label: "Text", + displayName: "Text", + type: GffFieldType.CExoLocString, + description: "The dialog text displayed to the player", + }, + { + label: "Speaker", + displayName: "Speaker", + type: GffFieldType.CExoString, + description: "Tag of the speaking creature (empty = conversation owner)", + }, + { + label: "Animation", + displayName: "Animation", + type: GffFieldType.Dword, + description: "Animation to play while speaking", + }, + { + label: "Sound", + displayName: "Sound", + type: GffFieldType.ResRef, + description: "Sound file to play with this line", + }, + { + label: "Script", + displayName: "Action Script", + type: GffFieldType.ResRef, + description: "Script fired when this entry is displayed", + }, + { + label: "Active", + displayName: "Condition Script", + type: GffFieldType.ResRef, + description: "Script that must return TRUE for this entry to appear", + }, + ], + }, + { + label: "ReplyList", + displayName: "PC Replies", + type: GffFieldType.List, + category: "Replies", + description: "Response options available to the player", + fields: [ + { + label: "Text", + displayName: "Text", + type: GffFieldType.CExoLocString, + description: "The reply text shown as a dialog option", + }, + { + label: "Speaker", + displayName: "Speaker", + type: GffFieldType.CExoString, + }, + { + label: "Animation", + displayName: "Animation", + type: GffFieldType.Dword, + }, + { + label: "Sound", + displayName: "Sound", + type: GffFieldType.ResRef, + }, + { + label: "Script", + displayName: "Action Script", + type: GffFieldType.ResRef, + description: "Script fired when this reply is selected", + }, + { + label: "Active", + displayName: "Condition Script", + type: GffFieldType.ResRef, + description: "Script that must return TRUE for this reply to appear", + }, + ], + }, + ], +}; diff --git a/packages/backend/src/gff/schema.ts b/packages/backend/src/gff/schema.ts new file mode 100644 index 0000000..5cef26b --- /dev/null +++ b/packages/backend/src/gff/schema.ts @@ -0,0 +1,53 @@ +export enum GffFieldType { + Byte = 0, + Char = 1, + Word = 2, + Short = 3, + Dword = 4, + Int = 5, + Dword64 = 6, + Int64 = 7, + Float = 8, + Double = 9, + CExoString = 10, + ResRef = 11, + CExoLocString = 12, + Void = 13, + Struct = 14, + List = 15, +} + +export interface GffFieldSchema { + label: string; + displayName: string; + type: GffFieldType; + description?: string; + category?: string; + editable?: boolean; + hidden?: boolean; + fields?: GffFieldSchema[]; +} + +export interface GffTypeSchema { + fileType: string; + displayName: string; + categories: string[]; + fields: GffFieldSchema[]; +} + +const schemaRegistry = new Map Promise>(); + +schemaRegistry.set("uti", () => import("./uti.schema.js").then((m) => m.schema)); +schemaRegistry.set("utc", () => import("./utc.schema.js").then((m) => m.schema)); +schemaRegistry.set("are", () => import("./are.schema.js").then((m) => m.schema)); +schemaRegistry.set("dlg", () => import("./dlg.schema.js").then((m) => m.schema)); +schemaRegistry.set("utp", () => import("./utp.schema.js").then((m) => m.schema)); +schemaRegistry.set("utm", () => import("./utm.schema.js").then((m) => m.schema)); + +export async function getSchema( + type: string, +): Promise { + const loader = schemaRegistry.get(type); + if (!loader) return undefined; + return loader(); +} diff --git a/packages/backend/src/gff/utc.schema.ts b/packages/backend/src/gff/utc.schema.ts new file mode 100644 index 0000000..90a57da --- /dev/null +++ b/packages/backend/src/gff/utc.schema.ts @@ -0,0 +1,144 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "utc", + displayName: "Creature", + categories: ["Identity", "Attributes", "Scripts", "Equipment", "AI"], + fields: [ + { + label: "Tag", + displayName: "Tag", + type: GffFieldType.CExoString, + category: "Identity", + }, + { + label: "TemplateResRef", + displayName: "Template ResRef", + type: GffFieldType.ResRef, + category: "Identity", + }, + { + label: "Appearance_Type", + displayName: "Appearance Type", + type: GffFieldType.Word, + category: "Identity", + description: "Row index into appearance.2da", + }, + { + label: "Race", + displayName: "Race", + type: GffFieldType.Byte, + category: "Attributes", + description: "Row index into racialtypes.2da", + }, + { + label: "Gender", + displayName: "Gender", + type: GffFieldType.Byte, + category: "Attributes", + description: "0 = Male, 1 = Female", + }, + { + label: "Str", + displayName: "Strength", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "Dex", + displayName: "Dexterity", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "Con", + displayName: "Constitution", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "Int", + displayName: "Intelligence", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "Wis", + displayName: "Wisdom", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "Cha", + displayName: "Charisma", + type: GffFieldType.Byte, + category: "Attributes", + }, + { + label: "FactionID", + displayName: "Faction ID", + type: GffFieldType.Word, + category: "AI", + description: "Determines NPC hostility and alliance behavior", + }, + { + label: "WalkRate", + displayName: "Walk Rate", + type: GffFieldType.Int, + category: "AI", + description: "Movement speed index (7 = default from appearance.2da)", + }, + { + label: "ChallengeRating", + displayName: "Challenge Rating", + type: GffFieldType.Float, + category: "AI", + }, + { + label: "ScriptHeartbeat", + displayName: "Heartbeat Script", + type: GffFieldType.ResRef, + category: "Scripts", + description: "Runs every 6 seconds", + }, + { + label: "ScriptOnDamaged", + displayName: "On Damaged Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + { + label: "ScriptDeath", + displayName: "On Death Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + { + label: "ScriptSpawn", + displayName: "On Spawn Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + { + label: "ClassList", + displayName: "Classes", + type: GffFieldType.List, + category: "Attributes", + description: "Character class levels", + }, + { + label: "Equip_ItemList", + displayName: "Equipped Items", + type: GffFieldType.List, + category: "Equipment", + description: "Items in equipment slots", + }, + { + label: "ItemList", + displayName: "Inventory", + type: GffFieldType.List, + category: "Equipment", + description: "Items carried in inventory", + }, + ], +}; diff --git a/packages/backend/src/gff/uti.schema.ts b/packages/backend/src/gff/uti.schema.ts new file mode 100644 index 0000000..121ad10 --- /dev/null +++ b/packages/backend/src/gff/uti.schema.ts @@ -0,0 +1,89 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "uti", + displayName: "Item", + categories: ["Identity", "Properties", "Appearance"], + fields: [ + { + label: "LocalizedName", + displayName: "Name", + type: GffFieldType.CExoLocString, + category: "Identity", + description: "Localized display name of the item", + }, + { + label: "Tag", + displayName: "Tag", + type: GffFieldType.CExoString, + category: "Identity", + }, + { + label: "TemplateResRef", + displayName: "Template ResRef", + type: GffFieldType.ResRef, + category: "Identity", + description: "Blueprint resource reference (max 16 chars)", + }, + { + label: "BaseItem", + displayName: "Base Item Type", + type: GffFieldType.Int, + category: "Identity", + description: "Row index into baseitems.2da", + }, + { + label: "StackSize", + displayName: "Stack Size", + type: GffFieldType.Word, + category: "Properties", + }, + { + label: "Cost", + displayName: "Cost", + type: GffFieldType.Dword, + category: "Properties", + description: "Gold piece value override (0 = use 2da calculation)", + }, + { + label: "Charges", + displayName: "Charges", + type: GffFieldType.Byte, + category: "Properties", + }, + { + label: "Identified", + displayName: "Identified", + type: GffFieldType.Byte, + category: "Properties", + description: "Whether the item starts identified", + }, + { + label: "Plot", + displayName: "Plot Item", + type: GffFieldType.Byte, + category: "Properties", + description: "Cannot be dropped or sold if set", + }, + { + label: "Stolen", + displayName: "Stolen", + type: GffFieldType.Byte, + category: "Properties", + }, + { + label: "Cursed", + displayName: "Cursed", + type: GffFieldType.Byte, + category: "Properties", + description: "Cannot be unequipped if set", + }, + { + label: "PropertiesList", + displayName: "Item Properties", + type: GffFieldType.List, + category: "Properties", + description: "Magical and special properties on this item", + }, + ], +}; diff --git a/packages/backend/src/gff/utm.schema.ts b/packages/backend/src/gff/utm.schema.ts new file mode 100644 index 0000000..4d83657 --- /dev/null +++ b/packages/backend/src/gff/utm.schema.ts @@ -0,0 +1,62 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "utm", + displayName: "Store", + categories: ["Identity", "Economy", "Inventory"], + fields: [ + { + label: "Tag", + displayName: "Tag", + type: GffFieldType.CExoString, + category: "Identity", + }, + { + label: "TemplateResRef", + displayName: "Template ResRef", + type: GffFieldType.ResRef, + category: "Identity", + }, + { + label: "MarkDown", + displayName: "Buy Price Adjustment", + type: GffFieldType.Int, + category: "Economy", + description: "Percentage adjustment when store buys from player (negative = pays less)", + }, + { + label: "MarkUp", + displayName: "Sell Price Adjustment", + type: GffFieldType.Int, + category: "Economy", + description: "Percentage adjustment when store sells to player (positive = charges more)", + }, + { + label: "BM_MarkDown", + displayName: "Black Market Buy Adjustment", + type: GffFieldType.Int, + category: "Economy", + }, + { + label: "StoreGold", + displayName: "Store Gold", + type: GffFieldType.Int, + category: "Economy", + description: "Gold available for buying from players (-1 = unlimited)", + }, + { + label: "BlackMarket", + displayName: "Black Market", + type: GffFieldType.Byte, + category: "Economy", + description: "Whether the store buys stolen goods", + }, + { + label: "StoreList", + displayName: "Store Inventory", + type: GffFieldType.List, + category: "Inventory", + description: "Categories and items available for sale", + }, + ], +}; diff --git a/packages/backend/src/gff/utp.schema.ts b/packages/backend/src/gff/utp.schema.ts new file mode 100644 index 0000000..0c04c77 --- /dev/null +++ b/packages/backend/src/gff/utp.schema.ts @@ -0,0 +1,116 @@ +import { GffFieldType, type GffTypeSchema } from "./schema.js"; + +export const schema: GffTypeSchema = { + fileType: "utp", + displayName: "Placeable", + categories: ["Identity", "Lock", "Trap", "Scripts"], + fields: [ + { + label: "Appearance", + displayName: "Appearance", + type: GffFieldType.Dword, + category: "Identity", + description: "Row index into placeables.2da", + }, + { + label: "Tag", + displayName: "Tag", + type: GffFieldType.CExoString, + category: "Identity", + }, + { + label: "TemplateResRef", + displayName: "Template ResRef", + type: GffFieldType.ResRef, + category: "Identity", + }, + { + label: "Faction", + displayName: "Faction", + type: GffFieldType.Dword, + category: "Identity", + }, + { + label: "Plot", + displayName: "Plot", + type: GffFieldType.Byte, + category: "Identity", + description: "Indestructible if set", + }, + { + label: "Useable", + displayName: "Useable", + type: GffFieldType.Byte, + category: "Identity", + description: "Whether the player can interact with this placeable", + }, + { + label: "HasInventory", + displayName: "Has Inventory", + type: GffFieldType.Byte, + category: "Identity", + }, + { + label: "Locked", + displayName: "Locked", + type: GffFieldType.Byte, + category: "Lock", + }, + { + label: "LockDC", + displayName: "Lock DC", + type: GffFieldType.Byte, + category: "Lock", + description: "Difficulty class to pick the lock", + }, + { + label: "OpenLockDC", + displayName: "Open Lock DC", + type: GffFieldType.Byte, + category: "Lock", + }, + { + label: "KeyName", + displayName: "Key Tag", + type: GffFieldType.CExoString, + category: "Lock", + description: "Tag of the key item that unlocks this", + }, + { + label: "KeyRequired", + displayName: "Key Required", + type: GffFieldType.Byte, + category: "Lock", + }, + { + label: "TrapDetectable", + displayName: "Trap Detectable", + type: GffFieldType.Byte, + category: "Trap", + }, + { + label: "TrapDisarmable", + displayName: "Trap Disarmable", + type: GffFieldType.Byte, + category: "Trap", + }, + { + label: "ScriptOnOpen", + displayName: "On Open Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + { + label: "ScriptOnClosed", + displayName: "On Closed Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + { + label: "ScriptOnUsed", + displayName: "On Used Script", + type: GffFieldType.ResRef, + category: "Scripts", + }, + ], +}; diff --git a/packages/backend/src/routes/editor.ts b/packages/backend/src/routes/editor.ts index d9926f3..cb298e5 100644 --- a/packages/backend/src/routes/editor.ts +++ b/packages/backend/src/routes/editor.ts @@ -9,6 +9,7 @@ import { import { lookupResref, getResrefCount } from "../nwscript/resref-index.js"; import { lookupTlk, getTlkCount } from "../nwscript/tlk-index.js"; import { get2DAFile, list2DAFiles, get2DARow } from "../nwscript/twoda-index.js"; +import { getSchema } from "../gff/schema.js"; const router = Router(); @@ -110,4 +111,15 @@ router.get("/2da/:name/:row", (req, res) => { res.json(data); }); +router.get("/gff-schema/:type", async (req, res) => { + try { + const schema = await getSchema(req.params.type); + if (!schema) return res.status(404).json({ error: "unknown GFF type" }); + res.json(schema); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Unknown error"; + res.status(500).json({ error: message }); + } +}); + export default router;