diff --git a/.gitignore b/.gitignore index b33f5ae2..2af09908 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. the_big_db.db +dump*.json # dependencies node_modules diff --git a/client/src/embodied/Live.jsx b/client/src/embodied/Live.jsx index 50c69dda..6c83bfda 100644 --- a/client/src/embodied/Live.jsx +++ b/client/src/embodied/Live.jsx @@ -19,7 +19,7 @@ function Live({editing, ...props}) { const [ currentNumber, setCurrentNumber ] = useState(1); const [ connecting, setConnecting ] = useState(true); - const { sendMessage, lastMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { + const { sendMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { onClose: () => setConnecting(false) }, connecting); @@ -34,24 +34,35 @@ function Live({editing, ...props}) { } } }, [lastMessage]); - - function handleSendMessage() { - console.log("sending a message..."); - sendMessage("button got clicked"); - } + + // spread this return value on elements in order to make them navigate + const commandLinkClick = (e) => { + if (e.target.tagName != "A") { return; } + if (!e.target.href.includes(window.location.origin)) { return; } + e.preventDefault(); + + const localLink = e.target.href.replace(window.location.origin, ""); + const targetString = localLink.replace("/", ""); + const targetNumber = Number(targetString); + + if (targetNumber != 0 && targetNumber != NaN) { + const warpMessage = `warp #${targetString}`; + console.log(`clicked a link, executing "warp #${targetString}"`); + sendMessage(warpMessage); + return; + } + + navigate(e.target.href.replace(window.location.origin, "")); + }; return ( <> - + - diff --git a/client/src/page/GhostPage.jsx b/client/src/page/GhostPage.jsx index 12603624..dd36a1b1 100644 --- a/client/src/page/GhostPage.jsx +++ b/client/src/page/GhostPage.jsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; -import { useFixLinks } from '../clientStuff.jsx'; import { useLoggedIn } from '../AuthProvider.jsx'; import Page from './Page.jsx'; @@ -10,10 +9,21 @@ import './Pages.css'; function GhostPage({editing, ...props}) { const { pagenumber, editid } = useParams(); + const navigate = useNavigate(); - return ( - - ); + const linkClick = (e) => { + if (e.target.tagName != "A") { return; } + if (!e.target.href.includes(window.location.origin)) { return; } + e.preventDefault(); + navigate(e.target.href.replace(window.location.origin, "")); + }; + + return ; } export default GhostPage; \ No newline at end of file diff --git a/client/src/page/Page.jsx b/client/src/page/Page.jsx index 4370eac0..9fa58966 100644 --- a/client/src/page/Page.jsx +++ b/client/src/page/Page.jsx @@ -6,11 +6,10 @@ import { useLoggedIn } from '../AuthProvider.jsx'; import './Pages.css'; -function Page({ editing, number, editid=null}) { +function Page({ editing, number, editid=null, linkClick=()=>{} }) { const queryClient = useQueryClient(); const navigate = useNavigate(); const loggedIn = useLoggedIn(); - const noLoad = useFixLinks(); const fetchQuery = useQuery({ // fetch the currrent values queryKey: ['page', number, editid], @@ -61,7 +60,7 @@ function Page({ editing, number, editid=null}) {

