/** * Copyright (c) 2025 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "map.h" #include "util/memory.h" #include "assert/assert.h" #include "asset/asset.h" #include "rpg/entity/entity.h" #include "util/string.h" #include "script/scriptcontext.h" map_t MAP; errorret_t mapInit() { memoryZero(&MAP, sizeof(map_t)); errorOk(); } bool_t mapIsLoaded() { return MAP.filePath[0] != '\0'; } errorret_t mapLoad(const char_t *path, const chunkpos_t position) { assertStrLenMin(path, 1, "Map file path cannot be empty"); assertStrLenMax(path, MAP_FILE_PATH_MAX - 1, "Map file path too long"); if(stringCompare(MAP.filePath, path) == 0) { // Same map, no need to reload errorOk(); } chunkindex_t i; // Unload all loaded chunks if(mapIsLoaded()) { for(i = 0; i < MAP_CHUNK_COUNT; i++) { mapChunkUnload(&MAP.chunks[i]); } } // Store the map file path stringCopy(MAP.filePath, path, MAP_FILE_PATH_MAX); // Determine directory path (it is dirname) stringCopy(MAP.dirPath, path, MAP_FILE_PATH_MAX); char_t *last = stringFindLastChar(MAP.dirPath, '/'); if(last == NULL) errorThrow("Invalid map file path"); // Store filename, sans extension stringCopy(MAP.fileName, last + 1, MAP_FILE_PATH_MAX); *last = '\0'; // Terminate to get directory path last = stringFindLastChar(MAP.fileName, '.'); if(last == NULL) errorThrow("Map file name has no extension"); *last = '\0'; // Terminate to remove extension // Reset map position MAP.chunkPosition = position; // Perform "initial load" i = 0; for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) { for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) { for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) { chunk_t *chunk = &MAP.chunks[i]; chunk->position.x = x + position.x; chunk->position.y = y + position.y; chunk->position.z = z + position.z; MAP.chunkOrder[i] = chunk; errorChain(mapChunkLoad(chunk)); i++; } } } // Execute map script. char_t scriptPath[MAP_FILE_PATH_MAX + 16]; stringFormat( scriptPath, sizeof(scriptPath), "%s/%s.dsf", MAP.dirPath, MAP.fileName ); if(assetFileExists(scriptPath)) { scriptcontext_t ctx; errorChain(scriptContextInit(&ctx)); errorChain(scriptContextExecFile(&ctx, scriptPath)); scriptContextDispose(&ctx); } errorOk(); } errorret_t mapPositionSet(const chunkpos_t newPos) { if(!mapIsLoaded()) errorThrow("No map loaded"); const chunkpos_t curPos = MAP.chunkPosition; if(chunkPositionIsEqual(curPos, newPos)) { errorOk(); } // Determine which chunks remain loaded chunkindex_t chunksRemaining[MAP_CHUNK_COUNT] = {0}; chunkindex_t chunksFreed[MAP_CHUNK_COUNT] = {0}; uint32_t remainingCount = 0; uint32_t freedCount = 0; for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { // Will this chunk remain loaded? chunk_t *chunk = &MAP.chunks[i]; if( chunk->position.x >= newPos.x && chunk->position.x < newPos.x + MAP_CHUNK_WIDTH && chunk->position.y >= newPos.y && chunk->position.y < newPos.y + MAP_CHUNK_HEIGHT && chunk->position.z >= newPos.z && chunk->position.z < newPos.z + MAP_CHUNK_DEPTH ) { // Stays loaded chunksRemaining[remainingCount++] = i; continue; } // Not remaining loaded chunksFreed[freedCount++] = i; } // Unload the freed chunks for(chunkindex_t i = 0; i < freedCount; i++) { chunk_t *chunk = &MAP.chunks[chunksFreed[i]]; mapChunkUnload(chunk); } // This can probably be optimized later, for now we check each chunk and see // if it needs loading or not, and update the chunk order chunkindex_t orderIndex = 0; for(chunkunit_t zOff = 0; zOff < MAP_CHUNK_DEPTH; zOff++) { for(chunkunit_t yOff = 0; yOff < MAP_CHUNK_HEIGHT; yOff++) { for(chunkunit_t xOff = 0; xOff < MAP_CHUNK_WIDTH; xOff++) { const chunkpos_t newChunkPos = { newPos.x + xOff, newPos.y + yOff, newPos.z + zOff }; // Is this chunk already loaded (was not unloaded earlier)? chunkindex_t chunkIndex = -1; for(chunkindex_t i = 0; i < remainingCount; i++) { chunk_t *chunk = &MAP.chunks[chunksRemaining[i]]; if(!chunkPositionIsEqual(chunk->position, newChunkPos)) continue; chunkIndex = chunksRemaining[i]; break; } // Need to load this chunk if(chunkIndex == -1) { // Find a freed chunk to reuse chunkIndex = chunksFreed[--freedCount]; chunk_t *chunk = &MAP.chunks[chunkIndex]; chunk->position = newChunkPos; errorChain(mapChunkLoad(chunk)); } MAP.chunkOrder[orderIndex++] = &MAP.chunks[chunkIndex]; } } } // Update map position MAP.chunkPosition = newPos; errorOk(); } void mapUpdate() { } void mapDispose() { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { mapChunkUnload(&MAP.chunks[i]); } } void mapChunkUnload(chunk_t* chunk) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { if(chunk->entities[i] == 0xFF) break; entity_t *entity = &ENTITIES[chunk->entities[i]]; entity->type = ENTITY_TYPE_NULL; } for(uint8_t i = 0; i < chunk->meshCount; i++) { if(chunk->meshes[i].vertexCount == 0) continue; meshDispose(&chunk->meshes[i]); } } errorret_t mapChunkLoad(chunk_t* chunk) { if(!mapIsLoaded()) errorThrow("No map loaded"); char_t buffer[64]; // TODO: Can probably move this to asset load logic? chunk->meshCount = 0; memoryZero(chunk->meshes, sizeof(chunk->meshes)); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); // Get chunk filepath. snprintf(buffer, sizeof(buffer), "%s/chunks/%d_%d_%d.dcf", MAP.dirPath, chunk->position.x, chunk->position.y, chunk->position.z ); // Chunk available? if(!assetFileExists(buffer)) { memoryZero(chunk->tiles, sizeof(chunk->tiles)); errorOk(); } // Load. errorChain(assetLoad(buffer, chunk)); errorOk(); } chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) { if(!mapIsLoaded()) return -1; chunkpos_t relPos = { position.x - MAP.chunkPosition.x, position.y - MAP.chunkPosition.y, position.z - MAP.chunkPosition.z }; if( relPos.x < 0 || relPos.y < 0 || relPos.z < 0 || relPos.x >= MAP_CHUNK_WIDTH || relPos.y >= MAP_CHUNK_HEIGHT || relPos.z >= MAP_CHUNK_DEPTH ) { return -1; } return chunkPosToIndex(&relPos); } chunk_t* mapGetChunk(const uint8_t index) { if(index >= MAP_CHUNK_COUNT) return NULL; if(!mapIsLoaded()) return NULL; return MAP.chunkOrder[index]; } tile_t mapGetTile(const worldpos_t position) { if(!mapIsLoaded()) return TILE_SHAPE_NULL; chunkpos_t chunkPos; worldPosToChunkPos(&position, &chunkPos); chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos); if(chunkIndex == -1) return TILE_SHAPE_NULL; chunk_t *chunk = mapGetChunk(chunkIndex); assertNotNull(chunk, "Chunk pointer cannot be NULL"); chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); return chunk->tiles[tileIndex]; }