@@ -75,11 +86,16 @@ function Page({ editing, number, editid=null, linkClick=()=>{} }) {
editing ?
+ dangerouslySetInnerHTML={{__html: description.replace(/\n/g, "
")}} />
:
-
+ (
+ verb ?
+ ")}}>
+ :
+
+ )
)
:
"..."
diff --git a/server/interpreter.js b/server/interpreter.js
index 7f6e04ef..23d13d7b 100644
--- a/server/interpreter.js
+++ b/server/interpreter.js
@@ -1,6 +1,27 @@
+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);
+}
+
+makeLua();
+
var sockets = new Map();
function interpret(context, player, command) {
@@ -14,20 +35,32 @@ function interpret(context, player, command) {
// command: the full string typed in by the player
const socket = sockets.get(player)
- const words = command.trim().split(' ');
+ const wordsAndQuotes = tokenizeQuotes(command.trim());
try {
- socket?.send(`interpreting and executing the received command: ${command}`);
-
//for the moment we'll only allow statements of the form "go north" with implicit subject: the player
- const [first, second, third, ...rest] = words;
+
+ const verb = findVerb(wordsAndQuotes[0], context, player);
- const verb = findVerb(first, 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;
+ }
- console.log(`executing verb ${verb} towards ${player} on ${player} ${second} ${third}`);
- executeVerb(player, verb, player, second, third, ...rest)
+ const [first, second, third, ...rest] = words;
+
+ executeVerb(command, player, verb, prepMap, player, second, third, ...rest)
+ } else {
+ interpret(1,1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${player}`);
+ }
} catch (error) {
- socket?.send(`got an error! ${error.mesage}`);
+ interpret(1,1, `system_send_message '{"error": "error found: ${error}"}' to ${player}`);
}
/*
@@ -42,26 +75,88 @@ function interpret(context, player, command) {
*/
}
+async function executeVerb(fullCommand, outputObject, verbId, prepositions, subject, object, ...rest) {
+ if (verbId == 2) {
+ // todo: make this more intelligently get the rright thing to send
+ let theObject;
+ try {
+ theObject = verifyObjectReference(object);
+ } catch (error) {
+ theObject = verifyObjectReference(outputObject);
+ }
+ if (prepositions["to"]) {
+ let destination = verifyObjectReference(prepositions["to"])
+ return sockets.get(destination)?.send(object);
+ }
+ return sockets
+ .get(theObject)
+ ?.send(`full command: ${fullCommand}. verb id: ${verbId}. subject: ${subject} object: ${object}`);
+ }
+
+ 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, subject, object, ...)
+ ${body}
+ end`;
+ console.log("verb we're running:");
+ console.log(verbDeclaration);
+ await lua.doString(verbDeclaration);
+
+
+ let returnValue = await lua.global.get(verbName)(fullCommand, outputObject, prepositions, subject, object, ...rest);
+ // 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;
+}
+
+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 = findVerbOnObject(word, actor);
- if (verb) return verb;
+ let verb = [];
- const actorContents = getAttribute(actor, "contents");
- for (const obj of (actorContents || [])) {
- verb = findVerbOnObject(word, obj);
- if (verb) return verb;
+ if (verifyObjectReference(word)) {
+ return verifyObjectReference(word);
}
- const locationContents = getAttribute(location, "contents");
- for (const obj of (locationContents || [])) {
- verb = findVerbOnObject(word, obj);
+ 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;
+ }
}
- verb = findVerbOnObject(word, location);
- 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;
}
@@ -89,75 +184,65 @@ function findVerbOnObject(word, object) {
return undefined;
}
-function executeVerb(outputObject, verbId, subject, object, ...rest) {
- const fullVerb = lookUpObject(verbId);
- if (!fullVerb) {
- return sockets.get(outputObject)?.send(`verb not found`);
+function findAllVerbsOnObject(object) {
+ let verbs = [];
+ let focus = object;
+
+ while (focus) {
+ const newVerbs = (getAttribute(focus, "verbs") || []).filter((verb) => !(verb in verbs));
+ verbs = verbs.concat(newVerbs);
+
+ focus = getAttribute(focus, "parent");
}
- sockets.get(outputObject)?.send(`verb found as ${fullVerb.name}`);
- const func = fullVerb.fn;
- if (!func) {
- return sockets.get(outputObject)?.send(`verb was found but didn't have a fn attribute`);
+
+ 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));
}
- return fullVerb.fn(outputObject, subject, object, ...rest);
-}
-const objectQuery = db.prepare('select * from pages where number=? order by time desc');
-function lookUpObject(number) {
- return hardCodedObjects(number) || objectQuery.get(number);
-}
-
-const attributeQuery = db.prepare(`select * from attributes where number=?`);
-function lookUpObjectAttributes(number) {
- return JSON.parse(attributeQuery.get(number).contents);
-}
-
-
-function hardCodedObjects(id) {
- // return objectQuery.get(id);
- if (id == 29) return {
- title: "look",
- verb: true,
- description: "send description of direct object to subject's socket",
- fn: (logReceiver, subject, object, ...rest) => {
- sockets.get(logReceiver)?.send(`you looked around! subject ${subject} object: ${object} args: ${rest}`);
- console.log(`${subject} looked at ${object} with args ${rest}, and output was directed to ${logReceiver}`);
+ if (location) {
+ const locationContents = getAttribute(location, "contents");
+ for (const obj of (locationContents || [])) {
+ verbs = concatWithoutDuplicates(verbs, findAllVerbsOnObject(obj));
}
- }
- if (id == 31) return {
- title: "warp",
- verb: true,
- description: "this built-in function changes the player's location, as well as telling their client to move",
- fn: (logReceiver, subject, object, ...rest) => {
- const objectNum = verifyObjectReference(object);
- const subjectNum = verifyObjectReference(subject);
- setAttribute(subjectNum, "location", objectNum);
- sockets.get(logReceiver)?.send(`location change to: ${object}`);
- }
+ verbs = concatWithoutDuplicates(verbs, findAllVerbsOnObject(location));
}
+
+ return verbs;
}
-
-
-
const pullAttribute = db.prepare(`select * from attributes where number=?`);
function getAttribute(obj, attributeName) {
- const attributeStore = pullAttribute.get(verifyObjectReference(obj));
- const contents = JSON.parse(attributeStore.contents);
+ if (!verifyObjectReference(obj)) return undefined;
+
+ let attributeStore = pullAttribute.get(verifyObjectReference(obj));
+
+ let contents = JSON.parse(attributeStore.contents);
if (contents.hasOwnProperty(attributeName)) {
return contents[attributeName];
}
if (contents.hasOwnProperty("parent")) {
- return getAttribute(contents.parent, attributeName);
+ 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);
@@ -166,6 +251,8 @@ function hasOwnAttribute(obj, 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);
@@ -175,10 +262,12 @@ function setAttribute(obj, attributeName, value) {
}
function deleteAttribute(obj, attributeName) {
+ if (!verifyObjectReference(obj)) return undefined;
+
const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents);
- delete contents["attributeName"];
+ delete contents[attributeName];
insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)});
}
@@ -192,9 +281,68 @@ function verifyObjectReference(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");
+ throw new Error(`Tried to get an attribute from something which wasn't an object: ${obj}`);
}
}
-module.exports = { interpret, sockets, lookUpObject, getAttribute, setAttribute, deleteAttribute, hasOwnAttribute };
\ No newline at end of file
+function concatWithoutDuplicates(a, b) {
+ let b2 = b.filter((x) => !(x in a));
+ return a.concat(b2);
+}
+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 };
diff --git a/server/package-lock.json b/server/package-lock.json
index 55e812b9..30980742 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -24,7 +24,8 @@
"highlight.js": "^11.10.0",
"jsdom": "^25.0.0",
"nodemon": "^3.1.5",
- "showdown": "^2.1.0"
+ "showdown": "^2.1.0",
+ "wasmoon": "^1.16.0"
}
},
"node_modules/@libsql/client": {
@@ -172,6 +173,11 @@
"node": ">=10"
}
},
+ "node_modules/@types/emscripten": {
+ "version": "1.39.10",
+ "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz",
+ "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw=="
+ },
"node_modules/@types/node": {
"version": "22.7.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz",
@@ -2327,6 +2333,17 @@
"node": ">=18"
}
},
+ "node_modules/wasmoon": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/wasmoon/-/wasmoon-1.16.0.tgz",
+ "integrity": "sha512-FlRLb15WwAOz1A9OQDbf6oOKKSiefi5VK0ZRF2wgH9xk3o5SnU11tNPaOnQuAh1Ucr66cwwvVXaeVRaFdRBt5g==",
+ "dependencies": {
+ "@types/emscripten": "1.39.10"
+ },
+ "bin": {
+ "wasmoon": "bin/wasmoon"
+ }
+ },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
diff --git a/server/package.json b/server/package.json
index 5dcb0f60..9fbc1008 100644
--- a/server/package.json
+++ b/server/package.json
@@ -29,6 +29,7 @@
"highlight.js": "^11.10.0",
"jsdom": "^25.0.0",
"nodemon": "^3.1.5",
- "showdown": "^2.1.0"
+ "showdown": "^2.1.0",
+ "wasmoon": "^1.16.0"
}
}
diff --git a/server/routes/api.js b/server/routes/api.js
index 225b0c27..eee2426c 100644
--- a/server/routes/api.js
+++ b/server/routes/api.js
@@ -12,8 +12,8 @@ app.use(page_routes);
const user_routes = require('./users.js');
app.use(user_routes);
-const socket_routes = require('./sockets.js');
-app.use(socket_routes);
+const live_connection_routes = require('./live.js');
+app.use(live_connection_routes);
const graphQuery = db.prepare(`
select p.number, p.html, p.time
diff --git a/server/routes/live.js b/server/routes/live.js
new file mode 100644
index 00000000..85bd3e28
--- /dev/null
+++ b/server/routes/live.js
@@ -0,0 +1,47 @@
+const express = require('express');
+const app = express.Router();
+const expressWs = require('express-ws')(app);
+
+const sqlite = require('better-sqlite3');
+const db = new sqlite('the_big_db.db', { verbose: console.log });
+
+const { loginRequired } = require('../authStuff.js');
+
+const { interpret, sockets, lookUpObject,
+ getAttribute, setAttribute, hasOwnAttribute, deleteAttribute,
+ findAllVerbsInArea } = require('../interpreter.js');
+
+
+
+app.ws('/embody', (ws, req) => {
+ const character = req.session.characterId;
+ if (!character) {
+ ws.send(`user ${req.session.userId} does not have an associated character object`);
+ ws.close();
+ return;
+ }
+
+ sockets.set(character, ws);
+ console.log("sending location change, should get attribute for 30");
+ ws.send(`location change to: #${getAttribute(character, "location")}`);
+
+ ws.on('message', (msg) => {
+ const location = getAttribute(character, "location");
+
+ interpret(location, character, msg);
+ });
+
+ ws.on('close', () => sockets.delete(character));
+});
+
+app.get('/embody/verbs', loginRequired, (req, res) => {
+ const location = getAttribute(req.session.characterId, "location");
+ const verbs = findAllVerbsInArea(location, req.session.characterId);
+
+ const verbWords = Array.from(verbs.map((v) => lookUpObject(v).title));
+
+ res.status(200).json(verbWords);
+});
+
+
+module.exports = app;
\ No newline at end of file
diff --git a/server/routes/pages.js b/server/routes/pages.js
index 36eb7b0f..ac40e773 100644
--- a/server/routes/pages.js
+++ b/server/routes/pages.js
@@ -9,6 +9,9 @@ const { loginRequired } = require('../authStuff.js');
const showdown = require('showdown');
const converter = new showdown.Converter();
+const hljs = require('highlight.js/lib/core');
+hljs.registerLanguage('lua', require('highlight.js/lib/languages/lua'));
+
const pageListQuery = db.prepare(`
select p.number, p.title, p.time
from pages p
@@ -30,10 +33,13 @@ app.get('/pages', (req, res) => {
app.post('/page/new', loginRequired, (req, res) => {
try {
- const maxPage = db.prepare('select max(number) as maximum from pages').get()
- const newPageNumber = maxPage.maximum + 1;
- const newPage = db.prepare('insert into pages (number, title, description, author) values (?, ?, ?, ?) returning number')
+ let maxPage = db.prepare('select max(number) as maximum from pages').get()
+ let newPageNumber = maxPage.maximum + 1;
+ let newPage = db.prepare('insert into pages (number, title, description, author) values (?, ?, ?, ?) returning number')
.get(newPageNumber, "new page", "this page is new!", req.session.userId);
+
+ let newAttributes = db.prepare('insert into attributes (number, contents) values (?, ?)')
+ .run(newPageNumber, JSON.stringify({"parent": 3}));
res.status(200).json(newPageNumber);
} catch (error) {
res.status(500).json({"error": error});
@@ -53,17 +59,21 @@ app.get('/page/:number', (req, res) => {
app.post('/page/:number', loginRequired, (req, res) => {
try {
const html = converter.makeHtml(req.body.description);
- const changes = db.prepare('insert into pages (number, title, description, html, author) values (?, ?, ?, ?, ?)')
- .run(req.params.number, req.body.title, req.body.description, html, req.session.userId);
+ const lua = hljs.highlight(req.body.description, {language: 'lua'}).value;
+ console.log(lua);
+ const changes = db.prepare('insert into pages (number, title, description, html, lua, author, type) values (?, ?, ?, ?, ?, ?, ?)')
+ .run(req.params.number, req.body.title, req.body.description, html, lua, req.session.userId, req.body.type ? 1 : 0);
res.status(200).json(changes);
} catch (error) {
- res.status(500).json({"error": error});
+ console.log(error);
+ res.status(500).json({"error": error});
}
});
app.delete('/page/:number', loginRequired, (req, res) => {
try {
- const changes = db.prepare('delete from pages where number = ?').run(req.params.number);
+ db.prepare('delete from pages where number = ?').run(req.params.number);
+ db.prepare('delete from attributes where number = ?').run(req.params.number);
res.status(200).json({id: req.params.id});
} catch (error) {
res.status(500).json({"error": error});
diff --git a/server/routes/sockets.js b/server/routes/sockets.js
deleted file mode 100644
index 48325eb9..00000000
--- a/server/routes/sockets.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const express = require('express');
-const app = express.Router();
-const expressWs = require('express-ws')(app);
-
-const sqlite = require('better-sqlite3');
-const db = new sqlite('the_big_db.db', { verbose: console.log });
-
-const { loginRequired } = require('../authStuff.js');
-
-const { interpret, sockets, lookUpObject,
- getAttribute, setAttribute, hasOwnAttribute, deleteAttribute } = require('../interpreter.js');
-
-
-
-app.ws('/embody', (ws, req) => {
- // const { playerObject } = db.prepare('select playerObject from users where id=?').get(req.session.userId)
- const playerObjectId = 30; // mocked out for now!
- sockets.set(playerObjectId, ws);
- ws.send(`location change to: #${getAttribute(playerObjectId, "location")}`);
-
- ws.on('message', (msg) => {
- const location = lookUpObject(playerObjectId).location;
-
- interpret(location, playerObjectId, msg);
- });
-
- ws.on('close', () => sockets.delete(playerObjectId));
-});
-
-
-
-module.exports = app;
\ No newline at end of file
diff --git a/server/routes/users.js b/server/routes/users.js
index 853442e4..c7db5777 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -50,6 +50,8 @@ app.post('/login', async (req, res) => {
// set the session cookie and rreturn 200!
req.session.name = name;
req.session.userId = storedUser.id;
+ req.session.characterId = db.prepare('select character from users where id=?').get(req.session.userId)?.character;
+
console.log('setting req.session.name! : ', req.session);
return res.status(200).json({message: "successfully logged in!", id: storedUser.id, name: name});
});