Skip to content
ArnoldSmith86 edited this page Jan 2, 2025 · 22 revisions

Node.js, NPM and nodemon

This project uses Node.js. Node.js acts as an HTTP server serving the client code to players. State changes are synchronized with each player through WebSocket connections.

The Node package manager (npm) is used to manage dependencies and start scripts.

It keeps its configuration in package.json. When calling npm install it looks at dependencies in the file and installs everything needed into the directory node_modules (which is not in version control).

The configuration file also contains scripts which tells npm what to do when you type npm start, npm test or any of the other commands.

The main command for starting the server in a development environment is npm run debug which sets NOCOMPRESS=1 and then starts the project with nodemon. NOCOMPRESS disables minification of client-side Javascript code so that the debugger in browsers (F12) are actually useful. nodemon watches for changes in the project files and automatically restarts the server if any were detected.

Downloading this repository and dealing with dependencies

Linux

This assumes a Debian based Linux. Find Node.js repositories for other Linux distributions at https://node.dev/node-binary. Consult your distribution's documentation for how to install the git and nodejs packages if your distribution does not use apt.

    curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -                         # adds a repository for Node.js v16
    sudo apt install -y git nodejs                                                          # installs the required software
    git clone --recurse-submodules https://github.com/ArnoldSmith86/virtualtabletop.git     # downloads everything in this repository
    cd virtualtabletop                                                                      # changes to the newly created directory
    npm install                                                                             # install all dependencies using npm

MacOS

Using brew:

    brew install node
    git clone --recurse-submodules https://github.com/ArnoldSmith86/virtualtabletop.git
    cd virtualtabletop
    npm install

Windows

  1. Install Git and Node.js.
  2. Open the command prompt and cd to a directory where you want the project to live.
  3. Do git clone --recurse-submodules https://github.com/ArnoldSmith86/virtualtabletop.git which downloads the project.
  4. Do cd virtualtabletop to get into the project directory git created.
  5. Do npm install so Node.js downloads all the dependencies of the project.

If you use "GitHub Desktop" you should follow these steps:

  1. Install GitHub Desktop and Node.js.
  2. Follow steps to setup the programs. Then open GitHub Desktop.
  3. Go to Current repository -> Add -> Clone a repository and select this one or your fork.
  4. Go to Repository -> Open in Command Prompt.
  5. Do npm install so Node.js downloads all the dependencies of the project.

Starting the server

Now you can start the server by typing:

    npm start

If that doesn't work, try:

    node server.mjs

This will serve the project at localhost:8272.

Debugging

To debug client-side code, first start the server so that it doesn't compress the code:

    npm run debug

On windows, enter SET NOCOMPRESS=1 in a terminal prior to starting the server with npm start.

In your browser, refresh to have it re-download the client code. Press F12 to show the developer tools.

In Chromium, select the Sources tab, then the select the file for the room. Press Ctrl-f to search for the code you want to set a breakpoint for an step through.

Testing

To run the unit tests (and get a coverage report):

    npm test

To run them continuously (on each save) while you develop:

    npm run test-cont

See also TestCafé.

Overview of Server Processing

The server is started by typing npm run debug. On windows, type SET NOCOMPRESS=1 in a terminal prior to starting the server with npm start.

Internally, npm calls nodejs server.mjs. server.mjs is the main entry point. Its main purpose is to start an HTTP server using Express.

The main file contains a few express.static calls for static files like game assets. They create a direct mapping from the HTTP server to files on the filesystem.

Several app.get, app.post and app.put calls register listeners to HTTP calls that in turn will serve game room states and similar helpers.

The main listener is app.get('/:id', ...) that catches any HTTP GET that was not handled by any of the special URLs. It serves a combined, minified and gzipped version of the room.html in the client directory that was created on application start by minify.mjs.

The HTTP server also acts as a WebSocket server. The client code initializes a WebSocket connection that gets handled in websocket.mjs and connection.mjs. It loads an instance of Player for the connected browser which in turn communicates its commands to the loaded Room.

Overview of Client Processing

The client currently consists only of room.html (see Server for its creation). It reads the room code from the URL, initializes a WebSocket connection in connection.js and initially receives the entire state of the room.

Creation and modification of widgets is codified into a delta, which is simply an object reflecting any new or changed properties of the widget. A delta is an object with one component, s, standing for state. This is itself a map whose keys are widget id's and whose values consist of an object with a collection of property name - property value pairs giving new values for the widget's state. Thus for example if delta.s is {a123: {x: 15}, b456: {parent: null}}, the value of the x property of widget a123 is changed to 15 and the parent attribute of b456 is deleted. If a123 or b456 does not exist, it is created. Note that an entry in delta.s of the form a123: null will remove a123.

Every widget created in the room is turned into an object using the function addWidget. The widget classes all inherit from Widget which contains many of the basic movement code and other general features. It in turn inherits from StateManaged which has the basic functions that handle widget property changes.