- 🌳 + 🌳 {number}.  + onClick={linkClick} /> ) : "..." diff --git a/server/db_scripts/dump_24-10-10.js b/server/db_scripts/dump_24-10-10.js new file mode 100644 index 00000000..149d449c --- /dev/null +++ b/server/db_scripts/dump_24-10-10.js @@ -0,0 +1,11 @@ +const sqlite = require("better-sqlite3"); +const db = new sqlite('the_big_db.db', { verbose: console.log }); + +const fs = require('node:fs'); + +const dump = { + pages: db.prepare('select * from pages').all(), + users: db.prepare('select * from users').all() +}; + +fs.writeFile(`dump_${new Date().toISOString()}.json`, JSON.stringify(dump), (err) => console.log(err) ); \ No newline at end of file diff --git a/server/db_scripts/initialize_db-24-10-10.1.js b/server/db_scripts/initialize_db-24-10-10.1.js new file mode 100644 index 00000000..9be4ca9f --- /dev/null +++ b/server/db_scripts/initialize_db-24-10-10.1.js @@ -0,0 +1,187 @@ +// wipe and load the database from a dump file created by dump_blahblah.js +const hljs = require('highlight.js/lib/core'); +hljs.registerLanguage('lua', require('highlight.js/lib/languages/lua')); + +const fs = require('node:fs'); +const sqlite = require("better-sqlite3"); +const showdown = require("showdown"); +const db = new sqlite('the_big_db.db', { verbose: console.log }); + +const converter = new showdown.Converter(); + +const dump_file_name = process.argv[2] +//get argument out of 'node db_scripts/initialize_db-blah-blah.js dumpfile' + +let data = "{}"; +try { + data = fs.readFileSync(dump_file_name, 'utf8'); +} catch (error) { + console.log(error); + return; +} +//open that file as text (unicode i guess?) +console.log("opened file"); +const filejson = JSON.parse(data); +//parse it as json + + +const pages = filejson["pages"]; + +const createPages = db.prepare(` +create table if not exists pages ( + id integer primary key, + number integer, + + title varchar(255), + + description text, + html text, + lua text, + + time timestamp default current_timestamp, + author integer, + + type integer +) +`); +/* an object has: + id = primary key + number = object identifier number + + title / NAME = a short display name for an object, or the title of a page + description / TEXT? = the markdown description of an object, or the lua script of the source code of a verb + + html / HTML_MARKDOWN? = the markdown for that object, rendered into html for display as a page + HTML_LUA? = the lua source code of the object, rendered into html with syntax highlighting? + + time = when this edit was saved + author = the user id of whoever authored this revision + + TYPE = whether this represents an object or a verb +*/ + +function migratePages(pages) { + console.log("moving old table to a temporary"); + console.log(db.prepare(`alter table pages rename to old_pages`).run()) + + console.log("creating new page table") + console.log(createPages.run()); + + console.log('iterating over dump pages'); + + const insertPage = db.prepare(`insert into pages + (id, number, title, description, html, lua, time, author, type) + values (:id, :number, :title, :description, :html, :lua, :time, :author, :type)`); + + pages.forEach((pageData) => { + let {id, number, description} = pageData; + console.log(`rendering page number ${number}.${id}`) + description = description.replace(/
/g, "\n"); + + const renderedPage = converter.makeHtml(description); + + const highlightedLua = hljs.highlight(description, {language: 'lua'}).value; + + args = { ...pageData, description: description, html: renderedPage, lua: highlightedLua, type: "noun"}; + + console.log("inserting args", args); + insertPage.run(args); + }); + + console.log("getting rid of old table"); + console.log(db.prepare("drop table old_pages").run()); + + //throw new Error("blahhh i'm a dracula"); +} +try { + db.transaction(migratePages)(filejson["pages"]); +} catch (error) { + console.log(error); +} + +const createAttributesQuery = db.prepare(` +create table if not exists attributes ( + number integer unique, + contents json +) +`); +/* an attribute store has: + number = the object number it's attached to + contents = a `text` object holding a json store of attributes defined on object #number, containing: + LOCATION? = the object which contains this object? should this be in the DB or in the json store? + PROTOTYPE = the number of the prototype for this object, to which we should look for attributes not found here + ATTRIBUTES? = json object of attached, script-controlled attributes. + + CONTENTS = json array of objectss contained inside this one + VERBS = json array of objecsts which are verbs on this one +*/ +function createAttributes() { + console.log("creating new attribute table") + console.log(createAttributesQuery.run()); + + console.log('iterating over all pages'); + + const insertAttribute = db.prepare(`insert into attributes + (number, contents) values (:number, :contents)`); + + db.prepare(`select * from pages`).all().forEach((pageData) => { + let { number } = pageData; + + let args = {contents: JSON.stringify({}), number: number}; + + console.log("inserting args", args); + insertAttribute.run(args); + }); + + //throw new Error("blahhh i'm a dracula"); +} + +try { + db.transaction(createAttributes)(); +} catch (error) { + console.log(error); +} + + + +const createUsers = db.prepare(` +create table if not exists users ( + id integer primary key, + + name varchar(64) unique, + password varchar(128), + + character integer +) +`); +/* a user has: + id = primary key + name = name string + password = argon2 hash of their password + + character = object in the game world representing your character +*/ + + + +function migrateUsers(users) { + console.log("moving old users"); + console.log(db.prepare("alter table users rename to old_users").run()); + + console.log("creating new users table"); + console.log(createUsers.run()); + + const insertUser = db.prepare("insert into users (name, password) values (:name, :password)") + users.forEach((user) => { + console.log(insertUser.run(user)); + }); + + console.log("clearing old table"); + console.log(db.prepare("drop table old_users").run()); + //throw new Error("i'm dracula 2"); +} +try { + db.transaction(migrateUsers)(filejson["users"]); +} catch (error) { + console.log(error); +} \ No newline at end of file diff --git a/server/db_scripts/initialize_db.js b/server/db_scripts/initialize_db.js index c0f3ca73..9f879d9c 100644 --- a/server/db_scripts/initialize_db.js +++ b/server/db_scripts/initialize_db.js @@ -22,6 +22,34 @@ create table if not exists pages ( author integer ) `); +/* an object has: + id = primary key + number = object identifier number + + title / NAME = a short display name for an object, or the title of a page + description / TEXT? = the markdown description of an object, or the lua script of the source code of a verb + + html / HTML_MARKDOWN? = the markdown for that object, rendered into html for display as a page + HTML_LUA? = the lua source code of the object, rendered into html with syntax highlighting? + + time = when this edit was saved + author = the user id of whoever authored this revision + + TYPE = whether this represents an object or a verb + +*/ + +/* an attribute store has: + number = the object number it's attached to + contents = a `text` object holding a json store of attributes defined on object #number, containing: + LOCATION? = the object which contains this object? should this be in the DB or in the json store? + PROTOTYPE = the number of the prototype for this object, to which we should look for attributes not found here + ATTRIBUTES? = json object of attached, script-controlled attributes. + + CONTENTS = json array of objectss contained inside this one + VERBS = json array of objecsts which are verbs on this one +*/ + const createUsers = db.prepare(` create table if not exists users ( @@ -31,7 +59,13 @@ create table if not exists users ( password varchar(128) ) `); +/* a user has: + id = primary key + name = name string + password = argon2 hash of their password + character = object in the game world representing your character +*/ function migratePages() { console.log("moving old table to a temporary"); diff --git a/server/interpreter.js b/server/interpreter.js index 69c64abf..7f6e04ef 100644 --- a/server/interpreter.js +++ b/server/interpreter.js @@ -10,19 +10,27 @@ function interpret(context, player, command) { // 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 const socket = sockets.get(player) const words = command.trim().split(' '); - socket?.send(`interpreting and executing the received command: ${command}`); + 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 verbs = findAvailableVerbs(context, player, command); - socket?.send(`found these verbs: ${Array.from(verbs.keys()).join(', ')}`) - - // first word is either a subject or a verb. either way there must be a verb. - const [first, second, third, ...rest] = words; + const verb = findVerb(first, context, player); + + console.log(`executing verb ${verb} towards ${player} on ${player} ${second} ${third}`); + executeVerb(player, verb, player, second, third, ...rest) + } catch (error) { + socket?.send(`got an error! ${error.mesage}`); + } + /* // 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. @@ -31,16 +39,54 @@ function interpret(context, player, command) { // 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) } + */ } -function findAvailableVerbs(location, actor, command) { +function findVerb(word, location, actor) { + // returns the number of a verb which matches the requested word. // check for verbs on actor - // check for verbs on objects in location - // check for verbs on location - const out = new Map(); - out.set("look", 29); - out.set("warp", 31); - return out; + let verb = findVerbOnObject(word, actor); + if (verb) return verb; + + const actorContents = getAttribute(actor, "contents"); + for (const obj of (actorContents || [])) { + verb = findVerbOnObject(word, obj); + if (verb) return verb; + } + + 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 + if (word.toLowerCase() == fullVerb.title.toLowerCase()) { + return verbId; + } + } + focus = getAttribute(focus, "parent"); + } + + return undefined; } function executeVerb(outputObject, verbId, subject, object, ...rest) { @@ -56,28 +102,99 @@ function executeVerb(outputObject, verbId, subject, object, ...rest) { return fullVerb.fn(outputObject, subject, object, ...rest); } -const objectQuery = db.prepare('select * from pages where id=?'); -function lookUpObject(id) { +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 == 30) return {name: "shoofle", contents: "this is a shoofle", location: 1}; if (id == 29) return { - name: "look", - verb: false, - contents: "send description of direct object to subject's socket", + 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 (id == 31) return { - name: "warp", + title: "warp", verb: true, - contents: "this built-in function changes the player's location, as well as telling their client to move", + description: "this built-in function changes the player's location, as well as telling their client to move", fn: (logReceiver, subject, object, ...rest) => { - // TODO: change subject's location + const objectNum = verifyObjectReference(object); + const subjectNum = verifyObjectReference(subject); + setAttribute(subjectNum, "location", objectNum); + sockets.get(logReceiver)?.send(`location change to: ${object}`); } } } -module.exports = { interpret, sockets, lookUpObject }; \ No newline at end of file + + + +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 (contents.hasOwnProperty(attributeName)) { + return contents[attributeName]; + } + + if (contents.hasOwnProperty("parent")) { + return getAttribute(contents.parent, attributeName); + } + + return undefined; +} + +function hasOwnAttribute(obj, attributeName) { + 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) { + const attributeStore = pullAttribute.get(verifyObjectReference(obj)); + const contents = JSON.parse(attributeStore.contents); + + contents[attributeName] = value; + + insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)}); +} + +function deleteAttribute(obj, attributeName) { + 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; + } + } catch (error) { + throw new Error("Tried to get an attribute from something which wasn't an object"); + } +} + +module.exports = { interpret, sockets, lookUpObject, getAttribute, setAttribute, deleteAttribute, hasOwnAttribute }; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index d02dd7af..55e812b9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,6 +21,7 @@ "graphology-layout": "^0.6.1", "graphology-layout-force": "^0.2.4", "graphology-layout-forceatlas2": "^0.10.1", + "highlight.js": "^11.10.0", "jsdom": "^25.0.0", "nodemon": "^3.1.5", "showdown": "^2.1.0" @@ -1128,6 +1129,14 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", diff --git a/server/package.json b/server/package.json index 8a3139fd..5dcb0f60 100644 --- a/server/package.json +++ b/server/package.json @@ -26,6 +26,7 @@ "graphology-layout": "^0.6.1", "graphology-layout-force": "^0.2.4", "graphology-layout-forceatlas2": "^0.10.1", + "highlight.js": "^11.10.0", "jsdom": "^25.0.0", "nodemon": "^3.1.5", "showdown": "^2.1.0" diff --git a/server/routes/sockets.js b/server/routes/sockets.js index 78a852c7..48325eb9 100644 --- a/server/routes/sockets.js +++ b/server/routes/sockets.js @@ -7,7 +7,8 @@ const db = new sqlite('the_big_db.db', { verbose: console.log }); const { loginRequired } = require('../authStuff.js'); -const { interpret, sockets, lookUpObject } = require('../interpreter.js'); +const { interpret, sockets, lookUpObject, + getAttribute, setAttribute, hasOwnAttribute, deleteAttribute } = require('../interpreter.js'); @@ -15,6 +16,7 @@ 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;