From 677768e6aba45a2e654c9fc0934efa5b45412f12 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 19 May 2026 23:13:41 -0500 Subject: [PATCH] Map Base --- assets/entities/CubeEntity.js | 2 +- assets/entities/OverworldEntity.js | 2 +- assets/maps/test/chunks/-1_0_0.js | 17 ++ assets/maps/test/chunks/0_0_0.js | 17 ++ assets/maps/test/chunks/0_0_1.js | 17 ++ assets/maps/test/chunks/0_1_0.js | 17 ++ assets/maps/test/chunks/1_0_0.js | 17 ++ assets/maps/test/chunks/2_0_0.js | 17 ++ assets/maps/test/chunks/3_0_0.js | 17 ++ assets/maps/test/init.js | 17 ++ assets/scenes/cube.js | 43 ++--- src/dusk/CMakeLists.txt | 1 + .../component/display/entityrenderable.c | 19 ++ .../component/display/entityrenderable.h | 23 +++ src/dusk/entity/componentlist.h | 2 +- src/dusk/entity/entity.c | 9 +- src/dusk/overworld/CMakeLists.txt | 11 ++ src/dusk/overworld/map.c | 178 ++++++++++++++++++ src/dusk/overworld/map.h | 102 ++++++++++ src/dusk/overworld/mapchunk.c | 96 ++++++++++ src/dusk/overworld/mapchunk.h | 39 ++++ src/dusk/overworld/maptypes.c | 12 ++ src/dusk/overworld/maptypes.h | 30 +++ src/dusk/scene/scene.c | 19 +- .../entity/component/moduleentityrenderable.h | 73 ++++++- src/dusk/script/module/module.h | 2 + src/dusk/script/module/overworld/modulemap.h | 137 ++++++++++++++ .../script/module/overworld/modulemapchunk.h | 34 ++++ 28 files changed, 926 insertions(+), 44 deletions(-) create mode 100644 assets/maps/test/chunks/-1_0_0.js create mode 100644 assets/maps/test/chunks/0_0_0.js create mode 100644 assets/maps/test/chunks/0_0_1.js create mode 100644 assets/maps/test/chunks/0_1_0.js create mode 100644 assets/maps/test/chunks/1_0_0.js create mode 100644 assets/maps/test/chunks/2_0_0.js create mode 100644 assets/maps/test/chunks/3_0_0.js create mode 100644 assets/maps/test/init.js create mode 100644 src/dusk/overworld/CMakeLists.txt create mode 100644 src/dusk/overworld/map.c create mode 100644 src/dusk/overworld/map.h create mode 100644 src/dusk/overworld/mapchunk.c create mode 100644 src/dusk/overworld/mapchunk.h create mode 100644 src/dusk/overworld/maptypes.c create mode 100644 src/dusk/overworld/maptypes.h create mode 100644 src/dusk/script/module/overworld/modulemap.h create mode 100644 src/dusk/script/module/overworld/modulemapchunk.h diff --git a/assets/entities/CubeEntity.js b/assets/entities/CubeEntity.js index 2dff2fd1..bc6ece92 100644 --- a/assets/entities/CubeEntity.js +++ b/assets/entities/CubeEntity.js @@ -14,7 +14,7 @@ CubeEntity.prototype.constructor = CubeEntity; CubeEntity.prototype.update = function() { OverworldEntity.prototype.update.call(this); - var speed = 3.0; + var speed = 5.0; var move = Input.axis2D(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, INPUT_ACTION_UP, INPUT_ACTION_DOWN); this.position.position.x += move.x * speed * TIME.delta; this.position.position.z += move.y * speed * TIME.delta; diff --git a/assets/entities/OverworldEntity.js b/assets/entities/OverworldEntity.js index 57fb2a17..2d4d6d5e 100644 --- a/assets/entities/OverworldEntity.js +++ b/assets/entities/OverworldEntity.js @@ -15,7 +15,7 @@ OverworldEntity.prototype.update = function() { } OverworldEntity.prototype.dispose = function() { - // Nothing to dispose + Entity.prototype.dispose.call(this); } module = OverworldEntity; \ No newline at end of file diff --git a/assets/maps/test/chunks/-1_0_0.js b/assets/maps/test/chunks/-1_0_0.js new file mode 100644 index 00000000..e3c60752 --- /dev/null +++ b/assets/maps/test/chunks/-1_0_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunkN100(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(-16, 0, 0); +} + +TestChunkN100.prototype = Object.create(MapChunk.prototype); +TestChunkN100.prototype.constructor = TestChunkN100; + +TestChunkN100.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunkN100; diff --git a/assets/maps/test/chunks/0_0_0.js b/assets/maps/test/chunks/0_0_0.js new file mode 100644 index 00000000..c0dae0e7 --- /dev/null +++ b/assets/maps/test/chunks/0_0_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk000(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(0, 0, 0); +} + +TestChunk000.prototype = Object.create(MapChunk.prototype); +TestChunk000.prototype.constructor = TestChunk000; + +TestChunk000.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk000; diff --git a/assets/maps/test/chunks/0_0_1.js b/assets/maps/test/chunks/0_0_1.js new file mode 100644 index 00000000..1a1614e9 --- /dev/null +++ b/assets/maps/test/chunks/0_0_1.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk001(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(0, 0, 16); +} + +TestChunk001.prototype = Object.create(MapChunk.prototype); +TestChunk001.prototype.constructor = TestChunk001; + +TestChunk001.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk001; diff --git a/assets/maps/test/chunks/0_1_0.js b/assets/maps/test/chunks/0_1_0.js new file mode 100644 index 00000000..5c78e046 --- /dev/null +++ b/assets/maps/test/chunks/0_1_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk010(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(0, 16, 0); +} + +TestChunk010.prototype = Object.create(MapChunk.prototype); +TestChunk010.prototype.constructor = TestChunk010; + +TestChunk010.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk010; diff --git a/assets/maps/test/chunks/1_0_0.js b/assets/maps/test/chunks/1_0_0.js new file mode 100644 index 00000000..ec7fafcc --- /dev/null +++ b/assets/maps/test/chunks/1_0_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk100(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(16, 0, 0); +} + +TestChunk100.prototype = Object.create(MapChunk.prototype); +TestChunk100.prototype.constructor = TestChunk100; + +TestChunk100.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk100; diff --git a/assets/maps/test/chunks/2_0_0.js b/assets/maps/test/chunks/2_0_0.js new file mode 100644 index 00000000..4c1c1db5 --- /dev/null +++ b/assets/maps/test/chunks/2_0_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk200(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(32, 0, 0); +} + +TestChunk200.prototype = Object.create(MapChunk.prototype); +TestChunk200.prototype.constructor = TestChunk200; + +TestChunk200.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk200; diff --git a/assets/maps/test/chunks/3_0_0.js b/assets/maps/test/chunks/3_0_0.js new file mode 100644 index 00000000..91cca6c8 --- /dev/null +++ b/assets/maps/test/chunks/3_0_0.js @@ -0,0 +1,17 @@ +var CubeEntity = include('entities/CubeEntity.js'); + +function TestChunk300(x, y, z) { + MapChunk.call(this, x, y, z); + + this.cube = new CubeEntity(); + this.cube.position.position = new Vec3(48, 0, 0); +} + +TestChunk300.prototype = Object.create(MapChunk.prototype); +TestChunk300.prototype.constructor = TestChunk300; + +TestChunk300.prototype.dispose = function() { + this.cube.dispose(); +}; + +module = TestChunk300; diff --git a/assets/maps/test/init.js b/assets/maps/test/init.js new file mode 100644 index 00000000..5ce0a9bc --- /dev/null +++ b/assets/maps/test/init.js @@ -0,0 +1,17 @@ +function TestMap() { + Map.call(this); + + Map.setChunkSize(16, 16, 16); + Map.setPosition(0, 0, 0); +} + +TestMap.prototype = Object.create(Map.prototype); +TestMap.prototype.constructor = TestMap; + +TestMap.prototype.update = function() { +}; + +TestMap.prototype.dispose = function() { +}; + +module = TestMap; diff --git a/assets/scenes/cube.js b/assets/scenes/cube.js index 54c216c4..bc745355 100644 --- a/assets/scenes/cube.js +++ b/assets/scenes/cube.js @@ -1,50 +1,33 @@ var CubeEntity = include('entities/CubeEntity.js'); -var MoveCubeCutscene = include('cutscenes/MoveCubeCutscene.js'); function CubeScene() { + Map.load('test'); + this.cam = new Entity(); this.cam.add(POSITION); this.cam.add(CAMERA); - this.cam.position.position = new Vec3(3, 3, 3); - this.cam.position.lookAt(new Vec3(0, 0, 0)); - - this.cube = new CubeEntity(); - - this.spriteEnt = new Entity(); - this.spriteEnt.add(POSITION); - this.spriteEnt.position.position = new Vec3(16, 16, 0); - - this.inputEnabled = false; - - var scene = this; - Cutscene.play(new MoveCubeCutscene({ cube: this.cube })).then(function() { - scene.inputEnabled = true; - }); - - - Textbox.setText( - "Hello! This is a visual novel textbox. It automatically " + - "wraps long lines and splits into pages when the content " + - "is too tall to fit. Press advance to continue...\t" + - "This is a second paragraph on a new page." - ); + this.player = new CubeEntity(); + this.player.position.position = new Vec3(0, 0, 0); } CubeScene.prototype = Object.create(Scene.prototype); CubeScene.prototype.constructor = CubeScene; CubeScene.prototype.update = function() { - if(this.inputEnabled) { - this.cube.update(); - } + this.player.update(); + + var pos = this.player.position.position; + this.cam.position.position = new Vec3(pos.x, pos.y + 8, pos.z + 8); + this.cam.position.lookAt(pos); + + Map.setPosition(Math.floor(pos.x), Math.floor(pos.y), Math.floor(pos.z)); }; CubeScene.prototype.dispose = function() { - Cutscene.stop(); this.cam.dispose(); - this.cube.dispose(); - this.spriteEnt.dispose(); + this.player.dispose(); + Map.dispose(); }; module = CubeScene; diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 589caa4e..954c9de6 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -76,6 +76,7 @@ add_subdirectory(system) add_subdirectory(time) add_subdirectory(ui) add_subdirectory(network) +add_subdirectory(overworld) add_subdirectory(save) add_subdirectory(util) add_subdirectory(thread) \ No newline at end of file diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c index abcfc784..a93a04f8 100644 --- a/src/dusk/entity/component/display/entityrenderable.c +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -128,3 +128,22 @@ void entityRenderableSpriteBatchClear( ); r->spritebatch.spriteCount = 0; } + +void entityRenderableDispose( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + if( + r->type == ENTITY_RENDERABLE_TYPE_CALLBACK && + r->userFree && + r->user + ) { + r->userFree(r->user); + r->user = NULL; + } + r->mesh = NULL; + r->shader = NULL; +} diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h index ea483344..7a1553c0 100644 --- a/src/dusk/entity/component/display/entityrenderable.h +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -16,8 +16,18 @@ typedef enum { ENTITY_RENDERABLE_TYPE_MATERIAL = 0, ENTITY_RENDERABLE_TYPE_SPRITEBATCH, + ENTITY_RENDERABLE_TYPE_CALLBACK, } entityrenderabletype_t; +typedef errorret_t (*entityrenderablecallback_t)( + const entityid_t entityId, + const componentid_t componentId, + const mat4 view, + const mat4 proj, + const mat4 model, + void *user +); + typedef struct { spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX]; uint16_t spriteCount; @@ -32,6 +42,11 @@ typedef struct { shadermaterial_t material; }; entityrenderablespritebatch_t spritebatch; + struct { + entityrenderablecallback_t callback; + void (*userFree)(void *user); + void *user; + }; }; } entityrenderable_t; @@ -44,6 +59,14 @@ void entityRenderableInit( const componentid_t componentId ); +/** + * Disposes the entity renderable component, freeing any callback user data. + */ +void entityRenderableDispose( + const entityid_t entityId, + const componentid_t componentId +); + /** * Gets the renderable type. */ diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h index 812cad11..d3cf5fbd 100644 --- a/src/dusk/entity/componentlist.h +++ b/src/dusk/entity/componentlist.h @@ -19,6 +19,6 @@ X(POSITION, entityposition_t, position, entityPositionInit, NULL) X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL) -X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, NULL) +X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose) X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose) X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL) diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index 82529516..332dd8df 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -86,12 +86,13 @@ void entityDispose(const entityid_t entityId) { for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) { compInd = componentGetIndex(entityId, i); - if(ENTITY_MANAGER.components[compInd].type == COMPONENT_TYPE_NULL) continue; - componentDispose(entityId, i); + componenttype_t type = ENTITY_MANAGER.components[compInd].type; + if(type == COMPONENT_TYPE_NULL) continue; ENTITY_MANAGER.entitiesWithComponent[ - ENTITY_MANAGER.components[compInd].type * ENTITY_COUNT_MAX + entityId + type * ENTITY_COUNT_MAX + entityId ] = COMPONENT_ID_INVALID; + componentDispose(entityId, i); } - + ent->state = 0; } \ No newline at end of file diff --git a/src/dusk/overworld/CMakeLists.txt b/src/dusk/overworld/CMakeLists.txt new file mode 100644 index 00000000..5523d4d9 --- /dev/null +++ b/src/dusk/overworld/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + maptypes.c + map.c + mapchunk.c +) diff --git a/src/dusk/overworld/map.c b/src/dusk/overworld/map.c new file mode 100644 index 00000000..459fd650 --- /dev/null +++ b/src/dusk/overworld/map.c @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "map.h" +#include "assert/assert.h" +#include "asset/assetfile.h" +#include "util/memory.h" +#include "util/string.h" +#include "console/console.h" +#include "script/module/overworld/modulemap.h" + +map_t MAP; + +chunkindex_t mapChunkRelToIndex( + chunkunit_t rx, + chunkunit_t ry, + chunkunit_t rz +) { + return (chunkindex_t)( + rz * (MAP_CHUNKS_WIDE * MAP_CHUNKS_HIGH) + + ry * MAP_CHUNKS_WIDE + + rx + ); +} + +void mapInit(void) { + memoryZero(&MAP, sizeof(map_t)); + MAP.scriptRef = MAP_SCRIPT_REF_NONE; +} + +errorret_t mapLoad(const char_t *handle) { + assertStrLenMin(handle, 1, "Map handle cannot be empty"); + assertStrLenMax(handle, MAP_HANDLE_MAX - 1, "Map handle too long"); + + if(mapIsLoaded()) mapDispose(); + + memoryZero(&MAP, sizeof(map_t)); + MAP.scriptRef = MAP_SCRIPT_REF_NONE; + stringCopy(MAP.handle, handle, MAP_HANDLE_MAX); + + char_t path[ASSET_FILE_NAME_MAX]; + stringFormat(path, sizeof(path), "maps/%s/init.js", handle); + + jerry_value_t mapClass = MAP_SCRIPT_REF_NONE; + errorChain(scriptManagerExecFile(path, &mapClass)); + + if(!jerry_value_is_function(mapClass)) { + if(mapClass != MAP_SCRIPT_REF_NONE) jerry_value_free(mapClass); + errorThrow("Map '%s' must export a constructor function", handle); + } + + jerry_value_t mapObj = jerry_construct(mapClass, NULL, 0); + jerry_value_free(mapClass); + + if(jerry_value_is_exception(mapObj)) { + char_t errMsg[512]; + moduleBaseExceptionMessage(mapObj, errMsg, sizeof(errMsg)); + jerry_value_free(mapObj); + errorThrow("Map '%s' constructor threw: %s", handle, errMsg); + } + + MAP.scriptRef = mapObj; + consolePrint("Map loaded: %s", handle); + errorOk(); +} + +bool_t mapIsLoaded(void) { + return MAP.loaded; +} + +errorret_t mapPositionSet(tilepos_t tilePos) { + assertTrue(MAP.chunkTileWidth > 0, "chunkTileWidth not set"); + assertTrue(MAP.chunkTileHeight > 0, "chunkTileHeight not set"); + assertTrue(MAP.chunkTileDepth > 0, "chunkTileDepth not set"); + + // Convert tile position to chunk-space window origin. + chunkpos_t newPos = { + .x = (chunkunit_t)(tilePos.x / MAP.chunkTileWidth), + .y = (chunkunit_t)(tilePos.y / MAP.chunkTileHeight), + .z = (chunkunit_t)(tilePos.z / MAP.chunkTileDepth), + }; + + if(MAP.loaded && chunkPosEqual(MAP.chunkPosition, newPos)) errorOk(); + + // Categorise existing chunks as remaining or freed. + chunkindex_t remaining[MAP_CHUNKS_COUNT]; + chunkindex_t freed[MAP_CHUNKS_COUNT]; + chunkindex_t remainingCount = 0; + chunkindex_t freedCount = 0; + + for(chunkindex_t i = 0; i < MAP_CHUNKS_COUNT; i++) { + mapchunk_t *chunk = &MAP.chunks[i]; + chunkpos_t p = chunk->position; + + bool_t stays = MAP.loaded && + p.x >= newPos.x && p.x < newPos.x + MAP_CHUNKS_WIDE && + p.y >= newPos.y && p.y < newPos.y + MAP_CHUNKS_HIGH && + p.z >= newPos.z && p.z < newPos.z + MAP_CHUNKS_DEEP; + + if(stays) { + remaining[remainingCount++] = i; + } else { + if(MAP.loaded) mapChunkUnload(chunk); + freed[freedCount++] = i; + } + } + + // Build chunkOrder for the new window, loading into freed slots as needed. + chunkindex_t orderIndex = 0; + for(chunkunit_t zOff = 0; zOff < MAP_CHUNKS_DEEP; zOff++) { + for(chunkunit_t yOff = 0; yOff < MAP_CHUNKS_HIGH; yOff++) { + for(chunkunit_t xOff = 0; xOff < MAP_CHUNKS_WIDE; xOff++) { + chunkpos_t target = { + .x = (chunkunit_t)(newPos.x + xOff), + .y = (chunkunit_t)(newPos.y + yOff), + .z = (chunkunit_t)(newPos.z + zOff), + }; + + // Check if the target chunk is already loaded. + chunkindex_t poolIdx = -1; + for(chunkindex_t r = 0; r < remainingCount; r++) { + if(chunkPosEqual(MAP.chunks[remaining[r]].position, target)) { + poolIdx = remaining[r]; + break; + } + } + + // Otherwise recycle a freed slot. + if(poolIdx == -1) { + poolIdx = freed[--freedCount]; + MAP.chunks[poolIdx].position = target; + errorChain(mapChunkLoad(&MAP.chunks[poolIdx])); + } + + MAP.chunkOrder[orderIndex++] = &MAP.chunks[poolIdx]; + }}} + + MAP.chunkPosition = newPos; + MAP.loaded = true; + errorOk(); +} + +mapchunk_t *mapGetChunkAt(chunkpos_t pos) { + if(!MAP.loaded) return NULL; + chunkpos_t p = MAP.chunkPosition; + if( + pos.x < p.x || pos.x >= p.x + MAP_CHUNKS_WIDE || + pos.y < p.y || pos.y >= p.y + MAP_CHUNKS_HIGH || + pos.z < p.z || pos.z >= p.z + MAP_CHUNKS_DEEP + ) return NULL; + chunkindex_t idx = mapChunkRelToIndex( + pos.x - p.x, pos.y - p.y, pos.z - p.z + ); + return MAP.chunkOrder[idx]; +} + +errorret_t mapUpdate(void) { + if(!MAP.loaded) errorOk(); + errorChain(moduleMapCall("update")); + errorOk(); +} + +void mapDispose(void) { + consolePrint("Map disposing: %s", MAP.handle); + if(MAP.scriptRef != MAP_SCRIPT_REF_NONE) { + moduleMapCall("dispose"); + moduleMapReset(); + } + if(!MAP.loaded) return; + for(chunkindex_t i = 0; i < MAP_CHUNKS_COUNT; i++) { + mapChunkUnload(&MAP.chunks[i]); + } + MAP.loaded = false; +} diff --git a/src/dusk/overworld/map.h b/src/dusk/overworld/map.h new file mode 100644 index 00000000..5bd275ca --- /dev/null +++ b/src/dusk/overworld/map.h @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "mapchunk.h" +#include "error/error.h" +#include "script/scriptmanager.h" + +#define MAP_NAME_MAX 64 +#define MAP_HANDLE_MAX 32 +#define MAP_CHUNKS_WIDE 3 +#define MAP_CHUNKS_HIGH 3 +#define MAP_CHUNKS_DEEP 2 +#define MAP_CHUNKS_COUNT (MAP_CHUNKS_WIDE * MAP_CHUNKS_HIGH * MAP_CHUNKS_DEEP) + +typedef struct { + char_t name[MAP_NAME_MAX]; + char_t handle[MAP_HANDLE_MAX]; + uint16_t chunkTileWidth; + uint16_t chunkTileHeight; + uint16_t chunkTileDepth; + mapchunk_t chunks[MAP_CHUNKS_COUNT]; + mapchunk_t *chunkOrder[MAP_CHUNKS_COUNT]; + chunkpos_t chunkPosition; + bool_t loaded; + jerry_value_t scriptRef; +} map_t; + +/** Sentinel value meaning no map script is loaded. */ +#define MAP_SCRIPT_REF_NONE ((jerry_value_t)0) + +extern map_t MAP; + +/** + * Initializes the map, zeroing all state. + */ +void mapInit(void); + +/** + * Prepares the map for use with the given handle. If a map is already loaded + * it is disposed first. Chunk positions are not set until mapPositionSet is + * called. + * + * @param handle Short identifier for this map (max MAP_HANDLE_MAX - 1 chars). + * @return An error code. + */ +errorret_t mapLoad(const char_t *handle); + +/** + * Returns true if a map is currently loaded. + * + * @return true if a map is loaded, false otherwise. + */ +bool_t mapIsLoaded(void); + +/** + * Converts a chunk position relative to the window origin to its pool index. + * + * @param rx Relative chunk X offset within the window. + * @param ry Relative chunk Y offset within the window. + * @param rz Relative chunk Z offset within the window. + * @return The flat pool index for that relative position. + */ +chunkindex_t mapChunkRelToIndex( + chunkunit_t rx, + chunkunit_t ry, + chunkunit_t rz +); + +/** + * Slides the loaded chunk window so its origin is the chunk that contains + * the given tile-space position. Only the delta is loaded/unloaded. + * + * @param tilePos Tile-space position of the new window origin. + * @return An error code. + */ +errorret_t mapPositionSet(tilepos_t tilePos); + +/** + * Updates the map each frame. + * + * @return An error code. + */ +errorret_t mapUpdate(void); + +/** + * Disposes the map, unloading all chunks. + */ +void mapDispose(void); + +/** + * Returns the chunk at the given absolute chunk-space position, or NULL if + * it is outside the currently loaded window. + * + * @param pos Absolute chunk-space position to look up. + * @return Pointer to the chunk, or NULL if not loaded. + */ +mapchunk_t *mapGetChunkAt(chunkpos_t pos); diff --git a/src/dusk/overworld/mapchunk.c b/src/dusk/overworld/mapchunk.c new file mode 100644 index 00000000..b067aeec --- /dev/null +++ b/src/dusk/overworld/mapchunk.c @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "mapchunk.h" +#include "map.h" +#include "entity/entitymanager.h" +#include "util/memory.h" +#include "util/string.h" +#include "asset/asset.h" +#include "console/console.h" +#include "script/module/overworld/modulemapchunk.h" + +errorret_t mapChunkLoad(mapchunk_t *chunk) { + chunk->entityCount = 0; + chunk->scriptRef = MAP_CHUNK_SCRIPT_REF_NONE; + memoryZero(chunk->entities, sizeof(chunk->entities)); + + if(MAP.handle[0] == '\0') errorOk(); + + char_t path[ASSET_FILE_NAME_MAX]; + stringFormat( + path, sizeof(path), + "maps/%s/chunks/%d_%d_%d.js", + MAP.handle, + (int)chunk->position.x, + (int)chunk->position.y, + (int)chunk->position.z + ); + + if(!assetFileExists(path)) errorOk(); + + jerry_value_t chunkClass = MAP_CHUNK_SCRIPT_REF_NONE; + errorChain(scriptManagerExecFile(path, &chunkClass)); + + if(!jerry_value_is_function(chunkClass)) { + if(chunkClass != MAP_CHUNK_SCRIPT_REF_NONE) jerry_value_free(chunkClass); + errorThrow("Chunk script '%s' must export a constructor", path); + } + + jerry_value_t args[3] = { + jerry_number((double)chunk->position.x), + jerry_number((double)chunk->position.y), + jerry_number((double)chunk->position.z), + }; + jerry_value_t chunkObj = jerry_construct(chunkClass, args, 3); + jerry_value_free(chunkClass); + jerry_value_free(args[0]); + jerry_value_free(args[1]); + jerry_value_free(args[2]); + + if(jerry_value_is_exception(chunkObj)) { + jerry_value_free(chunkObj); + errorThrow("Chunk script '%s' constructor threw", path); + } + + chunk->scriptRef = chunkObj; + consolePrint( + "Chunk loaded: %s [%d,%d,%d]", + path, + (int)chunk->position.x, + (int)chunk->position.y, + (int)chunk->position.z + ); + errorOk(); +} + +void mapChunkUnload(mapchunk_t *chunk) { + consolePrint( + "Chunk unloading: [%d,%d,%d]", + (int)chunk->position.x, + (int)chunk->position.y, + (int)chunk->position.z + ); + if(chunk->scriptRef != MAP_CHUNK_SCRIPT_REF_NONE) { + jerry_value_t key = jerry_string_sz("dispose"); + jerry_value_t fn = jerry_object_get(chunk->scriptRef, key); + jerry_value_free(key); + + if(jerry_value_is_function(fn)) { + jerry_value_t result = jerry_call(fn, chunk->scriptRef, NULL, 0); + jerry_value_free(result); + } + jerry_value_free(fn); + jerry_value_free(chunk->scriptRef); + chunk->scriptRef = MAP_CHUNK_SCRIPT_REF_NONE; + } + + for(uint8_t i = 0; i < chunk->entityCount; i++) { + entityDispose(chunk->entities[i]); + } + chunk->entityCount = 0; +} diff --git a/src/dusk/overworld/mapchunk.h b/src/dusk/overworld/mapchunk.h new file mode 100644 index 00000000..93ad8e47 --- /dev/null +++ b/src/dusk/overworld/mapchunk.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "maptypes.h" +#include "entity/entitybase.h" +#include "error/error.h" +#include "script/scriptmanager.h" + +#define MAP_CHUNK_ENTITY_COUNT_MAX 64 + +/** Sentinel value meaning no chunk script is loaded. */ +#define MAP_CHUNK_SCRIPT_REF_NONE ((jerry_value_t)0) + +typedef struct { + chunkpos_t position; + entityid_t entities[MAP_CHUNK_ENTITY_COUNT_MAX]; + uint8_t entityCount; + jerry_value_t scriptRef; +} mapchunk_t; + +/** + * Loads content into a chunk at its current position. + * + * @param chunk The chunk to load. + * @return An error code. + */ +errorret_t mapChunkLoad(mapchunk_t *chunk); + +/** + * Disposes all entities owned by the chunk and resets its state. + * + * @param chunk The chunk to unload. + */ +void mapChunkUnload(mapchunk_t *chunk); diff --git a/src/dusk/overworld/maptypes.c b/src/dusk/overworld/maptypes.c new file mode 100644 index 00000000..0ae48514 --- /dev/null +++ b/src/dusk/overworld/maptypes.c @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "maptypes.h" + +bool_t chunkPosEqual(chunkpos_t a, chunkpos_t b) { + return a.x == b.x && a.y == b.y && a.z == b.z; +} diff --git a/src/dusk/overworld/maptypes.h b/src/dusk/overworld/maptypes.h new file mode 100644 index 00000000..3815c41c --- /dev/null +++ b/src/dusk/overworld/maptypes.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef int16_t chunkunit_t; +typedef int16_t chunkindex_t; +typedef int32_t tileunit_t; + +typedef struct { + chunkunit_t x, y, z; +} chunkpos_t; + +typedef struct { + tileunit_t x, y, z; +} tilepos_t; + +/** + * Checks if two chunk positions are equal. + * + * @param a The first chunk position. + * @param b The second chunk position. + * @return true if the positions are equal, false otherwise. + */ +bool_t chunkPosEqual(chunkpos_t a, chunkpos_t b); diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 239e2557..c9439177 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -96,12 +96,19 @@ errorret_t sceneRender(void) { entityPositionGetTransform(entityId, posComp, model); } - shader_t *shader = r->shader; - if(!shader) continue; - switch(r->type) { + case ENTITY_RENDERABLE_TYPE_CALLBACK: { + if(!r->callback) break; + errorChain(r->callback( + entityId, renderComp, view, proj, model, r->user + )); + break; + } + case ENTITY_RENDERABLE_TYPE_MATERIAL: { - if(!r->mesh) continue; + shader_t *shader = r->shader; + if(!shader) break; + if(!r->mesh) break; if(shaderCurrent != shader) { shaderCurrent = shader; errorChain(shaderBind(shaderCurrent)); @@ -115,7 +122,9 @@ errorret_t sceneRender(void) { } case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: { - if(r->spritebatch.spriteCount == 0) continue; + if(r->spritebatch.spriteCount == 0) break; + shader_t *shader = r->shader; + if(!shader) break; if(shaderCurrent != shader) { shaderCurrent = shader; errorChain(shaderBind(shaderCurrent)); diff --git a/src/dusk/script/module/entity/component/moduleentityrenderable.h b/src/dusk/script/module/entity/component/moduleentityrenderable.h index 06e8f364..16a382bb 100644 --- a/src/dusk/script/module/entity/component/moduleentityrenderable.h +++ b/src/dusk/script/module/entity/component/moduleentityrenderable.h @@ -112,6 +112,68 @@ moduleBaseFunction(moduleEntityRenderableSpriteBatchClear) { return jerry_undefined(); } +static void jsRenderCallbackFree(void *user) { + jerry_value_t *cb = (jerry_value_t *)user; + jerry_value_free(*cb); + memoryFree(cb); +} + +static errorret_t jsRenderCallbackBridge( + const entityid_t entityId, + const componentid_t componentId, + const mat4 view, + const mat4 proj, + const mat4 model, + void *user +) { + (void)entityId; (void)componentId; + (void)view; (void)proj; (void)model; + jerry_value_t *cb = (jerry_value_t *)user; + jerry_value_t ret = jerry_call(*cb, jerry_undefined(), NULL, 0); + if(jerry_value_is_exception(ret)) { + jerry_value_free(ret); + errorThrow("Renderable callback threw a JS exception"); + } + jerry_value_free(ret); + errorOk(); +} + +moduleBaseFunction(moduleEntityRenderableSetCallback) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_RENDERABLE_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + + entityrenderable_t *r = (entityrenderable_t *)componentGetData( + h->eid, h->cid, COMPONENT_TYPE_RENDERABLE + ); + + if( + r->type == ENTITY_RENDERABLE_TYPE_CALLBACK && + r->userFree && + r->user + ) { + r->userFree(r->user); + r->user = NULL; + } + + if(argc >= 1 && jerry_value_is_function(args[0])) { + jerry_value_t *cb = (jerry_value_t *)memoryAllocate(sizeof(jerry_value_t)); + *cb = jerry_value_copy(args[0]); + r->type = ENTITY_RENDERABLE_TYPE_CALLBACK; + r->callback = jsRenderCallbackBridge; + r->userFree = jsRenderCallbackFree; + r->user = cb; + } else { + r->type = ENTITY_RENDERABLE_TYPE_CALLBACK; + r->callback = NULL; + r->userFree = NULL; + r->user = NULL; + } + + return jerry_undefined(); +} + static void moduleEntityRENDERABLE(void) { scriptProtoInit( &MODULE_ENTITY_RENDERABLE_PROTO, NULL, sizeof(componenthandle_t), NULL @@ -127,6 +189,13 @@ static void moduleEntityRENDERABLE(void) { scriptProtoDefineFunc(&MODULE_ENTITY_RENDERABLE_PROTO, "clearSprites", moduleEntityRenderableSpriteBatchClear); - moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_MATERIAL", ENTITY_RENDERABLE_TYPE_MATERIAL); - moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_SPRITEBATCH", ENTITY_RENDERABLE_TYPE_SPRITEBATCH); + scriptProtoDefineFunc(&MODULE_ENTITY_RENDERABLE_PROTO, "setCallback", + moduleEntityRenderableSetCallback); + + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_MATERIAL", + ENTITY_RENDERABLE_TYPE_MATERIAL); + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_SPRITEBATCH", + ENTITY_RENDERABLE_TYPE_SPRITEBATCH); + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_CALLBACK", + ENTITY_RENDERABLE_TYPE_CALLBACK); } diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h index 3d036b25..bf181fa9 100644 --- a/src/dusk/script/module/module.h +++ b/src/dusk/script/module/module.h @@ -29,6 +29,7 @@ #include "script/module/ui/moduletextbox.h" #include "script/module/ui/modulefullbox.h" #include "script/module/save/modulesave.h" +#include "script/module/overworld/modulemap.h" static void moduleRegister(void) { moduleInclude(); @@ -53,4 +54,5 @@ static void moduleRegister(void) { moduleTextbox(); moduleFullbox(); moduleSave(); + moduleMap(); } diff --git a/src/dusk/script/module/overworld/modulemap.h b/src/dusk/script/module/overworld/modulemap.h new file mode 100644 index 00000000..146e4b03 --- /dev/null +++ b/src/dusk/script/module/overworld/modulemap.h @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "overworld/map.h" +#include "modulemapchunk.h" + +static scriptproto_t MODULE_MAP_PROTO; + +moduleBaseFunction(moduleMapDefaultUpdate) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapDefaultDispose) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapDefaultConstructor) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapLoad) { + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t handle[MAP_HANDLE_MAX]; + moduleBaseToString(args[0], handle, sizeof(handle)); + if(handle[0] == '\0') return moduleBaseThrow("Map.load: handle cannot be empty"); + + errorret_t err = mapLoad(handle); + if(err.code != ERROR_OK) return moduleBaseThrow("Map.load: failed to load map"); + + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapIsLoaded) { + return jerry_boolean(mapIsLoaded()); +} + +moduleBaseFunction(moduleMapDispose) { + mapDispose(); + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapSetChunkSize) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); moduleBaseRequireNumber(2); + + MAP.chunkTileWidth = (uint16_t)moduleBaseArgInt(0); + MAP.chunkTileHeight = (uint16_t)moduleBaseArgInt(1); + MAP.chunkTileDepth = (uint16_t)moduleBaseArgInt(2); + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapSetPosition) { + moduleBaseRequireArgs(3); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); moduleBaseRequireNumber(2); + + tilepos_t pos = { + .x = (tileunit_t)moduleBaseArgInt(0), + .y = (tileunit_t)moduleBaseArgInt(1), + .z = (tileunit_t)moduleBaseArgInt(2), + }; + errorret_t err = mapPositionSet(pos); + if(err.code != ERROR_OK) return moduleBaseThrow("Map.setPosition: failed"); + return jerry_undefined(); +} + +static void moduleMapReset(void) { + if(MAP.scriptRef != MAP_SCRIPT_REF_NONE) { + jerry_value_free(MAP.scriptRef); + MAP.scriptRef = MAP_SCRIPT_REF_NONE; + } + + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz("module"); + jerry_value_t undef = jerry_undefined(); + jerry_object_set(global, key, undef); + jerry_value_free(undef); + jerry_value_free(key); + jerry_value_free(global); +} + +static errorret_t moduleMapCall(const char_t *method) { + assertStrLenMin(method, 1, "Method name cannot be empty"); + + if(MAP.scriptRef == MAP_SCRIPT_REF_NONE) errorOk(); + + jerry_value_t key = jerry_string_sz(method); + jerry_value_t fn = jerry_object_get(MAP.scriptRef, key); + jerry_value_free(key); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorOk(); + } + + jerry_value_t result = jerry_call(fn, MAP.scriptRef, NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + char_t errMsg[512]; + moduleBaseExceptionMessage(result, errMsg, sizeof(errMsg)); + jerry_value_free(result); + errorThrow("Map:%s failed: %s", method, errMsg); + } + + jerry_value_free(result); + errorOk(); +} + +static void moduleMap(void) { + moduleMapChunk(); + + scriptProtoInit( + &MODULE_MAP_PROTO, + "Map", + sizeof(uint8_t), + moduleMapDefaultConstructor + ); + + scriptProtoDefineFunc(&MODULE_MAP_PROTO, "update", moduleMapDefaultUpdate); + scriptProtoDefineFunc(&MODULE_MAP_PROTO, "dispose", moduleMapDefaultDispose); + + scriptProtoDefineStaticFunc(&MODULE_MAP_PROTO, "load", moduleMapLoad); + scriptProtoDefineStaticFunc(&MODULE_MAP_PROTO, "dispose", moduleMapDispose); + scriptProtoDefineStaticFunc(&MODULE_MAP_PROTO, "setChunkSize", moduleMapSetChunkSize); + scriptProtoDefineStaticFunc(&MODULE_MAP_PROTO, "setPosition", moduleMapSetPosition); + scriptProtoDefineStaticProp( + &MODULE_MAP_PROTO, "isLoaded", moduleMapIsLoaded, NULL + ); +} diff --git a/src/dusk/script/module/overworld/modulemapchunk.h b/src/dusk/script/module/overworld/modulemapchunk.h new file mode 100644 index 00000000..cf5fb808 --- /dev/null +++ b/src/dusk/script/module/overworld/modulemapchunk.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "overworld/mapchunk.h" + +static scriptproto_t MODULE_MAP_CHUNK_PROTO; + +moduleBaseFunction(moduleMapChunkDefaultConstructor) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleMapChunkDefaultDispose) { + return jerry_undefined(); +} + +static void moduleMapChunk(void) { + scriptProtoInit( + &MODULE_MAP_CHUNK_PROTO, + "MapChunk", + sizeof(uint8_t), + moduleMapChunkDefaultConstructor + ); + + scriptProtoDefineFunc( + &MODULE_MAP_CHUNK_PROTO, "dispose", moduleMapChunkDefaultDispose + ); +}