sm64coopdx/data/dynos_bin_tex.cpp

466 lines
15 KiB
C++
Raw Normal View History

2022-04-02 02:19:26 +02:00
#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) {
2022-04-02 02:19:26 +02:00
// Image file
SysPath _Filename;
int fileNameLen = strlen(aFile);
2022-05-08 01:39:19 +02:00
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);
}
2022-04-02 02:19:26 +02:00
FILE *_File = fopen(_Filename.c_str(), "rb");
// Check as if we're an Actor.
2022-04-02 02:19:26 +02:00
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) {
PrintError(" ERROR: Unable to open file at \"%s\" or \"%s\"", _Filename.c_str(), _ActorFilename.c_str());
return NULL;
}
2022-04-02 02:19:26 +02:00
}
// 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);
2022-04-02 02:19:26 +02:00
if (_Buffer == NULL) {
PrintError(" 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) {
PrintError(" 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) {
PrintError(" 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) {
PrintError(" ERROR: %s: missing .inc.c in String %s", aNode->mName.begin(), aNode->mTokens[0].begin());
return aNode;
}
// Filename
String _Filename = aNode->mTokens[0].SubString(i0 + 9, i1 - i0 - 9);
aNode->mData = LoadTextureFromFile(aGfxData, _Filename.begin());
2022-04-02 02:19:26 +02:00
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) {
PrintError(" 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());
2022-04-02 02:19:26 +02:00
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;
}
2022-04-02 04:50:42 +02:00
/////////////
// Writing //
/////////////
void DynOS_Tex_Write(FILE* aFile, GfxData* aGfxData, DataNode<TexData> *aNode) {
if (!aNode->mData) return;
// Header
WriteBytes<u8>(aFile, DATA_TYPE_TEXTURE);
aNode->mName.Write(aFile);
// Data
Fixed various audio bugs; DynOS can now detect texture duplicates to decrease generated bin files size (#110) Fixed the following audio bugs: Bug: Rom-hacks sequences don't seem to be affected by volume scaling and muting Fix: Force the BGM sequences to follow the vanilla behavior: Volume can't go higher than default volume Volume is reduced to 31% when the game is paused Audio is stopped when the game is paused outside the Castle levels Bug: (Pointed out by Draco) Mario's voice clips are not replaced by the player's character's in the following instances: fall to death barrier, "here we go" in the ending cutscene, "let's a go" after selecting a star, "okey dokey" after starting the game. Fix: The first two ones now call play_character_sound(m, CHAR_SOUND_...) instead of play_sound(SOUND_MARIO_..., pos). The last two ones couldn't be fixed the same way for two reasons: First, the corresponding sounds were not referenced in the sound table, second, the sound played is always cut-off after a few frames (due to how sm64 resets the sound banks after loading a level). Added SOUND_*_LETS_A_GO and SOUND_*_OKEY_DOKEY sounds for each playable character as Bass samples. Character Bass sounds work the same way as vanilla sounds (i.e. can be played with play_character_sound), but they cannot be prematurely stopped by sm64 sound banks shenanigans. This fixes the cut-off for both the star select and the castle grounds entry, plays the sound corresponding to the player's character, and doesn't need to extend or edit the sound table. DynOS can detect texture duplicates when generating a bin or lvl file. When a duplicate is detected, the name of the original texture node is written instead of the whole PNG data, decreasing significantly the resulting file size.
2022-05-20 01:40:45 +02:00
// 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
{
WriteBytes<u32>(aFile, TEX_REF_CODE);
_Node->mName.Write(aFile);
return;
}
}
}
2022-04-02 04:50:42 +02:00
aNode->mData->mPngData.Write(aFile);
}
static bool DynOS_Tex_WriteBinary(GfxData* aGfxData, const SysPath &aOutputFilename, String& aName, TexData* aTexData, bool aRawTexture) {
FILE *_File = fopen(aOutputFilename.c_str(), "wb");
if (!_File) {
PrintError(" ERROR: Unable to create file \"%s\"", aOutputFilename.c_str());
return false;
}
if (!aRawTexture) {
// Write png-texture
// Header
WriteBytes<u8>(_File, DATA_TYPE_TEXTURE);
aName.Write(_File);
// Data
aTexData->mPngData.Write(_File);
fclose(_File);
return true;
}
// Write raw-texture
// Header
WriteBytes<u8>(_File, 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
WriteBytes<s32>(_File, aTexData->mRawFormat);
WriteBytes<s32>(_File, aTexData->mRawSize);
WriteBytes<s32>(_File, aTexData->mRawWidth);
WriteBytes<s32>(_File, aTexData->mRawHeight);
aTexData->mRawData.Write(_File);
fclose(_File);
return true;
}
2022-04-02 04:50:42 +02:00
/////////////
// Reading //
/////////////
DataNode<TexData>* DynOS_Tex_Load(FILE *aFile, GfxData *aGfxData) {
2022-04-02 04:50:42 +02:00
DataNode<TexData> *_Node = New<DataNode<TexData>>();
// Name
_Node->mName.Read(aFile);
// Data
_Node->mData = New<TexData>();
_Node->mData->mUploaded = false;
Fixed various audio bugs; DynOS can now detect texture duplicates to decrease generated bin files size (#110) Fixed the following audio bugs: Bug: Rom-hacks sequences don't seem to be affected by volume scaling and muting Fix: Force the BGM sequences to follow the vanilla behavior: Volume can't go higher than default volume Volume is reduced to 31% when the game is paused Audio is stopped when the game is paused outside the Castle levels Bug: (Pointed out by Draco) Mario's voice clips are not replaced by the player's character's in the following instances: fall to death barrier, "here we go" in the ending cutscene, "let's a go" after selecting a star, "okey dokey" after starting the game. Fix: The first two ones now call play_character_sound(m, CHAR_SOUND_...) instead of play_sound(SOUND_MARIO_..., pos). The last two ones couldn't be fixed the same way for two reasons: First, the corresponding sounds were not referenced in the sound table, second, the sound played is always cut-off after a few frames (due to how sm64 resets the sound banks after loading a level). Added SOUND_*_LETS_A_GO and SOUND_*_OKEY_DOKEY sounds for each playable character as Bass samples. Character Bass sounds work the same way as vanilla sounds (i.e. can be played with play_character_sound), but they cannot be prematurely stopped by sm64 sound banks shenanigans. This fixes the cut-off for both the star select and the castle grounds entry, plays the sound corresponding to the player's character, and doesn't need to extend or edit the sound table. DynOS can detect texture duplicates when generating a bin or lvl file. When a duplicate is detected, the name of the original texture node is written instead of the whole PNG data, decreasing significantly the resulting file size.
2022-05-20 01:40:45 +02:00
// Check for the texture ref magic
s32 _FileOffset = (s32) ftell(aFile);
u32 _TexRefCode = ReadBytes<u32>(aFile);
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 {
fseek(aFile, _FileOffset, SEEK_SET);
_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;
}
2022-04-02 04:50:42 +02:00
}
// 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;
FILE *_File = fopen(aFilename.c_str(), "rb");
if (!_File) { return NULL; }
u8 type = ReadBytes<u8>(_File);
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);
fclose(_File);
if (aAddToPack) {
if (!_Pack) { _Pack = DynOS_Pack_Add(aPackFolder); }
DynOS_Pack_AddTex(_Pack, _TexNode);
}
return _TexNode;
} else if (type != DATA_TYPE_TEXTURE_RAW) {
fclose(_File);
return NULL;
}
// load raw-texture
_TexNode = New<DataNode<TexData>>();
_TexNode->mData = New<TexData>();
_TexNode->mName.Read(_File);
_TexNode->mData->mRawFormat = ReadBytes<s32>(_File);
_TexNode->mData->mRawSize = ReadBytes<s32>(_File);
_TexNode->mData->mRawWidth = ReadBytes<s32>(_File);
_TexNode->mData->mRawHeight = ReadBytes<s32>(_File);
_TexNode->mData->mRawData.Read(_File);
fclose(_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;
2022-05-08 01:39:19 +02:00
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;
}
// skip files that don't end in '.png'
size_t nameLen = strlen(_PackEnt->d_name);
if (nameLen < 4) continue;
if (strcmp(&_PackEnt->d_name[nameLen - 4], ".png")) {
continue;
}
// read the file
aGfxData->mModelIdentifier++;
TexData* _TexData = LoadTextureFromFile(aGfxData, _Path.c_str());
if (_TexData == NULL) {
PrintError("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)) {
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()))) {
continue;
}
SysPath _OutputPath = fstring("%s/%s.tex", aOutputFolder.c_str(), _BaseName.begin());
// 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));
}
closedir(_PackDir);
}
void DynOS_Tex_GeneratePack(const SysPath &aPackFolder, SysPath &aOutputFolder, bool aAllowCustomTextures) {
Print("---------- Texture pack folder: \"%s\" ----------", aPackFolder.c_str());
// skip generation if any .tex files exist
if (FileTypeExists(aOutputFolder, ".tex")) {
return;
}
GfxData *_GfxData = New<GfxData>();
_GfxData->mModelIdentifier = 0;
SysPath _Empty = "";
DynOS_Tex_GeneratePack_Recursive(aPackFolder, aOutputFolder, _Empty, _Empty, _GfxData, aAllowCustomTextures);
DynOS_Gfx_Free(_GfxData);
}