mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 09:35:27 -04:00
88d63666a5
Fix #8497
447 lines
16 KiB
TypeScript
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;
|
|
};
|
|
}
|
|
}
|
|
}
|