Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,46 @@ func TestTag8(t *testing.T) {
t.Errorf("The tag %d should not be returned as a Component from world.GetComponent", TAG_2)
}
}

// TestRemoveEntityWithTag guards against a regression where RemoveEntity
// panicked with an out-of-range index, because it tried to look up a storage
// for tag ids (which live outside the storage range and have no storage).
func TestRemoveEntityWithTag(t *testing.T) {
world := CreateWorld(1024)
RegisterComponent[testComponent1](world, &ComponentConfig[testComponent1]{})

entities := make([]EntityId, 5)
for i := range entities {
entities[i] = world.CreateEntity()
if err := AddComponent[testComponent1](world, entities[i], testComponent1{}); err != nil {
t.Fatalf("%s", err.Error())
}
if err := world.AddTag(TAG_1, entities[i]); err != nil {
t.Fatalf("%s", err.Error())
}
}

// Remove a middle entity (the hardest case: triggers the swap-with-last path).
world.RemoveEntity(entities[2])

// The remaining tagged entities must still be reachable and consistent.
q := CreateQuery1[testComponent1](world, QueryConfiguration{Tags: []TagId{TAG_1}})
if got := q.Count(); got != 4 {
t.Fatalf("expected 4 tagged entities after removal, got %d", got)
}
for _, e := range []EntityId{entities[0], entities[1], entities[3], entities[4]} {
if !world.HasTag(TAG_1, e) {
t.Fatalf("entity %d lost its tag after sibling removal", e)
}
if !world.HasComponents(e, testComponent1Id) {
t.Fatalf("entity %d lost its component after sibling removal", e)
}
}

// An entity carrying only a tag (no component) must also be removable.
tagOnly := world.CreateEntity()
if err := world.AddTag(TAG_2, tagOnly); err != nil {
t.Fatalf("%s", err.Error())
}
world.RemoveEntity(tagOnly)
}
11 changes: 6 additions & 5 deletions world.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// Package volt is an ECS for game development, based on the Archetype paradigm.
package volt

import (
"slices"
)

// uint16 identifier, for small scoped data.
type smallId uint16

Expand Down Expand Up @@ -248,8 +244,13 @@ func (world *World) RemoveEntity(entityId EntityId) {

lastEntityKey := len(archetype.entities) - 1
for _, componentId := range archetype.Type {
// Tags have no storage: their id lives outside the storage range,
// so indexing world.storage[componentId] would overflow.
if componentId >= TAGS_INDICES {
continue
}
s := world.storage[componentId]
if s != nil && slices.Contains(archetype.Type, s.getType()) {
if s != nil {
s.moveLastToKey(archetype.Id, entityRecord.key)
}
}
Expand Down
Loading