File Operations
OpenBOR supports file CRUD - create, read, update, and delete - through an API called filestream. Scripts can load external files into memory, read structured text, assemble or revise content, save the result to disk, and remove files that are no longer needed. Common uses include custom configuration, persistent game data, generated model or script files, and data exchanged with external tools.
Despite the name, an OpenBOR filestream is not a continuously open operating-system file. Instead, OpenBOR filestreams use memory-backed buffers. Opening a file loads its contents into memory, after which reading and editing require no additional disk access or filesystem overhead. Scripts may perform any number of operations at memory speed, then explicitly commit the finished buffer to storage with savefilestream().
Each filestream maintains two independent locations:
- Buffer end -
filestreamappend()always writes here. - Read position - Line, argument, byte, and position functions operate here.
Changing the read position does not change where appended data is written.
Important: Memory buffers and external files have separate lifecycles.
closefilestream()releases a buffer without deleting its saved file.deletefilestream()removes an external file without releasing any buffer previously loaded from it.
Lifecycle
Creating, reading, or updating a file normally follows four steps:
- Create an empty stream with
createfilestream(), or load an existing file withopenfilestream(). - Read from the current position or append content to the buffer.
- Call
savefilestream()when the buffer should be written to disk. - Call
closefilestream()when the buffer is no longer needed.
Deletion is independent of this handle lifecycle. Pass a filename directly to deletefilestream(); no stream needs to be open.
| Operation | Native support | Method |
|---|---|---|
| Create | Yes | Create an empty buffer, append content, then save it under a filename. |
| Read | Yes | Open a file, then read its current line, arguments, or byte. |
| Update | Append or rewrite | Open and append to the loaded buffer, or construct a replacement buffer, then save over the previous file. |
| Delete | Yes | Pass a filename to deletefilestream() to remove a writable external file.
|
Saving is explicit. Closing an unsaved stream discards its in-memory changes. Deleting a file does not discard a loaded buffer, so saving that buffer later may recreate the file.
Handle and Buffer Model
The returned filestream handle is a zero-based index into the engine's filestream collection. Internally, each entry contains:
| Member | Purpose |
|---|---|
buf
|
Allocated memory containing the loaded or constructed data. |
size
|
Current buffer size and append location. |
pos
|
Current byte position used by read functions. |
Closed entries become available for reuse. Once closefilestream() is called, the old handle is invalid and may later identify a different filestream.
The engine does not clamp filestream handles or manually assigned positions. Use only handles returned by createfilestream() or successful calls to openfilestream(), and never use a handle after closing it.
Walk-through
Create and Save
The following creates a two-line text file:
void filestream_handle = createfilestream();
filestreamappend(filestream_handle, "name", 1);
filestreamappend(filestream_handle, "Max");
filestreamappend(filestream_handle, "score", 0);
filestreamappend(filestream_handle, 125000);
savefilestream(filestream_handle, "profile.txt");
closefilestream(filestream_handle);
The default save location is the current module's subdirectory under Saves. Text mode adds a final carriage return and line feed, producing:
name Max score 125000
In this example, write type 1 adds a trailing space after name. Write type 0 begins a new line before score and then adds a trailing space.
Open and Read
Pass a nonzero location value to load from the current module's save directory:
void filestream_handle = openfilestream("profile.txt", 1);
if (filestream_handle >= 0) {
char player_name = getfilestreamargument(
filestream_handle,
1,
"string"
);
filestreamnextline(filestream_handle);
int player_score = getfilestreamargument(
filestream_handle,
1,
"int"
);
closefilestream(filestream_handle);
}
The first argument on each line is index 0. The example reads index 1, returning "Max" from the first line and 125000 from the second.
Append and Replace
Opening a file loads its existing content into the buffer. Appending new content and saving under the same name rewrites the external file with the expanded buffer:
void filestream_handle = openfilestream("profile.txt", 1);
if (filestream_handle >= 0) {
/*
* Files written by savefilestream() in text mode already
* end with a line break, so begin the next record directly.
*/
filestreamappend(filestream_handle, "unlocks", 1);
filestreamappend(filestream_handle, 3);
savefilestream(filestream_handle, "profile.txt");
closefilestream(filestream_handle);
}
savefilestream() opens the destination in write mode and replaces its previous contents. Existing data survives only because openfilestream() first copied that data into the memory buffer.
Filestreams do not provide insertion, replacement, or removal at an arbitrary read position. To modify existing records, read the source and assemble the desired result in another filestream, then save the replacement over the original filename.
Delete
Deletion targets the external file by name rather than a filestream handle:
int deleted = deletefilestream("profile.txt");
if (!deleted) {
log("Could not delete profile.txt.\n");
}
The default location is the current module's subdirectory under Saves, matching savefilestream(). Supply the same optional pathname used to save a file when deleting from a custom location:
int deleted = deletefilestream(
"profile.txt",
"data/generated/"
);
Only the external file is removed. If a filestream already contains data loaded from that file, its memory buffer and handle remain valid until closefilestream() is called.
Functions
closefilestream()
closefilestream(filestream_handle);
Releases the filestream's allocated buffer. The handle becomes invalid and its collection index may be reused by a later create or open operation.
This function does not save the buffer and does not delete an external file.
createfilestream()
int filestream_handle = createfilestream();
Allocates an empty memory buffer and returns its handle. The initial read position and content size are both 0.
No external file is created until the buffer is passed to savefilestream().
deletefilestream()
int deleted = deletefilestream(filename[, pathname]);
Deletes a writable external file and returns 1 when the operating system confirms deletion. A return value of 0 means the file could not be deleted, including when the file does not exist, the path is incorrect, or the operating system denies access.
When pathname is omitted, OpenBOR targets the current module's subdirectory under Saves:
int deleted = deletefilestream("profile.txt");
The optional pathname follows the same rules as savefilestream() and is relative to the engine's working directory:
int deleted = deletefilestream(
"profile.txt",
"data/generated/"
);
The pathname is joined directly to the filename. Include the final slash or backslash.
Deletion operates on external files only and cannot remove content from a packfile. It does not accept a filestream handle, release a memory buffer, or invalidate a handle. Saving a still-open buffer to the same filename after successful deletion creates the file again.
filestreamappend()
filestreamappend(filestream_handle, value[, write_type[, value_type]]);
Converts the supplied value to text and appends it at the end of the buffer. The current read position has no effect on the append location.
write_type
|
Behavior |
|---|---|
Omitted, -1, or any value other than 0 and 1
|
Appends the value without adding a separator. |
0
|
Adds \r\n before the value, then adds one trailing space after it.
|
1
|
Appends the value followed by one trailing space. |
Write type 0 starts a new line before the supplied value. Using it for the first value in an empty stream therefore creates an initial blank line.
Passing "byte" as value_type selects the legacy byte append path:
filestreamappend(filestream_handle, byte_value, write_type, "byte");
See Binary Data before using byte mode.
filestreamnextline()
filestreamnextline(filestream_handle);
Advances the read position from its current location to the next non-line-break byte. The function scans to the end of the current line, then skips all consecutive \r and \n characters.
Multiple empty lines are skipped as one group. At the end of the buffer, the position does not change.
getfilestreamargument()
mixed result = getfilestreamargument(
filestream_handle,
argument_index,
result_type
);
Reads an argument from the current line without advancing the read position. Argument indexes are zero-based and separated by spaces or tabs.
result_type
|
Return |
|---|---|
"string"
|
Argument text. |
"int"
|
Argument converted to an integer. |
"float"
|
Argument converted to a decimal value. |
"byte"
|
Unsigned value of the byte at the current read position. argument_index is ignored.
|
The result type names are case-insensitive.
Quoted text does not form a single argument. For example, "Max Thunder" still occupies two whitespace-separated argument indexes. Use getfilestreamline() when a complete line must be preserved.
Missing or nonnumeric arguments converted with "int" or "float" resolve to 0. Validate file structure when zero is also a meaningful value.
getfilestreamline()
char line = getfilestreamline(filestream_handle);
Returns text from the current read position through the next carriage return, line feed, or end of buffer. The line-ending characters are not included.
This function does not advance the read position. Repeated calls return the same line until filestreamnextline() or setfilestreamposition() changes the position.
An empty line and the end of the buffer both return an empty string.
getfilestreamposition()
int position = getfilestreamposition(filestream_handle);
Returns the current read position as a byte offset from the beginning of the buffer. The value is not a line number.
Positions may be stored and supplied later to setfilestreamposition().
openfilestream()
int filestream_handle = openfilestream(filename[, location]);
Loads an existing file into memory, sets its read position to 0, and returns the filestream handle. The complete file is loaded at once.
location
|
Source |
|---|---|
Omitted or 0
|
Module-relative path. OpenBOR checks loose files and the current packfile. |
| Any nonzero value | Current module's subdirectory under Saves.
|
For a module named Example.pak, the following reads Saves/Example/profile.txt:
int filestream_handle = openfilestream("profile.txt", 1);
The function returns -1 when the requested file does not exist or cannot be opened. Always validate the result before using it:
void filestream_handle = openfilestream("data/config.txt");
if (filestream_handle < 0) {
return;
}
savefilestream()
savefilestream(
filestream_handle,
filename
[, pathname
[, save_type]]
);
Writes the current buffer to an external file. The destination is opened in write mode, so an existing file at the same path is replaced.
When pathname is omitted, OpenBOR writes to the current module's subdirectory under Saves:
savefilestream(filestream_handle, "profile.txt");
An optional custom pathname is relative to the engine's working directory:
savefilestream(
filestream_handle,
"profile.txt",
"data/generated/"
);
The custom pathname is joined directly to the filename. Include the final slash or backslash in pathname.
Default save paths are recommended for portable persistent data. Custom relative paths may resolve differently between platforms and launch environments.
Text mode writes the buffer and then adds \r\n to the end of the file. Passing "byte" as save_type suppresses that final line break:
savefilestream(
filestream_handle,
"output.dat",
"data/generated/",
"byte"
);
Text mode adds this line break unconditionally. If a loaded buffer already ends with \r\n and is saved again without new content after it, the file gains another empty line.
Saving does not close the filestream. The same buffer may be appended or saved again before it is released.
setfilestreamposition()
setfilestreamposition(filestream_handle, position);
Sets the read position to a byte offset. Use 0 to return to the beginning:
setfilestreamposition(filestream_handle, 0);
The engine does not validate or clamp this value. Negative positions and positions beyond the allocated buffer are invalid and may cause unstable behavior.
Changing the read position does not affect filestreamappend(), which always writes at the buffer end.
Iterating Through Lines
OpenBOR does not provide a separate end-of-filestream function. Compare the position before and after filestreamnextline() to detect when no further progress is possible:
void filestream_handle = openfilestream("data/records.txt");
if (filestream_handle >= 0) {
while (1) {
int current_position = getfilestreamposition(
filestream_handle
);
char current_line = getfilestreamline(
filestream_handle
);
filestreamnextline(filestream_handle);
int next_position = getfilestreamposition(
filestream_handle
);
if (current_line != "") {
log(current_line + "\n");
}
if (next_position == current_position) {
break;
}
}
closefilestream(filestream_handle);
}
This pattern also tolerates empty lines. filestreamnextline() skips their line-ending characters and continues to the next populated line.
Binary Data
Byte mode lets scripts work with individual numeric byte values instead of parsed text. This is useful for compact records, custom encodings, byte-level inspection, and data containing non-printing values that are awkward to represent as ordinary strings. Reads return the byte at the current position, while append operations can add byte values directly without converting them to text.
Byte mode is not currently a complete binary-file interface. Saving determines the output length by scanning for a null terminator, so a byte value of 0 ends the writable content. Values from 1 through 255 can be manipulated and preserved, but files containing embedded null bytes cannot be saved reliably. Use byte mode for controlled byte-oriented formats, and text mode or another native API when arbitrary binary data must be preserved.
Practical Guidance
- Check every
openfilestream()result for-1. - Keep handles private to the code that owns their lifecycle when practical.
- Maximum file path length is 511 characters. Be aware that some operating systems impose stricter limits. Windows 11, for example, limits file paths to 260 characters by default. For portability, try to keep file paths reasonable in length.
- Save before closing when changes must persist.
- Close every filestream when it is no longer needed.
- Never use a closed handle or invent a handle index.
- Check the return value from
deletefilestream()before assuming a file was removed. - Close a buffer separately when deleting the file from which it was loaded.
- Use
getfilestreamposition()to preserve known-safe positions instead of guessing byte offsets. - Prefer one logical record per line and whitespace-separated fields when using
getfilestreamargument(). - Use
getfilestreamline()when field content may contain spaces. - Avoid loading unnecessarily large files. Opening a filestream allocates enough memory for the complete file.
- Remember that packfile content is not modified in place. Save changed data to the module's save directory or another writable external path.
The engine releases any remaining filestream buffers during shutdown, but does not save them automatically or delete their external files. Explicit cleanup keeps ownership clear and allows closed handle slots to be reused during play.