Regenerate DynOS assets when source files are modified (#873)
Some checks are pending
Build coop / build-linux (push) Waiting to run
Build coop / build-steamos (push) Waiting to run
Build coop / build-windows-opengl (push) Waiting to run
Build coop / build-windows-directx (push) Waiting to run
Build coop / build-macos-arm (push) Waiting to run
Build coop / build-macos-intel (push) Waiting to run

Previously to recompile DynOS assets you had to remove the
bin/lvl/bhv/tex/col/etc files. Now the game will compare the
last-modified-timestamps of the generated/compiled assets vs
the other files that are within those directories. If the presumed-
source files have a later modified timestamp DynOS will regenerate
those assets.

While this results in scanning the attributes of files more, it
also prevents parsing files unnecessarily. Previously actors
would always parse their source files and build up GfxData
unnecessarily.

---------

Co-authored-by: MysterD <myster@d>
This commit is contained in:
djoslin0 2025-07-02 06:24:35 -07:00 committed by GitHub
parent 6a5af9d23a
commit 81af37eef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 252 additions and 97 deletions

View file

@ -1019,6 +1019,12 @@ s64 DynOS_RecursiveDescent_Parse(const char* expr, bool* success, RDConstantFunc
void DynOS_Read_Source(GfxData *aGfxData, const SysPath &aFilename);
char *DynOS_Read_Buffer(FILE* aFile, GfxData* aGfxData);
bool DynOS_ShouldGeneratePack(const SysPath &aPackFolder, std::initializer_list<const char*> aExtensions);
bool DynOS_ShouldGeneratePack2Ext(const SysPath &aPackFolder, const char *aGenExtension, const char *aSrcExtension);
bool DynOS_GenFileExistsAndIsNewerThanFile(const SysPath &aGenFile, const SysPath &aSrcFile);
bool DynOS_GenFileExistsAndIsNewerThanFolder(const SysPath &aGenFile, const SysPath &aSrcFolder);
String DynOS_GetActorFolder(const Array<Pair<u64, String>> &aActorsFolders, u64 aModelIdentifier);
s64 DynOS_Misc_ParseInteger(const String& _Arg, bool* found);
void DynOS_Anim_ScanFolder(GfxData *aGfxData, const SysPath &aAnimsFolder);

View file

@ -18,6 +18,7 @@
#include <utility>
#include <string>
#include <map>
#include <initializer_list>
extern "C" {
#endif
#include "config.h"

View file

@ -131,33 +131,8 @@ GfxData *DynOS_Actor_LoadFromBinary(const SysPath &aPackFolder, const char *aAct
// Generate //
//////////////
static String GetActorFolder(const Array<Pair<u64, String>> &aActorsFolders, u64 aModelIdentifier) {
for (const auto &_Pair : aActorsFolders) {
if (_Pair.first == aModelIdentifier) {
return _Pair.second;
}
}
return String();
}
static void DynOS_Actor_Generate(const SysPath &aPackFolder, Array<Pair<u64, String>> _ActorsFolders, GfxData *_GfxData) {
// do not regen this folder if we find any existing bins
for (s32 geoIndex = _GfxData->mGeoLayouts.Count() - 1; geoIndex >= 0; geoIndex--) {
auto &_GeoNode = _GfxData->mGeoLayouts[geoIndex];
String _GeoRootName = _GeoNode->mName;
// If there is an existing binary file for this layout, skip and go to the next actor
SysPath _BinFilename = fstring("%s/%s.bin", aPackFolder.c_str(), _GeoRootName.begin());
if (fs_sys_file_exists(_BinFilename.c_str())) {
// Compress file to gain some space
if (configCompressOnStartup && !DynOS_Bin_IsCompressed(_BinFilename)) {
DynOS_Bin_Compress(_BinFilename);
}
return;
}
}
Array<String> _SkipActorFolders;
// generate in reverse order to detect children
for (s32 geoIndex = _GfxData->mGeoLayouts.Count() - 1; geoIndex >= 0; geoIndex--) {
@ -173,6 +148,18 @@ static void DynOS_Actor_Generate(const SysPath &aPackFolder, Array<Pair<u64, Str
// If there is an existing binary file for this layout, skip and go to the next actor
SysPath _BinFilename = fstring("%s/%s.bin", aPackFolder.c_str(), _GeoRootName.begin());
// If there is an existing binary file for this actor, skip and go to the next actor
String _ActorFolder = DynOS_GetActorFolder(_ActorsFolders, _GeoNode->mModelIdentifier);
SysPath _SrcFolder = fstring("%s/%s", aPackFolder.c_str(), _ActorFolder.begin());
if (DynOS_GenFileExistsAndIsNewerThanFolder(_BinFilename, _SrcFolder)) {
// Remember that we skipped this folder, so we can skip it again in the future.
// This prevents generating child geo bins when we shouldn't.
_SkipActorFolders.Add(_ActorFolder);
continue;
} else if (_SkipActorFolders.Find(_ActorFolder) != -1) {
continue;
}
// Init
_GfxData->mLoadIndex = 0;
_GfxData->mErrorCount = 0;
@ -201,7 +188,6 @@ static void DynOS_Actor_Generate(const SysPath &aPackFolder, Array<Pair<u64, Str
_GfxData->mAnimationTable.Clear();
// Scan anims folder for animation data
String _ActorFolder = GetActorFolder(_ActorsFolders, _GfxData->mModelIdentifier);
SysPath _AnimsFolder = fstring("%s/%s/anims", aPackFolder.c_str(), _ActorFolder.begin());
DynOS_Anim_ScanFolder(_GfxData, _AnimsFolder);
@ -246,6 +232,11 @@ static void DynOS_Actor_Generate(const SysPath &aPackFolder, Array<Pair<u64, Str
void DynOS_Actor_GeneratePack(const SysPath &aPackFolder) {
Print("Processing actors: \"%s\"", aPackFolder.c_str());
if (!DynOS_ShouldGeneratePack(aPackFolder, { ".bin", ".col" })) {
return;
}
Array<Pair<u64, String>> _ActorsFolders;
GfxData *_GfxData = New<GfxData>();

View file

@ -2602,18 +2602,6 @@ static String GetBehaviorFolder(const Array<Pair<u64, String>> &aBehaviorsFolder
}
static void DynOS_Bhv_Generate(const SysPath &aPackFolder, Array<Pair<u64, String>> _BehaviorsFolders, GfxData *_GfxData) {
// do not regen this folder if we find any existing bins
for (s32 bhvIndex = _GfxData->mBehaviorScripts.Count() - 1; bhvIndex >= 0; bhvIndex--) {
auto &_BhvNode = _GfxData->mBehaviorScripts[bhvIndex];
String _BhvRootName = _BhvNode->mName;
// If there is an existing binary file for this layout, skip and go to the next behavior.
SysPath _BinFilename = fstring("%s/%s.bhv", aPackFolder.c_str(), _BhvRootName.begin());
if (fs_sys_file_exists(_BinFilename.c_str())) {
return;
}
}
// generate in reverse order to detect children
for (s32 bhvIndex = _GfxData->mBehaviorScripts.Count() - 1; bhvIndex >= 0; bhvIndex--) {
auto &_BhvNode = _GfxData->mBehaviorScripts[bhvIndex];
@ -2660,6 +2648,11 @@ static void DynOS_Bhv_Generate(const SysPath &aPackFolder, Array<Pair<u64, Strin
void DynOS_Bhv_GeneratePack(const SysPath &aPackFolder) {
Print("Processing behaviors: \"%s\"", aPackFolder.c_str());
if (!DynOS_ShouldGeneratePack2Ext(aPackFolder, ".bhv", ".c")) {
return;
}
Array<Pair<u64, String>> _BehaviorsFolders;
GfxData *_GfxData = New<GfxData>();

View file

@ -687,16 +687,12 @@ DataNode<Collision>* DynOS_Col_LoadFromBinary(const SysPath &aFilename, const ch
void DynOS_Col_Generate(const SysPath &aPackFolder, Array<Pair<u64, String>> _ActorsFolders, GfxData *_GfxData) {
for (auto &_ColNode : _GfxData->mCollisions) {
String _ColRootName = _ColNode->mName;
// If there is an existing binary file for this collision, skip and go to the next actor
SysPath _ColFilename = fstring("%s/%s.col", aPackFolder.c_str(), _ColRootName.begin());
if (fs_sys_file_exists(_ColFilename.c_str())) {
// Compress file to gain some space
if (configCompressOnStartup && !DynOS_Bin_IsCompressed(_ColFilename)) {
DynOS_Bin_Compress(_ColFilename);
}
// If there is an existing binary file for this collision, skip and go to the next collision
String _ActorFolder = DynOS_GetActorFolder(_ActorsFolders, _ColNode->mModelIdentifier);
SysPath _SrcFilename = fstring("%s/%s/collision.inc.c", aPackFolder.c_str(), _ActorFolder.begin());
if (DynOS_GenFileExistsAndIsNewerThanFile(_ColFilename, _SrcFilename)) {
continue;
}

View file

@ -1126,15 +1126,6 @@ static bool DynOS_Lvl_GeneratePack_Internal(const SysPath &aPackFolder, Array<Pa
if (_LvlRootName.Find("_entry") == -1) { continue; }
// If there is an existing binary file for this level, skip and go to the next level
SysPath _LvlFilename = fstring("%s/%s.lvl", aPackFolder.c_str(), _LvlRootName.begin());
if (fs_sys_file_exists(_LvlFilename.c_str())) {
// Compress file to gain some space
if (configCompressOnStartup && !DynOS_Bin_IsCompressed(_LvlFilename)) {
DynOS_Bin_Compress(_LvlFilename);
}
continue;
}
// Init
_GfxData->mLoadIndex = 0;
@ -1241,6 +1232,11 @@ static void DynOS_Lvl_GeneratePack_Recursive(const SysPath &directory, GfxData *
void DynOS_Lvl_GeneratePack(const SysPath &aPackFolder) {
Print("Processing levels: \"%s\"", aPackFolder.c_str());
if (!DynOS_ShouldGeneratePack(aPackFolder, { ".lvl" })) {
return;
}
Array<Pair<u64, String>> _ActorsFolders;
GfxData *_GfxData = New<GfxData>();
@ -1256,8 +1252,9 @@ void DynOS_Lvl_GeneratePack(const SysPath &aPackFolder) {
if (SysPath(_PackEnt->d_name) == "..") continue;
// Compress .lvl files to gain some space
bool _IsLvl = (SysPath(_PackEnt->d_name).find(".lvl") != SysPath::npos);
SysPath _Filename = fstring("%s/%s", aPackFolder.c_str(), _PackEnt->d_name);
if (SysPath(_PackEnt->d_name).find(".lvl") != SysPath::npos && !DynOS_Bin_IsCompressed(_Filename)) {
if (_IsLvl && !DynOS_Bin_IsCompressed(_Filename)) {
if (configCompressOnStartup) { DynOS_Bin_Compress(_Filename); }
continue;
}
@ -1266,12 +1263,20 @@ void DynOS_Lvl_GeneratePack(const SysPath &aPackFolder) {
SysPath _Folder = fstring("%s/%s", aPackFolder.c_str(), _PackEnt->d_name);
if (!fs_sys_dir_exists(_Folder.c_str())) continue;
// Only parse folders with a 'script.c'
SysPath _ScriptFile = fstring("%s/script.c", _Folder.c_str());
if (!fs_sys_file_exists(_ScriptFile.c_str())) {
_ScriptFile = fstring("%s/custom.script.c", _Folder.c_str());
if (!fs_sys_file_exists(_ScriptFile.c_str())) {
continue;
}
}
// Prevent generating from folders that likely already generated
SysPath _LvlFile = fstring("%s/level_%s_entry.lvl", aPackFolder.c_str(), _PackEnt->d_name);
if (fs_sys_file_exists(_LvlFile.c_str())) continue;
// Only parse folders with a 'script.c'
if (!fs_sys_file_exists(fstring("%s/script.c", _Folder.c_str()).c_str()) && !fs_sys_file_exists(fstring("%s/custom.script.c", _Folder.c_str()).c_str())) continue;
if (DynOS_GenFileExistsAndIsNewerThanFolder(_LvlFile, _Folder)) {
continue;
}
_GfxData->mModelIdentifier++;
DynOS_Lvl_GeneratePack_Recursive(_Folder, _GfxData);

View file

@ -409,7 +409,6 @@ static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath
continue;
}
size_t nameLen = strlen(_PackEnt->d_name);
if (nameLen < 4) continue;
@ -418,13 +417,6 @@ static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath
continue;
}
// skip files that have already been generated
char buffer[SYS_MAX_PATH];
snprintf(buffer, SYS_MAX_PATH, "%s.tex", _Path.substr(0, _Path.size() - 4).c_str());
if (fs_sys_file_exists(buffer)) {
continue;
}
// read the file
aGfxData->mModelIdentifier++;
TexData* _TexData = LoadTextureFromFile(aGfxData, _Path.c_str());
@ -460,6 +452,12 @@ static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath
SysPath _OutputPath = fstring("%s/%s.tex", aOutputFolder.c_str(), _BaseName.begin());
// skip files that have already been generated
if (DynOS_GenFileExistsAndIsNewerThanFile(_OutputPath, _Path)) {
Delete<TexData>(_TexData);
continue;
}
// create output dir if it doesn't exist
if (!fs_sys_dir_exists(aOutputFolder.c_str())) {
fs_sys_mkdir(aOutputFolder.c_str());
@ -477,6 +475,10 @@ static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath
void DynOS_Tex_GeneratePack(const SysPath &aPackFolder, SysPath &aOutputFolder, bool aAllowCustomTextures) {
Print("Processing textures: \"%s\"", aPackFolder.c_str());
if (!DynOS_ShouldGeneratePack2Ext(aPackFolder, ".tex", ".png")) {
return;
}
GfxData *_GfxData = New<GfxData>();
_GfxData->mModelIdentifier = 0;
SysPath _Empty = "";

View file

@ -1,5 +1,166 @@
#include "dynos.cpp.h"
////////////////////////////////
// Should-generate-pack logic //
////////////////////////////////
static bool DynOS_PathHasExtension(const char *aPath, const char *aExtension) {
size_t _LenStr = strlen(aPath);
size_t _LenSuffix = strlen(aExtension);
if (_LenSuffix > _LenStr) {
return false;
}
return strcmp(aPath + (_LenStr - _LenSuffix), aExtension) == 0;
}
static bool DynOS_PathHasExtensions(const char *aPath, std::initializer_list<const char*> aExtensions) {
for (auto _Ext : aExtensions) {
if (DynOS_PathHasExtension(aPath, _Ext)) {
return true;
}
}
return false;
}
static void DynOS_GetMTimeInFolderSplitByExtensions(const SysPath &aPath, std::initializer_list<const char*> aExtensions, u64 *aLatestMTimeExt, u64 *aLatestMTimeNonExt) {
DIR *_DirPath = opendir(aPath.c_str());
if (_DirPath) {
struct dirent *_DirEnt = NULL;
while ((_DirEnt = readdir(_DirPath)) != NULL) {
// Skip . and ..
if (SysPath(_DirEnt->d_name) == ".") { continue; }
if (SysPath(_DirEnt->d_name) == "..") { continue; }
SysPath _Path = fstring("%s/%s", aPath.c_str(), _DirEnt->d_name);
// Recursively accumulate maximum mtimes
if (fs_sys_dir_exists(_Path.c_str())) {
DynOS_GetMTimeInFolderSplitByExtensions(_Path, aExtensions, aLatestMTimeExt, aLatestMTimeNonExt);
continue;
}
// Accumulate max mtime in the correct slot
u64 _PathMTime = fs_sys_get_modified_time(_Path.c_str());
if (DynOS_PathHasExtensions(_Path.c_str(), aExtensions)) {
*aLatestMTimeExt = MAX(*aLatestMTimeExt, _PathMTime);
} else {
*aLatestMTimeNonExt = MAX(*aLatestMTimeNonExt, _PathMTime);
}
}
closedir(_DirPath);
}
}
static void DynOS_GetMTimeInFolderSplitBy2Extensions(const SysPath &aPath, const char * aExt1, const char *aExt2, u64 *aLatestMTimeExt1, u64 *aLatestMTimeExt2) {
DIR *_DirPath = opendir(aPath.c_str());
if (_DirPath) {
struct dirent *_DirEnt = NULL;
while ((_DirEnt = readdir(_DirPath)) != NULL) {
// Skip . and ..
if (SysPath(_DirEnt->d_name) == ".") { continue; }
if (SysPath(_DirEnt->d_name) == "..") { continue; }
SysPath _Path = fstring("%s/%s", aPath.c_str(), _DirEnt->d_name);
// Recursively accumulate maximum mtimes
if (fs_sys_dir_exists(_Path.c_str())) {
DynOS_GetMTimeInFolderSplitBy2Extensions(_Path, aExt1, aExt2, aLatestMTimeExt1, aLatestMTimeExt2);
continue;
}
// Accumulate max mtime in the correct slot
u64 _PathMTime = fs_sys_get_modified_time(_Path.c_str());
if (DynOS_PathHasExtension(_Path.c_str(), aExt1)) {
*aLatestMTimeExt1 = MAX(*aLatestMTimeExt1, _PathMTime);
} else if (DynOS_PathHasExtension(_Path.c_str(), aExt2)) {
*aLatestMTimeExt2 = MAX(*aLatestMTimeExt2, _PathMTime);
}
}
closedir(_DirPath);
}
}
static u64 DynOS_GetMTimeInFolder(const SysPath &aPath) {
u64 _LatestMTimeSubDir = 0;
DIR *_DirPath = opendir(aPath.c_str());
if (_DirPath) {
struct dirent *_DirEnt = NULL;
while ((_DirEnt = readdir(_DirPath)) != NULL) {
// Skip . and ..
if (SysPath(_DirEnt->d_name) == ".") { continue; }
if (SysPath(_DirEnt->d_name) == "..") { continue; }
// Check get the mtime of the file and store the max mtime
SysPath _Path = fstring("%s/%s", aPath.c_str(), _DirEnt->d_name);
if (fs_sys_dir_exists(_Path.c_str())) {
u64 _PathMTime = DynOS_GetMTimeInFolder(_Path);
_LatestMTimeSubDir = MAX(_LatestMTimeSubDir, _PathMTime);
} else {
u64 _PathMTime = fs_sys_get_modified_time(_Path.c_str());
_LatestMTimeSubDir = MAX(_LatestMTimeSubDir, _PathMTime);
}
}
closedir(_DirPath);
}
return _LatestMTimeSubDir;
}
bool DynOS_ShouldGeneratePack(const SysPath &aPackFolder, std::initializer_list<const char*> aExtensions) {
u64 _LatestMTimeExt = 0;
u64 _LatestMTimeNonExt = 0;
DynOS_GetMTimeInFolderSplitByExtensions(aPackFolder, aExtensions, &_LatestMTimeExt, &_LatestMTimeNonExt);
return _LatestMTimeExt < _LatestMTimeNonExt;
}
bool DynOS_ShouldGeneratePack2Ext(const SysPath &aPackFolder, const char *aGenExtension, const char *aSrcExtension) {
u64 _LatestMTimeGenExt = 0;
u64 _LatestMTimeSrcExt = 0;
DynOS_GetMTimeInFolderSplitBy2Extensions(aPackFolder, aGenExtension, aSrcExtension, &_LatestMTimeGenExt, &_LatestMTimeSrcExt);
return _LatestMTimeGenExt < _LatestMTimeSrcExt;
}
bool DynOS_GenFileExistsAndIsNewerThanFile(const SysPath &aGenFile, const SysPath &aSrcFile) {
if (fs_sys_file_exists(aGenFile.c_str())) {
// compare modified times
u64 _MTimeGenFile = fs_sys_get_modified_time(aGenFile.c_str());
u64 _MTimeSourceFile = fs_sys_get_modified_time(aSrcFile.c_str());
return (_MTimeGenFile >= _MTimeSourceFile);
}
return false;
}
bool DynOS_GenFileExistsAndIsNewerThanFolder(const SysPath &aGenFile, const SysPath &aSrcFolder) {
if (fs_sys_file_exists(aGenFile.c_str())) {
// compare modified times
u64 _MTimeGenFile = fs_sys_get_modified_time(aGenFile.c_str());
u64 _MTimeSourceFolder = DynOS_GetMTimeInFolder(aSrcFolder);
return (_MTimeGenFile >= _MTimeSourceFolder);
}
return false;
}
String DynOS_GetActorFolder(const Array<Pair<u64, String>> &aActorsFolders, u64 aModelIdentifier) {
for (const auto &_Pair : aActorsFolders) {
if (_Pair.first == aModelIdentifier) {
return _Pair.second;
}
}
return String();
}
//////////
// Misc //
//////////

View file

@ -346,6 +346,31 @@ bool fs_sys_dir_is_empty(const char *name) {
return ret;
}
uint64_t fs_sys_get_modified_time(const char *path) {
#ifdef _WIN32
WIN32_FILE_ATTRIBUTE_DATA fad;
// get attributes, return 0 on error
if (!GetFileAttributesExA(path, GetFileExInfoStandard, &fad)) { return 0; }
// filetime is 100-ns intervals since 1601-01-01 UTC
ULARGE_INTEGER ull;
ull.LowPart = fad.ftLastWriteTime.dwLowDateTime;
ull.HighPart = fad.ftLastWriteTime.dwHighDateTime;
// 100-ns from 1601 to 1970
const uint64_t EPOCH_DIFF = 116444736000000000ULL;
uint64_t time100ns = ull.QuadPart;
// convert to seconds
return (time100ns - EPOCH_DIFF) / 10000000ULL;
#else
struct stat st;
// get stat, return 0 on error
if (stat(path, &st) != 0) { return 0; }
return (uint64_t)st.st_mtime;
#endif
}
bool fs_sys_walk(const char *base, walk_fn_t walk, void *user, const bool recur) {
#ifdef DOCKERBUILD
return false;

View file

@ -108,5 +108,6 @@ bool fs_sys_dir_exists(const char *name);
bool fs_sys_dir_is_empty(const char *name);
bool fs_sys_mkdir(const char *name); // creates with 0777 by default
bool fs_sys_rmdir(const char *name); // removes an empty directory
uint64_t fs_sys_get_modified_time(const char *path);
#endif // _SM64_FS_H_

View file

@ -443,7 +443,7 @@ void smlua_live_reload_update(lua_State* L) {
struct ModFile* file = &mod->files[j];
// check modified time
u64 timestamp = mod_get_file_mtime_seconds(file);
u64 timestamp = fs_sys_get_modified_time(file->cachedPath);
if (timestamp <= file->modifiedTimestamp) { continue; }
// update modified time and reload the module

View file

@ -17,31 +17,6 @@
#include <sys/stat.h>
#endif
u64 mod_get_file_mtime_seconds(struct ModFile* file) {
#ifdef _WIN32
WIN32_FILE_ATTRIBUTE_DATA fad;
if (!GetFileAttributesExA(file->cachedPath, GetFileExInfoStandard, &fad)) {
// error; you could also GetLastError() here
return 0;
}
// FILETIME is 100-ns intervals since 1601-01-01 UTC
ULARGE_INTEGER ull;
ull.LowPart = fad.ftLastWriteTime.dwLowDateTime;
ull.HighPart = fad.ftLastWriteTime.dwHighDateTime;
const u64 EPOCH_DIFF = 116444736000000000ULL; // 100-ns from 1601 to 1970
u64 time100ns = ull.QuadPart;
return (time100ns - EPOCH_DIFF) / 10000000ULL; // to seconds
#else
struct stat st;
if (stat(file->cachedPath, &st) != 0) {
// error; errno is set
return 0;
}
return (u64)st.st_mtime;
#endif
}
size_t mod_get_lua_size(struct Mod* mod) {
if (!mod) { return 0; }
size_t size = 0;
@ -178,7 +153,7 @@ void mod_activate(struct Mod* mod) {
// activate dynos models
for (int i = 0; i < mod->fileCount; i++) {
struct ModFile* file = &mod->files[i];
file->modifiedTimestamp = mod_get_file_mtime_seconds(file);
file->modifiedTimestamp = fs_sys_get_modified_time(file->cachedPath);
mod_cache_add(mod, file, false);
// forcefully update md5 hash

View file

@ -46,7 +46,6 @@ struct Mod {
u8 customBehaviorIndex;
};
u64 mod_get_file_mtime_seconds(struct ModFile* file);
size_t mod_get_lua_size(struct Mod* mod);
void mod_activate(struct Mod* mod);
void mod_clear(struct Mod* mod);