Skip to content

Commit d78e8f0

Browse files
committed
Add orderBy, order, and limit support to ForEach event
Adds optional ordering and limiting to ForEach events: - orderBy: an expression evaluated for each instance to determine sort order - order: "asc" (default) or "desc" direction for iteration - limit: optional expression for max iterations (e.g., limit=1 picks highest/lowest) Changes: - Core C++ (ForEachEvent.h/.cpp): new fields with serialization - GDevelop.js bindings: expose getters/setters for orderBy, order, limit - GDJS codegen: sorting phase evaluates expression per instance, sorts indices, applies limit, then iterates in order. Unordered path unchanged. - UI (ForEachEvent.js): select between "(any order)" and "ordered by" with expression field, asc/desc selector, and optional limit - TextRenderer: display ordering info in text representation - Tests: 12 integration tests covering simple/nested ForEach, orderBy with asc/desc, limits (1, 2, none), local variables, loop index, and groups https://claude.ai/code/session_01LXTeNz7ehNS5TqFYSU6aw5
1 parent ec5dc89 commit d78e8f0

File tree

10 files changed

+1430
-78
lines changed

10 files changed

+1430
-78
lines changed

Core/GDCore/Events/Builtin/ForEachEvent.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ namespace gd {
1616
ForEachEvent::ForEachEvent()
1717
: BaseEvent(),
1818
objectsToPick(""),
19+
orderBy(""),
20+
order("asc"),
21+
limit(""),
1922
variables(gd::VariablesContainer::SourceType::Local) {}
2023

2124
vector<gd::InstructionsList*> ForEachEvent::GetAllConditionsVectors() {
@@ -85,6 +88,13 @@ void ForEachEvent::SerializeTo(SerializerElement& element) const {
8588
if (!loopIndexVariableName.empty()) {
8689
element.AddChild("loopIndexVariable").SetStringValue(loopIndexVariableName);
8790
}
91+
if (!orderBy.GetPlainString().empty()) {
92+
element.AddChild("orderBy").SetValue(orderBy.GetPlainString());
93+
element.AddChild("order").SetStringValue(order);
94+
if (!limit.GetPlainString().empty()) {
95+
element.AddChild("limit").SetValue(limit.GetPlainString());
96+
}
97+
}
8898
}
8999

90100
void ForEachEvent::UnserializeFrom(gd::Project& project,
@@ -111,6 +121,16 @@ void ForEachEvent::UnserializeFrom(gd::Project& project,
111121
element.HasChild("loopIndexVariable")
112122
? element.GetChild("loopIndexVariable").GetStringValue()
113123
: "";
124+
125+
orderBy = element.HasChild("orderBy")
126+
? gd::Expression(element.GetChild("orderBy").GetValue().GetString())
127+
: gd::Expression("");
128+
order = element.HasChild("order")
129+
? element.GetChild("order").GetStringValue()
130+
: "asc";
131+
limit = element.HasChild("limit")
132+
? gd::Expression(element.GetChild("limit").GetValue().GetString())
133+
: gd::Expression("");
114134
}
115135

116136
} // namespace gd

Core/GDCore/Events/Builtin/ForEachEvent.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
5959
const gd::String& GetLoopIndexVariableName() const { return loopIndexVariableName; }
6060
void SetLoopIndexVariableName(const gd::String& name) { loopIndexVariableName = name; }
6161

62+
const gd::String& GetOrderBy() const {
63+
return orderBy.GetPlainString();
64+
};
65+
void SetOrderBy(gd::String orderBy_) {
66+
orderBy = gd::Expression(orderBy_);
67+
};
68+
69+
const gd::String& GetOrder() const { return order; }
70+
void SetOrder(const gd::String& order_) { order = order_; }
71+
72+
const gd::String& GetLimit() const {
73+
return limit.GetPlainString();
74+
};
75+
void SetLimit(gd::String limit_) {
76+
limit = gd::Expression(limit_);
77+
};
78+
6279
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors()
6380
const;
6481
virtual std::vector<const gd::InstructionsList*> GetAllActionsVectors() const;
@@ -81,6 +98,9 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
8198
gd::EventsList events;
8299
VariablesContainer variables;
83100
gd::String loopIndexVariableName;
101+
gd::Expression orderBy;
102+
gd::String order;
103+
gd::Expression limit;
84104
};
85105

86106
} // namespace gd

