const { LuaFactory } = require('wasmoon'); const sqlite = require('better-sqlite3'); const db = new sqlite('the_big_db.db', { verbose: console.log }); const factory = new LuaFactory(); let lua; async function makeLua() { lua = await factory.createEngine(); lua.global.set('executeVerb', executeVerb); lua.global.set('getAttribute', luaSafe(getAttribute)); lua.global.set('setAttribute', luaSafe(setAttribute)); lua.global.set('deleteAttribute', luaSafe(deleteAttribute)); lua.global.set('hasOwnAttribute', luaSafe(hasOwnAttribute)); lua.global.set('verifyObjectReference', luaSafe(verifyObjectReference)); lua.global.set('lookUpObject', luaSafe(lookUpObject)); lua.global.set('lookUpObjectAttributes', luaSafe(lookUpObjectAttributes)); lua.global.set('interpret', interpret); // let's get cheeky with it lua.global.set('console_log', console.log); } makeLua(); var sockets = new Map(); function interpret(context, player, command) { // interpret and execute a command entered by a player. // // context: the location the player has entered the command in from. // may be null if it's executing from a script? // player: the id of the player object who typed in the command. // may? be null if it's executing from a script? used for output. // so arguably // command: the full string typed in by the player console.log(context, player, command); const socket = sockets.get(player) const wordsAndQuotes = tokenizeQuotes(command.trim()); try { //for the moment we'll only allow statements of the form "go north" with implicit subject: the player const verb = findVerb(wordsAndQuotes[0], context, player); if (verb) { let prepositions = getAttribute(verb, "prepositions"); const wordsAndPreps = tokenizePrepositions(wordsAndQuotes, prepositions || []); let words = wordsAndPreps.filter((x) => typeof(x) == "string"); let preps = wordsAndPreps.filter((x) => typeof(x) == "object"); let prepMap = {}; for (const obj of preps) { prepMap[obj.preposition] = obj.contents; } const [first, second, third, ...rest] = words; return executeVerb(command, player, verb, prepMap, context, player, second, third, ...rest); } else { interpret(1,1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${player}`); } } catch (error) { executeVerb('', player, 2, {to: player}, null, `{"error": "error found: ${error}"}`); } /* // if the second word is a verb, but the first word is also a verb, what do we do? if (second in verbs) { // for now, assume that if the second word is a verb, it's the verb we want. executeVerb(player, verbs.get(second), first, third, ...rest); } else { // if the second word is not a verb, then the first word is the verb, and the player is the implied subject. executeVerb(player, verbs.get(first), player, second, third, ...rest) } */ } async function executeVerb(fullCommand, outputObject, verbId, prepositions, context, subject, object, ...rest) { if (verbId == 2) { // todo: make this more intelligently get the rright thing to send if (prepositions["to"]) { let destination = verifyObjectReference(prepositions["to"]) return sockets.get(destination)?.send(object); } return sockets .get(outputObject) ?.send(`{"message": "missing \"to\" clause trying to execute command: ${fullCommand}"}`); } const fullVerb = lookUpObject(verbId); if (!fullVerb) { return interpret(1, 1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${outputObject}`); } // generate a temp name for this verb, then define it and execute. const verbName = "verb" + Math.random().toString(36).substring(2); const body = fullVerb.description.replace(/ /g, " "); const verbDeclaration = ` function ${verbName} (fullCommand, outputObject, prepositionMap, context, subject, object, ...) ${body} end`; console.log("verb we're running:"); console.log(verbDeclaration); await lua.doString(verbDeclaration).catch((e) => { console.log("found an error heyyoyyoyoyoyo", e); let msg = {"error": "syntax error defining a verb!", "errorObject": e}; sockets.get(outputObject)?.send(JSON.stringify(msg)); }) let func = lua.global.get(verbName); if (typeof func === "function") { let returnValue; try { returnValue = lua.global.get(verbName)(fullCommand, outputObject, prepositions, context, subject, object, ...rest) } catch (error) { sockets.get(outputObject)?.send(JSON.stringify({"error": "error executing a verb!"})); } // maybe unset it so we dno't have an ever-growing set of functions cluttering up the space? //lua.global.set(verbName, null); return returnValue; } else { sockets.get(outputObject)?.send(JSON.stringify({"error": "error defining a verb!"})); } return null; } const objectQuery = db.prepare('select * from pages where number=? order by time desc'); function lookUpObject(number) { return objectQuery.get(number); } const attributeQuery = db.prepare(`select * from attributes where number=?`); function lookUpObjectAttributes(number) { return JSON.parse(attributeQuery.get(number).contents); } function findVerb(word, location, actor) { // returns the number of a verb which matches the requested word. // check for verbs on actor let verb = []; if (verifyObjectReference(word)) { return verifyObjectReference(word); } if (actor) { verb = findVerbOnObject(word, actor); if (verb) return verb; let actorContents = getAttribute(actor, "contents"); for (const obj of (actorContents || [])) { verb = findVerbOnObject(word, obj); if (verb) return verb; } } if (location) { const locationContents = getAttribute(location, "contents"); for (const obj of (locationContents || [])) { verb = findVerbOnObject(word, obj); if (verb) return verb; } verb = findVerbOnObject(word, location); if (verb) return verb; } return undefined; } function findVerbOnObject(word, object) { // check for verbs on a single object, and its chain of parents. return a matching verbId, or undefined if (!word) return undefined; let focus = object; while (focus) { const focusVerbs = getAttribute(focus, "verbs"); for (const verbId of (focusVerbs || [])) { const fullVerb = lookUpObject(verbId); if (!fullVerb) continue; // test our word against each verb in turn if (word.toLowerCase() == fullVerb.title.toLowerCase()) { return verbId; } } focus = getAttribute(focus, "parent"); } return undefined; } function findAllVerbsOnObject(object) { let verbs = []; let focus = object; while (focus) { verbs = concatWithoutDuplicates(verbs, getAttribute(focus, "verbs")); focus = getAttribute(focus, "parent"); } return verbs; } function findAllVerbsInArea(location, actor) { // returns all verb ids in an area or on an actor let verbs = findAllVerbsOnObject(actor); const actorContents = getAttribute(actor, "contents"); for (const obj of (actorContents || [])) { verbs = concatWithoutDuplicates(verbs, findAllVerbsOnObject(obj)); } if (location) { const locationContents = getAttribute(location, "contents"); for (const obj of (locationContents || [])) { verbs = concatWithoutDuplicates(verbs, findAllVerbsOnObject(obj)); } verbs = concatWithoutDuplicates(verbs, findAllVerbsOnObject(location)); } return verbs; } const pullAttribute = db.prepare(`select * from attributes where number=?`); function getAttribute(obj, attributeName) { if (!verifyObjectReference(obj)) return undefined; let attributeStore = pullAttribute.get(verifyObjectReference(obj)); if (!attributeStore || !attributeStore.contents) return undefined; let contents = JSON.parse(attributeStore.contents); if (contents.hasOwnProperty(attributeName)) { return contents[attributeName]; } if (contents.hasOwnProperty("parent")) { if (contents["parent"]) { return getAttribute(verifyObjectReference(contents["parent"]), attributeName); } } return undefined; } function hasOwnAttribute(obj, attributeName) { if (!verifyObjectReference(obj)) return undefined; const attributeStore = pullAttribute.get(verifyObjectReference(obj)); const contents = JSON.parse(attributeStore.contents); return contents.hasOwnProperty(attributeName); } const insertAttribute = db.prepare(`update or replace attributes set contents=:contents where number=:number`); function setAttribute(obj, attributeName, value) { if (!verifyObjectReference(obj)) return undefined; const attributeStore = pullAttribute.get(verifyObjectReference(obj)); const contents = JSON.parse(attributeStore.contents); if (isEmptyObject(value) && isArray(contents[attributes])) contents[attributeName] = []; else contents[attributeName] = value; // hacky fix so that an empty array can't be replaced with an empty object insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)}); } function isArray(obj) { return Object.prototype.toString.apply(value) === '[object Array]'; } function isEmptyObject(obj) { if (typeof obj !== "obj") return false; for (const prop in obj) { if (Object.hasOwn(obj, prop)) return true; } return true; } function deleteAttribute(obj, attributeName) { if (!verifyObjectReference(obj)) return undefined; const attributeStore = pullAttribute.get(verifyObjectReference(obj)); const contents = JSON.parse(attributeStore.contents); delete contents[attributeName]; insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)}); } function verifyObjectReference(obj) { try { if (typeof(obj) === "string") { return Number(obj.replace("#", "")); } else if (typeof(obj) === "number") { return obj; } else if (typeof(obj.number) == "number") { return obj.number; } return undefined; } catch (error) { throw new Error(`Tried to get an attribute from something which wasn't an object: ${obj}`); } } function concatWithoutDuplicates(a, b) { let c = [] a?.forEach((x) => { if (!c.includes(x)) c.push(x) }) b?.forEach((x) => { if (!c.includes(x)) c.push(x) }); return c; } function luaSafe(func) { return (...all) => { let value = func(...all); if (value == null) { return undefined; } return value; }; } function tokenizeQuotes(string) { let arr = string.split("'"); let out = []; let inQuotes = false; for (const bit of arr) { if (inQuotes) { out.push(bit); } else { if (bit != '') { out = out.concat(bit.trim().split(" ").filter((x) => x != '')); } } inQuotes = !inQuotes; } return out; } function tokenizePrepositions(sequence, prepositions) { let out = []; let current = {}; let inPreposition = false; for (const bit of sequence) { if (inPreposition) { current.contents = bit; out.push(current); current = {}; inPreposition = false; } else if (prepositions.includes(bit)) { current.preposition = bit; inPreposition = true; } else { out.push(bit); } } return out; } module.exports = { interpret, sockets, lookUpObject, getAttribute, setAttribute, deleteAttribute, hasOwnAttribute, findVerbOnObject, findAllVerbsOnObject, findAllVerbsInArea };