Repository: mlange-42/arche
Pull request #315 (2024-01-10 13:38:38)

Type and ID getters
Allows users to:

* get `reflect.Type` for component and resource IDs
* get list of components for an entity or for the current query position

Preparation to allow for serialization/deserialization. See #314.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdbaf14..ac7af66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,11 @@
 * `Filters.Matches(Mask)` became `Filters.Matches(*Mask)`; same for all `Filter` implementations (#313)  
 This change was necessary to get the same performance as before, despite the more heavyweight implementation of the now 256 bits `Mask`.
 
+### Features
+
+* Adds functions `ComponentType(*World, ID) reflect.Type` and `ResourceType(*World, ID) reflect.Type` (#315)
+* Adds methods `World.Ids(Entity) []ID` and `Query.Ids() []ID` (#315)
+
 ## [[v0.9.0]](https://github.com/mlange-42/arche/compare/v0.8.1...v0.9.0)
 
 ### Infrastructure
diff --git a/ecs/query.go b/ecs/query.go
index db47199..3de4beb 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -19,6 +19,7 @@ type Query struct {
 	nodes          []*archNode      // The query's nodes.
 	world          *World           // The [World].
 	access         *archetypeAccess // Access helper for the archetype currently being iterated.
+	archetype      *archetype       // The archetype currently being iterated.
 	entityIndex    uint32           // Iteration index of the current [Entity] current archetype.
 	entityIndexMax uint32           // Maximum entity index in the current archetype.
 	archIndex      int32            // Iteration index of the current archetype.
@@ -160,12 +161,15 @@ func (q *Query) Count() int {
 }
 
 // Mask returns the archetype [Mask] for the [Entity] at the iterator's current position.
-//
-// Can be used for fast checks of the entity composition, e.g. using a [Filter].
 func (q *Query) Mask() Mask {
 	return q.access.Mask
 }
 
+// Ids returns the component IDs for the archetype of the [Entity] at the iterator's current position.
+func (q *Query) Ids() []ID {
+	return q.archetype.node.Ids
+}
+
 // Close closes the Query and unlocks the world.
 //
 // Automatically called when iteration finishes.
@@ -201,6 +205,7 @@ func (q *Query) nextArchetypeBatch() bool {
 		aLen := a.Len()
 		if aLen > 0 {
 			q.access = &a.archetypeAccess
+			q.archetype = a
 			batch := q.nodeArchetypes.(*batchArchetypes)
 			q.entityIndex = batch.StartIndex[q.archIndex]
 			q.entityIndexMax = batch.EndIndex[q.archIndex] - 1
@@ -220,6 +225,7 @@ func (q *Query) nextArchetypeSimple() bool {
 			continue
 		}
 		q.access = &a.archetypeAccess
+		q.archetype = a
 		q.entityIndex = 0
 		q.entityIndexMax = aLen - 1
 		return true
@@ -237,6 +243,7 @@ func (q *Query) nextArchetypeFiltered() bool {
 			continue
 		}
 		q.access = &a.archetypeAccess
+		q.archetype = a
 		q.entityIndex = 0
 		q.entityIndexMax = aLen - 1
 		return true
@@ -273,7 +280,7 @@ func (q *Query) nextNode() bool {
 			arch := arches.Get(0)
 			archLen := arch.Len()
 			if archLen > 0 {
-				q.setArchetype(nil, &arch.archetypeAccess, arch.index, archLen-1)
+				q.setArchetype(nil, &arch.archetypeAccess, arch, arch.index, archLen-1)
 				return true
 			}
 			continue
@@ -282,13 +289,13 @@ func (q *Query) nextNode() bool {
 		if rf, ok := q.filter.(*RelationFilter); ok {
 			target := rf.Target
 			if arch, ok := n.archetypeMap[target]; ok && arch.Len() > 0 {
-				q.setArchetype(nil, &arch.archetypeAccess, arch.index, arch.Len()-1)
+				q.setArchetype(nil, &arch.archetypeAccess, arch, arch.index, arch.Len()-1)
 				return true
 			}
 			continue
 		}
 
-		q.setArchetype(arches, nil, -1, 0)
+		q.setArchetype(arches, nil, nil, -1, 0)
 		if q.nextArchetypeSimple() {
 			return true
 		}
@@ -298,10 +305,11 @@ func (q *Query) nextNode() bool {
 	return false
 }
 
-func (q *Query) setArchetype(arches archetypes, access *archetypeAccess, archIndex int32, maxIndex uint32) {
+func (q *Query) setArchetype(arches archetypes, access *archetypeAccess, arch *archetype, archIndex int32, maxIndex uint32) {
 	q.nodeArchetypes = arches
 	q.archIndex = archIndex
 	q.access = access
+	q.archetype = arch
 	q.entityIndex = 0
 	q.entityIndexMax = maxIndex
 }
diff --git a/ecs/world.go b/ecs/world.go
index 5306e2b..d320ab8 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -25,6 +25,11 @@ func TypeID(w *World, tp reflect.Type) ID {
 	return w.componentID(tp)
 }
 
+// ComponentType returns the reflect.Type for a component [ID], and whether the ID is assigned.
+func ComponentType(w *World, id ID) (reflect.Type, bool) {
+	return w.registry.ComponentType(id)
+}
+
 // ResourceID returns the [ResID] for a resource type via generics.
 // Registers the type if it is not already registered.
 //
@@ -34,6 +39,11 @@ func ResourceID[T any](w *World) ResID {
 	return w.resourceID(tp)
 }
 
+// ResourceType returns the reflect.Type for a resource [ResID], and whether the ID is assigned.
+func ResourceType(w *World, id ResID) (reflect.Type, bool) {
+	return w.resources.registry.ComponentType(id)
+}
+
 // GetResource returns a pointer to the given resource type in the world.
 //
 // Returns nil if there is no such resource.
@@ -1018,8 +1028,6 @@ func (w *World) IsLocked() bool {
 }
 
 // Mask returns the archetype [Mask] for the given [Entity].
-//
-// Can be used for fast checks of the entity composition, e.g. using a [Filter].
 func (w *World) Mask(entity Entity) Mask {
 	if !w.entityPool.Alive(entity) {
 		panic("can't get mask for a dead entity")
@@ -1027,6 +1035,14 @@ func (w *World) Mask(entity Entity) Mask {
 	return w.entities[entity.id].arch.Mask
 }
 
+// Ids returns the component IDs for the archetype of the given [Entity].
+func (w *World) Ids(entity Entity) []ID {
+	if !w.entityPool.Alive(entity) {
+		panic("can't get mask for a dead entity")
+	}
+	return w.entities[entity.id].arch.node.Ids
+}
+
 // ComponentType returns the reflect.Type for a given component ID, as well as whether the ID is in use.
 func (w *World) ComponentType(id ID) (reflect.Type, bool) {
 	return w.registry.ComponentType(id)


Test patch:
diff --git a/ecs/query_test.go b/ecs/query_test.go
index 7191650..069731a 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -441,3 +441,21 @@ func TestQueryRelations(t *testing.T) {
 		assert.Panics(t, func() { query.Relation(velID) })
 	}
 }
+
+func TestQueryIds(t *testing.T) {
+	world := NewWorld()
+
+	posID := ComponentID[Position](&world)
+	velID := ComponentID[Velocity](&world)
+
+	_ = world.NewEntity(velID)
+	_ = world.NewEntity(velID, posID)
+
+	filter := All()
+	query := world.Query(filter)
+
+	query.Next()
+	assert.Equal(t, query.Ids(), []ID{1})
+	query.Next()
+	assert.Equal(t, query.Ids(), []ID{0, 1})
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 0be9789..a770040 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -182,6 +182,48 @@ func TestWorldComponents(t *testing.T) {
 	assert.Panics(t, func() { w.Get(newEntityGen(1, 0), posID) })
 }
 
+func TestWorldTypes(t *testing.T) {
+	w := NewWorld()
+	_ = ComponentID[Velocity](&w)
+	posID := ComponentID[Position](&w)
+
+	tp, ok := ComponentType(&w, posID)
+	assert.True(t, ok)
+	assert.Equal(t, tp, reflect.TypeOf(Position{}))
+
+	tp, ok = ComponentType(&w, 3)
+	assert.False(t, ok)
+	assert.Equal(t, tp, nil)
+
+	resID := ResourceID[Velocity](&w)
+
+	tp, ok = ResourceType(&w, resID)
+	assert.True(t, ok)
+	assert.Equal(t, tp, reflect.TypeOf(Velocity{}))
+
+	tp, ok = ResourceType(&w, 3)
+	assert.False(t, ok)
+	assert.Equal(t, tp, nil)
+}
+
+func TestWorldIds(t *testing.T) {
+	w := NewWorld()
+	velID := ComponentID[Velocity](&w)
+	posID := ComponentID[Position](&w)
+
+	e1 := w.NewEntity(posID)
+	e2 := w.NewEntity(posID, velID)
+	e3 := w.NewEntity(velID)
+
+	assert.Equal(t, w.Ids(e1), []ID{1})
+	assert.Equal(t, w.Ids(e2), []ID{0, 1})
+	assert.Equal(t, w.Ids(e3), []ID{0})
+
+	w.RemoveEntity(e1)
+
+	assert.Panics(t, func() { _ = w.Ids(e1) })
+}
+
 func TestWorldLabels(t *testing.T) {
 	w := NewWorld()