GDJS/GDJS/Extensions/Builtin/CommonInstructionsExtension.cpp

Lines changed: 217 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,8 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
792792
for (unsigned int i = 0; i < realObjects.size(); ++i)
793793
parentContext.ObjectsListNeeded(realObjects[i]);
794794

795+
const bool hasOrderBy = !event.GetOrderBy().empty();
796+
795797
// Context is "reset" each time the event is repeated (i.e. objects are
796798
// picked again)
797799
gd::EventsCodeGenerationContext context;
@@ -809,6 +811,25 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
809811
for (unsigned int i = 0; i < realObjects.size(); ++i)
810812
context.EmptyObjectsListNeeded(realObjects[i]);
811813

814+
// When orderBy is set, we also need a sorting context to generate the
815+
// expression code for evaluating the orderBy expression on each object.
816+
gd::String orderByExpressionCode;
817+
gd::String sortObjectDeclaration;
818+
if (hasOrderBy) {
819+
gd::EventsCodeGenerationContext sortContext;
820+
sortContext.InheritsFrom(parentContext);
821+
sortContext.ForbidReuse();
822+
for (unsigned int i = 0; i < realObjects.size(); ++i)
823+
sortContext.EmptyObjectsListNeeded(realObjects[i]);
824+
825+
orderByExpressionCode =
826+
gd::ExpressionCodeGenerator::GenerateExpressionCode(
827+
codeGenerator, sortContext, "number",
828+
gd::Expression(event.GetOrderBy()));
829+
sortObjectDeclaration =
830+
codeGenerator.GenerateObjectsDeclarationCode(sortContext) + "\n";
831+
}
832+
812833
// Prepare conditions/actions codes
813834
gd::String conditionsCode = codeGenerator.GenerateConditionsListCode(
814835
event.GetConditions(), context);
@@ -845,12 +866,29 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
845866
event.GetLoopIndexVariableName(), context);
846867
}
847868

869+
// Declare additional variables for the orderBy sorting phase
870+
gd::String forEachSortedList;
871+
gd::String forEachSortKeysList;
872+
gd::String forEachLimitVar;
873+
if (hasOrderBy) {
874+
forEachSortedList =
875+
codeGenerator.GetCodeNamespaceAccessor() + "forEachSorted" +
876+
gd::String::From(context.GetContextDepth());
877+
codeGenerator.AddGlobalDeclaration(forEachSortedList + " = [];\n");
878+
forEachSortKeysList =
879+
codeGenerator.GetCodeNamespaceAccessor() + "forEachSortKeys" +
880+
gd::String::From(context.GetContextDepth());
881+
codeGenerator.AddGlobalDeclaration(forEachSortKeysList + " = [];\n");
882+
forEachLimitVar =
883+
codeGenerator.GetCodeNamespaceAccessor() + "forEachLimit" +
884+
gd::String::From(context.GetContextDepth());
885+
codeGenerator.AddGlobalDeclaration(forEachLimitVar + " = 0;\n");
886+
}
887+
848888
outputCode += localVariablesInitializationCode;
849889

