Skip to content

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

The functions are in clight::fs and return clight::Expected:

clight::fs::write_text("data/hello.txt", "Hello from my mod!");

if (auto text = clight::fs::read_text("data/hello.txt")) clight::log::info("{}", *text);
else clight::log::warn("no file ({})", int(text.error()));

clight::fs::append_text("data/log.txt", "a player joined\n");
clight::fs::mkdir("data/players");
if (auto info = clight::fs::stat("data/log.txt"); info && info->kind == CL_FS_FILE)
    clight::log::info("log.txt has {} bytes", info->size);
auto names = clight::fs::list("data");          // one name per line, folders end with '/'
clight::fs::rename("data/log.txt", "data/old_log.txt");
clight::fs::remove("data/players", true);       // true: with everything inside

Writes are atomic: the game writes a new file and then swaps it in, so a crash never leaves half a file. std::ifstream / std::ofstream work too, on the same files: your folder is mounted read-only at / and data/ read/write at /data.

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:

static void on_prices(cl_http_req req, int32_t status, cl_buf body, void*) {
    clight::HttpReq r(req);
    if (status != 200) {
        clight::log::warn("no prices: {} {}", status, r.http_error().value_or(""));
        return;
    }
    std::string text(cl_buf_size(body), '\0');
    size_t got = 0;
    cl_buf_read_bytes(body, text.data(), text.size(), &got);
    clight::log::info("prices: {}", text);
}

void ask() {
    auto req = clight::http::request("https://api.ruby-tools.example/v1/prices", nullptr, on_prices, nullptr);
    if (!req) clight::log::warn("not sent ({})", int(req.error()));  // CL_E_PERMISSION_DENIED: not allowed
}

status is 0 when no answer came; http_error() says why (timeout, cancelled, not allowed: <host>, …). body and req are valid only during the callback. For a POST, a download into data/ (save_to) and the limits, fill a cl_http_opts exactly as in C (Filesystem and HTTP):

cl_http_opts o = CL_HTTP_OPTS_INIT;
o.method = CL_HTTP_POST;
o.body = clight::s(R"({"player":"Alex","score":12})");
o.content_type = clight::s("application/json");
clight::http::request("https://api.ruby-tools.example/v1/scores", &o, on_saved, nullptr);

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); the answer fires mod.permission_changed.

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
void on_server_started() {
    int starts = 0;
    if (auto t = clight::fs::read_text("data/starts.txt")) starts = std::atoi(t->c_str());
    ++starts;
    clight::fs::write_text("data/starts.txt", std::to_string(starts));
    clight::log::info("server start number {}", starts);
}

Recap: clight::fs reads your folder and writes data/, and clight::http::request talks to the websites the player allowed, with the answer at the next tick.