345 lines
9.1 KiB
C
345 lines
9.1 KiB
C
/**
|
|
* Copyright (c) 2025 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "asset.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#include "assert/assert.h"
|
|
#include "asset/assettype.h"
|
|
#include "engine/engine.h"
|
|
#include "debug/debug.h"
|
|
#include "util/string.h"
|
|
|
|
errorret_t assetInit(void) {
|
|
memoryZero(&ASSET, sizeof(asset_t));
|
|
|
|
#if DOLPHIN
|
|
// Init FAT driver.
|
|
if(!fatInitDefault()) errorThrow("Failed to initialize FAT filesystem.");
|
|
|
|
char_t **dolphinSearchPath = (char_t **)ASSET_DOLPHIN_PATHS;
|
|
char_t foundPath[FILENAME_MAX];
|
|
foundPath[0] = '\0';
|
|
do {
|
|
// Try open dir
|
|
DIR *pdir = opendir(*dolphinSearchPath);
|
|
if(pdir == NULL) continue;
|
|
|
|
|
|
// Scan if file is present
|
|
while(true) {
|
|
struct dirent* pent = readdir(pdir);
|
|
if(pent == NULL) break;
|
|
|
|
if(stringCompareInsensitive(pent->d_name, ASSET_FILE) != 0) {
|
|
continue;
|
|
}
|
|
|
|
// Copy out filename
|
|
snprintf(
|
|
foundPath,
|
|
FILENAME_MAX,
|
|
"%s/%s",
|
|
*dolphinSearchPath,
|
|
ASSET_FILE
|
|
);
|
|
break;
|
|
}
|
|
|
|
// Close dir.
|
|
closedir(pdir);
|
|
|
|
// Did we find the file here?
|
|
if(foundPath[0] != '\0') break;
|
|
} while(*(++dolphinSearchPath) != NULL);
|
|
|
|
if(foundPath[0] != '\0') {
|
|
}
|
|
|
|
// Did we find the asset file?
|
|
if(foundPath[0] == '\0') {
|
|
errorThrow("Failed to find asset file on FAT filesystem.");
|
|
}
|
|
|
|
ASSET.zip = zip_open(foundPath, ZIP_RDONLY, NULL);
|
|
if(ASSET.zip == NULL) {
|
|
errorThrow("Failed to open asset file on FAT filesystem.");
|
|
}
|
|
errorOk();
|
|
#endif
|
|
|
|
// Engine may have been provided the launch path
|
|
if(ENGINE.argc > 0) {
|
|
// Get the directory of the executable
|
|
char_t buffer[FILENAME_MAX];
|
|
stringCopy(buffer, ENGINE.argv[0], FILENAME_MAX);
|
|
size_t len = strlen(buffer);
|
|
|
|
// Normalize slashes
|
|
for(size_t i = 0; i < FILENAME_MAX; i++) {
|
|
if(buffer[i] == '\0') break;
|
|
if(buffer[i] == '\\') buffer[i] = '/';
|
|
}
|
|
|
|
// Now find the last slash
|
|
char_t *end = buffer + len - 1;
|
|
do {
|
|
end--;
|
|
if(*end == '/') {
|
|
*end = '\0';
|
|
break;
|
|
}
|
|
} while(end != buffer);
|
|
|
|
|
|
// Did we find a slash?
|
|
if(end != buffer) {
|
|
// We found the directory, set as system path
|
|
stringCopy(ASSET.systemPath, buffer, FILENAME_MAX);
|
|
}
|
|
}
|
|
|
|
// Default system path, intended to be overridden by the platform
|
|
stringCopy(ASSET.systemPath, ".", FILENAME_MAX);
|
|
|
|
// PSP specific asset loading.
|
|
#if PSP
|
|
assertTrue(ENGINE.argc >= 1, "PSP requires launch argument.");
|
|
|
|
// PSP is given either the prx OR the PBP file.
|
|
// In the format of "ms0:/PSP/GAME/DUSK/EBOOT.PBP" or "host0:/Dusk.prx"
|
|
// IF the file is the PBP file, we are loading directly on the PSP itself.
|
|
// IF the file is the .prx then we are debugging and fopen will return
|
|
// relative filepaths correctly, e.g. host0:/dusk.dsk will be on host.
|
|
if(
|
|
stringEndsWithCaseInsensitive(ENGINE.argv[0], ".pbp") ||
|
|
ASSET_PBP_READ_PBP_FROM_HOST
|
|
) {
|
|
const char_t *pbpPath = (
|
|
ASSET_PBP_READ_PBP_FROM_HOST ? "./EBOOT.PBP" : ENGINE.argv[0]
|
|
);
|
|
ASSET.pbpFile = fopen(pbpPath, "rb");
|
|
if(ASSET.pbpFile == NULL) {
|
|
errorThrow("Failed to open PBP file: %s", pbpPath);
|
|
}
|
|
|
|
// Get size of PBP file.
|
|
if(fseek(ASSET.pbpFile, 0, SEEK_END) != 0) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to seek to end of PBP file : %s", pbpPath);
|
|
}
|
|
size_t pbpSize = ftell(ASSET.pbpFile);
|
|
|
|
// Rewind to start
|
|
if(fseek(ASSET.pbpFile, 0, SEEK_SET) != 0) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to seek to start of PBP file : %s", pbpPath);
|
|
}
|
|
|
|
// Read the PBP header
|
|
size_t read = fread(
|
|
&ASSET.pbpHeader,
|
|
1,
|
|
sizeof(assetpbp_t),
|
|
ASSET.pbpFile
|
|
);
|
|
if(read != sizeof(assetpbp_t)) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to read PBP header", pbpPath);
|
|
}
|
|
if(memoryCompare(
|
|
ASSET.pbpHeader.signature,
|
|
ASSET_PBP_SIGNATURE,
|
|
sizeof(ASSET_PBP_SIGNATURE)
|
|
) != 0) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Invalid PBP signature in file: %s", pbpPath);
|
|
}
|
|
|
|
// If we seek to the PSAR offset, we can read the WAD file from there
|
|
if(fseek(ASSET.pbpFile, ASSET.pbpHeader.psarOffset, SEEK_SET) != 0) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to seek to PSAR offset in PBP file: %s", pbpPath);
|
|
}
|
|
|
|
zip_uint64_t zipPsarOffset = (zip_uint64_t)ASSET.pbpHeader.psarOffset;
|
|
zip_int64_t zipPsarSize = (zip_int64_t)(
|
|
pbpSize - ASSET.pbpHeader.psarOffset
|
|
);
|
|
|
|
zip_source_t *psarSource = zip_source_filep_create(
|
|
ASSET.pbpFile,
|
|
zipPsarOffset,
|
|
zipPsarSize,
|
|
NULL
|
|
);
|
|
if(psarSource == NULL) {
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to create zip source in PBP file: %s", pbpPath);
|
|
}
|
|
|
|
ASSET.zip = zip_open_from_source(
|
|
psarSource,
|
|
ZIP_RDONLY,
|
|
NULL
|
|
);
|
|
if(ASSET.zip == NULL) {
|
|
zip_source_free(psarSource);
|
|
fclose(ASSET.pbpFile);
|
|
errorThrow("Failed to open zip from PBP file: %s", pbpPath);
|
|
}
|
|
|
|
errorOk();
|
|
}
|
|
#endif
|
|
|
|
// Open zip file
|
|
char_t searchPath[FILENAME_MAX];
|
|
const char_t **path = ASSET_SEARCH_PATHS;
|
|
do {
|
|
sprintf(
|
|
searchPath,
|
|
*path,
|
|
ASSET.systemPath,
|
|
ASSET_FILE
|
|
);
|
|
|
|
// Try open
|
|
ASSET.zip = zip_open(searchPath, ZIP_RDONLY, NULL);
|
|
if(ASSET.zip == NULL) continue;
|
|
break;// Found!
|
|
} while(*(++path) != NULL);
|
|
|
|
// Did we open the asset?
|
|
if(ASSET.zip == NULL) errorThrow("Failed to open asset file.");
|
|
|
|
errorOk();
|
|
}
|
|
|
|
bool_t assetFileExists(const char_t *filename) {
|
|
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
|
|
|
|
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
|
|
if(idx < 0) return false;
|
|
return true;
|
|
}
|
|
|
|
errorret_t assetLoad(const char_t *filename, void *output) {
|
|
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
|
|
assertNotNull(output, "Output pointer cannot be NULL.");
|
|
|
|
// Determine the asset type by reading the extension
|
|
const assettypedef_t *def = NULL;
|
|
for(uint_fast8_t i = 0; i < ASSET_TYPE_COUNT; i++) {
|
|
const assettypedef_t *cmp = &ASSET_TYPE_DEFINITIONS[i];
|
|
assertNotNull(cmp, "Asset type definition cannot be NULL.");
|
|
if(cmp->extension == NULL) continue;
|
|
if(!stringEndsWithCaseInsensitive(filename, cmp->extension)) continue;
|
|
def = cmp;
|
|
break;
|
|
}
|
|
if(def == NULL) {
|
|
errorThrow("Unknown asset type for file: %s", filename);
|
|
}
|
|
|
|
// Get file size of the asset.
|
|
zip_stat_t st;
|
|
zip_stat_init(&st);
|
|
if(!zip_stat(ASSET.zip, filename, 0, &st) == 0) {
|
|
errorThrow("Failed to stat asset file: %s", filename);
|
|
}
|
|
|
|
// Minimum file size.
|
|
zip_int64_t fileSize = (zip_int64_t)st.size;
|
|
if(fileSize <= 0) {
|
|
errorThrow("Asset file is empty: %s", filename);
|
|
}
|
|
|
|
// Try to open the file
|
|
zip_file_t *file = zip_fopen(ASSET.zip, filename, 0);
|
|
if(file == NULL) {
|
|
errorThrow("Failed to open asset file: %s", filename);
|
|
}
|
|
|
|
// Load the asset data
|
|
switch(def->loadStrategy) {
|
|
case ASSET_LOAD_STRAT_ENTIRE:
|
|
assertNotNull(def->entire, "Asset load function cannot be NULL.");
|
|
|
|
// Must have more to read
|
|
if(fileSize <= 0) {
|
|
zip_fclose(file);
|
|
errorThrow("No data remaining to read for asset: %s", filename);
|
|
}
|
|
|
|
if(fileSize > def->dataSize) {
|
|
zip_fclose(file);
|
|
errorThrow(
|
|
"Asset file has too much data remaining after header: %s",
|
|
filename
|
|
);
|
|
}
|
|
|
|
// Create space to read the entire asset data
|
|
void *data = memoryAllocate(fileSize);
|
|
if(!data) {
|
|
zip_fclose(file);
|
|
errorThrow("Failed to allocate memory for asset data of file: %s", filename);
|
|
}
|
|
|
|
// Read in the asset data.
|
|
zip_int64_t bytesRead = zip_fread(file, data, fileSize);
|
|
if(bytesRead == 0 || bytesRead > fileSize) {
|
|
memoryFree(data);
|
|
zip_fclose(file);
|
|
errorThrow("Failed to read asset data for file: %s", filename);
|
|
}
|
|
fileSize -= bytesRead;
|
|
|
|
// Close the file now we have the data
|
|
zip_fclose(file);
|
|
|
|
// Pass to the asset type loader
|
|
assetentire_t entire = {
|
|
.data = data,
|
|
.output = output
|
|
};
|
|
errorret_t ret = def->entire(entire);
|
|
memoryFree(data);
|
|
|
|
errorChain(ret);
|
|
break;
|
|
|
|
case ASSET_LOAD_STRAT_CUSTOM:
|
|
assertNotNull(def->custom, "Asset load function cannot be NULL.");
|
|
assetcustom_t customData = {
|
|
.zipFile = file,
|
|
.output = output
|
|
};
|
|
errorChain(def->custom(customData));
|
|
break;
|
|
|
|
default:
|
|
assertUnreachable("Unknown asset load strategy.");
|
|
}
|
|
|
|
errorOk();
|
|
}
|
|
|
|
void assetDispose(void) {
|
|
if(ASSET.zip != NULL) {
|
|
zip_close(ASSET.zip);
|
|
ASSET.zip = NULL;
|
|
}
|
|
|
|
#if PSP
|
|
if(ASSET.pbpFile != NULL) {
|
|
fclose(ASSET.pbpFile);
|
|
ASSET.pbpFile = NULL;
|
|
}
|
|
#endif
|
|
} |