Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions Core/GDCore/IDE/Project/EventsBasedObjectDependencyFinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/
#include "EventsBasedObjectDependencyFinder.h"

#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Project/EventsBasedObject.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"

Expand Down Expand Up @@ -38,4 +40,69 @@ bool EventsBasedObjectDependencyFinder::IsDependentFromEventsBasedObject(
}
return false;
}

std::vector<gd::String>
EventsBasedObjectDependencyFinder::GetExtensionDependencyCycleCreatedByObject(
const gd::Project &project, const gd::String &parentExtensionName,
const gd::String &objectType) {
const gd::String usedExtensionName =
gd::PlatformExtension::GetExtensionFromFullObjectType(objectType);
if (usedExtensionName.empty() || usedExtensionName == parentExtensionName ||
!project.HasEventsFunctionsExtensionNamed(usedExtensionName)) {
// Objects from the platform (or from the same extension) can't create
// a cycle between extensions.
return {};
}

std::set<gd::String> visitedExtensionNames;
std::vector<gd::String> path;
if (!FindExtensionDependencyPath(project, usedExtensionName,
parentExtensionName, visitedExtensionNames,
path)) {
return {};
}
std::vector<gd::String> cycle;
cycle.push_back(parentExtensionName);
cycle.insert(cycle.end(), path.begin(), path.end());
return cycle;
}

bool EventsBasedObjectDependencyFinder::FindExtensionDependencyPath(
const gd::Project &project, const gd::String &fromExtensionName,
const gd::String &toExtensionName,
std::set<gd::String> &visitedExtensionNames, std::vector<gd::String> &path) {
if (fromExtensionName == toExtensionName) {
path.push_back(fromExtensionName);
return true;
}
if (visitedExtensionNames.find(fromExtensionName) !=
visitedExtensionNames.end()) {
return false;
}
visitedExtensionNames.insert(fromExtensionName);

const auto &extension =
project.GetEventsFunctionsExtension(fromExtensionName);
path.push_back(fromExtensionName);
for (auto &eventsBasedObject :
extension.GetEventsBasedObjects().GetInternalVector()) {
for (auto &object : eventsBasedObject->GetObjects().GetObjects()) {
const gd::String childExtensionName =
gd::PlatformExtension::GetExtensionFromFullObjectType(
object->GetType());
if (childExtensionName.empty() ||
childExtensionName == fromExtensionName ||
!project.HasEventsFunctionsExtensionNamed(childExtensionName)) {
continue;
}
if (FindExtensionDependencyPath(project, childExtensionName,
toExtensionName, visitedExtensionNames,
path)) {
return true;
}
}
}
path.pop_back();
return false;
}
} // namespace gd
32 changes: 31 additions & 1 deletion Core/GDCore/IDE/Project/EventsBasedObjectDependencyFinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

#pragma once

#include <set>
#include <vector>

#include "GDCore/String.h"

namespace gd {
Expand All @@ -16,7 +19,8 @@ class EventsBasedObject;
namespace gd {

/**
* \brief Find resource usages in several parts of the project.
* \brief Find dependencies between events-based objects and between
* the extensions declaring them.
*
* \ingroup IDE
*/
Expand All @@ -27,11 +31,37 @@ class EventsBasedObjectDependencyFinder {
const gd::EventsBasedObject &eventsBasedObject,
const gd::EventsBasedObject &dependency);

/**
* \brief Check if adding an object of the given type as a child of an
* events-based object of the given extension would create a dependency
* cycle between events functions extensions.
*
* Extensions are unserialized in dependency order when the project is
* opened (see `Project::GetUnserializingOrderExtensionNames`), and
* extensions forming a cycle can't be ordered: their entire content would
* be skipped at loading (and erased for good at the next save).
*
* \return the extension names forming the cycle, starting and ending with
* the given extension (e.g: ["A", "B", "A"]), or an empty vector if no
* cycle would be created.
*/
static std::vector<gd::String> GetExtensionDependencyCycleCreatedByObject(
const gd::Project &project,
const gd::String &parentExtensionName,
const gd::String &objectType);

private:
static bool IsDependentFromEventsBasedObject(
const gd::Project &project,
const gd::EventsBasedObject &eventsBasedObject,
const gd::EventsBasedObject &dependency, int depth);

static bool FindExtensionDependencyPath(
const gd::Project &project,
const gd::String &fromExtensionName,
const gd::String &toExtensionName,
std::set<gd::String> &visitedExtensionNames,
std::vector<gd::String> &path);
};

} // namespace gd
199 changes: 199 additions & 0 deletions Core/tests/EventsBasedObjectDependencyFinder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* GDevelop Core
* Copyright 2008-2026 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering dependencies between events-based objects and
* between the extensions declaring them.
*/
#include "GDCore/IDE/Project/EventsBasedObjectDependencyFinder.h"

