import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js"; import { guidGenerator } from "../core/util.js"; let instanceCounter = 0; export class Instance { constructor(className = "Instance") { this.ClassName = className; this.Name = `${className}`; this.Parent = null; this.Children = []; this.InstanceId = guidGenerator(); // Unique identifier, used for replication // Signals this.AncestryChanged = new BO3ScriptSignal(); this.Changed = new BO3ScriptSignal(); } // Set a property dynamically SetProperty(prop, value) { this[prop] = value; this.Changed.Fire(prop, value); } GetDataModel() { // Recursively calls itself on parent until DataModel is found, since DataModel overrides this method to return itself if (this.Parent) return this.Parent.GetDataModel(); } FindInstanceRecursively(instanceId) { if (this.InstanceId === instanceId) return this; if (this.Children) { for (const child of this.Children) { if (typeof child.FindInstanceRecursively === 'function') { const found = child.FindInstanceRecursively(instanceId); if (found) return found; } } } return null; } OnReplicated() { // Hook for when instance is replicated to client. Override in subclasses. } // Parent management SetParent(newParent) { if (this.Parent === newParent) return; // Remove from old parent if (this.Parent) { const oldChildren = this.Parent.Children; this.Parent.Children = oldChildren.filter(c => c !== this); } const oldParent = this.Parent; this.Parent = newParent; // Add to new parent if (newParent) { newParent.Children.push(this); } try { this.AncestryChanged.Fire(this, oldParent); } catch { console.warn("AncestryChanged signal error! May be caused by ReplicatorService during replication."); } if (oldParent === null && newParent !== null && this.OnAddedToScene) { // If added to scene (i.e. RenderService), call hook setTimeout(() => { this.OnAddedToScene(newParent); }, 0); // Defer to avoid issues during replication } } // Utility: find first child with name FindFirstChild(name) { return this.Children.find(c => c.Name === name) || null; } // Utility: create deep clone Clone() { const clone = new Instance(this.ClassName); for (const key of Object.keys(this)) { if (key !== "Parent" && key !== "Children") { clone[key] = structuredClone(this[key]); } } for (const child of this.Children) { const childClone = child.Clone(); childClone.SetParent(clone); } return clone; } Destroy() { // Remove from parent const dm = this.GetDataModel(); // Get DataModel for replication service, BEFORE removing parent. otherwise can't find it. this.SetParent(null); // Recursively destroy children for (const child of [...this.Children]) { if (typeof child.Destroy === 'function') { child.Destroy(); } else { child.SetParent(null); } } // Clear signals this.AncestryChanged.DisconnectAll(); this.Changed.DisconnectAll(); // Request replicator to remove this instance if needed if (dm) { const replicator = dm.GetService("ReplicatorService"); if (replicator) { replicator.requestReplicateDestroy(this); } else { console.warn("Destroy: No ReplicatorService found in DataModel."); } } else { console.warn("Destroy: No DataModel found in ancestry. Are you destroying an unparented instance?"); } } // Garbage collected after this LuaBridge(exclude, visited = new Set()) { // Convert to a plain object for LuaBridge. Should be overridden in subclasses to add properties. if (visited.has(this.InstanceId)) { // Already visited, avoid circular reference return null; } visited.add(this.InstanceId); const obj = { ClassName: this.ClassName, Name: this.Name, InstanceId: this.InstanceId, Properties: {}, Children: this.Children.filter(c => c !== exclude).map(c => c.LuaBridge(this, visited)).filter(Boolean), Parent: this.Parent && this.Parent !== exclude ? this.Parent.LuaBridge(this, visited) : null // Avoid circular reference by excluding self }; return obj; } }