Skip to content

Commit ac7dd0d

Browse files
committed
fix: prune orphaned static actor vid conflicts
1 parent dd39f9a commit ac7dd0d

3 files changed

Lines changed: 56 additions & 2 deletions

File tree

internal/worldruntime/non_player_directory.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (d *NonPlayerDirectory) Register(actor StaticEntity) bool {
2222
if _, ok := d.byEntityID[actor.Entity.ID]; ok {
2323
return false
2424
}
25-
if vid, ok := StaticActorVisibilityVID(actor); ok && conflictingEntityID(d.entityIDByVID, vid, actor.Entity.ID) {
25+
if vid, ok := StaticActorVisibilityVID(actor); ok && d.conflictingVisibilityVID(vid, actor.Entity.ID) {
2626
return false
2727
}
2828
d.removeVisibilityVIDsForEntityID(actor.Entity.ID)
@@ -65,7 +65,7 @@ func (d *NonPlayerDirectory) Update(actor StaticEntity) bool {
6565
if _, ok := d.byEntityID[actor.Entity.ID]; !ok {
6666
return false
6767
}
68-
if vid, ok := StaticActorVisibilityVID(actor); ok && conflictingEntityID(d.entityIDByVID, vid, actor.Entity.ID) {
68+
if vid, ok := StaticActorVisibilityVID(actor); ok && d.conflictingVisibilityVID(vid, actor.Entity.ID) {
6969
return false
7070
}
7171
d.removeVisibilityVIDsForEntityID(actor.Entity.ID)
@@ -100,6 +100,18 @@ func (d *NonPlayerDirectory) removeVisibilityVIDsForEntityID(entityID uint64) {
100100
}
101101
}
102102

103+
func (d *NonPlayerDirectory) conflictingVisibilityVID(vid uint32, entityID uint64) bool {
104+
indexedEntityID, ok := d.entityIDByVID[vid]
105+
if !ok || indexedEntityID == entityID {
106+
return false
107+
}
108+
if _, exists := d.byEntityID[indexedEntityID]; exists {
109+
return true
110+
}
111+
delete(d.entityIDByVID, vid)
112+
return false
113+
}
114+
103115
func (d *NonPlayerDirectory) StaticActors() []StaticEntity {
104116
if d == nil || len(d.byEntityID) == 0 {
105117
return nil

internal/worldruntime/non_player_directory_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,47 @@ func TestNonPlayerDirectoryLookupPrunesStaleVisibilityVID(t *testing.T) {
8080
}
8181
}
8282

83+
func TestNonPlayerDirectoryRegisterPrunesOrphanedVisibilityVIDConflict(t *testing.T) {
84+
directory := NewNonPlayerDirectory()
85+
directory.entityIDByVID[7] = 999
86+
87+
actor := StaticEntity{
88+
Entity: Entity{ID: 7, Kind: EntityKindStaticActor, Name: "VillageGuard"},
89+
Position: NewPosition(42, 1700, 2800),
90+
RaceNum: 20300,
91+
}
92+
if !directory.Register(actor) {
93+
t.Fatal("expected static actor registration to prune orphaned VID conflict")
94+
}
95+
lookup, ok := directory.ByVID(7)
96+
if !ok || lookup.Entity.ID != actor.Entity.ID || lookup.Entity.Name != actor.Entity.Name {
97+
t.Fatalf("expected current visibility VID lookup after orphan prune, got actor=%+v ok=%v", lookup, ok)
98+
}
99+
}
100+
101+
func TestNonPlayerDirectoryUpdatePrunesOrphanedVisibilityVIDConflict(t *testing.T) {
102+
directory := NewNonPlayerDirectory()
103+
actor := StaticEntity{
104+
Entity: Entity{ID: 7, Kind: EntityKindStaticActor, Name: "VillageGuard"},
105+
Position: NewPosition(42, 1700, 2800),
106+
RaceNum: uint32(^uint16(0)) + 1,
107+
}
108+
if !directory.Register(actor) {
109+
t.Fatal("expected non-encodable static actor registration to succeed")
110+
}
111+
directory.entityIDByVID[7] = 999
112+
113+
updated := actor
114+
updated.RaceNum = 20300
115+
if !directory.Update(updated) {
116+
t.Fatal("expected static actor update to prune orphaned VID conflict")
117+
}
118+
lookup, ok := directory.ByVID(7)
119+
if !ok || lookup.Entity.ID != actor.Entity.ID || lookup.RaceNum != 20300 {
120+
t.Fatalf("expected current visibility VID lookup after update orphan prune, got actor=%+v ok=%v", lookup, ok)
121+
}
122+
}
123+
83124
func TestNonPlayerDirectoryUpdateReplacesStaticActorByEntityID(t *testing.T) {
84125
directory := NewNonPlayerDirectory()
85126
actor := StaticEntity{

spec/protocol/non-player-entity-bootstrap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ This slice does not yet freeze:
102102
- if a bad partial-teardown or repair path leaves duplicate map-bucket presence for the same static actor entity ID, later register, in-place update, or remove must prune all stale map buckets before inserting the new effective-map presence or reporting final removal; map occupancy must never keep ghost static actors for the same entity ID on older maps
103103
- if a stale visibility-VID index entry survives for the same static actor entity ID, later registration or in-place update must prune the stale VID before inserting the actor's current encodable visibility VID; removal must prune every visibility VID alias for the removed entity ID; visible-VID lookup must never keep an old alias for the same static actor after registration, update repair, or removal
104104
- if a stale visibility-VID index entry points at a missing static actor entity ID, a later `ByVID` lookup must prune that orphaned index entry before failing closed; visible-VID lookup must not keep returning through stale non-player directory aliases after partial teardown
105+
- if that orphaned visibility-VID entry would otherwise conflict with later registration or in-place update of the real actor for that VID, the non-player directory must prune the orphan and allow the current actor to claim the VID; only live conflicting actor ownership should fail closed
105106
- inter-channel ownership
106107

107108
## Success definition for the next slice

0 commit comments

Comments
 (0)