#include <vector>

#include "DummyPlatform.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/EventsBasedObject.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Project.h"
#include "catch.hpp"

namespace {

gd::EventsBasedObject &
InsertEventsBasedObject(gd::Project &project, const gd::String &extensionName,
const gd::String &objectName) {
auto &extension =
project.HasEventsFunctionsExtensionNamed(extensionName)
? project.GetEventsFunctionsExtension(extensionName)
: project.InsertNewEventsFunctionsExtension(
extensionName, project.GetEventsFunctionsExtensionsCount());
return extension.GetEventsBasedObjects().InsertNew(
objectName, extension.GetEventsBasedObjects().GetCount());
}

void AddChildObject(gd::Project &project,
gd::EventsBasedObject &eventsBasedObject,
const gd::String &childObjectType) {
auto &childObjects = eventsBasedObject.GetObjects();
childObjects.InsertNewObject(
project, childObjectType,
"Child" + gd::String::From(childObjects.GetObjectsCount()),
childObjects.GetObjectsCount());
}

} // namespace

TEST_CASE("EventsBasedObjectDependencyFinder", "[common]") {

SECTION("GetExtensionDependencyCycleCreatedByObject") {
SECTION("Allows an object from the platform") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
InsertEventsBasedObject(project, "A", "ObjectA");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(
project, "A", "MyExtension::Sprite")
.empty());
}

SECTION("Allows an object from the same extension") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
InsertEventsBasedObject(project, "A", "ObjectA");
InsertEventsBasedObject(project, "A", "ObjectA2");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(project, "A",
"A::ObjectA2")
.empty());
}

SECTION("Allows an object from an extension without dependencies") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
InsertEventsBasedObject(project, "A", "ObjectA");
InsertEventsBasedObject(project, "B", "ObjectB");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(project, "A",
"B::ObjectB")
.empty());
}

SECTION("Allows an object creating a one-way dependency") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &objectA = InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectB = InsertEventsBasedObject(project, "B", "ObjectB");
InsertEventsBasedObject(project, "C", "ObjectC");

// B already depends on A.
AddChildObject(project, objectB, "A::ObjectA");

// A new dependency of C on A is one-way: no cycle.
REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(project, "C",
"A::ObjectA")
.empty());
}

SECTION("Forbids an object creating a direct cycle between 2 extensions") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectB = InsertEventsBasedObject(project, "B", "ObjectB");

// B depends on A.
AddChildObject(project, objectB, "A::ObjectA");

// Making A depend on B would create a cycle.
REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(project, "A",
"B::ObjectB") ==
std::vector<gd::String>({"A", "B", "A"}));
}

SECTION(
"Forbids an object creating a cycle through an intermediate "
"extension") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectB = InsertEventsBasedObject(project, "B", "ObjectB");
auto &objectC = InsertEventsBasedObject(project, "C", "ObjectC");

// B depends on C, which depends on A.
AddChildObject(project, objectB, "C::ObjectC");
AddChildObject(project, objectC, "A::ObjectA");

