mirror of
https://github.com/coop-deluxe/sm64coopdx.git
synced 2025-10-30 08:01:01 +00:00
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>
487 lines
16 KiB
C++
487 lines
16 KiB
C++
#include "dynos.cpp.h"
|
|
extern "C" {
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#include "stb/stb_image_write.h"
|
|
}
|
|
|
|
///////////
|
|
// Utils //
|
|
///////////
|
|
|
|
static bool FileTypeExists(SysPath& aFolder, const char* fileType) {
|
|
DIR *_Dir = opendir(aFolder.c_str());
|
|
if (!_Dir) { return false; }
|
|
|
|
int fileTypeLen = strlen(fileType);
|
|
|
|
struct dirent *_Ent = NULL;
|
|
while ((_Ent = readdir(_Dir)) != NULL) {
|
|
int nameLen = strlen(_Ent->d_name);
|
|
if (nameLen > fileTypeLen && !strcmp(&_Ent->d_name[nameLen - fileTypeLen], fileType)) {
|
|
closedir(_Dir);
|
|
return true;
|
|
}
|
|
}
|
|
closedir(_Dir);
|
|
return false;
|
|
}
|
|
|
|
static TexData* LoadTextureFromFile(GfxData *aGfxData, const char* aFile) {
|
|
// Image file
|
|
SysPath _Filename;
|
|
int fileNameLen = strlen(aFile);
|
|
if (aGfxData->mPackFolder.length() == 0) {
|
|
_Filename = aFile;
|
|
} else if (fileNameLen > 4 && !strcmp(&aFile[fileNameLen - 4], ".png")) {
|
|
_Filename = fstring("%s/%s", aGfxData->mPackFolder.c_str(), aFile);
|
|
} else {
|
|
_Filename = fstring("%s/%s.png", aGfxData->mPackFolder.c_str(), aFile);
|
|
}
|
|
|
|
FILE *_File = fopen(_Filename.c_str(), "rb");
|
|
|
|
// Check as if we're an Actor.
|
|
if (!_File) {
|
|
SysPath _ActorFilename = "";
|
|
const char* _SubString = strchr(aFile, '/'); // Remove the "actors/"
|
|
if (_SubString && *_SubString) {
|
|
_SubString++;
|
|
_ActorFilename = fstring("%s/%s.png", aGfxData->mPackFolder.c_str(), _SubString);
|
|
_File = fopen(_ActorFilename.c_str(), "rb");
|
|
}
|
|
|
|
// The file does not exist in either spot!
|
|
if (!_File) {
|
|
PrintDataError(" ERROR: Unable to open file at \"%s\" or \"%s\"", _Filename.c_str(), _ActorFilename.c_str());
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Texture data
|
|
fseek(_File, 0, SEEK_END);
|
|
TexData* _Texture = New<TexData>();
|
|
_Texture->mPngData.Resize(ftell(_File)); rewind(_File);
|
|
fread(_Texture->mPngData.begin(), sizeof(u8), _Texture->mPngData.Count(), _File);
|
|
fclose(_File);
|
|
return _Texture;
|
|
}
|
|
|
|
void DynOS_Tex_ConvertTextureDataToPng(GfxData *aGfxData, TexData* aTexture) {
|
|
|
|
// Convert to RGBA32
|
|
const u8 *_Palette = (aGfxData->mGfxContext.mCurrentPalette ? aGfxData->mGfxContext.mCurrentPalette->mData->mRawData.begin() : NULL);
|
|
u8 *_Buffer = DynOS_Tex_ConvertToRGBA32(aTexture->mRawData.begin(), aTexture->mRawData.Count(), aTexture->mRawFormat, aTexture->mRawSize, _Palette);
|
|
if (_Buffer == NULL) {
|
|
PrintDataError(" ERROR: Unknown texture format");
|
|
return;
|
|
}
|
|
|
|
// Convert to PNG
|
|
s32 _PngLength = 0;
|
|
u8 *_PngData = stbi_write_png_to_mem(_Buffer, 0, aTexture->mRawWidth, aTexture->mRawHeight, 4, &_PngLength);
|
|
if (!_PngData || !_PngLength) {
|
|
PrintDataError(" ERROR: Cannot convert texture to PNG");
|
|
return;
|
|
}
|
|
|
|
aTexture->mPngData = Array<u8>(_PngData, _PngData + _PngLength);
|
|
Delete(_PngData);
|
|
}
|
|
|
|
/////////////
|
|
// Parsing //
|
|
/////////////
|
|
|
|
DataNode<TexData>* DynOS_Tex_Parse(GfxData* aGfxData, DataNode<TexData>* aNode) {
|
|
if (aNode->mData) return aNode;
|
|
|
|
// Check tokens Count
|
|
if (aNode->mTokens.Count() < 1) {
|
|
PrintDataError(" ERROR: %s: not enough data", aNode->mName.begin());
|
|
return aNode;
|
|
}
|
|
|
|
// #include"[texture].inc.c"
|
|
s32 i0 = aNode->mTokens[0].Find("#include");
|
|
if (i0 != -1) {
|
|
s32 i1 = aNode->mTokens[0].Find(".inc.c");
|
|
if (i1 == -1) {
|
|
if (strstr(aNode->mName.begin(), "_pal_") == NULL) {
|
|
PrintDataError(" ERROR: %s: missing .inc.c in String %s", aNode->mName.begin(), aNode->mTokens[0].begin());
|
|
} else {
|
|
// hack for pal textures to be "found"
|
|
TexData* _Texture = New<TexData>();
|
|
aNode->mData = _Texture;
|
|
aNode->mLoadIndex = aGfxData->mLoadIndex++;
|
|
}
|
|
return aNode;
|
|
}
|
|
|
|
// Filename
|
|
String _Filename = aNode->mTokens[0].SubString(i0 + 9, i1 - i0 - 9);
|
|
aNode->mData = LoadTextureFromFile(aGfxData, _Filename.begin());
|
|
aNode->mLoadIndex = aGfxData->mLoadIndex++;
|
|
return aNode;
|
|
}
|
|
|
|
// double quoted String
|
|
s32 dq0 = aNode->mTokens[0].Find('\"');
|
|
if (dq0 != -1) {
|
|
s32 dq1 = aNode->mTokens[0].Find('\"', dq0 + 1);
|
|
if (dq1 == -1) {
|
|
PrintDataError(" ERROR: %s: missing second quote in String %s", aNode->mName.begin(), aNode->mTokens[0].begin());
|
|
return aNode;
|
|
}
|
|
|
|
// Filename
|
|
String _Filename = aNode->mTokens[0].SubString(dq0 + 1, dq1 - dq0 - 1);
|
|
aNode->mData = LoadTextureFromFile(aGfxData, _Filename.begin());
|
|
aNode->mLoadIndex = aGfxData->mLoadIndex++;
|
|
return aNode;
|
|
}
|
|
|
|
// Stream of bytes
|
|
aNode->mData = New<TexData>();
|
|
aNode->mData->mRawWidth = -1; // Unknown for now, will be set later
|
|
aNode->mData->mRawHeight = -1; // Unknown for now, will be set later
|
|
aNode->mData->mRawFormat = -1; // Unknown for now, will be set later
|
|
aNode->mData->mRawSize = -1; // Unknown for now, will be set later
|
|
aNode->mData->mRawData.Resize(aNode->mTokens.Count());
|
|
for (u64 j = 0; j != aNode->mTokens.Count(); ++j) {
|
|
aNode->mData->mRawData[j] = aNode->mTokens[j].ParseInt();
|
|
}
|
|
aNode->mLoadIndex = aGfxData->mLoadIndex++;
|
|
return aNode;
|
|
}
|
|
|
|
/////////////
|
|
// Writing //
|
|
/////////////
|
|
|
|
void DynOS_Tex_Write(BinFile* aFile, GfxData* aGfxData, DataNode<TexData> *aNode) {
|
|
if (!aNode->mData) return;
|
|
|
|
// Header
|
|
aFile->Write<u8>(DATA_TYPE_TEXTURE);
|
|
aNode->mName.Write(aFile);
|
|
|
|
// Data
|
|
// Look for texture duplicates
|
|
// If that's the case, store the name of the texture node instead of the whole PNG data
|
|
// (Don't bother to look for duplicates if there is no data to write)
|
|
if (!aNode->mData->mPngData.Empty()) {
|
|
for (const auto& _Node : aGfxData->mTextures) {
|
|
if (_Node->mLoadIndex < aNode->mLoadIndex && // Check load order: duplicates should reference only an already loaded node
|
|
_Node->mData != NULL && // Check node data
|
|
aNode->mData->mPngData.Count() == _Node->mData->mPngData.Count() && // Check PNG data lengths
|
|
memcmp(aNode->mData->mPngData.begin(), _Node->mData->mPngData.begin(), aNode->mData->mPngData.Count()) == 0) // Check PNG data content
|
|
{
|
|
aFile->Write<u32>(TEX_REF_CODE);
|
|
_Node->mName.Write(aFile);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
aNode->mData->mPngData.Write(aFile);
|
|
}
|
|
|
|
static bool DynOS_Tex_WriteBinary(GfxData* aGfxData, const SysPath &aOutputFilename, String& aName, TexData* aTexData, bool aRawTexture) {
|
|
BinFile *_File = BinFile::OpenW(aOutputFilename.c_str());
|
|
if (!_File) {
|
|
PrintDataError(" ERROR: Unable to create file \"%s\"", aOutputFilename.c_str());
|
|
return false;
|
|
}
|
|
|
|
if (!aRawTexture) {
|
|
// Write png-texture
|
|
|
|
// Header
|
|
_File->Write<u8>(DATA_TYPE_TEXTURE);
|
|
aName.Write(_File);
|
|
|
|
// Data
|
|
aTexData->mPngData.Write(_File);
|
|
|
|
BinFile::Close(_File);
|
|
return true;
|
|
}
|
|
|
|
// Write raw-texture
|
|
|
|
// Header
|
|
_File->Write<u8>(DATA_TYPE_TEXTURE_RAW);
|
|
aName.Write(_File);
|
|
|
|
// load
|
|
u8 *_RawData = stbi_load_from_memory(aTexData->mPngData.begin(), aTexData->mPngData.Count(), &aTexData->mRawWidth, &aTexData->mRawHeight, NULL, 4);
|
|
aTexData->mRawFormat = G_IM_FMT_RGBA;
|
|
aTexData->mRawSize = G_IM_SIZ_32b;
|
|
aTexData->mRawData = Array<u8>(_RawData, _RawData + (aTexData->mRawWidth * aTexData->mRawHeight * 4));
|
|
free(_RawData);
|
|
|
|
// Data
|
|
_File->Write<s32>(aTexData->mRawFormat);
|
|
_File->Write<s32>(aTexData->mRawSize);
|
|
_File->Write<s32>(aTexData->mRawWidth);
|
|
_File->Write<s32>(aTexData->mRawHeight);
|
|
aTexData->mRawData.Write(_File);
|
|
|
|
BinFile::Close(_File);
|
|
return true;
|
|
}
|
|
|
|
/////////////
|
|
// Reading //
|
|
/////////////
|
|
|
|
DataNode<TexData>* DynOS_Tex_Load(BinFile *aFile, GfxData *aGfxData) {
|
|
if (!aFile || !aGfxData) { return NULL; }
|
|
|
|
DataNode<TexData> *_Node = New<DataNode<TexData>>();
|
|
|
|
// Name
|
|
_Node->mName.Read(aFile);
|
|
|
|
// Data
|
|
_Node->mData = New<TexData>();
|
|
_Node->mData->mUploaded = false;
|
|
|
|
// Check for the texture ref magic
|
|
s32 _FileOffset = aFile->Offset();
|
|
u32 _TexRefCode = aFile->Read<u32>();
|
|
if (_TexRefCode == TEX_REF_CODE) {
|
|
|
|
// That's a duplicate, find the original node and copy its content
|
|
String _NodeName; _NodeName.Read(aFile);
|
|
for (const auto& _LoadedNode : aGfxData->mTextures) {
|
|
if (_LoadedNode->mName == _NodeName) {
|
|
_Node->mData->mPngData = _LoadedNode->mData->mPngData;
|
|
_Node->mData->mRawData = _LoadedNode->mData->mRawData;
|
|
_Node->mData->mRawWidth = _LoadedNode->mData->mRawWidth;
|
|
_Node->mData->mRawHeight = _LoadedNode->mData->mRawHeight;
|
|
_Node->mData->mRawFormat = _LoadedNode->mData->mRawFormat;
|
|
_Node->mData->mRawSize = _LoadedNode->mData->mRawSize;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
aFile->SetOffset(_FileOffset);
|
|
_Node->mData->mPngData.Read(aFile);
|
|
if (!_Node->mData->mPngData.Empty()) {
|
|
u8 *_RawData = stbi_load_from_memory(_Node->mData->mPngData.begin(), _Node->mData->mPngData.Count(), &_Node->mData->mRawWidth, &_Node->mData->mRawHeight, NULL, 4);
|
|
_Node->mData->mRawFormat = G_IM_FMT_RGBA;
|
|
_Node->mData->mRawSize = G_IM_SIZ_32b;
|
|
_Node->mData->mRawData = Array<u8>(_RawData, _RawData + (_Node->mData->mRawWidth * _Node->mData->mRawHeight * 4));
|
|
free(_RawData);
|
|
} else { // Probably a palette
|
|
_Node->mData->mRawData = Array<u8>();
|
|
_Node->mData->mRawWidth = 0;
|
|
_Node->mData->mRawHeight = 0;
|
|
_Node->mData->mRawFormat = 0;
|
|
_Node->mData->mRawSize = 0;
|
|
}
|
|
}
|
|
|
|
// Append
|
|
if (aGfxData) {
|
|
aGfxData->mTextures.Add(_Node);
|
|
}
|
|
|
|
return _Node;
|
|
}
|
|
|
|
DataNode<TexData>* DynOS_Tex_LoadFromBinary(const SysPath &aPackFolder, const SysPath &aFilename, const char *aTexName, bool aAddToPack) {
|
|
// Look for pack in cache
|
|
PackData* _Pack = DynOS_Pack_GetFromPath(aPackFolder);
|
|
|
|
// Look for tex in pack
|
|
if (_Pack) {
|
|
auto _Tex = DynOS_Pack_GetTex(_Pack, aTexName);
|
|
if (_Tex != NULL) {
|
|
return _Tex;
|
|
}
|
|
}
|
|
|
|
// Load data from binary file
|
|
DataNode<TexData>* _TexNode = NULL;
|
|
BinFile *_File = BinFile::OpenR(aFilename.c_str());
|
|
if (!_File) { return NULL; }
|
|
|
|
u8 type = _File->Read<u8>();
|
|
if (type == DATA_TYPE_TEXTURE) {
|
|
// load png-texture
|
|
_TexNode = New<DataNode<TexData>>();
|
|
_TexNode->mData = New<TexData>();
|
|
|
|
_TexNode->mName.Read(_File);
|
|
_TexNode->mData->mPngData.Read(_File);
|
|
BinFile::Close(_File);
|
|
|
|
if (aAddToPack) {
|
|
if (!_Pack) { _Pack = DynOS_Pack_Add(aPackFolder); }
|
|
DynOS_Pack_AddTex(_Pack, _TexNode);
|
|
}
|
|
|
|
return _TexNode;
|
|
} else if (type != DATA_TYPE_TEXTURE_RAW) {
|
|
BinFile::Close(_File);
|
|
return NULL;
|
|
}
|
|
|
|
// load raw-texture
|
|
_TexNode = New<DataNode<TexData>>();
|
|
_TexNode->mData = New<TexData>();
|
|
|
|
_TexNode->mName.Read(_File);
|
|
_TexNode->mData->mRawFormat = _File->Read<s32>();
|
|
_TexNode->mData->mRawSize = _File->Read<s32>();
|
|
_TexNode->mData->mRawWidth = _File->Read<s32>();
|
|
_TexNode->mData->mRawHeight = _File->Read<s32>();
|
|
_TexNode->mData->mRawData.Read(_File);
|
|
|
|
BinFile::Close(_File);
|
|
|
|
if (aAddToPack) {
|
|
if (!_Pack) { _Pack = DynOS_Pack_Add(aPackFolder); }
|
|
DynOS_Pack_AddTex(_Pack, _TexNode);
|
|
}
|
|
|
|
return _TexNode;
|
|
}
|
|
|
|
//////////////
|
|
// Generate //
|
|
//////////////
|
|
|
|
static bool is_level_number_png(SysPath& aPath) {
|
|
// normalize
|
|
String path = aPath.c_str();
|
|
char* p = path.begin();
|
|
while (*p != '\0') {
|
|
if (*p == '\\') {
|
|
*p = '/';
|
|
}
|
|
break;
|
|
}
|
|
p = path.begin();
|
|
|
|
// compare 'levels/'
|
|
s16 levelsLength = strlen("levels/");
|
|
if (strncmp(p, "levels/", levelsLength)) {
|
|
return false;
|
|
}
|
|
|
|
// skip past level name
|
|
p += levelsLength;
|
|
while (*p != '\0') {
|
|
if (*p == '/') { break; }
|
|
p++;
|
|
}
|
|
if (*p != '/') { return false; }
|
|
p++;
|
|
|
|
return (*p >= '0' && *p <= '9');
|
|
}
|
|
|
|
static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath &aOutputFolder, SysPath& aRelativePath, SysPath& aPrefix, GfxData *aGfxData, bool aAllowCustomTextures) {
|
|
SysPath _DirPath = fstring("%s/%s", aPackFolder.c_str(), aRelativePath.c_str());
|
|
|
|
// skip generation if any .c files exist, and it isn't levels/xxx/NUMBER
|
|
bool containsC = FileTypeExists(_DirPath, ".c");
|
|
|
|
DIR *_PackDir = opendir(_DirPath.c_str());
|
|
if (!_PackDir) { return; }
|
|
|
|
struct dirent *_PackEnt = NULL;
|
|
while ((_PackEnt = readdir(_PackDir)) != NULL) {
|
|
|
|
// Skip . and ..
|
|
if (SysPath(_PackEnt->d_name) == ".") continue;
|
|
if (SysPath(_PackEnt->d_name) == "..") continue;
|
|
|
|
SysPath _Path = fstring("%s%s", _DirPath.c_str(), _PackEnt->d_name);
|
|
|
|
// Recurse through subfolders
|
|
if (fs_sys_dir_exists(_Path.c_str())) {
|
|
SysPath _NextPath = fstring("%s%s/", aRelativePath.c_str(), _PackEnt->d_name);
|
|
SysPath _Prefix = fstring("%s.", _PackEnt->d_name);
|
|
DynOS_Tex_GeneratePack_Recursive(aPackFolder, aOutputFolder, _NextPath, _Prefix, aGfxData, aAllowCustomTextures);
|
|
continue;
|
|
}
|
|
|
|
size_t nameLen = strlen(_PackEnt->d_name);
|
|
if (nameLen < 4) continue;
|
|
|
|
// skip files that don't end in '.png'
|
|
if (strcmp(&_PackEnt->d_name[nameLen - 4], ".png")) {
|
|
continue;
|
|
}
|
|
|
|
// read the file
|
|
aGfxData->mModelIdentifier++;
|
|
TexData* _TexData = LoadTextureFromFile(aGfxData, _Path.c_str());
|
|
if (_TexData == NULL) {
|
|
PrintDataError("Error reading texture from file: %s", _Path.c_str());
|
|
continue;
|
|
}
|
|
|
|
SysPath _RelativePath = fstring("%s%s", aRelativePath.c_str(), _PackEnt->d_name);
|
|
if (containsC && !is_level_number_png(_RelativePath)) {
|
|
// Don't forgot to free the texture data we've read.
|
|
Delete<TexData>(_TexData);
|
|
continue;
|
|
}
|
|
|
|
// write the file
|
|
String _BaseName;
|
|
const char* _OverrideName = DynOS_Builtin_Tex_GetNameFromFileName(_RelativePath.c_str());
|
|
if (_OverrideName) {
|
|
_BaseName = _OverrideName;
|
|
} else {
|
|
_BaseName = _PackEnt->d_name;
|
|
_BaseName = _BaseName.SubString(0, nameLen - 4);
|
|
}
|
|
|
|
// if we aren't overriding a texture, only generate textures in the output directory
|
|
SysPath _OutputFolder = fstring("%s/", aOutputFolder.c_str());
|
|
if (_OverrideName == NULL && (!aAllowCustomTextures || strcmp(_DirPath.c_str(), _OutputFolder.c_str()))) {
|
|
// Don't forgot to free the texture data we've read.
|
|
Delete<TexData>(_TexData);
|
|
continue;
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
DynOS_Tex_WriteBinary(aGfxData, _OutputPath, _BaseName, _TexData, (_OverrideName != NULL));
|
|
|
|
// Don't forgot to free the texture data we've read.
|
|
Delete<TexData>(_TexData);
|
|
}
|
|
|
|
closedir(_PackDir);
|
|
}
|
|
|
|
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 = "";
|
|
DynOS_Tex_GeneratePack_Recursive(aPackFolder, aOutputFolder, _Empty, _Empty, _GfxData, aAllowCustomTextures);
|
|
DynOS_Gfx_Free(_GfxData);
|
|
}
|