/** * 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 }