Files
@amxts/core/fs reads and writes files the way Node's fs does:
import * as fs from "@amxts/core/fs";
const rulesFile = `${server.configsDir}/myplugin/rules.txt`;
server.addCommand("/rules", ({ player }) => showRules(player));
function showRules(player: Player) {
const text = fs.readFileSync(rulesFile);
if (text == null) return; // no such file
for (const line of text.split("\n")) console.log(line);
fs.appendFileSync(`${server.dataDir}/myplugin-rules.log`, `${player.name} read the rules\n`);
}
Every function has a synchronous form and a promise form, for await:
| Synchronous | Promise | Does |
|---|---|---|
readFileSync(path) → string | null | readFile(path) → Promise<string> | reads the whole file as text |
writeFileSync(path, text) → boolean | writeFile(path, text) → Promise<void> | writes the file, replacing what was there |
appendFileSync(path, text) → boolean | appendFile(path, text) → Promise<void> | adds to the end, making the file if needed |
existsSync(path) → boolean | exists(path) → Promise<boolean> | a file or a folder is there |
readdirSync(path) → string[] | null | readdir(path) → Promise<string[]> | the names in a folder, without . and .. |
mkdirSync(path, options?) → boolean | mkdir(path, options?) → Promise<void> | makes a folder; { recursive: true } makes the missing folders above it too |
import * as fs from "@amxts/core/fs";
async function saveMap() {
await fs.mkdir(`${server.dataDir}/myplugin`, { recursive: true });
await fs.writeFile(`${server.dataDir}/myplugin/last-map.txt`, server.map);
}
fs.readFile("missing.txt").catch((error) => console.log(error.message));
// ENOENT: no such file or directory, open 'missing.txt'
Paths
A path is relative to the game folder, cstrike/, as for any AMX Mod X
plugin. server.configsDir is the configs folder (addons/amxmodx/configs)
and server.dataDir the data folder (addons/amxmodx/data), wherever the
server keeps them. Keep your plugin's files in a folder or under a name of
its own: configs/myplugin/, data/myplugin-stats.txt.
Text
Files are read and written as UTF-8 text. A file is read whole, whatever its size. A byte-order mark at the start of a file is kept, as in Node.
JSON
JSON reads and writes JSON text as in JavaScript. JSON.parse is told
what it reads - JSON.parse<Stats>(text), or a type the value goes to - and
builds that object, its arrays, Records and fields included:
import * as fs from "@amxts/core/fs";
interface Stats {
kills: number;
deaths: number;
lastMap?: string;
}
const statsFile = `${server.dataDir}/myplugin/stats.json`;
function loadStats(): Stats {
const text = fs.readFileSync(statsFile);
if (text == null) return { kills: 0, deaths: 0 };
return JSON.parse(text);
}
function saveStats(stats: Stats) {
fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2));
}
Text that is not JSON is a SyntaxError; a value of another kind than the
type says - text where a number goes, a field that must be there and is not -
is a TypeError that names the field. Catch them with try/catch.
JSON.stringify leaves out a field that is undefined, writes a Date as
its ISO text and an object with a toJSON() as what that gives.
For settings a server owner edits, a typed config of config-core reads YAML, JSON and INI alike.
When it fails
A synchronous function says it failed by its result: null from
readFileSync and readdirSync, false from writeFileSync,
appendFileSync and mkdirSync - a folder that does not exist, say. A
promise function rejects with an Error whose message starts with ENOENT,
as Node's do (mkdir's with EEXIST or ENOENT).
The work is done at once either way: a promise function has settled by the time it returns.
The operating system
import { EOL, platform } from "@amxts/core/os";
if (platform() == "linux") console.log("running on Linux");
const lines = ["first", "second"].join(EOL); // "\r\n" on Windows, "\n" on Linux
platform() is "win32" or "linux", and EOL the line ending of that
system.
In a test, the fake server keeps the game folder in memory:
server.writeFile(path, text) before the plugin reads, server.file(path)
after it writes (Testing).