diff --git a/.gitmodules b/.gitmodules index 25caf7aec1..c2b5f02c63 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ [submodule "Kha"] path = Kha url = https://github.com/armory3d/Kha + +[submodule "lib/libdragon"] + path = lib/libdragon + url = https://github.com/DragonMinded/libdragon.git + branch = preview + +[submodule "lib/tiny3d"] + path = lib/tiny3d + url = https://github.com/HailToDodongo/tiny3d.git diff --git a/.vscode/settings.json b/.vscode/settings.json index 6778ea66a8..2295e84898 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,4 @@ "[python]": { "editor.insertSpaces": true } -} +} \ No newline at end of file diff --git a/armorcore/Deployment/n64/Makefile.j2 b/armorcore/Deployment/n64/Makefile.j2 new file mode 100644 index 0000000000..3d18c4533f --- /dev/null +++ b/armorcore/Deployment/n64/Makefile.j2 @@ -0,0 +1,75 @@ +BUILD_DIR=build + +T3D_INST={tiny3d_path} + +include $(N64_INST)/include/n64.mk +include $(T3D_INST)/t3d.mk + +GAME_TITLE := {game_title} +ROM_NAME := {game_title} + +N64_CFLAGS += -std=gnu2x + +src = \ + src/main.c \ + src/engine.c \ + src/renderer.c \ + src/system/signal.c \ + src/data/models.c \ + src/data/scenes.c \ + src/data/traits.c \ + src/events/trait_events.c \ + src/iron/object/transform.c \ + src/iron/object/object.c \ + src/iron/system/input.c \ +{scene_files} + +# Physics sources (conditionally included) +{physics_sources} + +# UI sources (conditionally included) +{koui_sources} + +assets_png = $(wildcard assets/*.png) +assets_gltf = $(wildcard assets/*.gltf) +assets_ttf = $(wildcard assets/*.ttf) +assets_conv = $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite))) \ + $(addprefix filesystem/,$(notdir $(assets_ttf:%.ttf=%.font64))) \ + $(addprefix filesystem/,$(notdir $(assets_gltf:%.gltf=%.t3dm))) + +all: $(ROM_NAME).z64 + +filesystem/%.sprite: assets/%.png + @mkdir -p $(dir $@) + @echo " [SPRITE] $@" + $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<" + +filesystem/%.font64: assets/%.ttf + @mkdir -p $(dir $@) + @echo " [FONT] $@" + $(N64_MKFONT) $(MKFONT_FLAGS) -o filesystem "$<" + +filesystem/%.t3dm: assets/%.gltf + @mkdir -p $(dir $@) + @echo " [T3D-MODEL] $@" + $(T3D_GLTF_TO_3D) "$<" $@ + $(N64_BINDIR)/mkasset -c 2 -o filesystem $@ + +$(BUILD_DIR)/$(ROM_NAME).dfs: $(assets_conv) +$(BUILD_DIR)/$(ROM_NAME).elf: $(src:%.c=$(BUILD_DIR)/%.o) + +$(ROM_NAME).z64: N64_ROM_TITLE=$(GAME_TITLE) +$(ROM_NAME).z64: $(BUILD_DIR)/$(ROM_NAME).dfs + +clean: + rm -rf $(BUILD_DIR) *.z64 + rm -rf filesystem + +build_lib: + rm -rf $(BUILD_DIR) *.z64 + make -C $(T3D_INST) + make all + +-include $(wildcard $(BUILD_DIR)/*.d) + +.PHONY: all clean diff --git a/armorcore/Deployment/.keepme b/armorcore/Deployment/n64/docs/.keepme similarity index 100% rename from armorcore/Deployment/.keepme rename to armorcore/Deployment/n64/docs/.keepme diff --git a/armorcore/Deployment/n64/src/data/fonts.c.j2 b/armorcore/Deployment/n64/src/data/fonts.c.j2 new file mode 100644 index 0000000000..926f363b7c --- /dev/null +++ b/armorcore/Deployment/n64/src/data/fonts.c.j2 @@ -0,0 +1,61 @@ +#include +#include "fonts.h" + +// Font paths (converted from .ttf to .font64 by Makefile) +static const char *FONT_PATHS[FONT_COUNT] = {{ +{font_paths} +}}; + +// rdpq font IDs start at 2 (1 is reserved for libdragon debug font) +#define FONT_ID_BASE 2 + +typedef struct {{ + rdpq_font_t *font; + uint8_t loaded; +}} FontEntry; + +static FontEntry g_fonts[FONT_COUNT]; + +void fonts_init(void) +{{ + for (uint8_t i = 0; i < FONT_COUNT; i++) {{ + g_fonts[i].font = NULL; + g_fonts[i].loaded = 0; + }} +}} + +void fonts_shutdown(void) +{{ + for (uint8_t i = 0; i < FONT_COUNT; i++) {{ + if (g_fonts[i].loaded) {{ + if (g_fonts[i].font) {{ + rdpq_font_free(g_fonts[i].font); + g_fonts[i].font = NULL; + }} + g_fonts[i].loaded = 0; + }} + }} +}} + +rdpq_font_t *fonts_get(FontId id) +{{ + if (id >= FONT_COUNT) {{ + return NULL; + }} + + FontEntry *e = &g_fonts[id]; + if (!e->loaded) {{ + const char *path = FONT_PATHS[id]; + e->font = rdpq_font_load(path); + if (e->font) {{ + rdpq_text_register_font(FONT_ID_BASE + id, e->font); + e->loaded = 1; + }} + }} + return e->font; +}} + +uint8_t fonts_get_rdpq_id(FontId id) +{{ + return FONT_ID_BASE + id; +}} diff --git a/armorcore/Deployment/n64/src/data/fonts.h.j2 b/armorcore/Deployment/n64/src/data/fonts.h.j2 new file mode 100644 index 0000000000..533f761bd9 --- /dev/null +++ b/armorcore/Deployment/n64/src/data/fonts.h.j2 @@ -0,0 +1,22 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" {{ +#endif + +typedef enum {{ +{font_enum_entries} + FONT_COUNT = {font_count} +}} FontId; + +void fonts_init(void); +void fonts_shutdown(void); + +rdpq_font_t *fonts_get(FontId id); +uint8_t fonts_get_rdpq_id(FontId id); + +#ifdef __cplusplus +}} +#endif diff --git a/armorcore/Deployment/n64/src/data/models.c.j2 b/armorcore/Deployment/n64/src/data/models.c.j2 new file mode 100644 index 0000000000..e26c1ba8de --- /dev/null +++ b/armorcore/Deployment/n64/src/data/models.c.j2 @@ -0,0 +1,129 @@ +#include +#include +#include + +#include "models.h" + +static const char *MODEL_PATHS[MODEL_COUNT] = {{ +{mesh_paths} +}}; + +typedef struct {{ + T3DModel *model; + rspq_block_t *dpl; + uint16_t ref_count; // Reference count for shared models + uint8_t loaded; +}} ModelEntry; + +static ModelEntry g_models[MODEL_COUNT]; + +void models_init(void) +{{ + for (uint8_t i = 0; i < MODEL_COUNT; i++) {{ + g_models[i].model = NULL; + g_models[i].dpl = NULL; + g_models[i].ref_count = 0; + g_models[i].loaded = 0; + }} +}} + +void models_shutdown(void) +{{ + for (uint8_t i = 0; i < MODEL_COUNT; i++) {{ + if (g_models[i].loaded) {{ + if (g_models[i].model) {{ + t3d_model_free(g_models[i].model); + g_models[i].model = NULL; + }} + if (g_models[i].dpl) {{ + rspq_block_free(g_models[i].dpl); + g_models[i].dpl = NULL; + }} + g_models[i].ref_count = 0; + g_models[i].loaded = 0; + }} + }} +}} + +T3DModel *models_get(ModelId id) +{{ + if (id < 0 || id >= MODEL_COUNT) {{ + return NULL; + }} + + ModelEntry *e = &g_models[id]; + if (!e->loaded) {{ + const char *path = MODEL_PATHS[id]; + e->model = t3d_model_load(path); + if (!e->model) {{ + debugf("Failed to load model: %s\n", path); + return NULL; + }} + rspq_block_begin(); + t3d_model_draw(e->model); + e->dpl = rspq_block_end(); + e->loaded = 1; + }} + e->ref_count++; // Increment reference count + return e->model; +}} + +rspq_block_t *models_get_dpl(ModelId id) +{{ + return g_models[id].dpl; +}} + +// Release a reference to a model +void models_release(ModelId id) +{{ + if (id < 0 || id >= MODEL_COUNT) {{ + return; + }} + ModelEntry *e = &g_models[id]; + if (e->ref_count > 0) {{ + e->ref_count--; + }} +}} + +// Unload a model and free its resources. +// Note: Models are cached by default for performance. Call this explicitly +// when transitioning between scenes that use different model sets, or when +// memory pressure requires freeing unused models. +void models_unload(ModelId id) +{{ + if (id < 0 || id >= MODEL_COUNT) {{ + return; + }} + + ModelEntry *e = &g_models[id]; + if (e->loaded) {{ + if (e->model) {{ + t3d_model_free(e->model); + e->model = NULL; + }} + if (e->dpl) {{ + rspq_block_free(e->dpl); + e->dpl = NULL; + }} + e->ref_count = 0; + e->loaded = 0; + }} +}} + +// Unload only models with zero references (safe after scene transition) +void models_unload_unused(void) +{{ + for (uint8_t i = 0; i < MODEL_COUNT; i++) {{ + if (g_models[i].loaded && g_models[i].ref_count == 0) {{ + models_unload(i); + }} + }} +}} + +// Unload all models to free memory (useful before scene transitions) +void models_unload_all(void) +{{ + for (uint8_t i = 0; i < MODEL_COUNT; i++) {{ + models_unload(i); + }} +}} diff --git a/armorcore/Deployment/n64/src/data/models.h.j2 b/armorcore/Deployment/n64/src/data/models.h.j2 new file mode 100644 index 0000000000..c50b802779 --- /dev/null +++ b/armorcore/Deployment/n64/src/data/models.h.j2 @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" {{ +#endif + +typedef enum {{ +{model_enum_entries} + MODEL_COUNT = {model_count} +}} ModelId; + +void models_init(void); +void models_shutdown(void); + +T3DModel *models_get(ModelId id); +rspq_block_t *models_get_dpl(ModelId id); + +void models_release(ModelId id); // Release a reference to a model +void models_unload(ModelId id); // Force unload a model +void models_unload_unused(void); // Unload models with zero references +void models_unload_all(void); // Unload all models to free memory + +#ifdef __cplusplus +}} +#endif diff --git a/armorcore/Deployment/n64/src/data/scenes.c.j2 b/armorcore/Deployment/n64/src/data/scenes.c.j2 new file mode 100644 index 0000000000..24818d218f --- /dev/null +++ b/armorcore/Deployment/n64/src/data/scenes.c.j2 @@ -0,0 +1,396 @@ +#include +#include +#include +#include + +#include "../types.h" +#include "../engine.h" +#include "../renderer.h" +#include "scenes.h" + +#if ENGINE_ENABLE_PHYSICS +#include "../oimo/physics.h" +#endif + +static ArmScene g_scenes[SCENE_COUNT]; +static SceneId g_current_scene = SCENE_COUNT; +static SceneId g_pending_scene = SCENE_COUNT; + +void scene_init(SceneId id) +{{ + if (id >= SCENE_COUNT) return; + g_current_scene = id; + + switch (id) {{ +{scene_init_switch_cases} + default: break; + }} + scene_on_ready(id); +}} + +void scene_on_ready(SceneId id) +{{ + ArmScene *s = &g_scenes[id]; + + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].on_ready) s->traits[i].on_ready(s, s->traits[i].data); + }} + if (s->cameras) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].on_ready) s->cameras[i].traits[t].on_ready(&s->cameras[i], s->cameras[i].traits[t].data); + }} + }} + }} + if (s->lights) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].on_ready) s->lights[i].traits[t].on_ready(&s->lights[i], s->lights[i].traits[t].data); + }} + }} + }} + if (s->objects) {{ + for (uint16_t i = 0; i < s->object_count; i++) {{ + if (s->objects[i].is_removed) continue; + for (uint8_t t = 0; t < s->objects[i].trait_count; t++) {{ + if (s->objects[i].traits[t].on_ready) s->objects[i].traits[t].on_ready(&s->objects[i], s->objects[i].traits[t].data); + }} + }} + }} +}} + +void scene_on_fixed_update(SceneId id, float dt) +{{ + ArmScene *s = &g_scenes[id]; + + // Scene-level traits + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].on_fixed_update) s->traits[i].on_fixed_update(s, dt, s->traits[i].data); + }} + + // Cameras (usually 1, direct iteration is fine) + if (s->cameras) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + if (!(s->cameras[i].lifecycle_flags & ARM_LIFECYCLE_FIXED_UPDATE)) continue; + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].on_fixed_update) s->cameras[i].traits[t].on_fixed_update(&s->cameras[i], dt, s->cameras[i].traits[t].data); + }} + }} + }} + + // Lights (usually few, direct iteration is fine) + if (s->lights) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + if (!(s->lights[i].lifecycle_flags & ARM_LIFECYCLE_FIXED_UPDATE)) continue; + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].on_fixed_update) s->lights[i].traits[t].on_fixed_update(&s->lights[i], dt, s->lights[i].traits[t].data); + }} + }} + }} + + // Objects - use active list for O(active) instead of O(total) + if (s->active_objects && s->active_trait_count > 0) {{ + for (uint16_t i = 0; i < s->active_trait_count; i++) {{ + ArmObject *obj = &s->objects[s->active_objects[i]]; + if (obj->is_removed || !(obj->lifecycle_flags & ARM_LIFECYCLE_FIXED_UPDATE)) continue; + for (uint8_t t = 0; t < obj->trait_count; t++) {{ + if (obj->traits[t].on_fixed_update) obj->traits[t].on_fixed_update(obj, dt, obj->traits[t].data); + }} + }} + }} +}} + +void scene_sync_physics(SceneId id) +{{ +#if ENGINE_ENABLE_PHYSICS + ArmScene *s = &g_scenes[id]; + if (s->objects) {{ + for (uint16_t i = 0; i < s->object_count; i++) {{ + if (s->objects[i].is_removed) continue; + physics_sync_object(&s->objects[i]); + }} + }} +#else + (void)id; +#endif +}} + +void scene_on_update(SceneId id, float dt) +{{ + ArmScene *s = &g_scenes[id]; + + // Scene-level traits + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].on_update) s->traits[i].on_update(s, dt, s->traits[i].data); + }} + + // Cameras + if (s->cameras) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + if (!(s->cameras[i].lifecycle_flags & ARM_LIFECYCLE_UPDATE)) continue; + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].on_update) s->cameras[i].traits[t].on_update(&s->cameras[i], dt, s->cameras[i].traits[t].data); + }} + }} + }} + + // Lights + if (s->lights) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + if (!(s->lights[i].lifecycle_flags & ARM_LIFECYCLE_UPDATE)) continue; + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].on_update) s->lights[i].traits[t].on_update(&s->lights[i], dt, s->lights[i].traits[t].data); + }} + }} + }} + + // Objects - use active list + if (s->active_objects && s->active_trait_count > 0) {{ + for (uint16_t i = 0; i < s->active_trait_count; i++) {{ + ArmObject *obj = &s->objects[s->active_objects[i]]; + if (obj->is_removed || !(obj->lifecycle_flags & ARM_LIFECYCLE_UPDATE)) continue; + for (uint8_t t = 0; t < obj->trait_count; t++) {{ + if (obj->traits[t].on_update) obj->traits[t].on_update(obj, dt, obj->traits[t].data); + }} + }} + }} +}} + +void scene_on_late_update(SceneId id, float dt) +{{ + ArmScene *s = &g_scenes[id]; + + // Scene-level traits + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].on_late_update) s->traits[i].on_late_update(s, dt, s->traits[i].data); + }} + + // Cameras + if (s->cameras) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + if (!(s->cameras[i].lifecycle_flags & ARM_LIFECYCLE_LATE_UPDATE)) continue; + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].on_late_update) s->cameras[i].traits[t].on_late_update(&s->cameras[i], dt, s->cameras[i].traits[t].data); + }} + }} + }} + + // Lights + if (s->lights) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + if (!(s->lights[i].lifecycle_flags & ARM_LIFECYCLE_LATE_UPDATE)) continue; + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].on_late_update) s->lights[i].traits[t].on_late_update(&s->lights[i], dt, s->lights[i].traits[t].data); + }} + }} + }} + + // Objects - use active list + if (s->active_objects && s->active_trait_count > 0) {{ + for (uint16_t i = 0; i < s->active_trait_count; i++) {{ + ArmObject *obj = &s->objects[s->active_objects[i]]; + if (obj->is_removed || !(obj->lifecycle_flags & ARM_LIFECYCLE_LATE_UPDATE)) continue; + for (uint8_t t = 0; t < obj->trait_count; t++) {{ + if (obj->traits[t].on_late_update) obj->traits[t].on_late_update(obj, dt, obj->traits[t].data); + }} + }} + }} +}} + +void scene_on_remove(SceneId id) +{{ + ArmScene *s = &g_scenes[id]; + + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].on_remove) s->traits[i].on_remove(s, s->traits[i].data); + }} + if (s->cameras) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].on_remove) s->cameras[i].traits[t].on_remove(&s->cameras[i], s->cameras[i].traits[t].data); + }} + }} + }} + if (s->lights) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].on_remove) s->lights[i].traits[t].on_remove(&s->lights[i], s->lights[i].traits[t].data); + }} + }} + }} + if (s->objects) {{ + for (uint16_t i = 0; i < s->object_count; i++) {{ + for (uint8_t t = 0; t < s->objects[i].trait_count; t++) {{ + if (s->objects[i].traits[t].on_remove) s->objects[i].traits[t].on_remove(&s->objects[i], s->objects[i].traits[t].data); + }} + }} + }} +}} + +void scene_clear(SceneId id) +{{ + if (id >= SCENE_COUNT) return; + + ArmScene *s = &g_scenes[id]; + scene_on_remove(id); + + // Free static display list + renderer_free_static_dpl(s); + + // Free object resources + if (s->objects != NULL) {{ + for (uint16_t i = 0; i < s->object_count; i++) {{ +#if ENGINE_ENABLE_PHYSICS + if (s->objects[i].rigid_body) physics_remove_body(&s->objects[i]); +#endif + if (s->objects[i].model_mat) {{ free_uncached(s->objects[i].model_mat); s->objects[i].model_mat = NULL; }} + // Free trait data (malloc'd) before freeing traits array + if (s->objects[i].traits) {{ + for (uint8_t t = 0; t < s->objects[i].trait_count; t++) {{ + if (s->objects[i].traits[t].data) {{ free(s->objects[i].traits[t].data); s->objects[i].traits[t].data = NULL; }} + }} + free(s->objects[i].traits); + s->objects[i].traits = NULL; + }} + s->objects[i].dpl = NULL; + s->objects[i].trait_count = 0; + }} + }} + + // Free camera trait data + if (s->cameras != NULL) {{ + for (uint8_t i = 0; i < s->camera_count; i++) {{ + if (s->cameras[i].traits) {{ + for (uint8_t t = 0; t < s->cameras[i].trait_count; t++) {{ + if (s->cameras[i].traits[t].data) {{ free(s->cameras[i].traits[t].data); s->cameras[i].traits[t].data = NULL; }} + }} + free(s->cameras[i].traits); + s->cameras[i].traits = NULL; + }} + s->cameras[i].trait_count = 0; + }} + }} + + // Free light trait data + if (s->lights != NULL) {{ + for (uint8_t i = 0; i < s->light_count; i++) {{ + if (s->lights[i].traits) {{ + for (uint8_t t = 0; t < s->lights[i].trait_count; t++) {{ + if (s->lights[i].traits[t].data) {{ free(s->lights[i].traits[t].data); s->lights[i].traits[t].data = NULL; }} + }} + free(s->lights[i].traits); + s->lights[i].traits = NULL; + }} + s->lights[i].trait_count = 0; + }} + }} + + // Free scene-level trait data (note: scene_traits array itself is static, don't free it) + if (s->traits != NULL) {{ + for (uint8_t i = 0; i < s->trait_count; i++) {{ + if (s->traits[i].data) {{ free(s->traits[i].data); s->traits[i].data = NULL; }} + }} + // Don't free s->traits - it's a static array + s->traits = NULL; + }} + + s->world.clear_color[0] = s->world.clear_color[1] = s->world.clear_color[2] = 0; + s->world.clear_color[3] = 0xFF; + s->world.ambient_color[0] = s->world.ambient_color[1] = s->world.ambient_color[2] = 0; + s->world.ambient_color[3] = 0xFF; + s->cameras = NULL; + s->camera_count = 0; + s->active_camera_id = 0; + s->lights = NULL; + s->light_count = 0; + s->objects = NULL; + s->object_count = 0; + s->active_count = 0; + s->free_list = NULL; + s->free_count = 0; + s->free_capacity = 0; + s->active_objects = NULL; + s->active_trait_count = 0; + s->trait_count = 0; + s->lifecycle_flags = 0; +}} + +ArmScene *scene_get(SceneId id) +{{ + if (id >= SCENE_COUNT) return NULL; + return &g_scenes[id]; +}} + +// Scene name to ID lookup table +static const struct {{ const char *name; SceneId id; }} g_scene_names[] = {{ +{scene_name_entries} +}}; + +SceneId scene_get_id_by_name(const char *name) +{{ + if (name == NULL) return SCENE_COUNT; + for (int i = 0; i < {scene_count}; i++) {{ + if (strcmp(g_scene_names[i].name, name) == 0) {{ + return g_scene_names[i].id; + }} + }} + return SCENE_COUNT; // Not found +}} + +SceneId scene_get_current_id(void) {{ return g_current_scene; }} +SceneId scene_get_pending(void) {{ return g_pending_scene; }} +void scene_switch_to(SceneId id) {{ g_pending_scene = id; }} +void scene_clear_pending(void) {{ g_pending_scene = SCENE_COUNT; }} + +// ============================================================================= +// Object Pool Management +// ============================================================================= + +void scene_recycle_object(SceneId id, ArmObject *obj) +{{ + if (id >= SCENE_COUNT) return; + ArmScene *s = &g_scenes[id]; + + if (!obj || !s->free_list) return; + + // Find object index in the array + ptrdiff_t idx = obj - s->objects; + if (idx < 0 || idx >= s->object_count) return; + + // Add to free list if there's room + if (s->free_count < s->free_capacity) {{ + s->free_list[s->free_count++] = (uint16_t)idx; + s->active_count--; + }} +}} + +ArmObject *scene_alloc_object(SceneId id) +{{ + if (id >= SCENE_COUNT) return NULL; + ArmScene *s = &g_scenes[id]; + + if (!s->free_list || s->free_count == 0) {{ + debugf("Scene: object pool exhausted\\n"); + return NULL; + }} + + // Pop from free list (LIFO for cache locality) + uint16_t idx = s->free_list[--s->free_count]; + ArmObject *obj = &s->objects[idx]; + + // Reset object to clean state + memset(obj, 0, sizeof(ArmObject)); + obj->visible = true; + obj->transform.scale = (T3DVec3){{{{1.0f, 1.0f, 1.0f}}}}; + obj->transform.rot = (T3DQuat){{{{0.0f, 0.0f, 0.0f, 1.0f}}}}; + obj->transform.dirty = TRANSFORM_DIRTY_FRAMES; + + s->active_count++; + return obj; +}} + +uint16_t scene_get_active_count(SceneId id) +{{ + if (id >= SCENE_COUNT) return 0; + return g_scenes[id].active_count; +}} diff --git a/armorcore/Deployment/n64/src/data/scenes.h.j2 b/armorcore/Deployment/n64/src/data/scenes.h.j2 new file mode 100644 index 0000000000..32b7f55f16 --- /dev/null +++ b/armorcore/Deployment/n64/src/data/scenes.h.j2 @@ -0,0 +1,46 @@ +#pragma once + +#include "../types.h" + +#ifdef __cplusplus +extern "C" {{ +#endif + +typedef enum SceneId {{ +{scene_enum_entries} + SCENE_COUNT = {scene_count} +}} SceneId; + +{scene_declarations} + +void scene_init(SceneId id); +void scene_on_ready(SceneId id); +void scene_on_fixed_update(SceneId id, float dt); +void scene_on_update(SceneId id, float dt); +void scene_on_late_update(SceneId id, float dt); +void scene_on_remove(SceneId id); +void scene_clear(SceneId id); + +void scene_sync_physics(SceneId id); + +void scene_switch_to(SceneId id); +SceneId scene_get_id_by_name(const char *name); +SceneId scene_get_current_id(void); +SceneId scene_get_pending(void); +void scene_clear_pending(void); + +ArmScene *scene_get(SceneId id); + +// Object pool management +// Recycle a removed object's slot back to the pool for reuse +void scene_recycle_object(SceneId id, ArmObject *obj); + +// Allocate an object slot from the pool (returns NULL if pool exhausted) +ArmObject *scene_alloc_object(SceneId id); + +// Get count of active (non-removed) objects +uint16_t scene_get_active_count(SceneId id); + +#ifdef __cplusplus +}} +#endif \ No newline at end of file diff --git a/armorcore/Deployment/n64/src/data/traits.c.j2 b/armorcore/Deployment/n64/src/data/traits.c.j2 new file mode 100644 index 0000000000..b23c9258fe --- /dev/null +++ b/armorcore/Deployment/n64/src/data/traits.c.j2 @@ -0,0 +1,24 @@ +#include "traits.h" +#include "scenes.h" +#include "../types.h" +#include "../engine.h" +#include "../utils.h" +#include "../events/trait_events.h" +#include "../iron/object/transform.h" +#include "../iron/object/object.h" +#include "../iron/system/input.h" +#include + +#if ENGINE_ENABLE_PHYSICS +#include "../events/physics_events.h" +#include "../oimo/physics.h" +#endif + +#if ENGINE_ENABLE_UI +#include "../koui/koui.h" +#endif + +// Global signals (shared across traits) +{global_signals} + +{trait_implementations} \ No newline at end of file diff --git a/armorcore/Deployment/n64/src/data/traits.h.j2 b/armorcore/Deployment/n64/src/data/traits.h.j2 new file mode 100644 index 0000000000..cfa822d1fa --- /dev/null +++ b/armorcore/Deployment/n64/src/data/traits.h.j2 @@ -0,0 +1,28 @@ +#pragma once + +#include "scenes.h" +#include "../types.h" +#include "../engine.h" +#include "../events/trait_events.h" +#include "../iron/system/input.h" + +#if ENGINE_ENABLE_UI +#include "../koui/koui.h" +#endif + +#ifdef __cplusplus +extern "C" {{ +#endif + +// Trait data structures (for per-instance data) +{trait_data_structs} + +// Trait function declarations +{trait_declarations} + +// Event handler declarations +{event_handler_declarations} + +#ifdef __cplusplus +}} +#endif diff --git a/armorcore/Deployment/n64/src/engine.c b/armorcore/Deployment/n64/src/engine.c new file mode 100644 index 0000000000..719c10b6c5 --- /dev/null +++ b/armorcore/Deployment/n64/src/engine.c @@ -0,0 +1,45 @@ +#include +#include +#include + +#include "types.h" +#include "engine.h" +#include "data/models.h" +#include "iron/system/input.h" +#if ENGINE_ENABLE_PHYSICS +#include "oimo/physics.h" +#endif + +void engine_init(void) +{ + debug_init_isviewer(); + debug_init_usblog(); + + asset_init_compression(2); + dfs_init(DFS_DEFAULT_LOCATION); + + display_init(RESOLUTION_320x240, DEPTH_16_BPP, FB_COUNT, GAMMA_NONE, FILTERS_RESAMPLE_ANTIALIAS); + + rdpq_init(); +#ifdef ARM_DEBUG_HUD + rdpq_debug_start(); +#endif + t3d_init((T3DInitParams){}); + rdpq_text_register_font(FONT_BUILTIN_DEBUG_MONO, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO)); + + input_init(); + models_init(); + +#if ENGINE_ENABLE_PHYSICS + physics_init(); +#endif +} + +void engine_shutdown(void) +{ +#if ENGINE_ENABLE_PHYSICS + physics_shutdown(); +#endif + models_shutdown(); + t3d_destroy(); +} diff --git a/armorcore/Deployment/n64/src/engine.h.j2 b/armorcore/Deployment/n64/src/engine.h.j2 new file mode 100644 index 0000000000..19b189c5dd --- /dev/null +++ b/armorcore/Deployment/n64/src/engine.h.j2 @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" {{ +#endif + +#define ENGINE_ENABLE_PHYSICS {enable_physics} +#define ENGINE_ENABLE_PHYSICS_DEBUG {enable_physics_debug} +#define ENGINE_ENABLE_UI {enable_ui} + +void engine_init(void); +void engine_shutdown(void); + +#ifdef __cplusplus +}} +#endif diff --git a/armorcore/Deployment/n64/src/events/physics_events.c b/armorcore/Deployment/n64/src/events/physics_events.c new file mode 100644 index 0000000000..fac3c6aca2 --- /dev/null +++ b/armorcore/Deployment/n64/src/events/physics_events.c @@ -0,0 +1,157 @@ +// physics_events.c - Per-object physics contact event subscriptions +#include "physics_events.h" +#include "../oimo/physics.h" +#include + +// Static storage for body contact subscriptions +static BodyContactSubs g_body_subs[MAX_CONTACT_BODIES]; +static int g_body_sub_count = 0; + +void physics_contact_init(void) +{ + memset(g_body_subs, 0, sizeof(g_body_subs)); + g_body_sub_count = 0; +} + +void physics_contact_clear(void) +{ + memset(g_body_subs, 0, sizeof(g_body_subs)); + g_body_sub_count = 0; +} + +// Find or create subscription slot for a rigid body +static BodyContactSubs* find_or_create_body_subs(OimoRigidBody* rb) +{ + // Find existing + for (int i = 0; i < g_body_sub_count; i++) { + if (g_body_subs[i].rb == rb) { + return &g_body_subs[i]; + } + } + // Create new if space available + if (g_body_sub_count >= MAX_CONTACT_BODIES) { + return NULL; + } + BodyContactSubs* slot = &g_body_subs[g_body_sub_count++]; + slot->rb = rb; + slot->count = 0; + return slot; +} + +// Find subscription slot for a rigid body +static BodyContactSubs* find_body_subs(OimoRigidBody* rb) +{ + for (int i = 0; i < g_body_sub_count; i++) { + if (g_body_subs[i].rb == rb) { + return &g_body_subs[i]; + } + } + return NULL; +} + +void physics_contact_subscribe(OimoRigidBody* rb, PhysicsContactHandler handler, void* obj, void* data) +{ + if (!rb || !handler) return; + + BodyContactSubs* subs = find_or_create_body_subs(rb); + if (!subs || subs->count >= MAX_CONTACT_SUBSCRIBERS) return; + + subs->subs[subs->count].handler = handler; + subs->subs[subs->count].obj = obj; + subs->subs[subs->count].data = data; + subs->count++; +} + +void physics_contact_unsubscribe(OimoRigidBody* rb, PhysicsContactHandler handler, void* obj) +{ + if (!rb || !handler) return; + + BodyContactSubs* subs = find_body_subs(rb); + if (!subs) return; + + // Find and remove matching subscription + for (int i = 0; i < subs->count; ) { + if (subs->subs[i].handler == handler && subs->subs[i].obj == obj) { + // Shift remaining elements + for (int j = i; j < subs->count - 1; j++) { + subs->subs[j] = subs->subs[j + 1]; + } + subs->count--; + } else { + i++; + } + } +} + +void physics_contact_unsubscribe_all(void* obj) +{ + for (int b = 0; b < g_body_sub_count; b++) { + BodyContactSubs* subs = &g_body_subs[b]; + for (int i = 0; i < subs->count; ) { + if (subs->subs[i].obj == obj) { + // Shift remaining elements + for (int j = i; j < subs->count - 1; j++) { + subs->subs[j] = subs->subs[j + 1]; + } + subs->count--; + } else { + i++; + } + } + } +} + +void physics_contact_dispatch(void) +{ + // Fast path: skip entirely if no bodies have contact subscriptions + if (g_body_sub_count == 0) return; + + // Called after physics step + // Iterate all touching contacts and dispatch to subscribed handlers + OimoWorld* world = physics_get_world(); + if (!world) return; + + OimoContact* contact = world->_contactManager._contactList; + while (contact != NULL) { + // Only process touching contacts + if (contact->_touching) { + // Get both bodies' ArmObject pointers + OimoRigidBody* rb1 = contact->_b1; + OimoRigidBody* rb2 = contact->_b2; + + // Early exit: check if either body has subscribers before getting objects + BodyContactSubs* subs1 = find_body_subs(rb1); + BodyContactSubs* subs2 = find_body_subs(rb2); + + // Skip this contact if neither body has subscribers + if (!subs1 && !subs2) { + contact = contact->_next; + continue; + } + + ArmObject* obj1 = (ArmObject*)rb1->userData; + ArmObject* obj2 = (ArmObject*)rb2->userData; + + // Skip if either object has been marked for removal + if (!obj1 || !obj2 || obj1->is_removed || obj2->is_removed) { + contact = contact->_next; + continue; + } + + // Dispatch to body1's subscribers with body2 as "other" + if (subs1) { + for (int i = 0; i < subs1->count; i++) { + subs1->subs[i].handler(subs1->subs[i].obj, subs1->subs[i].data, obj2); + } + } + + // Dispatch to body2's subscribers with body1 as "other" + if (subs2) { + for (int i = 0; i < subs2->count; i++) { + subs2->subs[i].handler(subs2->subs[i].obj, subs2->subs[i].data, obj1); + } + } + } + contact = contact->_next; + } +} diff --git a/armorcore/Deployment/n64/src/events/physics_events.h.j2 b/armorcore/Deployment/n64/src/events/physics_events.h.j2 new file mode 100644 index 0000000000..51d345cea2 --- /dev/null +++ b/armorcore/Deployment/n64/src/events/physics_events.h.j2 @@ -0,0 +1,74 @@ +// physics_events.h - Per-object physics contact event subscriptions +#pragma once + +#include "../types.h" +#include "../oimo/oimo.h" + +#ifdef __cplusplus +extern "C" {{ +#endif + +/** + * Physics Contact Event System + * + * Similar to trait_events for input, but for physics contacts. + * Traits can subscribe to contact notifications for specific rigid bodies. + * + * Usage: + * physics_contact_subscribe(rb, handler, obj, data); // In on_ready + * physics_contact_unsubscribe(rb, handler, obj); // In on_remove + * physics_contact_dispatch(); // Called by physics_step + */ + +// Maximum contact subscriptions per rigid body +#define MAX_CONTACT_SUBSCRIBERS {max_contact_subscribers} + +// Maximum rigid bodies with contact subscriptions +#define MAX_CONTACT_BODIES {max_contact_bodies} + +// Contact handler signature: (obj, data, other_obj) +// - obj: the object this trait is attached to +// - data: trait data pointer +// - other_obj: the object we collided with +typedef void (*PhysicsContactHandler)(void* obj, void* data, ArmObject* other); + +// Subscription entry +typedef struct {{ + PhysicsContactHandler handler; + void* obj; + void* data; +}} PhysicsContactSub; + +// Per-body subscriptions +typedef struct {{ + OimoRigidBody* rb; + PhysicsContactSub subs[MAX_CONTACT_SUBSCRIBERS]; + uint8_t count; +}} BodyContactSubs; + +// Initialize contact event system +void physics_contact_init(void); + +// Clear all subscriptions (call on scene change) +void physics_contact_clear(void); + +// Subscribe to contact events for a rigid body +// handler: function to call on contact +// obj: object pointer (usually the ArmObject with this rigid body) +// data: trait data pointer +void physics_contact_subscribe(OimoRigidBody* rb, PhysicsContactHandler handler, void* obj, void* data); + +// Unsubscribe from contact events +// Matches by rb + handler + obj (same as subscribe) +void physics_contact_unsubscribe(OimoRigidBody* rb, PhysicsContactHandler handler, void* obj); + +// Unsubscribe all handlers for an object (call when trait is removed) +void physics_contact_unsubscribe_all(void* obj); + +// Dispatch contact events for all touching bodies +// Called internally by physics_step after world simulation +void physics_contact_dispatch(void); + +#ifdef __cplusplus +}} +#endif \ No newline at end of file diff --git a/armorcore/Deployment/n64/src/events/trait_events.c b/armorcore/Deployment/n64/src/events/trait_events.c new file mode 100644 index 0000000000..9f6497689e --- /dev/null +++ b/armorcore/Deployment/n64/src/events/trait_events.c @@ -0,0 +1,171 @@ +#include "trait_events.h" +#include "../iron/system/input.h" +#include + +static TraitEventState g_events; + +void trait_events_init(void) +{ + memset(&g_events, 0, sizeof(g_events)); +} + +void trait_events_clear(void) +{ + memset(&g_events, 0, sizeof(g_events)); +} + +void trait_events_subscribe_started(N64Button btn, TraitEventHandler handler, void* obj, void* data) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + if (subs->started_count >= MAX_BUTTON_SUBSCRIBERS) return; + + subs->started[subs->started_count].handler = handler; + subs->started[subs->started_count].obj = obj; + subs->started[subs->started_count].data = data; + subs->started_count++; + + // Set bitmask flag for fast dispatch + g_events.has_started_subs |= (1 << btn); +} + +void trait_events_subscribe_released(N64Button btn, TraitEventHandler handler, void* obj, void* data) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + if (subs->released_count >= MAX_BUTTON_SUBSCRIBERS) return; + + subs->released[subs->released_count].handler = handler; + subs->released[subs->released_count].obj = obj; + subs->released[subs->released_count].data = data; + subs->released_count++; + + // Set bitmask flag for fast dispatch + g_events.has_released_subs |= (1 << btn); +} + +void trait_events_subscribe_down(N64Button btn, TraitEventHandler handler, void* obj, void* data) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + if (subs->down_count >= MAX_BUTTON_SUBSCRIBERS) return; + + subs->down[subs->down_count].handler = handler; + subs->down[subs->down_count].obj = obj; + subs->down[subs->down_count].data = data; + subs->down_count++; + + // Set bitmask flag for fast dispatch + g_events.has_down_subs |= (1 << btn); +} + +// Helper to remove subscription from an array by object pointer +static void remove_sub_by_obj(TraitEventSub* subs, uint8_t* count, void* obj) +{ + for (int i = 0; i < *count; ) { + if (subs[i].obj == obj) { + // Shift remaining elements down + for (int j = i; j < *count - 1; j++) { + subs[j] = subs[j + 1]; + } + (*count)--; + } else { + i++; + } + } +} + +// Helper to update bitmask after unsubscribe +static void update_bitmask(uint16_t* mask, int btn, uint8_t count) +{ + if (count == 0) { + *mask &= ~(1 << btn); + } +} + +void trait_events_unsubscribe_started(N64Button btn, void* obj) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + remove_sub_by_obj(subs->started, &subs->started_count, obj); + update_bitmask(&g_events.has_started_subs, btn, subs->started_count); +} + +void trait_events_unsubscribe_released(N64Button btn, void* obj) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + remove_sub_by_obj(subs->released, &subs->released_count, obj); + update_bitmask(&g_events.has_released_subs, btn, subs->released_count); +} + +void trait_events_unsubscribe_down(N64Button btn, void* obj) +{ + if (btn >= N64_BTN_COUNT) return; + ButtonEventSubs* subs = &g_events.buttons[btn]; + remove_sub_by_obj(subs->down, &subs->down_count, obj); + update_bitmask(&g_events.has_down_subs, btn, subs->down_count); +} + +void trait_events_unsubscribe_all(void* obj) +{ + for (int btn = 0; btn < N64_BTN_COUNT; btn++) { + ButtonEventSubs* subs = &g_events.buttons[btn]; + remove_sub_by_obj(subs->started, &subs->started_count, obj); + remove_sub_by_obj(subs->released, &subs->released_count, obj); + remove_sub_by_obj(subs->down, &subs->down_count, obj); + update_bitmask(&g_events.has_started_subs, btn, subs->started_count); + update_bitmask(&g_events.has_released_subs, btn, subs->released_count); + update_bitmask(&g_events.has_down_subs, btn, subs->down_count); + } +} + +void trait_events_dispatch(float dt) +{ + // Fast path: skip entirely if no subscribers at all + if (g_events.has_started_subs == 0 && + g_events.has_released_subs == 0 && + g_events.has_down_subs == 0) { + return; + } + + for (int btn = 0; btn < N64_BTN_COUNT; btn++) { + uint16_t btn_mask = (1 << btn); + + // Dispatch "started" events - skip if no subscribers for this button + if ((g_events.has_started_subs & btn_mask) && input_started((N64Button)btn)) { + ButtonEventSubs* subs = &g_events.buttons[btn]; + for (uint8_t i = 0; i < subs->started_count; i++) { + subs->started[i].handler( + subs->started[i].obj, + subs->started[i].data, + dt + ); + } + } + + // Dispatch "released" events - skip if no subscribers for this button + if ((g_events.has_released_subs & btn_mask) && input_released((N64Button)btn)) { + ButtonEventSubs* subs = &g_events.buttons[btn]; + for (uint8_t i = 0; i < subs->released_count; i++) { + subs->released[i].handler( + subs->released[i].obj, + subs->released[i].data, + dt + ); + } + } + + // Dispatch "down" events - skip if no subscribers for this button + if ((g_events.has_down_subs & btn_mask) && input_down((N64Button)btn)) { + ButtonEventSubs* subs = &g_events.buttons[btn]; + for (uint8_t i = 0; i < subs->down_count; i++) { + subs->down[i].handler( + subs->down[i].obj, + subs->down[i].data, + dt + ); + } + } + } +} diff --git a/armorcore/Deployment/n64/src/events/trait_events.h.j2 b/armorcore/Deployment/n64/src/events/trait_events.h.j2 new file mode 100644 index 0000000000..0bfafd98eb --- /dev/null +++ b/armorcore/Deployment/n64/src/events/trait_events.h.j2 @@ -0,0 +1,78 @@ +#pragma once + +#include "../types.h" +#include "../iron/system/input.h" + +#ifdef __cplusplus +extern "C" {{ +#endif + +/** + * Event-driven trait system. + * + * Instead of checking input inside on_update, traits register handlers + * for specific button events. The engine polls input once and dispatches + * to all registered handlers. + * + * Event types: + * - btn_