Project structure

Once a resource outgrows a single file, this layout keeps server code, client code and assets apart.

Suggested layout

resources/
  market/
    package.json
    server/
      main.js          server entry script
      prices.js        server module, loaded with require('./prices')
      config.json      server settings, never sent to players
    client/
      main.js          client entry script
      hud.js           client module
    shared/
      items.js         code that runs on both sides
    ui/
      shop.html        a page for Web.open
      shop.css
    stream/
      objects/kcds/market/stall.cgf   a model the game loads
    node_modules/      packages for server scripts (run npm install inside market/)

Its package.json:

{
  "name": "market",
  "version": "1.2.0",
  "dependencies": {
    "pg": "^8.13.0"
  },
  "kcds": {
    "serverScripts": ["server/main.js"],
    "clientScripts": ["client/main.js"],
    "sharedScripts": ["shared/items.js"],
    "files": ["client/**", "ui/**"],
    "exports": ["priceOf"]
  }
}

For files under stream/, see Sounds and game files.

Choosing what players download

Players receive clientScripts, sharedScripts, whatever files matches, and everything under stream/ and replace/. They never receive serverScripts, package.json or anything else.

Keep client and server in separate folders. Without a files list, each folder holding a client entry script is sent whole. With client.js next to package.json, that means every file in the resource except its server entry scripts.

A files list is exact. Patterns can use *, ?, **, [a-z] and {css,json}. Hidden files match only when a pattern names the dot.

Watch out for broad patterns. .env, .git and node_modules are not sent by default, but a pattern such as ** does pick up node_modules.

Downloaded files are kept and checked by hash, so unchanged files do not download again; see Client resources.

How the parts connect

FromToHow
any scriptits own modulerequire('./prices'), with .js, .cjs or .json, relative to the file
server scriptNode and npm packagesrequire('node:fs'), or require('pg') from the resource's node_modules
server scriptits own filesprocess.cwd() is the resource folder
clientserverkcds.core.Events.toServer(name, data); answer with player.emit (Events)
one resourceanotherexports and imports (Imports and exports), or Messages
web pageits client scriptwindow.sendToGame and kcds.ui.Web.on (Web views)
// server/main.js
const prices = require('./prices');
const settings = require('./config.json');

kcds.core.Events.on('resourceStart', (name) => {
  if (name === 'market') {
    console.info(`market opens with ${Object.keys(prices).length} prices`);
  }
});

Start order

  • On each side, sharedScripts run first, then that side's scripts, in the listed order.
  • Resources in requires start first. A dependent resource stops before the one it depends on. Use { "name": "economy", "optional": true } for one that may be missing.
  • resourceStart handlers can be async, and dependents start once they finish.

Saving data

Nothing held in memory survives a restart of the resource or the server, so anything important belongs in a file or database.

Save under the server's folder, because the resource's own folder may be replaced with a new copy:

// server/main.js
const fs = require('node:fs');
const path = require('node:path');

const ledgerFile = path.join(kcds.core.Runtime.serverDir, 'data', 'market', 'balances.json');
const balances = fs.existsSync(ledgerFile) ? JSON.parse(fs.readFileSync(ledgerFile, 'utf8')) : {};

kcds.core.Events.on('resourceStop', (name) => {
  if (name !== 'market') {
    return;
  }
  fs.mkdirSync(path.dirname(ledgerFile), { recursive: true });
  fs.writeFileSync(ledgerFile, JSON.stringify(balances));
});

Backups from KCDS-Admin copy the resources folder with its data, without node_modules; see Running a server.

Next steps