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
| From | To | How |
|---|---|---|
| any script | its own module | require('./prices'), with .js, .cjs or .json, relative to the file |
| server script | Node and npm packages | require('node:fs'), or require('pg') from the resource's node_modules |
| server script | its own files | process.cwd() is the resource folder |
| client | server | kcds.core.Events.toServer(name, data); answer with player.emit (Events) |
| one resource | another | exports and imports (Imports and exports), or Messages |
| web page | its client script | window.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,
sharedScriptsrun first, then that side's scripts, in the listed order. - Resources in
requiresstart first. A dependent resource stops before the one it depends on. Use{ "name": "economy", "optional": true }for one that may be missing. resourceStarthandlers 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
- TypeScript: the same layout with a build step
- Trying code quickly: a local server for experiments