Shared state and messaging
Module-level variables are the right default for state. They are simple, they are scoped, and a reload clears them, which is usually exactly what you want.
Sometimes it is not. Four globals cover the cases where state has to cross a boundary:
| Global | Scope | Survives reload | Use for |
|---|---|---|---|
StaticStorage |
Whole runtime, one flat namespace | Yes | A handful of values that must persist across reloads |
GlobalMap |
Whole runtime, named maps | Yes | The same, but namespaced |
GlobalNotification |
Whole runtime, named channels | Listeners are cleaned up | Decoupled messaging between scripts |
DataFile |
On disk | Yes | Configuration and data a human might edit |
StaticStorage
A single process-wide ConcurrentHashMap, unaffected by script reloads.
StaticStorage.set('economy.enabled', true);
const enabled = StaticStorage.get('economy.enabled');
// Get, or write and return the default if the key is absent
const cache = StaticStorage.getOrFillDefault('warp.cache', new HashMap());
getOrFillDefault is the useful one. It gives you a “create once, reuse across reloads” primitive:
// Survives reloads of this script, so players do not lose their cooldowns
const cooldowns = StaticStorage.getOrFillDefault('teleport.cooldowns', new HashMap());
Two things to be careful about:
- It is one flat namespace for the entire server. Prefix your keys with your feature name.
- Values you leave in it are never garbage collected. Do not park player objects or large caches there and forget about them.
GlobalMap
The same idea with namespacing built in. GlobalMap.getInstance(name) returns a named map, creating it on
first use.
const warps = GlobalMap.getInstance('warps');
warps.put('spawn', spawnLocation); // set() is an alias
const spawn = warps.get('spawn');
warps.remove('old-lobby');
Setting a key to null removes it.
Prefer GlobalMap over StaticStorage when you have more than a couple of values. The namespace stops two
features from colliding on a key name, and it makes intent obvious at the call site.
Both are backed by ConcurrentHashMap, so concurrent reads and writes are safe. Compound operations
(“read, decide, write”) are not atomic; see the concurrency model.
GlobalNotification
A lightweight publish/subscribe bus, useful for letting features react to each other without importing each other. Channels are identified by name.
const channel = GlobalNotification.getInstance('economy:balance-changed');
channel.on((message) => {
log(`Balance changed for ${message.uuid}: ${message.newBalance}`);
});
// A completely different script, no import between the two
const channel = GlobalNotification.getInstance('economy:balance-changed');
channel.send({ uuid: player.getUniqueId().toString(), newBalance: 500 });
Method aliases, so you can use whichever vocabulary you prefer:
| Subscribe | Publish |
|---|---|
registerListener(fn) |
send(message) |
subscribe(fn) |
publish(message) |
on(fn) |
notify(message) |
getInstance must be called during initial script evaluation. After a script finishes loading, the
extension locks and further getInstance calls throw. Grab your channel at the top level and keep it in a
const.
Listeners are unregistered automatically when the script unloads, so a reload does not leave stale subscribers behind.
This is the right tool when the alternative would be a circular import, or when several unrelated features need to hear about the same thing. It is the wrong tool for a direct call between two modules that already know about each other; just import the function.
DataFile
A wrapper over Bukkit’s YAML configuration API, for data you want on disk in a form a human can edit.
const warps = new DataFile('scripts/resources/warps.yml');
Paths are relative to plugins/PixelScript. The file and its parent directories are created if they do not
exist, and the contents are read into memory when you construct it.
| Method | Purpose |
|---|---|
getString(path) / setString(path, value) |
Strings |
getInt(path) / setInt(path, value) |
Integers |
getBoolean(path) / setBoolean(path, value) |
Booleans |
getStringList(path) |
A list of strings |
getLocation(path) / setLocation(path, location) |
A Location, stored as world/x/y/z/yaw/pitch |
getKeysInSection(path) |
The keys directly under a section, as a JS array |
hasSection(path) |
Whether a path is a section |
save() |
Write changes back to disk |
Paths are dot-separated: 'warps.spawn.name'.
Changes live in memory until you call save().
Worked example: loading a warp list
# plugins/PixelScript/scripts/resources/warps.yml
warps:
spawn:
name: Spawn
world: world
x: 0.5
y: 64.0
z: 0.5
yaw: 90.0
pitch: 0.0
hub:
name: Hub
world: world
x: 120.5
y: 70.0
z: -34.5
yaw: 0.0
pitch: 0.0
const WARP_FILE = new DataFile('scripts/resources/warps.yml');
const WARP_CACHE = new HashMap();
const warpIds = WARP_FILE.getKeysInSection('warps');
for (let i = 0; i < warpIds.length; i++) {
const warpId = warpIds[i];
const name = WARP_FILE.getString(`warps.${warpId}.name`);
WARP_CACHE.put(warpId, {
id: warpId,
name,
nameLower: name.toLowerCase(),
location: WARP_FILE.getLocation(`warps.${warpId}`),
});
}
export function getWarp(id) {
return WARP_CACHE.get(id);
}
Note the shape: read the file once at load time into an in-memory structure, then serve every lookup from
memory. DataFile reads are cheap but not free, and this pattern keeps disk access off your hot paths.
Because the file is re-read on every reload, editing the YAML and touching the script picks up the change.
DataFile versus the Sql module
DataFilefor configuration, static content, and anything a server admin should be able to open in an editor. Reloading a script re-reads it.Sqlfor per-player data, anything that grows without bound, and anything that has to be shared between servers.
DataFile writes are not concurrency-safe and rewrite the whole file, so do not use it as a database.