48 Commits

Author SHA1 Message Date
YourWishes d8fe0f6923 textbox 2026-05-06 22:42:28 -05:00
YourWishes 581dbc2b3c Update linux docker 2026-05-06 20:31:50 -05:00
YourWishes 7301d2ad76 luce bree 2026-05-06 20:24:16 -05:00
YourWishes 3232a14d1d Consistent build 2026-05-06 14:40:06 -05:00
YourWishes 84c1f88d42 Fix PSP blending issues 2026-05-06 11:17:34 -05:00
YourWishes 3695b10e4b Easing test 2026-05-05 22:24:25 -05:00
YourWishes 6da02b25fa Testing cutscenes 2026-05-05 22:10:47 -05:00
YourWishes 3bc544fba1 Re-enable build pulling fetched modules 2026-05-05 19:29:48 -05:00
YourWishes 368d370f49 Cleanup modules 2026-05-05 19:29:29 -05:00
YourWishes bb29c0edef Working on some script modules 2026-05-05 16:22:04 -05:00
YourWishes 6edcf75a0c add display state 2026-05-04 22:16:30 -05:00
YourWishes 31cc186424 Fixed small compile bugs 2026-05-04 08:39:47 -05:00
YourWishes 0e94c1fa6d Fixed a bunch of messy over 80 char lines 2026-05-04 08:29:43 -05:00
YourWishes 6d9e2dd3e1 UI first pass 2026-05-03 21:52:12 -05:00
YourWishes 4a4adeb3c8 Finally fixed linux asset weirdness 2026-05-02 15:18:49 -05:00
YourWishes ff77f8cfa0 Fix dolphin rendering 2026-05-01 23:11:59 -05:00
YourWishes 36db89c36e Nuke the old input system, use the new UI system 2026-05-01 15:21:46 -05:00
YourWishes a9948142ad Fix linux warning 2026-05-01 14:00:24 -05:00
YourWishes 8d05510584 Fix linux building 2026-05-01 13:58:05 -05:00
YourWishes d373de7a29 Some adjustments 2026-05-01 13:44:51 -05:00
YourWishes 1efa9a9f7b More cleanup 2026-05-01 09:43:50 -05:00
YourWishes 0fb3ba2f91 Cleanup, prepping for example game stuff 2026-04-30 23:43:49 -05:00
YourWishes 3b4c5b5153 Added FPS meter 2026-04-30 23:34:32 -05:00
YourWishes 9293aeeec8 Fix position 2026-04-30 23:18:36 -05:00
YourWishes 03ae83b119 More cleanup? 2026-04-30 23:07:17 -05:00
YourWishes abd63cc6cf More cleanup 2026-04-30 22:40:32 -05:00
YourWishes 2e43aa2c44 Bit more cleanup 2026-04-30 20:03:44 -05:00
YourWishes 3d984e13c2 Module input improvements 2026-04-29 23:40:01 -05:00
YourWishes 010900fe21 Better again. 2026-04-29 23:26:21 -05:00
YourWishes ffed626447 More cleanup 2026-04-29 22:39:47 -05:00
YourWishes 61f69af35a Refactor pass 1 2026-04-29 14:53:35 -05:00
YourWishes bd248ee91c Build script on PSP, Dolphin and Engine. 2026-04-28 21:34:09 -05:00
YourWishes 194255bffe Fix merge conflcits 2026-04-28 14:02:59 -05:00
YourWishes 52ee627079 Merge branch 'jerryscript' into playertest 2026-04-28 14:02:53 -05:00
YourWishes bd4200e707 Finished getting JerryScript on all the platforms. 2026-04-28 13:59:46 -05:00
YourWishes 73e7d6c7f3 Add epoch 2026-04-28 10:33:23 -05:00
YourWishes a41b0e916b prog 2026-04-28 08:04:01 -05:00
YourWishes 19f2a2c616 Bit more consistent but still far from perfect 2026-04-27 09:14:14 -05:00
YourWishes 998601f722 Playertest: scene/script system refactor and Wii ABI fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 23:30:08 -05:00
YourWishes 7c3386cf3e Add entity scripting 2026-04-20 17:01:12 -05:00
YourWishes d161182997 Entity modules 2026-04-20 16:50:16 -05:00
YourWishes 1646dc2dbd Fixed build 2026-04-20 15:43:18 -05:00
YourWishes b640295be2 Scene script 2026-04-20 15:34:24 -05:00
YourWishes b89ae2391b Reg console. 2026-04-20 14:31:22 -05:00
YourWishes a0fad441d0 Updated bind command 2026-04-20 12:59:25 -05:00
YourWishes 340084dac3 Removed console aliases 2026-04-20 12:49:06 -05:00
YourWishes c78135aa09 Fixed bugs with console 2026-04-20 12:05:35 -05:00
YourWishes d19f8bbd30 Restored console, has a bug 2026-04-20 09:26:25 -05:00
245 changed files with 8662 additions and 4411 deletions
+15 -15
View File
@@ -53,21 +53,21 @@ jobs:
path: ./git-artifcats/Dusk
if-no-files-found: error
build-vita:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build Vita
run: ./scripts/build-vita-docker.sh
- name: Upload Vita binary
uses: actions/upload-artifact@v6
with:
name: dusk-vita
path: build-vita/Dusk.vpk
if-no-files-found: error
# build-vita:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v6
# - name: Set up Docker
# uses: docker/setup-docker-action@v5
# - name: Build Vita
# run: ./scripts/build-vita-docker.sh
# - name: Upload Vita binary
# uses: actions/upload-artifact@v6
# with:
# name: dusk-vita
# path: build-vita/Dusk.vpk
# if-no-files-found: error
build-knulli:
runs-on: ubuntu-latest
+3 -1
View File
@@ -4,11 +4,13 @@
# https://opensource.org/licenses/MIT
# Setup
cmake_minimum_required(VERSION 3.18)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF)
-13
View File
@@ -1,13 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct {
void *nothing;
} inventory_t;
+35
View File
@@ -0,0 +1,35 @@
var SPEED = 3.0;
var DURATION = 2.0;
function MoveCubeCutscene(params) {
Cutscene.call(this);
this.cube = params.cube;
var startX = this.cube.position.position.x;
this.animLeft = new Animation([
{ time: 0.0, value: startX, easing: Easing.outQuad },
{ time: DURATION, value: startX - SPEED * DURATION }
]);
this.animRight = new Animation([
{ time: 0.0, value: startX - SPEED * DURATION, easing: Easing.inOutQuad },
{ time: DURATION, value: startX }
]);
}
MoveCubeCutscene.prototype = Object.create(Cutscene.prototype);
MoveCubeCutscene.prototype.constructor = MoveCubeCutscene;
MoveCubeCutscene.prototype.update = function() {
if(!this.animLeft.complete) {
this.cube.position.position.x = this.animLeft.update(TIME.delta);
} else {
this.cube.position.position.x = this.animRight.update(TIME.delta);
if(this.animRight.complete) {
Cutscene.finish();
}
}
};
module = MoveCubeCutscene;
+25
View File
@@ -0,0 +1,25 @@
var OverworldEntity = include('entities/OverworldEntity.js');
function CubeEntity() {
OverworldEntity.call(this);
this.add(MESH);
this.add(MATERIAL);
this.cubeMesh = Mesh.createCube();
this.mesh.mesh = this.cubeMesh;
}
CubeEntity.prototype = Object.create(OverworldEntity.prototype);
CubeEntity.prototype.constructor = CubeEntity;
CubeEntity.prototype.update = function() {
OverworldEntity.prototype.update.call(this);
var speed = 3.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;
this.material.color = Color.rainbow();
};
module = CubeEntity;
+21
View File
@@ -0,0 +1,21 @@
function OverworldEntity() {
Entity.call(this);
this.add(POSITION);
}
OverworldEntity.prototype = Object.create(Entity.prototype);
OverworldEntity.prototype.constructor = OverworldEntity;
OverworldEntity.prototype.update = function() {
// var speed = 3.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;
}
OverworldEntity.prototype.dispose = function() {
// Nothing to dispose
}
module = OverworldEntity;
+77
View File
@@ -0,0 +1,77 @@
Console.visible = true;
// Default input bindings.
if (typeof PSP !== 'undefined') {
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("accept", INPUT_ACTION_ACCEPT);
Input.bind("cancel", INPUT_ACTION_CANCEL);
Input.bind("select", INPUT_ACTION_RAGEQUIT);
Input.bind("lstick_up", INPUT_ACTION_UP);
Input.bind("lstick_down", INPUT_ACTION_DOWN);
Input.bind("lstick_left", INPUT_ACTION_LEFT);
Input.bind("lstick_right", INPUT_ACTION_RIGHT);
Input.bind("triangle", INPUT_ACTION_CONSOLE);
} else if (typeof DOLPHIN !== 'undefined') {
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("b", INPUT_ACTION_CANCEL);
Input.bind("a", INPUT_ACTION_ACCEPT);
Input.bind("z", INPUT_ACTION_CONSOLE);
Input.bind("lstick_up", INPUT_ACTION_UP);
Input.bind("lstick_down", INPUT_ACTION_DOWN);
Input.bind("lstick_left", INPUT_ACTION_LEFT);
Input.bind("lstick_right", INPUT_ACTION_RIGHT);
} else if (typeof LINUX !== 'undefined') {
if (typeof INPUT_KEYBOARD !== 'undefined') {
Input.bind("w", INPUT_ACTION_UP);
Input.bind("s", INPUT_ACTION_DOWN);
Input.bind("a", INPUT_ACTION_LEFT);
Input.bind("d", INPUT_ACTION_RIGHT);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("enter", INPUT_ACTION_ACCEPT);
Input.bind("e", INPUT_ACTION_ACCEPT);
Input.bind("q", INPUT_ACTION_CANCEL);
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
Input.bind("`", INPUT_ACTION_CONSOLE);
}
if (typeof INPUT_GAMEPAD !== 'undefined') {
Input.bind("gamepad_up", INPUT_ACTION_UP);
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
Input.bind("gamepad_b", INPUT_ACTION_CANCEL);
Input.bind("gamepad_back", INPUT_ACTION_RAGEQUIT);
Input.bind("gamepad_lstick_up", INPUT_ACTION_UP);
Input.bind("gamepad_lstick_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_lstick_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_lstick_right", INPUT_ACTION_RIGHT);
}
if (typeof INPUT_POINTER !== 'undefined') {
Input.bind("mouse_x", INPUT_ACTION_POINTERX);
Input.bind("mouse_y", INPUT_ACTION_POINTERY);
}
} else {
print("Unknown platform, no default input bindings set.");
}
Scene.set('scenes/cube.js');
-76
View File
@@ -1,76 +0,0 @@
module('input')
module('platform')
module('scene')
module('locale')
-- Default Input bindings.
if PSP then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("accept", INPUT_ACTION_ACCEPT)
inputBind("cancel", INPUT_ACTION_CANCEL)
inputBind("select", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif DOLPHIN then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("b", INPUT_ACTION_CANCEL)
inputBind("a", INPUT_ACTION_ACCEPT)
inputBind("z", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif LINUX then
if INPUT_KEYBOARD then
inputBind("w", INPUT_ACTION_UP)
inputBind("s", INPUT_ACTION_DOWN)
inputBind("a", INPUT_ACTION_LEFT)
inputBind("d", INPUT_ACTION_RIGHT)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("enter", INPUT_ACTION_ACCEPT)
inputBind("e", INPUT_ACTION_ACCEPT)
inputBind("q", INPUT_ACTION_CANCEL)
inputBind("escape", INPUT_ACTION_RAGEQUIT)
end
if INPUT_GAMEPAD then
inputBind("gamepad_up", INPUT_ACTION_UP)
inputBind("gamepad_down", INPUT_ACTION_DOWN)
inputBind("gamepad_left", INPUT_ACTION_LEFT)
inputBind("gamepad_right", INPUT_ACTION_RIGHT)
inputBind("gamepad_a", INPUT_ACTION_ACCEPT)
inputBind("gamepad_b", INPUT_ACTION_CANCEL)
inputBind("gamepad_back", INPUT_ACTION_RAGEQUIT)
inputBind("gamepad_lstick_up", INPUT_ACTION_UP)
inputBind("gamepad_lstick_down", INPUT_ACTION_DOWN)
inputBind("gamepad_lstick_left", INPUT_ACTION_LEFT)
inputBind("gamepad_lstick_right", INPUT_ACTION_RIGHT)
end
if INPUT_POINTER then
inputBind("mouse_x", INPUT_ACTION_POINTERX)
inputBind("mouse_y", INPUT_ACTION_POINTERY)
end
else
print("Unknown platform, no default input bindings set.")
end
+50
View File
@@ -0,0 +1,50 @@
var CubeEntity = include('entities/CubeEntity.js');
var MoveCubeCutscene = include('cutscenes/MoveCubeCutscene.js');
function CubeScene() {
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."
);
}
CubeScene.prototype = Object.create(Scene.prototype);
CubeScene.prototype.constructor = CubeScene;
CubeScene.prototype.update = function() {
if(this.inputEnabled) {
this.cube.update();
}
};
CubeScene.prototype.dispose = function() {
Cutscene.stop();
this.cam.dispose();
this.cube.dispose();
this.spriteEnt.dispose();
};
module = CubeScene;
+6
View File
@@ -0,0 +1,6 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
+96
View File
@@ -0,0 +1,96 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Turn things off we don't need
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
set(JERRY_EXT ON CACHE BOOL "" FORCE)
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
# Fetch Jerry
include(FetchContent)
FetchContent_Declare(
jerryscript
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
GIT_TAG float32-fix
)
FetchContent_MakeAvailable(jerryscript)
# Mark found
set(jerryscript_FOUND ON)
# Define targets
if(TARGET jerryscript-core)
set(JERRY_CORE_TARGET jerryscript-core)
elseif(TARGET jerry-core)
set(JERRY_CORE_TARGET jerry-core)
endif()
if(TARGET jerryscript-ext)
set(JERRY_EXT_TARGET jerryscript-ext)
elseif(TARGET jerry-ext)
set(JERRY_EXT_TARGET jerry-ext)
endif()
if(TARGET jerryscript-port-default)
set(JERRY_PORT_TARGET jerryscript-port-default)
elseif(TARGET jerry-port-default)
set(JERRY_PORT_TARGET jerry-port-default)
elseif(TARGET jerryscript-port)
set(JERRY_PORT_TARGET jerryscript-port)
elseif(TARGET jerry-port)
set(JERRY_PORT_TARGET jerry-port)
endif()
if(NOT JERRY_CORE_TARGET)
message(FATAL_ERROR "JerryScript core target not found")
endif()
if(NOT JERRY_EXT_TARGET)
message(FATAL_ERROR "JerryScript ext target not found")
endif()
if(NOT JERRY_PORT_TARGET)
message(FATAL_ERROR "JerryScript port target not found")
endif()
foreach(tgt IN ITEMS
${JERRY_CORE_TARGET}
${JERRY_EXT_TARGET}
${JERRY_PORT_TARGET}
)
if(TARGET ${tgt})
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
JERRY_NUMBER_TYPE_FLOAT64=0
JERRY_BUILTIN_DATE=0
)
endif()
endforeach()
# Export include dirs through the targets
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-core/include
)
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-ext/include
)
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-port/default/include
)
# Suppress JerryScript-only warning
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
-Wno-error
)
endif()
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
+1 -22
View File
@@ -4,6 +4,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
)
# Custom compiler flags
@@ -21,31 +22,9 @@ set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
cglm
liblua
m
fat
PkgConfig::zip
+2 -1
View File
@@ -30,6 +30,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_CONSOLE_POSIX
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
@@ -41,5 +42,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
DUSK_NETWORK_IPV6
THREAD_PTHREAD=1
DUSK_THREAD_PTHREAD
)
+21 -4
View File
@@ -1,6 +1,19 @@
# Fixes some problems building JerryScript
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib <TARGET>")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE)
set(JERRY_LTO OFF CACHE BOOL "" FORCE)
find_package(jerryscript REQUIRED)
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
SDL2
pthread
@@ -29,13 +42,17 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
pspnet_apctl
psphttp
pspssl
jerryscript::core
jerryscript::ext
jerryscript::port
)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_PSP
@@ -44,7 +61,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=480
DUSK_DISPLAY_HEIGHT=272
THREAD_PTHREAD=1
DUSK_THREAD_PTHREAD
)
# Postbuild, create .pbp file for PSP.
-22
View File
@@ -20,31 +20,9 @@ set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
liblua
cglm
SDL2
SDL2main
+1 -1
View File
@@ -14,8 +14,8 @@ RUN apt-get install -y \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.3-dev \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
libzip-dev \
-1
View File
@@ -4,7 +4,6 @@
# https://opensource.org/licenses/MIT
add_subdirectory(dusk)
add_subdirectory(duskrpg)
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
add_subdirectory(dusklinux)
+13 -14
View File
@@ -32,18 +32,13 @@ if(NOT yyjson_FOUND)
endif()
endif()
if(NOT Lua_FOUND)
find_package(Lua REQUIRED)
if(Lua_FOUND AND NOT TARGET Lua::Lua)
add_library(Lua::Lua INTERFACE IMPORTED)
set_target_properties(
Lua::Lua
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
if(NOT jerryscript_FOUND)
find_package(jerryscript REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
jerryscript::core
jerryscript::ext
jerryscript::port
)
endif()
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Lua::Lua)
endif()
# Includes
@@ -59,14 +54,18 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
)
# Subdirs
add_subdirectory(animation)
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(log)
add_subdirectory(cutscene)
add_subdirectory(item)
add_subdirectory(story)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(log)
add_subdirectory(engine)
add_subdirectory(entity)
add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(locale)
add_subdirectory(physics)
@@ -77,4 +76,4 @@ add_subdirectory(time)
add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(util)
# add_subdirectory(thread)
add_subdirectory(thread)
@@ -3,8 +3,8 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulescene.c
easing.c
animation.c
)
+75
View File
@@ -0,0 +1,75 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <math.h>
void animationInit(animation_t *anim) {
memoryZero(anim, sizeof(animation_t));
}
void animationAddKeyframe(
animation_t *anim,
float_t time,
float_t value,
easingtype_t easing
) {
assertTrue(
anim->keyframeCount < ANIMATION_KEYFRAME_COUNT_MAX,
"Keyframe count exceeds ANIMATION_KEYFRAME_COUNT_MAX"
);
uint8_t i = anim->keyframeCount++;
anim->keyframes[i].time = time;
anim->keyframes[i].value = value;
anim->keyframes[i].easing = easing;
if(time > anim->duration) anim->duration = time;
}
float_t animationGetValue(const animation_t *anim) {
if(anim->keyframeCount == 0) return 0.0f;
uint8_t last = anim->keyframeCount - 1;
if(anim->keyframeCount == 1) return anim->keyframes[0].value;
if(anim->time <= anim->keyframes[0].time) return anim->keyframes[0].value;
if(anim->time >= anim->keyframes[last].time) {
return anim->keyframes[last].value;
}
for(uint8_t i = 0; i < last; i++) {
const keyframe_t *a = &anim->keyframes[i];
const keyframe_t *b = &anim->keyframes[i + 1];
if(anim->time < a->time || anim->time >= b->time) continue;
float_t t = (anim->time - a->time) / (b->time - a->time);
t = easingApply(a->easing, t);
return a->value + (b->value - a->value) * t;
}
return anim->keyframes[last].value;
}
float_t animationUpdate(animation_t *anim, float_t delta) {
if(anim->complete && !anim->loop) return animationGetValue(anim);
anim->time += delta;
if(anim->duration > 0.0f && anim->time >= anim->duration) {
if(anim->loop) {
anim->time = fmodf(anim->time, anim->duration);
} else {
anim->time = anim->duration;
anim->complete = true;
}
}
return animationGetValue(anim);
}
void animationReset(animation_t *anim) {
anim->time = 0.0f;
anim->complete = false;
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
#include "error/error.h"
typedef struct {
keyframe_t keyframes[ANIMATION_KEYFRAME_COUNT_MAX];
uint8_t keyframeCount;
float_t time;
float_t duration;
bool_t loop;
bool_t complete;
} animation_t;
/**
* Zeroes an animation struct.
*
* @param anim The animation to initialize.
*/
void animationInit(animation_t *anim);
/**
* Appends a keyframe. Keyframes must be added in ascending time order.
*
* @param anim The animation to modify.
* @param time Time in seconds for this keyframe.
* @param value Value at this keyframe.
* @param easing Easing applied to the segment after this keyframe.
*/
void animationAddKeyframe(
animation_t *anim,
float_t time,
float_t value,
easingtype_t easing
);
/**
* Advances the animation by delta seconds and returns the current value.
*
* @param anim The animation to update.
* @param delta Seconds elapsed since last update.
* @return Interpolated value at the new time.
*/
float_t animationUpdate(animation_t *anim, float_t delta);
/**
* Returns the interpolated value at the current time without advancing.
*
* @param anim The animation to read.
* @return Interpolated value.
*/
float_t animationGetValue(const animation_t *anim);
/**
* Resets the animation to the beginning.
*
* @param anim The animation to reset.
*/
void animationReset(animation_t *anim);
+106
View File
@@ -0,0 +1,106 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "easing.h"
#include "assert/assert.h"
#include <math.h>
#define EASING_PI 3.14159265358979323846f
#define EASING_C1 1.70158f
#define EASING_C2 (EASING_C1 * 1.525f)
#define EASING_C3 (EASING_C1 + 1.0f)
float_t easingLinear(float_t t) {
return t;
}
float_t easingInSine(float_t t) {
return 1.0f - cosf(t * EASING_PI * 0.5f);
}
float_t easingOutSine(float_t t) {
return sinf(t * EASING_PI * 0.5f);
}
float_t easingInOutSine(float_t t) {
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
}
float_t easingInQuad(float_t t) {
return t * t;
}
float_t easingOutQuad(float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u;
}
float_t easingInOutQuad(float_t t) {
if(t < 0.5f) return 2.0f * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * 0.5f;
}
float_t easingInCubic(float_t t) {
return t * t * t;
}
float_t easingOutCubic(float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u;
}
float_t easingInOutCubic(float_t t) {
if(t < 0.5f) return 4.0f * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * 0.5f;
}
float_t easingInQuart(float_t t) {
return t * t * t * t;
}
float_t easingOutQuart(float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u * u;
}
float_t easingInOutQuart(float_t t) {
if(t < 0.5f) return 8.0f * t * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * u * 0.5f;
}
float_t easingInBack(float_t t) {
return EASING_C3 * t * t * t - EASING_C1 * t * t;
}
float_t easingOutBack(float_t t) {
float_t u = t - 1.0f;
return 1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u;
}
float_t easingInOutBack(float_t t) {
if(t < 0.5f) {
float_t u = 2.0f * t;
return u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f;
}
float_t u = 2.0f * t - 2.0f;
return (u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f;
}
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
easingInSine, easingOutSine, easingInOutSine,
easingInQuad, easingOutQuad, easingInOutQuad,
easingInCubic, easingOutCubic, easingInOutCubic,
easingInQuart, easingOutQuart, easingInOutQuart,
easingInBack, easingOutBack, easingInOutBack,
};
float_t easingApply(easingtype_t type, float_t t) {
assertTrue(type < EASING_COUNT, "Invalid easing type");
return EASING_FUNCTIONS[type](t);
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "dusk.h"
#define EASING_LINEAR 0
#define EASING_IN_SINE 1
#define EASING_OUT_SINE 2
#define EASING_IN_OUT_SINE 3
#define EASING_IN_QUAD 4
#define EASING_OUT_QUAD 5
#define EASING_IN_OUT_QUAD 6
#define EASING_IN_CUBIC 7
#define EASING_OUT_CUBIC 8
#define EASING_IN_OUT_CUBIC 9
#define EASING_IN_QUART 10
#define EASING_OUT_QUART 11
#define EASING_IN_OUT_QUART 12
#define EASING_IN_BACK 13
#define EASING_OUT_BACK 14
#define EASING_IN_OUT_BACK 15
#define EASING_COUNT 16
typedef uint8_t easingtype_t;
typedef float_t (*easingfn_t)(float_t t);
extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
float_t easingApply(easingtype_t type, float_t t);
float_t easingLinear(float_t t);
float_t easingInSine(float_t t);
float_t easingOutSine(float_t t);
float_t easingInOutSine(float_t t);
float_t easingInQuad(float_t t);
float_t easingOutQuad(float_t t);
float_t easingInOutQuad(float_t t);
float_t easingInCubic(float_t t);
float_t easingOutCubic(float_t t);
float_t easingInOutCubic(float_t t);
float_t easingInQuart(float_t t);
float_t easingOutQuart(float_t t);
float_t easingInOutQuart(float_t t);
float_t easingInBack(float_t t);
float_t easingOutBack(float_t t);
float_t easingInOutBack(float_t t);
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "easing.h"
#define ANIMATION_KEYFRAME_COUNT_MAX 16
typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
+2 -2
View File
@@ -25,7 +25,7 @@ errorret_t assetInit(void) {
}
bool_t assetFileExists(const char_t *filename) {
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
assertStrLenMax(filename, ASSET_FILE_PATH_MAX, "Filename too long.");
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
if(idx < 0) return false;
@@ -38,7 +38,7 @@ errorret_t assetLoad(
void *params,
void *output
) {
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
assertStrLenMax(filename, ASSET_FILE_PATH_MAX, "Filename too long.");
assertNotNull(output, "Output pointer cannot be NULL.");
assertNotNull(loader, "Asset file loader cannot be NULL.");
+14 -4
View File
@@ -146,7 +146,9 @@ size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) {
return reader->bufferEnd - reader->bufferStart;
}
const uint8_t *assetFileLineReaderUnreadPtr(const assetfilelinereader_t *reader) {
const uint8_t *assetFileLineReaderUnreadPtr(
const assetfilelinereader_t *reader
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
return reader->readBuffer + reader->bufferStart;
@@ -177,11 +179,16 @@ static errorret_t assetFileLineReaderAppend(
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
assertTrue(reader->lineLength < reader->outBufferSize, "Line length exceeds out buffer.");
assertTrue(
reader->lineLength < reader->outBufferSize,
"Line length exceeds out buffer."
);
reader->outBuffer[reader->lineLength] = '\0';
}
static ssize_t assetFileLineReaderFindNewline(const assetfilelinereader_t *reader) {
static ssize_t assetFileLineReaderFindNewline(
const assetfilelinereader_t *reader
) {
size_t i;
assertNotNull(reader, "Reader cannot be NULL.");
@@ -284,7 +291,10 @@ errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
errorret_t ret;
/* strip CR in CRLF */
if(chunkLength > 0 && reader->readBuffer[(size_t)newlineIndex - 1] == '\r') {
if(
chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
) {
chunkLength--;
}
+2
View File
@@ -9,6 +9,8 @@
#include "error/error.h"
#include <zip.h>
#define ASSET_FILE_PATH_MAX FILENAME_MAX
typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
@@ -152,7 +152,7 @@ errorret_t assetLocaleParseHeader(
if(pluralIndex >= localeFile->pluralStateCount - 1) {
errorThrow(
"Too many plural conditions. Expected %d conditional clauses for nplurals=%d.",
"Too many plural conditions. Expected %d clauses for nplurals=%d.",
localeFile->pluralStateCount - 1,
localeFile->pluralStateCount
);
@@ -7,76 +7,86 @@
#include "assetscriptloader.h"
#include "assert/assert.h"
#include <stdlib.h>
#include <zip.h>
errorret_t assetScriptLoader(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL");
assertNull(file->zipFile, "Asset file zip handle must be NULL");
assertNull(file->zipFile, "Asset file zip handle must be NULL before open");
assertNotNull(file->output, "Asset file output cannot be NULL");
assetscript_t *script = (assetscript_t *)file->output;
// Open the asset for buffering
errorChain(assetFileOpen(file));
// Request loading
if(!lua_load(
script->ctx->luaState,
assetScriptReader,
file,
file->filename,
NULL
) == LUA_OK) {
const char_t *strErr = lua_tostring(script->ctx->luaState, -1);
lua_pop(script->ctx->luaState, 1);
errorThrow("Failed to load Lua script: %s", strErr);
// Accumulate full source into a dynamically grown buffer.
size_t srcLen = 0;
size_t capacity = ASSET_SCRIPT_CHUNK_SIZE;
char_t *src = (char_t *)malloc(capacity + 1);
if(!src) {
assetFileClose(file);
errorThrow("Out of memory reading script: %s", file->filename);
}
// Now loaded, exec
if(lua_pcall(script->ctx->luaState, 0, LUA_MULTRET, 0) != LUA_OK) {
const char_t *strErr = lua_tostring(script->ctx->luaState, -1);
lua_pop(script->ctx->luaState, 1);
errorThrow("Failed to execute Lua script: %s", strErr);
while(1) {
if(srcLen + ASSET_SCRIPT_CHUNK_SIZE > capacity) {
capacity = srcLen + ASSET_SCRIPT_CHUNK_SIZE;
char_t *tmp = (char_t *)realloc(src, capacity + 1);
if(!tmp) {
free(src);
assetFileClose(file);
errorThrow("Out of memory reading script: %s", file->filename);
}
src = tmp;
}
zip_int64_t n = zip_fread(
file->zipFile, src + srcLen, ASSET_SCRIPT_CHUNK_SIZE
);
if(n <= 0) break;
srcLen += (size_t)n;
}
src[srcLen] = '\0';
errorret_t closeRet = assetFileClose(file);
jerry_value_t result = jerry_eval(
(const jerry_char_t *)src,
srcLen,
JERRY_PARSE_NO_OPTS
);
free(src);
if(jerry_value_is_exception(result)) {
jerry_value_t errVal = jerry_exception_value(result, false);
jerry_value_t errStr = jerry_value_to_string(errVal);
char_t buf[256];
jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
jerry_value_free(result);
errorThrow("Script error in '%s': %s", file->filename, buf);
}
// Close the file
return assetFileClose(file);
if(script->resultOut != NULL) {
*(script->resultOut) = result;
} else {
jerry_value_free(result);
}
return closeRet;
}
errorret_t assetScriptLoad(const char_t *path, scriptcontext_t *ctx) {
errorret_t assetScriptLoad(
const char_t *path,
jerry_value_t *resultOut
) {
assertNotNull(path, "Script path cannot be NULL");
assertNotNull(ctx, "Script context cannot be NULL");
assetscript_t script;
script.ctx = ctx;
assetscript_t scriptData;
scriptData.resultOut = resultOut;
return assetLoad(
path,
assetScriptLoader,
NULL,
&script
);
}
const char_t * assetScriptReader(lua_State* L, void* data, size_t* size) {
assetfile_t *file = (assetfile_t*)data;
assertNotNull(file, "Script asset file cannot be NULL");
assertNotNull(file->zipFile, "Script asset zip handle cannot be NULL");
assertNotNull(file->output, "Script asset output cannot be NULL");
assetscript_t *script = (assetscript_t *)file->output;
assertNotNull(script, "Script asset output cannot be NULL");
zip_int64_t read = zip_fread(
file->zipFile,
script->buffer,
sizeof(script->buffer)
);
if(read < 0) {
*size = 0;
return NULL;
}
*size = (size_t)read;
return script->buffer;
return assetLoad(path, assetScriptLoader, NULL, &scriptData);
}
@@ -7,21 +7,16 @@
#pragma once
#include "asset/asset.h"
#include "script/scriptcontext.h"
#define ASSET_SCRIPT_BUFFER_SIZE 1024
#define ASSET_SCRIPT_CHUNK_SIZE 1024
typedef struct {
void *nothing;
} assetscriptloaderparams_t;
typedef struct {
scriptcontext_t *ctx;
char_t buffer[ASSET_SCRIPT_BUFFER_SIZE];
jerry_value_t *resultOut;
} assetscript_t;
/**
* Handler for script assets.
* Handler for script assets. Reads the full source, evaluates it with
* JerryScript, and optionally stores the result.
*
* @param file Asset file to load the script from.
* @return Any error that occurs during loading.
@@ -32,17 +27,12 @@ errorret_t assetScriptLoader(assetfile_t *file);
* Loads a script from the specified path.
*
* @param path Path to the script asset.
* @param ctx Script context to load the script into.
* @param resultOut Optional out-parameter for the script return value.
* Caller must call jerry_value_free() if non-NULL.
* Pass NULL to discard the return value.
* @return Any error that occurs during loading.
*/
errorret_t assetScriptLoad(const char_t *path, scriptcontext_t *ctx);
/**
* Reader function for Lua to read script data from the asset.
*
* @param L Lua state.
* @param data Pointer to the scriptcontext_t structure.
* @param size Pointer to store the size of the read data.
* @return Pointer to the read data buffer.
*/
const char_t * assetScriptReader(lua_State* L, void* data, size_t* size);
errorret_t assetScriptLoad(
const char_t *path,
jerry_value_t *resultOut
);
@@ -3,8 +3,7 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
event.c
console.c
)
+192
View File
@@ -0,0 +1,192 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "console.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "input/input.h"
#include "log/log.h"
#include "engine/engine.h"
#include "script/scriptmanager.h"
#include "display/shader/shaderunlit.h"
#include "display/text/text.h"
#include "display/spritebatch/spritebatch.h"
console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
#ifdef DUSK_CONSOLE_POSIX
threadInit(&CONSOLE.thread, consoleInputThread);
threadMutexInit(&CONSOLE.execMutex);
threadMutexInit(&CONSOLE.printMutex);
threadStartRequest(&CONSOLE.thread);
#endif
}
void consolePrint(const char_t *message, ...) {
char_t buffer[CONSOLE_LINE_MAX];
va_list args;
va_start(args, message);
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
va_end(args);
#ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.printMutex);
#endif
memoryMove(
CONSOLE.line[0],
CONSOLE.line[1],
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
);
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.printMutex);
#endif
logDebug("%s\n", buffer);
}
void consoleExec(const char_t *line) {
assertNotNull(line, "line must not be NULL");
#ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.execMutex);
#endif
assertTrue(
CONSOLE.execBufferCount < CONSOLE_EXEC_BUFFER_MAX,
"Console exec buffer is full"
);
stringCopy(
CONSOLE.execBuffer[CONSOLE.execBufferCount],
line,
CONSOLE_LINE_MAX
);
CONSOLE.execBufferCount++;
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.execMutex);
#endif
}
void consoleUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) return;
#endif
if(inputPressed(INPUT_ACTION_CONSOLE)) {
CONSOLE.visible = !CONSOLE.visible;
}
if(CONSOLE.execBufferCount == 0) return;
#ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.execMutex);
#endif
char_t execBuffer[CONSOLE_EXEC_BUFFER_MAX][CONSOLE_LINE_MAX];
uint32_t execBufferCount = CONSOLE.execBufferCount;
memoryCopy(execBuffer, CONSOLE.execBuffer, sizeof(execBuffer));
CONSOLE.execBufferCount = 0;
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.execMutex);
#endif
for(uint32_t i = 0; i < execBufferCount; i++) {
jerry_value_t result = 0;
errorret_t err = scriptManagerExec(execBuffer[i], &result);
if(err.code != ERROR_OK) {
consolePrint("Error: %s", err.state->message);
errorCatch(err);
} else if(!jerry_value_is_undefined(result)) {
jerry_value_t strVal = jerry_value_to_string(result);
char_t buf[CONSOLE_LINE_MAX];
jerry_size_t len = jerry_string_to_buffer(
strVal, JERRY_ENCODING_UTF8, (jerry_char_t*)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(strVal);
consolePrint("%s", buf);
}
if(result != 0) jerry_value_free(result);
}
}
errorret_t consoleDraw(void) {
if(!CONSOLE.visible) errorOk();
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
errorChain(textDraw(
0, FONT_TILESET_DEFAULT.tileHeight * i,
CONSOLE.line[i],
COLOR_WHITE,
&FONT_TILESET_DEFAULT,
&FONT_TEXTURE_DEFAULT
));
}
return spriteBatchFlush();
}
void consoleDispose(void) {
#ifdef DUSK_CONSOLE_POSIX
threadStop(&CONSOLE.thread);
threadMutexDispose(&CONSOLE.execMutex);
threadMutexDispose(&CONSOLE.printMutex);
#endif
}
#ifdef DUSK_CONSOLE_POSIX
void consoleInputThread(thread_t *thread) {
assertNotNull(thread, "Thread cannot be NULL.");
char_t line[CONSOLE_LINE_MAX];
struct pollfd pfd = {
.fd = STDIN_FILENO,
.events = POLLIN
};
while(!threadShouldStop(thread) && ENGINE.running) {
int32_t rc = poll(&pfd, 1, CONSOLE_POSIX_POLL_RATE);
if(rc == 0) continue;
if(rc < 0) {
if(errno == EINTR) continue;
assertUnreachable("poll() failed with unexpected error.");
}
if(pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) break;
if(!(pfd.revents & POLLIN)) {
pfd.revents = 0;
continue;
}
if(!fgets(line, CONSOLE_LINE_MAX, stdin)) {
if(feof(stdin)) break;
clearerr(stdin);
continue;
}
size_t len = strlen(line);
while(len && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
if(len > 0) consoleExec(line);
pfd.revents = 0;
}
}
#endif
+79
View File
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "consoledefs.h"
#include "error/error.h"
#include "dusk.h"
#ifdef DUSK_CONSOLE_POSIX
#include "thread/thread.h"
#include <poll.h>
#include <unistd.h>
#define CONSOLE_POSIX_POLL_RATE 75
#endif
typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
bool_t visible;
char_t execBuffer[CONSOLE_EXEC_BUFFER_MAX][CONSOLE_LINE_MAX];
uint32_t execBufferCount;
#ifdef DUSK_CONSOLE_POSIX
thread_t thread;
threadmutex_t execMutex;
threadmutex_t printMutex;
#endif
} console_t;
extern console_t CONSOLE;
/**
* Initializes the console.
*/
void consoleInit(void);
/**
* Prints a message to the console history.
*
* @param message The message to print (printf-style).
*/
void consolePrint(const char_t *message, ...);
/**
* Queues a JS string for execution on the main thread. Thread-safe.
*
* @param line The JS source line to execute.
*/
void consoleExec(const char_t *line);
/**
* Processes pending queued script lines. Call once per frame from main thread.
*/
void consoleUpdate(void);
/**
* Renders the console history to the screen (UI space).
*
* @return The error return value.
*/
errorret_t consoleDraw(void);
/**
* Disposes of the console.
*/
void consoleDispose(void);
#ifdef DUSK_CONSOLE_POSIX
/**
* Input thread handler for POSIX stdin.
*
* @param thread The thread that is running.
*/
void consoleInputThread(thread_t *thread);
#endif
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#define CONSOLE_LINE_MAX 512
#define CONSOLE_HISTORY_MAX 16
#define CONSOLE_EXEC_BUFFER_MAX 32
@@ -3,8 +3,7 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduleui.c
cutscene.c
)
+187
View File
@@ -0,0 +1,187 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "cutscene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "script/module/modulebase.h"
#include "script/module/cutscene/modulecutscene.h"
#include "console/console.h"
#include "time/time.h"
cutscene_t CUTSCENE;
static errorret_t cutsceneCallMethod(const char_t *method) {
if(CUTSCENE.scriptRef == CUTSCENE_SCRIPT_REF_NONE) errorOk();
jerry_value_t key = jerry_string_sz(method);
jerry_value_t fn = jerry_object_get(CUTSCENE.scriptRef, key);
jerry_value_free(key);
if(!jerry_value_is_function(fn)) {
jerry_value_free(fn);
errorOk();
}
jerry_value_t result = jerry_call(fn, CUTSCENE.scriptRef, NULL, 0);
jerry_value_free(fn);
if(jerry_value_is_exception(result)) {
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
char_t errMsg[512];
moduleBaseExceptionMessage(result, errMsg, sizeof(errMsg));
jerry_value_free(result);
errorThrow(
"Cutscene event '%s' %s failed: %s",
event->script.script, method, errMsg
);
}
jerry_value_free(result);
errorOk();
}
static errorret_t cutsceneEventStart(void) {
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) {
if(event->native.onStart) errorChain(event->native.onStart());
} else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) {
jerry_value_t eventClass = CUTSCENE_SCRIPT_REF_NONE;
errorChain(scriptManagerExecFile(event->script.script, &eventClass));
if(!jerry_value_is_function(eventClass)) {
if(eventClass != CUTSCENE_SCRIPT_REF_NONE) jerry_value_free(eventClass);
errorThrow(
"Cutscene event '%s' must export a constructor function",
event->script.script
);
}
jerry_value_t eventObj = jerry_construct(eventClass, NULL, 0);
jerry_value_free(eventClass);
if(jerry_value_is_exception(eventObj)) {
char_t errMsg[512];
moduleBaseExceptionMessage(eventObj, errMsg, sizeof(errMsg));
jerry_value_free(eventObj);
errorThrow(
"Cutscene event '%s' constructor threw: %s",
event->script.script, errMsg
);
}
CUTSCENE.scriptRef = eventObj;
errorChain(cutsceneCallMethod("onStart"));
}
errorOk();
}
static errorret_t cutsceneEventEnd(void) {
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) {
if(event->native.onEnd) errorChain(event->native.onEnd());
} else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) {
errorret_t err = cutsceneCallMethod("onEnd");
if(CUTSCENE.scriptRef != CUTSCENE_SCRIPT_REF_NONE) {
jerry_value_free(CUTSCENE.scriptRef);
CUTSCENE.scriptRef = CUTSCENE_SCRIPT_REF_NONE;
}
errorChain(err);
}
errorOk();
}
errorret_t cutsceneInit(void) {
memoryZero(&CUTSCENE, sizeof(cutscene_t));
CUTSCENE.scriptRef = CUTSCENE_SCRIPT_REF_NONE;
CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE;
consolePrint("Cutscene init");
errorOk();
}
errorret_t cutsceneUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) {
errorOk();
}
#endif
errorChain(moduleCutsceneUpdate());
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(event->type == CUTSCENE_EVENT_TYPE_NATIVE) {
if(event->native.onUpdate) errorChain(event->native.onUpdate());
} else if(event->type == CUTSCENE_EVENT_TYPE_SCRIPT) {
errorChain(cutsceneCallMethod("onUpdate"));
}
errorOk();
}
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
) {
assertNotNull(events, "Events cannot be null");
assertTrue(eventCount > 0, "Event count must be greater than zero");
assertTrue(
eventCount <= CUTSCENE_EVENT_COUNT_MAX,
"Event count exceeds CUTSCENE_EVENT_COUNT_MAX"
);
if(CUTSCENE.active) {
errorChain(cutsceneStop());
}
memoryCopy(CUTSCENE.events, events, sizeof(cutsceneevent_t) * eventCount);
CUTSCENE.eventCount = eventCount;
CUTSCENE.eventCurrent = 0;
CUTSCENE.active = true;
errorChain(cutsceneEventStart());
consolePrint("Cutscene play");
errorOk();
}
errorret_t cutsceneAdvance(void) {
if(!CUTSCENE.active) errorOk();
errorChain(cutsceneEventEnd());
CUTSCENE.eventCurrent++;
if(CUTSCENE.eventCurrent >= CUTSCENE.eventCount) {
CUTSCENE.active = false;
errorOk();
}
errorChain(cutsceneEventStart());
consolePrint("Cutscene advance");
errorOk();
}
errorret_t cutsceneStop(void) {
if(!CUTSCENE.active) errorOk();
errorChain(cutsceneEventEnd());
CUTSCENE.active = false;
consolePrint("Cutscene stop");
errorOk();
}
errorret_t cutsceneDispose(void) {
errorChain(cutsceneStop());
errorOk();
}
bool_t cutsceneIsActive(void) {
return CUTSCENE.active;
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "script/scriptmanager.h"
#include "error/error.h"
#define CUTSCENE_EVENT_COUNT_MAX 16
#define CUTSCENE_SCRIPT_REF_NONE ((jerry_value_t)0)
typedef enum {
CUTSCENE_EVENT_TYPE_NULL,
CUTSCENE_EVENT_TYPE_NATIVE,
CUTSCENE_EVENT_TYPE_SCRIPT,
CUTSCENE_EVENT_TYPE_COUNT
} cutsceneeventtype_t;
typedef struct {
cutsceneeventtype_t type;
union {
struct {
errorret_t (*onStart)(void);
errorret_t (*onEnd)(void);
errorret_t (*onUpdate)(void);
} native;
struct {
const char_t *script;
} script;
};
} cutsceneevent_t;
typedef struct {
cutsceneevent_t events[CUTSCENE_EVENT_COUNT_MAX];
uint8_t eventCount;
uint8_t eventCurrent;
bool_t active;
jerry_value_t scriptRef;
jerry_value_t activeRef;
} cutscene_t;
extern cutscene_t CUTSCENE;
/**
* Initializes the cutscene manager.
*
* @return Any error state that happened.
*/
errorret_t cutsceneInit(void);
/**
* Ticks the active cutscene event, calling its onUpdate callback.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneUpdate(void);
/**
* Copies the given event array and begins playing from the first
* event. If a cutscene is already playing it is stopped first.
*
* @param events Array of events to copy.
* @param eventCount Number of events. Must be > 0 and
* <= CUTSCENE_EVENT_COUNT_MAX.
* @return Any error state that happened.
*/
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
);
/**
* Ends the current event and starts the next one.
* Marks the cutscene as inactive after the last event ends.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneAdvance(void);
/**
* Ends the current event and stops the cutscene immediately.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneStop(void);
/**
* Disposes of the cutscene manager, stopping any active cutscene.
*
* @return Any error state that happened.
*/
errorret_t cutsceneDispose(void);
/**
* Returns whether a cutscene is currently playing.
*
* @return true if a cutscene is active.
*/
bool_t cutsceneIsActive(void);
+8 -2
View File
@@ -25,8 +25,6 @@
#include "display/shader/shaderunlit.h"
#include "time/time.h"
#include "script/module/display/moduleshader.h"
display_t DISPLAY = { 0 };
errorret_t displayInit(void) {
@@ -35,6 +33,7 @@ errorret_t displayInit(void) {
#ifdef displayPlatformInit
errorChain(displayPlatformInit());
#endif
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
errorChain(textureInit(
&TEXTURE_WHITE, 4, 4,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
@@ -112,6 +111,13 @@ errorret_t displayUpdate(void) {
errorOk();
}
errorret_t displaySetState(displaystate_t state) {
#ifdef displayPlatformSetState
errorChain(displayPlatformSetState(state));
#endif
errorOk();
}
errorret_t displayDispose(void) {
errorChain(shaderDispose(&SHADER_UNLIT));
errorChain(spriteBatchDispose());
+11
View File
@@ -40,15 +40,26 @@ extern display_t DISPLAY;
/**
* Initializes the display system.
* @return An errorret_t indicating success or failure.
*/
errorret_t displayInit(void);
/**
* Tells the display system to actually draw the frame.
* @return An errorret_t indicating success or failure.
*/
errorret_t displayUpdate(void);
/**
* Sets the display state.
*
* @param state The state to set.
* @return An errorret_t indicating success or failure.
*/
errorret_t displaySetState(displaystate_t state);
/**
* Disposes of the display system.
* @return An errorret_t indicating success or failure.
*/
errorret_t displayDispose(void);
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#define DISPLAY_STATE_FLAG_CULL (1 << 0)
#define DISPLAY_STATE_FLAG_DEPTH_TEST (1 << 1)
#define DISPLAY_STATE_FLAG_BLEND (1 << 2)
typedef struct {
uint8_t flags;
} displaystate_t;
+12 -4
View File
@@ -119,8 +119,12 @@ void capsuleBuffer(
{
const float_t yTop = cy + halfHeight;
const float_t yBot = cy - halfHeight;
const float_t vTop = 1.0f - (float_t)capRings / (float_t)(2 * capRings + 1);
const float_t vBot = 1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1);
const float_t vTop = (
1.0f - (float_t)capRings / (float_t)(2 * capRings + 1)
);
const float_t vBot = (
1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1)
);
for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep;
@@ -152,8 +156,12 @@ void capsuleBuffer(
const float_t lxz1 = radius * cosf(phi1);
const float_t lxz2 = radius * cosf(phi2);
const float_t v1 = 1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1);
const float_t v2 = 1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1);
const float_t v1 = (
1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1)
);
const float_t v2 = (
1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1)
);
for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep;
+1 -1
View File
@@ -28,7 +28,7 @@ errorret_t cubeInit();
* Buffers a 3D axis-aligned cube into the provided vertex array.
* Writes CUBE_VERTEX_COUNT vertices (6 faces x 6 vertices, CCW winding).
*
* @param vertices The vertex array to buffer into (must hold CUBE_VERTEX_COUNT).
* @param vertices The vertex array to buffer into.
* @param min The minimum XYZ corner of the cube.
* @param max The maximum XYZ corner of the cube.
* @param color The color applied to all vertices.
+6 -2
View File
@@ -34,10 +34,14 @@ errorret_t meshFlush(
#ifdef meshFlushPlatform
assertNotNull(mesh, "Mesh cannot be NULL");
assertTrue(vertexOffset >= 0, "Vertex offset must be non-negative.");
assertTrue(vertexCount == -1 || vertexCount > 0, "Vertex count incorrect.");
assertTrue(
vertexCount == -1 || vertexCount > 0, "Vertex count incorrect."
);
int32_t vertCount = meshGetVertexCount(mesh);
assertTrue(vertexOffset < (vertCount - 1), "Need at least one vert to draw");
assertTrue(
vertexOffset < (vertCount - 1), "Need at least one vert to draw"
);
int32_t drawCount = vertexCount;
if(vertexCount == -1) {
+1 -7
View File
@@ -9,12 +9,6 @@
#include "display/mesh/mesh.h"
#include "display/color.h"
/**
* Vertex layout:
* 2 triangular end-caps (3 verts each) = 6
* 3 rectangular side faces (6 verts each) = 18
* Total = 24
*/
#define TRIPRISM_VERTEX_COUNT 24
#define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
@@ -35,7 +29,7 @@ errorret_t triPrismInit();
* the prism is extruded along the Z axis between minZ and maxZ.
* Writes TRIPRISM_VERTEX_COUNT (24) vertices (CCW winding).
*
* @param vertices Vertex array to write into (must hold TRIPRISM_VERTEX_COUNT).
* @param vertices Vertex array to write into.
* @param x0,y0 First triangle vertex (XY).
* @param x1,y1 Second triangle vertex (XY).
* @param x2,y2 Third triangle vertex (XY).
+2 -2
View File
@@ -8,9 +8,9 @@
#pragma once
#include "display/mesh/quad.h"
#define SPRITEBATCH_SPRITES_MAX 32
#define SPRITEBATCH_SPRITES_MAX 256
#define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT)
#define SPRITEBATCH_FLUSH_COUNT 4
#define SPRITEBATCH_FLUSH_COUNT 8
#define SPRITEBATCH_SPRITES_MAX_PER_FLUSH (\
SPRITEBATCH_SPRITES_MAX / SPRITEBATCH_FLUSH_COUNT \
)
+5 -5
View File
@@ -13,19 +13,19 @@
#include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h"
texture_t DEFAULT_FONT_TEXTURE;
tileset_t DEFAULT_FONT_TILESET;
texture_t FONT_TEXTURE_DEFAULT;
tileset_t FONT_TILESET_DEFAULT;
errorret_t textInit(void) {
errorChain(assetTextureLoad(
"ui/minogram.png", &DEFAULT_FONT_TEXTURE, TEXTURE_FORMAT_RGBA
"ui/minogram.png", &FONT_TEXTURE_DEFAULT, TEXTURE_FORMAT_RGBA
));
errorChain(assetTilesetLoad("ui/minogram.dtf", &DEFAULT_FONT_TILESET));
errorChain(assetTilesetLoad("ui/minogram.dtf", &FONT_TILESET_DEFAULT));
errorOk();
}
errorret_t textDispose(void) {
errorChain(textureDispose(&DEFAULT_FONT_TEXTURE));
errorChain(textureDispose(&FONT_TEXTURE_DEFAULT));
errorOk();
}
+2 -2
View File
@@ -12,8 +12,8 @@
#define TEXT_CHAR_START '!'
extern texture_t DEFAULT_FONT_TEXTURE;
extern tileset_t DEFAULT_FONT_TILESET;
extern texture_t FONT_TEXTURE_DEFAULT;
extern tileset_t FONT_TILESET_DEFAULT;
/**
* Initializes the text system.
+1
View File
@@ -21,6 +21,7 @@
#include <cglm/cglm.h>
#include <cglm/types.h>
#include <cglm/vec2.h>
#include <jerryscript.h>
#include "duskplatform.h"
+34 -145
View File
@@ -12,85 +12,31 @@
#include "locale/localemanager.h"
#include "display/display.h"
#include "scene/scene.h"
#include "cutscene/cutscene.h"
#include "asset/asset.h"
#include "ui/ui.h"
#include "ui/uitextbox.h"
#include "script/scriptmanager.h"
#include "assert/assert.h"
#include "entity/entitymanager.h"
#include "entity/component/physics/entityphysics.h"
#include "game/game.h"
#include "physics/physicsmanager.h"
#include "network/network.h"
#include "network/networkinfo.h"
#include "system/system.h"
#include "console/console.h"
#include "item/backpack.h"
#include "display/mesh/cube.h"
#include "display/mesh/plane.h"
double jerry_port_current_time(void) {
dusktimeepoch_t epoch = timeGetEpoch();
return epoch.time * 1000.0;
}
int32_t jerry_port_local_tza(double unix_ms) {
(void) unix_ms;
return 0;
}
engine_t ENGINE;
entityid_t phBoxEnt;
componentid_t phBoxPhys;
float_t onlineSwapTime = FLT_MAX;
void goOnline();
void goOffline();
void onNetworkConnected(void *user) {
onlineSwapTime = TIME.time + 3.0f;
networkinfo_t info = networkGetInfo();
if(info.type == NETWORK_TYPE_IPV4) {
sceneLog(
"Connected to network with IPv4 address: " NETWORK_INFO_FORMAT_IPV4 "\n",
info.ipv4.ip[0], info.ipv4.ip[1], info.ipv4.ip[2], info.ipv4.ip[3]
);
#ifdef DUSK_NETWORK_IPV6
} else if(info.type == NETWORK_TYPE_IPV6) {
sceneLog(
"Connected to network with IPv6 address: " NETWORK_INFO_FORMAT_IPV6 "\n",
info.ipv6.ip[0], info.ipv6.ip[1], info.ipv6.ip[2], info.ipv6.ip[3],
info.ipv6.ip[4], info.ipv6.ip[5], info.ipv6.ip[6], info.ipv6.ip[7],
info.ipv6.ip[8], info.ipv6.ip[9], info.ipv6.ip[10], info.ipv6.ip[11],
info.ipv6.ip[12], info.ipv6.ip[13], info.ipv6.ip[14], info.ipv6.ip[15]
);
#endif
}
sceneLog("Network connected, I will disconnect at: %.2f1.\n", onlineSwapTime);
}
void onNetworkFailed(errorret_t error, void *user) {
onlineSwapTime = TIME.time + 3.0f;
sceneLog("Failed to connect to network, will try again at %.2f1.\n", onlineSwapTime);
}
void onNetworkDisconnected(errorret_t error, void *user) {
onlineSwapTime = TIME.time + 3.0f;
sceneLog("Network disconnected, will go online at %.2f1.\n", onlineSwapTime);
errorCatch(errorPrint(error));
}
void onNetworkDisconnectFinished(void *user) {
onlineSwapTime = TIME.time + 3.0f;
sceneLog("Finished disconnecting from network, will go online at %.2f1.\n", onlineSwapTime);
}
void goOnline() {
sceneLog("Going online...\n");
networkRequestConnection(
onNetworkConnected,
onNetworkFailed,
onNetworkDisconnected,
NULL
);
}
void goOffline() {
sceneLog("Going offline...\n");
networkRequestDisconnection(onNetworkDisconnectFinished, NULL);
}
errorret_t engineInit(const int32_t argc, const char_t **argv) {
memoryZero(&ENGINE, sizeof(engine_t));
@@ -101,106 +47,46 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
// Init systems. Order is important.
errorChain(systemInit());
timeInit();
consoleInit();
errorChain(inputInit());
errorChain(assetInit());
errorChain(localeManagerInit());
errorChain(scriptManagerInit());
errorChain(displayInit());
errorChain(uiInit());
errorChain(uiTextboxInit());
errorChain(cutsceneInit());
errorChain(sceneInit());
entityManagerInit();
backpackInit();
physicsManagerInit();
// errorChain(networkInit());
errorChain(gameInit());
sceneLog("Init done, going to queue online in 3 seconds...\n");
onlineSwapTime = TIME.time + 3.0f;
// Camera
entityid_t cam = entityManagerAdd();
componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION);
float_t distance = 6.0f;
entityPositionLookAt(
cam, camPos,
(vec3){ 0.0f, 1.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
(vec3){ distance, distance, distance }
);
componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA);
entityCameraSetZFar(cam, camCam, 100.0f);
// Floor
entityid_t floorEnt = entityManagerAdd();
componentid_t floorPos = entityAddComponent(floorEnt, COMPONENT_TYPE_POSITION);
componentid_t floorMesh = entityAddComponent(floorEnt, COMPONENT_TYPE_MESH);
componentid_t floorMat = entityAddComponent(floorEnt, COMPONENT_TYPE_MATERIAL);
componentid_t floorPhys = entityAddComponent(floorEnt, COMPONENT_TYPE_PHYSICS);
entityPositionSetPosition(floorEnt, floorPos, (vec3){ -5.0f, 0.0f, -5.0f });
entityPositionSetScale(floorEnt, floorPos, (vec3){ 10.0f, 1.0f, 10.0f });
entityMeshSetMesh(floorEnt, floorMesh, &PLANE_MESH_SIMPLE);
entityMaterialGetShaderMaterial(floorEnt, floorMat)->unlit.color = COLOR_GREEN;
entityphysics_t *floorPhysData = entityPhysicsGet(floorEnt, floorPhys);
floorPhysData->type = PHYSICS_BODY_STATIC;
floorPhysData->shape.type = PHYSICS_SHAPE_PLANE;
floorPhysData->shape.data.plane.normal[0] = 0.0f;
floorPhysData->shape.data.plane.normal[1] = 1.0f;
floorPhysData->shape.data.plane.normal[2] = 0.0f;
floorPhysData->shape.data.plane.distance = 0.0f;
// Box
phBoxEnt = entityManagerAdd();
componentid_t boxPos = entityAddComponent(phBoxEnt, COMPONENT_TYPE_POSITION);
componentid_t boxMesh = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MESH);
componentid_t boxMat = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MATERIAL);
phBoxPhys = entityAddComponent(phBoxEnt, COMPONENT_TYPE_PHYSICS);
entityMeshSetMesh(phBoxEnt, boxMesh, &CUBE_MESH_SIMPLE);
entityMaterialGetShaderMaterial(phBoxEnt, boxMat)->unlit.color = COLOR_RED;
entityPositionSetPosition(phBoxEnt, boxPos, (vec3){ 0.0f, 4.0f, 0.0f });
errorChain(networkInit());
/* Run the init script. */
scriptcontext_t ctx;
errorChain(scriptContextInit(&ctx));
errorChain(scriptContextExecFile(&ctx, "init.lua"));
scriptContextDispose(&ctx);
consolePrint("Engine initialized");
errorChain(scriptManagerExecFile("init.js", NULL));
errorOk();
}
errorret_t engineUpdate(void) {
// errorChain(networkUpdate());
// Order here is important.
errorChain(networkUpdate());
timeUpdate();
inputUpdate();
consoleUpdate();
uiUpdate();
errorChain(sceneUpdate());
/* Reset the box to its start position on demand. */
if(inputIsDown(INPUT_ACTION_ACCEPT)) {
componentid_t posComp = entityGetComponent(phBoxEnt, COMPONENT_TYPE_POSITION);
entityPositionSetPosition(phBoxEnt, posComp, (vec3){ 0.0f, 4.0f, 0.0f });
entityPhysicsSetVelocity(phBoxEnt, phBoxPhys, (vec3){ 0.0f, 0.0f, 0.0f });
}
/* Step physics: positions are updated directly on POSITION components. */
errorChain(uiTextboxUpdate());
physicsManagerUpdate();
errorChain(gameUpdate());
errorChain(displayUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
if(TIME.time >= onlineSwapTime) {
onlineSwapTime = FLT_MAX;
if(NETWORK.state == NETWORK_STATE_CONNECTED) {
goOffline();
} else {
goOnline();
}
}
// Scene update occurs last because only after rendering would we want to do
// scene switching, refer to sceneSet() for information.
errorChain(cutsceneUpdate());
errorChain(sceneUpdate());
errorOk();
}
@@ -210,13 +96,16 @@ void engineExit(void) {
}
errorret_t engineDispose(void) {
// errorChain(networkDispose());
uiTextboxDispose();
cutsceneDispose();
sceneDispose();
errorChain(gameDispose());
errorChain(networkDispose());
entityManagerDispose();
localeManagerDispose();
uiDispose();
consoleDispose();
errorChain(displayDispose());
errorChain(assetDispose());
errorOk();
}
+1
View File
@@ -36,3 +36,4 @@ errorret_t engineUpdate(void);
* Shuts down the engine.
*/
errorret_t engineDispose(void);
+9 -3
View File
@@ -12,8 +12,14 @@
componentdefinition_t COMPONENT_DEFINITIONS[] = {
[COMPONENT_TYPE_NULL] = { 0 },
#define X(enumName, type, field, iMethod, dMethod) \
[COMPONENT_TYPE_##enumName] = { .init = iMethod, .dispose = dMethod },
#define X(enm, type, field, iMethod, dMethod) \
[COMPONENT_TYPE_##enm] = { \
.enumName = #enm, \
.name = #field, \
.init = iMethod, \
.dispose = dMethod \
},
#include "componentlist.h"
#undef X
@@ -95,7 +101,7 @@ entityid_t componentGetEntitiesWithComponent(
"Component ID OOB in entitiesWithComponent lookup"
);
assertTrue(
componentGetIndex(i, used) < ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX,
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
"Component index OOB in entitiesWithComponent lookup"
);
assertTrue(
+2
View File
@@ -20,6 +20,8 @@ typedef union {
} componentdata_t;
typedef struct {
const char_t *enumName;
const char_t *name;
void (*init)(const entityid_t, const componentid_t);
void (*dispose)(const entityid_t, const componentid_t);
} componentdefinition_t;
+1
View File
@@ -5,3 +5,4 @@
add_subdirectory(display)
add_subdirectory(physics)
add_subdirectory(script)
@@ -6,6 +6,8 @@
*/
#include "entity/entitymanager.h"
#include "entity/entity.h"
#include "entity/component/display/entityposition.h"
#include "display/framebuffer/framebuffer.h"
#include "display/screen/screen.h"
@@ -19,42 +21,6 @@ void entityCameraInit(const entityid_t ent, const componentid_t comp) {
cam->perspective.fov = glm_rad(45.0f);
}
float_t entityCameraGetZNear(const entityid_t ent, const componentid_t comp) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
ent, comp, COMPONENT_TYPE_CAMERA
);
return cam->nearClip;
}
void entityCameraSetZNear(
const entityid_t ent,
const componentid_t comp,
const float_t zNear
) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
ent, comp, COMPONENT_TYPE_CAMERA
);
cam->nearClip = zNear;
}
float_t entityCameraGetZFar(const entityid_t ent, const componentid_t comp) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
ent, comp, COMPONENT_TYPE_CAMERA
);
return cam->farClip;
}
void entityCameraSetZFar(
const entityid_t ent,
const componentid_t comp,
const float_t zFar
) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
ent, comp, COMPONENT_TYPE_CAMERA
);
cam->farClip = zFar;
}
void entityCameraGetProjection(
const entityid_t ent,
const componentid_t comp,
@@ -93,3 +59,38 @@ void entityCameraGetProjection(
);
}
}
entityid_t entityCameraGetCurrent(void) {
entityid_t camEnts[ENTITY_COUNT_MAX];
componentid_t camComps[ENTITY_COUNT_MAX];
entityid_t count = componentGetEntitiesWithComponent(
COMPONENT_TYPE_CAMERA, camEnts, camComps
);
if(count == 0) return ENTITY_COUNT_MAX;
return camEnts[0];
}
void entityCameraGetForward(const entityid_t entityId, vec2 out) {
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
entityposition_t *pos = entityPositionGet(entityId, posComp);
// View matrix column layout: M[col][row],
// forward = {-M[0][2], -M[1][2], -M[2][2]}
float_t fx = -pos->transform[0][2];
float_t fz = -pos->transform[2][2];
float_t len = sqrtf(fx * fx + fz * fz);
if(len > 1e-6f) { fx /= len; fz /= len; }
out[0] = fx;
out[1] = fz;
}
void entityCameraGetRight(const entityid_t entityId, vec2 out) {
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
entityposition_t *pos = entityPositionGet(entityId, posComp);
// View matrix column layout: right = {M[0][0], M[1][0], M[2][0]}
float_t rx = pos->transform[0][0];
float_t rz = pos->transform[2][0];
float_t len = sqrtf(rx * rx + rz * rz);
if(len > 1e-6f) { rx /= len; rz /= len; }
out[0] = rx;
out[1] = rz;
}
@@ -55,45 +55,25 @@ void entityCameraGetProjection(
);
/**
* Gets the near clip distance of a camera.
*
* @param ent The entity ID.
* @param comp The component ID.
* @return The near clip distance.
* Returns the entity ID of the first active camera, or ENTITY_COUNT_MAX if
* none are active.
*/
float_t entityCameraGetZNear(const entityid_t ent, const componentid_t comp);
entityid_t entityCameraGetCurrent(void);
/**
* Sets the near clip distance of a camera.
* Gets the camera's horizontal forward direction (XZ plane) from its position
* component. Automatically finds the position component on the entity.
*
* @param ent The entity ID.
* @param comp The component ID.
* @param zNear The near clip distance.
* @param entityId The camera entity ID.
* @param out Output vec2: {forwardX, forwardZ} normalized.
*/
void entityCameraSetZNear(
const entityid_t ent,
const componentid_t comp,
const float_t zNear
);
void entityCameraGetForward(const entityid_t entityId, vec2 out);
/**
* Gets the far clip distance of a camera.
* Gets the camera's horizontal right direction (XZ plane) from its position
* component. Automatically finds the position component on the entity.
*
* @param ent The entity ID.
* @param comp The component ID.
* @return The far clip distance.
* @param entityId The camera entity ID.
* @param out Output vec2: {rightX, rightZ} normalized.
*/
float_t entityCameraGetZFar(const entityid_t ent, const componentid_t comp);
/**
* Sets the far clip distance of a camera.
*
* @param ent The entity ID.
* @param comp The component ID.
* @param zFar The far clip distance.
*/
void entityCameraSetZFar(
const entityid_t ent,
const componentid_t comp,
const float_t zFar
);
void entityCameraGetRight(const entityid_t entityId, vec2 out);
@@ -49,3 +49,14 @@ void entityMaterialSetShader(
);
mat->shader = shader;
}
void entityMaterialSetColor(
const entityid_t entityId,
const componentid_t componentId,
const color_t color
) {
entitymaterial_t *mat = componentGetData(
entityId, componentId, COMPONENT_TYPE_MATERIAL
);
mat->material.unlit.color = color;
}
@@ -61,3 +61,16 @@ void entityMaterialSetShader(
const componentid_t componentId,
shader_t *shader
);
/**
* Sets the unlit color for the given entity and component.
*
* @param entityId The entity ID.
* @param componentId The component ID.
* @param color The color to set.
*/
void entityMaterialSetColor(
const entityid_t entityId,
const componentid_t componentId,
const color_t color
);
@@ -5,6 +5,7 @@
* https://opensource.org/licenses/MIT
*/
#include "entitymesh.h"
#include "entity/entitymanager.h"
void entityMeshInit(
@@ -37,3 +38,9 @@ void entityMeshSetMesh(
);
comp->mesh = mesh;
}
void entityMeshDispose(
const entityid_t entityId,
const componentid_t componentId
) {
}
+12 -1
View File
@@ -41,10 +41,21 @@ mesh_t * entityMeshGetMesh(
*
* @param entityId The entity ID.
* @param componentId The component ID.
* @param mesh A pointer to the mesh to associate with the entity mesh component.
* @param mesh A pointer to the mesh to associate.
*/
void entityMeshSetMesh(
const entityid_t entityId,
const componentid_t componentId,
mesh_t *mesh
);
/**
* Disposes the entity mesh component.
*
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityMeshDispose(
const entityid_t entityId,
const componentid_t componentId
);
@@ -100,6 +100,25 @@ bool_t entityPhysicsIsOnGround(
return phys->onGround;
}
void entityPhysicsSetBodyType(
const entityid_t entityId,
const componentid_t componentId,
const physicsbodytype_t type
) {
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
assertNotNull(phys, "Failed to get physics component data");
phys->type = type;
}
physicsbodytype_t entityPhysicsGetBodyType(
const entityid_t entityId,
const componentid_t componentId
) {
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
assertNotNull(phys, "Failed to get physics component data");
return phys->type;
}
void entityPhysicsDispose(
const entityid_t entityId,
const componentid_t componentId
@@ -122,6 +122,31 @@ bool_t entityPhysicsIsOnGround(
const componentid_t componentId
);
/**
* Sets the body type of the entity's physics body.
*
* @param entityId The entity ID.
* @param componentId The component ID.
* @param type The body type to set.
*/
void entityPhysicsSetBodyType(
const entityid_t entityId,
const componentid_t componentId,
const physicsbodytype_t type
);
/**
* Gets the body type of the entity's physics body.
*
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The body type of the physics body.
*/
physicsbodytype_t entityPhysicsGetBodyType(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Releases the body slot back to PHYSICS_WORLD. Called automatically when
* the component is disposed via the component system.
@@ -3,5 +3,5 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Subdirs
add_subdirectory(module)
+7 -1
View File
@@ -11,8 +11,14 @@
#include "entity/component/display/entitymaterial.h"
#include "entity/component/physics/entityphysics.h"
// Name (Uppercase)
// Structure
// Field name (lowercase)
// Init function (optional)
// Dispose function (optional)
X(POSITION, entityposition_t, position, entityPositionInit, NULL)
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL)
X(MESH, entitymesh_t, mesh, entityMeshInit, NULL)
X(MESH, entitymesh_t, mesh, entityMeshInit, entityMeshDispose)
X(MATERIAL, entitymaterial_t, material, entityMaterialInit, NULL)
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose)
+2 -2
View File
@@ -8,8 +8,8 @@
#pragma once
#include "dusk.h"
#define ENTITY_COUNT_MAX 6
#define ENTITY_COMPONENT_COUNT_MAX 6
#define ENTITY_COUNT_MAX 20
#define ENTITY_COMPONENT_COUNT_MAX 8
typedef uint8_t entityid_t;
typedef uint8_t componentid_t;
+3 -3
View File
@@ -8,7 +8,7 @@
#include "entitymanager.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "scene/scene.h"
#include "console/console.h"
entitymanager_t ENTITY_MANAGER;
@@ -19,8 +19,8 @@ void entityManagerInit(void) {
sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
);
sceneLog(
"Entity Manager size: %zu bytes (%.2f KB)\n",
consolePrint(
"Entity Manager size: %zu bytes (%.2f KB)",
sizeof(entitymanager_t),
sizeof(entitymanager_t) / 1024.0f
);
+3 -1
View File
@@ -83,7 +83,9 @@ errorret_t errorChainImpl(
assertNotNull(retval.state->message, "Message cannot be NULL");
// Create a new line string.
int32_t newLineLen = snprintf(NULL, 0, ERROR_LINE_FORMAT, file, line, function);
int32_t newLineLen = snprintf(
NULL, 0, ERROR_LINE_FORMAT, file, line, function
);
assertTrue(newLineLen >= 0, "Line formatting failed");
char_t *newLine = (char_t *)memoryAllocate(newLineLen + 1);
snprintf(newLine, newLineLen + 1, ERROR_LINE_FORMAT, file, line, function);
-252
View File
@@ -1,252 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "event.h"
#include "assert/assert.h"
#include "util/memory.h"
void eventInit(
event_t *event,
eventlistener_t *array,
const uint16_t maxListeners
) {
assertNotNull(event, "Event cannot be NULL");
assertNotNull(array, "Listener array cannot be NULL");
assertTrue(maxListeners > 0, "Max listeners must be greater than 0");
memoryZero(event, sizeof(event_t));
event->listenerArray = array;
event->maxListeners = maxListeners;
}
eventsub_t eventSubscribeUser(
event_t *event,
const eventtype_t type,
const eventuserdata_t user
) {
assertNotNull(event, "Event cannot be NULL");
assertTrue(
event->listenerCount < event->maxListeners,
"Maximum number of listeners reached"
);
if(type == EVENT_TYPE_C) {
assertNotNull(
user.c.callback,
"C event listener callback cannot be NULL"
);
} else if(type == EVENT_TYPE_SCRIPT) {
assertNotNull(
user.script.context,
"Script event listener context cannot be NULL"
);
assertTrue(
user.script.luaFunctionRef != LUA_NOREF,
"Script event listener function reference is invalid"
);
} else {
assertUnreachable("Unknown event listener type");
}
// Gen a new ID
eventsub_t id = event->nextId++;
// Did we wrap?
assertTrue(event->nextId != 0, "Event subscription ID overflow");
// Append listener
eventlistener_t *listener = &event->listenerArray[event->listenerCount++];
memoryZero(listener, sizeof(eventlistener_t));
listener->user = user;
listener->id = id;
listener->type = type;
return id;
}
eventsub_t eventSubscribe(
event_t *event,
const eventcallback_t callback,
const void *user
) {
eventSubscribeUser(
event,
EVENT_TYPE_C,
(eventuserdata_t){ .c = { .callback = callback, .user = (void *)user } }
);
}
eventsub_t eventSubscribeScriptContext(
event_t *event,
scriptcontext_t *context,
const int functionIndex
) {
assertNotNull(context, "Script context cannot be NULL");
assertTrue(
lua_isfunction(context->luaState, functionIndex),
"Expected function at given index"
);
// Create a reference to the function
lua_pushvalue(context->luaState, functionIndex);
int funcRef = luaL_ref(context->luaState, LUA_REGISTRYINDEX);
eventscript_t scriptUser = {
.context = context,
.luaFunctionRef = funcRef
};
// Note to the context that it is now a part of this event
bool_t alreadySubbed = false;
uint8_t i;
i = 0;
do {
if(context->subscribedEvents[i] != event) {
i++;
continue;
}
if(context->subscribedEvents[i] == NULL) break;
alreadySubbed = true;
break;
} while(i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS);
if(!alreadySubbed) {
i = 0;
do {
if(context->subscribedEvents[i] != NULL) {
i++;
continue;
}
context->subscribedEvents[i] = event;
break;
} while(i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS);
assertTrue(
i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS,
"Script context has reached maximum event subscriptions"
);
}
return eventSubscribeUser(
event,
EVENT_TYPE_SCRIPT,
(eventuserdata_t){ .script = scriptUser }
);
}
void eventUnsubscribe(event_t *event, const eventsub_t id) {
assertNotNull(event, "Event cannot be NULL");
assertFalse(event->isInvoking, "Cannot unsubscribe while invoking event");
if(event->listenerCount == 0) return;
// Find listener
uint16_t index = 0;
do {
if(event->listenerArray[index].id == id) {
// Found it, remove by swapping with last and reducing count
event->listenerArray[index] = event->listenerArray[--event->listenerCount];
return;
}
index++;
} while(index < event->listenerCount);
// Did we find it?
if(index == event->listenerCount) return;
// Shift remaining listeners down (if any)
if(index < event->listenerCount - 1) {
memoryMove(
&event->listenerArray[index],
&event->listenerArray[index + 1],
sizeof(eventlistener_t) * (event->listenerCount - index - 1)
);
}
event->listenerCount--;
}
void eventUnsubscribeScriptContext(event_t *event, const scriptcontext_t *ctx) {
assertNotNull(event, "Event cannot be NULL");
assertNotNull(ctx, "Script context cannot be NULL");
if(event->listenerCount == 0) return;
uint16_t i = 0;
do {
eventlistener_t *listener = &event->listenerArray[i];
if(
listener->type != EVENT_TYPE_SCRIPT ||
listener->user.script.context != ctx
) {
i++;
continue;
}
// This listener belongs to the context and will need to go away. This will
// in turn decrement the listener count so we don't increment i here.
eventUnsubscribe(event, listener->id);
} while(i < event->listenerCount);
}
void eventInvoke(
event_t *event,
const void *eventParams,
const char_t *metatableName
) {
assertNotNull(event, "Event cannot be NULL");
if(event->listenerCount == 0) return;
event->isInvoking = true;
uint16_t i = 0;
eventdata_t data ={
.event = event,
.eventParams = eventParams,
};
do {
eventlistener_t *listener = &event->listenerArray[i];
if(listener->type == EVENT_TYPE_C) {
listener->user.c.callback(&data, listener->user.c);
} else if(listener->type == EVENT_TYPE_SCRIPT) {
// Call Lua function
lua_State *L = listener->user.script.context->luaState;
assertNotNull(L, "Lua state in event listener cannot be NULL");
// Push function
lua_rawgeti(L, LUA_REGISTRYINDEX, listener->user.script.luaFunctionRef);
if(eventParams != NULL && metatableName != NULL) {
lua_getmetatable(L, -1);
luaL_getmetatable(L, metatableName);
assertTrue(
lua_rawequal(L, -1, -2),
"Event parameter metatable does not match expected type"
);
}
// Call function with 1 arg, 0 return values
if(lua_pcall(L, 1, 0, 0) != LUA_OK) {
const char_t *strErr = lua_tostring(L, -1);
lua_pop(L, 1);
// Log error but continue
printf("Error invoking Lua event listener: %s\n", strErr);
}
} else {
assertUnreachable("Unknown event listener type");
}
i++;
} while(i < event->listenerCount);
event->isInvoking = false;
}
-121
View File
@@ -1,121 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "eventuser.h"
typedef struct event_s event_t;
typedef struct eventdata_s {
const void *eventParams;
const event_t *event;
} eventdata_t;
typedef struct eventlistener_s {
eventuserdata_t user;
eventtype_t type;
uint16_t id;
} eventlistener_t;
typedef uint16_t eventsub_t;
typedef struct event_s {
eventlistener_t *listenerArray;
uint16_t listenerCount;
uint16_t maxListeners;
eventsub_t nextId;
bool_t isInvoking;
} event_t;
/**
* Initialize an event structure.
*
* @param event The event to initialize.
* @param array The array to hold event listeners.
* @param maxListeners The maximum number of listeners.
*/
void eventInit(
event_t *event,
eventlistener_t *array,
const uint16_t maxListeners
);
/**
* Subscribe to an event.
*
* @param event The event to subscribe to.
* @param type The type of the event (C or Lua).
* @param user User data to pass to the callback.
* @return The subscription ID, used to unsubscribe later.
*/
eventsub_t eventSubscribeUser(
event_t *event,
const eventtype_t type,
const eventuserdata_t user
);
/**
* Subscribe to an event with a C function callback.
*
* @param event The event to subscribe to.
* @param callback The C function callback.
* @param user User data pointer to pass to the callback.
* @return The subscription ID, used to unsubscribe later.
*/
eventsub_t eventSubscribe(
event_t *event,
const eventcallback_t callback,
const void *user
);
/**
* Subscribe to an event with a script function callback.
*
* @param event The event to subscribe to.
* @param context The script context.
* @param functionIndex The index of the Lua function on the stack.
* @return The subscription ID, used to unsubscribe later.
*/
eventsub_t eventSubscribeScriptContext(
event_t *event,
scriptcontext_t *context,
const int functionIndex
);
/**
* Unsubscribe from an event.
*
* @param event The event to unsubscribe from.
* @param subscription The subscription ID to remove.
*/
void eventUnsubscribe(event_t *event, const eventsub_t subscription);
/**
* Unsubscribe all event listeners associated with a script context.
*
* @param event The event to unsubscribe from.
* @param context The script context whose listeners should be removed.
*/
void eventUnsubscribeScriptContext(event_t *event, const scriptcontext_t *ctx);
/**
* Invoke an event, calling all subscribed listeners. Optionally provide event
* parameters, and if desired pass a metatable name. Event listeners will be
* able to read event params, and if metatable name is provided the script
* listeners will lookup metatable information matching that name to access the
* params as a structure within the script.
*
* @param event The event to invoke.
* @param eventParams Parameters to pass to the event listeners.
* @param metatableName Metatable name. See details.
*/
void eventInvoke(
event_t *event,
const void *eventParams,
const char_t *metatableName
);
-14
View File
@@ -1,14 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct eventdata_s eventdata_t;
typedef struct eventc_s eventc_t;
typedef void (*eventcallback_t)(eventdata_t *data, eventc_t user);
-30
View File
@@ -1,30 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "eventcallback.h"
#include "script/scriptcontext.h"
typedef enum {
EVENT_TYPE_C = 0,
EVENT_TYPE_SCRIPT = 1
} eventtype_t;
typedef struct {
scriptcontext_t *context;
int luaFunctionRef;
} eventscript_t;
typedef struct eventc_s {
void *user;
eventcallback_t callback;
} eventc_t;
typedef union eventuserdata_u {
eventscript_t script;
eventc_t c;
} eventuserdata_t;
+14 -37
View File
@@ -27,13 +27,6 @@ errorret_t inputInit(void) {
errorChain(inputInitPlatform());
#endif
eventInit(
&INPUT.eventPressed, INPUT.pressedListeners, INPUT_LISTENER_PRESSED_MAX
);
eventInit(
&INPUT.eventReleased, INPUT.releasedListeners, INPUT_LISTENER_RELEASED_MAX
);
errorOk();
}
@@ -94,28 +87,6 @@ void inputUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) return;
#endif
if(INPUT.eventPressed.listenerCount > 0) {
action = &INPUT.actions[0];
do {
if(inputPressed(action->action)) {
inputevent_t inputEvent = { .action = action->action };
eventInvoke(&INPUT.eventPressed, &inputEvent, "input_mt");
}
action++;
} while(action < &INPUT.actions[INPUT_ACTION_COUNT]);
}
if(INPUT.eventReleased.listenerCount > 0) {
action = &INPUT.actions[0];
do {
if(inputReleased(action->action)) {
inputevent_t inputEvent = { .action = action->action };
eventInvoke(&INPUT.eventReleased, &inputEvent, "input_mt");
}
action++;
} while(action < &INPUT.actions[INPUT_ACTION_COUNT]);
}
}
float_t inputGetCurrentValue(const inputaction_t action) {
@@ -171,6 +142,16 @@ float_t inputAxis(const inputaction_t neg, const inputaction_t pos) {
return inputGetCurrentValue(pos) - inputGetCurrentValue(neg);
}
void inputAxis2D(
const inputaction_t negX, const inputaction_t posX,
const inputaction_t negY, const inputaction_t posY,
vec2 result
) {
assertNotNull(result, "Result vector cannot be null");
result[0] = inputAxis(negX, posX);
result[1] = inputAxis(negY, posY);
}
void inputBind(const inputbutton_t button, const inputaction_t act) {
assertTrue(
act < INPUT_ACTION_COUNT,
@@ -179,19 +160,15 @@ void inputBind(const inputbutton_t button, const inputaction_t act) {
assertTrue(act != INPUT_ACTION_NULL, "Cannot bind to NULL action");
// Get the button data for this button.
inputbuttondata_t *data = INPUT_BUTTON_DATA;
do {
if(memoryCompare(&data->button, &button, sizeof(inputbutton_t)) == 0) {
break;
}
data++;
} while(data->name != NULL);
assertNotNull(data->name, "Input button not found");
inputbuttondata_t *data = inputButtonGetData(button);
assertNotNull(data, "Input button not found");
// Bind the action.
data->action = act;
}
float_t inputDeadzone(const float_t rawValue, const float_t deadzone) {
if(rawValue < deadzone) return 0.0f;
return (rawValue - deadzone) / (1.0f - deadzone);
+2 -1
View File
@@ -5,6 +5,7 @@ LEFT,
RIGHT,
ACCEPT,
CANCEL,
RAGEQUIT
RAGEQUIT,
CONSOLE,
POINTERX,
POINTERY,
1 id, id
5 RIGHT, RIGHT
6 ACCEPT, ACCEPT
7 CANCEL, CANCEL
8 RAGEQUIT RAGEQUIT
9 CONSOLE
10 POINTERX, POINTERX
11 POINTERY, POINTERY
+16 -10
View File
@@ -9,23 +9,13 @@
#include "error/error.h"
#include "inputbutton.h"
#include "inputaction.h"
#include "event/event.h"
#define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
typedef struct {
const inputaction_t action;
} inputevent_t;
typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT];
eventlistener_t pressedListeners[INPUT_LISTENER_PRESSED_MAX];
event_t eventPressed;
eventlistener_t releasedListeners[INPUT_LISTENER_RELEASED_MAX];
event_t eventReleased;
inputplatform_t platform;
} input_t;
@@ -120,6 +110,22 @@ bool_t inputReleased(const inputaction_t action);
*/
float_t inputAxis(const inputaction_t neg, const inputaction_t pos);
/**
* Gets the values of a 2D input axis, defined by two pairs of actions (negative
* and positive for each axis).
*
* @param negX The action representing the negative direction of the X axis.
* @param posX The action representing the positive direction of the X axis.
* @param negY The action representing the negative direction of the Y axis.
* @param posY The action representing the positive direction of the Y axis.
* @param result A vec2 to store the resulting axis values (-1.0f to 1.0f).
*/
void inputAxis2D(
const inputaction_t negX, const inputaction_t posX,
const inputaction_t negY, const inputaction_t posY,
vec2 result
);
/**
* Binds an input button to an action.
*
+9 -9
View File
@@ -9,14 +9,14 @@
#include "assert/assert.h"
#include "util/string.h"
// inputaction_t inputActionGetByName(const char_t *name) {
// assertNotNull(name, "name must not be NULL");
inputaction_t inputActionGetByName(const char_t *name) {
assertNotNull(name, "name must not be NULL");
// for(inputaction_t i = 0; i < INPUT_ACTION_COUNT; i++) {
// if(INPUT_ACTION_IDS[i] == NULL) continue;
// if(stringCompareInsensitive(INPUT_ACTION_IDS[i], name) != 0) continue;
// return i;
// }
for(inputaction_t i = 0; i < INPUT_ACTION_COUNT; i++) {
if(INPUT_ACTION_IDS[i] == NULL) continue;
if(stringCompareInsensitive(INPUT_ACTION_IDS[i], name) != 0) continue;
return i;
}
// return INPUT_ACTION_COUNT;
// }
return INPUT_ACTION_COUNT;
}
+1 -1
View File
@@ -26,4 +26,4 @@ typedef struct {
* @param name The name of the input action.
* @return The input action, or INPUT_ACTION_COUNT if not found.
*/
// inputaction_t inputActionGetByName(const char_t *name);
inputaction_t inputActionGetByName(const char_t *name);
+13
View File
@@ -9,6 +9,7 @@
#include "input.h"
#include "assert/assert.h"
#include "util/string.h"
#include "util/memory.h"
inputbutton_t inputButtonGetByName(const char_t *name) {
assertNotNull(name, "name must not be NULL");
@@ -27,3 +28,15 @@ inputbutton_t inputButtonGetByName(const char_t *name) {
float_t inputButtonGetValue(const inputbutton_t button) {
return inputButtonGetValuePlatform(button);
}
inputbuttondata_t * inputButtonGetData(const inputbutton_t button) {
inputbuttondata_t *data = INPUT_BUTTON_DATA;
do {
if(memoryCompare(&data->button, &button, sizeof(inputbutton_t)) == 0) {
return data;
}
data++;
} while(data->name != NULL);
return NULL;
}
+8
View File
@@ -97,3 +97,11 @@ inputbutton_t inputButtonGetByName(const char_t *name);
* @return The current value of the input button (0.0f to 1.0f).
*/
float_t inputButtonGetValue(const inputbutton_t button);
/**
* Gets the button data for a specific input button.
*
* @param button The input button to get the data for.
* @return The button data, or NULL if not found.
*/
inputbuttondata_t * inputButtonGetData(const inputbutton_t button);
-6
View File
@@ -25,17 +25,11 @@ typedef enum {
typedef struct {
uint8_t ip[NETWORK_INFO_IPV4_OCTET_COUNT];
// uint8_t subnet[NETWORK_INFO_IPV4_OCTET_COUNT];
// uint8_t gateway[NETWORK_INFO_IPV4_OCTET_COUNT];
// uint8_t dns[NETWORK_INFO_IPV4_OCTET_COUNT][NETWORK_INFO_IPV4_DNS_COUNT_MAX];
} networkinfoipv4_t;
#ifdef DUSK_NETWORK_IPV6
typedef struct {
uint8_t ip[NETWORK_INFO_IPV6_OCTET_COUNT];
// uint8_t subnet[NETWORK_INFO_IPV6_OCTET_COUNT];
// uint8_t gateway[NETWORK_INFO_IPV6_OCTET_COUNT];
// uint8_t dns[NETWORK_INFO_IPV6_OCTET_COUNT][NETWORK_INFO_IPV6_DNS_COUNT_MAX];
} networkinfoipv6_t;
#endif
+1 -1
View File
@@ -45,7 +45,7 @@ void physicsWorldStep(const float_t dt) {
}
/* Phase 1: integrate dynamic bodies (gravity + velocity → position).
* Writes directly to pos->position matrix rebuilt at end. */
* Writes directly to pos->position, matrix rebuilt at end. */
for(entityid_t i = 0; i < physCount; i++) {
if(!positions[i]) continue;
entityphysics_t *phys = physBodies[i];
+130 -105
View File
@@ -11,60 +11,19 @@
#include "display/screen/screen.h"
#include "entity/entitymanager.h"
#include "display/shader/shaderunlit.h"
#include "display/mesh/cube.h"
#include "display/spritebatch/spritebatch.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "console/console.h"
#include "util/string.h"
#include "script/scriptmanager.h"
#include "script/module/scene/modulescene.h"
#include "script/module/cutscene/modulecutscene.h"
#include "ui/ui.h"
scene_t SCENE;
char_t SCENE_LOG[SCENE_LOG_SIZE];
void sceneLog(const char *fmt, ...) {
char temp[512];
// 1. Format input like printf
va_list args;
va_start(args, fmt);
vsnprintf(temp, sizeof(temp), fmt, args);
va_end(args);
printf("%s", temp);
// 2. Split into lines
char *lines[64];
int line_count = 0;
char *ptr = temp;
while (*ptr && line_count < 64) {
lines[line_count++] = ptr;
char *nl = strchr(ptr, '\n');
if (!nl) break;
*nl = '\0';
ptr = nl + 1;
}
// 3. Prepend lines in reverse order (so final order is correct)
for (int i = 0; i < line_count; i++) {
char new_log[SCENE_LOG_SIZE];
snprintf(new_log, sizeof(new_log), "%s\n%s", lines[i], SCENE_LOG);
// Copy back safely
strncpy(SCENE_LOG, new_log, SCENE_LOG_SIZE - 1);
SCENE_LOG[SCENE_LOG_SIZE - 1] = '\0';
}
}
errorret_t sceneInit(void) {
memoryZero(&SCENE, sizeof(scene_t));
memoryZero(SCENE_LOG, sizeof(SCENE_LOG));
sceneLog("Init\n");
SCENE.scriptRef = SCENE_SCRIPT_REF_NONE;
errorOk();
}
@@ -75,31 +34,35 @@ errorret_t sceneUpdate(void) {
}
#endif
if(stringCompare(SCENE.sceneNext, SCENE.sceneCurrent) != 0) {
errorChain(sceneSetImmediate(SCENE.sceneNext));
}
if(SCENE.sceneActive) {
errorChain(moduleSceneCall("update"));
}
errorOk();
}
dusktimeepoch_t LAST;
errorret_t sceneRender(void) {
// Get Cameras
entityid_t camEnts[ENTITY_COUNT_MAX];
componentid_t camComps[ENTITY_COUNT_MAX];
entityid_t camCount = componentGetEntitiesWithComponent(
COMPONENT_TYPE_CAMERA, camEnts, camComps
);
if(camCount == 0) errorOk();
// Get meshes
entityid_t meshEnts[ENTITY_COUNT_MAX];
componentid_t meshComps[ENTITY_COUNT_MAX];
entityid_t meshCount = componentGetEntitiesWithComponent(
COMPONENT_TYPE_MESH, meshEnts, meshComps
);
if(meshCount == 0) errorOk();
// Prep Matricies
shader_t *shaderCurrent = NULL;
mat4 view, proj, model;
errorChain(shaderBind(&SHADER_UNLIT));
// For each camera.
errorChain(displaySetState((displaystate_t){
// .flags = DISPLAY_STATE_FLAG_CULL | DISPLAY_STATE_FLAG_DEPTH_TEST
.flags = 0
}));
// For each camera
for(entityid_t camIndex = 0; camIndex < camCount; camIndex++) {
entityid_t camEnt = camEnts[camIndex];
componentid_t camComp = camComps[camIndex];
@@ -112,53 +75,67 @@ errorret_t sceneRender(void) {
entityCameraGetProjection(camEnt, camComp, proj);
entityPositionGetTransform(camEnt, camPos, view);
// For each mesh.
for(entityid_t meshIndex = 0; meshIndex < meshCount; meshIndex++) {
entityid_t meshEnt = meshEnts[meshIndex];
componentid_t meshComp = meshComps[meshIndex];
mesh_t *mesh = entityMeshGetMesh(meshEnt, meshComp);
// For each entity (I could iterate only over entities with mesh/material)
// but in future I may have different renderable types.
for(entityid_t entityId = 0; entityId < ENTITY_COUNT_MAX; entityId++) {
// Does this entity have a material?
componentid_t matComp = entityGetComponent(
entityId, COMPONENT_TYPE_MATERIAL
);
if(matComp != 0xFF) {
// Yes, get the mesh
componentid_t meshComp = entityGetComponent(
entityId, COMPONENT_TYPE_MESH
);
if(meshComp == 0xFF) {
logError("Entity with material but no mesh found\n");
continue;
}
mesh_t *mesh = entityMeshGetMesh(entityId, meshComp);
if(mesh == NULL) {
logError("Entity with material but no mesh found\n");
continue;
}
// Yes, get the material and shader.
shadermaterial_t *material = entityMaterialGetShaderMaterial(
entityId, matComp
);
shader_t *shader = entityMaterialGetShader(entityId, matComp);
if(shader == NULL) {
logError("Entity with material but no shader found\n");
continue;
}
// Get the transform.
componentid_t meshPos = entityGetComponent(
meshEnt, COMPONENT_TYPE_POSITION
entityId, COMPONENT_TYPE_POSITION
);
if(meshPos == 0xFF) {
logError("Mesh entity without entity position found\n");
continue;
glm_mat4_identity(model);
} else {
entityPositionGetTransform(entityId, meshPos, model);
}
componentid_t meshMat = entityGetComponent(
meshEnt, COMPONENT_TYPE_MATERIAL
);
if(meshMat == 0xFF) {
logError("Mesh entity without material component found\n");
continue;
}
shadermaterial_t *material = entityMaterialGetShaderMaterial(
meshEnt, meshMat
);
shader_t *shader = entityMaterialGetShader(meshEnt, meshMat);
if(shader == NULL) {
logError("Mesh entity with material component without shader found\n");
continue;
}
entityPositionGetTransform(meshEnt, meshPos, model);
errorChain(shaderBind(shader));
// Render the mesh.
if(shaderCurrent != shader) {
shaderCurrent = shader;
errorChain(shaderBind(shaderCurrent));
errorChain(shaderSetMatrix(shader, SHADER_UNLIT_PROJECTION, proj));
errorChain(shaderSetMatrix(shader, SHADER_UNLIT_VIEW, view));
}
errorChain(shaderSetMatrix(shader, SHADER_UNLIT_MODEL, model));
errorChain(shaderSetMaterial(shader, material));
errorChain(meshDraw(mesh, 0, -1));
continue;
}
// No, in future there may be other renderable types.
}
}
// Here is where UI will go
// UI Rendering
glm_ortho(
0.0f, SCREEN.width,
SCREEN.height, 0.0f,
@@ -177,25 +154,73 @@ errorret_t sceneRender(void) {
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
// errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_0TEXTURE, &DEFAULT_FONT_TEXTURE));
// errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE));
errorChain(textDraw(
32, 32,
// "Hello World",
SCENE_LOG,
COLOR_WHITE,
&DEFAULT_FONT_TILESET,
&DEFAULT_FONT_TEXTURE
));
errorChain(spriteBatchFlush());
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
errorChain(uiRender());
errorOk();
}
errorret_t sceneSet(const char_t *script) {
errorret_t sceneSetImmediate(const char_t *scene) {
if(scene != SCENE.sceneNext) {
stringCopy(
SCENE.sceneNext,
scene == NULL ? "" : scene,
ASSET_FILE_PATH_MAX
);
}
if(SCENE.sceneActive) {
errorChain(moduleSceneCall("dispose"));
SCENE.sceneActive = false;
}
moduleCutsceneReset();
moduleSceneReset();
stringCopy(
SCENE.sceneCurrent,
scene == NULL ? "" : scene,
ASSET_FILE_PATH_MAX
);
if(scene != NULL) {
jerry_value_t sceneClass = SCENE_SCRIPT_REF_NONE;
errorChain(scriptManagerExecFile(scene, &sceneClass));
if(!jerry_value_is_function(sceneClass)) {
if(sceneClass != SCENE_SCRIPT_REF_NONE) jerry_value_free(sceneClass);
errorThrow("Scene '%s' must export a constructor function", scene);
}
jerry_value_t sceneObj = jerry_construct(sceneClass, NULL, 0);
jerry_value_free(sceneClass);
if(jerry_value_is_exception(sceneObj)) {
char_t errMsg[512];
moduleBaseExceptionMessage(sceneObj, errMsg, sizeof(errMsg));
jerry_value_free(sceneObj);
errorThrow("Scene '%s' constructor threw: %s", scene, errMsg);
}
SCENE.scriptRef = sceneObj;
SCENE.sceneActive = true;
}
errorOk();
}
void sceneDispose(void) {
void sceneSet(const char_t *scene) {
stringCopy(
SCENE.sceneNext,
scene == NULL ? "" : scene,
ASSET_FILE_PATH_MAX
);
}
errorret_t sceneDispose(void) {
errorChain(moduleSceneCall("dispose"));
errorOk();
}
+31 -16
View File
@@ -6,47 +6,62 @@
*/
#pragma once
#include "error/error.h"
#include "script/scriptmanager.h"
#include "asset/assetfile.h"
#define SCENE_EVENT_UPDATE_MAX 16
typedef struct {
void *nothing;
bool_t sceneActive;
jerry_value_t scriptRef;
char_t sceneCurrent[ASSET_FILE_PATH_MAX];
char_t sceneNext[ASSET_FILE_PATH_MAX];
} scene_t;
extern scene_t SCENE;
#define SCENE_LOG_SIZE 1024
extern char_t SCENE_LOG[SCENE_LOG_SIZE];
void sceneLog(const char *fmt, ...);
/** Sentinel value meaning no scene script is loaded. */
#define SCENE_SCRIPT_REF_NONE ((jerry_value_t)0)
/**
* Initialize the scene subsystem.
* Initializes the scene manager.
*
* @return The error return value.
* @return Any error state that happened.
*/
errorret_t sceneInit(void);
/**
* Update the current scene.
* Ticks the scene manager; may call the scene's update method.
*
* @return The error return value.
* @return Any error state that happened.
*/
errorret_t sceneUpdate(void);
/**
* Render the current scene.
* Renders the scene.
*
* @return The error return value.
* @return Any error state that happened.
*/
errorret_t sceneRender(void);
/**
* Set the current scene by script name.
* Immediately switches scenes, disposing the current one first.
*
* @param script The script name of the scene to set.
* @param scene Scene to switch to (asset file path).
* @return Any error state that happened.
*/
errorret_t sceneSet(const char_t *script);
errorret_t sceneSetImmediate(const char_t *scene);
/**
* Dispose of the scene subsystem.
* Requests a scene change on the next safe opportunity.
*
* @param scene Which scene to set.
*/
void sceneDispose(void);
void sceneSet(const char_t *scene);
/**
* Disposes of the current scene.
*
* @return Any error state that happened.
*/
errorret_t sceneDispose(void);
+1 -3
View File
@@ -7,9 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
scriptmanager.c
scriptcontext.c
scriptmodule.c
scriptproto.c
)
# Subdirectories
add_subdirectory(module)
-14
View File
@@ -1,14 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Subdirectories
add_subdirectory(display)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(locale)
add_subdirectory(system)
add_subdirectory(scene)
add_subdirectory(time)
add_subdirectory(ui)
@@ -0,0 +1,293 @@
// 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 "animation/animation.h"
#include "util/memory.h"
static scriptproto_t MODULE_ANIMATION_PROTO;
static inline animation_t *moduleAnimationGet(
const jerry_call_info_t *callInfo
) {
return (animation_t *)scriptProtoGetValue(
&MODULE_ANIMATION_PROTO, callInfo->this_value
);
}
// Extracts an easingtype_t from a JS value. Accepts either a plain number
// or an Easing.xxx function (which has a .type numeric property).
static easingtype_t moduleAnimationReadEasing(
const jerry_value_t val
) {
if(jerry_value_is_number(val)) {
return (easingtype_t)(uint32_t)jerry_value_as_number(val);
}
if(jerry_value_is_object(val) || jerry_value_is_function(val)) {
jerry_value_t key = jerry_string_sz("type");
jerry_value_t typeVal = jerry_object_get(val, key);
jerry_value_free(key);
easingtype_t result = EASING_LINEAR;
if(jerry_value_is_number(typeVal)) {
result = (easingtype_t)(uint32_t)jerry_value_as_number(typeVal);
}
jerry_value_free(typeVal);
return result;
}
return EASING_LINEAR;
}
// Fires onReach callbacks for any keyframes crossed between prevTime and
// newTime. Returns an exception value on error, or jerry_undefined().
static jerry_value_t moduleAnimationFireKeyframes(
const jerry_value_t thisVal,
float_t prevTime,
float_t newTime
) {
jerry_value_t kfKey = jerry_string_sz("_keyframes");
jerry_value_t kfArr = jerry_object_get(thisVal, kfKey);
jerry_value_free(kfKey);
if(!jerry_value_is_array(kfArr)) {
jerry_value_free(kfArr);
return jerry_undefined();
}
jerry_length_t len = jerry_array_length(kfArr);
for(jerry_length_t i = 0; i < len; i++) {
jerry_value_t kfObj = jerry_object_get_index(kfArr, i);
jerry_value_t tKey = jerry_string_sz("time");
jerry_value_t tVal = jerry_object_get(kfObj, tKey);
jerry_value_free(tKey);
float_t kfTime = (float_t)jerry_value_as_number(tVal);
jerry_value_free(tVal);
if(prevTime < kfTime && newTime >= kfTime) {
jerry_value_t cbKey = jerry_string_sz("onReach");
jerry_value_t cb = jerry_object_get(kfObj, cbKey);
jerry_value_free(cbKey);
if(jerry_value_is_function(cb)) {
jerry_value_t r = jerry_call(cb, thisVal, NULL, 0);
jerry_value_free(cb);
if(jerry_value_is_exception(r)) {
jerry_value_free(kfObj);
jerry_value_free(kfArr);
return r;
}
jerry_value_free(r);
} else {
jerry_value_free(cb);
}
}
jerry_value_free(kfObj);
}
jerry_value_free(kfArr);
return jerry_undefined();
}
// Fires onComplete once. Returns an exception on error or jerry_undefined().
static jerry_value_t moduleAnimationFireComplete(
const jerry_value_t thisVal
) {
jerry_value_t firedKey = jerry_string_sz("_completeFired");
jerry_value_t firedVal = jerry_object_get(thisVal, firedKey);
bool_t alreadyFired = jerry_value_is_true(firedVal);
jerry_value_free(firedVal);
if(alreadyFired) {
jerry_value_free(firedKey);
return jerry_undefined();
}
jerry_value_t trueVal = jerry_boolean(true);
jerry_object_set(thisVal, firedKey, trueVal);
jerry_value_free(firedKey);
jerry_value_free(trueVal);
jerry_value_t cbKey = jerry_string_sz("onComplete");
jerry_value_t cb = jerry_object_get(thisVal, cbKey);
jerry_value_free(cbKey);
if(jerry_value_is_function(cb)) {
jerry_value_t r = jerry_call(cb, thisVal, NULL, 0);
jerry_value_free(cb);
if(jerry_value_is_exception(r)) return r;
jerry_value_free(r);
} else {
jerry_value_free(cb);
}
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationConstructor) {
animation_t *anim = (animation_t *)memoryAllocate(sizeof(animation_t));
animationInit(anim);
if(argc > 0 && jerry_value_is_boolean(args[argc - 1])) {
anim->loop = jerry_value_is_true(args[argc - 1]);
}
if(argc > 0 && jerry_value_is_array(args[0])) {
jerry_length_t len = jerry_array_length(args[0]);
for(jerry_length_t i = 0; i < len; i++) {
jerry_value_t kf = jerry_object_get_index(args[0], i);
jerry_value_t tKey = jerry_string_sz("time");
jerry_value_t vKey = jerry_string_sz("value");
jerry_value_t eKey = jerry_string_sz("easing");
float_t t = (float_t)jerry_value_as_number(
jerry_object_get(kf, tKey)
);
float_t v = (float_t)jerry_value_as_number(
jerry_object_get(kf, vKey)
);
jerry_value_t eVal = jerry_object_get(kf, eKey);
easingtype_t e = moduleAnimationReadEasing(eVal);
jerry_value_free(tKey);
jerry_value_free(vKey);
jerry_value_free(eKey);
jerry_value_free(eVal);
jerry_value_free(kf);
animationAddKeyframe(anim, t, v, e);
}
// Store the JS keyframes array for onReach callback detection.
jerry_value_t kfKey = jerry_string_sz("_keyframes");
jerry_object_set(callInfo->this_value, kfKey, args[0]);
jerry_value_free(kfKey);
}
jerry_value_t firedKey = jerry_string_sz("_completeFired");
jerry_value_t falseVal = jerry_boolean(false);
jerry_object_set(callInfo->this_value, firedKey, falseVal);
jerry_value_free(firedKey);
jerry_value_free(falseVal);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_ANIMATION_PROTO.info, anim
);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationUpdate) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
if(argc < 1 || !jerry_value_is_number(args[0])) {
return moduleBaseThrow("update() expects a number delta");
}
float_t prevTime = anim->time;
float_t delta = (float_t)jerry_value_as_number(args[0]);
float_t value = animationUpdate(anim, delta);
jerry_value_t kfResult = moduleAnimationFireKeyframes(
callInfo->this_value, prevTime, anim->time
);
if(jerry_value_is_exception(kfResult)) return kfResult;
jerry_value_free(kfResult);
if(anim->complete) {
jerry_value_t cResult = moduleAnimationFireComplete(
callInfo->this_value
);
if(jerry_value_is_exception(cResult)) return cResult;
jerry_value_free(cResult);
}
return jerry_number((double)value);
}
moduleBaseFunction(moduleAnimationGetValue) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
return jerry_number((double)animationGetValue(anim));
}
moduleBaseFunction(moduleAnimationReset) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
animationReset(anim);
jerry_value_t key = jerry_string_sz("_completeFired");
jerry_value_t falseVal = jerry_boolean(false);
jerry_object_set(callInfo->this_value, key, falseVal);
jerry_value_free(key);
jerry_value_free(falseVal);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationGetComplete) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_boolean(anim->complete) : jerry_boolean(false);
}
moduleBaseFunction(moduleAnimationGetLoop) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_boolean(anim->loop) : jerry_boolean(false);
}
moduleBaseFunction(moduleAnimationSetLoop) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
if(argc < 1) return moduleBaseThrow("Expected boolean");
anim->loop = jerry_value_is_true(args[0]);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationGetTime) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_number((double)anim->time) : jerry_number(0.0);
}
moduleBaseFunction(moduleAnimationGetDuration) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_number((double)anim->duration) : jerry_number(0.0);
}
static void moduleAnimation(void) {
scriptProtoInit(
&MODULE_ANIMATION_PROTO,
"Animation",
sizeof(animation_t),
moduleAnimationConstructor
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "update", moduleAnimationUpdate
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "getValue", moduleAnimationGetValue
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "reset", moduleAnimationReset
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "complete",
moduleAnimationGetComplete, NULL
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "loop",
moduleAnimationGetLoop, moduleAnimationSetLoop
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "time",
moduleAnimationGetTime, NULL
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "duration",
moduleAnimationGetDuration, NULL
);
}
@@ -0,0 +1,70 @@
// 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 "animation/easing.h"
static scriptproto_t MODULE_EASING_PROTO;
// Generate one handler per easing curve.
#define EASING_MODULE_TABLE \
X(Linear, "linear", EASING_LINEAR, easingLinear) \
X(InSine, "inSine", EASING_IN_SINE, easingInSine) \
X(OutSine, "outSine", EASING_OUT_SINE, easingOutSine) \
X(InOutSine, "inOutSine", EASING_IN_OUT_SINE, easingInOutSine) \
X(InQuad, "inQuad", EASING_IN_QUAD, easingInQuad) \
X(OutQuad, "outQuad", EASING_OUT_QUAD, easingOutQuad) \
X(InOutQuad, "inOutQuad", EASING_IN_OUT_QUAD, easingInOutQuad) \
X(InCubic, "inCubic", EASING_IN_CUBIC, easingInCubic) \
X(OutCubic, "outCubic", EASING_OUT_CUBIC, easingOutCubic) \
X(InOutCubic, "inOutCubic", EASING_IN_OUT_CUBIC, easingInOutCubic) \
X(InQuart, "inQuart", EASING_IN_QUART, easingInQuart) \
X(OutQuart, "outQuart", EASING_OUT_QUART, easingOutQuart) \
X(InOutQuart, "inOutQuart", EASING_IN_OUT_QUART, easingInOutQuart) \
X(InBack, "inBack", EASING_IN_BACK, easingInBack) \
X(OutBack, "outBack", EASING_OUT_BACK, easingOutBack) \
X(InOutBack, "inOutBack", EASING_IN_OUT_BACK, easingInOutBack)
#define X(CName, jsName, type, fn) \
moduleBaseFunction(moduleEasing##CName) { \
if(argc < 1 || !jerry_value_is_number(args[0])) { \
return moduleBaseThrow("Expected number t"); \
} \
float_t t = (float_t)jerry_value_as_number(args[0]); \
return jerry_number((double)fn(t)); \
}
EASING_MODULE_TABLE
#undef X
// Adds a callable easing function with a .type property to the Easing object.
static void moduleEasingRegister(
const char_t *jsName,
uint8_t type,
jerry_external_handler_t fn
) {
jerry_value_t fnVal = jerry_function_external(fn);
jerry_value_t typeKey = jerry_string_sz("type");
jerry_value_t typeVal = jerry_number((double)type);
jerry_object_set(fnVal, typeKey, typeVal);
jerry_value_free(typeKey);
jerry_value_free(typeVal);
jerry_value_t nameKey = jerry_string_sz(jsName);
jerry_object_set(MODULE_EASING_PROTO.prototype, nameKey, fnVal);
jerry_value_free(nameKey);
jerry_value_free(fnVal);
}
static void moduleEasing(void) {
scriptProtoInit(&MODULE_EASING_PROTO, "Easing", sizeof(uint8_t), NULL);
#define X(CName, jsName, type, fn) \
moduleEasingRegister(jsName, type, moduleEasing##CName);
EASING_MODULE_TABLE
#undef X
}
@@ -0,0 +1,67 @@
/**
* 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 "console/console.h"
static scriptproto_t MODULE_CONSOLE_PROTO;
moduleBaseFunction(moduleConsolePrint) {
char_t buf[512];
char_t msg[4096];
size_t msgLen = 0;
for(jerry_length_t i = 0; i < argc; ++i) {
jerry_value_t strVal = jerry_value_to_string(args[i]);
moduleBaseToString(strVal, buf, sizeof(buf));
jerry_value_free(strVal);
size_t partLen = strlen(buf);
if(msgLen + partLen + 1 < sizeof(msg)) {
stringCopy(msg + msgLen, buf, sizeof(msg) - msgLen);
msgLen += partLen;
}
if(i + 1 < argc && msgLen + 1 < sizeof(msg)) {
msg[msgLen++] = '\t';
msg[msgLen] = '\0';
}
}
consolePrint("%s", msg);
return jerry_undefined();
}
moduleBaseFunction(moduleConsoleGetVisible) {
return jerry_boolean(CONSOLE.visible);
}
moduleBaseFunction(moduleConsoleSetVisible) {
if(argc < 1) return moduleBaseThrow("Expected at least 1 argument");
if(!jerry_value_is_boolean(args[0])) {
return moduleBaseThrow("Console.visible: expected boolean");
}
CONSOLE.visible = jerry_value_is_true(args[0]);
return jerry_undefined();
}
static void moduleConsole(void) {
scriptProtoInit(
&MODULE_CONSOLE_PROTO, "Console",
sizeof(uint8_t), NULL
);
scriptProtoDefineStaticFunc(
&MODULE_CONSOLE_PROTO, "print", moduleConsolePrint
);
scriptProtoDefineStaticProp(
&MODULE_CONSOLE_PROTO, "visible",
moduleConsoleGetVisible, moduleConsoleSetVisible
);
}
@@ -0,0 +1,204 @@
// 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 "cutscene/cutscene.h"
#include "error/error.h"
static scriptproto_t MODULE_CUTSCENE_PROTO;
/**
* Disposes the active JS cutscene without firing the then callback.
* Safe to call when no cutscene is active.
*/
static void moduleCutsceneReset(void) {
if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) return;
jerry_value_t key = jerry_string_sz("dispose");
jerry_value_t fn = jerry_object_get(CUTSCENE.activeRef, key);
jerry_value_free(key);
if(jerry_value_is_function(fn)) {
jerry_value_t result = jerry_call(fn, CUTSCENE.activeRef, NULL, 0);
jerry_value_free(fn);
jerry_value_free(result);
} else {
jerry_value_free(fn);
}
jerry_value_free(CUTSCENE.activeRef);
CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE;
}
/**
* Ticks the active JS cutscene. Called from cutsceneUpdate().
*
* @return Any error state that happened.
*/
static errorret_t moduleCutsceneUpdate(void) {
if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) errorOk();
jerry_value_t key = jerry_string_sz("update");
jerry_value_t fn = jerry_object_get(CUTSCENE.activeRef, key);
jerry_value_free(key);
if(!jerry_value_is_function(fn)) {
jerry_value_free(fn);
errorOk();
}
jerry_value_t result = jerry_call(fn, CUTSCENE.activeRef, 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("Cutscene update failed: %s", errMsg);
}
jerry_value_free(result);
errorOk();
}
moduleBaseFunction(moduleCutsceneDefaultConstructor) {
return jerry_undefined();
}
moduleBaseFunction(moduleCutsceneDefaultUpdate) {
return jerry_undefined();
}
moduleBaseFunction(moduleCutsceneDefaultDispose) {
return jerry_undefined();
}
/**
* cutscene.then(callback)
*
* Sets the completion callback fired when Cutscene.finish() is called.
* Only one callback may be set; calling then() again replaces it.
* Returns the instance for optional chaining.
*/
moduleBaseFunction(moduleCutsceneThen) {
if(argc < 1 || !jerry_value_is_function(args[0])) {
return moduleBaseThrow("then() expects a function argument");
}
jerry_value_t key = jerry_string_sz("_onFinished");
jerry_object_set(callInfo->this_value, key, args[0]);
jerry_value_free(key);
return jerry_value_copy(callInfo->this_value);
}
/**
* Cutscene.play(instance)
*
* Registers a cutscene instance with the engine and begins calling its
* update() each tick. Stops any currently active cutscene first.
* Returns the instance to allow chaining .then() directly.
*/
moduleBaseFunction(moduleCutscenePlay) {
if(argc < 1 || !jerry_value_is_object(args[0])) {
return moduleBaseThrow("play() expects a cutscene instance");
}
if(CUTSCENE.activeRef != CUTSCENE_SCRIPT_REF_NONE) {
moduleCutsceneReset();
}
CUTSCENE.activeRef = jerry_value_copy(args[0]);
return jerry_value_copy(args[0]);
}
/**
* Cutscene.finish()
*
* Marks the active cutscene as complete: calls dispose(), clears the
* active ref, then fires the then() callback if one was set.
* Intended to be called from within the cutscene's own update().
*/
moduleBaseFunction(moduleCutsceneFinish) {
if(CUTSCENE.activeRef == CUTSCENE_SCRIPT_REF_NONE) {
return jerry_undefined();
}
jerry_value_t cbKey = jerry_string_sz("_onFinished");
jerry_value_t callback = jerry_object_get(CUTSCENE.activeRef, cbKey);
jerry_value_free(cbKey);
jerry_value_t dispKey = jerry_string_sz("dispose");
jerry_value_t dispFn = jerry_object_get(CUTSCENE.activeRef, dispKey);
jerry_value_free(dispKey);
if(jerry_value_is_function(dispFn)) {
jerry_value_t r = jerry_call(dispFn, CUTSCENE.activeRef, NULL, 0);
jerry_value_free(dispFn);
if(jerry_value_is_exception(r)) {
jerry_value_free(callback);
return r;
}
jerry_value_free(r);
} else {
jerry_value_free(dispFn);
}
jerry_value_free(CUTSCENE.activeRef);
CUTSCENE.activeRef = CUTSCENE_SCRIPT_REF_NONE;
if(jerry_value_is_function(callback)) {
jerry_value_t r = jerry_call(callback, jerry_undefined(), NULL, 0);
jerry_value_free(callback);
if(jerry_value_is_exception(r)) return r;
jerry_value_free(r);
} else {
jerry_value_free(callback);
}
return jerry_undefined();
}
/**
* Cutscene.stop()
*
* Immediately disposes the active cutscene without firing the then
* callback. No-op when no cutscene is active.
*/
moduleBaseFunction(moduleCutsceneStop) {
moduleCutsceneReset();
return jerry_undefined();
}
static void moduleCutscene(void) {
scriptProtoInit(
&MODULE_CUTSCENE_PROTO,
"Cutscene",
sizeof(uint8_t),
moduleCutsceneDefaultConstructor
);
scriptProtoDefineFunc(
&MODULE_CUTSCENE_PROTO, "update", moduleCutsceneDefaultUpdate
);
scriptProtoDefineFunc(
&MODULE_CUTSCENE_PROTO, "dispose", moduleCutsceneDefaultDispose
);
scriptProtoDefineFunc(
&MODULE_CUTSCENE_PROTO, "then", moduleCutsceneThen
);
scriptProtoDefineStaticFunc(
&MODULE_CUTSCENE_PROTO, "play", moduleCutscenePlay
);
scriptProtoDefineStaticFunc(
&MODULE_CUTSCENE_PROTO, "finish", moduleCutsceneFinish
);
scriptProtoDefineStaticFunc(
&MODULE_CUTSCENE_PROTO, "stop", moduleCutsceneStop
);
}
@@ -1,18 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduleglm.c
modulespritebatch.c
moduleglm.c
modulecolor.c
moduletext.c
modulescreen.c
moduletileset.c
moduletexture.c
moduleshader.c
)
@@ -1,186 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecolor.h"
#include "display/color.h"
#include "assert/assert.h"
#include "util/string.h"
#include "time/time.h"
void moduleColor(scriptcontext_t *context) {
assertNotNull(context, "Context cannot be NULL.");
if(luaL_newmetatable(context->luaState, "color_mt")) {
// Metatable is new, set __index and __newindex
lua_pushstring(context->luaState, "__index");
lua_pushcfunction(context->luaState, moduleColorIndex);
lua_settable(context->luaState, -3);
lua_pushstring(context->luaState, "__newindex");
lua_pushcfunction(context->luaState, moduleColorNewIndex);
lua_settable(context->luaState, -3);
lua_pushstring(context->luaState, "__tostring");
lua_pushcfunction(context->luaState, moduleColorToString);
lua_settable(context->luaState, -3);
}
lua_pop(context->luaState, 1);
lua_register(context->luaState, "color", moduleColorFuncColor);
lua_register(context->luaState, "colorRainbow", moduleColorRainbow);
scriptContextExec(context, COLOR_SCRIPT);
}
int moduleColorFuncColor(lua_State *L) {
assertNotNull(L, "Lua state cannot be NULL.");
scriptcontext_t *context = *(scriptcontext_t **)lua_getextraspace(L);
assertNotNull(context, "Script context cannot be NULL.");
// Needs 4 channel uint8_t
if(
!lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
!lua_isnumber(L, 3) || !lua_isnumber(L, 4)
) {
return luaL_error(L, "color(r, g, b, a) requires four number arguments.");
}
colorchannel8_t r = (colorchannel8_t)lua_tonumber(L, 1);
colorchannel8_t g = (colorchannel8_t)lua_tonumber(L, 2);
colorchannel8_t b = (colorchannel8_t)lua_tonumber(L, 3);
colorchannel8_t a = (colorchannel8_t)lua_tonumber(L, 4);
// Create color_t as lua memory, and push metatable
color_t *color = (color_t *)lua_newuserdata(L, sizeof(color_t));
luaL_getmetatable(L, "color_mt");
lua_setmetatable(L, -2);
// Initial values.
color->r = r;
color->g = g;
color->b = b;
color->a = a;
return 1;
}
int moduleColorIndex(lua_State *L) {
assertNotNull(L, "Lua state cannot be NULL.");
const char_t *key = lua_tostring(L, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
const color_t *color = (const color_t*)luaL_checkudata(L, 1, "color_mt");
assertNotNull(color, "Color struct cannot be NULL.");
if(stringCompare(key, "r") == 0) {
lua_pushnumber(L, color->r);
return 1;
}
if(stringCompare(key, "g") == 0) {
lua_pushnumber(L, color->g);
return 1;
}
if(stringCompare(key, "b") == 0) {
lua_pushnumber(L, color->b);
return 1;
}
if(stringCompare(key, "a") == 0) {
lua_pushnumber(L, color->a);
return 1;
}
lua_pushnil(L);
return 1;
}
int moduleColorNewIndex(lua_State *L) {
assertNotNull(L, "Lua state cannot be NULL.");
const char_t *key = lua_tostring(L, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
color_t *color = (color_t*)luaL_checkudata(L, 1, "color_mt");
assertNotNull(color, "Color struct cannot be NULL.");
if(stringCompare(key, "r") == 0) {
if(!lua_isnumber(L, 3)) {
return luaL_error(L, "color channel r must be a number.");
}
color->r = (colorchannel8_t)lua_tonumber(L, 3);
return 0;
} else if(stringCompare(key, "g") == 0) {
if(!lua_isnumber(L, 3)) {
return luaL_error(L, "color channel g must be a number.");
}
color->g = (colorchannel8_t)lua_tonumber(L, 3);
return 0;
} else if(stringCompare(key, "b") == 0) {
if(!lua_isnumber(L, 3)) {
return luaL_error(L, "color channel b must be a number.");
}
color->b = (colorchannel8_t)lua_tonumber(L, 3);
return 0;
} else if(stringCompare(key, "a") == 0) {
if(!lua_isnumber(L, 3)) {
return luaL_error(L, "color channel a must be a number.");
}
color->a = (colorchannel8_t)lua_tonumber(L, 3);
return 0;
}
return 0;
}
int moduleColorToString(lua_State *L) {
assertNotNull(L, "Lua state cannot be NULL.");
const color_t *color = (const color_t*)luaL_checkudata(L, 1, "color_mt");
assertNotNull(color, "Color struct cannot be NULL.");
lua_pushfstring(
L, "color(r=%d, g=%d, b=%d, a=%d)",
color->r, color->g, color->b, color->a
);
return 1;
}
int moduleColorRainbow(lua_State *L) {
assertNotNull(L, "Lua state cannot be NULL.");
// Allow time offset
float_t t = TIME.time;
if(lua_gettop(L) >= 1) {
if(!lua_isnumber(L, 1)) {
return luaL_error(L, "Rainbow time offset must be a number.");
}
t += (float_t)lua_tonumber(L, 1);
}
// Allow speed multiplier
if(lua_gettop(L) >= 2) {
if(!lua_isnumber(L, 2)) {
return luaL_error(L, "Rainbow speed multiplier must be a number.");
}
t *= (float_t)lua_tonumber(L, 2);
}
// Generate rainbow based on time.
color_t *color = (color_t *)lua_newuserdata(L, sizeof(color_t));
color->r = (colorchannel8_t)((sinf(t) + 1.0f) * 0.5f * 255.0f);
color->g = (colorchannel8_t)((sinf(t + 2.0f) + 1.0f) * 0.5f * 255.0f);
color->b = (colorchannel8_t)((sinf(t + 4.0f) + 1.0f) * 0.5f * 255.0f);
color->a = 255;
// Set metatable
luaL_getmetatable(L, "color_mt");
lua_setmetatable(L, -2);
return 1;
}
+160 -41
View File
@@ -6,50 +6,169 @@
*/
#pragma once
#include "script/scriptcontext.h"
#include "script/module/modulebase.h"
#include "display/color.h"
#include "time/time.h"
#include "script/scriptproto.h"
/**
* Registers the color module with the given script context.
*
* @param context The script context to register the module with.
*/
void moduleColor(scriptcontext_t *context);
static scriptproto_t MODULE_COLOR_PROTO;
/**
* Lua function to create a color.
*
* @param L The Lua state.
* @return Number of return values.
*/
int moduleColorFuncColor(lua_State *L);
static inline color_t * moduleColorGet(
const jerry_call_info_t *callInfo
) {
return (color_t*)scriptProtoGetValue(
&MODULE_COLOR_PROTO, callInfo->this_value
);
}
/**
* Index function for the color structure.
* @param L The Lua state.
* @return Number of return values.
*/
int moduleColorIndex(lua_State *L);
moduleBaseFunction(moduleColorGetR) {
color_t *c = moduleColorGet(callInfo);
return c ? jerry_number(c->r) : jerry_undefined();
}
/**
* New index function for the color structure.
*
* @param L The Lua state.
* @return Number of return values.
*/
int moduleColorNewIndex(lua_State *L);
moduleBaseFunction(moduleColorGetG) {
color_t *c = moduleColorGet(callInfo);
return c ? jerry_number(c->g) : jerry_undefined();
}
/**
* Color to string method for script
*
* @param L The Lua state.
* @return Number of return values.
*/
int moduleColorToString(lua_State *L);
moduleBaseFunction(moduleColorGetB) {
color_t *c = moduleColorGet(callInfo);
return c ? jerry_number(c->b) : jerry_undefined();
}
/**
* Lua function to create a rainbow color based on time.
*
* @param L The Lua state.
* @return Number of return values.
*/
int moduleColorRainbow(lua_State *L);
moduleBaseFunction(moduleColorGetA) {
color_t *c = moduleColorGet(callInfo);
return c ? jerry_number(c->a) : jerry_undefined();
}
moduleBaseFunction(moduleColorSetR) {
if(argc < 1) {
return moduleBaseThrow("Expected at least 1 argument");
}
moduleBaseRequireNumber(0);
color_t *c = moduleColorGet(callInfo);
if(!c) return jerry_undefined();
c->r = (colorchannel8_t)jerry_value_as_number(args[0]);
return args[0];
}
moduleBaseFunction(moduleColorSetG) {
if(argc < 1) {
return moduleBaseThrow("Expected at least 1 argument");
}
moduleBaseRequireNumber(0);
color_t *c = moduleColorGet(callInfo);
if(!c) return jerry_undefined();
c->g = (colorchannel8_t)jerry_value_as_number(args[0]);
return args[0];
}
moduleBaseFunction(moduleColorSetB) {
if(argc < 1) {
return moduleBaseThrow("Expected at least 1 argument");
}
moduleBaseRequireNumber(0);
color_t *c = moduleColorGet(callInfo);
if(!c) return jerry_undefined();
c->b = (colorchannel8_t)jerry_value_as_number(args[0]);
return args[0];
}
moduleBaseFunction(moduleColorSetA) {
if(argc < 1) {
return moduleBaseThrow("Expected at least 1 argument");
}
moduleBaseRequireNumber(0);
color_t *c = moduleColorGet(callInfo);
if(!c) return jerry_undefined();
c->a = (colorchannel8_t)jerry_value_as_number(args[0]);
return args[0];
}
moduleBaseFunction(moduleColorToString) {
color_t *c = moduleColorGet(callInfo);
if(!c) return jerry_undefined();
char_t buf[64];
stringFormat(
buf, sizeof(buf),
"{ \"r\": %d, \"g\": %d, \"b\": %d, \"a\": %d }",
(int32_t)c->r, (int32_t)c->g,
(int32_t)c->b, (int32_t)c->a
);
return jerry_string_sz(buf);
}
static jerry_value_t moduleColorMakeObject(color_t color) {
return scriptProtoCreateValue(&MODULE_COLOR_PROTO, &color);
}
moduleBaseFunction(moduleColorConstructor) {
if(argc > 0 && !jerry_value_is_number(args[0])) {
return moduleBaseThrow("Color: r must be a number");
}
if(argc > 1 && !jerry_value_is_number(args[1])) {
return moduleBaseThrow("Color: g must be a number");
}
if(argc > 2 && !jerry_value_is_number(args[2])) {
return moduleBaseThrow("Color: b must be a number");
}
if(argc > 3 && !jerry_value_is_number(args[3])) {
return moduleBaseThrow("Color: a must be a number");
}
color_t c;
c.r = argc > 0 ? (colorchannel8_t)jerry_value_as_number(args[0]) : 255;
c.g = argc > 1 ? (colorchannel8_t)jerry_value_as_number(args[1]) : 255;
c.b = argc > 2 ? (colorchannel8_t)jerry_value_as_number(args[2]) : 255;
c.a = argc > 3 ? (colorchannel8_t)jerry_value_as_number(args[3]) : 255;
return moduleColorMakeObject(c);
}
moduleBaseFunction(moduleColorRainbow) {
float_t t;
if(argc >= 1 && jerry_value_is_number(args[0])) {
t = (float_t)jerry_value_as_number(args[0]);
} else {
t = TIME.time * 4.0f;
}
if(argc >= 2 && jerry_value_is_number(args[1])) {
t *= (float_t)jerry_value_as_number(args[1]);
}
color_t c;
c.r = (colorchannel8_t)((sinf(t) + 1.0f) * 0.5f * 255.0f);
c.g = (colorchannel8_t)((sinf(t + 2.0f) + 1.0f) * 0.5f * 255.0f);
c.b = (colorchannel8_t)((sinf(t + 4.0f) + 1.0f) * 0.5f * 255.0f);
c.a = 255;
return moduleColorMakeObject(c);
}
static void moduleColor(void) {
scriptProtoInit(
&MODULE_COLOR_PROTO, "Color", sizeof(color_t), moduleColorConstructor
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "r", moduleColorGetR, moduleColorSetR
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "g", moduleColorGetG, moduleColorSetG
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "b", moduleColorGetB, moduleColorSetB
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "a", moduleColorGetA, moduleColorSetA
);
scriptProtoDefineStaticFunc(
&MODULE_COLOR_PROTO, "rainbow", moduleColorRainbow
);
scriptProtoDefineToString(&MODULE_COLOR_PROTO, moduleColorToString);
moduleBaseEval(COLOR_SCRIPT);
}
-283
View File
@@ -1,283 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleglm.h"
#include "assert/assert.h"
#include "util/string.h"
#include "util/memory.h"
void moduleGLM(scriptcontext_t *context) {
assertNotNull(context, "Context cannot be NULL.");
// Create metatable for vec3 structure.
if(luaL_newmetatable(context->luaState, "vec3_mt")) {
// Metatable methods
lua_pushcfunction(context->luaState, moduleVec3Index);
lua_setfield(context->luaState, -2, "__index");
lua_pushcfunction(context->luaState, moduleVec3NewIndex);
lua_setfield(context->luaState, -2, "__newindex");
lua_pushcfunction(context->luaState, moduleVec3ToString);
lua_setfield(context->luaState, -2, "__tostring");
}
lua_pop(context->luaState, 1);
if(luaL_newmetatable(context->luaState, "vec4_mt")) {
// Metatable methods
lua_pushcfunction(context->luaState, moduleVec4Index);
lua_setfield(context->luaState, -2, "__index");
lua_pushcfunction(context->luaState, moduleVec4NewIndex);
lua_setfield(context->luaState, -2, "__newindex");
lua_pushcfunction(context->luaState, moduleVec4ToString);
lua_setfield(context->luaState, -2, "__tostring");
}
lua_pop(context->luaState, 1);
lua_register(context->luaState, "vec3", moduleVec3Create);
}
int moduleVec3Create(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
vec3 *v = (vec3 *)lua_newuserdata(l, sizeof(vec3));
memoryZero(v, sizeof(vec3));
// May be expecting between 1 and 3 values.
int top = lua_gettop(l);
if(top >= 1) {
if(!lua_isnumber(l, 1)) {
luaL_error(l, "Vec3 x component must be a number.");
}
(*v)[0] = (float_t)lua_tonumber(l, 1);
}
if(top >= 2) {
if(!lua_isnumber(l, 2)) {
luaL_error(l, "Vec3 y component must be a number.");
}
(*v)[1] = (float_t)lua_tonumber(l, 2);
}
if(top >= 3) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec3 z component must be a number.");
}
(*v)[2] = (float_t)lua_tonumber(l, 3);
}
// Set metatable
luaL_getmetatable(l, "vec3_mt");
lua_setmetatable(l, -2);
return 1;
}
int moduleVec3Index(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
const char_t *key = lua_tostring(l, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
vec3 *vec = (vec3 *)luaL_checkudata(l, 1, "vec3_mt");
assertNotNull(vec, "Vec3 pointer cannot be NULL.");
if(stringCompare(key, "x") == 0) {
lua_pushnumber(l, (*vec)[0]);
return 1;
} else if(stringCompare(key, "y") == 0) {
lua_pushnumber(l, (*vec)[1]);
return 1;
} else if(stringCompare(key, "z") == 0) {
lua_pushnumber(l, (*vec)[2]);
return 1;
}
}
int moduleVec3NewIndex(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
const char_t *key = luaL_checkstring(l, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
vec3 *vec = (vec3 *)luaL_checkudata(l, 1, "vec3_mt");
assertNotNull(vec, "Vec3 pointer cannot be NULL.");
if(stringCompare(key, "x") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec3 x component must be a number.");
}
(*vec)[0] = (float_t)lua_tonumber(l, 3);
return 0;
} else if(stringCompare(key, "y") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec3 y component must be a number.");
}
(*vec)[1] = (float_t)lua_tonumber(l, 3);
return 0;
} else if(stringCompare(key, "z") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec3 z component must be a number.");
}
(*vec)[2] = (float_t)lua_tonumber(l, 3);
return 0;
}
luaL_error(l, "Invalid key for vec3: %s", key);
return 0;
}
int moduleVec3ToString(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
vec3 *vec = (vec3 *)luaL_checkudata(l, 1, "vec3_mt");
assertNotNull(vec, "Vec3 pointer cannot be NULL.");
char buf[128];
snprintf(
buf, sizeof(buf),
"vec3(%.3f, %.3f, %.3f)",
(*vec)[0], (*vec)[1], (*vec)[2]
);
lua_pushstring(l, buf);
return 1;
}
int moduleVec4Create(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
vec4 *v = (vec4 *)lua_newuserdata(l, sizeof(vec4));
memoryZero(v, sizeof(vec4));
// May be expecting between 1 and 4 values.
int top = lua_gettop(l);
if(top >= 1) {
if(!lua_isnumber(l, 1)) {
luaL_error(l, "Vec4 x component must be a number.");
}
(*v)[0] = (float_t)lua_tonumber(l, 1);
}
if(top >= 2) {
if(!lua_isnumber(l, 2)) {
luaL_error(l, "Vec4 y component must be a number.");
}
(*v)[1] = (float_t)lua_tonumber(l, 2);
}
if(top >= 3) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec4 z component must be a number.");
}
(*v)[2] = (float_t)lua_tonumber(l, 3);
}
if(top >= 4) {
if(!lua_isnumber(l, 4)) {
luaL_error(l, "Vec4 w component must be a number.");
}
(*v)[3] = (float_t)lua_tonumber(l, 4);
}
// Set metatable
luaL_getmetatable(l, "vec4_mt");
lua_setmetatable(l, -2);
return 1;
}
int moduleVec4Index(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
const char_t *key = lua_tostring(l, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
vec4 *vec = (vec4 *)luaL_checkudata(l, 1, "vec4_mt");
assertNotNull(vec, "Vec4 pointer cannot be NULL.");
if(stringCompare(key, "x") == 0) {
lua_pushnumber(l, (*vec)[0]);
return 1;
} else if(stringCompare(key, "y") == 0) {
lua_pushnumber(l, (*vec)[1]);
return 1;
} else if(stringCompare(key, "z") == 0) {
lua_pushnumber(l, (*vec)[2]);
return 1;
} else if(stringCompare(key, "w") == 0) {
lua_pushnumber(l, (*vec)[3]);
return 1;
} else if(stringCompare(key, "u0") == 0) {
lua_pushnumber(l, (*vec)[0]);
return 1;
} else if(stringCompare(key, "v0") == 0) {
lua_pushnumber(l, (*vec)[1]);
return 1;
} else if(stringCompare(key, "u1") == 0) {
lua_pushnumber(l, (*vec)[2]);
return 1;
} else if(stringCompare(key, "v1") == 0) {
lua_pushnumber(l, (*vec)[3]);
return 1;
}
lua_pushnil(l);
return 1;
}
int moduleVec4NewIndex(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
const char_t *key = luaL_checkstring(l, 2);
assertStrLenMin(key, 1, "Key cannot be empty.");
vec4 *vec = (vec4 *)luaL_checkudata(l, 1, "vec4_mt");
assertNotNull(vec, "Vec4 pointer cannot be NULL.");
if(stringCompare(key, "x") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec4 x component must be a number.");
}
(*vec)[0] = (float_t)lua_tonumber(l, 3);
return 0;
} else if(stringCompare(key, "y") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec4 y component must be a number.");
}
(*vec)[1] = (float_t)lua_tonumber(l, 3);
return 0;
} else if(stringCompare(key, "z") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec4 z component must be a number.");
}
(*vec)[2] = (float_t)lua_tonumber(l, 3);
return 0;
} else if(stringCompare(key, "w") == 0) {
if(!lua_isnumber(l, 3)) {
luaL_error(l, "Vec4 w component must be a number.");
}
(*vec)[3] = (float_t)lua_tonumber(l, 3);
return 0;
}
luaL_error(l, "Invalid key for vec4: %s", key);
return 0;
}
int moduleVec4ToString(lua_State *l) {
assertNotNull(l, "Lua state cannot be NULL.");
vec4 *vec = (vec4 *)luaL_checkudata(l, 1, "vec4_mt");
assertNotNull(vec, "Vec4 pointer cannot be NULL.");
char buf[128];
snprintf(
buf, sizeof(buf),
"vec4(%.3f, %.3f, %.3f, %.3f)",
(*vec)[0], (*vec)[1], (*vec)[2], (*vec)[3]
);
lua_pushstring(l, buf);
return 1;
}
@@ -1,80 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/scriptcontext.h"
/**
* Registers the GLM module with the given script context.
*
* @param context The script context to register the module with.
*/
void moduleGLM(scriptcontext_t *context);
/**
* Creates a new vec3 structure in Lua.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec3Create(lua_State *l);
/**
* Lua __index metamethod for vec3 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec3Index(lua_State *l);
/**
* Lua __newindex metamethod for vec3 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec3NewIndex(lua_State *l);
/**
* Lua __tostring metamethod for vec3 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec3ToString(lua_State *l);
/**
* Creates a new vec4 structure in Lua.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec4Create(lua_State *l);
/**
* Lua __index metamethod for vec4 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec4Index(lua_State *l);
/**
* Lua __newindex metamethod for vec4 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec4NewIndex(lua_State *l);
/**
* Lua __tostring metamethod for vec4 structure.
*
* @param l The Lua state.
* @return Number of return values on the Lua stack.
*/
int moduleVec4ToString(lua_State *l);
+452
View File
@@ -0,0 +1,452 @@
/**
* 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 "script/module/math/modulevec3ref.h"
#include "display/mesh/mesh.h"
#include "display/mesh/cube.h"
#include "display/mesh/quad.h"
#include "display/mesh/sphere.h"
#include "display/mesh/plane.h"
#include "display/mesh/capsule.h"
#include "display/mesh/triprism.h"
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
int32_t vertexCount;
bool_t initialized;
} meshscript_t;
typedef struct {
meshvertex_t *vertex;
} meshvertexscript_t;
static scriptproto_t MODULE_MESH_PROTO;
static scriptproto_t MODULE_MESH_VERTEX_PROTO;
static inline meshscript_t * moduleMeshGet(
const jerry_call_info_t *callInfo
) {
return (meshscript_t*)scriptProtoGetValue(
&MODULE_MESH_PROTO, callInfo->this_value
);
}
static inline meshscript_t * moduleMeshFrom(const jerry_value_t val) {
return (meshscript_t*)scriptProtoGetValue(&MODULE_MESH_PROTO, val);
}
static void moduleMeshFreeData(
void *ptr,
jerry_object_native_info_t *info
) {
(void)info;
meshscript_t *ms = (meshscript_t*)ptr;
if(ms->initialized) (void)meshDispose(&ms->mesh);
if(ms->vertices) memoryFree(ms->vertices);
memoryFree(ptr);
}
// Creates a new JS Mesh object from an already-filled heap-allocated meshscript_t.
static jerry_value_t moduleMeshWrapNew(meshscript_t *ms) {
jerry_value_t obj = jerry_object();
jerry_object_set_native_ptr(obj, &MODULE_MESH_PROTO.info, ms);
jerry_object_set_proto(obj, MODULE_MESH_PROTO.prototype);
jerry_value_t arr = jerry_array((jerry_length_t)ms->vertexCount);
for(int32_t i = 0; i < ms->vertexCount; i++) {
meshvertexscript_t mv = { .vertex = &ms->vertices[i] };
jerry_value_t vobj = scriptProtoCreateValue(&MODULE_MESH_VERTEX_PROTO, &mv);
jerry_value_t res = jerry_object_set_index(arr, (uint32_t)i, vobj);
jerry_value_free(res);
jerry_value_free(vobj);
}
jerry_value_t key = jerry_string_sz("_verts");
jerry_object_set(obj, key, arr);
jerry_value_free(key);
jerry_value_free(arr);
return obj;
}
// Allocates a meshscript_t for the given vertex count and buffers it.
// Caller is responsible for populating ms->vertices before calling meshInit.
static meshscript_t * moduleMeshAlloc(const int32_t vertexCount) {
meshscript_t *ms = (meshscript_t*)memoryAllocate(sizeof(meshscript_t));
memoryZero(ms, sizeof(meshscript_t));
ms->vertices = (meshvertex_t*)memoryAllocate(
(size_t)vertexCount * sizeof(meshvertex_t)
);
memoryZero(ms->vertices, (size_t)vertexCount * sizeof(meshvertex_t));
ms->vertexCount = vertexCount;
return ms;
}
// Finalizes a meshscript_t: uploads to GPU and wraps in a JS object.
static jerry_value_t moduleMeshFinalize(
meshscript_t *ms,
const meshprimitivetype_t type
) {
(void)meshInit(&ms->mesh, type, ms->vertexCount, ms->vertices);
ms->initialized = true;
return moduleMeshWrapNew(ms);
}
// ---- MeshVertex ----
moduleBaseFunction(moduleMeshVertexGetPosition) {
meshvertexscript_t *mv = (meshvertexscript_t*)scriptProtoGetValue(
&MODULE_MESH_VERTEX_PROTO, callInfo->this_value
);
if(!mv) return jerry_undefined();
return moduleVec3RefPush(mv->vertex->pos, NULL, NULL);
}
moduleBaseFunction(moduleMeshVertexSetPosition) {
if(argc < 1) return moduleBaseThrow("Expected position argument");
meshvertexscript_t *mv = (meshvertexscript_t*)scriptProtoGetValue(
&MODULE_MESH_VERTEX_PROTO, callInfo->this_value
);
if(!mv) return jerry_undefined();
vec3 v;
if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3");
glm_vec3_copy(v, mv->vertex->pos);
return jerry_undefined();
}
// ---- Mesh instance ----
moduleBaseFunction(moduleMeshConstructor) {
if(argc < 1) return moduleBaseThrow("Expected vertex count");
moduleBaseRequireNumber(0);
int32_t vertexCount = (int32_t)jerry_value_as_number(args[0]);
if(vertexCount <= 0) return moduleBaseThrow("Vertex count must be > 0");
meshscript_t *ms = moduleMeshAlloc(vertexCount);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_MESH_PROTO.info, ms
);
jerry_value_t arr = jerry_array((jerry_length_t)vertexCount);
for(int32_t i = 0; i < vertexCount; i++) {
meshvertexscript_t mv = { .vertex = &ms->vertices[i] };
jerry_value_t vobj = scriptProtoCreateValue(&MODULE_MESH_VERTEX_PROTO, &mv);
jerry_value_t res = jerry_object_set_index(arr, (uint32_t)i, vobj);
jerry_value_free(res);
jerry_value_free(vobj);
}
jerry_value_t key = jerry_string_sz("_verts");
jerry_object_set(callInfo->this_value, key, arr);
jerry_value_free(key);
jerry_value_free(arr);
return jerry_undefined();
}
moduleBaseFunction(moduleMeshGetVertices) {
jerry_value_t key = jerry_string_sz("_verts");
jerry_value_t arr = jerry_object_get(callInfo->this_value, key);
jerry_value_free(key);
return arr;
}
moduleBaseFunction(moduleMeshGetVertexCount) {
meshscript_t *ms = moduleMeshGet(callInfo);
if(!ms) return jerry_undefined();
return jerry_number((double)ms->vertexCount);
}
moduleBaseFunction(moduleMeshFlush) {
meshscript_t *ms = moduleMeshGet(callInfo);
if(!ms) return jerry_undefined();
if(!ms->initialized) {
(void)meshInit(
&ms->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
ms->vertexCount,
ms->vertices
);
ms->initialized = true;
} else {
(void)meshFlush(&ms->mesh, 0, -1);
}
return jerry_undefined();
}
moduleBaseFunction(moduleMeshDispose) {
meshscript_t *ms = moduleMeshGet(callInfo);
if(!ms) return jerry_undefined();
if(ms->initialized) {
(void)meshDispose(&ms->mesh);
ms->initialized = false;
}
if(ms->vertices) {
memoryFree(ms->vertices);
ms->vertices = NULL;
}
return jerry_undefined();
}
moduleBaseFunction(moduleMeshToString) {
meshscript_t *ms = moduleMeshGet(callInfo);
if(!ms) return jerry_string_sz("Mesh(?)");
char_t buf[64];
stringFormat(buf, sizeof(buf), "Mesh(%d)", ms->vertexCount);
return jerry_string_sz(buf);
}
// ---- Static defaults (engine-owned, not GC'd) ----
moduleBaseFunction(moduleMeshDefaultCube) {
return moduleBaseWrapPointer(&CUBE_MESH_SIMPLE);
}
moduleBaseFunction(moduleMeshDefaultQuad) {
return moduleBaseWrapPointer(&QUAD_MESH_SIMPLE);
}
moduleBaseFunction(moduleMeshDefaultSphere) {
return moduleBaseWrapPointer(&SPHERE_MESH_SIMPLE);
}
moduleBaseFunction(moduleMeshDefaultPlane) {
return moduleBaseWrapPointer(&PLANE_MESH_SIMPLE);
}
moduleBaseFunction(moduleMeshDefaultCapsule) {
return moduleBaseWrapPointer(&CAPSULE_MESH_SIMPLE);
}
moduleBaseFunction(moduleMeshDefaultTriPrism) {
return moduleBaseWrapPointer(&TRIPRISM_MESH_SIMPLE);
}
// ---- Static factory methods ----
// Mesh.createCube(min?, max?) — defaults to (-0.5,-0.5,-0.5)..(0.5,0.5,0.5)
moduleBaseFunction(moduleMeshCreateCube) {
vec3 min = { -0.5f, -0.5f, -0.5f };
vec3 max = { 0.5f, 0.5f, 0.5f };
if(argc >= 2) {
if(!moduleVec3AnyCheck(args[0], min)) {
return moduleBaseThrow("Mesh.createCube: expected Vec3 min");
}
if(!moduleVec3AnyCheck(args[1], max)) {
return moduleBaseThrow("Mesh.createCube: expected Vec3 max");
}
}
meshscript_t *ms = moduleMeshAlloc(CUBE_VERTEX_COUNT);
cubeBuffer(
ms->vertices, min, max
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
return moduleMeshFinalize(ms, CUBE_PRIMITIVE_TYPE);
}
// Mesh.createQuad(minX, minY, maxX, maxY) — 2D XY quad, u0-u1=0-1 v0-v1=0-1
moduleBaseFunction(moduleMeshCreateQuad) {
float_t minX = -0.5f, minY = -0.5f, maxX = 0.5f, maxY = 0.5f;
if(argc >= 4) {
if(!jerry_value_is_number(args[0]) || !jerry_value_is_number(args[1]) ||
!jerry_value_is_number(args[2]) || !jerry_value_is_number(args[3])) {
return moduleBaseThrow("Mesh.createQuad: expected (minX, minY, maxX, maxY)");
}
minX = (float_t)jerry_value_as_number(args[0]);
minY = (float_t)jerry_value_as_number(args[1]);
maxX = (float_t)jerry_value_as_number(args[2]);
maxY = (float_t)jerry_value_as_number(args[3]);
}
meshscript_t *ms = moduleMeshAlloc(QUAD_VERTEX_COUNT);
quadBuffer(
ms->vertices,
minX, minY, maxX, maxY,
0.0f, 0.0f, 1.0f, 1.0f
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
return moduleMeshFinalize(ms, QUAD_PRIMITIVE_TYPE);
}
// Mesh.createSphere(radius?, stacks?, sectors?)
moduleBaseFunction(moduleMeshCreateSphere) {
float_t radius = 0.5f;
int32_t stacks = SPHERE_STACKS;
int32_t sectors = SPHERE_SECTORS;
if(argc >= 1 && jerry_value_is_number(args[0])) {
radius = (float_t)jerry_value_as_number(args[0]);
}
if(argc >= 2 && jerry_value_is_number(args[1])) {
stacks = (int32_t)jerry_value_as_number(args[1]);
}
if(argc >= 3 && jerry_value_is_number(args[2])) {
sectors = (int32_t)jerry_value_as_number(args[2]);
}
int32_t vertexCount = stacks * sectors * 6;
vec3 center = { 0.0f, 0.0f, 0.0f };
meshscript_t *ms = moduleMeshAlloc(vertexCount);
sphereBuffer(
ms->vertices, center, radius, stacks, sectors
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
return moduleMeshFinalize(ms, SPHERE_PRIMITIVE_TYPE);
}
// Mesh.createPlane(width?, height?) — XZ-aligned, centered at origin
moduleBaseFunction(moduleMeshCreatePlane) {
float_t width = 1.0f, height = 1.0f;
if(argc >= 1 && jerry_value_is_number(args[0])) {
width = (float_t)jerry_value_as_number(args[0]);
}
if(argc >= 2 && jerry_value_is_number(args[1])) {
height = (float_t)jerry_value_as_number(args[1]);
}
vec3 min = { -width * 0.5f, 0.0f, -height * 0.5f };
vec3 max = { width * 0.5f, 0.0f, height * 0.5f };
vec2 uvMin = { 0.0f, 0.0f };
vec2 uvMax = { 1.0f, 1.0f };
meshscript_t *ms = moduleMeshAlloc(PLANE_VERTEX_COUNT);
planeBuffer(
ms->vertices, PLANE_AXIS_XZ, min, max
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
, uvMin, uvMax
);
return moduleMeshFinalize(ms, PLANE_PRIMITIVE_TYPE);
}
// Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)
moduleBaseFunction(moduleMeshCreateCapsule) {
float_t radius = 0.5f;
float_t halfHeight = 0.5f;
int32_t capRings = CAPSULE_CAP_RINGS;
int32_t sectors = CAPSULE_SECTORS;
if(argc >= 1 && jerry_value_is_number(args[0])) {
radius = (float_t)jerry_value_as_number(args[0]);
}
if(argc >= 2 && jerry_value_is_number(args[1])) {
halfHeight = (float_t)jerry_value_as_number(args[1]);
}
if(argc >= 3 && jerry_value_is_number(args[2])) {
capRings = (int32_t)jerry_value_as_number(args[2]);
}
if(argc >= 4 && jerry_value_is_number(args[3])) {
sectors = (int32_t)jerry_value_as_number(args[3]);
}
int32_t vertexCount = (2 * capRings + 1) * sectors * 6;
vec3 center = { 0.0f, 0.0f, 0.0f };
meshscript_t *ms = moduleMeshAlloc(vertexCount);
capsuleBuffer(
ms->vertices, center, radius, halfHeight, capRings, sectors
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
return moduleMeshFinalize(ms, CAPSULE_PRIMITIVE_TYPE);
}
// Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)
moduleBaseFunction(moduleMeshCreateTriPrism) {
if(argc < 8) {
return moduleBaseThrow(
"Mesh.createTriPrism: expected (x0,y0, x1,y1, x2,y2, minZ, maxZ)"
);
}
float_t x0 = (float_t)jerry_value_as_number(args[0]);
float_t y0 = (float_t)jerry_value_as_number(args[1]);
float_t x1 = (float_t)jerry_value_as_number(args[2]);
float_t y1 = (float_t)jerry_value_as_number(args[3]);
float_t x2 = (float_t)jerry_value_as_number(args[4]);
float_t y2 = (float_t)jerry_value_as_number(args[5]);
float_t minZ = (float_t)jerry_value_as_number(args[6]);
float_t maxZ = (float_t)jerry_value_as_number(args[7]);
meshscript_t *ms = moduleMeshAlloc(TRIPRISM_VERTEX_COUNT);
triPrismBuffer(
ms->vertices, x0, y0, x1, y1, x2, y2, minZ, maxZ
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
return moduleMeshFinalize(ms, TRIPRISM_PRIMITIVE_TYPE);
}
// ---- Registration ----
static void moduleMesh(void) {
// MeshVertex - internal type, no global constructor
scriptProtoInit(
&MODULE_MESH_VERTEX_PROTO, NULL,
sizeof(meshvertexscript_t), NULL
);
scriptProtoDefineProp(
&MODULE_MESH_VERTEX_PROTO, "position",
moduleMeshVertexGetPosition, moduleMeshVertexSetPosition
);
// Mesh - global constructor: new Mesh(vertexCount)
scriptProtoInit(
&MODULE_MESH_PROTO, "Mesh",
sizeof(meshscript_t), moduleMeshConstructor
);
MODULE_MESH_PROTO.info.free_cb = moduleMeshFreeData;
scriptProtoDefineToString(&MODULE_MESH_PROTO, moduleMeshToString);
scriptProtoDefineProp(
&MODULE_MESH_PROTO, "vertices",
moduleMeshGetVertices, NULL
);
scriptProtoDefineProp(
&MODULE_MESH_PROTO, "vertexCount",
moduleMeshGetVertexCount, NULL
);
scriptProtoDefineFunc(&MODULE_MESH_PROTO, "flush", moduleMeshFlush);
scriptProtoDefineFunc(&MODULE_MESH_PROTO, "dispose", moduleMeshDispose);
// Static default mesh references
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_CUBE", moduleMeshDefaultCube, NULL
);
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_QUAD", moduleMeshDefaultQuad, NULL
);
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_SPHERE", moduleMeshDefaultSphere, NULL
);
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_PLANE", moduleMeshDefaultPlane, NULL
);
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_CAPSULE", moduleMeshDefaultCapsule, NULL
);
scriptProtoDefineStaticProp(
&MODULE_MESH_PROTO, "DEFAULT_TRIPRISM", moduleMeshDefaultTriPrism, NULL
);
// Static factory methods
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createCube", moduleMeshCreateCube
);
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createQuad", moduleMeshCreateQuad
);
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createSphere", moduleMeshCreateSphere
);
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createPlane", moduleMeshCreatePlane
);
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createCapsule", moduleMeshCreateCapsule
);
scriptProtoDefineStaticFunc(
&MODULE_MESH_PROTO, "createTriPrism", moduleMeshCreateTriPrism
);
}
@@ -1,43 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulescreen.h"
#include "assert/assert.h"
#include "display/screen/screen.h"
void moduleScreen(scriptcontext_t *context) {
assertNotNull(context, "Context cannot be NULL.");
lua_register(context->luaState, "screenGetWidth", moduleScreenGetWidth);
lua_register(context->luaState, "screenGetHeight", moduleScreenGetHeight);
lua_register(context->luaState, "screenSetBackground", moduleScreenSetBackground);
}
int moduleScreenGetWidth(lua_State *L) {
assertNotNull(L, "Lua state is null");
lua_pushnumber(L, SCREEN.width);
return 1;
}
int moduleScreenGetHeight(lua_State *L) {
assertNotNull(L, "Lua state is null");
lua_pushnumber(L, SCREEN.height);
return 1;
}
int moduleScreenSetBackground(lua_State *L) {
assertNotNull(L, "Lua state is null");
if(!lua_isuserdata(L, 1)) {
luaL_error(L, "Screen background color must be a color struct");
return 0;
}
color_t *color = (color_t*)luaL_checkudata(L, 1, "color_mt");
SCREEN.background = *color;
return 0;
}

Some files were not shown because too many files have changed in this diff Show More