When a widget is first created, be it through initial room upload, editing, or gameplay, addWidget creates an object of the appropriate type, including the domElement itself, and adds it to the widgets map. It then calls applyInitialDelta, which gets the JSON and then calls applyDelta. applyDelta places the JSON into the state property of the object, following which it calls applyDeltaToDOM. applyDeltaToDOM makes sure that the z value from the JSON is properly set in the object, that the movable attribute is set if the JSON specifies draggable, and that the widget's (new) parent is informed, so that what is actually visible in the room conforms to the JSON. Note that most specific widget types have their own version of this function that takes care of widget-specific properties after calling the version in widget.js.

The use of applyInitialDelta described above is only for the purpose of initial widget creation; other widget changes cause applyDelta to be called directly. Any interaction from the player is triggered through mousehandling.js and calls moveStart, move, moveEnd and click in the widget objects.

These functions then evaluate game logic, and change properties using the function set which in turn triggers sendPropertyUpdate. All of these changes of one player interaction are collected in a delta that gets sent to the server in sendDelta. On the server it is applied to the current room state and also broadcast to all other players. The initiating player skips the server roundtrip and immediately calls receiveDelta with the delta it just sent. All other players call the same function when they receive the delta.

The function receiveDelta then calls applyDelta on all affected widgets, and the resulting calls to the applyDeltaToDOM functions in the widgets take care that changes from the deltas get applied to the actual DOM objects the player can see.

Conflict resolution

Conflict resolution is currently being done like this:

  1. The server (and clients) contain a state (the JSON of the room) and a deltaID for every room.
  2. Whenever the client sends a new delta (any changes to the state), it adds that it's based on its last known deltaID.
  3. If the server receives a delta from a player that is not based on the latest deltaID, it checks if any intermediate deltas change the same widget. If none does, the delta is accepted.
  4. If two deltas do contain changes for the same widget, the one that gets to the server second triggers a complete state refresh for that client. This might be noticeable by a white flicker while the complete room is recreated.

Consider this example. There is a button that adds one card to the player's hand. Two players press the button at almost the same time. Both players will see the card added to their hand, but since that delta must be resolved, only one of the players gets to keep the card following the conflict resolution process above. For the other player, the card will quickly disappear.

JSON editor

There are essentially three parts to the JSON editor. The first part, more or less at the top of the file, is the setup. This is called once the first time the editor is opened in a given room. The second part, in the middle (although it should probably be separated from the rest of the editor) is the logging code; this is delimited by comments. The third part is the code that runs when edits get made. And finally, at the end, there are a number of event handlers to dispatch events. There are of course overlaps between the first and third parts.

Dispatching for button presses in the command area is done in setup. For each possible button, there is an object containing a bunch of stuff. In particular, show defines a function that returns a boolean saying whether to show this button or not in the current context. For example, if you are inside a SELECT, and there is already a collection in that select, collection will not be shown in the command list. Also call defines the action routine to execute when the button is pressed.

The code keeps track of where edits are by using a context. If you think of a widget's JSON as a tree, the context corresponding to a given cursor position is the concatenation of the nodes in the tree leading to the cursor. This is probably the most important concept in the editor. The context is computed in jeGetContext. You may notice that many command definitions contain a context key; this defines the context in which that command can appear. When the cursor position changes, jeShowCommands is called, which compares the current context with each command definition. If the contexts don't match, that command will not appear. But even if the contexts do match, the command may not appear if show returns false.

Note that the pane containing the text of a widget actually contains two copies of that text. The one on top, with id #jeText, is invisible, monocolor, and editable - this is what you are actually editing when you change a widget. The one on the bottom, with id #jeTextHighlight, is colored. Keeping these two synchronized adds some complexity to the code.

Additionally, changes to the room from other clients are reflected when you are in the JSON editor. This is handled in jeApplyChanges.

Editing VTT Symbols Font

The VTT Symbols font files (.woff and .woff2 file types in the assets/fonts directory) contain a number of specialized symbols/glyphs created specifically for VTT. The custom glyphs start in position number 59648. You can view and edit them using a font editor such as FontForge (which is open source and free to use). To add new glyphs, create it using a graphics editor and/or a font editor. Add the glyph to the next open position in the font file. Then add the ligature (shorthand VTT uses to display the glyph) to the file using the font editor. (See Fonts for examples of the the types of ligatures currently in use). Finally, export the file as both a .woff and .woff2 and add those files to a pull request.

Editing Public Library

In the file config.json in the main directory, set "allowPublicLibraryEdits": true, After making this change, you will have the option to edit public library games, tutorials, and assets. When creating a new game, tutorial, or asset from your Game Shelf, you will have a new option to add game to public library.

Editing Custom CSS

In the client/css folder is an empty "custom.css" file. This exists to support a particular use case of someone running a custom server. If you want to run your own server and customize the css, you can add that in this file and it will be applied to your server. You could use custom fonts, colors, or logos; limit the visibility of parts of the user interface; and do almost anything else that css can do.

Clone this wiki locally