300 lines
7.8 KiB
C++
300 lines
7.8 KiB
C++
/**
|
|
* 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.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
|
|
} |