Files and the web
Lesson 10.3
Your mod can keep its own files, and with the player’s permission, ask a website for data. Both stay inside the sandbox: Permissions explained the rules.
Your folder
Your mod lives in mods/<your mod>/. You may read every file in it and write only in its data/ folder. Paths use / and start inside your folder: "data/scores.txt" is mods/<your mod>/data/scores.txt (during clight run: the project’s build/run/<your mod>/data/).
The engine refuses .., absolute paths (C:/...), writes outside data/ and files that could run as programs (.exe, .bat, .js, …) with CL_E_PERMISSION_DENIED. data/ holds 256 MiB.
Reading and writing
cl_fs_write_text(CL_STR("data/hello.txt"), CL_STR("Hello from my mod!"));
char text[256];
size_t n = 0;
if (cl_fs_read_text(CL_STR("data/hello.txt"), text, sizeof text, &n) == CL_OK)
cl_log(CL_LOG_INFO, (cl_str){text, n});
cl_fs_append_text(CL_STR("data/log.txt"), CL_STR("a player joined\n"));
cl_fs_read_text is a two-call function like the other text getters: when the buffer is too small it returns CL_E_BUFFER_TOO_SMALL and sets n to the size you need (The easy header shows the pattern). Writes are atomic: the game writes a new file and then swaps it in, so a crash never leaves half a file.
Folders and file facts:
cl_fs_mkdir(CL_STR("data/players"));
cl_fs_info info;
if (cl_fs_stat(CL_STR("data/log.txt"), &info) == CL_OK && info.kind == CL_FS_FILE)
CL_LOGF(CL_LOG_INFO, "log.txt has %llu bytes", (unsigned long long)info.size);
char names[1024];
cl_fs_list(CL_STR("data"), names, sizeof names, &n); /* one name per line, folders end with '/' */
cl_fs_rename(CL_STR("data/log.txt"), CL_STR("data/old_log.txt"));
cl_fs_remove(CL_STR("data/players"), true); /* true: with everything inside */
The C library’s own file functions work too, on the same files: your folder is mounted read-only at / and data/ read/write at /data, so fopen("/data/hello.txt", "r") opens what cl_fs_write_text("data/hello.txt", ...) wrote.
Asking a website
List the website in the manifest; the player allows it on your mod’s Permissions screen:
"permissions": ["net.http:api.ruby-tools.example"]
A request runs in the background. Your callback runs at the next tick, on the side that asked (the server tick for a server mod):
static void on_prices(cl_http_req req, int32_t status, cl_buf body, void* user) {
(void)user;
if (status != 200) {
char why[128];
size_t n = 0;
cl_http_error(req, why, sizeof why, &n);
CL_LOGF(CL_LOG_WARN, "no prices: %d %.*s", status, (int)n, why);
return;
}
char text[512];
size_t got = 0;
cl_buf_read_bytes(body, text, sizeof text - 1, &got);
text[got] = 0;
CL_LOGF(CL_LOG_INFO, "prices: %s", text);
}
static void ask(void) {
cl_http_req req;
cl_result r = cl_http_request(CL_STR("https://api.ruby-tools.example/v1/prices"), NULL, on_prices, NULL, &req);
if (r == CL_E_PERMISSION_DENIED) cl_log(CL_LOG_WARN, CL_STR("the player did not allow the connection"));
}
status is 0 when no answer came; cl_http_error says why (timeout, cancelled, not allowed: <host>, …). body and req are valid only during the callback. For a POST, fill a cl_http_opts:
cl_http_opts o = CL_HTTP_OPTS_INIT;
o.method = CL_HTTP_POST;
o.body = CL_STR("{\"player\":\"Alex\",\"score\":12}");
o.content_type = CL_STR("application/json");
cl_http_request(CL_STR("https://api.ruby-tools.example/v1/scores"), &o, on_saved, NULL, &req);
o.save_to = CL_STR("data/pack.zip") downloads a file straight into data/ (the callback then gets an empty body). Only https:// works (plain http:// only to localhost), a request takes at most a minute, and an answer at most 64 MiB.
Asking for permission later
Websites your mod can live without go into optional_permissions. Check with cl_cap_granted and ask with cl_cap_request when the player uses the feature (on the client: it opens your mod’s Permissions screen and returns CL_E_BUSY); the answer fires mod.permission_changed.
if (!cl_cap_granted(CL_STR("net.http:api.ruby-tools.example")))
cl_cap_request(CL_STR("net.http:api.ruby-tools.example"));
Try it
Count how often the server started in data/starts.txt: read the number (0 when the file is missing), add one, write it back, and log it.
Show a solution
static void on_started(void* payload, void* user) {
(void)payload; (void)user;
char text[32];
size_t n = 0;
int starts = 0;
if (cl_fs_read_text(CL_STR("data/starts.txt"), text, sizeof text, &n) == CL_OK) starts = atoi(text);
starts++;
snprintf(text, sizeof text, "%d", starts);
cl_fs_write_text(CL_STR("data/starts.txt"), CL_STR_C(text));
CL_LOGF(CL_LOG_INFO, "server start number %d", starts);
}
/* in mod_main: cl_event_subscribe(CL_EV_LIFECYCLE_SERVER_STARTED, NULL, on_started, NULL, NULL); */
Recap: cl_fs_* reads your folder and writes data/ (WASI’s /data is the same folder), and cl_http_request talks to the websites the player allowed, with the answer at the next tick.