Skip to content

Commit

Permalink
feat(cardinal): add GetAllEntities method to WorldContext (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
smsunarto authored Mar 8, 2025
1 parent 2d429cc commit 58d7567
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 0 deletions.
43 changes: 43 additions & 0 deletions cardinal/world_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

"pkg.world.dev/world-engine/cardinal/filter"
"pkg.world.dev/world-engine/cardinal/gamestate"
"pkg.world.dev/world-engine/cardinal/persona/component"
"pkg.world.dev/world-engine/cardinal/receipt"
"pkg.world.dev/world-engine/cardinal/txpool"
"pkg.world.dev/world-engine/cardinal/types"
Expand Down Expand Up @@ -53,6 +55,10 @@ type WorldContext interface {
// The given Task must have been registered using RegisterTask.
ScheduleTimeTask(time.Duration, Task) error

// GetAllEntities returns all entities and their components as a map.
// The map is keyed by entity ID, and the value is a map of component name to component data.
GetAllEntities() (map[types.EntityID]map[string]any, error)

// Private methods for internal use.
setLogger(logger zerolog.Logger)
addMessageError(id types.TxHash, err error)
Expand Down Expand Up @@ -161,6 +167,43 @@ func (ctx *worldContext) Namespace() string {
return ctx.world.Namespace()
}

// GetAllEntities returns all entities and their components as a map.
// The map is keyed by entity ID, and the value is a map of component name to component data.
func (ctx *worldContext) GetAllEntities() (map[types.EntityID]map[string]any, error) {
entities := make(map[types.EntityID]map[string]any)

// Get all entities excluding internal Persona components
err := NewSearch().Entity(
filter.Not(
filter.Or(
filter.Contains(filter.Component[component.SignerComponent]()),
filter.Contains(filter.Component[taskMetadata]()),
),
),
).Each(ctx, func(id types.EntityID) bool {
entities[id] = make(map[string]any)

components, err := ctx.world.StoreReader().GetComponentTypesForEntity(id)
if err != nil {
return false
}

for _, c := range components {
compJSON, err := ctx.world.StoreReader().GetComponentForEntityInRawJSON(c, id)
if err != nil {
return false
}
entities[id][c.Name()] = compJSON
}
return true
})
if err != nil {
return nil, err
}

return entities, nil
}

// -----------------------------------------------------------------------------
// Private methods
// -----------------------------------------------------------------------------
Expand Down
150 changes: 150 additions & 0 deletions cardinal/world_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cardinal

import (
"encoding/json"
"testing"

"pkg.world.dev/world-engine/assert"
"pkg.world.dev/world-engine/cardinal/filter"
"pkg.world.dev/world-engine/cardinal/persona/component"
"pkg.world.dev/world-engine/cardinal/types"
)

// Test component types
type TestComponentA struct {
Value string
}

type TestComponentB struct {
Counter int
}

func (TestComponentA) Name() string { return "test_component_a" }
func (TestComponentB) Name() string { return "test_component_b" }

type TestTask struct {
Value string
}

func (TestTask) Name() string { return "test_task" }

func (t TestTask) Handle(_ WorldContext) error {
return nil
}

// TestGetAllEntities verifies that the GetAllEntities function returns all entities
// and their components as expected.
func TestGetAllEntities(t *testing.T) {
// Setup a test fixture
tf := NewTestFixture(t, nil)
world := tf.World

// Register test components
assert.NilError(t, RegisterComponent[TestComponentA](world))
assert.NilError(t, RegisterComponent[TestComponentB](world))

// Register a task
assert.NilError(t, RegisterTask[TestTask](world))

// Start the world
tf.StartWorld()

// Create world context
wCtx := NewWorldContext(world)

// Create test entities
entity1, err := Create(wCtx, TestComponentA{Value: "Entity 1"}, TestComponentB{Counter: 10})
assert.NilError(t, err)

entity2, err := Create(wCtx, TestComponentA{Value: "Entity 2"}, TestComponentB{Counter: 20})
assert.NilError(t, err)

// Create a persona
tf.CreatePersona("testpersona", "testaddress")

// Create a task
err = wCtx.ScheduleTickTask(1000, TestTask{Value: "Task 1"})
assert.NilError(t, err)

// Verify that all 4 entities are created
count, err := NewSearch().Entity(filter.All()).Count(wCtx)
assert.NilError(t, err)
assert.Equal(t, 4, count, "Expected 4 entities, got %d", count)

// Call GetAllEntities
entities, err := wCtx.GetAllEntities()
assert.NilError(t, err)

// Verify results
assert.Equal(t, 2, len(entities), "Expected 2 entities, got %d", len(entities))

// Check that we have the expected entities
_, hasEntity1 := entities[entity1]
_, hasEntity2 := entities[entity2]

// Check if there's a persona entity with a signer component
hasSignerEntity := false
for _, components := range entities {
// Check if this entity has a signer component
_, hasSigner := components[component.SignerComponent{}.Name()]
if hasSigner {
hasSignerEntity = true
break
}
}

// Check if there's a task entity with a task component
hasTaskEntity := false
for _, components := range entities {
// Check if this entity has a task component
_, hasTask := components[taskMetadata{}.Name()]
if hasTask {
hasTaskEntity = true
break
}
}

assert.Equal(t, true, hasEntity1, "Entity 1 should be included in results")
assert.Equal(t, true, hasEntity2, "Entity 2 should be included in results")
assert.Equal(t, false, hasSignerEntity, "Signer entity should be excluded from results")
assert.Equal(t, false, hasTaskEntity, "Task entity should be excluded from results")

// Verify that the components have the expected values
// Note: We need to convert the raw JSON back to our component types
if hasEntity1 {
entity1Components := entities[entity1]

// Verify TestComponentA
var compA TestComponentA
compAJSON, ok := entity1Components["test_component_a"].(json.RawMessage)
assert.Equal(t, true, ok, "TestComponentA should be present for entity1")
assert.NilError(t, json.Unmarshal(compAJSON, &compA))
assert.Equal(t, "Entity 1", compA.Value)

// Verify TestComponentB
var compB TestComponentB
compBJSON, ok := entity1Components["test_component_b"].(json.RawMessage)
assert.Equal(t, true, ok, "TestComponentB should be present for entity1")
assert.NilError(t, json.Unmarshal(compBJSON, &compB))
assert.Equal(t, 10, compB.Counter)
}

// Test that we can find entity1 using Search with the correct filter
searchResult := NewSearch().Entity(
filter.Contains(filter.Component[TestComponentA]()),
)

foundEntities := make([]bool, 2)
err = searchResult.Each(wCtx, func(id types.EntityID) bool {
if id == entity1 {
foundEntities[0] = true
} else if id == entity2 {
foundEntities[1] = true
}
return true
})
assert.NilError(t, err)

assert.Equal(t, true, foundEntities[0], "Entity 1 should be found via Search")
assert.Equal(t, true, foundEntities[1], "Entity 2 should be found via Search")
}

0 comments on commit 58d7567

Please sign in to comment.