-
-
Notifications
You must be signed in to change notification settings - Fork 32
Internals
- HTTP environment
- Downloading and setup
- Debugging
- Testing
- Overview of Server Processing
- Overview of Client Processing
- Conflict Resolution
- JSON editor internals
- Editing VTT Symbols Font
- Editing Public Library
- Editing Custom CSS
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.
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
Using brew:
brew install node
git clone --recurse-submodules https://github.com/ArnoldSmith86/virtualtabletop.git
cd virtualtabletop
npm install
- Install Git and Node.js.
- Open the command prompt and cd to a directory where you want the project to live.
- Do
git clone --recurse-submodules https://github.com/ArnoldSmith86/virtualtabletop.git
which downloads the project. - Do
cd virtualtabletop
to get into the project directory git created. - Do
npm install
so Node.js downloads all the dependencies of the project.
If you use "GitHub Desktop" you should follow these steps:
- Install GitHub Desktop and Node.js.
- Follow steps to setup the programs. Then open GitHub Desktop.
- Go to Current repository -> Add -> Clone a repository and select this one or your fork.
- Go to Repository -> Open in Command Prompt.
- Do
npm install
so Node.js downloads all the dependencies of the project.
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.
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.
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é.
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.
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 is currently being done like this:
- The server (and clients) contain a state (the JSON of the room) and a deltaID for every room.
- Whenever the client sends a new delta (any changes to the state), it adds that it's based on its last known deltaID.
- 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.
- 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.
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
.
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.
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.
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.
- Widgets
- Functions, automation, and routines
- Dynamic Expressions and using variables
- Math, string, array, color, JSON functions
- Cards and Decks
- Editing JSON
- Using CSS
- Fonts
- Publicly available games
- Tutorials
- Available icons, card backs, button styles, and other images
- Demonstration Features
- Useful Code Snippets