File IO - HimxhMC/HimDSL GitHub Wiki

文件 I/O / File I/O

为什么需要文件 / Why Files

脚本的变量在服务器重启后消失。文件是唯一的持久化手段——保存配置、记录数据、输出日志。

Variables vanish on server restart. Files are the only persistence — save config, record data, produce logs.

HimDSL 提供两组文件 API:

HimDSL offers two groups:

  • 句柄式:fopen / fread / fwrite / fclose——适合大文件、流式处理。

  • 便捷式:freadlines / fwritelines——一次读写全部行。

  • Handle-based: fopen / fread / fwrite / fclose — for streaming.

  • Convenience: freadlines / fwritelines — read/write all lines at once.

日常脚本 90% 用便捷式。

90% of daily scripts use the convenience form.


路径规则 / Path Rules

相对路径基于插件数据目录(plugins/HimDSL/):

Relative paths resolve against the plugin data folder (plugins/HimDSL/):

freadlines("config.txt")                    // plugins/HimDSL/config.txt
freadlines("data/players.txt")              // plugins/HimDSL/data/players.txt

绝对路径也可以,但会绕过安全目录检查:

Absolute paths work but bypass the safety directory check:

freadlines("/tmp/whatever.txt")             // 允许,但小心

相对路径不能逃逸数据目录:

Relative paths cannot escape the data folder:

freadlines("../other/file.txt")             // ✗ 报错

这个限制是保护,防止脚本意外读写系统文件。日常脚本用相对路径。

This restriction is protective — prevents accidental access to system files. Use relative paths.


便捷读取:freadlines / Convenience Read

读取整个文件为字符串列表,每行一个元素:

Reads the entire file as a list of strings, one per line:

var lines = freadlines("data.txt");
int count = lines.size();
for (int i = 0; i < count; i = i + 1) {
    runcmd("CONSOLE", "say " + lines.get(i));
}

文件不存在会报错。先检查:

Missing files error. Check first:

if (!fexists("data.txt")) {
    runcmd("CONSOLE", "say 文件不存在");
    return;
}
var lines = freadlines("data.txt");

便捷写入:fwritelines / Convenience Write

把字符串列表写入文件,一行一个元素:

Writes a list of strings, one per line:

fwritelines("output.txt", {
    "第一行",
    "第二行",
    "第三行"
});

这会覆盖文件——已有的内容全部丢失。没有追加模式。要追加内容,先读出来再写回去:

This overwrites the file — existing content is lost. No append mode. To append, read first and write back:

var lines = vector();
if (fexists("log.txt")) {
    lines = freadlines("log.txt");
}
lines.add("新的一行");
fwritelines("log.txt", lines);

注意这个模式在文件很大时会很慢。不要用它做日志——日志应用句柄式的 fopen("log.txt", "a")。

This pattern is slow for large files. Do not use it for logging — use handle-based fopen("log.txt", "a").


句柄式读写 / Handle-Based I/O

fopen 打开文件,返回一个数字句柄:

fopen opens a file and returns a numeric handle:

int f = fopen("data.txt", "r");     // 只读
int f = fopen("data.txt", "w");     // 覆盖写
int f = fopen("data.txt", "a");     // 追加写

三种模式:

Three modes:

模式 含义
"r" 读取(文件必须存在)
"w" 覆盖写(文件不存在则创建)
"a" 追加写(文件不存在则创建)

返回的句柄一定要 fclose,否则文件保持占用:

Always fclose the handle, or the file stays open:

int f = fopen("log.txt", "a");
fwrite(f, "新日志行\n");
fclose(f);

fwrite 追加日志 / fwrite for Logging

"a" 模式配合 fwrite 是正确的日志做法:

"a" mode with fwrite is the correct logging pattern:

int f = fopen("actions.log", "a");
fwrite(f, player.name + " 使用了钻石\n");
fclose(f);

每次追加一行,不影响已有内容。

Each call appends a line, without touching existing content.

fread 读取全部 / fread Reads Everything

fread(f) 返回文件剩余内容的单个字符串:

fread(f) returns the remaining content as one string:

int f = fopen("data.txt", "r");
string content = fread(f);
fclose(f);
// content 里可能包含多个 \n

小文件用 freadlines 更方便。fread 主要用于处理二进制或大文件。

For small files, freadlines is easier. Use fread for binary or huge files.


存在与删除 / Existence and Deletion

bool exists = fexists("data.txt");
bool deleted = fdelete("old.txt");        // 返回是否成功删除

fdelete 删除失败时不报错,返回 false。删除前先 fexists 检查更稳妥。

fdelete does not error on failure; returns false. Check fexists first for safety.


一个完整的保存/读取例子 / A Complete Save/Load Example

把玩家等级保存到文件:

Save a player's level to a file:

void saveLevel(string playerName, int level) {
    fwritelines("levels.txt", { playerName + "=" + level });
}

int loadLevel(string playerName) {
    if (!fexists("levels.txt")) return 0;
    var lines = freadlines("levels.txt");
    if (lines.size() == 0) return 0;

    var parts = lines.get(0).split("=");
    if (parts.size() != 2) return 0;
    return toInt(parts.get(1));
}

int main() {
    saveLevel("Steve", 30);
    int level = loadLevel("Steve");
    runcmd("CONSOLE", "say Steve 等级 " + level);
}

这个例子只保存一个玩家。多玩家需要更多行——fwritelines 会覆盖,所以要先读全部、改一行、再写回。或者用 JSON(见《JSON》一章)。

This stores one player. Multiple players need multiple lines — since fwritelines overwrites, read all, modify one, write back. Or use JSON (see the JSON chapter).


常见错误 / Common Mistakes

错误一:路径逃逸

freadlines("../config.yml");        // ✗ 报错

相对路径不能离开插件目录。

错误二:把 fwritelines 当追加

fwritelines("log.txt", {"new line"});    // ✗ 覆盖全部

追加要用 fopen("log.txt", "a")。

错误三:忘记 fclose

int f = fopen("data.txt", "r");
string s = fread(f);
// 忘记 fclose(f) → 文件一直开着

错误四:以 "r" 模式打开不存在的文件

int f = fopen("nope.txt", "r");     // ✗ 报错

先 fexists 检查,或用 "a"(追加模式会创建文件)。

Mistake 1: Path escape

Relative paths stay inside the plugin directory.

Mistake 2: Treating fwritelines as append

It overwrites. Use fopen("...", "a") for append.

Mistake 3: Forgetting fclose

The file stays open.

Mistake 4: Opening a missing file with "r"

Check fexists, or use "a" (creates the file).


小结 / Summary

  • 相对路径基于 plugins/HimDSL/,不能逃逸。

  • 便捷式:freadlines / fwritelines,适合小文件。

  • 句柄式:fopen / fread / fwrite / fclose,适合追加、大文件。

  • fopen 模式:"r" / "w" / "a"。

  • fwritelines 会覆盖,追加要用 "a" 模式。

  • 存在与删除:fexists / fdelete。

  • 句柄用完 fclose。

  • Relative paths resolve against plugins/HimDSL/ and cannot escape.

  • Convenience: freadlines / fwritelines for small files.

  • Handle-based: fopen / fread / fwrite / fclose for appends and large files.

  • fopen modes: "r" / "w" / "a".

  • fwritelines overwrites; append with "a".

  • Existence and deletion: fexists / fdelete.

  • Close handles with fclose.


下一步 / Next Steps

下一章 [命令与 sleep / Commands-and-Sleep] 讲 runcmd 的细节和 sleep 的正确用法。

Next, [命令与 sleep / Commands-and-Sleep] covers runcmd details and correct sleep usage.