package companion import "sync" type RunKey struct { Instance string SessionGeneration uint64 OwnerCharacterID string LifecycleToken uint64 SpawnSequence uint64 Nonce string } func (k RunKey) valid() bool { return validIdentity(k.Instance) && k.SessionGeneration > 0 && validCharacterID(k.OwnerCharacterID) && k.LifecycleToken > 0 && k.SpawnSequence > 0 && validHash(k.Nonce) } type Phase string const ( Prepared Phase = "prepared" Ready Phase = "room_ready" Created Phase = "spawned_inactive" Bound Phase = "owner_bound" Active Phase = "active" Transitioning Phase = "transitioning" Dead Phase = "dead" Retiring Phase = "retiring" Disposed Phase = "disposed" Skipped Phase = "skipped" ) type Event struct { Run RunKey Sequence uint64 Kind string RoomGeneration uint64 ObjectKey uint16 ObjectGeneration uint64 } type Status struct { Run RunKey Phase Phase ConfigRevision uint64 SnapshotHash string LastSequence uint64 RoomGeneration uint64 ObjectKey uint16 ObjectGeneration uint64 OwnerBound bool } // Instance is an isolated coordinator, not a client message handler. // Callers must authenticate events and hold the real runtime/object admission // boundary. An object-confirmation event is never proof of native creation by // itself. No network endpoint or game callback currently invokes this type. type Instance struct { mu sync.Mutex status Status snapshot Snapshot last Event playerObjectKey uint16 } func NewInstance(key RunKey, snapshot Snapshot, configRevision uint64, playerObjectKey uint16) (*Instance, error) { if !key.valid() || playerObjectKey == 0 || playerObjectKey == 65535 { return nil, ErrStaleRun } if snapshot.SchemaVersion != 1 || snapshot.OwnerCharacterID != key.OwnerCharacterID || !validCharacterID(snapshot.SourceCharacterID) || snapshot.SourceCharacterID == key.OwnerCharacterID || snapshot.ProfileID == "" || !validHash(snapshot.ClientSHA256) || !validHash(snapshot.PVFSHA256) { return nil, ErrSnapshot } frozen := snapshot.Clone() return &Instance{status: Status{Run: key, Phase: Prepared, ConfigRevision: configRevision, SnapshotHash: frozen.Fingerprint()}, snapshot: frozen, playerObjectKey: playerObjectKey}, nil } func (i *Instance) Status() Status { i.mu.Lock(); defer i.mu.Unlock(); return i.status } func (i *Instance) Snapshot() Snapshot { i.mu.Lock(); defer i.mu.Unlock(); return i.snapshot.Clone() } // occupied contains other actors only, freshly inspected by the future bridge. // nil means collision evidence missing and rejects a first native creation. // Successful repeated messages are no-ops; a reused sequence with new content // is an error. No transition can create another APC or respawn a dead one. func (i *Instance) Apply(e Event, occupied map[uint16]bool) error { i.mu.Lock() defer i.mu.Unlock() s := i.status if e.Run != s.Run { return ErrStaleRun } if e.Sequence == 0 { return ErrTransition } if e.Sequence == s.LastSequence && e == i.last { return nil } if e.Sequence <= s.LastSequence { return ErrTransition } switch e.Kind { case "room_ready": if s.Phase != Prepared && s.Phase != Transitioning { return ErrTransition } if e.RoomGeneration <= s.RoomGeneration { return ErrRoom } s.RoomGeneration = e.RoomGeneration if s.Phase == Prepared { s.Phase = Ready } else { s.Phase = Active } case "native_created": if (s.Phase != Ready && s.Phase != Retiring) || e.RoomGeneration != s.RoomGeneration { return ErrRoom } // A native create may finish after cancellation. Track it for exact // cleanup, but never authorize it or replace an existing actor. if s.Phase == Retiring && s.ObjectKey != 0 { return ErrCollision } if occupied == nil || e.ObjectKey == 0 || e.ObjectKey == 65535 || e.ObjectKey == i.playerObjectKey || occupied[e.ObjectKey] || e.ObjectGeneration == 0 { return ErrCollision } s.ObjectKey = e.ObjectKey s.ObjectGeneration = e.ObjectGeneration if s.Phase == Ready { s.Phase = Created } case "owner_bound", "activate": if e.ObjectKey != s.ObjectKey || e.ObjectGeneration != s.ObjectGeneration || e.RoomGeneration != s.RoomGeneration { return ErrCollision } if e.Kind == "owner_bound" { if s.Phase != Created { return ErrTransition } s.Phase = Bound s.OwnerBound = true } else { if s.Phase != Bound { return ErrTransition } s.Phase = Active } case "room_leaving": if s.Phase != Active || e.RoomGeneration != s.RoomGeneration { return ErrTransition } s.Phase = Transitioning case "dead": if s.Phase != Active && s.Phase != Transitioning { return ErrTransition } if e.ObjectKey != s.ObjectKey || e.ObjectGeneration != s.ObjectGeneration { return ErrCollision } s.Phase = Dead case "retire": if s.Phase == Disposed || s.Phase == Skipped { return ErrTransition } s.Phase = Retiring case "disposed": if s.Phase != Retiring { return ErrTransition } // The bridge must also prove the native operation/callbacks drained; // a missing room manager is not proof that the actor was destroyed. if e.ObjectKey != s.ObjectKey || e.ObjectGeneration != s.ObjectGeneration { return ErrCollision } if e.RoomGeneration != s.RoomGeneration { return ErrRoom } s.Phase = Disposed s.OwnerBound = false case "skip": if s.Phase != Prepared && s.Phase != Ready { return ErrTransition } s.Phase = Skipped default: return ErrTransition } s.LastSequence = e.Sequence i.status = s i.last = e return nil } // Retiring/dead owners remain valid for already-in-flight combat. The bridge // must validate targets and safely drain reports before reporting disposed. // Object generation is internal evidence; no existing game wire is changed. func (i *Instance) Owns(run RunKey, objectKey uint16, generation uint64) bool { i.mu.Lock() defer i.mu.Unlock() s := i.status return run == s.Run && s.OwnerBound && objectKey != 0 && objectKey == s.ObjectKey && generation == s.ObjectGeneration }