File Operations: Difference between revisions
| (2 intermediate revisions by the same user not shown) | |||
| Line 221: | Line 221: | ||
Result type names are case-insensitive. | Result type names are case-insensitive. | ||
=== String === | ==== String ==== | ||
String parsing has two modes selected by the optional <code>decode_quotes</code> parameter. Legacy behavior remains the default so existing modules continue to work without modification. Quote-aware behavior is available by passing a nonzero fourth parameter. | String parsing has two modes selected by the optional <code>decode_quotes</code> parameter. Legacy behavior remains the default so existing modules continue to work without modification. Quote-aware behavior is available by passing a nonzero fourth parameter. | ||
String results may contain up to <code>MAX_SCRIPT_STRING_LENGTH</code>, currently 65,535 characters - about 36 Standard Academic Pages. | String results may contain up to <code>MAX_SCRIPT_STRING_LENGTH</code>, currently 65,535 characters - about 36 Standard Academic Pages. | ||
==== Legacy Parsing ==== | ===== Legacy Parsing ===== | ||
Omitting <code>decode_quotes</code>, or passing <code>0</code>, uses legacy whitespace parsing: | Omitting <code>decode_quotes</code>, or passing <code>0</code>, uses legacy whitespace parsing: | ||
| Line 251: | Line 249: | ||
This behavior supports older scripts that read and reconstruct files one whitespace-delimited token at a time. Use <code>getfilestreamline()</code> instead when the complete line and its original formatting must be returned as one string. | This behavior supports older scripts that read and reconstruct files one whitespace-delimited token at a time. Use <code>getfilestreamline()</code> instead when the complete line and its original formatting must be returned as one string. | ||
==== Quote-Aware Parsing ==== | ===== Quote-Aware Parsing ===== | ||
Pass a nonzero fourth parameter to enable modern quote-aware parsing: | Pass a nonzero fourth parameter to enable modern quote-aware parsing: | ||
| Line 270: | Line 267: | ||
Empty quoted arguments are supported. The maximum string length applies after quote delimiters are removed. | Empty quoted arguments are supported. The maximum string length applies after quote delimiters are removed. | ||
===== Double Quotes ===== | ====== Double Quotes ====== | ||
Enclose multiple-word text in double quotes to group it as a single string. | Enclose multiple-word text in double quotes to group it as a single string. | ||
| Line 286: | Line 282: | ||
</pre> | </pre> | ||
===== Single Quotes ===== | ====== Single Quotes ====== | ||
Single quotes may also group text: | Single quotes may also group text: | ||
| Line 302: | Line 297: | ||
</pre> | </pre> | ||
===== Mixed Quotes ===== | ====== Mixed Quotes ====== | ||
The opposite quote type remains literal inside a quoted span: | The opposite quote type remains literal inside a quoted span: | ||
| Line 316: | Line 310: | ||
</pre> | </pre> | ||
=== Numeric === | ==== Numeric ==== | ||
Missing or nonnumeric arguments converted with <code>"int"</code> or <code>"float"</code> resolve to <code>0</code>. Validate file structure when zero is also a meaningful value. | Missing or nonnumeric arguments converted with <code>"int"</code> or <code>"float"</code> resolve to <code>0</code>. Validate file structure when zero is also a meaningful value. | ||
Parsing mode also applies before numeric conversion. Legacy parsing retains quote characters, so a quoted number such as <code>"125"</code> converts to <code>0</code>. Quote-aware parsing removes the delimiters first, allowing the same value to convert to <code>125</code>. | Parsing mode also applies before numeric conversion. Legacy parsing retains quote characters, so a quoted number such as <code>"125"</code> converts to <code>0</code>. Quote-aware parsing removes the delimiters first, allowing the same value to convert to <code>125</code>. | ||
=== getfilestreamline() === | === getfilestreamline() === | ||
Latest revision as of 01:44, 24 August 2026
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
[, decode_quotes]
);
Reads an argument beginning at the current filestream position without advancing the position. Argument indexes are zero-based.
The optional decode_quotes parameter controls quote handling. Omit it or pass 0 for legacy whitespace parsing. Pass any nonzero value for modern quote-aware parsing. See String for details and examples.
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 and decode_quotes are ignored.
|
Result type names are case-insensitive.
String
String parsing has two modes selected by the optional decode_quotes parameter. Legacy behavior remains the default so existing modules continue to work without modification. Quote-aware behavior is available by passing a nonzero fourth parameter.
String results may contain up to MAX_SCRIPT_STRING_LENGTH, currently 65,535 characters - about 36 Standard Academic Pages.
Legacy Parsing
Omitting decode_quotes, or passing 0, uses legacy whitespace parsing:
char value = getfilestreamargument(
filestream_handle,
argument_index,
"string"
);
Spaces and tabs always separate arguments. Quotation marks have no special meaning and remain part of the returned text.
Given this line:
name "Max Thunder"
Legacy parsing returns "Max at argument index 1 and Thunder" at argument index 2.
This behavior supports older scripts that read and reconstruct files one whitespace-delimited token at a time. Use getfilestreamline() instead when the complete line and its original formatting must be returned as one string.
Quote-Aware Parsing
Pass a nonzero fourth parameter to enable modern quote-aware parsing:
char value = getfilestreamargument(
filestream_handle,
argument_index,
"string",
1
);
Matching single or double quotes group text into one argument. Opening and closing quote delimiters are discarded from the returned value. Outside quoted spans, spaces and tabs separate arguments, an unquoted line break ends the current record, and # begins a comment.
Using the previous example, argument index 1 returns Max Thunder as one string.
Empty quoted arguments are supported. The maximum string length applies after quote delimiters are removed.
Double Quotes
Enclose multiple-word text in double quotes to group it as a single string.
Value: "Hello world" Result: Hello world
Double-quoted spans may appear within an argument and are concatenated with adjacent unquoted text:
Value: prefix"two words"suffix Result: prefixtwo wordssuffix
Single Quotes
Single quotes may also group text:
Value: 'Multiple words' Result: Multiple words
Single quotes begin grouping only at an argument boundary. This preserves apostrophes in ordinary words:
Value: It's Result: It's
Mixed Quotes
The opposite quote type remains literal inside a quoted span:
Value: "Hello, it's a good day." Result: Hello, it's a good day.
Value: 'She said "hello".' Result: She said "hello".
Numeric
Missing or nonnumeric arguments converted with "int" or "float" resolve to 0. Validate file structure when zero is also a meaningful value.
Parsing mode also applies before numeric conversion. Legacy parsing retains quote characters, so a quoted number such as "125" converts to 0. Quote-aware parsing removes the delimiters first, allowing the same value to convert to 125.
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.