2025-10-12 18:03:20 +02:00
|
|
|
import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js";
|
|
|
|
|
import { guidGenerator } from "../core/util.js";
|
|
|
|
|
let instanceCounter = 0;
|
|
|
|
|
|
|
|
|
|
export class Instance {
|
|
|
|
|
constructor(className = "Instance") {
|
|
|
|
|
this.ClassName = className;
|
2025-10-23 20:58:19 +02:00
|
|
|
this.Name = `${className}`;
|
2025-10-12 18:03:20 +02:00
|
|
|
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
|
2025-10-23 20:58:19 +02:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-10-12 18:03:20 +02:00
|
|
|
}
|