// Making A depend on B would create a cycle going through C.
REQUIRE(gd::EventsBasedObjectDependencyFinder::
GetExtensionDependencyCycleCreatedByObject(project, "A",
"B::ObjectB") ==
std::vector<gd::String>({"A", "B", "C", "A"}));
}
}

SECTION("IsDependentFromEventsBasedObject") {
SECTION("An object is dependent on itself") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &objectA = InsertEventsBasedObject(project, "A", "ObjectA");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
IsDependentFromEventsBasedObject(project, objectA,
objectA) == true);
}

SECTION("An object is dependent on a direct child") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &objectA = InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectA2 = InsertEventsBasedObject(project, "A", "ObjectA2");
AddChildObject(project, objectA2, "A::ObjectA");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
IsDependentFromEventsBasedObject(project, objectA2,
objectA) == true);
}

SECTION("An object is dependent on an indirect child") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &objectA = InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectB = InsertEventsBasedObject(project, "B", "ObjectB");
auto &objectC = InsertEventsBasedObject(project, "C", "ObjectC");
AddChildObject(project, objectB, "C::ObjectC");
AddChildObject(project, objectC, "A::ObjectA");

REQUIRE(gd::EventsBasedObjectDependencyFinder::
IsDependentFromEventsBasedObject(project, objectB,
objectA) == true);
}

SECTION("An object is not dependent on an object it doesn't contain") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &objectA = InsertEventsBasedObject(project, "A", "ObjectA");
auto &objectB = InsertEventsBasedObject(project, "B", "ObjectB");
AddChildObject(project, objectB, "A::ObjectA");

// ObjectB contains ObjectA, but not the reverse.
REQUIRE(gd::EventsBasedObjectDependencyFinder::
IsDependentFromEventsBasedObject(project, objectA,
objectB) == false);
}
}
}
4 changes: 4 additions & 0 deletions GDevelop.js/Bindings/Bindings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -3063,6 +3063,10 @@ interface EventsBasedObjectDependencyFinder {
[Const, Ref] Project project,
[Const, Ref] EventsBasedObject eventsBasedObject,
[Const, Ref] EventsBasedObject dependency);
[Value] VectorString STATIC_GetExtensionDependencyCycleCreatedByObject(
[Const, Ref] Project project,
[Const] DOMString parentExtensionName,
[Const] DOMString objectType);
};

interface PropertyFunctionGenerator {
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/Bindings/Wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,8 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_GetSwitchableVariableInstructionIdentifier GetSwitchableVariableInstructionIdentifier
#define STATIC_GetSwitchableInstructionVariableType GetSwitchableInstructionVariableType
#define STATIC_IsDependentFromEventsBasedObject IsDependentFromEventsBasedObject
#define STATIC_GetExtensionDependencyCycleCreatedByObject \
GetExtensionDependencyCycleCreatedByObject

#define STATIC_IsFreeFunctionOnlyCallingItself IsFreeFunctionOnlyCallingItself
#define STATIC_IsBehaviorFunctionOnlyCallingItself \
Expand Down
1 change: 1 addition & 0 deletions GDevelop.js/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,7 @@ export class ObjectTools extends EmscriptenObject {

export class EventsBasedObjectDependencyFinder extends EmscriptenObject {
static isDependentFromEventsBasedObject(project: Project, eventsBasedObject: EventsBasedObject, dependency: EventsBasedObject): boolean;
static getExtensionDependencyCycleCreatedByObject(project: Project, parentExtensionName: string, objectType: string): VectorString;
}

export class PropertyFunctionGenerator extends EmscriptenObject {
Expand Down
1 change: 1 addition & 0 deletions GDevelop.js/types/gdeventsbasedobjectdependencyfinder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdEventsBasedObjectDependencyFinder {
static isDependentFromEventsBasedObject(project: gdProject, eventsBasedObject: gdEventsBasedObject, dependency: gdEventsBasedObject): boolean;
static getExtensionDependencyCycleCreatedByObject(project: gdProject, parentExtensionName: string, objectType: string): gdVectorString;
delete(): void;
ptr: number;
};
Loading
Loading