Files
2026-04-08 18:20:16 +02:00

447 lines
16 KiB
TypeScript

/*
GDevelop - LinkedObjects Extension
Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('LinkedObjects');
/**
* Manages the links between objects.
* @category Other extensions > Linked Objects
*/
export class LinksManager {
private _links = new Map<integer, IterableLinkedObjects>();
/**
* Get the links manager of a scene.
*/
static getManager(
instanceContainer: gdjs.RuntimeInstanceContainer
): gdjs.LinksManager {
// @ts-ignore
if (!instanceContainer.linkedObjectsManager) {
//Create the shared manager if necessary.
// @ts-ignore
instanceContainer.linkedObjectsManager = new gdjs.LinksManager();
}
// @ts-ignore
return instanceContainer.linkedObjectsManager;
}
/**
* This function is for internal use and could disappear in next versions.
* Prefer using:
* * {@link LinksManager.getObjectsLinkedWithAndNamed}
* * {@link LinksManager.getObjectsLinkedWith}
* * {@link evtTools.linkedObjects.quickPickObjectsLinkedTo}
*
* @param objA
* @returns the linked objects by name
*/
_getMapOfObjectsLinkedWith(
objA: gdjs.RuntimeObject
): Map<string, gdjs.RuntimeObject[]> {
if (!this._links.has(objA.id)) {
this._links.set(objA.id, new IterableLinkedObjects(objA));
}
return this._links.get(objA.id)!.linkedObjectMap;
}
// These 2 following functions give JS extensions an implementation dependent access to links.
/**
* @returns an iterable on every object linked with objA.
*/
getObjectsLinkedWith(
objA: gdjs.RuntimeObject
): Iterable<gdjs.RuntimeObject> {
if (!this._links.has(objA.id)) {
this._links.set(objA.id, new IterableLinkedObjects(objA));
}
return this._links.get(objA.id)!;
}
/**
* @returns an iterable of the objects with the given name that are linked with objA.
*/
getObjectsLinkedWithAndNamed(
objA: gdjs.RuntimeObject,
objectName: string
): Iterable<gdjs.RuntimeObject> {
let objects = this._getMapOfObjectsLinkedWith(objA).get(objectName);
if (!objects) {
// Give an empty Array
objects = gdjs.staticArray(
LinksManager.prototype.getObjectsLinkedWithAndNamed
);
}
return objects;
}
linkObjects(objA: gdjs.RuntimeObject, objB: gdjs.RuntimeObject) {
const objALinkedObjectMap = this._getMapOfObjectsLinkedWith(objA);
if (!objALinkedObjectMap.has(objB.getName())) {
objALinkedObjectMap.set(objB.getName(), []);
}
const objALinkedObjects = objALinkedObjectMap.get(objB.getName())!;
if (objALinkedObjects.indexOf(objB) === -1) {
objALinkedObjects.push(objB);
}
const objBLinkedObjectMap = this._getMapOfObjectsLinkedWith(objB);
if (!objBLinkedObjectMap.has(objA.getName())) {
objBLinkedObjectMap.set(objA.getName(), []);
}
const objBLinkedObjects = objBLinkedObjectMap.get(objA.getName())!;
if (objBLinkedObjects.indexOf(objA) === -1) {
objBLinkedObjects.push(objA);
}
}
removeAllLinksOf(removedObject: gdjs.RuntimeObject) {
// Remove the other side of the links
// Note: don't use `this._getMapOfObjectsLinkedWith` as this would
// create an empty map of linked objects if not existing already.
const links = this._links.get(removedObject.id);
if (!links) {
// No existing links to other objects.
// This also means no links to the object from other objects.
return;
}
for (const linkedObjects of links.linkedObjectMap.values()) {
for (let i = 0; i < linkedObjects.length; i++) {
// This is the object on the other side of the link.
// We find the removed object in its list of linked objects and remove it.
const linkedObject = linkedObjects[i];
if (this._links.has(linkedObject.id)) {
const otherObjList = this._links
.get(linkedObject.id)!
.linkedObjectMap.get(removedObject.getName());
if (!otherObjList) {
logger.error(
`Can't find link from ${linkedObject.id} (${linkedObject.name}) to ${removedObject.id} (${removedObject.name})`
);
return;
}
const index = otherObjList.indexOf(removedObject);
if (index !== -1) {
otherObjList.splice(index, 1);
}
}
}
}
// Remove the links on the removedObject side.
this._links.delete(removedObject.id);
}
removeLinkBetween(objA: gdjs.RuntimeObject, objB: gdjs.RuntimeObject) {
if (this._links.has(objA.id)) {
const map = this._links.get(objA.id)!.linkedObjectMap;
if (map.has(objB.getName())) {
const list = map.get(objB.getName())!;
const index = list.indexOf(objB);
if (index !== -1) {
list.splice(index, 1);
}
}
}
if (this._links.has(objB.id)) {
const map = this._links.get(objB.id)!.linkedObjectMap;
if (map.has(objA.getName())) {
const list = map.get(objA.getName())!;
const index = list.indexOf(objA);
if (index !== -1) {
list.splice(index, 1);
}
}
}
}
clearAllLinks() {
this._links.clear();
}
/**
* Serialize all links between objects that have a networkId,
* so they can be persisted and restored later.
* Each link is stored once as a pair of networkIds.
*/
getNetworkSyncData(): Array<[string, string]> {
const linkedObjects: Array<[string, string]> = [];
const serializedLinks = new Set<string>();
for (const iterableLinkedObjects of this._links.values()) {
const objectA = iterableLinkedObjects.ownerObject;
if (!objectA || !objectA.networkId) {
continue;
}
for (const objectB of iterableLinkedObjects) {
if (!objectB.networkId) continue;
const pairKey =
objectA.networkId < objectB.networkId
? `${objectA.networkId}|${objectB.networkId}`
: `${objectB.networkId}|${objectA.networkId}`;
if (serializedLinks.has(pairKey)) continue;
serializedLinks.add(pairKey);
linkedObjects.push([objectA.networkId, objectB.networkId]);
}
}
return linkedObjects;
}
/**
* Restore links from serialized data. Objects must already exist
* in the scene with their networkId set.
*
* Links for objects managed by the save state should be cleared
* before calling this method, so that stale links are removed.
* This method only adds the saved links back.
*/
updateFromNetworkSyncData(
linksNetworkSyncData: Array<[string, string]>,
runtimeScene: gdjs.RuntimeScene
): void {
if (!linksNetworkSyncData) return;
// Build a map from networkId to object instance for quick lookup.
const objectsByNetworkId = new Map<string, gdjs.RuntimeObject>();
for (const object of runtimeScene.getAdhocListOfAllInstances()) {
if (object.networkId) {
objectsByNetworkId.set(object.networkId, object);
}
}
for (const [networkIdA, networkIdB] of linksNetworkSyncData) {
const objectA = objectsByNetworkId.get(networkIdA);
const objectB = objectsByNetworkId.get(networkIdB);
if (!objectA || !objectB) continue;
this.linkObjects(objectA, objectB);
}
}
}
class IterableLinkedObjects implements Iterable<gdjs.RuntimeObject> {
ownerObject: gdjs.RuntimeObject;
linkedObjectMap: Map<string, gdjs.RuntimeObject[]>;
static emptyItr: Iterator<gdjs.RuntimeObject> = {
next: () => ({ value: undefined, done: true }),
};
constructor(ownerObject: gdjs.RuntimeObject) {
this.ownerObject = ownerObject;
this.linkedObjectMap = new Map<string, gdjs.RuntimeObject[]>();
}
[Symbol.iterator]() {
let mapItr = this.linkedObjectMap.values();
let listItr: Iterator<gdjs.RuntimeObject> =
IterableLinkedObjects.emptyItr;
return {
next: () => {
let listNext = listItr.next();
while (listNext.done) {
const mapNext = mapItr.next();
if (mapNext.done) {
return listNext;
}
listItr = mapNext.value[Symbol.iterator]();
listNext = listItr.next();
}
return listNext;
},
};
}
}
export namespace evtTools {
export namespace linkedObjects {
gdjs.registerObjectDeletedFromSceneCallback(
function (instanceContainer, obj) {
LinksManager.getManager(instanceContainer).removeAllLinksOf(obj);
}
);
gdjs.registerRuntimeSceneGetSyncDataCallback(
function (runtimeScene, currentLayoutSyncData, syncOptions) {
if (!syncOptions.syncLinkedObjects) return;
currentLayoutSyncData.linkedObjects =
LinksManager.getManager(runtimeScene).getNetworkSyncData();
}
);
gdjs.registerRuntimeSceneUpdateFromSyncDataCallback(
function (runtimeScene, receivedSyncData, _syncOptions) {
if (!receivedSyncData.linkedObjects) return;
const linksManager = LinksManager.getManager(runtimeScene);
// Clear links only for objects with a networkId (managed by save state).
// DoNotSave objects don't have networkIds, so their links are preserved.
for (const object of runtimeScene.getAdhocListOfAllInstances()) {
if (object.networkId) {
linksManager.removeAllLinksOf(object);
}
}
linksManager.updateFromNetworkSyncData(
receivedSyncData.linkedObjects,
runtimeScene
);
}
);
export const linkObjects = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
objA: gdjs.RuntimeObject | null,
objB: gdjs.RuntimeObject | null
) {
if (objA === null || objB === null) {
return;
}
LinksManager.getManager(instanceContainer).linkObjects(objA, objB);
};
export const removeLinkBetween = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
objA: gdjs.RuntimeObject | null,
objB: gdjs.RuntimeObject | null
) {
if (objA === null || objB === null) {
return;
}
LinksManager.getManager(instanceContainer).removeLinkBetween(
objA,
objB
);
};
export const removeAllLinksOf = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
objA: gdjs.RuntimeObject
) {
if (objA === null) {
return;
}
LinksManager.getManager(instanceContainer).removeAllLinksOf(objA);
};
export const pickObjectsLinkedTo = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
objectsLists: Hashtable<gdjs.RuntimeObject[]>,
obj: gdjs.RuntimeObject | null,
eventsFunctionContext: EventsFunctionContext | null | undefined
) {
if (obj === null) {
return false;
}
const linkedObjectMap =
LinksManager.getManager(instanceContainer)._getMapOfObjectsLinkedWith(
obj
);
let pickedSomething = false;
for (const contextObjectName in objectsLists.items) {
if (objectsLists.containsKey(contextObjectName)) {
const parentEventPickedObjects =
objectsLists.items[contextObjectName];
if (parentEventPickedObjects.length === 0) {
continue;
}
// Find the object names in the scene
const parentEventPickedObjectNames = gdjs.staticArray2(
gdjs.evtTools.linkedObjects.pickObjectsLinkedTo
);
parentEventPickedObjectNames.length = 0;
if (eventsFunctionContext) {
// For functions, objects lists may contain objects with different names
// indexed not by their name, but by the parameter name representing them.
// This means that each object can have a different name,
// so we iterate on them to get all the names.
for (const pickedObject of parentEventPickedObjects) {
if (
parentEventPickedObjectNames.indexOf(pickedObject.getName()) <
0
) {
parentEventPickedObjectNames.push(pickedObject.getName());
}
}
} else {
// In the case of a scene, the list of objects are guaranteed
// to be indexed by the object name (no mix of objects with
// different names in a list).
parentEventPickedObjectNames.push(contextObjectName);
}
// Sum the number of instances in the scene for each objects found
// previously in parentEventPickedObjects, so that we know if we can
// avoid running an intersection with the picked objects later.
let objectCount = 0;
for (const objectName of parentEventPickedObjectNames) {
objectCount += instanceContainer.getObjects(objectName)!.length;
}
if (parentEventPickedObjects.length === objectCount) {
// The parent event didn't make any selection on the current object,
// (because the number of picked objects is the total object count on the scene).
// There is no need to make an intersection.
// We will only replace the picked list with the linked object list.
parentEventPickedObjects.length = 0;
for (const objectName of parentEventPickedObjectNames) {
if (linkedObjectMap.has(objectName)) {
const linkedObjects = linkedObjectMap.get(objectName)!;
pickedSomething = pickedSomething || linkedObjects.length > 0;
parentEventPickedObjects.push.apply(
parentEventPickedObjects,
linkedObjects
);
}
}
} else {
// Run an intersection between objects picked by parent events
// and the linked ones.
const pickedAndLinkedObjects = gdjs.staticArray(
gdjs.evtTools.linkedObjects.pickObjectsLinkedTo
);
pickedAndLinkedObjects.length = 0;
for (const objectName of parentEventPickedObjectNames) {
if (linkedObjectMap.has(objectName)) {
const linkedObjects = linkedObjectMap.get(objectName)!;
for (const otherObject of linkedObjects) {
if (parentEventPickedObjects.indexOf(otherObject) >= 0) {
pickedAndLinkedObjects.push(otherObject);
}
}
}
}
pickedSomething =
pickedSomething || pickedAndLinkedObjects.length > 0;
parentEventPickedObjects.length = 0;
parentEventPickedObjects.push.apply(
parentEventPickedObjects,
pickedAndLinkedObjects
);
pickedAndLinkedObjects.length = 0;
}
parentEventPickedObjectNames.length = 0;
}
}
return pickedSomething;
};
}
}
}