This commit is contained in:
2026-04-30 19:07:35 +03:00
parent 0ed83966da
commit 4d843c18c7
12 changed files with 4596 additions and 7 deletions

View File

@@ -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

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -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<GeneratedPhysicsTag>() ? 1 : 0);
},
[](lua_State *L, flecs::entity e, int idx) {
(void)idx;
if (lua_toboolean(L, -1))
e.add<GeneratedPhysicsTag>();
else
e.remove<GeneratedPhysicsTag>();
}
};
// --- 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<ParentComponent>() ? 1 : 0);
},
[](lua_State *L, flecs::entity e, int idx) {
(void)idx;
if (lua_toboolean(L, -1))
e.add<ParentComponent>();
else
e.remove<ParentComponent>();
}
};
// --- ModifiedComponent ---
s_components["ModifiedComponent"] = {
"ModifiedComponent",
[](lua_State *L, flecs::entity e) {
if (!e.has<ModifiedComponent>()) {
lua_pushnil(L);
return;
}
const auto &c = e.get<ModifiedComponent>();
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<ModifiedComponent>())
c = e.get<ModifiedComponent>();
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<ModifiedComponent>(c);
}
};
}
// ---------------------------------------------------------------------------

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 <flecs.h>
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

View File

@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// 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 <typename Func> void children(Func) const
{
}
template <typename T> void add()
{
}
template <typename T> bool has() const
{
return false;
}
template <typename T> const T *get() const
{
return nullptr;
}
template <typename T> 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;
}

View File

@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// 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;
}

View File

@@ -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 <lua.hpp>
#include <cstdio>
#include <cassert>
#include <string>
#include <unordered_map>
#include <vector>
#include <map>
// ---------------------------------------------------------------------------
// 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<double> numArr;
std::vector<std::string> strArr;
std::map<std::string, ComponentFieldValue> tableVal;
};
using ComponentData = std::unordered_map<std::string, ComponentFieldValue>;
std::unordered_map<int, std::unordered_map<std::string, ComponentData> >
s_components;
} // namespace editScene
// ---------------------------------------------------------------------------
// Stub: LuaEntityApi
// ---------------------------------------------------------------------------
namespace editScene
{
// Entity state for stubs
struct EntityState {
bool alive;
std::string name;
};
static std::unordered_map<int, EntityState> 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<EventSubscription> 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

View File

@@ -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