850-
if (realObjects.size() !=
851-
1) //(We write a slightly more simple ( and optimized ) output code
852-
// when only one object list is used.)
853-
{
890+
// --- Build the combined objects list ---
891+
if (realObjects.size() != 1) {
854892
outputCode += forEachTotalCountVar + " = 0;\n";
855893
outputCode += forEachObjectsList + ".length = 0;\n";
856894
for (unsigned int i = 0; i < realObjects.size(); ++i) {
@@ -873,66 +911,196 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
873911
}
874912
}
875913

876-
// Write final code :
914+
if (hasOrderBy) {
915+
// --- OrderBy sorting phase ---
916+
// Build a combined list (always use forEachObjectsList for sorting)
917+
if (realObjects.size() == 1) {
918+
// For single object: copy to the combined list for uniform handling
919+
outputCode += forEachObjectsList + ".length = 0;\n";
920+
outputCode +=
921+
forEachObjectsList + ".push.apply(" + forEachObjectsList +
922+
"," +
923+
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
924+
");\n";
925+
outputCode += forEachTotalCountVar + " = " + forEachObjectsList +
926+
".length;\n";
927+
}
877928

878-
// For loop declaration
879-
if (realObjects.size() ==
880-
1) // We write a slightly more simple ( and optimized ) output code
881-
// when only one object list is used.
929+
// Evaluate the orderBy expression for each object and store in sort keys
930+
outputCode += forEachSortKeysList + ".length = 0;\n";
882931
outputCode +=
883932
"for (" + forEachIndexVar + " = 0;" + forEachIndexVar + " < " +
884-
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
885-
".length;++" + forEachIndexVar + ") {\n";
886-
else
887-
outputCode += "for (" + forEachIndexVar + " = 0;" + forEachIndexVar +
888-
" < " + forEachTotalCountVar + ";++" + forEachIndexVar +
889-
") {\n";
890-
891-
// Empty object lists declaration
892-
outputCode += objectDeclaration;
933+
forEachTotalCountVar + ";++" + forEachIndexVar + ") {\n";
934+
935+
// Pick one object for evaluation
936+
outputCode += sortObjectDeclaration;
937+
for (unsigned int i = 0; i < realObjects.size(); ++i) {
938+
if (realObjects.size() == 1) {
939+
outputCode +=
940+
codeGenerator.GetObjectListName(realObjects[0], context) +
941+
".push(" + forEachObjectsList + "[" + forEachIndexVar +
942+
"]);\n";
943+
} else {
944+
gd::String count;
945+
for (unsigned int j = 0; j <= i; ++j) {
946+
gd::String forEachCountVar =
947+
codeGenerator.GetCodeNamespaceAccessor() + "forEachCount" +
948+
gd::String::From(j) + "_" +
949+
gd::String::From(context.GetContextDepth());
950+
if (j != 0)
951+
count += "+";
952+
count += forEachCountVar;
953+
}
954+
if (i != 0)
955+
outputCode += "else ";
956+
outputCode +=
957+
"if (" + forEachIndexVar + " < " + count + ") {\n";
958+
outputCode +=
959+
" " +
960+
codeGenerator.GetObjectListName(realObjects[i], context) +
961+
".push(" + forEachObjectsList + "[" + forEachIndexVar +
962+
"]);\n";
963+
outputCode += "}\n";
964+
}
965+
}
966+
967+
outputCode += forEachSortKeysList + ".push(" +
968+
orderByExpressionCode + ");\n";
969+
outputCode += "}\n"; // End of sort key evaluation loop
970+
971+
// Build sorted indices and sort them
972+
outputCode += forEachSortedList + ".length = 0;\n";
973+
outputCode += "for (" + forEachIndexVar + " = 0;" +
974+
forEachIndexVar + " < " + forEachTotalCountVar + ";++" +
975+
forEachIndexVar + ") " + forEachSortedList + ".push(" +
976+
forEachIndexVar + ");\n";
893977

894-
// Pick one object
895-
if (realObjects.size() == 1) {
896-
// We write a slightly more simple ( and optimized ) output code
897-
// when only one object list is used.
898-
gd::String temporary = codeGenerator.GetCodeNamespaceAccessor() +
899-
"forEachTemporary" +
900-
gd::String::From(context.GetContextDepth());
901-
codeGenerator.AddGlobalDeclaration(temporary + " = null;\n");
978+
gd::String isDesc =
979+
event.GetOrder() == "desc" ? "true" : "false";
902980
outputCode +=
903-
temporary + " = " +
904-
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
905-
"[" + forEachIndexVar + "];\n";
981+
forEachSortedList + ".sort(function(a, b) { return " + isDesc +
982+
" ? " + forEachSortKeysList + "[b] - " + forEachSortKeysList +
983+
"[a] : " + forEachSortKeysList + "[a] - " +
984+
forEachSortKeysList + "[b]; });\n";
985+
986+
// Apply limit
987+
const bool hasLimit = !event.GetLimit().empty();
988+
if (hasLimit) {
989+
gd::String limitCode =
990+
gd::ExpressionCodeGenerator::GenerateExpressionCode(
991+
codeGenerator, parentContext, "number",
992+
gd::Expression(event.GetLimit()));
993+
outputCode += forEachLimitVar + " = " + limitCode + ";\n";
994+
outputCode += "if (" + forEachLimitVar + " > 0 && " +
995+
forEachSortedList + ".length > " + forEachLimitVar +
996+
") " + forEachSortedList + ".length = " +
997+
forEachLimitVar + ";\n";
998+
}
906999

1000+
// Iterate through sorted indices
9071001
outputCode +=
908-
codeGenerator.GetObjectListName(realObjects[0], context) +
909-
".push(" + temporary + ");\n";
910-
} else {
911-
// Generate the code to pick only one object in the lists
1002+
"for (" + forEachIndexVar + " = 0;" + forEachIndexVar + " < " +
1003+
forEachSortedList + ".length;++" + forEachIndexVar + ") {\n";
1004+
1005+
// Empty object lists and pick the right object
1006+
outputCode += objectDeclaration;
9121007
for (unsigned int i = 0; i < realObjects.size(); ++i) {
913-
gd::String count;
914-
for (unsigned int j = 0; j <= i; ++j) {
915-
gd::String forEachCountVar =
916-
codeGenerator.GetCodeNamespaceAccessor() + "forEachCount" +
917-
gd::String::From(j) + "_" +
1008+
if (realObjects.size() == 1) {
1009+
gd::String temporary =
1010+
codeGenerator.GetCodeNamespaceAccessor() +
1011+
"forEachTemporary" +
9181012
gd::String::From(context.GetContextDepth());
919-
920-
if (j != 0)
921-
count += "+";
922-
count += forEachCountVar;
1013+
codeGenerator.AddGlobalDeclaration(temporary + " = null;\n");
1014+
outputCode +=
1015+
temporary + " = " + forEachObjectsList + "[" +
1016+
forEachSortedList + "[" + forEachIndexVar + "]];\n";
1017+
outputCode +=
1018+
codeGenerator.GetObjectListName(realObjects[0], context) +
1019+
".push(" + temporary + ");\n";
1020+
} else {
1021+
gd::String count;
1022+
for (unsigned int j = 0; j <= i; ++j) {
1023+
gd::String forEachCountVar =
1024+
codeGenerator.GetCodeNamespaceAccessor() + "forEachCount" +
1025+
gd::String::From(j) + "_" +
1026+
gd::String::From(context.GetContextDepth());
1027+
if (j != 0)
1028+
count += "+";
1029+
count += forEachCountVar;
1030+
}
1031+
if (i != 0)
1032+
outputCode += "else ";
1033+
outputCode += "if (" + forEachSortedList + "[" + forEachIndexVar +
1034+
"] < " + count + ") {\n";
1035+
outputCode +=
1036+
" " +
1037+
codeGenerator.GetObjectListName(realObjects[i], context) +
1038+
".push(" + forEachObjectsList + "[" + forEachSortedList +
1039+
"[" + forEachIndexVar + "]]);\n";
1040+
outputCode += "}\n";
9231041
}
1042+
}
1043+
} else {
1044+
// --- Standard (no orderBy) path ---
1045+
1046+
// For loop declaration
1047+
if (realObjects.size() == 1)
1048+
outputCode +=
1049+
"for (" + forEachIndexVar + " = 0;" + forEachIndexVar + " < " +
1050+
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
1051+
".length;++" + forEachIndexVar + ") {\n";
1052+
else
1053+
outputCode +=
1054+
"for (" + forEachIndexVar + " = 0;" + forEachIndexVar + " < " +
1055+
forEachTotalCountVar + ";++" + forEachIndexVar + ") {\n";
1056+
1057+
// Empty object lists declaration
1058+
outputCode += objectDeclaration;
1059+
1060+
// Pick one object
1061+
if (realObjects.size() == 1) {
1062+
gd::String temporary =
1063+
codeGenerator.GetCodeNamespaceAccessor() +
1064+
"forEachTemporary" +
1065+
gd::String::From(context.GetContextDepth());
1066+
codeGenerator.AddGlobalDeclaration(temporary + " = null;\n");
1067+
outputCode +=
1068+
temporary + " = " +
1069+
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
1070+
"[" + forEachIndexVar + "];\n";
9241071

925-
if (i != 0)
926-
outputCode += "else ";
927-
outputCode += "if (" + forEachIndexVar + " < " + count + ") {\n";
9281072
outputCode +=
929-
" " +
930-
codeGenerator.GetObjectListName(realObjects[i], context) +
931-
".push(" + forEachObjectsList + "[" + forEachIndexVar + "]);\n";
932-
outputCode += "}\n";
1073+
codeGenerator.GetObjectListName(realObjects[0], context) +
1074+
".push(" + temporary + ");\n";
1075+
} else {
1076+
for (unsigned int i = 0; i < realObjects.size(); ++i) {
1077+
gd::String count;
1078+
for (unsigned int j = 0; j <= i; ++j) {
1079+
gd::String forEachCountVar =
1080+
codeGenerator.GetCodeNamespaceAccessor() + "forEachCount" +
1081+
gd::String::From(j) + "_" +
1082+
gd::String::From(context.GetContextDepth());
1083+
1084+
if (j != 0)
1085+
count += "+";
1086+
count += forEachCountVar;
1087+
}
1088+
1089+
if (i != 0)
1090+
outputCode += "else ";
1091+
outputCode +=
1092+
"if (" + forEachIndexVar + " < " + count + ") {\n";
1093+
outputCode +=
1094+
" " +
1095+
codeGenerator.GetObjectListName(realObjects[i], context) +
1096+
".push(" + forEachObjectsList + "[" + forEachIndexVar +
1097+
"]);\n";
1098+
outputCode += "}\n";
1099+
}
9331100
}
9341101
}
9351102

1103+
// --- Common iteration body (both ordered and unordered) ---
9361104
if (hasIndexVariable) {
9371105
outputCode +=
9381106
indexVariableAccessor + ".setNumber(" + forEachIndexVar + ");\n";

GDevelop.js/Bindings/Bindings.idl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,6 +2518,12 @@ interface ForEachEvent {
25182518
[Ref] InstructionsList GetActions();
25192519
[Const, Ref] DOMString GetLoopIndexVariableName();
25202520
void SetLoopIndexVariableName([Const] DOMString name);
2521+
[Const, Ref] DOMString GetOrderBy();
2522+
void SetOrderBy([Const] DOMString orderBy);
2523+
[Const, Ref] DOMString GetOrder();
2524+
void SetOrder([Const] DOMString order);
2525+
[Const, Ref] DOMString GetLimit();
2526+
void SetLimit([Const] DOMString limit);
25212527
};
25222528
ForEachEvent implements BaseEvent;
25232529

0 commit comments

Comments
 (0)