sm64coopdx/docs/lua/guides/modfs.md
PeachyPeach 32f395fb0c
Some checks failed
Build coop / build-linux (push) Has been cancelled
Build coop / build-steamos (push) Has been cancelled
Build coop / build-windows-opengl (push) Has been cancelled
Build coop / build-windows-directx (push) Has been cancelled
Build coop / build-macos-arm (push) Has been cancelled
Build coop / build-macos-intel (push) Has been cancelled
ModFs improvements (#907)
* zip + json properties; check existing file in create file

* smlua_audio_utils_replace_sequence

* audio_stream_load, audio_sample_load, smlua_model_util_get_id

* get_texture_info + can also load PNG files

* smlua_collision_util_get

* add wildcard in properties files + set text mode

* filepath restrictions

* Some mod_storage improvements

- Cache mod storage files into a map to reduce file I/O
- Fix a bug in mod_storage_save
- Add mod_storage_load_all that returns all keys/values as a table

* shutdown; fix buffer overflow; fix warnings; lua table

* reject binary files starting with MZ or ELF

* function members

* better doc

* adding file rewind

* ModFS guide; replace yaml by ini; read string buffer changes
2025-10-21 19:42:06 +02:00

272 lines
9.5 KiB
Markdown

## [:rewind: Lua Reference](../lua.md)
# ModFS
`ModFS` enables a small, sandboxed file system for mods. It allows to store and retrieve binary and text files, no matter their content.<br>
Each mod has its own file system, and can allow other mods to read its files.
<br>
## Specs
### File system
Each ModFS file system:
- Has a maximum size of **32 MB** (`MOD_FS_MAX_SIZE`). Files can be any size, as long as the cumulative sum of file sizes doesn't exceed this limit.
- Can store at most **512 files** (`MOD_FS_MAX_FILES`).
- Is stored on disk as a `.modfs` file, which is a ZIP file, containing all files written in it.
The ModFS files are located in the `sav` directory at the usual save file location:
- Windows: `%appdata%/sm64coopdx`
- Linux: `~/.local/share/sm64coopdx`
- MacOS: `~/Library/Application Support/sm64coopdx`
### Files
- The maximum filepath length is **256 characters** (`MOD_FS_MAX_PATH`), including the NUL terminator.
- Filepaths have the following restrictions:
- Cannot start, end or have two or more consecutive `/`
- Can contain only valid ASCII characters, no `*` or `\`
- Cannot be called `properties.json` (this name is reserved for ModFS internal properties)
- Only the following extensions (and extension-less files) are allowed:
- text: `.txt`, `.json`, `.ini`, `.sav`
- actors: `.bin`, `.col`
- behaviors: `.bhv`
- textures: `.tex`, `.png`
- levels: `.lvl`
- audio: `.m64`, `.aiff`, `.mp3`, `.ogg`
<br>
## [`ModFs`](../structs.md#ModFs)
The object holding the file system of the mod.
### Fields
All fields are immutable.
| Name | Type |
| ----- | ---- |
| mod | [Mod](../structs.md#Mod) |
| modPath | `string` |
| numFiles | `integer` |
| totalSize | `integer` |
| isPublic | `boolean` |
Fields can be accessed in Lua with the dot `.` character:
```lua
print("The ModFS " .. modFs.modPath .. " contains " .. modFs.numFiles .. " files.")
```
### Methods
| Name | Reference |
| ---- | --------- |
| get_filename | [`mod_fs_get_filename`](../functions-5.md#mod_fs_get_filename) |
| get_file | [`mod_fs_get_file`](../functions-5.md#mod_fs_get_file) |
| create_file | [`mod_fs_create_file`](../functions-5.md#mod_fs_create_file) |
| move_file | [`mod_fs_move_file`](../functions-5.md#mod_fs_move_file) |
| copy_file | [`mod_fs_copy_file`](../functions-5.md#mod_fs_copy_file) |
| delete_file | [`mod_fs_delete_file`](../functions-5.md#mod_fs_delete_file) |
| clear | [`mod_fs_clear`](../functions-5.md#mod_fs_clear) |
| save | [`mod_fs_save`](../functions-5.md#mod_fs_save) |
| delete | [`mod_fs_delete`](../functions-5.md#mod_fs_delete) |
| set_public | [`mod_fs_set_public`](../functions-5.md#mod_fs_set_public) |
Methods can be called in Lua with the colon `:` character:
```lua
print("The first file of ModFS " .. modFs.modPath .. " is named " .. modFs:get_filename(0) .. ".")
```
<br>
## [`ModFsFile`](../structs.md#ModFsFile)
A handle to a ModFS file.
### Fields
All fields are immutable.
| Field | Type |
| ----- | ---- |
| modFs | [ModFs](../structs.md#ModFs) |
| filepath | `string` |
| size | `integer` |
| offset | `integer` |
| isText | `boolean` |
| isPublic | `boolean` |
Fields can be accessed in Lua with the dot `.` character:
```lua
print("The ModFS file " .. file.filepath .. " is " .. file.size .. " bytes long.")
```
### Methods
| Name | Reference |
| ---- | --------- |
| read_bool | [`mod_fs_file_read_bool`](../functions-5.md#mod_fs_file_read_bool) |
| read_integer | [`mod_fs_file_read_integer`](../functions-5.md#mod_fs_file_read_integer) |
| read_number | [`mod_fs_file_read_number`](../functions-5.md#mod_fs_file_read_number) |
| read_bytes | [`mod_fs_file_read_bytes`](../functions-5.md#mod_fs_file_read_bytes) |
| read_string | [`mod_fs_file_read_string`](../functions-5.md#mod_fs_file_read_string) |
| read_line | [`mod_fs_file_read_line`](../functions-5.md#mod_fs_file_read_line) |
| write_bool | [`mod_fs_file_write_bool`](../functions-5.md#mod_fs_file_write_bool) |
| write_integer | [`mod_fs_file_write_integer`](../functions-5.md#mod_fs_file_write_integer) |
| write_number | [`mod_fs_file_write_number`](../functions-5.md#mod_fs_file_write_number) |
| write_bytes | [`mod_fs_file_write_bytes`](../functions-5.md#mod_fs_file_write_bytes) |
| write_string | [`mod_fs_file_write_string`](../functions-5.md#mod_fs_file_write_string) |
| write_line | [`mod_fs_file_write_line`](../functions-5.md#mod_fs_file_write_line) |
| seek | [`mod_fs_file_seek`](../functions-5.md#mod_fs_file_seek) |
| rewind | [`mod_fs_file_rewind`](../functions-5.md#mod_fs_file_rewind) |
| is_eof | [`mod_fs_file_is_eof`](../functions-5.md#mod_fs_file_is_eof) |
| fill | [`mod_fs_file_fill`](../functions-5.md#mod_fs_file_fill) |
| erase | [`mod_fs_file_erase`](../functions-5.md#mod_fs_file_erase) |
| set_text_mode | [`mod_fs_file_set_text_mode`](../functions-5.md#mod_fs_file_set_text_mode) |
| set_public | [`mod_fs_file_set_public`](../functions-5.md#mod_fs_file_set_public) |
Methods can be called in Lua with the colon `:` character:
```lua
file:erase(file.size)
print("The ModFS file " .. file.filepath .. " is now empty.")
```
<br>
## Error handling
All errors coming from ModFS functions are not blocking. However, they appear in the console and raise the "Mod has script errors" message.
- The function [`mod_fs_hide_errors`](../functions-5.md#mod_fs_hide_errors) can suppress the ModFS errors from the console.
- Use the function [`mod_fs_get_last_error`](../functions-5.md#mod_fs_get_last_error) to retrieve the last error raised by ModFS. This function always return an error message if an error occurred, even if errors are hidden.
<br>
## Usage with other sm64coopdx features
One of the strengths of this feature is its interactions with other existing features of sm64coopdx:
- Load models with `smlua_model_util_get_id`
- Load textures with `get_texture_info`
- Load collisions with `smlua_collision_util_get`
- Load sequences with `smlua_audio_utils_replace_sequence`
- Load audio streams with `audio_stream_load`
- Load audio samples with `audio_sample_load`
These functions can take a **ModFS URI** as argument instead of a resource name.<br>
Generate a ModFS URI from a `ModFs` object with the following code:
```lua
local uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "<filepath>")
```
Here are some examples:
```lua
-- Models
local custom_geo_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "custom_geo.bin")
local E_MODEL_CUSTOM = smlua_model_util_get_id(custom_geo_uri)
-- Textures (both PNG and TEX)
local texture_png_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "texture.png")
local TEXTURE_PNG = get_texture_info(texture_png_uri)
local texture_tex_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "texture.tex")
local TEXTURE_TEX = get_texture_info(texture_tex_uri)
-- Collisions
local custom_col_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "custom_col.col")
local COL_CUSTOM = smlua_collision_util_get(custom_col_uri)
-- Sequences
local custom_m64_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "custom.m64")
smlua_audio_utils_replace_sequence(SEQ_LEVEL_GRASS, 0x11, 0x80, custom_m64_uri)
-- Streams
local custom_stream_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "custom_stream.mp3")
local custom_stream = audio_stream_load(custom_stream_uri)
-- Samples
local custom_sample_uri = string.format(MOD_FS_URI_FORMAT, modFs.modPath, "custom_sample.mp3")
local custom_sample = audio_sample_load(custom_sample_uri)
```
<br>
## Good practices
### Always valid `ModFs` object
Use the following piece of code to always retrieve a valid `ModFs` object:
```lua
local modFs = mod_fs_get() or mod_fs_create()
```
If the ModFS for the current mod doesn't exist, it will create one.
<br>
### Always valid `ModFsFile` object
Use the following piece of code to always retrieve a valid `ModFsFile` object:
```lua
local file = modFs:get_file("myfile.txt") or modFs:create_file("myfile.txt", true)
```
Like previously, if the file doesn't exist, it will create one.<br>
To make sure the file is empty when requested, add the following line to clear the existing file content.
```lua
file:erase(file.size)
```
<br>
### Correctly initialize a file
The `get_file` method of a `ModFs` object opens a file only if the file is not loaded yet. Subsequent calls with the same filename will return the file handle without resetting its offset or mode.<br>
For example, one function could write to a file while another could read from the same file, so it's better to set the appropriate file offset and mode when it's needed before starting reading/writing:
```lua
local file = modFs:get_file("myfile.txt")
file:set_text_mode(true) -- Set mode to text
file:rewind() -- Reset offset to the beginning of the file
```
<br>
### Methods over functions
Always use `ModFs` and `ModFsFile` objects methods over regular functions.<br>
It's more clear that way and helps to reduce errors:
```lua
-- Don't
local file = mod_fs_create_file(modFs, "myfile.txt", true)
-- Do
local file = modFs:create_file("myfile.txt", true)
```
```lua
-- Don't
mod_fs_file_write_string(file, "some text")
-- Do
file:write_string("some text")
```
<br>
### Handle possible failures
In addition to error messages that can be retrieved with [`mod_fs_get_last_error`](../functions-5.md#mod_fs_get_last_error), almost all ModFS functions have a boolean return value indicating if the function succeeded or failed.
```lua
if not modFs:delete_file("somefile") then
print(mod_fs_get_last_error())
end
```
<br>
### Don't forget to save
ModFS are not saved automatically when writing to files.<br>
The mod has to explicitly call the method `save` to save its ModFS on the disk.
```lua
modFs:save()
```