386 lines
11 KiB
JavaScript
386 lines
11 KiB
JavaScript
const { LuaFactory } = require('wasmoon');
|
|
|
|
const sqlite = require('better-sqlite3');
|
|
const db = new sqlite('the_big_db.db');
|
|
|
|
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 {
|
|
return interpret(1,1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${player}`);
|
|
}
|
|
} catch (error) {
|
|
return 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"])
|
|
sockets.get(destination)?.send(object);
|
|
return object;
|
|
}
|
|
const msg = JSON.stringify({"message": `missing "to" clause trying to execute command: ${fullCommand}`})
|
|
sockets.get(outputObject)?.send();
|
|
return msg;
|
|
}
|
|
|
|
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": "found that word but it wasn't 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 };
|