/** * Copyright (c) 2025 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "asset.hpp" #include "util/memory.hpp" #include "util/string.hpp" #include "assert/assert.hpp" #include "asset/assettype.h" #include "engine/Engine.hpp" #include "debug/debug.hpp" using namespace Dusk; void assetInit(void) { memoryZero(&ASSET, sizeof(asset_t)); // Engine may have been provided the launch path if(Engine::ENGINE.argc > 0) { // This first arg is the executable, so on most platforms it is say // "/path/file" or "C:\Path\file.exe". On PSP this would be something // like "ms0:/PSP/GAME/DUSK/EBOOT.PBP" or if we are debugging it is // "host0:/Dusk.prx" // Get the directory of the executable char_t buffer[FILENAME_MAX]; stringCopy(buffer, Engine::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 time. #if PSP assertTrue(ENGINE.argc >= 1, "PSP requires launch argument."); // PSP is given either host0:/Dusk.prx (debugging) OR the PBP file. 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) { throw std::runtime_error( "Failed to open PBP file: " + std::string(pbpPath) ); } // Get size of PBP file. if(fseek(ASSET.pbpFile, 0, SEEK_END) != 0) { fclose(ASSET.pbpFile); throw std::runtime_error( "Failed to seek to end of PBP file : " + std::string(pbpPath) ); } size_t pbpSize = ftell(ASSET.pbpFile); // Rewind to start if(fseek(ASSET.pbpFile, 0, SEEK_SET) != 0) { fclose(ASSET.pbpFile); throw std::runtime_error( "Failed to seek to start of PBP file : " + std::string(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); throw std::runtime_error( "Failed to read PBP header: " + std::string(pbpPath) ); } if(memoryCompare( ASSET.pbpHeader.signature, ASSET_PBP_SIGNATURE, sizeof(ASSET_PBP_SIGNATURE) ) != 0) { fclose(ASSET.pbpFile); throw std::runtime_error( "Invalid PBP signature in file: " + std::string(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); throw std::runtime_error( "Failed to seek to PSAR offset in PBP file: " + std::string(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); throw std::runtime_error( "Failed to create zip source in PBP file: " + std::string(pbpPath) ); } ASSET.zip = zip_open_from_source( psarSource, ZIP_RDONLY, NULL ); if(ASSET.zip == NULL) { zip_source_free(psarSource); fclose(ASSET.pbpFile); throw std::runtime_error( "Failed to open zip from PBP file: " + std::string(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) throw std::runtime_error("Failed to open asset file."); } 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; } void assetLoad(const char_t *filename, void *output) { assertStrLenMax(filename, FILENAME_MAX, "Filename too long."); assertNotNull(output, "Output pointer cannot be NULL."); // Try to open the file zip_file_t *file = zip_fopen(ASSET.zip, filename, 0); if(file == NULL) { throw std::runtime_error( "Failed to open asset file: " + std::string(filename) ); } // Read the header. assetheader_t header; memoryZero(&header, sizeof(assetheader_t)); zip_int64_t bytesRead = zip_fread(file, &header, sizeof(assetheader_t)); if(bytesRead != sizeof(assetheader_t)) { zip_fclose(file); throw std::runtime_error( "Failed to read asset header for: " + std::string(filename) ); } // Find the asset type based on the header const assettypedef_t *def = NULL; for(uint_fast8_t i = 0; i < ASSET_TYPE_COUNT; i++) { const assettypedef_t *cmp = &ASSET_TYPE_DEFINITIONS[i]; if(cmp->header == NULL) continue; // strcmp didn't work because it's a fixed char_t[3] I think, or maybe // because of the packed struct? bool_t match = true; for(size_t h = 0; h < ASSET_HEADER_SIZE; h++) { if(header.header[h] == cmp->header[h]) continue; match = false; break; } if(!match) continue; def = cmp; break; } if(def == NULL) { zip_fclose(file); throw std::runtime_error( "Unknown asset type for file: " + std::string(filename) ); } // We found the asset type, now load the asset data switch(def->loadStrategy) { case ASSET_LOAD_STRAT_ENTIRE: { assertNotNull(def->entire, "Asset load function cannot be NULL."); void *data = memoryAllocate(def->dataSize); bytesRead = zip_fread(file, data, def->dataSize); if(bytesRead == 0 || bytesRead > def->dataSize) { memoryFree(data); zip_fclose(file); throw std::runtime_error( "Failed to read entire asset data for file: " + std::string(filename) ); } // Close the file now we have the data zip_fclose(file); // Pass to the asset type loader try { def->entire(data, output); } catch(...) { memoryFree(data); throw; } memoryFree(data); break; } case ASSET_LOAD_STRAT_CUSTOM: { assertNotNull(def->custom, "Asset load function cannot be NULL."); assetcustom_t customData = { .zipFile = file, .output = output }; def->custom(customData); break; } default: assertUnreachable("Unknown asset load strategy."); } } 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 }