Entity Loaders
Ranvier does not lock you down to storing your data in a specific way. Each entity in the game can be customized to be loaded from a difference source. For example, the default Ranvier setup is to load areas from YAML files and account/player data from JSON files. Suppose you wanted to store your areas in a Sqlite database and your account/player data in a PostgreSQL database; that's where the Data and Entity Source system comes in.
Before data is loaded in Ranvier there are two pieces that fit together: a DataSource
and an EntityLoader
.
- a
DataSource
is a class which is used to actually connect to a source of data and retrieve records. Each configuredDataSource
is created once and shared among eachEntityLoader
configured to use it - an
EntityLoader
connects aDataSource
to a specific entity in the engine like NPCs, items, or accounts
First we'll cover how to configure a DataSource
and EntityLoader
, then we'll cover the creation of a new
DataSource
, finally how one uses an EntityLoader
in their code to, well, load entities.
Configuration¶
Configuration of DataSources and EntityLoaders is done in the ranvier.json
file in the root of the project. As
mentioned the default Ranvier setup is to load areas from YAML so we'll use that as our example:
{ // ... // the `dataSources` key is where we will register the sources available to use // for the entity loaders "dataSources": { // the key here names the DataSource for use in an EntityLoader "Yaml": { /* 'require' specifies which file or package to require. It will follow the same API as the node require() method, which is to say you could either have the class locally or it could be from a node module. If a node module exports more than one data source you may specify which export to use with <module>.<object> */ "require": "ranvier-datasource-file.YamlDataSource", /* A require from a local file may look like: "require": "./lib/path/to/MyDataSource.js" */ /* An arbitrary config passed to the DataSource constructor. Each DataSorce might have a different config: file paths, database details, etc. NOTE: If storing database credentials it's recommended you use the approach detailed in the next section to keep that information out of source control */ "config": { "bundlePath": "bundles", } } }, "entityLoaders": { /* The keys here will be a specific list of game entities which the BundleManager will use to load data. However, you may also add additional entities if you have a bundle with some custom data like a vendor's product list or loot tables */ "items": { /* specifies which registered DataSource to use */ "source": "Yaml", /* Additional configuration for the datasource specific to this entity. Each `DataSource` may have have a different entity config such as a table name or, in this case, a file name. Refer to the documentation for that DataSource */ "config": { "path": "items.yml", }, }, }, }
Sensitive Data¶
It is very bad practice to keep sensitive information like database hostnames, usernames, or passwords in source
control. As such it's a good idea to keep that information in local environment variables and read those in. To
facilitate this the ranvier.json
configuration can instead be read from a ranvier.conf.js
JS file to allow the use
of process.env
.
The only difference is that instead of a standard JSON file ranvier.conf.js
should module.exports
the configuration.
// reuse the configs in ranvier.json so we don't have to duplicate our settings const config = require('./ranvier.json'); // inject our environment variables into the config config.dataSources.MySql = { require: './lib/some/dataSource.js', config: { hostname: process.env.RANVIER_MYSQL_HOSTNAME, user: process.env.RANVIER_MYSQL_USER, password: process.env.RANVIER_MYSQL_PASSWORD, }, }; // export our updated config module.exports = config;
To make this easier you may want to use a tool like dotenv. Make sure you do
not store the .env
files in git otherwise you will defeat the purpose of using environment variables.
Required EntityLoaders¶
The engine requires that you define entity loaders for the following keys: accounts
, players
, areas
, npcs
,
items
, rooms
, quests
, and help
. You may use whichever DataSource
you like for each loader. However, the data
returned from the sources is expected to be of a certain type:
fetchAll()
fornpcs
,items
,rooms
, andquests
should return anArray
of objects. For the specific data in each object see the corresponding guide in the Building section of the documentation.fetchAll()
foraccounts
,players
, andhelp
should return anObject
, with each key representing their respective entity by id.
Creating a new DataSource¶
A DataSource
is simply a Javascript class that has specific methods; how you implement those methods is ultimately up
to you as long as the methods take the correct parameters and return the correct values from the methods.
class ExampleDataSource { /** * The constructor of the DataSource takes two parameters: * config: the value of 'config' from the `dataSources` configuration in * ranvier.json * * rootPath: A string representing the project root directory (the same * directory that contains ranvier.json) */ constructor(config = {}, rootPath) { this.config = config; this.root = rootPath; // this is not required, this is only for our example data source this._records = [ { id: 1, foo: "bar" }, ]; } /* The first parameter of each method from here on will be the config defined in the the 'entityLoaders' entry. For example: "entityLoaders": { "items": { "source": "Yaml", "config": { "path": "foo.yml" }, } } `config` would equal `{ path: "foo.yml" }` Each method also returns a `Promise` */ /** * This is the only required method of a DataSource, all others are optional * but not implementing them will obviously limit its funtionality. * * Returns whether or not there is data for a given config. In the case of the * YamlDataSource this would be whether or not the configured file exists * * @param {object} config * @return {Promise<boolean>} */ hasData(config = {}) { return Promise.resolve(true); } /** * Returns all entries for a given config. * @param {object} config * @return {Promise<any>} */ fetchAll(config = {}) { return Promise.resolve(this._records); } /** * Gets a specific record by id for a given config * @param {Object} config * @param {string} id * @return {Promise<any>} */ async fetch(config = {}, id) { const records = await this.fetchAll(config); if (!records.hasOwnProperty(id)) { throw new ReferenceError(`Record with id [${id}] not found.`); } return records[id]; } /** * Perform a full replace of all data for a given config. This is the write * version of fetchAll * @param {Object} config * @param {any} data * @return {Promise} */ replace(config = {}, data) { this._records = data; return Promise.resolve(); } /** * Update specific record. Write version of `fetch` * @param {Object} config * @param {string} id * @param {any} data * @return {Promise} */ update(config = {}, id, data) { this._records[id] = data; return Promise.resolve(); } }
As mentioned in the configuration section this class can either be local to your project or it can be part of a node module, however you want to distribute it is up to you.
For a real-life implementation see ranvier-datasource-file
Using an EntityLoader¶
Taking the example configuration from the start we have one EntityLoader
defined: items. To use an EntityLoader
you
retrieve it from the EntityLoaderRegistry
from state
:
const itemsLoader = state.EntityLoaderRegistry.get('items'); /* We don't have to pass a config to any of the EntityLoader methods because we already defined that in the ranvier.json file */ const haveItems = await itemsLoader.hasData(); if (!haveItems) { return; } const items = itemsLoader.fetchAll(); for (const item of items) { console.log(item); }
For a real life example of using a custom EntityLoader
for your bundle see the
lootable-npcs bundle.