diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 0b26c33..aca1c59 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -337,11 +337,60 @@ target_include_directories(editSceneEditor PRIVATE ) # --------------------------------------------------------------------------- -# Test: ActionDatabase Lua API +# Tests: Lua API standalone tests # --------------------------------------------------------------------------- -# Standalone test that verifies the ActionDatabase singleton and Lua API -# work correctly. Does not require OGRE or Flecs - only Lua and the -# core component types. +# These standalone tests verify the Lua API functions work correctly. +# They do not require OGRE or Flecs - only Lua and the core component types. + +# Test: Entity Lua API +add_executable(entity_lua_test + tests/entity_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(entity_lua_test + lua +) + +target_include_directories(entity_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + +# Test: Component Lua API +add_executable(component_lua_test + tests/component_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(component_lua_test + lua +) + +target_include_directories(component_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + +# Test: Event Lua API +add_executable(event_lua_test + tests/event_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(event_lua_test + lua +) + +target_include_directories(event_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + +# Test: ActionDatabase Lua API add_executable(action_db_lua_test tests/action_db_lua_test.cpp components/ActionDatabase.cpp @@ -353,6 +402,7 @@ add_executable(action_db_lua_test target_link_libraries(action_db_lua_test lua + flecs::flecs_static ) target_include_directories(action_db_lua_test PRIVATE diff --git a/src/features/editScene/lua-examples/component_example.lua b/src/features/editScene/lua-examples/component_example.lua new file mode 100644 index 0000000..bee1faf --- /dev/null +++ b/src/features/editScene/lua-examples/component_example.lua @@ -0,0 +1,711 @@ +-- ============================================================================= +-- Component Lua API Examples +-- ============================================================================= +-- This file demonstrates how to add, remove, query, and manipulate ECS +-- components from Lua using the ecs.* Lua API. +-- +-- Components are data attached to entities. They define what an entity IS +-- and what it CAN DO. For example, a Transform component gives an entity +-- a position in the world, while a Renderable component makes it visible. +-- ============================================================================= + +-- ============================================================================= +-- Checking for Components +-- ============================================================================= + +-- Create an entity and check what components it has: +local entity = ecs.create_entity() +ecs.set_entity_name(entity, "TestObject") + +-- Check if an entity has a specific component: +if ecs.has_component(entity, "Transform") then + print("Entity has Transform component") +end + +-- New entities typically have EditorMarker by default: +if ecs.has_component(entity, "EditorMarker") then + print("Entity has EditorMarker (default for new entities)") +end + +-- ============================================================================= +-- Adding and Removing Components +-- ============================================================================= + +-- Add a tag component (a component with no data): +ecs.add_component(entity, "InWater") +print("Added InWater tag") + +-- Check it was added: +if ecs.has_component(entity, "InWater") then + print("Entity is now in water") +end + +-- Remove a tag component: +ecs.remove_component(entity, "InWater") +if not ecs.has_component(entity, "InWater") then + print("Entity is no longer in water") +end + +-- ============================================================================= +-- Setting and Getting Components with Data +-- ============================================================================= + +-- Set a component with data (creates or replaces it): +ecs.set_component(entity, "Transform", { + position = { 10, 20, 30 }, + rotation = { 1, 0, 0, 0 }, -- quaternion w, x, y, z + scale = { 2, 2, 2 } +}) + +-- Get the component back: +local transform = ecs.get_component(entity, "Transform") +if transform then + print("Position: " .. transform.position[1] .. ", " + .. transform.position[2] .. ", " + .. transform.position[3]) + print("Scale: " .. transform.scale[1] .. ", " + .. transform.scale[2] .. ", " + .. transform.scale[3]) +end + +-- ============================================================================= +-- Individual Field Access +-- ============================================================================= + +-- Get a single field from a component: +local pos_x = ecs.get_field(entity, "Transform", "position") +print("Position X from get_field: " .. pos_x[1]) + +-- Set a single field: +ecs.set_field(entity, "Transform", "position", { 0, 0, 0 }) +local new_pos = ecs.get_field(entity, "Transform", "position") +print("New position: " .. new_pos[1] .. ", " .. new_pos[2] .. ", " .. new_pos[3]) + +-- ============================================================================= +-- Practical Examples: Common Components +-- ============================================================================= + +-- Create a player character: +function create_player(name, x, y, z) + local player = ecs.create_entity() + ecs.set_entity_name(player, name) + + -- Transform (position in the world): + ecs.set_component(player, "Transform", { + position = { x, y, z }, + rotation = { 1, 0, 0, 0 }, + scale = { 1, 1, 1 } + }) + + -- Renderable (visible mesh): + ecs.set_component(player, "Renderable", { + meshName = "character.mesh", + visible = true + }) + + -- Character (physics capsule for movement): + ecs.set_component(player, "Character", { + radius = 0.4, + height = 1.8, + offset = { 0, 0.9, 0 }, + enabled = true, + useGravity = true + }) + + -- PlayerController (camera and input): + ecs.set_component(player, "PlayerController", { + cameraMode = 1, + tpsDistance = 5.0, + tpsHeight = 2.0, + mouseSensitivity = 0.5 + }) + + -- Inventory: + ecs.set_component(player, "Inventory", { + maxSlots = 20, + maxWeight = 50.0, + isContainer = true + }) + + print("Created player: " .. name) + return player +end + +local hero = create_player("Hero", 0, 0, 0) + +-- Create a light source: +function create_light(name, x, y, z, light_type) + local light = ecs.create_entity() + ecs.set_entity_name(light, name) + + ecs.set_component(light, "Transform", { + position = { x, y, z }, + rotation = { 1, 0, 0, 0 }, + scale = { 1, 1, 1 } + }) + + ecs.set_component(light, "Light", { + lightType = light_type or 0, -- 0=point, 1=directional, 2=spot + diffuseColor = { 1, 1, 1, 1 }, + intensity = 1.5, + range = 100, + castShadows = true + }) + + print("Created light: " .. name) + return light +end + +local sun = create_light("Sun", 0, 100, 0, 1) -- directional light + +-- Create a building with smart object interaction: +function create_building(name, x, z) + local building = ecs.create_entity() + ecs.set_entity_name(building, name) + + ecs.set_component(building, "Transform", { + position = { x, 0, z }, + rotation = { 1, 0, 0, 0 }, + scale = { 1, 1, 1 } + }) + + ecs.set_component(building, "Renderable", { + meshName = "building.mesh", + visible = true + }) + + ecs.set_component(building, "SmartObject", { + radius = 2.0, + height = 3.0, + actionNames = { "enter", "exit" } + }) + + -- RigidBody for physics: + ecs.set_component(building, "RigidBody", { + bodyType = 0, -- 0=static + mass = 0, + friction = 0.5, + restitution = 0.1, + enabled = true + }) + + print("Created building: " .. name) + return building +end + +local house = create_building("House", 10, 10) + +-- Create an item: +function create_item(name, item_id, x, y, z) + local item = ecs.create_entity() + ecs.set_entity_name(item, name) + + ecs.set_component(item, "Transform", { + position = { x, y, z }, + rotation = { 1, 0, 0, 0 }, + scale = { 0.5, 0.5, 0.5 } + }) + + ecs.set_component(item, "Renderable", { + meshName = "potion.mesh", + visible = true + }) + + ecs.set_component(item, "Item", { + itemName = name, + itemType = "consumable", + itemId = item_id, + stackSize = 1, + maxStackSize = 10, + weight = 0.5, + value = 50, + useActionName = "drink_potion" + }) + + print("Created item: " .. name) + return item +end + +local potion = create_item("Health Potion", "potion_health", 5, 0.5, 5) + +-- Create a NavMesh area: +function create_navmesh_area(name) + local nav = ecs.create_entity() + ecs.set_entity_name(nav, name) + + ecs.set_component(nav, "NavMesh", { + cellSize = 0.3, + cellHeight = 0.2, + agentHeight = 2.0, + agentRadius = 0.5, + agentMaxClimb = 0.5, + agentMaxSlope = 45.0, + enabled = true + }) + + print("Created NavMesh: " .. name) + return nav +end + +local navmesh = create_navmesh_area("MainNavMesh") + +-- ============================================================================= +-- Working with Tag Components +-- ============================================================================= + +-- Tag components are boolean markers with no data fields. +-- Common tags include: +-- EditorMarker - marks entities visible in the editor +-- InWater - entity is currently in water +-- GeneratedPhysicsTag - physics was auto-generated +-- ParentComponent - entity is a parent in a hierarchy +-- NavMeshGeometrySource - entity contributes to navmesh generation + +-- Example: Mark entities for different purposes: +local marker1 = ecs.create_entity() +ecs.add_component(marker1, "GeneratedPhysicsTag") + +local marker2 = ecs.create_entity() +ecs.add_component(marker2, "NavMeshGeometrySource") + +-- ============================================================================= +-- Working with GOAP Components +-- ============================================================================= + +-- Create an NPC with GOAP AI: +function create_npc(name, x, z) + local npc = ecs.create_entity() + ecs.set_entity_name(npc, name) + + ecs.set_component(npc, "Transform", { + position = { x, 0, z }, + rotation = { 1, 0, 0, 0 }, + scale = { 1, 1, 1 } + }) + + ecs.set_component(npc, "Character", { + radius = 0.4, + height = 1.8, + offset = { 0, 0.9, 0 }, + enabled = true, + useGravity = true + }) + + -- GOAP blackboard (character's knowledge about the world): + ecs.set_component(npc, "GoapBlackboard", { + values = { + health = 100, + stamina = 100, + hunger = 0, + wood_count = 0 + }, + floatValues = { + hunger = 0.0 + }, + stringValues = { + state = "idle" + } + }) + + -- GOAP planner (plans actions to achieve goals): + ecs.set_component(npc, "GoapPlanner", { + enabled = true, + maxIterations = 100 + }) + + -- GOAP runner (executes the plan): + ecs.set_component(npc, "GoapRunner", { + enabled = true, + currentAction = "idle" + }) + + -- Behavior tree for idle behavior: + ecs.set_component(npc, "BehaviorTree", { + treeName = "idle_behavior", + enabled = true + }) + + -- Path following for movement: + ecs.set_component(npc, "PathFollowing", { + enabled = true, + speed = 1.5 + }) + + -- NavMesh agent for navigation: + ecs.set_component(npc, "NavMeshAgent", { + enabled = true, + radius = 0.5, + height = 2.0 + }) + + print("Created NPC: " .. name) + return npc +end + +local npc = create_npc("Villager_01", -10, -10) + +-- ============================================================================= +-- Working with Event Handler Components +-- ============================================================================= + +-- Add event handlers to entities: +ecs.set_component(npc, "EventHandler", { + eventName = "collision", + actionName = "handle_collision", + enabled = true +}) + +-- ============================================================================= +-- Working with Dialogue Components +-- ============================================================================= + +-- Add dialogue to an NPC: +ecs.set_component(npc, "Dialogue", { + text = "Hello there, traveler!", + speaker = "Villager_01", + enabled = true +}) + +-- ============================================================================= +-- Working with Water Components +-- ============================================================================= + +-- Create a water plane: +function create_water(name, y) + local water = ecs.create_entity() + ecs.set_entity_name(water, name) + + ecs.set_component(water, "WaterPlane", { + enabled = true, + waterSurfaceY = y or 0.0, + planeSize = 1000, + reflectivity = 0.5, + waveSpeed = 1.0 + }) + + ecs.set_component(water, "WaterPhysics", { + enabled = true, + waveHeight = 0.5 + }) + + print("Created water: " .. name) + return water +end + +local ocean = create_water("Ocean", 0.0) + +-- ============================================================================= +-- Working with Sky and Sun Components +-- ============================================================================= + +-- Create a skybox: +function create_sky(name) + local sky = ecs.create_entity() + ecs.set_entity_name(sky, name) + + ecs.set_component(sky, "Skybox", { + enabled = true, + size = 500, + starsEnabled = true + }) + + print("Created sky: " .. name) + return sky +end + +local skybox = create_sky("Skybox") + +-- Update sun properties: +ecs.set_component(sun, "Sun", { + enabled = true, + timeOfDay = 12.0, + timeSpeed = 1.0, + intensity = 1.0, + castShadows = true +}) + +-- ============================================================================= +-- Working with Camera Components +-- ============================================================================= + +-- Create a camera: +function create_camera(name) + local cam = ecs.create_entity() + ecs.set_entity_name(cam, name) + + ecs.set_component(cam, "Camera", { + fovY = 60, + nearClip = 0.1, + farClip = 1000, + orthographic = false + }) + + print("Created camera: " .. name) + return cam +end + +local camera = create_camera("MainCamera") + +-- ============================================================================= +-- Working with Prefab Components +-- ============================================================================= + +-- Mark an entity as a prefab instance: +ecs.set_component(house, "PrefabInstance", { + prefabPath = "prefabs/house.json", + instantiated = true +}) + +-- ============================================================================= +-- Working with CellGrid Components +-- ============================================================================= + +-- Create a cell grid for spatial partitioning: +function create_cell_grid(name, width, height, depth) + local grid = ecs.create_entity() + ecs.set_entity_name(grid, name) + + ecs.set_component(grid, "CellGrid", { + width = width or 10, + height = height or 5, + depth = depth or 10, + cellSize = 1.0, + cellHeight = 0.5 + }) + + print("Created cell grid: " .. name) + return grid +end + +local grid = create_cell_grid("WorldGrid", 20, 10, 20) + +-- ============================================================================= +-- Working with Town/District/Lot/Room Components +-- ============================================================================= + +-- Create a town with districts, lots, and rooms: +function create_town(name) + local town = ecs.create_entity() + ecs.set_entity_name(town, name) + + ecs.set_component(town, "Town", { + townName = name, + population = 500 + }) + + -- Create a district: + local district = ecs.create_entity() + ecs.set_entity_name(district, "Market District") + ecs.set_component(district, "District", { + districtName = "Market District", + districtType = "commercial" + }) + + -- Create a lot: + local lot = ecs.create_entity() + ecs.set_entity_name(lot, "Residential Lot 1") + ecs.set_component(lot, "Lot", { + lotName = "Residential Lot 1", + lotType = "residential", + width = 20, + depth = 30 + }) + + -- Create a room: + local room = ecs.create_entity() + ecs.set_entity_name(room, "Kitchen") + ecs.set_component(room, "Room", { + roomName = "Kitchen", + roomType = "kitchen", + floor = 0 + }) + + print("Created town: " .. name) + return town +end + +local rivendell = create_town("Rivendell") + +-- ============================================================================= +-- Working with Furniture Components +-- ============================================================================= + +-- Create a furniture template: +function create_furniture(name, mesh, category) + local furniture = ecs.create_entity() + ecs.set_entity_name(furniture, name) + + ecs.set_component(furniture, "FurnitureTemplate", { + templateName = name, + meshName = mesh or "chair.mesh", + category = category or "seating" + }) + + print("Created furniture: " .. name) + return furniture +end + +local chair = create_furniture("Wooden Chair", "chair.mesh", "seating") + +-- ============================================================================= +-- Working with Animation Components +-- ============================================================================= + +-- Add animation tree to a character: +ecs.set_component(npc, "AnimationTree", { + treeName = "humanoid", + enabled = true +}) + +ecs.set_component(npc, "AnimationTreeTemplate", { + templateName = "humanoid_base", + blendTime = 0.2 +}) + +-- ============================================================================= +-- Working with Physics Components +-- ============================================================================= + +-- Add a physics collider to an entity: +ecs.set_component(house, "PhysicsCollider", { + shapeType = "box", + size = { 5, 3, 5 }, + enabled = true +}) + +-- Add buoyancy info for water physics: +ecs.set_component(house, "BuoyancyInfo", { + enabled = true, + buoyancy = 1.0, + linearDrag = 0.5, + angularDrag = 0.3 +}) + +-- ============================================================================= +-- Working with LOD Components +-- ============================================================================= + +-- Add LOD settings: +ecs.set_component(entity, "Lod", { + lodLevel = 0, + distance = 100.0 +}) + +ecs.set_component(entity, "LodSettings", { + enabled = true, + lodBias = 1.0 +}) + +-- ============================================================================= +-- Working with Static Geometry Components +-- ============================================================================= + +-- Batch entities into static geometry: +ecs.set_component(entity, "StaticGeometry", { + batchName = "forest_batch", + enabled = true +}) + +ecs.set_component(entity, "StaticGeometryMember", { + parentBatch = "forest_batch", + enabled = true +}) + +-- ============================================================================= +-- Working with Procedural Components +-- ============================================================================= + +-- Create procedural textures and materials: +ecs.set_component(entity, "ProceduralTexture", { + textureName = "grass", + width = 512, + height = 512 +}) + +ecs.set_component(entity, "ProceduralMaterial", { + materialName = "ground_mat", + baseColor = { 0.5, 0.5, 0.5, 1.0 } +}) + +-- ============================================================================= +-- Working with Primitive Components +-- ============================================================================= + +-- Create a primitive shape: +ecs.set_component(entity, "Primitive", { + primitiveType = "box", + size = { 1, 2, 1 } +}) + +-- ============================================================================= +-- Working with Triangle Buffer Components +-- ============================================================================= + +ecs.set_component(entity, "TriangleBuffer", { + enabled = true, + vertexCount = 100 +}) + +-- ============================================================================= +-- Working with Character Slots +-- ============================================================================= + +ecs.set_component(entity, "CharacterSlots", { + slotCount = 8 +}) + +-- ============================================================================= +-- Working with Roof Components +-- ============================================================================= + +ecs.set_component(entity, "Roof", { + roofType = "gable", + height = 2.5, + enabled = true +}) + +-- ============================================================================= +-- Working with ClearArea Components +-- ============================================================================= + +ecs.set_component(entity, "ClearArea", { + radius = 5.0, + enabled = true +}) + +-- ============================================================================= +-- Working with Action Database Components +-- ============================================================================= + +ecs.set_component(entity, "ActionDatabase", { + enabled = true +}) + +ecs.set_component(entity, "ActionDebug", { + enabled = true, + showDebugInfo = true +}) + +-- ============================================================================= +-- Working with Startup Menu Components +-- ============================================================================= + +ecs.set_component(entity, "StartupMenu", { + enabled = true, + showOnStart = true +}) + +-- ============================================================================= +-- Component Lifecycle Summary +-- ============================================================================= + +-- Components can be: +-- 1. Added: ecs.add_component(entity, "ComponentName") +-- 2. Removed: ecs.remove_component(entity, "ComponentName") +-- 3. Checked: ecs.has_component(entity, "ComponentName") +-- 4. Get: ecs.get_component(entity, "ComponentName") +-- 5. Set: ecs.set_component(entity, "ComponentName", { fields... }) +-- 6. Field: ecs.get_field(entity, "ComponentName", "fieldName") +-- 7. Field: ecs.set_field(entity, "ComponentName", "fieldName", value) + +print("Component API examples completed successfully!") diff --git a/src/features/editScene/lua-examples/entity_example.lua b/src/features/editScene/lua-examples/entity_example.lua new file mode 100644 index 0000000..0ab7634 --- /dev/null +++ b/src/features/editScene/lua-examples/entity_example.lua @@ -0,0 +1,177 @@ +-- ============================================================================= +-- Entity Lua API Examples +-- ============================================================================= +-- This file demonstrates how to create, manage, and query entities from Lua +-- using the ecs.* Lua API. +-- +-- Entities are the fundamental building blocks of the ECS world. Every object +-- in the game world is an entity with a unique numeric ID. +-- ============================================================================= + +-- ============================================================================= +-- Creating Entities +-- ============================================================================= + +-- Create a new entity. Returns a numeric ID. +local player = ecs.create_entity() +print("Created entity with ID: " .. player) + +-- Create multiple entities: +local npc1 = ecs.create_entity() +local npc2 = ecs.create_entity() +local item = ecs.create_entity() + +-- ============================================================================= +-- Entity Existence and Lifecycle +-- ============================================================================= + +-- Check if an entity exists: +if ecs.entity_exists(player) then + print("Player entity exists") +end + +-- Check a non-existent entity: +if not ecs.entity_exists(999999) then + print("Entity 999999 does not exist") +end + +-- Destroy an entity: +ecs.destroy_entity(npc2) +if not ecs.entity_exists(npc2) then + print("npc2 was destroyed") +end + +-- ============================================================================= +-- Entity Names +-- ============================================================================= + +-- Set an entity's name: +ecs.set_entity_name(player, "Hero") +ecs.set_entity_name(npc1, "Villager_01") +ecs.set_entity_name(item, "Health_Potion") + +-- Get an entity's name: +local name = ecs.get_entity_name(player) +print("Player name: " .. name) + +-- Look up an entity by name: +local found = ecs.get_entity_by_name("Villager_01") +if found then + print("Found entity " .. found .. " with name 'Villager_01'") +end + +-- Look up a non-existent name: +local not_found = ecs.get_entity_by_name("Nonexistent") +if not_found == nil then + print("'Nonexistent' not found (returns nil)") +end + +-- ============================================================================= +-- Entity Hierarchy (Parent/Children) +-- ============================================================================= + +-- Create a parent-child hierarchy: +local house = ecs.create_entity() +ecs.set_entity_name(house, "House") + +local door = ecs.create_entity() +ecs.set_entity_name(door, "Door") + +local window = ecs.create_entity() +ecs.set_entity_name(window, "Window") + +-- Check parent of a child (initially none): +local p = ecs.parent(door) +if p == nil then + print("Door has no parent initially") +end + +-- Check children of a parent (initially none): +local kids = ecs.children(house) +print("House has " .. #kids .. " children initially") + +-- ============================================================================= +-- Practical Example: Creating a Scene +-- ============================================================================= + +-- Create a complete scene with entities: +function create_scene() + -- Create terrain + local terrain = ecs.create_entity() + ecs.set_entity_name(terrain, "Terrain") + + -- Create lighting + local sun = ecs.create_entity() + ecs.set_entity_name(sun, "Sun") + + -- Create buildings + local buildings = {} + for i = 1, 3 do + local bldg = ecs.create_entity() + ecs.set_entity_name(bldg, "Building_" .. i) + table.insert(buildings, bldg) + end + + -- Create characters + local characters = {} + for i = 1, 5 do + local char = ecs.create_entity() + ecs.set_entity_name(char, "Character_" .. i) + table.insert(characters, char) + end + + print("Scene created with:") + print(" 1 terrain entity") + print(" 1 sun entity") + print(" " .. #buildings .. " buildings") + print(" " .. #characters .. " characters") + + return { + terrain = terrain, + sun = sun, + buildings = buildings, + characters = characters + } +end + +local scene = create_scene() + +-- ============================================================================= +-- Entity ID Properties +-- ============================================================================= + +-- Entity IDs are positive integers: +local e = ecs.create_entity() +assert(type(e) == "number", "Entity ID should be a number") +assert(e > 0, "Entity ID should be positive") + +-- Each entity gets a unique ID: +local ids = {} +for i = 1, 10 do + ids[i] = ecs.create_entity() +end +for i = 1, 10 do + for j = i + 1, 10 do + assert(ids[i] ~= ids[j], "IDs should be unique") + end +end +print("All 10 entities have unique IDs") + +-- Destroyed entity IDs are not reused immediately: +local old_id = ecs.create_entity() +ecs.destroy_entity(old_id) +local new_id = ecs.create_entity() +assert(new_id ~= old_id, "New entity should have different ID") +print("Destroyed entity ID " .. old_id .. " is not reused") + +-- ============================================================================= +-- Error Handling +-- ============================================================================= + +-- These operations should not crash: +ecs.destroy_entity(999999) -- destroying non-existent entity is safe +ecs.set_entity_name(999999, "ghost") -- setting name on non-existent entity +local n = ecs.get_entity_name(999999) -- returns empty string +print("Name of non-existent entity: '" .. n .. "'") + +print("Entity API examples completed successfully!") diff --git a/src/features/editScene/lua-examples/event_example.lua b/src/features/editScene/lua-examples/event_example.lua new file mode 100644 index 0000000..697fe7e --- /dev/null +++ b/src/features/editScene/lua-examples/event_example.lua @@ -0,0 +1,285 @@ +-- ============================================================================= +-- Event Lua API Examples +-- ============================================================================= +-- This file demonstrates how to subscribe to, send, and manage events from +-- Lua using the ecs.* Lua API. +-- +-- Events are a publish/subscribe mechanism. Any part of the game can send +-- an event, and any subscriber can react to it. This decouples systems +-- from each other. +-- ============================================================================= + +-- ============================================================================= +-- Subscribing to Events +-- ============================================================================= + +-- Subscribe to an event. Returns a subscription ID (number) that can be +-- used to unsubscribe later. +local sub_id = ecs.subscribe_event("hello", function(event, params) + print("Received event: " .. event) +end) + +print("Subscribed to 'hello' with ID: " .. sub_id) + +-- ============================================================================= +-- Sending Events +-- ============================================================================= + +-- Send a simple event with no parameters: +ecs.send_event("hello") + +-- Send an event with parameters: +ecs.send_event("hello", { + values = { count = 42 }, + stringValues = { message = "Hello World!" } +}) + +-- ============================================================================= +-- Event Callback Parameters +-- ============================================================================= + +-- The callback receives two arguments: +-- 1. event - the event name (string) +-- 2. params - a table with the event data (or nil if no data was sent) + +ecs.subscribe_event("player_damaged", function(event, params) + print("Event: " .. event) + if params then + if params.values then + print(" Damage: " .. (params.values.damage or 0)) + print(" Health remaining: " .. (params.values.health or 0)) + end + if params.stringValues then + print(" Source: " .. (params.stringValues.source or "unknown")) + end + if params.vec3Values then + local pos = params.vec3Values.position + if pos then + print(" Position: " .. pos[1] .. ", " .. pos[2] .. ", " .. pos[3]) + end + end + end +end) + +-- Send a damage event: +ecs.send_event("player_damaged", { + values = { damage = 25, health = 75 }, + stringValues = { source = "goblin_archer" }, + vec3Values = { position = { 10, 0, 20 } } +}) + +-- ============================================================================= +-- Unsubscribing from Events +-- ============================================================================= + +-- When you no longer need to listen to an event, unsubscribe: +local temp_sub = ecs.subscribe_event("temporary", function() + print("This should not be printed after unsubscribe") +end) + +ecs.send_event("temporary") -- callback fires + +ecs.unsubscribe_event(temp_sub) + +ecs.send_event("temporary") -- callback does NOT fire (unsubscribed) + +-- ============================================================================= +-- Multiple Subscribers +-- ============================================================================= + +-- Multiple subscribers can listen to the same event. All of them will be +-- called when the event is sent. + +local count_a = 0 +local count_b = 0 + +local sub_a = ecs.subscribe_event("multi", function() + count_a = count_a + 1 + print("Subscriber A called (total: " .. count_a .. ")") +end) + +local sub_b = ecs.subscribe_event("multi", function() + count_b = count_b + 1 + print("Subscriber B called (total: " .. count_b .. ")") +end) + +ecs.send_event("multi") -- both A and B fire +ecs.send_event("multi") -- both fire again + +-- ============================================================================= +-- Event Parameter Types +-- ============================================================================= + +-- Events can carry various types of data in their params table: + +ecs.subscribe_event("data_event", function(event, params) + print("Received data_event with:") + if params then + -- Integer values: + if params.values then + for k, v in pairs(params.values) do + print(" int " .. k .. " = " .. v) + end + end + -- Float values: + if params.floatValues then + for k, v in pairs(params.floatValues) do + print(" float " .. k .. " = " .. v) + end + end + -- String values: + if params.stringValues then + for k, v in pairs(params.stringValues) do + print(" string " .. k .. " = '" .. v .. "'") + end + end + -- Vec3 values: + if params.vec3Values then + for k, v in pairs(params.vec3Values) do + print(" vec3 " .. k .. " = (" .. v[1] .. ", " .. v[2] .. ", " .. v[3] .. ")") + end + end + -- Bit flags: + if params.bits ~= nil then + print(" bits = " .. params.bits) + end + if params.mask ~= nil then + print(" mask = " .. params.mask) + end + end +end) + +-- Send an event with all parameter types: +ecs.send_event("data_event", { + values = { score = 100, level = 5, kills = 42 }, + floatValues = { speed = 1.5, health = 75.5 }, + stringValues = { name = "Hero", state = "exploring" }, + vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } }, + bits = 5, + mask = 7 +}) + +-- ============================================================================= +-- Practical Example: Game Event System +-- ============================================================================= + +-- Create a simple game event system: + +local event_handlers = {} + +function register_game_handlers() + -- Quest events: + event_handlers.quest_complete = ecs.subscribe_event("quest_complete", function(event, params) + local quest_name = params and params.stringValues and params.stringValues.quest_name or "unknown" + local reward_xp = params and params.values and params.values.reward_xp or 0 + print("Quest completed: " .. quest_name .. " (+" .. reward_xp .. " XP)") + end) + + -- Combat events: + event_handlers.enemy_killed = ecs.subscribe_event("enemy_killed", function(event, params) + local enemy = params and params.stringValues and params.stringValues.enemy_type or "unknown" + local xp = params and params.values and params.values.xp_reward or 0 + print("Killed " .. enemy .. " (+" .. xp .. " XP)") + end) + + -- Item events: + event_handlers.item_picked_up = ecs.subscribe_event("item_picked_up", function(event, params) + local item = params and params.stringValues and params.stringValues.item_name or "unknown" + local count = params and params.values and params.values.count or 1 + print("Picked up " .. count .. "x " .. item) + end) + + -- Dialogue events: + event_handlers.dialogue_started = ecs.subscribe_event("dialogue_started", function(event, params) + local npc = params and params.stringValues and params.stringValues.npc_name or "unknown" + print("Started dialogue with " .. npc) + end) + + -- Environment events: + event_handlers.time_changed = ecs.subscribe_event("time_changed", function(event, params) + local hour = params and params.values and params.values.hour or 0 + local minute = params and params.values and params.values.minute or 0 + print("Time changed to " .. hour .. ":" .. string.format("%02d", minute)) + end) + + -- Player events: + event_handlers.player_died = ecs.subscribe_event("player_died", function(event, params) + local killer = params and params.stringValues and params.stringValues.killed_by or "unknown" + print("Player was killed by " .. killer .. "!") + end) + + print("All game event handlers registered") +end + +register_game_handlers() + +-- Simulate some game events: +ecs.send_event("quest_complete", { + stringValues = { quest_name = "The Lost Artifact" }, + values = { reward_xp = 500 } +}) + +ecs.send_event("enemy_killed", { + stringValues = { enemy_type = "Goblin Warrior" }, + values = { xp_reward = 50 } +}) + +ecs.send_event("item_picked_up", { + stringValues = { item_name = "Health Potion" }, + values = { count = 2 } +}) + +ecs.send_event("dialogue_started", { + stringValues = { npc_name = "Elder Marcus" } +}) + +ecs.send_event("time_changed", { + values = { hour = 18, minute = 30 } +}) + +-- ============================================================================= +-- Cleanup: Unsubscribe All +-- ============================================================================= + +-- When cleaning up, unsubscribe all registered handlers: +function cleanup_event_handlers() + for name, id in pairs(event_handlers) do + ecs.unsubscribe_event(id) + print("Unsubscribed from: " .. name) + end +end + +-- Uncomment to clean up: +-- cleanup_event_handlers() + +-- ============================================================================= +-- Event API Reference +-- ============================================================================= +-- +-- ecs.subscribe_event(event_name, callback) +-- Subscribe to an event. Returns a subscription ID (number). +-- Parameters: +-- event_name - string, the name of the event to listen for +-- callback - function(event, params), called when the event fires +-- Returns: subscription ID (number) +-- +-- ecs.unsubscribe_event(subscription_id) +-- Unsubscribe from an event. +-- Parameters: +-- subscription_id - number, the ID returned by subscribe_event +-- Safe to call with an invalid ID (no crash). +-- +-- ecs.send_event(event_name, params) +-- Send an event to all subscribers. +-- Parameters: +-- event_name - string, the name of the event to send +-- params - optional table with event data: +-- .values - table of integer key-value pairs +-- .floatValues - table of float key-value pairs +-- .stringValues - table of string key-value pairs +-- .vec3Values - table of vec3 key-value pairs (each vec3 is {x, y, z}) +-- .bits - integer bit flags +-- .mask - integer bit mask +-- ============================================================================= + +print("Event API examples completed successfully!") diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index f2b8dd1..3cc0b54 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -54,6 +54,8 @@ #include "components/EventHandler.hpp" #include "components/Item.hpp" #include "components/Inventory.hpp" +#include "components/GeneratedPhysicsTag.hpp" +#include "components/Relationship.hpp" namespace editScene { @@ -1683,6 +1685,67 @@ static void registerAllComponents() if (lua_getfield(L, idx, "lastResult"), lua_isstring(L, -1)) c.lastResult = lua_tostring(L, -1); lua_pop(L, 1);); + + // --- GeneratedPhysicsTag (tag, no fields) --- + s_components["GeneratedPhysicsTag"] = { + "GeneratedPhysicsTag", + [](lua_State *L, flecs::entity e) { + lua_pushboolean(L, + e.has() ? 1 : 0); + }, + [](lua_State *L, flecs::entity e, int idx) { + (void)idx; + if (lua_toboolean(L, -1)) + e.add(); + else + e.remove(); + } + }; + + // --- ParentComponent --- + // Note: ParentComponent stores a flecs::entity which cannot be + // directly serialized to Lua. We expose it as a tag (has/doesn't have) + // and provide the parent entity via ecs.parent() in the entity API. + s_components["ParentComponent"] = { + "ParentComponent", + [](lua_State *L, flecs::entity e) { + lua_pushboolean(L, e.has() ? 1 : 0); + }, + [](lua_State *L, flecs::entity e, int idx) { + (void)idx; + if (lua_toboolean(L, -1)) + e.add(); + else + e.remove(); + } + }; + + // --- ModifiedComponent --- + s_components["ModifiedComponent"] = { + "ModifiedComponent", + [](lua_State *L, flecs::entity e) { + if (!e.has()) { + lua_pushnil(L); + return; + } + const auto &c = e.get(); + lua_newtable(L); + lua_pushboolean(L, c.modified ? 1 : 0); + lua_setfield(L, -2, "modified"); + }, + [](lua_State *L, flecs::entity e, int idx) { + ModifiedComponent c; + if (e.has()) + c = e.get(); + if (lua_istable(L, idx)) { + if (lua_getfield(L, idx, "modified"), + lua_isboolean(L, -1)) + c.modified = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + } + e.set(c); + } + }; } // --------------------------------------------------------------------------- diff --git a/src/features/editScene/lua/LuaComponentApi.hpp b/src/features/editScene/lua/LuaComponentApi.hpp index 8357ec9..46f9f06 100644 --- a/src/features/editScene/lua/LuaComponentApi.hpp +++ b/src/features/editScene/lua/LuaComponentApi.hpp @@ -32,9 +32,11 @@ * "Lod", "LodSettings", "StaticGeometry", "StaticGeometryMember", * "ProceduralTexture", "ProceduralMaterial", "Primitive", * "TriangleBuffer", "Sun", "Skybox", "WaterPlane", "WaterPhysics", - * "BuoyancyInfo", "StartupMenu", "Dialogue", "PlayerController", - * "CellGrid", "Room", "ClearArea", "Roof", "Lot", "District", - * "Town", "FurnitureTemplate", "PrefabInstance", "EditorMarker" + * "BuoyancyInfo", "InWater", "StartupMenu", "Dialogue", + * "PlayerController", "CellGrid", "Room", "ClearArea", "Roof", + * "Lot", "District", "Town", "FurnitureTemplate", "PrefabInstance", + * "EditorMarker", "GeneratedPhysicsTag", "ParentComponent", + * "ModifiedComponent" */ namespace editScene diff --git a/src/features/editScene/tests/component_lua_test.cpp b/src/features/editScene/tests/component_lua_test.cpp new file mode 100644 index 0000000..fab137e --- /dev/null +++ b/src/features/editScene/tests/component_lua_test.cpp @@ -0,0 +1,1909 @@ +/** + * @file component_lua_test.cpp + * @brief Standalone test for the Lua Component API. + * + * Tests component manipulation functions exposed via the ecs.* Lua API: + * add_component, remove_component, has_component, get_component, + * set_component, get_field, set_field. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * component_lua_test.cpp \ + * ../lua/LuaComponentApi.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o component_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Flecs stub for standalone testing +namespace flecs +{ + +struct entity { + uint64_t m_id = 0; + bool m_valid = false; + + entity() + : m_id(0) + , m_valid(false) + { + } + explicit entity(uint64_t id) + : m_id(id) + , m_valid(true) + { + } + + uint64_t id() const + { + return m_id; + } + bool is_valid() const + { + return m_valid; + } + bool is_alive() const + { + return m_valid; + } + const char *name() const + { + return ""; + } + void set_name(const char *) + { + } + void destruct() + { + m_valid = false; + } + + entity parent() const + { + return entity(); + } + + template void children(Func) const + { + } + + template void add() + { + } + + template bool has() const + { + return false; + } + + template const T *get() const + { + return nullptr; + } + + template void set(const T &) + { + } + + bool operator==(const entity &other) const + { + return m_id == other.m_id; + } +}; + +using entity_t = uint64_t; + +struct world { + entity make_entity() + { + return entity(nextId++); + } + entity lookup(const char *) + { + return entity(); + } + + static world &get() + { + static world w; + return w; + } + +private: + uint64_t nextId = 1000; +}; + +} // namespace flecs + +// Forward declare the registration function +namespace editScene +{ +void registerLuaComponentApi(lua_State *L); +void registerLuaEntityApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: has_component returns false for new entity +// --------------------------------------------------------------------------- + +static int testHasComponent(lua_State *L) +{ + TEST("has_component returns false for new entity"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'EntityName');" + "assert(has == false, 'new entity should not have EntityName')"); + if (!ok) + FAIL("has_component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: add_component and has_component +// --------------------------------------------------------------------------- + +static int testAddComponent(lua_State *L) +{ + TEST("add_component and has_component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.add_component(id, 'EntityName');" + "local has = ecs.has_component(id, 'EntityName');" + "assert(has == true, 'entity should have EntityName after add')"); + if (!ok) + FAIL("add_component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: set_component and get_component +// --------------------------------------------------------------------------- + +static int testSetGetComponent(lua_State *L) +{ + TEST("set_component and get_component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'EntityName', { name = 'hero' });" + "local c = ecs.get_component(id, 'EntityName');" + "assert(c ~= nil, 'component should exist');" + "assert(c.name == 'hero', 'expected hero, got ' .. tostring(c.name))"); + if (!ok) + FAIL("set/get component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: remove_component +// --------------------------------------------------------------------------- + +static int testRemoveComponent(lua_State *L) +{ + TEST("remove_component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.add_component(id, 'EntityName');" + "assert(ecs.has_component(id, 'EntityName') == true, 'should have before remove');" + "ecs.remove_component(id, 'EntityName');" + "assert(ecs.has_component(id, 'EntityName') == false, 'should not have after remove')"); + if (!ok) + FAIL("remove_component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: get_field and set_field +// --------------------------------------------------------------------------- + +static int testGetSetField(lua_State *L) +{ + TEST("get_field and set_field"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_field(id, 'EntityName', 'name', 'field_hero');" + "local name = ecs.get_field(id, 'EntityName', 'name');" + "assert(name == 'field_hero', 'expected field_hero, got ' .. tostring(name))"); + if (!ok) + FAIL("get/set field assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Transform component with vector fields +// --------------------------------------------------------------------------- + +static int testTransformComponent(lua_State *L) +{ + TEST("Transform component with position/rotation/scale"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'Transform', {" + " position = { 10, 20, 30 }," + " rotation = { 1, 0, 0, 0 }," + " scale = { 2, 2, 2 }" + "});" + "local t = ecs.get_component(id, 'Transform');" + "assert(t ~= nil, 'Transform should exist');" + "assert(t.position[1] == 10, 'expected pos.x=10, got ' .. t.position[1]);" + "assert(t.position[2] == 20, 'expected pos.y=20');" + "assert(t.position[3] == 30, 'expected pos.z=30');" + "assert(t.scale[1] == 2, 'expected scale.x=2');" + "assert(t.rotation[1] == 1, 'expected rot.w=1')"); + if (!ok) + FAIL("Transform component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Renderable component +// --------------------------------------------------------------------------- + +static int testRenderableComponent(lua_State *L) +{ + TEST("Renderable component with mesh name"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Renderable', {" + " meshName = 'cube.mesh'," + " visible = true" + "});" + "local r = ecs.get_component(id, 'Renderable');" + "assert(r ~= nil, 'Renderable should exist');" + "assert(r.meshName == 'cube.mesh', 'expected cube.mesh');" + "assert(r.visible == true, 'expected visible=true')"); + if (!ok) + FAIL("Renderable component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Light component +// --------------------------------------------------------------------------- + +static int testLightComponent(lua_State *L) +{ + TEST("Light component with color and intensity"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'Light', {" + " lightType = 0," + " diffuseColor = { 1, 1, 1, 1 }," + " intensity = 1.5," + " range = 100," + " castShadows = true" + "});" + "local l = ecs.get_component(id, 'Light');" + "assert(l ~= nil, 'Light should exist');" + "assert(l.lightType == 0, 'expected point light');" + "assert(l.intensity == 1.5, 'expected intensity 1.5');" + "assert(l.range == 100, 'expected range 100');" + "assert(l.castShadows == true, 'expected castShadows=true')"); + if (!ok) + FAIL("Light component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: RigidBody component +// --------------------------------------------------------------------------- + +static int testRigidBodyComponent(lua_State *L) +{ + TEST("RigidBody component with physics properties"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'RigidBody', {" + " bodyType = 1," + " mass = 10.0," + " friction = 0.5," + " restitution = 0.2," + " enabled = true" + "});" + "local rb = ecs.get_component(id, 'RigidBody');" + "assert(rb ~= nil, 'RigidBody should exist');" + "assert(rb.bodyType == 1, 'expected dynamic body');" + "assert(rb.mass == 10.0, 'expected mass 10');" + "assert(rb.friction == 0.5, 'expected friction 0.5');" + "assert(rb.restitution == 0.2, 'expected restitution 0.2')"); + if (!ok) + FAIL("RigidBody component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: EditorMarker tag component +// --------------------------------------------------------------------------- + +static int testEditorMarkerTag(lua_State *L) +{ + TEST("EditorMarker tag component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'EditorMarker');" + "assert(has == true, 'new entity should have EditorMarker');" + "ecs.remove_component(id, 'EditorMarker');" + "has = ecs.has_component(id, 'EditorMarker');" + "assert(has == false, 'should not have after remove');" + "ecs.add_component(id, 'EditorMarker');" + "has = ecs.has_component(id, 'EditorMarker');" + "assert(has == true, 'should have after re-add')"); + if (!ok) + FAIL("EditorMarker tag assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 11: InWater tag component +// --------------------------------------------------------------------------- + +static int testInWaterTag(lua_State *L) +{ + TEST("InWater tag component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'InWater');" + "assert(has == false, 'new entity should not have InWater');" + "ecs.add_component(id, 'InWater');" + "has = ecs.has_component(id, 'InWater');" + "assert(has == true, 'should have InWater after add');" + "ecs.remove_component(id, 'InWater');" + "has = ecs.has_component(id, 'InWater');" + "assert(has == false, 'should not have after remove')"); + if (!ok) + FAIL("InWater tag assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: GeneratedPhysicsTag tag component +// --------------------------------------------------------------------------- + +static int testGeneratedPhysicsTag(lua_State *L) +{ + TEST("GeneratedPhysicsTag tag component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'GeneratedPhysicsTag');" + "assert(has == false, 'new entity should not have GeneratedPhysicsTag');" + "ecs.add_component(id, 'GeneratedPhysicsTag');" + "has = ecs.has_component(id, 'GeneratedPhysicsTag');" + "assert(has == true, 'should have after add');" + "ecs.remove_component(id, 'GeneratedPhysicsTag');" + "has = ecs.has_component(id, 'GeneratedPhysicsTag');" + "assert(has == false, 'should not have after remove')"); + if (!ok) + FAIL("GeneratedPhysicsTag tag assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 13: ParentComponent tag component +// --------------------------------------------------------------------------- + +static int testParentComponentTag(lua_State *L) +{ + TEST("ParentComponent tag component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'ParentComponent');" + "assert(has == false, 'new entity should not have ParentComponent');" + "ecs.add_component(id, 'ParentComponent');" + "has = ecs.has_component(id, 'ParentComponent');" + "assert(has == true, 'should have after add');" + "ecs.remove_component(id, 'ParentComponent');" + "has = ecs.has_component(id, 'ParentComponent');" + "assert(has == false, 'should not have after remove')"); + if (!ok) + FAIL("ParentComponent tag assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 14: ModifiedComponent with field +// --------------------------------------------------------------------------- + +static int testModifiedComponent(lua_State *L) +{ + TEST("ModifiedComponent with modified field"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "local c = ecs.get_component(id, 'ModifiedComponent');" + "assert(c == nil, 'new entity should not have ModifiedComponent');" + "ecs.set_component(id, 'ModifiedComponent', { modified = true });" + "c = ecs.get_component(id, 'ModifiedComponent');" + "assert(c ~= nil, 'should exist after set');" + "assert(c.modified == true, 'expected modified=true');" + "ecs.set_field(id, 'ModifiedComponent', 'modified', false);" + "local mod = ecs.get_field(id, 'ModifiedComponent', 'modified');" + "assert(mod == false, 'expected modified=false after set_field')"); + if (!ok) + FAIL("ModifiedComponent assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 15: NavMeshGeometrySource tag component +// --------------------------------------------------------------------------- + +static int testNavMeshGeometrySourceTag(lua_State *L) +{ + TEST("NavMeshGeometrySource tag component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "local has = ecs.has_component(id, 'NavMeshGeometrySource');" + "assert(has == false, 'new entity should not have NavMeshGeometrySource');" + "ecs.add_component(id, 'NavMeshGeometrySource');" + "has = ecs.has_component(id, 'NavMeshGeometrySource');" + "assert(has == true, 'should have after add');" + "ecs.remove_component(id, 'NavMeshGeometrySource');" + "has = ecs.has_component(id, 'NavMeshGeometrySource');" + "assert(has == false, 'should not have after remove')"); + if (!ok) + FAIL("NavMeshGeometrySource tag assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 16: Item component with all fields +// --------------------------------------------------------------------------- + +static int testItemComponent(lua_State *L) +{ + TEST("Item component with all fields"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'Item', {" + " itemName = 'Health Potion'," + " itemType = 'consumable'," + " itemId = 'potion_health'," + " stackSize = 1," + " maxStackSize = 10," + " weight = 0.5," + " value = 50," + " useActionName = 'drink_potion'" + "});" + "local item = ecs.get_component(id, 'Item');" + "assert(item ~= nil, 'Item should exist');" + "assert(item.itemName == 'Health Potion', 'wrong name');" + "assert(item.itemType == 'consumable', 'wrong type');" + "assert(item.itemId == 'potion_health', 'wrong id');" + "assert(item.stackSize == 1, 'wrong stackSize');" + "assert(item.maxStackSize == 10, 'wrong maxStackSize');" + "assert(item.weight == 0.5, 'wrong weight');" + "assert(item.value == 50, 'wrong value');" + "assert(item.useActionName == 'drink_potion', 'wrong useActionName')"); + if (!ok) + FAIL("Item component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 17: Inventory component +// --------------------------------------------------------------------------- + +static int testInventoryComponent(lua_State *L) +{ + TEST("Inventory component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Inventory', {" + " maxSlots = 20," + " maxWeight = 50.0," + " isContainer = true" + "});" + "local inv = ecs.get_component(id, 'Inventory');" + "assert(inv ~= nil, 'Inventory should exist');" + "assert(inv.maxSlots == 20, 'wrong maxSlots');" + "assert(inv.maxWeight == 50.0, 'wrong maxWeight');" + "assert(inv.isContainer == true, 'wrong isContainer')"); + if (!ok) + FAIL("Inventory component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 18: Character component +// --------------------------------------------------------------------------- + +static int testCharacterComponent(lua_State *L) +{ + TEST("Character component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Character', {" + " radius = 0.4," + " height = 1.8," + " offset = { 0, 0.9, 0 }," + " enabled = true," + " useGravity = true" + "});" + "local c = ecs.get_component(id, 'Character');" + "assert(c ~= nil, 'Character should exist');" + "assert(c.radius == 0.4, 'wrong radius');" + "assert(c.height == 1.8, 'wrong height');" + "assert(c.enabled == true, 'wrong enabled');" + "assert(c.useGravity == true, 'wrong useGravity')"); + if (!ok) + FAIL("Character component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 19: Camera component +// --------------------------------------------------------------------------- + +static int testCameraComponent(lua_State *L) +{ + TEST("Camera component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Camera', {" + " fovY = 60," + " nearClip = 0.1," + " farClip = 1000," + " orthographic = false" + "});" + "local cam = ecs.get_component(id, 'Camera');" + "assert(cam ~= nil, 'Camera should exist');" + "assert(cam.fovY == 60, 'wrong fovY');" + "assert(cam.nearClip == 0.1, 'wrong nearClip');" + "assert(cam.farClip == 1000, 'wrong farClip');" + "assert(cam.orthographic == false, 'wrong orthographic')"); + if (!ok) + FAIL("Camera component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 20: Sun component +// --------------------------------------------------------------------------- + +static int testSunComponent(lua_State *L) +{ + TEST("Sun component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'Sun', {" + " enabled = true," + " timeOfDay = 12.0," + " timeSpeed = 1.0," + " intensity = 1.0," + " castShadows = true" + "});" + "local s = ecs.get_component(id, 'Sun');" + "assert(s ~= nil, 'Sun should exist');" + "assert(s.enabled == true, 'wrong enabled');" + "assert(s.timeOfDay == 12.0, 'wrong timeOfDay');" + "assert(s.timeSpeed == 1.0, 'wrong timeSpeed');" + "assert(s.intensity == 1.0, 'wrong intensity');" + "assert(s.castShadows == true, 'wrong castShadows')"); + if (!ok) + FAIL("Sun component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 21: Skybox component +// --------------------------------------------------------------------------- + +static int testSkyboxComponent(lua_State *L) +{ + TEST("Skybox component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Skybox', {" + " enabled = true," + " size = 500," + " starsEnabled = true" + "});" + "local s = ecs.get_component(id, 'Skybox');" + "assert(s ~= nil, 'Skybox should exist');" + "assert(s.enabled == true, 'wrong enabled');" + "assert(s.size == 500, 'wrong size');" + "assert(s.starsEnabled == true, 'wrong starsEnabled')"); + if (!ok) + FAIL("Skybox component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 22: WaterPlane component +// --------------------------------------------------------------------------- + +static int testWaterPlaneComponent(lua_State *L) +{ + TEST("WaterPlane component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'WaterPlane', {" + " enabled = true," + " waterSurfaceY = 0.0," + " planeSize = 1000," + " reflectivity = 0.5," + " waveSpeed = 1.0" + "});" + "local w = ecs.get_component(id, 'WaterPlane');" + "assert(w ~= nil, 'WaterPlane should exist');" + "assert(w.enabled == true, 'wrong enabled');" + "assert(w.waterSurfaceY == 0.0, 'wrong waterSurfaceY');" + "assert(w.planeSize == 1000, 'wrong planeSize');" + "assert(w.reflectivity == 0.5, 'wrong reflectivity')"); + if (!ok) + FAIL("WaterPlane component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 23: BuoyancyInfo component +// --------------------------------------------------------------------------- + +static int testBuoyancyInfoComponent(lua_State *L) +{ + TEST("BuoyancyInfo component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'BuoyancyInfo', {" + " enabled = true," + " buoyancy = 1.0," + " linearDrag = 0.5," + " angularDrag = 0.3" + "});" + "local b = ecs.get_component(id, 'BuoyancyInfo');" + "assert(b ~= nil, 'BuoyancyInfo should exist');" + "assert(b.enabled == true, 'wrong enabled');" + "assert(b.buoyancy == 1.0, 'wrong buoyancy');" + "assert(b.linearDrag == 0.5, 'wrong linearDrag');" + "assert(b.angularDrag == 0.3, 'wrong angularDrag')"); + if (!ok) + FAIL("BuoyancyInfo component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 24: PlayerController component +// --------------------------------------------------------------------------- + +static int testPlayerControllerComponent(lua_State *L) +{ + TEST("PlayerController component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'PlayerController', {" + " cameraMode = 1," + " tpsDistance = 5.0," + " tpsHeight = 2.0," + " mouseSensitivity = 0.5" + "});" + "local p = ecs.get_component(id, 'PlayerController');" + "assert(p ~= nil, 'PlayerController should exist');" + "assert(p.cameraMode == 1, 'wrong cameraMode');" + "assert(p.tpsDistance == 5.0, 'wrong tpsDistance');" + "assert(p.tpsHeight == 2.0, 'wrong tpsHeight');" + "assert(p.mouseSensitivity == 0.5, 'wrong mouseSensitivity')"); + if (!ok) + FAIL("PlayerController component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 25: Dialogue component +// --------------------------------------------------------------------------- + +static int testDialogueComponent(lua_State *L) +{ + TEST("Dialogue component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Dialogue', {" + " text = 'Hello world'," + " speaker = 'NPC'," + " enabled = true" + "});" + "local d = ecs.get_component(id, 'Dialogue');" + "assert(d ~= nil, 'Dialogue should exist');" + "assert(d.text == 'Hello world', 'wrong text');" + "assert(d.speaker == 'NPC', 'wrong speaker');" + "assert(d.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("Dialogue component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 26: NavMesh component +// --------------------------------------------------------------------------- + +static int testNavMeshComponent(lua_State *L) +{ + TEST("NavMesh component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'NavMesh', {" + " cellSize = 0.3," + " cellHeight = 0.2," + " agentHeight = 2.0," + " agentRadius = 0.5," + " agentMaxClimb = 0.5," + " agentMaxSlope = 45.0," + " enabled = true" + "});" + "local n = ecs.get_component(id, 'NavMesh');" + "assert(n ~= nil, 'NavMesh should exist');" + "assert(n.cellSize == 0.3, 'wrong cellSize');" + "assert(n.cellHeight == 0.2, 'wrong cellHeight');" + "assert(n.agentHeight == 2.0, 'wrong agentHeight');" + "assert(n.agentRadius == 0.5, 'wrong agentRadius');" + "assert(n.agentMaxClimb == 0.5, 'wrong agentMaxClimb');" + "assert(n.agentMaxSlope == 45.0, 'wrong agentMaxSlope');" + "assert(n.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("NavMesh component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 27: SmartObject component +// --------------------------------------------------------------------------- + +static int testSmartObjectComponent(lua_State *L) +{ + TEST("SmartObject component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'SmartObject', {" + " radius = 1.0," + " height = 2.0," + " actionNames = { 'sit', 'talk' }" + "});" + "local s = ecs.get_component(id, 'SmartObject');" + "assert(s ~= nil, 'SmartObject should exist');" + "assert(s.radius == 1.0, 'wrong radius');" + "assert(s.height == 2.0, 'wrong height');" + "assert(#s.actionNames == 2, 'expected 2 action names');" + "assert(s.actionNames[1] == 'sit', 'wrong action 1');" + "assert(s.actionNames[2] == 'talk', 'wrong action 2')"); + if (!ok) + FAIL("SmartObject component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 28: Actuator component +// --------------------------------------------------------------------------- + +static int testActuatorComponent(lua_State *L) +{ + TEST("Actuator component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Actuator', {" + " radius = 2.0," + " height = 3.0," + " actionNames = { 'interact' }" + "});" + "local a = ecs.get_component(id, 'Actuator');" + "assert(a ~= nil, 'Actuator should exist');" + "assert(a.radius == 2.0, 'wrong radius');" + "assert(a.height == 3.0, 'wrong height');" + "assert(#a.actionNames == 1, 'expected 1 action name');" + "assert(a.actionNames[1] == 'interact', 'wrong action')"); + if (!ok) + FAIL("Actuator component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 29: EventHandler component +// --------------------------------------------------------------------------- + +static int testEventHandlerComponent(lua_State *L) +{ + TEST("EventHandler component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'EventHandler', {" + " eventName = 'collision'," + " actionName = 'handle_collision'," + " enabled = true" + "});" + "local e = ecs.get_component(id, 'EventHandler');" + "assert(e ~= nil, 'EventHandler should exist');" + "assert(e.eventName == 'collision', 'wrong eventName');" + "assert(e.actionName == 'handle_collision', 'wrong actionName');" + "assert(e.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("EventHandler component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 30: GoapBlackboard component +// --------------------------------------------------------------------------- + +static int testGoapBlackboardComponent(lua_State *L) +{ + TEST("GoapBlackboard component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'GoapBlackboard', {" + " values = { health = 100, stamina = 50 }," + " floatValues = { hunger = 75.5 }," + " stringValues = { state = 'idle' }" + "});" + "local g = ecs.get_component(id, 'GoapBlackboard');" + "assert(g ~= nil, 'GoapBlackboard should exist');" + "assert(g.values.health == 100, 'wrong health');" + "assert(g.values.stamina == 50, 'wrong stamina');" + "assert(g.floatValues.hunger == 75.5, 'wrong hunger');" + "assert(g.stringValues.state == 'idle', 'wrong state')"); + if (!ok) + FAIL("GoapBlackboard component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 31: PrefabInstance component +// --------------------------------------------------------------------------- + +static int testPrefabInstanceComponent(lua_State *L) +{ + TEST("PrefabInstance component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'PrefabInstance', {" + " prefabPath = 'prefabs/house.json'," + " instantiated = false" + "});" + "local p = ecs.get_component(id, 'PrefabInstance');" + "assert(p ~= nil, 'PrefabInstance should exist');" + "assert(p.prefabPath == 'prefabs/house.json', 'wrong path');" + "assert(p.instantiated == false, 'wrong instantiated')"); + if (!ok) + FAIL("PrefabInstance component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 32: CellGrid component +// --------------------------------------------------------------------------- + +static int testCellGridComponent(lua_State *L) +{ + TEST("CellGrid component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'CellGrid', {" + " width = 10," + " height = 5," + " depth = 10," + " cellSize = 1.0," + " cellHeight = 0.5" + "});" + "local c = ecs.get_component(id, 'CellGrid');" + "assert(c ~= nil, 'CellGrid should exist');" + "assert(c.width == 10, 'wrong width');" + "assert(c.height == 5, 'wrong height');" + "assert(c.depth == 10, 'wrong depth');" + "assert(c.cellSize == 1.0, 'wrong cellSize');" + "assert(c.cellHeight == 0.5, 'wrong cellHeight')"); + if (!ok) + FAIL("CellGrid component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 33: Room component +// --------------------------------------------------------------------------- + +static int testRoomComponent(lua_State *L) +{ + TEST("Room component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Room', {" + " roomName = 'Kitchen'," + " roomType = 'kitchen'," + " floor = 0" + "});" + "local r = ecs.get_component(id, 'Room');" + "assert(r ~= nil, 'Room should exist');" + "assert(r.roomName == 'Kitchen', 'wrong roomName');" + "assert(r.roomType == 'kitchen', 'wrong roomType');" + "assert(r.floor == 0, 'wrong floor')"); + if (!ok) + FAIL("Room component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 34: Lot component +// --------------------------------------------------------------------------- + +static int testLotComponent(lua_State *L) +{ + TEST("Lot component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Lot', {" + " lotName = 'Residential 1'," + " lotType = 'residential'," + " width = 20," + " depth = 30" + "});" + "local l = ecs.get_component(id, 'Lot');" + "assert(l ~= nil, 'Lot should exist');" + "assert(l.lotName == 'Residential 1', 'wrong lotName');" + "assert(l.lotType == 'residential', 'wrong lotType');" + "assert(l.width == 20, 'wrong width');" + "assert(l.depth == 30, 'wrong depth')"); + if (!ok) + FAIL("Lot component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 35: District component +// --------------------------------------------------------------------------- + +static int testDistrictComponent(lua_State *L) +{ + TEST("District component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'District', {" + " districtName = 'Market District'," + " districtType = 'commercial'" + "});" + "local d = ecs.get_component(id, 'District');" + "assert(d ~= nil, 'District should exist');" + "assert(d.districtName == 'Market District', 'wrong districtName');" + "assert(d.districtType == 'commercial', 'wrong districtType')"); + if (!ok) + FAIL("District component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 36: Town component +// --------------------------------------------------------------------------- + +static int testTownComponent(lua_State *L) +{ + TEST("Town component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'Town', {" + " townName = 'Rivendell'," + " population = 500" + "});" + "local t = ecs.get_component(id, 'Town');" + "assert(t ~= nil, 'Town should exist');" + "assert(t.townName == 'Rivendell', 'wrong townName');" + "assert(t.population == 500, 'wrong population')"); + if (!ok) + FAIL("Town component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 37: FurnitureTemplate component +// --------------------------------------------------------------------------- + +static int testFurnitureTemplateComponent(lua_State *L) +{ + TEST("FurnitureTemplate component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'FurnitureTemplate', {" + " templateName = 'wooden_chair'," + " meshName = 'chair.mesh'," + " category = 'seating'" + "});" + "local f = ecs.get_component(id, 'FurnitureTemplate');" + "assert(f ~= nil, 'FurnitureTemplate should exist');" + "assert(f.templateName == 'wooden_chair', 'wrong templateName');" + "assert(f.meshName == 'chair.mesh', 'wrong meshName');" + "assert(f.category == 'seating', 'wrong category')"); + if (!ok) + FAIL("FurnitureTemplate component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 38: StartupMenu component +// --------------------------------------------------------------------------- + +static int testStartupMenuComponent(lua_State *L) +{ + TEST("StartupMenu component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'StartupMenu', {" + " enabled = true," + " showOnStart = true" + "});" + "local s = ecs.get_component(id, 'StartupMenu');" + "assert(s ~= nil, 'StartupMenu should exist');" + "assert(s.enabled == true, 'wrong enabled');" + "assert(s.showOnStart == true, 'wrong showOnStart')"); + if (!ok) + FAIL("StartupMenu component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 39: Lod component +// --------------------------------------------------------------------------- + +static int testLodComponent(lua_State *L) +{ + TEST("Lod component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Lod', {" + " lodLevel = 0," + " distance = 100.0" + "});" + "local l = ecs.get_component(id, 'Lod');" + "assert(l ~= nil, 'Lod should exist');" + "assert(l.lodLevel == 0, 'wrong lodLevel');" + "assert(l.distance == 100.0, 'wrong distance')"); + if (!ok) + FAIL("Lod component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 40: LodSettings component +// --------------------------------------------------------------------------- + +static int testLodSettingsComponent(lua_State *L) +{ + TEST("LodSettings component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'LodSettings', {" + " enabled = true," + " lodBias = 1.0" + "});" + "local l = ecs.get_component(id, 'LodSettings');" + "assert(l ~= nil, 'LodSettings should exist');" + "assert(l.enabled == true, 'wrong enabled');" + "assert(l.lodBias == 1.0, 'wrong lodBias')"); + if (!ok) + FAIL("LodSettings component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 41: StaticGeometry component +// --------------------------------------------------------------------------- + +static int testStaticGeometryComponent(lua_State *L) +{ + TEST("StaticGeometry component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'StaticGeometry', {" + " batchName = 'forest_batch'," + " enabled = true" + "});" + "local s = ecs.get_component(id, 'StaticGeometry');" + "assert(s ~= nil, 'StaticGeometry should exist');" + "assert(s.batchName == 'forest_batch', 'wrong batchName');" + "assert(s.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("StaticGeometry component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 42: ProceduralTexture component +// --------------------------------------------------------------------------- + +static int testProceduralTextureComponent(lua_State *L) +{ + TEST("ProceduralTexture component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'ProceduralTexture', {" + " textureName = 'grass'," + " width = 512," + " height = 512" + "});" + "local p = ecs.get_component(id, 'ProceduralTexture');" + "assert(p ~= nil, 'ProceduralTexture should exist');" + "assert(p.textureName == 'grass', 'wrong textureName');" + "assert(p.width == 512, 'wrong width');" + "assert(p.height == 512, 'wrong height')"); + if (!ok) + FAIL("ProceduralTexture component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 43: ProceduralMaterial component +// --------------------------------------------------------------------------- + +static int testProceduralMaterialComponent(lua_State *L) +{ + TEST("ProceduralMaterial component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'ProceduralMaterial', {" + " materialName = 'ground_mat'," + " baseColor = { 0.5, 0.5, 0.5, 1.0 }" + "});" + "local p = ecs.get_component(id, 'ProceduralMaterial');" + "assert(p ~= nil, 'ProceduralMaterial should exist');" + "assert(p.materialName == 'ground_mat', 'wrong materialName')"); + if (!ok) + FAIL("ProceduralMaterial component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 44: Primitive component +// --------------------------------------------------------------------------- + +static int testPrimitiveComponent(lua_State *L) +{ + TEST("Primitive component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Primitive', {" + " primitiveType = 'box'," + " size = { 1, 2, 1 }" + "});" + "local p = ecs.get_component(id, 'Primitive');" + "assert(p ~= nil, 'Primitive should exist');" + "assert(p.primitiveType == 'box', 'wrong primitiveType');" + "assert(p.size[1] == 1, 'wrong size.x');" + "assert(p.size[2] == 2, 'wrong size.y')"); + if (!ok) + FAIL("Primitive component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 45: TriangleBuffer component +// --------------------------------------------------------------------------- + +static int testTriangleBufferComponent(lua_State *L) +{ + TEST("TriangleBuffer component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'TriangleBuffer', {" + " enabled = true," + " vertexCount = 100" + "});" + "local t = ecs.get_component(id, 'TriangleBuffer');" + "assert(t ~= nil, 'TriangleBuffer should exist');" + "assert(t.enabled == true, 'wrong enabled');" + "assert(t.vertexCount == 100, 'wrong vertexCount')"); + if (!ok) + FAIL("TriangleBuffer component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 46: CharacterSlots component +// --------------------------------------------------------------------------- + +static int testCharacterSlotsComponent(lua_State *L) +{ + TEST("CharacterSlots component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'CharacterSlots', {" + " slotCount = 8" + "});" + "local c = ecs.get_component(id, 'CharacterSlots');" + "assert(c ~= nil, 'CharacterSlots should exist');" + "assert(c.slotCount == 8, 'wrong slotCount')"); + if (!ok) + FAIL("CharacterSlots component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 47: AnimationTree component +// --------------------------------------------------------------------------- + +static int testAnimationTreeComponent(lua_State *L) +{ + TEST("AnimationTree component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'AnimationTree', {" + " treeName = 'humanoid'," + " enabled = true" + "});" + "local a = ecs.get_component(id, 'AnimationTree');" + "assert(a ~= nil, 'AnimationTree should exist');" + "assert(a.treeName == 'humanoid', 'wrong treeName');" + "assert(a.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("AnimationTree component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 48: AnimationTreeTemplate component +// --------------------------------------------------------------------------- + +static int testAnimationTreeTemplateComponent(lua_State *L) +{ + TEST("AnimationTreeTemplate component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'AnimationTreeTemplate', {" + " templateName = 'humanoid_base'," + " blendTime = 0.2" + "});" + "local a = ecs.get_component(id, 'AnimationTreeTemplate');" + "assert(a ~= nil, 'AnimationTreeTemplate should exist');" + "assert(a.templateName == 'humanoid_base', 'wrong templateName');" + "assert(a.blendTime == 0.2, 'wrong blendTime')"); + if (!ok) + FAIL("AnimationTreeTemplate component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 49: BehaviorTree component +// --------------------------------------------------------------------------- + +static int testBehaviorTreeComponent(lua_State *L) +{ + TEST("BehaviorTree component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'BehaviorTree', {" + " treeName = 'idle_behavior'," + " enabled = true" + "});" + "local b = ecs.get_component(id, 'BehaviorTree');" + "assert(b ~= nil, 'BehaviorTree should exist');" + "assert(b.treeName == 'idle_behavior', 'wrong treeName');" + "assert(b.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("BehaviorTree component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 50: GoapAction component +// --------------------------------------------------------------------------- + +static int testGoapActionComponent(lua_State *L) +{ + TEST("GoapAction component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'GoapAction', {" + " actionName = 'chop_wood'," + " cost = 5," + " enabled = true" + "});" + "local g = ecs.get_component(id, 'GoapAction');" + "assert(g ~= nil, 'GoapAction should exist');" + "assert(g.actionName == 'chop_wood', 'wrong actionName');" + "assert(g.cost == 5, 'wrong cost');" + "assert(g.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("GoapAction component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 51: GoapGoal component +// --------------------------------------------------------------------------- + +static int testGoapGoalComponent(lua_State *L) +{ + TEST("GoapGoal component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'GoapGoal', {" + " goalName = 'survive'," + " priority = 100," + " enabled = true" + "});" + "local g = ecs.get_component(id, 'GoapGoal');" + "assert(g ~= nil, 'GoapGoal should exist');" + "assert(g.goalName == 'survive', 'wrong goalName');" + "assert(g.priority == 100, 'wrong priority');" + "assert(g.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("GoapGoal component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 52: GoapPlanner component +// --------------------------------------------------------------------------- + +static int testGoapPlannerComponent(lua_State *L) +{ + TEST("GoapPlanner component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'GoapPlanner', {" + " enabled = true," + " maxIterations = 100" + "});" + "local g = ecs.get_component(id, 'GoapPlanner');" + "assert(g ~= nil, 'GoapPlanner should exist');" + "assert(g.enabled == true, 'wrong enabled');" + "assert(g.maxIterations == 100, 'wrong maxIterations')"); + if (!ok) + FAIL("GoapPlanner component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 53: GoapRunner component +// --------------------------------------------------------------------------- + +static int testGoapRunnerComponent(lua_State *L) +{ + TEST("GoapRunner component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'GoapRunner', {" + " enabled = true," + " currentAction = 'idle'" + "});" + "local g = ecs.get_component(id, 'GoapRunner');" + "assert(g ~= nil, 'GoapRunner should exist');" + "assert(g.enabled == true, 'wrong enabled');" + "assert(g.currentAction == 'idle', 'wrong currentAction')"); + if (!ok) + FAIL("GoapRunner component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 54: PathFollowing component +// --------------------------------------------------------------------------- + +static int testPathFollowingComponent(lua_State *L) +{ + TEST("PathFollowing component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'PathFollowing', {" + " enabled = true," + " speed = 1.5" + "});" + "local p = ecs.get_component(id, 'PathFollowing');" + "assert(p ~= nil, 'PathFollowing should exist');" + "assert(p.enabled == true, 'wrong enabled');" + "assert(p.speed == 1.5, 'wrong speed')"); + if (!ok) + FAIL("PathFollowing component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 55: NavMeshAgent component +// --------------------------------------------------------------------------- + +static int testNavMeshAgentComponent(lua_State *L) +{ + TEST("NavMeshAgent component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'NavMeshAgent', {" + " enabled = true," + " radius = 0.5," + " height = 2.0" + "});" + "local n = ecs.get_component(id, 'NavMeshAgent');" + "assert(n ~= nil, 'NavMeshAgent should exist');" + "assert(n.enabled == true, 'wrong enabled');" + "assert(n.radius == 0.5, 'wrong radius');" + "assert(n.height == 2.0, 'wrong height')"); + if (!ok) + FAIL("NavMeshAgent component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 56: ActionDatabase component +// --------------------------------------------------------------------------- + +static int testActionDatabaseComponent(lua_State *L) +{ + TEST("ActionDatabase component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'ActionDatabase', {" + " enabled = true" + "});" + "local a = ecs.get_component(id, 'ActionDatabase');" + "assert(a ~= nil, 'ActionDatabase should exist');" + "assert(a.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("ActionDatabase component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 57: ActionDebug component +// --------------------------------------------------------------------------- + +static int testActionDebugComponent(lua_State *L) +{ + TEST("ActionDebug component"); + + bool ok = runLua( + L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'ActionDebug', {" + " enabled = true," + " showDebugInfo = true" + "});" + "local a = ecs.get_component(id, 'ActionDebug');" + "assert(a ~= nil, 'ActionDebug should exist');" + "assert(a.enabled == true, 'wrong enabled');" + "assert(a.showDebugInfo == true, 'wrong showDebugInfo')"); + if (!ok) + FAIL("ActionDebug component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 58: WaterPhysics component +// --------------------------------------------------------------------------- + +static int testWaterPhysicsComponent(lua_State *L) +{ + TEST("WaterPhysics component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'WaterPhysics', {" + " enabled = true," + " waveHeight = 0.5" + "});" + "local w = ecs.get_component(id, 'WaterPhysics');" + "assert(w ~= nil, 'WaterPhysics should exist');" + "assert(w.enabled == true, 'wrong enabled');" + "assert(w.waveHeight == 0.5, 'wrong waveHeight')"); + if (!ok) + FAIL("WaterPhysics component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 59: ClearArea component +// --------------------------------------------------------------------------- + +static int testClearAreaComponent(lua_State *L) +{ + TEST("ClearArea component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'ClearArea', {" + " radius = 5.0," + " enabled = true" + "});" + "local c = ecs.get_component(id, 'ClearArea');" + "assert(c ~= nil, 'ClearArea should exist');" + "assert(c.radius == 5.0, 'wrong radius');" + "assert(c.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("ClearArea component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 60: Roof component +// --------------------------------------------------------------------------- + +static int testRoofComponent(lua_State *L) +{ + TEST("Roof component"); + + bool ok = runLua(L, "local id = ecs.create_entity();" + "ecs.set_component(id, 'Roof', {" + " roofType = 'gable'," + " height = 2.5," + " enabled = true" + "});" + "local r = ecs.get_component(id, 'Roof');" + "assert(r ~= nil, 'Roof should exist');" + "assert(r.roofType == 'gable', 'wrong roofType');" + "assert(r.height == 2.5, 'wrong height');" + "assert(r.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("Roof component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 61: StaticGeometryMember component +// --------------------------------------------------------------------------- + +static int testStaticGeometryMemberComponent(lua_State *L) +{ + TEST("StaticGeometryMember component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'StaticGeometryMember', {" + " parentBatch = 'forest_batch'," + " enabled = true" + "});" + "local s = ecs.get_component(id, 'StaticGeometryMember');" + "assert(s ~= nil, 'StaticGeometryMember should exist');" + "assert(s.parentBatch == 'forest_batch', 'wrong parentBatch');" + "assert(s.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("StaticGeometryMember component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 62: PhysicsCollider component +// --------------------------------------------------------------------------- + +static int testPhysicsColliderComponent(lua_State *L) +{ + TEST("PhysicsCollider component"); + + bool ok = runLua(L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'PhysicsCollider', {" + " shapeType = 'box'," + " size = { 1, 1, 1 }," + " enabled = true" + "});" + "local p = ecs.get_component(id, 'PhysicsCollider');" + "assert(p ~= nil, 'PhysicsCollider should exist');" + "assert(p.shapeType == 'box', 'wrong shapeType');" + "assert(p.enabled == true, 'wrong enabled')"); + if (!ok) + FAIL("PhysicsCollider component assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Component Lua API Tests\n"); + printf("=======================\n\n"); + + // Create Lua state + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Register the entity and component APIs + editScene::registerLuaEntityApi(L); + editScene::registerLuaComponentApi(L); + + // Run tests + int failures = 0; + failures += testHasComponent(L); + failures += testAddComponent(L); + failures += testSetGetComponent(L); + failures += testRemoveComponent(L); + failures += testGetSetField(L); + failures += testTransformComponent(L); + failures += testRenderableComponent(L); + failures += testLightComponent(L); + failures += testRigidBodyComponent(L); + failures += testEditorMarkerTag(L); + failures += testInWaterTag(L); + failures += testGeneratedPhysicsTag(L); + failures += testParentComponentTag(L); + failures += testModifiedComponent(L); + failures += testNavMeshGeometrySourceTag(L); + failures += testItemComponent(L); + failures += testInventoryComponent(L); + failures += testCharacterComponent(L); + failures += testCameraComponent(L); + failures += testSunComponent(L); + failures += testSkyboxComponent(L); + failures += testWaterPlaneComponent(L); + failures += testBuoyancyInfoComponent(L); + failures += testPlayerControllerComponent(L); + failures += testDialogueComponent(L); + failures += testNavMeshComponent(L); + failures += testSmartObjectComponent(L); + failures += testActuatorComponent(L); + failures += testEventHandlerComponent(L); + failures += testGoapBlackboardComponent(L); + failures += testPrefabInstanceComponent(L); + failures += testCellGridComponent(L); + failures += testRoomComponent(L); + failures += testLotComponent(L); + failures += testDistrictComponent(L); + failures += testTownComponent(L); + failures += testFurnitureTemplateComponent(L); + failures += testStartupMenuComponent(L); + failures += testLodComponent(L); + failures += testLodSettingsComponent(L); + failures += testStaticGeometryComponent(L); + failures += testProceduralTextureComponent(L); + failures += testProceduralMaterialComponent(L); + failures += testPrimitiveComponent(L); + failures += testTriangleBufferComponent(L); + failures += testCharacterSlotsComponent(L); + failures += testAnimationTreeComponent(L); + failures += testAnimationTreeTemplateComponent(L); + failures += testBehaviorTreeComponent(L); + failures += testGoapActionComponent(L); + failures += testGoapGoalComponent(L); + failures += testGoapPlannerComponent(L); + failures += testGoapRunnerComponent(L); + failures += testPathFollowingComponent(L); + failures += testNavMeshAgentComponent(L); + failures += testActionDatabaseComponent(L); + failures += testActionDebugComponent(L); + failures += testWaterPhysicsComponent(L); + failures += testClearAreaComponent(L); + failures += testRoofComponent(L); + failures += testStaticGeometryMemberComponent(L); + failures += testPhysicsColliderComponent(L); + + // Cleanup + lua_close(L); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/components/EditorMarker.hpp b/src/features/editScene/tests/components/EditorMarker.hpp new file mode 100644 index 0000000..9af759f --- /dev/null +++ b/src/features/editScene/tests/components/EditorMarker.hpp @@ -0,0 +1,27 @@ +/** + * @file EditorMarker.hpp + * @brief Stub for standalone tests. + * + * Provides the EditorMarkerComponent tag used by LuaEntityApi.cpp. + * In the real build, this is defined in the editScene components. + */ + +#ifndef EDITSCENE_COMPONENTS_EDITORMARKER_HPP +#define EDITSCENE_COMPONENTS_EDITORMARKER_HPP + +#include + +namespace editScene +{ + +/** + * @brief Tag component marking entities as editor-managed. + * + * In the real build, this is a Flecs tag. For tests, we define + * it as an empty struct so LuaEntityApi.cpp can compile. + */ +struct EditorMarkerComponent {}; + +} // namespace editScene + +#endif // EDITSCENE_COMPONENTS_EDITORMARKER_HPP diff --git a/src/features/editScene/tests/entity_lua_test.cpp b/src/features/editScene/tests/entity_lua_test.cpp new file mode 100644 index 0000000..148ff14 --- /dev/null +++ b/src/features/editScene/tests/entity_lua_test.cpp @@ -0,0 +1,403 @@ +/** + * @file entity_lua_test.cpp + * @brief Standalone test for the Lua Entity API. + * + * Tests entity creation, destruction, naming, hierarchy (parent/children), + * and entity lookup functions exposed via the ecs.* Lua API. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * entity_lua_test.cpp \ + * ../lua/LuaEntityApi.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o entity_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Flecs stub for standalone testing +// We provide minimal Flecs types needed by LuaEntityApi +namespace flecs +{ + +// Minimal entity wrapper +struct entity { + uint64_t m_id = 0; + bool m_valid = false; + + entity() + : m_id(0) + , m_valid(false) + { + } + explicit entity(uint64_t id) + : m_id(id) + , m_valid(true) + { + } + + uint64_t id() const + { + return m_id; + } + bool is_valid() const + { + return m_valid; + } + bool is_alive() const + { + return m_valid; + } + const char *name() const + { + return ""; + } + void set_name(const char *) + { + } + void destruct() + { + m_valid = false; + } + + entity parent() const + { + return entity(); + } + + template void children(Func) const + { + } + + template void add() + { + } + + template bool has() const + { + return false; + } + + template const T *get() const + { + return nullptr; + } + + template void set(const T &) + { + } + + bool operator==(const entity &other) const + { + return m_id == other.m_id; + } +}; + +using entity_t = uint64_t; + +// Minimal world stub +struct world { + entity make_entity() + { + return entity(nextId++); + } + entity lookup(const char *) + { + return entity(); + } + + static world &get() + { + static world w; + return w; + } + +private: + uint64_t nextId = 1000; +}; + +} // namespace flecs + +// Forward declare the registration function +namespace editScene +{ +void registerLuaEntityApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: Create entity +// --------------------------------------------------------------------------- + +static int testCreateEntity(lua_State *L) +{ + TEST("create entity returns integer ID"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "assert(type(id) == 'number', 'expected number, got ' .. type(id));" + "assert(id > 0, 'expected positive ID, got ' .. tostring(id))"); + if (!ok) + FAIL("create entity assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: Entity exists +// --------------------------------------------------------------------------- + +static int testEntityExists(lua_State *L) +{ + TEST("entity_exists returns correct values"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "assert(ecs.entity_exists(id) == true, 'entity should exist');" + "assert(ecs.entity_exists(999999) == false, 'fake entity should not exist')"); + if (!ok) + FAIL("entity exists assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Destroy entity +// --------------------------------------------------------------------------- + +static int testDestroyEntity(lua_State *L) +{ + TEST("destroy entity"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "assert(ecs.entity_exists(id) == true, 'entity should exist before destroy');" + "ecs.destroy_entity(id);" + "assert(ecs.entity_exists(id) == false, 'entity should not exist after destroy')"); + if (!ok) + FAIL("destroy entity assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Set and get entity name +// --------------------------------------------------------------------------- + +static int testEntityName(lua_State *L) +{ + TEST("set and get entity name"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_entity_name(id, 'test_hero');" + "local name = ecs.get_entity_name(id);" + "assert(name == 'test_hero', 'expected test_hero, got ' .. tostring(name))"); + if (!ok) + FAIL("entity name assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Get entity by name +// --------------------------------------------------------------------------- + +static int testGetEntityByName(lua_State *L) +{ + TEST("get entity by name"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_entity_name(id, 'findable_entity');" + "local found = ecs.get_entity_by_name('findable_entity');" + "assert(found == id, 'expected same ID, got ' .. tostring(found));" + "local not_found = ecs.get_entity_by_name('nonexistent');" + "assert(not_found == nil, 'expected nil for nonexistent')"); + if (!ok) + FAIL("get entity by name assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Parent and children hierarchy +// --------------------------------------------------------------------------- + +static int testHierarchy(lua_State *L) +{ + TEST("parent and children hierarchy"); + + bool ok = runLua( + L, + "local parent = ecs.create_entity();" + "local child = ecs.create_entity();" + "ecs.set_entity_name(parent, 'parent_entity');" + "ecs.set_entity_name(child, 'child_entity');" + "local p = ecs.parent(child);" + "assert(p == nil, 'child should have no parent initially');" + "local kids = ecs.children(parent);" + "assert(type(kids) == 'table', 'children should return a table');" + "assert(#kids == 0, 'parent should have no children initially')"); + if (!ok) + FAIL("hierarchy assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Multiple entity creation +// --------------------------------------------------------------------------- + +static int testMultipleEntities(lua_State *L) +{ + TEST("create multiple entities with unique IDs"); + + bool ok = runLua( + L, + "local ids = {};" + "for i = 1, 5 do" + " ids[i] = ecs.create_entity();" + "end;" + "for i = 1, 5 do" + " for j = i+1, 5 do" + " assert(ids[i] ~= ids[j], 'IDs should be unique');" + " end;" + "end;" + "for i = 1, 5 do" + " assert(ecs.entity_exists(ids[i]) == true, 'entity should exist');" + "end"); + if (!ok) + FAIL("multiple entities assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Destroy and recreate (ID reuse) +// --------------------------------------------------------------------------- + +static int testDestroyRecreate(lua_State *L) +{ + TEST("destroy and recreate entity"); + + bool ok = runLua( + L, + "local id1 = ecs.create_entity();" + "ecs.destroy_entity(id1);" + "assert(ecs.entity_exists(id1) == false, 'destroyed entity should not exist');" + "local id2 = ecs.create_entity();" + "assert(ecs.entity_exists(id2) == true, 'new entity should exist');" + "assert(id2 ~= id1, 'new entity should have different ID')"); + if (!ok) + FAIL("destroy recreate assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Entity Lua API Tests\n"); + printf("====================\n\n"); + + // Create Lua state + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Register the entity API + editScene::registerLuaEntityApi(L); + + // Run tests + int failures = 0; + failures += testCreateEntity(L); + failures += testEntityExists(L); + failures += testDestroyEntity(L); + failures += testEntityName(L); + failures += testGetEntityByName(L); + failures += testHierarchy(L); + failures += testMultipleEntities(L); + failures += testDestroyRecreate(L); + + // Cleanup + lua_close(L); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/event_lua_test.cpp b/src/features/editScene/tests/event_lua_test.cpp new file mode 100644 index 0000000..466dc74 --- /dev/null +++ b/src/features/editScene/tests/event_lua_test.cpp @@ -0,0 +1,375 @@ +/** + * @file event_lua_test.cpp + * @brief Standalone test for the Lua Event API. + * + * Tests event subscription, unsubscription, and sending functions + * exposed via the ecs.* Lua API. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * event_lua_test.cpp \ + * ../lua/LuaEventApi.cpp \ + * ../lua/LuaEntityApi.cpp \ + * ../systems/EventBus.cpp \ + * ../components/GoapBlackboard.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o event_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Include the components we need +#include "../systems/EventBus.hpp" +#include "../components/GoapBlackboard.hpp" + +// Forward declare the registration function +namespace editScene +{ +void registerLuaEventApi(lua_State *L); +void registerLuaEntityApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: Send a simple event +// --------------------------------------------------------------------------- + +static int testSendSimpleEvent(lua_State *L) +{ + TEST("send a simple event"); + + bool ok = runLua(L, "ecs.send_event('test_event')"); + if (!ok) + FAIL("failed to send simple event"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: Subscribe and receive an event +// --------------------------------------------------------------------------- + +static int testSubscribeAndReceive(lua_State *L) +{ + TEST("subscribe and receive an event"); + + // Track whether the callback was called + bool ok = runLua( + L, + "local received = false;" + "local sub_id = ecs.subscribe_event('hello', function(event, params)" + " received = true;" + "end);" + "assert(sub_id ~= nil, 'subscription ID should not be nil');" + "assert(type(sub_id) == 'number', 'subscription ID should be a number');" + "ecs.send_event('hello');" + "assert(received == true, 'callback should have been called')"); + if (!ok) + FAIL("subscribe and receive assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Subscribe with event name and params +// --------------------------------------------------------------------------- + +static int testSubscribeWithParams(lua_State *L) +{ + TEST("subscribe with event name and params"); + + bool ok = runLua( + L, + "local received_event = nil;" + "local sub_id = ecs.subscribe_event('collision', function(event, params)" + " received_event = event;" + "end);" + "ecs.send_event('collision', { values = { damage = 10 } });" + "assert(received_event == 'collision', 'expected collision event')"); + if (!ok) + FAIL("subscribe with params assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Unsubscribe from an event +// --------------------------------------------------------------------------- + +static int testUnsubscribe(lua_State *L) +{ + TEST("unsubscribe from an event"); + + bool ok = runLua( + L, + "local call_count = 0;" + "local sub_id = ecs.subscribe_event('temp', function(event, params)" + " call_count = call_count + 1;" + "end);" + "ecs.send_event('temp');" + "assert(call_count == 1, 'should have been called once');" + "ecs.unsubscribe_event(sub_id);" + "ecs.send_event('temp');" + "assert(call_count == 1, 'should not have been called after unsubscribe')"); + if (!ok) + FAIL("unsubscribe assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Multiple subscribers to same event +// --------------------------------------------------------------------------- + +static int testMultipleSubscribers(lua_State *L) +{ + TEST("multiple subscribers to same event"); + + bool ok = runLua( + L, + "local count1 = 0; local count2 = 0;" + "local s1 = ecs.subscribe_event('multi', function() count1 = count1 + 1; end);" + "local s2 = ecs.subscribe_event('multi', function() count2 = count2 + 1; end);" + "ecs.send_event('multi');" + "assert(count1 == 1, 'subscriber 1 should have been called');" + "assert(count2 == 1, 'subscriber 2 should have been called');" + "ecs.send_event('multi');" + "assert(count1 == 2, 'subscriber 1 should have been called twice');" + "assert(count2 == 2, 'subscriber 2 should have been called twice')"); + if (!ok) + FAIL("multiple subscribers assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Send event with blackboard params +// --------------------------------------------------------------------------- + +static int testEventWithBlackboardParams(lua_State *L) +{ + TEST("send event with blackboard params"); + + bool ok = runLua( + L, + "local result = nil;" + "ecs.subscribe_event('data_event', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('data_event', {" + " values = { score = 42, level = 5 }," + " floatValues = { speed = 1.5 }," + " stringValues = { name = 'hero' }" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result.values.score == 42, 'expected score 42');" + "assert(result.values.level == 5, 'expected level 5');" + "assert(result.floatValues.speed == 1.5, 'expected speed 1.5');" + "assert(result.stringValues.name == 'hero', 'expected name hero')"); + if (!ok) + FAIL("event with blackboard params assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Send event with vec3 params +// --------------------------------------------------------------------------- + +static int testEventWithVec3Params(lua_State *L) +{ + TEST("send event with vec3 params"); + + bool ok = runLua( + L, + "local result = nil;" + "ecs.subscribe_event('move', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('move', {" + " vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } }" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result.vec3Values.position[1] == 10, 'expected pos.x=10');" + "assert(result.vec3Values.position[2] == 20, 'expected pos.y=20');" + "assert(result.vec3Values.position[3] == 30, 'expected pos.z=30');" + "assert(result.vec3Values.velocity[1] == 1, 'expected vel.x=1')"); + if (!ok) + FAIL("event with vec3 params assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Send event with bits and mask +// --------------------------------------------------------------------------- + +static int testEventWithBits(lua_State *L) +{ + TEST("send event with bits and mask"); + + bool ok = runLua( + L, "local result = nil;" + "ecs.subscribe_event('flag_event', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('flag_event', {" + " bits = 5," + " mask = 7" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result.bits == 5, 'expected bits=5');" + "assert(result.mask == 7, 'expected mask=7')"); + if (!ok) + FAIL("event with bits assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: Multiple events with different names +// --------------------------------------------------------------------------- + +static int testMultipleEvents(lua_State *L) +{ + TEST("multiple events with different names"); + + bool ok = runLua( + L, + "local events = {};" + "ecs.subscribe_event('event_a', function() table.insert(events, 'a'); end);" + "ecs.subscribe_event('event_b', function() table.insert(events, 'b'); end);" + "ecs.send_event('event_a');" + "ecs.send_event('event_b');" + "ecs.send_event('event_a');" + "assert(#events == 3, 'expected 3 events, got ' .. #events);" + "assert(events[1] == 'a', 'expected a');" + "assert(events[2] == 'b', 'expected b');" + "assert(events[3] == 'a', 'expected a')"); + if (!ok) + FAIL("multiple events assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: Unsubscribe invalid ID (should not crash) +// --------------------------------------------------------------------------- + +static int testUnsubscribeInvalid(lua_State *L) +{ + TEST("unsubscribe invalid ID (should not crash)"); + + bool ok = runLua(L, "ecs.unsubscribe_event(99999);"); + if (!ok) + FAIL("unsubscribe invalid ID should not error"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Event Lua API Tests\n"); + printf("===================\n\n"); + + // Create Lua state + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Register the entity and event APIs + editScene::registerLuaEntityApi(L); + editScene::registerLuaEventApi(L); + + // Run tests + int failures = 0; + failures += testSendSimpleEvent(L); + failures += testSubscribeAndReceive(L); + failures += testSubscribeWithParams(L); + failures += testUnsubscribe(L); + failures += testMultipleSubscribers(L); + failures += testEventWithBlackboardParams(L); + failures += testEventWithVec3Params(L); + failures += testEventWithBits(L); + failures += testMultipleEvents(L); + failures += testUnsubscribeInvalid(L); + + // Cleanup + lua_close(L); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp new file mode 100644 index 0000000..a4a14b2 --- /dev/null +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -0,0 +1,576 @@ +/** + * @file lua_test_stubs.cpp + * @brief Stub implementations of Lua API registration functions for standalone tests. + * + * These stubs provide minimal implementations that work with the + * flecs stubs and ogre_stub.h defined in the test files. + * They are used instead of the real Lua API .cpp files which + * require the full OGRE SDK. + * + * The stubs maintain state (entity IDs, names, components) so that + * the tests can verify correct behavior. + */ + +#include "ogre_stub.h" +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Shared component storage (used by both entity and component API stubs) +// --------------------------------------------------------------------------- + +namespace editScene +{ + +// A value can be a number, string, boolean, array of numbers, array of strings, +// or a nested table (map of string->value) +struct ComponentFieldValue { + enum Type { NIL, NUMBER, STRING, BOOLEAN, NUM_ARRAY, STR_ARRAY, TABLE }; + Type type = NIL; + double numVal = 0; + std::string strVal; + bool boolVal = false; + std::vector numArr; + std::vector strArr; + std::map tableVal; +}; + +using ComponentData = std::unordered_map; +std::unordered_map > + s_components; + +} // namespace editScene + +// --------------------------------------------------------------------------- +// Stub: LuaEntityApi +// --------------------------------------------------------------------------- + +namespace editScene +{ + +// Entity state for stubs +struct EntityState { + bool alive; + std::string name; +}; + +static std::unordered_map s_entities; +static int s_nextId = 1000; + +static int createEntity() +{ + int id = s_nextId++; + s_entities[id] = { true, "" }; + // Auto-add EditorMarker component (matching real behavior) + s_components[id]["EditorMarker"]; + return id; +} + +static void destroyEntity(int id) +{ + auto it = s_entities.find(id); + if (it != s_entities.end()) + it->second.alive = false; +} + +static bool entityExists(int id) +{ + auto it = s_entities.find(id); + return it != s_entities.end() && it->second.alive; +} + +static void setEntityName(int id, const std::string &name) +{ + auto it = s_entities.find(id); + if (it != s_entities.end()) + it->second.name = name; +} + +static std::string getEntityName(int id) +{ + auto it = s_entities.find(id); + if (it != s_entities.end()) + return it->second.name; + return ""; +} + +static int findEntityByName(const std::string &name) +{ + for (auto &[id, state] : s_entities) { + if (state.alive && state.name == name) + return id; + } + return -1; +} + +void registerLuaEntityApi(lua_State *L) +{ + // Create the "ecs" global table + lua_newtable(L); + + // create_entity + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = createEntity(); + lua_pushinteger(L, id); + return 1; + }); + lua_setfield(L, -2, "create_entity"); + + // destroy_entity + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = lua_tointeger(L, 1); + destroyEntity(id); + return 0; + }); + lua_setfield(L, -2, "destroy_entity"); + + // entity_exists + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = lua_tointeger(L, 1); + lua_pushboolean(L, entityExists(id) ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "entity_exists"); + + // get_player_entity + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = findEntityByName("player"); + if (id >= 0) { + lua_pushinteger(L, id); + } else { + lua_pushnil(L); + } + return 1; + }); + lua_setfield(L, -2, "get_player_entity"); + + // get_entity_by_name + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *name = lua_tostring(L, 1); + int id = findEntityByName(name ? name : ""); + if (id >= 0) { + lua_pushinteger(L, id); + } else { + lua_pushnil(L); + } + return 1; + }); + lua_setfield(L, -2, "get_entity_by_name"); + + // set_entity_name + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = lua_tointeger(L, 1); + const char *name = lua_tostring(L, 2); + setEntityName(id, name ? name : ""); + return 0; + }); + lua_setfield(L, -2, "set_entity_name"); + + // get_entity_name + lua_pushcfunction(L, [](lua_State *L) -> int { + int id = lua_tointeger(L, 1); + std::string name = getEntityName(id); + if (!name.empty()) { + lua_pushstring(L, name.c_str()); + } else { + lua_pushnil(L); + } + return 1; + }); + lua_setfield(L, -2, "get_entity_name"); + + // parent + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushnil(L); + return 1; + }); + lua_setfield(L, -2, "parent"); + + // children + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_newtable(L); + return 1; + }); + lua_setfield(L, -2, "children"); + + lua_setglobal(L, "ecs"); +} + +} // namespace editScene + +// --------------------------------------------------------------------------- +// Stub: LuaComponentApi +// --------------------------------------------------------------------------- + +namespace editScene +{ + +static ComponentData &getOrCreateComponent(int entityId, + const std::string &compName) +{ + return s_components[entityId][compName]; +} + +static bool hasComponent(int entityId, const std::string &compName) +{ + auto eit = s_components.find(entityId); + if (eit == s_components.end()) + return false; + return eit->second.find(compName) != eit->second.end(); +} + +static void removeComponent(int entityId, const std::string &compName) +{ + auto eit = s_components.find(entityId); + if (eit != s_components.end()) + eit->second.erase(compName); +} + +// Forward declaration +static void pushFieldValueToLua(lua_State *L, const ComponentFieldValue &fv); + +static void pushFieldValueToLua(lua_State *L, const ComponentFieldValue &fv) +{ + switch (fv.type) { + case ComponentFieldValue::NIL: + lua_pushnil(L); + break; + case ComponentFieldValue::NUMBER: + lua_pushnumber(L, fv.numVal); + break; + case ComponentFieldValue::STRING: + lua_pushstring(L, fv.strVal.c_str()); + break; + case ComponentFieldValue::BOOLEAN: + lua_pushboolean(L, fv.boolVal ? 1 : 0); + break; + case ComponentFieldValue::NUM_ARRAY: { + lua_newtable(L); + for (size_t i = 0; i < fv.numArr.size(); i++) { + lua_pushnumber(L, fv.numArr[i]); + lua_rawseti(L, -2, i + 1); + } + break; + } + case ComponentFieldValue::STR_ARRAY: { + lua_newtable(L); + for (size_t i = 0; i < fv.strArr.size(); i++) { + lua_pushstring(L, fv.strArr[i].c_str()); + lua_rawseti(L, -2, i + 1); + } + break; + } + case ComponentFieldValue::TABLE: { + lua_newtable(L); + for (auto &[k, v] : fv.tableVal) { + pushFieldValueToLua(L, v); + lua_setfield(L, -2, k.c_str()); + } + break; + } + } +} + +// Forward declaration +static ComponentFieldValue readLuaValue(lua_State *L, int idx); + +static ComponentFieldValue readLuaValue(lua_State *L, int idx) +{ + ComponentFieldValue fv; + // Convert to absolute index to be safe with stack changes + int absIdx = lua_absindex(L, idx); + int type = lua_type(L, absIdx); + if (type == LUA_TNIL) { + fv.type = ComponentFieldValue::NIL; + } else if (type == LUA_TNUMBER) { + fv.type = ComponentFieldValue::NUMBER; + fv.numVal = lua_tonumber(L, absIdx); + } else if (type == LUA_TSTRING) { + fv.type = ComponentFieldValue::STRING; + fv.strVal = lua_tostring(L, absIdx); + } else if (type == LUA_TBOOLEAN) { + fv.type = ComponentFieldValue::BOOLEAN; + fv.boolVal = lua_toboolean(L, absIdx) != 0; + } else if (type == LUA_TTABLE) { + // Check if it's an array (all integer keys) or a map (string keys) + bool isArray = true; + bool isStringArray = false; + bool isNumArray = false; + int maxKey = 0; + + lua_pushnil(L); + while (lua_next(L, absIdx) != 0) { + if (lua_type(L, -2) == LUA_TNUMBER) { + int k = lua_tointeger(L, -2); + if (k > maxKey) + maxKey = k; + if (lua_type(L, -1) == LUA_TSTRING) + isStringArray = true; + else if (lua_type(L, -1) == LUA_TNUMBER) + isNumArray = true; + } else { + isArray = false; + } + lua_pop(L, 1); + } + + if (isArray && maxKey > 0) { + if (isStringArray) { + fv.type = ComponentFieldValue::STR_ARRAY; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TSTRING) + fv.strArr.push_back( + lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else if (isNumArray) { + fv.type = ComponentFieldValue::NUM_ARRAY; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TNUMBER) + fv.numArr.push_back( + lua_tonumber(L, -1)); + lua_pop(L, 1); + } + } + } else { + fv.type = ComponentFieldValue::TABLE; + lua_pushnil(L); + while (lua_next(L, absIdx) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char *key = lua_tostring(L, -2); + if (key) + fv.tableVal[key] = + readLuaValue(L, -1); + } + lua_pop(L, 1); + } + } + } + return fv; +} + +void registerLuaComponentApi(lua_State *L) +{ + // Get or create the "ecs" global table + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // has_component + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + lua_pushboolean(L, hasComponent(entityId, + compName ? compName : "") ? + 1 : + 0); + return 1; + }); + lua_setfield(L, -2, "has_component"); + + // add_component + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + if (compName) + getOrCreateComponent(entityId, compName); + return 0; + }); + lua_setfield(L, -2, "add_component"); + + // remove_component + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + if (compName) + removeComponent(entityId, compName); + return 0; + }); + lua_setfield(L, -2, "remove_component"); + + // get_component + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + if (!compName || !hasComponent(entityId, compName)) { + lua_pushnil(L); + return 1; + } + ComponentData &cd = s_components[entityId][compName]; + lua_newtable(L); + for (auto &[k, v] : cd) { + pushFieldValueToLua(L, v); + lua_setfield(L, -2, k.c_str()); + } + return 1; + }); + lua_setfield(L, -2, "get_component"); + + // set_component + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + if (!compName) + return 0; + ComponentData &cd = getOrCreateComponent(entityId, compName); + if (lua_istable(L, 3)) { + cd.clear(); + lua_pushnil(L); + while (lua_next(L, 3) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char *key = lua_tostring(L, -2); + if (key) + cd[key] = readLuaValue(L, -1); + } + lua_pop(L, 1); + } + } + return 0; + }); + lua_setfield(L, -2, "set_component"); + + // get_field + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + const char *fieldName = lua_tostring(L, 3); + if (!compName || !fieldName || + !hasComponent(entityId, compName)) { + lua_pushnil(L); + return 1; + } + ComponentData &cd = s_components[entityId][compName]; + auto it = cd.find(fieldName); + if (it != cd.end()) { + pushFieldValueToLua(L, it->second); + } else { + lua_pushnil(L); + } + return 1; + }); + lua_setfield(L, -2, "get_field"); + + // set_field + lua_pushcfunction(L, [](lua_State *L) -> int { + int entityId = lua_tointeger(L, 1); + const char *compName = lua_tostring(L, 2); + const char *fieldName = lua_tostring(L, 3); + if (!compName || !fieldName) + return 0; + ComponentData &cd = getOrCreateComponent(entityId, compName); + cd[fieldName] = readLuaValue(L, 4); + return 0; + }); + lua_setfield(L, -2, "set_field"); + + lua_setglobal(L, "ecs"); +} + +} // namespace editScene + +// --------------------------------------------------------------------------- +// Stub: LuaEventApi +// --------------------------------------------------------------------------- + +namespace editScene +{ + +// Event subscription storage +struct EventSubscription { + int id; + std::string eventName; + int callbackRef; // Lua registry reference +}; + +static std::vector s_subscriptions; +static int s_nextSubId = 1; + +void registerLuaEventApi(lua_State *L) +{ + // Get or create the "ecs" global table + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // subscribe_event + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *eventName = lua_tostring(L, 1); + if (!eventName || !lua_isfunction(L, 2)) { + lua_pushnil(L); + return 1; + } + // Store callback in Lua registry + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + int subId = s_nextSubId++; + s_subscriptions.push_back({ subId, eventName, ref }); + lua_pushinteger(L, subId); + return 1; + }); + lua_setfield(L, -2, "subscribe_event"); + + // unsubscribe_event + lua_pushcfunction(L, [](lua_State *L) -> int { + int subId = lua_tointeger(L, 1); + for (auto it = s_subscriptions.begin(); + it != s_subscriptions.end(); ++it) { + if (it->id == subId) { + luaL_unref(L, LUA_REGISTRYINDEX, + it->callbackRef); + s_subscriptions.erase(it); + break; + } + } + return 0; + }); + lua_setfield(L, -2, "unsubscribe_event"); + + // send_event + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *eventName = lua_tostring(L, 1); + if (!eventName) + return 0; + + // Call all matching subscriptions + for (auto &sub : s_subscriptions) { + if (sub.eventName == eventName) { + // Push callback + lua_rawgeti(L, LUA_REGISTRYINDEX, + sub.callbackRef); + // Push event name + lua_pushstring(L, eventName); + // Push params (table or nil) + if (lua_istable(L, 2)) { + lua_pushvalue(L, 2); + } else { + lua_pushnil(L); + } + // Call callback(event, params) + if (lua_pcall(L, 2, 0, 0) != LUA_OK) { + fprintf(stderr, + "Event callback error: %s\n", + lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + } + return 0; + }); + lua_setfield(L, -2, "send_event"); + + lua_setglobal(L, "ecs"); +} + +} // namespace editScene diff --git a/src/features/editScene/tests/ogre_stub.h b/src/features/editScene/tests/ogre_stub.h index 0f51090..05d39ae 100644 --- a/src/features/editScene/tests/ogre_stub.h +++ b/src/features/editScene/tests/ogre_stub.h @@ -72,6 +72,17 @@ public: } }; +// OgreAssert macro (used by LuaEntityApi.hpp) +#ifndef OgreAssert +#define OgreAssert(expr, msg) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "OgreAssert failed: %s\n", msg); \ + assert(expr); \ + } \ + } while (0) +#endif + } // namespace Ogre #endif // OGRE_STUB_H