Files
bo3-js/js/instances/Instance.js
usernames122 d9d5460856 Add launchable
2025-10-23 20:58:19 +02:00

141 lines
4.3 KiB
JavaScript

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;
}
}