Compare commits

..

33 Commits

Author SHA1 Message Date
Davy Hélard
2a03c8902d Fix a lost of focus when a condition is pasted 2024-07-26 15:53:31 +02:00
D8H
2585ad5dca Avoid children variables to collapse in the editor after an undo (#6805) 2024-07-26 14:43:38 +02:00
D8H
a9108fa87b Add a tolerance parameter on the "linear velocity angle" condition (#6804) 2024-07-26 14:27:17 +02:00
D8H
cd3186d2d8 Fix duplicated "email" choice for text input type (#6803) 2024-07-26 12:22:36 +02:00
D8H
6a737f7d51 Fix particle emitter blending mode action (#6801) 2024-07-26 12:01:19 +02:00
github-actions[bot]
9211aa7a3b Update translations [skip ci] (#6799)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-07-26 11:41:23 +02:00
D8H
864ba181e9 Improve a comment (#6800)
Don't show in changelog
2024-07-26 11:22:52 +02:00
D8H
10fa3296a9 Fix text input disabled/read only actions, as well as a few other actions (#6797) 2024-07-26 11:04:25 +02:00
AlexandreS
2fb39b9dbe Add tooltips on tilemap painting icons (#6798)
Don't show in changelog
2024-07-26 10:00:27 +02:00
Clément Pasteau
6f43e896d6 Bump version to 5.4.206 (#6796) 2024-07-25 18:06:10 +02:00
github-actions[bot]
122df05f99 Update translations [skip ci] (#6795)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-25 18:05:36 +02:00
D8H
b7a5122b07 Add a variable editor for object groups (#6781) 2024-07-25 17:21:05 +02:00
Clément Pasteau
eb9d680d95 Instantly update lobby when a player leaves mid-game (#6794)
* Prevents seeing a player who has disconnected still in the lobby
2024-07-25 16:05:47 +02:00
github-actions[bot]
354da42a9e Update translations [skip ci] (#6791)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-07-25 14:22:52 +02:00
AlexandreS
aca37fdade Fix tile set identifier computation (#6792)
Don't show in changelog
Also adds expressions to get tileset dimensions
2024-07-25 12:39:33 +02:00
Clément Pasteau
bb6eb01153 New condition for multiplayer games to know if a player is connected (#6790) 2024-07-25 09:49:07 +02:00
github-actions[bot]
9fb086dcdf Update translations [skip ci] (#6787)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-24 18:48:20 +02:00
D8H
7e60a0246e Fix custom object getDrawableX/Y (#6789)
- Don't show in changelog
2024-07-24 17:48:00 +02:00
D8H
3cb2da3de5 Add autocompletion for parameter fields in events-based object events (#6770)
- Don't show in changelog
2024-07-24 17:23:20 +02:00
AlexandreS
ef23470a00 Add new object: (Integrated) TileMap (#6782)
This PR adds a new TileMap object that can be configured directly in GDevelop.

Import an atlas image, configure the tile size and start painting in the editor!
You can also configure which tiles should have a hit box and use them as platforms.
2024-07-24 17:06:13 +02:00
Clément Pasteau
196ea5e480 Fix multiplayer notifications (#6788)
Do not show in changelog
2024-07-24 15:14:53 +02:00
Clément Pasteau
e732f1952c Fix typo (#6786)
Do not show in changelog
2024-07-23 17:23:19 +02:00
github-actions[bot]
f5f024cc42 Update translations [skip ci] (#6784)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-23 17:16:20 +02:00
Clément Pasteau
6a3df62598 Improve lobbies connections (#6762)
* Lobby start is now more reliable, and wait for everyone to be connected to the host to start the game, rather than wait on a fixed countdown. This can speed up the start (as well as slow it down on slower connections.)
  * A timeout is still in place to start the game without the missing players if they couldn't connect

* Lobbies can now be joined after the game is started, if defined as such in the lobbies section of the game dashboard
  * new actions & conditions are available

* Slightly improve disconnection time in preview
2024-07-23 16:13:50 +02:00
D8H
75f049d911 Show the top of the 1st page when the asset store search changes (#6765) 2024-07-22 16:17:50 +02:00
D8H
4d0ac6f355 Add icons for scenes and scene events (#6785)
- Don't show in changelog
2024-07-22 15:22:36 +02:00
D8H
00a5c93b35 Automatically rename identifiers in event-based objects (#6769)
- Don't show in changelog
2024-07-22 15:14:03 +02:00
D8H
a90cc83967 Add a dialog to choose between 2D and 3D when creating a custom object (#6776) 2024-07-22 14:43:50 +02:00
Aurélien Vivet
87a5934df3 Update the AdMob extension to support Play Services v23 (#6780) 2024-07-22 13:42:09 +02:00
github-actions[bot]
d0245b8f1a Update translations [skip ci] (#6774)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-22 12:54:05 +02:00
D8H
45d73df6fb Fix initialization of local array variables (#6777) 2024-07-22 11:07:47 +02:00
AlexandreS
7ac600e92d Fix svg attrs (#6783)
Don't show in changelog
2024-07-22 10:31:50 +02:00
Aurélien Vivet
7ba8d0133e Declare shortcut CTRL+D to duplicate in object list (#6273)
Add duplicate shortcuts to objects list.
- Also applies the same shortcuts to the objects groups list.

---------

Co-authored-by: osmaneTKT <72160458+osmaneTKT@users.noreply.github.com>
2024-07-18 09:53:06 +02:00
229 changed files with 7644 additions and 2689 deletions

View File

@@ -1179,8 +1179,13 @@ gd::String EventsCodeGenerator::GenerateFreeAction(
// Generate call
gd::String call;
if (instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
instrInfos.codeExtraInformation.type == "boolean") {
instrInfos.codeExtraInformation.type == "string" ||
// Boolean actions declared with addExpressionAndConditionAndAction uses
// MutatorAndOrAccessor even though they don't declare an operator parameter.
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
(instrInfos.codeExtraInformation.type == "boolean" &&
instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
if (instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
call = GenerateOperatorCall(

View File

@@ -31,6 +31,8 @@
namespace gd {
VariablesContainer EventsVariableInstructionTypeSwitcher::nullVariablesContainer;
bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction& instruction,
bool isCondition) {
const auto& metadata = isCondition
@@ -82,7 +84,8 @@ bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction&
// Every occurrence of the variable or its children are checked.
// Ensuring that a child is actually the one with a type change would
// take more time.
if (variablesContainer == &targetVariablesContainer) {
if (variablesContainer == &targetVariablesContainer ||
lastObjectName == groupName) {
if (typeChangedVariableNames.find(variableName) !=
typeChangedVariableNames.end()) {
gd::VariableInstructionSwitcher::

View File

@@ -17,7 +17,7 @@
namespace gd {
class VariablesContainer;
class Platform;
} // namespace gd
} // namespace gd
namespace gd {
/**
@@ -33,21 +33,36 @@ class GD_CORE_API EventsVariableInstructionTypeSwitcher
public:
EventsVariableInstructionTypeSwitcher(
const gd::Platform &platform_,
const gd::VariablesContainer &targetVariablesContainer_,
const std::unordered_set<gd::String> &typeChangedVariableNames_)
const std::unordered_set<gd::String> &typeChangedVariableNames_,
const gd::VariablesContainer &targetVariablesContainer_)
: platform(platform_),
targetVariablesContainer(targetVariablesContainer_),
typeChangedVariableNames(typeChangedVariableNames_){};
typeChangedVariableNames(typeChangedVariableNames_),
targetVariablesContainer(targetVariablesContainer_), groupName(""){};
EventsVariableInstructionTypeSwitcher(
const gd::Platform &platform_,
const std::unordered_set<gd::String> &typeChangedVariableNames_,
const gd::String &groupName_)
: platform(platform_),
typeChangedVariableNames(typeChangedVariableNames_),
targetVariablesContainer(nullVariablesContainer),
groupName(groupName_){};
virtual ~EventsVariableInstructionTypeSwitcher();
private:
private:
bool DoVisitInstruction(gd::Instruction &instruction,
bool isCondition) override;
const gd::Platform &platform;
const gd::VariablesContainer &targetVariablesContainer;
gd::String objectName;
/**
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
* pointing to `nullVariablesContainer` and the group name is use instead to
* check which instruction to modify.
*/
const gd::String groupName;
const std::unordered_set<gd::String> &typeChangedVariableNames;
static VariablesContainer nullVariablesContainer;
};
} // namespace gd

View File

@@ -32,6 +32,8 @@
namespace gd {
VariablesContainer EventsVariableReplacer::nullVariablesContainer;
/**
* \brief Go through the nodes and rename variables,
* or signal if the instruction must be renamed if a removed variable is used.
@@ -44,22 +46,26 @@ class GD_CORE_API ExpressionVariableReplacer
ExpressionVariableReplacer(
const gd::Platform& platform_,
const gd::ProjectScopedContainers& projectScopedContainers_,
const gd::VariablesContainer& targetVariablesContainer_,
const VariablesRenamingChangesetNode& variablesRenamingChangesetRoot_,
const std::unordered_set<gd::String>& removedVariableNames_)
const std::unordered_set<gd::String>& removedVariableNames_,
const gd::VariablesContainer& targetVariablesContainer_,
const gd::String &groupName_,
const gd::String &forcedInitialObjectName)
: hasDoneRenaming(false),
removedVariableUsed(false),
platform(platform_),
projectScopedContainers(projectScopedContainers_),
forcedInitialVariablesContainer(nullptr),
targetVariablesContainer(targetVariablesContainer_),
forcedVariablesContainer(nullptr),
forcedObjectName(forcedInitialObjectName),
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
removedVariableNames(removedVariableNames_){};
removedVariableNames(removedVariableNames_),
targetVariablesContainer(targetVariablesContainer_),
targetGroupName(groupName_){};
virtual ~ExpressionVariableReplacer(){};
void SetForcedInitialVariablesContainer(
const gd::VariablesContainer* forcedInitialVariablesContainer_) {
forcedInitialVariablesContainer = forcedInitialVariablesContainer_;
forcedVariablesContainer = forcedInitialVariablesContainer_;
}
bool HasDoneRenaming() const { return hasDoneRenaming; }
@@ -82,12 +88,13 @@ class GD_CORE_API ExpressionVariableReplacer
// The node represents a variable or an object name on which a variable
// will be accessed.
if (forcedInitialVariablesContainer) {
if (forcedVariablesContainer) {
const gd::String oldVariableName = node.name;
PushVariablesRenamingChangesetRoot();
// A scope was forced. Honor it: it means this node represents a variable
// of the forced variables container.
if (forcedInitialVariablesContainer == &targetVariablesContainer) {
if (forcedVariablesContainer == &targetVariablesContainer ||
IsTargetingObjectGroup(forcedObjectName)) {
RenameOrRemoveVariableOfTargetVariableContainer(node.name);
}
@@ -150,7 +157,8 @@ class GD_CORE_API ExpressionVariableReplacer
// This is always true because MatchIdentifierWithName is used to get
// objectNameToUseForVariableAccessor.
if (objectsContainersList.HasObjectOrGroupVariablesContainer(
objectNameToUseForVariableAccessor, targetVariablesContainer)) {
objectNameToUseForVariableAccessor, targetVariablesContainer) ||
IsTargetingObjectGroup(objectNameToUseForVariableAccessor)) {
objectNameToUseForVariableAccessor = "";
// The node represents an object variable, and this object variables are
// the target. Do the replacement or removals:
@@ -197,10 +205,11 @@ class GD_CORE_API ExpressionVariableReplacer
// (and if it's a variable reference or a value does not have any importance
// here).
if (forcedInitialVariablesContainer) {
if (forcedVariablesContainer) {
// A scope was forced. Honor it: it means this node represents a variable
// of the forced variables container.
if (forcedInitialVariablesContainer == &targetVariablesContainer) {
if (forcedVariablesContainer == &targetVariablesContainer ||
IsTargetingObjectGroup(forcedObjectName)) {
renameVariableAndChild();
}
return;
@@ -213,7 +222,8 @@ class GD_CORE_API ExpressionVariableReplacer
[&]() {
// This represents an object.
if (objectsContainersList.HasObjectOrGroupVariablesContainer(
node.identifierName, targetVariablesContainer)) {
node.identifierName, targetVariablesContainer) ||
IsTargetingObjectGroup(node.identifierName)) {
// The node represents an object variable, and this object variables
// are the target. Do the replacement or removals:
PushVariablesRenamingChangesetRoot();
@@ -261,31 +271,33 @@ class GD_CORE_API ExpressionVariableReplacer
// force the "scope" at which starts the evalution of variables.
if (parameterMetadata && parameterMetadata->GetValueTypeMetadata()
.IsLegacyPreScopedVariable()) {
const gd::VariablesContainer* oldForcedInitialVariablesContainer =
forcedInitialVariablesContainer;
const gd::VariablesContainer *oldForcedVariablesContainer =
forcedVariablesContainer;
const gd::String &oldForcedObjectName = forcedObjectName;
forcedInitialVariablesContainer = nullptr;
forcedVariablesContainer = nullptr;
forcedObjectName = "";
if (parameterMetadata->GetType() == "globalvar") {
forcedInitialVariablesContainer =
forcedVariablesContainer =
projectScopedContainers.GetVariablesContainersList()
.GetTopMostVariablesContainer();
} else if (parameterMetadata->GetType() == "scenevar") {
forcedInitialVariablesContainer =
forcedVariablesContainer =
projectScopedContainers.GetVariablesContainersList()
.GetBottomMostVariablesContainer();
} else if (parameterMetadata->GetType() == "objectvar") {
auto objectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
platform,
projectScopedContainers.GetObjectsContainersList(),
node.objectName,
*node.parameters[parameterIndex].get());
forcedInitialVariablesContainer =
platform, projectScopedContainers.GetObjectsContainersList(),
node.objectName, *node.parameters[parameterIndex].get());
forcedVariablesContainer =
projectScopedContainers.GetObjectsContainersList()
.GetObjectOrGroupVariablesContainer(objectName);
forcedObjectName = objectName;
}
node.parameters[parameterIndex]->Visit(*this);
forcedInitialVariablesContainer = oldForcedInitialVariablesContainer;
forcedVariablesContainer = oldForcedVariablesContainer;
forcedObjectName = oldForcedObjectName;
} else {
// For any other parameter, there is no special treatment being needed.
node.parameters[parameterIndex]->Visit(*this);
@@ -298,6 +310,10 @@ class GD_CORE_API ExpressionVariableReplacer
bool hasDoneRenaming;
bool removedVariableUsed;
bool IsTargetingObjectGroup(const gd::String &objectGroupName) {
return !targetGroupName.empty() && objectGroupName == targetGroupName;
}
bool RenameOrRemoveVariableOfTargetVariableContainer(
gd::String& variableName) {
const auto *currentVariablesRenamingChangesetNode =
@@ -382,10 +398,17 @@ class GD_CORE_API ExpressionVariableReplacer
// Scope:
const gd::Platform& platform;
const gd::ProjectScopedContainers& projectScopedContainers;
const gd::VariablesContainer* forcedInitialVariablesContainer;
const gd::VariablesContainer* forcedVariablesContainer;
gd::String forcedObjectName;
// Renaming or removing to do:
const gd::VariablesContainer& targetVariablesContainer;
/**
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
* pointing to `nullVariablesContainer` and the group name is use instead to
* check which variable accesses to modify in expressions.
*/
const gd::String& targetGroupName;
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot;
const std::unordered_set<gd::String>& removedVariableNames;
@@ -397,7 +420,7 @@ const gd::VariablesContainer*
EventsVariableReplacer::FindForcedVariablesContainerIfAny(
const gd::String& type, const gd::String& lastObjectName) {
// Handle legacy pre-scoped variable parameters: in this case, we
// force the "scope" at which starts the evalution of variables.
// force the "scope" at which starts the evaluation of variables.
if (type == "objectvar") {
return GetProjectScopedContainers()
.GetObjectsContainersList()
@@ -442,9 +465,11 @@ bool EventsVariableReplacer::DoVisitInstruction(gd::Instruction& instruction,
if (node) {
ExpressionVariableReplacer renamer(platform,
GetProjectScopedContainers(),
targetVariablesContainer,
variablesRenamingChangesetRoot,
removedVariableNames);
removedVariableNames,
targetVariablesContainer,
targetGroupName,
type == "objectvar" ? lastObjectName : "");
renamer.SetForcedInitialVariablesContainer(
FindForcedVariablesContainerIfAny(type, lastObjectName));
node->Visit(renamer);
@@ -474,9 +499,11 @@ bool EventsVariableReplacer::DoVisitEventExpression(
if (node) {
ExpressionVariableReplacer renamer(platform,
GetProjectScopedContainers(),
targetVariablesContainer,
variablesRenamingChangesetRoot,
removedVariableNames);
removedVariableNames,
targetVariablesContainer,
targetGroupName,
"");
renamer.SetForcedInitialVariablesContainer(
FindForcedVariablesContainerIfAny(type, ""));
node->Visit(renamer);

View File

@@ -35,13 +35,24 @@ class GD_CORE_API EventsVariableReplacer
public:
EventsVariableReplacer(
const gd::Platform &platform_,
const gd::VariablesContainer &targetVariablesContainer_,
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot_,
const std::unordered_set<gd::String> &removedVariableNames_)
const std::unordered_set<gd::String> &removedVariableNames_,
const gd::VariablesContainer &targetVariablesContainer_)
: platform(platform_),
targetVariablesContainer(targetVariablesContainer_),
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
removedVariableNames(removedVariableNames_) {};
removedVariableNames(removedVariableNames_),
targetVariablesContainer(targetVariablesContainer_),
targetGroupName("") {};
EventsVariableReplacer(
const gd::Platform &platform_,
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot_,
const std::unordered_set<gd::String> &removedVariableNames_,
const gd::String &targetGroupName_)
: platform(platform_),
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
removedVariableNames(removedVariableNames_),
targetVariablesContainer(nullVariablesContainer),
targetGroupName(targetGroupName_) {};
virtual ~EventsVariableReplacer();
private:
@@ -55,9 +66,16 @@ class GD_CORE_API EventsVariableReplacer
const gd::Platform &platform;
const gd::VariablesContainer &targetVariablesContainer;
gd::String objectName;
/**
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
* pointing to `nullVariablesContainer` and the group name is use instead to
* check which variable accesses to modify in expressions.
*/
const gd::String targetGroupName;
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot;
const std::unordered_set<gd::String> &removedVariableNames;
static VariablesContainer nullVariablesContainer;
};
} // namespace gd

View File

@@ -0,0 +1,185 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "GroupVariableHelper.h"
#include "GDCore/IDE/WholeProjectRefactorer.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectGroup.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/Project/ObjectsContainersList.h"
#include "GDCore/Project/Variable.h"
#include "GDCore/Project/VariablesContainer.h"
#include "GDCore/String.h"
namespace gd {
void GroupVariableHelper::FillAnyVariableBetweenObjects(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::ObjectGroup &objectGroup) {
const auto &objectNames = objectGroup.GetAllObjectsNames();
for (const gd::String &sourceObjectName : objectNames) {
const bool hasObject = objectsContainer.HasObjectNamed(sourceObjectName);
if (!hasObject &&
!globalObjectsContainer.HasObjectNamed(sourceObjectName)) {
continue;
}
const auto &sourceObject =
hasObject ? objectsContainer.GetObject(sourceObjectName)
: globalObjectsContainer.GetObject(sourceObjectName);
const auto &sourceVariablesContainer = sourceObject.GetVariables();
for (const gd::String &destinationObjectName : objectNames) {
if (sourceObjectName == destinationObjectName) {
continue;
}
const bool hasObject =
objectsContainer.HasObjectNamed(destinationObjectName);
if (!hasObject &&
!globalObjectsContainer.HasObjectNamed(destinationObjectName)) {
continue;
}
auto &destinationObject =
hasObject ? objectsContainer.GetObject(destinationObjectName)
: globalObjectsContainer.GetObject(destinationObjectName);
auto &destinationVariablesContainer = destinationObject.GetVariables();
for (std::size_t sourceVariableIndex = 0;
sourceVariableIndex < sourceVariablesContainer.Count();
++sourceVariableIndex) {
auto &sourceVariable =
sourceVariablesContainer.Get(sourceVariableIndex);
const auto &variableName =
sourceVariablesContainer.GetNameAt(sourceVariableIndex);
if (!destinationVariablesContainer.Has(variableName)) {
destinationVariablesContainer.Insert(
variableName, sourceVariable,
destinationVariablesContainer.Count());
}
}
}
}
}
gd::VariablesContainer GroupVariableHelper::MergeVariableContainers(
const gd::ObjectsContainersList &objectsContainersList,
const gd::ObjectGroup &objectGroup) {
gd::VariablesContainer mergedVariablesContainer;
const auto &objectNames = objectGroup.GetAllObjectsNames();
std::size_t objectIndex = 0;
bool isFirstObjectFound = false;
for (; objectIndex < objectNames.size() && !isFirstObjectFound;
objectIndex++) {
const gd::String &objectName = objectNames[objectIndex];
if (!objectsContainersList.HasObjectOrGroupNamed(objectName)) {
continue;
}
isFirstObjectFound = true;
mergedVariablesContainer =
*objectsContainersList.GetObjectOrGroupVariablesContainer(objectName);
}
for (; objectIndex < objectNames.size(); objectIndex++) {
const gd::String &objectName = objectNames[objectIndex];
if (!objectsContainersList.HasObjectOrGroupNamed(objectName)) {
continue;
}
const auto &variablesContainer =
*objectsContainersList.GetObjectOrGroupVariablesContainer(objectName);
for (std::size_t variableIndex = 0;
variableIndex < mergedVariablesContainer.Count(); ++variableIndex) {
auto &mergedVariable = mergedVariablesContainer.Get(variableIndex);
const auto &variableName =
mergedVariablesContainer.GetNameAt(variableIndex);
if (variablesContainer.Has(variableName)) {
auto &variable = variablesContainer.Get(variableName);
if (mergedVariable.GetType() != variable.GetType()) {
mergedVariable.CastTo(gd::Variable::Type::MixedTypes);
} else if (mergedVariable != variable) {
mergedVariable.MarkAsMixedValues();
}
} else {
mergedVariablesContainer.Remove(variableName);
variableIndex--;
}
}
}
return mergedVariablesContainer;
}
void GroupVariableHelper::FillMissingGroupVariablesToObjects(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer, const gd::ObjectGroup &objectGroup,
const gd::SerializerElement &originalSerializedVariables) {
gd::VariablesContainer groupVariablesContainer;
groupVariablesContainer.UnserializeFrom(originalSerializedVariables);
// Add missing variables to objects added in the group.
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
continue;
}
auto &object = hasObject ? objectsContainer.GetObject(objectName)
: globalObjectsContainer.GetObject(objectName);
auto &variablesContainer = object.GetVariables();
for (std::size_t variableIndex = 0;
variableIndex < groupVariablesContainer.Count(); ++variableIndex) {
auto &groupVariable = groupVariablesContainer.Get(variableIndex);
const auto &variableName =
groupVariablesContainer.GetNameAt(variableIndex);
if (!variablesContainer.Has(variableName)) {
variablesContainer.Insert(variableName, groupVariable,
variablesContainer.Count());
}
}
}
};
// TODO Handle position changes for group variables.
// We could try to change the order of object variables in a way that the next
// call to MergeVariableContainers rebuild them in the same order.
void GroupVariableHelper::ApplyChangesToObjects(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::VariablesContainer &groupVariablesContainer,
const gd::ObjectGroup &objectGroup,
const gd::VariablesChangeset &changeset) {
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
continue;
}
auto &object = hasObject ? objectsContainer.GetObject(objectName)
: globalObjectsContainer.GetObject(objectName);
auto &variablesContainer = object.GetVariables();
for (const gd::String &variableName : changeset.removedVariableNames) {
variablesContainer.Remove(variableName);
}
for (const gd::String &variableName : changeset.addedVariableNames) {
variablesContainer.Insert(variableName,
groupVariablesContainer.Get(variableName),
variablesContainer.Count());
}
for (const auto &pair : changeset.oldToNewVariableNames) {
const gd::String &oldVariableName = pair.first;
const gd::String &newVariableName = pair.second;
variablesContainer.Rename(oldVariableName, newVariableName);
}
// Apply type and value changes
for (const gd::String &variableName : changeset.valueChangedVariableNames) {
size_t index = variablesContainer.GetPosition(variableName);
variablesContainer.Remove(variableName);
variablesContainer.Insert(
variableName, groupVariablesContainer.Get(variableName), index);
}
}
}
} // namespace gd

View File

@@ -0,0 +1,75 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/Project/VariablesContainer.h"
namespace gd {
class ObjectsContainersList;
class ObjectsContainer;
class ObjectGroup;
class VariablesContainer;
struct VariablesChangeset;
} // namespace gd
namespace gd {
/**
* Help handling variables of group objects as a whole.
*
* This is used by the object group variable editor.
*/
class GD_CORE_API GroupVariableHelper {
public:
/**
* Copy every variable from every object of the group to the other objects
* if they don't have it already.
*
* In the editor, when an object group is created, users can choose between:
* - doing no change and only see variables that are already shared by any
* objects of the group
* - applying this function and see every variable
*/
static void
FillAnyVariableBetweenObjects(gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::ObjectGroup &objectGroup);
/**
* Build a variable container with the intersection of variables from the
* every objects of the given group.
*/
static gd::VariablesContainer MergeVariableContainers(
const gd::ObjectsContainersList &objectsContainersList,
const gd::ObjectGroup &objectGroup);
/**
* @brief Copy the variables of the group to all objects.
*
* Objects can be added during the group edition and may not necessarily have
* all the variables initially shared by the group.
*
* \see gd::GroupVariableHelper::MergeVariableContainers
*/
static void FillMissingGroupVariablesToObjects(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::ObjectGroup &objectGroup,
const gd::SerializerElement &originalSerializedVariables);
/**
* @brief Apply the changes done with the variables editor to the objects of
* the group.
*/
static void
ApplyChangesToObjects(gd::ObjectsContainer &globalObjectsContainers,
gd::ObjectsContainer &objectsContainers,
const gd::VariablesContainer &groupVariablesContainer,
const gd::ObjectGroup &objectGroup,
const gd::VariablesChangeset &changeset);
};
} // namespace gd

View File

@@ -11,22 +11,23 @@
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/DependenciesAnalyzer.h"
#include "GDCore/IDE/GroupVariableHelper.h"
#include "GDCore/IDE/EventBasedBehaviorBrowser.h"
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
#include "GDCore/IDE/Events/BehaviorParametersFiller.h"
#include "GDCore/IDE/Events/BehaviorTypeRenamer.h"
#include "GDCore/IDE/Events/CustomObjectTypeRenamer.h"
#include "GDCore/IDE/Events/EventsBehaviorRenamer.h"
#include "GDCore/IDE/Events/EventsPropertyReplacer.h"
#include "GDCore/IDE/Events/EventsRefactorer.h"
#include "GDCore/IDE/Events/EventsVariableReplacer.h"
#include "GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.h"
#include "GDCore/IDE/Events/EventsVariableReplacer.h"
#include "GDCore/IDE/Events/ExpressionsParameterMover.h"
#include "GDCore/IDE/Events/ExpressionsRenamer.h"
#include "GDCore/IDE/Events/InstructionsParameterMover.h"
#include "GDCore/IDE/Events/InstructionsTypeRenamer.h"
#include "GDCore/IDE/Events/LinkEventTargetRenamer.h"
#include "GDCore/IDE/Events/ProjectElementRenamer.h"
#include "GDCore/IDE/Events/BehaviorParametersFiller.h"
#include "GDCore/IDE/EventsFunctionTools.h"
#include "GDCore/IDE/Project/ArbitraryBehaviorSharedDataWorker.h"
#include "GDCore/IDE/Project/ArbitraryEventBasedBehaviorsWorker.h"
@@ -173,6 +174,7 @@ WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
removedUuidAndNames.find(variable.GetPersistentUuid());
if (existingOldVariableUuidAndName == removedUuidAndNames.end()) {
// This is a new variable.
changeset.addedVariableNames.insert(variableName);
} else {
const gd::String &oldName = existingOldVariableUuidAndName->second;
@@ -182,9 +184,15 @@ WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
}
const auto &oldVariable = oldVariablesContainer.Get(oldName);
if (gd::WholeProjectRefactorer::HasAnyVariableTypeChanged(oldVariable, variable)) {
if (gd::WholeProjectRefactorer::HasAnyVariableTypeChanged(oldVariable,
variable)) {
changeset.typeChangedVariableNames.insert(variableName);
}
if (oldVariable != variable
// Mixed values are never equals, but they must not override anything.
&& !variable.HasMixedValues()) {
changeset.valueChangedVariableNames.insert(variableName);
}
const auto &variablesRenamingChangesetNode =
gd::WholeProjectRefactorer::ComputeChangesetForVariable(oldVariable,
@@ -310,8 +318,8 @@ void WholeProjectRefactorer::ApplyRefactoringForVariablesContainer(
// Rename and remove variables
gd::EventsVariableReplacer eventsVariableReplacer(
project.GetCurrentPlatform(), variablesContainer,
changeset, changeset.removedVariableNames);
project.GetCurrentPlatform(), changeset, changeset.removedVariableNames,
variablesContainer);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsVariableReplacer);
@@ -321,8 +329,83 @@ void WholeProjectRefactorer::ApplyRefactoringForVariablesContainer(
// Switch types of instructions
gd::EventsVariableInstructionTypeSwitcher
eventsVariableInstructionTypeSwitcher(project.GetCurrentPlatform(),
variablesContainer,
changeset.typeChangedVariableNames);
changeset.typeChangedVariableNames,
variablesContainer);
gd::ProjectBrowserHelper::ExposeProjectEvents(
project, eventsVariableInstructionTypeSwitcher);
}
void WholeProjectRefactorer::ApplyRefactoringForGroupVariablesContainer(
gd::Project &project, gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::VariablesContainer &groupVariablesContainer,
const gd::ObjectGroup &objectGroup,
const gd::VariablesChangeset &changeset,
const gd::SerializerElement &originalSerializedVariables) {
// While we support refactoring that would remove all references (actions, conditions...)
// it's both a bit dangerous for the user and we would need to show the user what
// will be removed before doing so. For now, just clear the removed variables so they don't
// trigger any refactoring.
std::unordered_set<gd::String> removedVariableNames;
// Rename variables in events for the objects of the group.
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
continue;
}
auto &object = hasObject ? objectsContainer.GetObject(objectName)
: globalObjectsContainer.GetObject(objectName);
auto &variablesContainer = object.GetVariables();
gd::EventsVariableReplacer eventsVariableReplacer(
project.GetCurrentPlatform(), changeset,
removedVariableNames, variablesContainer);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsVariableReplacer);
}
// Rename variables in events for the group.
gd::EventsVariableReplacer eventsVariableReplacer(
project.GetCurrentPlatform(), changeset, removedVariableNames,
objectGroup.GetName());
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsVariableReplacer);
// Apply changes to objects.
gd::GroupVariableHelper::FillMissingGroupVariablesToObjects(
globalObjectsContainer,
objectsContainer,
objectGroup,
originalSerializedVariables);
gd::GroupVariableHelper::ApplyChangesToObjects(
globalObjectsContainer, objectsContainer, groupVariablesContainer,
objectGroup, changeset);
// Switch types of instructions for the group objects.
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
continue;
}
auto &object = hasObject ? objectsContainer.GetObject(objectName)
: globalObjectsContainer.GetObject(objectName);
auto &variablesContainer = object.GetVariables();
gd::EventsVariableInstructionTypeSwitcher
eventsVariableInstructionTypeSwitcher(
project.GetCurrentPlatform(), changeset.typeChangedVariableNames,
variablesContainer);
gd::ProjectBrowserHelper::ExposeProjectEvents(
project, eventsVariableInstructionTypeSwitcher);
}
// Switch types of instructions for the group.
gd::EventsVariableInstructionTypeSwitcher
eventsVariableInstructionTypeSwitcher(project.GetCurrentPlatform(),
changeset.typeChangedVariableNames,
objectGroup.GetName());
gd::ProjectBrowserHelper::ExposeProjectEvents(
project, eventsVariableInstructionTypeSwitcher);
}
@@ -849,7 +932,8 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorProperty(
oldPropertyName, newPropertyName);
gd::ProjectBrowserHelper::ExposeEventsBasedBehaviorEvents(
project, eventsFunctionsExtension, eventsBasedBehavior, behaviorRenamer);
project, eventsFunctionsExtension, eventsBasedBehavior,
behaviorRenamer);
} else {
// Properties that represent primitive values will be used through
// their related actions/conditions/expressions. Rename these.
@@ -919,7 +1003,8 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorSharedProperty(
oldPropertyName, newPropertyName);
gd::ProjectBrowserHelper::ExposeEventsBasedBehaviorEvents(
project, eventsFunctionsExtension, eventsBasedBehavior, behaviorRenamer);
project, eventsFunctionsExtension, eventsBasedBehavior,
behaviorRenamer);
} else {
// Properties that represent primitive values will be used through
// their related actions/conditions/expressions. Rename these.
@@ -1518,7 +1603,7 @@ void WholeProjectRefactorer::DoRenameObject(
projectBrowser.ExposeFunctions(project, objectParameterRenamer);
}
void WholeProjectRefactorer::ObjectRemovedInLayout(
void WholeProjectRefactorer::ObjectRemovedInScene(
gd::Project &project, gd::Layout &layout, const gd::String &objectName) {
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
@@ -1540,7 +1625,7 @@ void WholeProjectRefactorer::ObjectRemovedInLayout(
}
}
void WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(
void WholeProjectRefactorer::BehaviorsAddedToObjectInScene(
gd::Project &project, gd::Layout &layout, const gd::String &objectName) {
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
@@ -1550,7 +1635,7 @@ void WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(
project, layout, behaviorParameterFiller);
}
void WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
void WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
gd::Project &project, gd::Layout &layout, const gd::String &oldName,
const gd::String &newName, bool isObjectGroup) {
if (oldName == newName || newName.empty() || oldName.empty())
@@ -1641,81 +1726,150 @@ void WholeProjectRefactorer::RenameExternalEvents(gd::Project &project,
linkEventTargetRenamer);
}
void WholeProjectRefactorer::RenameLayer(gd::Project &project,
gd::Layout &layout,
const gd::String &oldName,
const gd::String &newName) {
void WholeProjectRefactorer::RenameLayerInScene(gd::Project &project,
gd::Layout &scene,
const gd::String &oldName,
const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(project.GetCurrentPlatform(),
"layer", oldName, newName);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
layout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
project, scene, projectElementRenamer);
scene.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
std::vector<gd::String> externalLayoutsNames =
GetAssociatedExternalLayouts(project, layout);
GetAssociatedExternalLayouts(project, scene);
for (gd::String name : externalLayoutsNames) {
auto &externalLayout = project.GetExternalLayout(name);
externalLayout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
}
}
void WholeProjectRefactorer::RenameLayerEffect(gd::Project &project,
gd::Layout &layout,
gd::Layer &layer,
const gd::String &oldName,
const gd::String &newName) {
void WholeProjectRefactorer::RenameLayerInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, const gd::String &oldName,
const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(project.GetCurrentPlatform(),
"layer", oldName, newName);
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
project, eventsFunctionsExtension, eventsBasedObject,
projectElementRenamer);
eventsBasedObject.GetInitialInstances().MoveInstancesToLayer(oldName,
newName);
}
void WholeProjectRefactorer::RenameLayerEffectInScene(
gd::Project &project, gd::Layout &scene, gd::Layer &layer,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "layerEffectName", oldName, newName);
projectElementRenamer.SetLayerConstraint(layer.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
project, scene, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectAnimation(gd::Project &project,
gd::Layout &layout,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName) {
void WholeProjectRefactorer::RenameLayerEffectInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Layer &layer,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "layerEffectName", oldName, newName);
projectElementRenamer.SetLayerConstraint(layer.GetName());
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
project, eventsFunctionsExtension, eventsBasedObject,
projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectAnimationInScene(
gd::Project &project, gd::Layout &scene, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectAnimationName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
project, scene, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectPoint(gd::Project &project,
gd::Layout &layout,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName) {
void WholeProjectRefactorer::RenameObjectAnimationInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectAnimationName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
project, eventsFunctionsExtension, eventsBasedObject,
projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectPointInScene(
gd::Project &project, gd::Layout &scene, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectPointName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
project, scene, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectEffect(gd::Project &project,
gd::Layout &layout,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName) {
void WholeProjectRefactorer::RenameObjectPointInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectPointName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
project, eventsFunctionsExtension, eventsBasedObject,
projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectEffectInScene(
gd::Project &project, gd::Layout &scene, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectEffectName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
project, scene, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectEffectInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectEffectName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
project, eventsFunctionsExtension, eventsBasedObject,
projectElementRenamer);
}
void WholeProjectRefactorer::ObjectRemovedInEventsBasedObject(
@@ -1793,10 +1947,9 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
bool isObjectGroup) {
// Object groups can't be in other groups
if (!isObjectGroup) {
for (std::size_t g = 0;
g < project.GetObjects().GetObjectGroups().size(); ++g) {
project.GetObjects().GetObjectGroups()[g].RenameObject(oldName,
newName);
for (std::size_t g = 0; g < project.GetObjects().GetObjectGroups().size();
++g) {
project.GetObjects().GetObjectGroups()[g].RenameObject(oldName, newName);
}
}
@@ -1805,13 +1958,13 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
if (layout.GetObjects().HasObjectNamed(oldName))
continue;
ObjectOrGroupRenamedInLayout(project, layout, oldName, newName,
ObjectOrGroupRenamedInScene(project, layout, oldName, newName,
isObjectGroup);
}
}
void WholeProjectRefactorer::GlobalObjectRemoved(
gd::Project &project, const gd::String &objectName) {
void WholeProjectRefactorer::GlobalObjectRemoved(gd::Project &project,
const gd::String &objectName) {
auto &globalGroups = project.GetObjects().GetObjectGroups();
for (std::size_t g = 0; g < globalGroups.size(); ++g) {
globalGroups[g].RemoveObject(objectName);
@@ -1822,7 +1975,7 @@ void WholeProjectRefactorer::GlobalObjectRemoved(
if (layout.GetObjects().HasObjectNamed(objectName))
continue;
ObjectRemovedInLayout(project, layout, objectName);
ObjectRemovedInScene(project, layout, objectName);
}
}
@@ -1833,13 +1986,13 @@ void WholeProjectRefactorer::BehaviorsAddedToGlobalObject(
if (layout.GetObjects().HasObjectNamed(objectName))
continue;
BehaviorsAddedToObjectInLayout(project, layout, objectName);
BehaviorsAddedToObjectInScene(project, layout, objectName);
}
}
void WholeProjectRefactorer::RemoveLayerInScene(gd::Project &project,
gd::Layout &scene,
const gd::String &layerName) {
gd::Layout &scene,
const gd::String &layerName) {
if (layerName.empty())
return;
@@ -1853,15 +2006,14 @@ void WholeProjectRefactorer::RemoveLayerInScene(gd::Project &project,
}
}
void WholeProjectRefactorer::MergeLayersInScene(gd::Project &project,
gd::Layout &scene,
const gd::String &originLayerName,
const gd::String &targetLayerName) {
void WholeProjectRefactorer::MergeLayersInScene(
gd::Project &project, gd::Layout &scene, const gd::String &originLayerName,
const gd::String &targetLayerName) {
if (originLayerName == targetLayerName || originLayerName.empty())
return;
scene.GetInitialInstances().MoveInstancesToLayer(originLayerName,
targetLayerName);
targetLayerName);
std::vector<gd::String> externalLayoutsNames =
GetAssociatedExternalLayouts(project, scene);

View File

@@ -19,6 +19,7 @@ class Project;
class Layout;
class Layer;
class Object;
class ObjectGroup;
class String;
class EventsFunctionsExtension;
class EventsFunction;
@@ -58,6 +59,8 @@ struct VariablesChangeset : VariablesRenamingChangesetNode {
* would take more time than checking the instruction type is rightly set.
*/
std::unordered_set<gd::String> typeChangedVariableNames;
std::unordered_set<gd::String> valueChangedVariableNames;
std::unordered_set<gd::String> addedVariableNames;
bool HasRemovedVariables() { return !removedVariableNames.empty(); }
@@ -68,7 +71,7 @@ struct VariablesChangeset : VariablesRenamingChangesetNode {
* \brief Tool functions to do refactoring on the whole project after
* changes like deletion or renaming of an object.
*
* \TODO Ideally ObjectOrGroupRenamedInLayout, ObjectRemovedInLayout,
* \TODO Ideally ObjectOrGroupRenamedInScene, ObjectRemovedInScene,
* GlobalObjectOrGroupRenamed, GlobalObjectRemoved would be implemented
* using ExposeProjectEvents.
*/
@@ -91,6 +94,18 @@ class GD_CORE_API WholeProjectRefactorer {
const gd::VariablesChangeset &changeset,
const gd::SerializerElement &originalSerializedVariables);
/**
* \brief Refactor the project according to the changes (renaming or deletion)
* made to variables of a group.
*/
static void ApplyRefactoringForGroupVariablesContainer(
gd::Project &project, gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer,
const gd::VariablesContainer &groupVariablesContainer,
const gd::ObjectGroup &objectGroup,
const gd::VariablesChangeset &changeset,
const gd::SerializerElement &originalSerializedVariables);
/**
* \brief Refactor the project **before** an events function extension is
* renamed.
@@ -339,40 +354,91 @@ class GD_CORE_API WholeProjectRefactorer {
static void RenameExternalEvents(gd::Project &project,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after a layer is renamed.
*/
static void RenameLayer(gd::Project &project, gd::Layout &layout,
const gd::String &oldName, const gd::String &newName);
static void RenameLayerInScene(gd::Project &project, gd::Layout &scene,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after a layer is renamed.
*/
static void RenameLayerInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after a layer effect is renamed.
*/
static void RenameLayerEffect(gd::Project &project, gd::Layout &layout,
gd::Layer &layer, const gd::String &oldName,
const gd::String &newName);
static void RenameLayerEffectInScene(gd::Project &project, gd::Layout &scene,
gd::Layer &layer,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after a layer effect is renamed.
*/
static void RenameLayerEffectInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Layer &layer,
const gd::String &oldName, const gd::String &newName);
/**
* \brief Refactor the project after an object animation is renamed.
*/
static void RenameObjectAnimation(gd::Project &project, gd::Layout &layout,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName);
static void RenameObjectAnimationInScene(gd::Project &project,
gd::Layout &scene,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after an object animation is renamed.
*/
static void RenameObjectAnimationInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName);
/**
* \brief Refactor the project after an object point is renamed.
*/
static void RenameObjectPoint(gd::Project &project, gd::Layout &layout,
gd::Object &object, const gd::String &oldName,
const gd::String &newName);
static void RenameObjectPointInScene(gd::Project &project, gd::Layout &scene,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after an object point is renamed.
*/
static void RenameObjectPointInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName);
/**
* \brief Refactor the project after an object effect is renamed.
*/
static void RenameObjectEffect(gd::Project &project, gd::Layout &layout,
gd::Object &object, const gd::String &oldName,
const gd::String &newName);
static void RenameObjectEffectInScene(gd::Project &project, gd::Layout &scene,
gd::Object &object,
const gd::String &oldName,
const gd::String &newName);
/**
* \brief Refactor the project after an object effect is renamed.
*/
static void RenameObjectEffectInEventsBasedObject(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
const gd::String &oldName, const gd::String &newName);
/**
* \brief Refactor the project after an object is renamed in a layout
@@ -380,11 +446,11 @@ class GD_CORE_API WholeProjectRefactorer {
* This will update the layout, all external layouts associated with it
* and all external events associated with it.
*/
static void ObjectOrGroupRenamedInLayout(gd::Project& project,
gd::Layout& layout,
const gd::String& oldName,
const gd::String& newName,
bool isObjectGroup);
static void ObjectOrGroupRenamedInScene(gd::Project &project,
gd::Layout &scene,
const gd::String &oldName,
const gd::String &newName,
bool isObjectGroup);
/**
* \brief Refactor the project after an object is removed in a layout
@@ -392,9 +458,8 @@ class GD_CORE_API WholeProjectRefactorer {
* This will update the layout, all external layouts associated with it
* and all external events associated with it.
*/
static void ObjectRemovedInLayout(gd::Project& project,
gd::Layout& layout,
const gd::String& objectName);
static void ObjectRemovedInScene(gd::Project &project, gd::Layout &scene,
const gd::String &objectName);
/**
* \brief Refactor the project after behaviors are added to an object in a
@@ -404,9 +469,9 @@ class GD_CORE_API WholeProjectRefactorer {
* The refactor is actually applied to all objects because it allow to handle
* groups.
*/
static void BehaviorsAddedToObjectInLayout(gd::Project &project,
gd::Layout &layout,
const gd::String &objectName);
static void BehaviorsAddedToObjectInScene(gd::Project &project,
gd::Layout &layout,
const gd::String &objectName);
/**
* \brief Refactor the project after an object is removed in an events-based

View File

@@ -30,6 +30,8 @@ gd::String Variable::TypeAsString(Type t) {
return "structure";
case Type::Array:
return "array";
case Type::MixedTypes:
return "mixed";
default:
return "error-type";
}
@@ -46,6 +48,8 @@ Variable::Type Variable::StringAsType(const gd::String& str) {
return Type::Structure;
else if (str == "array")
return Type::Array;
else if (str == "mixed")
return Type::MixedTypes;
// Default to number
return Type::Number;
@@ -56,6 +60,7 @@ bool Variable::IsPrimitive(const Type type) {
}
void Variable::CastTo(const Type newType) {
hasMixedValues = false;
if (newType == Type::Number)
SetValue(GetValue());
else if (newType == Type::String)
@@ -85,6 +90,9 @@ void Variable::CastTo(const Type newType) {
type = Type::Array;
// Free now unused memory
children.clear();
} else if (newType == Type::MixedTypes) {
type = Type::MixedTypes;
hasMixedValues = true;
}
}
@@ -142,6 +150,7 @@ Variable& Variable::GetChild(const gd::String& name) {
if (it != children.end()) return *it->second;
type = Type::Structure;
hasMixedValues = false;
children[name] = std::make_shared<gd::Variable>();
return *children[name];
}
@@ -202,6 +211,7 @@ Variable& Variable::PushNew() {
const size_t count = GetChildrenCount();
auto& variable = GetAtIndex(count);
if (type == Type::Array && count > 0) {
hasMixedValues = false;
const auto childType = GetAtIndex(count - 1).type;
variable.type = childType;
if (childType == Type::Number) {
@@ -224,6 +234,7 @@ void Variable::RemoveAtIndex(const size_t index) {
bool Variable::InsertAtIndex(const gd::Variable& variable, const size_t index) {
if (type != Type::Array) return false;
hasMixedValues = false;
auto newVariable = std::make_shared<gd::Variable>(variable);
if (index < childrenArray.size()) {
childrenArray.insert(childrenArray.begin() + index, newVariable);
@@ -238,6 +249,7 @@ bool Variable::InsertChild(const gd::String& name,
if (type != Type::Structure || HasChild(name)) {
return false;
}
hasMixedValues = false;
children[name] = std::make_shared<gd::Variable>(variable);
return true;
};
@@ -270,6 +282,9 @@ void Variable::SerializeTo(SerializerElement& element) const {
child->SerializeTo(childrenElement.AddChild("variable"));
}
}
if (hasMixedValues) {
element.SetBoolAttribute("hasMixedValues", true);
}
}
void Variable::UnserializeFrom(const SerializerElement& element) {
@@ -313,6 +328,9 @@ void Variable::UnserializeFrom(const SerializerElement& element) {
PushNew().UnserializeFrom(childElement);
}
}
if (element.GetBoolAttribute("hasMixedValues", false)) {
MarkAsMixedValues();
}
}
Variable& Variable::ResetPersistentUuid() {
@@ -384,7 +402,8 @@ Variable::Variable(const Variable& other)
folded(other.folded),
boolVal(other.boolVal),
type(other.type),
persistentUuid(other.persistentUuid) {
persistentUuid(other.persistentUuid),
hasMixedValues(other.hasMixedValues) {
CopyChildren(other);
}
@@ -396,6 +415,7 @@ Variable& Variable::operator=(const Variable& other) {
boolVal = other.boolVal;
type = other.type;
persistentUuid = other.persistentUuid;
hasMixedValues = other.hasMixedValues;
CopyChildren(other);
}
@@ -411,4 +431,61 @@ void Variable::CopyChildren(const gd::Variable& other) {
childrenArray.push_back(std::make_shared<gd::Variable>(*child.get()));
}
}
bool Variable::operator==(const gd::Variable &variable) const {
if (type != variable.type || hasMixedValues || variable.hasMixedValues) {
return false;
}
if (type == Variable::Type::Number) {
return value == variable.value;
}
if (type == Variable::Type::String) {
return str == variable.str;
}
if (type == Variable::Type::Boolean) {
return boolVal == variable.boolVal;
}
if (type == Variable::Type::Structure) {
if (children.size() != variable.children.size()) {
return false;
}
for (auto &pair : children) {
const gd::String &name = pair.first;
const auto &child = pair.second;
auto it = variable.children.find(name);
if (it == children.end()) {
return false;
}
auto &otherChild = it->second;
if (*child != *otherChild) {
return false;
}
}
return true;
}
if (type == Variable::Type::Array) {
if (childrenArray.size() != variable.childrenArray.size()) {
return false;
}
for (int i = 0; i < childrenArray.size(); ++i) {
if (*childrenArray[i] != *variable.childrenArray[i]) {
return false;
}
}
return true;
}
// MixedTypes variables can't equal another variable.
return false;
}
bool Variable::operator!=(const gd::Variable &variable) const {
return !(*this == variable);
}
void Variable::MarkAsMixedValues() {
hasMixedValues = true;
ClearChildren();
}
} // namespace gd

View File

@@ -4,8 +4,8 @@
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_VARIABLE_H
#define GDCORE_VARIABLE_H
#pragma once
#include <cmath>
#include <map>
#include <memory>
@@ -31,6 +31,8 @@ class GD_CORE_API Variable {
static gd::Variable badVariable;
enum Type {
Unknown,
/** Used when objects of a group have different types for a variable. */
MixedTypes,
// Primitive types
String,
@@ -50,7 +52,7 @@ class GD_CORE_API Variable {
/**
* \brief Default constructor creating a variable with 0 as value.
*/
Variable() : value(0), type(Type::Number){};
Variable() : value(0), type(Type::Number), hasMixedValues(false) {};
Variable(const Variable&);
virtual ~Variable(){};
@@ -87,6 +89,7 @@ class GD_CORE_API Variable {
void SetString(const gd::String& newStr) {
str = newStr;
type = Type::String;
hasMixedValues = false;
}
/**
@@ -102,6 +105,7 @@ class GD_CORE_API Variable {
// NaN values are not supported by GDevelop nor the serializer.
if (std::isnan(value)) value = 0.0;
type = Type::Number;
hasMixedValues = false;
}
/**
@@ -115,8 +119,23 @@ class GD_CORE_API Variable {
void SetBool(bool val) {
boolVal = val;
type = Type::Boolean;
hasMixedValues = false;
}
/**
* \brief Return true when objects of a group have different values for a
* variable.
*/
bool HasMixedValues() const {
return hasMixedValues;
}
/**
* \brief Return true when objects of a group have different values for a
* variable.
*/
void MarkAsMixedValues();
// Operators are overloaded to allow accessing to variable using a simple
// int-like semantic.
void operator=(double val) { SetValue(val); };
@@ -168,6 +187,9 @@ class GD_CORE_API Variable {
bool operator==(const bool val) const { return GetBool() == val; };
bool operator!=(const bool val) const { return GetBool() != val; };
bool operator==(const gd::Variable& variable) const;
bool operator!=(const gd::Variable& variable) const;
///@}
/** \name Collection types
@@ -376,6 +398,7 @@ class GD_CORE_API Variable {
mutable gd::String str;
mutable double value;
mutable bool boolVal;
mutable bool hasMixedValues;
mutable std::map<gd::String, std::shared_ptr<Variable>>
children; ///< Children, when the variable is considered as a structure.
mutable std::vector<std::shared_ptr<Variable>>
@@ -392,5 +415,3 @@ class GD_CORE_API Variable {
};
} // namespace gd
#endif // GDCORE_VARIABLE_H

View File

@@ -55,8 +55,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object or expression completions when type is string") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("string", "My", 0, 2).ToString()
};
// clang-format on
@@ -67,8 +67,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object or expression completions when type is number") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, number, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, number, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number", "My", 0, 2).ToString()
};
// clang-format on
@@ -79,8 +79,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object or expression completions when type is number|string") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, number|string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, number|string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number|string", "My", 0, 2).ToString()
};
// clang-format on
@@ -94,8 +94,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object or expression completions in a variable name") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("string", "My", 0, 2).ToString()
};
// clang-format on
@@ -115,8 +115,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object or expression completions in a variable index") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, number, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, number, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number", "My", 0, 2).ToString()
};
// clang-format on
@@ -136,7 +136,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object when type is an object") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, object, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 0, object, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
};
// clang-format on
REQUIRE(getCompletionsFor("object", "My", 0) == expectedCompletions);
@@ -149,7 +149,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
// result in different code generation):
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, objectPtr, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 0, objectPtr, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
};
// clang-format on
REQUIRE(getCompletionsFor("objectPtr", "My", 0) == expectedCompletions);
@@ -204,8 +204,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 0, unknown, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 0, unknown, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("unknown", "My", 9, 10).ToString()
};
// clang-format on
@@ -215,7 +215,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Function with a Variable as argument") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
};
// clang-format on
REQUIRE(getCompletionsFor("number",
@@ -225,7 +225,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Object function with a Variable as argument") {
// clang-format off
std::vector<gd::String> expectedCompletions{
"{ 3, no type, 2, no prefix, myObjectVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
"{ 3, no type, 3, no prefix, myObjectVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
};
// clang-format on
REQUIRE(getCompletionsFor("number",
@@ -254,7 +254,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test with string type") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
@@ -277,7 +277,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test with 'number|string' type") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, number|string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, number|string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
@@ -303,7 +303,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test 1") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
@@ -336,7 +336,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test 1") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()};
@@ -366,7 +366,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test 2") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()
@@ -396,7 +396,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
SECTION("Test 1") {
// clang-format off
std::vector<gd::String> expectedObjectCompletions{
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
};
std::vector<gd::String> expectedBehaviorCompletions{
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()

View File

@@ -91,4 +91,49 @@ TEST_CASE("Variable", "[common][variables]") {
"Hello second copied World");
REQUIRE(variable3.GetChild("Child2").GetValue() == 44);
}
SECTION("Can find identical number variables") {
gd::Variable variable;
variable.SetValue(123);
gd::Variable otherVariable;
otherVariable.SetValue(123);
REQUIRE(variable == otherVariable);
}
SECTION("Can find different number variables") {
gd::Variable variable;
variable.SetValue(123);
gd::Variable otherVariable;
otherVariable.SetValue(456);
REQUIRE(variable != otherVariable);
}
SECTION("Can find identical structure variables") {
gd::Variable variable;
variable.GetChild("MyChild").SetValue(123);
gd::Variable otherVariable;
otherVariable.GetChild("MyChild").SetValue(123);
REQUIRE(variable == otherVariable);
}
SECTION("Can find structure with different child value") {
gd::Variable variable;
variable.GetChild("MyChild").SetValue(123);
gd::Variable otherVariable;
otherVariable.GetChild("MyChild").SetValue(456);
REQUIRE(variable != otherVariable);
}
SECTION("Can find structure with different child name") {
gd::Variable variable;
variable.GetChild("MyChild").SetValue(123);
gd::Variable otherVariable;
otherVariable.GetChild("MyOtherChild").SetValue(123);
REQUIRE(variable != otherVariable);
}
}

View File

@@ -1228,7 +1228,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
layout1.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "Object2", 0);
gd::WholeProjectRefactorer::ObjectRemovedInLayout(project, layout1,
gd::WholeProjectRefactorer::ObjectRemovedInScene(project, layout1,
"Object1");
gd::WholeProjectRefactorer::GlobalObjectRemoved(project, "GlobalObject1");
REQUIRE(layout1.GetObjects().GetObjectGroups()[0].Find(
@@ -1262,7 +1262,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
layout1.GetInitialInstances().InsertInitialInstance(instance2);
layout1.GetInitialInstances().InsertInitialInstance(instance3);
gd::WholeProjectRefactorer::ObjectRemovedInLayout(
gd::WholeProjectRefactorer::ObjectRemovedInScene(
project, layout1, "Object1");
gd::WholeProjectRefactorer::GlobalObjectRemoved(
project, "GlobalObject1");
@@ -1306,7 +1306,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
externalLayout2.GetInitialInstances().InsertInitialInstance(instance2);
externalLayout2.GetInitialInstances().InsertInitialInstance(instance3);
gd::WholeProjectRefactorer::ObjectRemovedInLayout(
gd::WholeProjectRefactorer::ObjectRemovedInScene(
project, layout1, "Object1");
gd::WholeProjectRefactorer::GlobalObjectRemoved(
project, "GlobalObject1");
@@ -1344,7 +1344,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
layout1.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "Object2", 0);
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
@@ -1381,7 +1381,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
layout1.GetInitialInstances().InsertInitialInstance(instance2);
layout1.GetInitialInstances().InsertInitialInstance(instance3);
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
@@ -1427,7 +1427,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
externalLayout2.GetInitialInstances().InsertInitialInstance(instance2);
externalLayout2.GetInitialInstances().InsertInitialInstance(instance3);
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
@@ -1462,7 +1462,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
auto &layout = project.GetLayout("Scene");
// Trigger the refactoring after the renaming of an object
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
project, layout, "ObjectWithMyBehavior",
"RenamedObjectWithMyBehavior",
/* isObjectGroup=*/false);
@@ -1490,7 +1490,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
auto &layout = project.GetLayout("Scene");
// Trigger the refactoring after the renaming of a group
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
project, layout, "GroupWithMyBehavior", "RenamedGroupWithMyBehavior",
/* isObjectGroup=*/true);
@@ -1534,7 +1534,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
// Attach the behavior to the object.
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(project, scene,
gd::WholeProjectRefactorer::BehaviorsAddedToObjectInScene(project, scene,
"Object");
// The behavior parameter is now filled.
@@ -3584,7 +3584,7 @@ CreateExpressionWithLayerParameter(gd::Project &project,
} // namespace
TEST_CASE("RenameLayer", "[common]") {
SECTION("Can update layer names in events") {
SECTION("Can update layer names in scene events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
@@ -3616,7 +3616,7 @@ TEST_CASE("RenameLayer", "[common]") {
auto &otherExternalExpression = CreateExpressionWithLayerParameter(
project, otherExternalEvents.GetEvents(), "My layer");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
"My renamed layer");
REQUIRE(layoutAction.GetParameter(3).GetPlainString() ==
@@ -3644,6 +3644,38 @@ TEST_CASE("RenameLayer", "[common]") {
"MyExtension::CameraCenterX(\"My layer\")");
}
SECTION("Can update layer names in event-based object events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
"MyEventsBasedObject", 0);
auto &eventsFunction =
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyEventsFunction", 0);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
eventsExtension, eventsBasedObject);
auto &action =
CreateActionWithLayerParameter(project, eventsFunction.GetEvents());
auto &expression = CreateExpressionWithLayerParameter(
project, eventsFunction.GetEvents(), "My layer");
gd::WholeProjectRefactorer::RenameLayerInEventsBasedObject(
project, eventsExtension, eventsBasedObject, "My layer",
"My renamed layer");
REQUIRE(action.GetParameter(3).GetPlainString() == "\"My renamed layer\"");
REQUIRE(expression.GetParameter(0).GetPlainString() ==
"MyExtension::CameraCenterX(\"My renamed layer\") + "
"MyExtension::CameraCenterX(\"My renamed layer\")");
}
SECTION("Can update layer names in expressions with a smaller name") {
gd::Project project;
gd::Platform platform;
@@ -3654,7 +3686,7 @@ TEST_CASE("RenameLayer", "[common]") {
auto &layoutExpression =
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "My layer");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
"layerA");
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
@@ -3662,7 +3694,7 @@ TEST_CASE("RenameLayer", "[common]") {
"MyExtension::CameraCenterX(\"layerA\")");
}
SECTION("Renaming a layer also moves the instances on this layer and of the associated external layouts") {
SECTION("Renaming a layer also moves the instances on this layer in its scene and associated external layouts") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
@@ -3713,7 +3745,7 @@ TEST_CASE("RenameLayer", "[common]") {
REQUIRE(otherInitialInstance1.GetLayer() == "My layer");
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer", "My new layer");
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer", "My new layer");
// Instances on the renamed layer are moved to the new layer.
REQUIRE(initialInstance1.GetLayer() == "My new layer");
@@ -3728,6 +3760,46 @@ TEST_CASE("RenameLayer", "[common]") {
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
}
SECTION("Renaming a layer also moves the instances on this layer in its "
"event-based object") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
"MyEventsBasedObject", 0);
auto &eventsFunction =
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyEventsFunction", 0);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
eventsExtension, eventsBasedObject);
eventsBasedObject.GetLayers().InsertNewLayer("My layer", 0);
auto &initialInstances = eventsBasedObject.GetInitialInstances();
auto &initialInstance1 = initialInstances.InsertNewInitialInstance();
initialInstance1.SetLayer("My layer");
auto &initialInstance2 = initialInstances.InsertNewInitialInstance();
initialInstance2.SetLayer("My layer");
auto &initialInstance3 = initialInstances.InsertNewInitialInstance();
initialInstance3.SetLayer("");
REQUIRE(initialInstance1.GetLayer() == "My layer");
REQUIRE(initialInstance2.GetLayer() == "My layer");
REQUIRE(initialInstance3.GetLayer() == "");
gd::WholeProjectRefactorer::RenameLayerInEventsBasedObject(
project, eventsExtension, eventsBasedObject, "My layer",
"My new layer");
// Instances on the renamed layer are moved to the new layer.
REQUIRE(initialInstance1.GetLayer() == "My new layer");
REQUIRE(initialInstance2.GetLayer() == "My new layer");
REQUIRE(initialInstance3.GetLayer() == "");
}
SECTION("Can rename a layer when a layer parameter is empty") {
gd::Project project;
gd::Platform platform;
@@ -3738,7 +3810,7 @@ TEST_CASE("RenameLayer", "[common]") {
auto &layoutAction =
CreateActionWithEmptyLayerParameter(project, layout.GetEvents());
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
"layerA");
REQUIRE(layoutAction.GetParameter(0).GetPlainString() == "");
@@ -3754,7 +3826,7 @@ TEST_CASE("RenameLayer", "[common]") {
auto &layoutExpression =
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "My layer");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
"");
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
@@ -3772,7 +3844,7 @@ TEST_CASE("RenameLayer", "[common]") {
auto &layoutExpression =
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "", "My layer");
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "", "My layer");
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
"MyExtension::CameraCenterX(\"\") + "
@@ -3813,7 +3885,7 @@ CreateExpressionWithAnimationParameter(gd::Project &project,
} // namespace
TEST_CASE("RenameObjectAnimation", "[common]") {
SECTION("Can update object animation names in event") {
SECTION("Can update object animation names in scene events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
@@ -3855,7 +3927,7 @@ TEST_CASE("RenameObjectAnimation", "[common]") {
auto &wrongObjectExpression =
CreateExpressionWithAnimationParameter(project, layout.GetEvents(), "MySprite2");
gd::WholeProjectRefactorer::RenameObjectAnimation(project, layout, object, "My animation",
gd::WholeProjectRefactorer::RenameObjectAnimationInScene(project, layout, object, "My animation",
"My renamed animation");
REQUIRE(layoutAction.GetParameter(1).GetPlainString() ==
@@ -3887,6 +3959,53 @@ TEST_CASE("RenameObjectAnimation", "[common]") {
"MySprite2.AnimationFrameCount(\"My animation\") + "
"MySprite2.AnimationFrameCount(\"My animation\")");
}
SECTION("Can update object animation names in events-based object events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
"MyEventsBasedObject", 0);
auto &eventsFunction =
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyEventsFunction", 0);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
eventsExtension, eventsBasedObject);
auto &object = eventsBasedObject.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MySprite", 0);
eventsBasedObject.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MySprite2", 1);
auto &action = CreateActionWithAnimationParameter(
project, eventsFunction.GetEvents(), "MySprite");
auto &wrongObjectAction = CreateActionWithAnimationParameter(
project, eventsFunction.GetEvents(), "MySprite2");
auto &expression = CreateExpressionWithAnimationParameter(
project, eventsFunction.GetEvents(), "MySprite");
auto &wrongObjectExpression = CreateExpressionWithAnimationParameter(
project, eventsFunction.GetEvents(), "MySprite2");
gd::WholeProjectRefactorer::RenameObjectAnimationInEventsBasedObject(
project, eventsExtension, eventsBasedObject, object, "My animation",
"My renamed animation");
REQUIRE(action.GetParameter(1).GetPlainString() ==
"\"My renamed animation\"");
REQUIRE(wrongObjectAction.GetParameter(1).GetPlainString() ==
"\"My animation\"");
REQUIRE(expression.GetParameter(0).GetPlainString() ==
"MySprite.AnimationFrameCount(\"My renamed animation\") + "
"MySprite.AnimationFrameCount(\"My renamed animation\")");
REQUIRE(wrongObjectExpression.GetParameter(0).GetPlainString() ==
"MySprite2.AnimationFrameCount(\"My animation\") + "
"MySprite2.AnimationFrameCount(\"My animation\")");
}
}
namespace {
@@ -3922,7 +4041,7 @@ CreateExpressionWithLayerEffectParameter(gd::Project &project,
} // namespace
TEST_CASE("RenameLayerEffect", "[common]") {
SECTION("Can update layer effect names in event") {
SECTION("Can update layer effect names in scene events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
@@ -3966,7 +4085,7 @@ TEST_CASE("RenameLayerEffect", "[common]") {
auto &wrongLayerExpression =
CreateExpressionWithLayerEffectParameter(project, layout.GetEvents(), "My layer 2");
gd::WholeProjectRefactorer::RenameLayerEffect(project, layout, layer, "My effect",
gd::WholeProjectRefactorer::RenameLayerEffectInScene(project, layout, layer, "My effect",
"My renamed effect");
REQUIRE(layoutAction.GetParameter(2).GetPlainString() ==
@@ -3998,10 +4117,61 @@ TEST_CASE("RenameLayerEffect", "[common]") {
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\") + "
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\")");
}
SECTION("Can update layer effect names in events-based object events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
"MyEventsBasedObject", 0);
auto &eventsFunction =
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyEventsFunction", 0);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
eventsExtension, eventsBasedObject);
eventsBasedObject.GetLayers().InsertNewLayer("My layer", 0);
auto &layer = eventsBasedObject.GetLayers().GetLayer("My layer");
auto &object = eventsBasedObject.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MySprite", 0);
eventsBasedObject.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MySprite2", 1);
auto &action = CreateActionWithLayerEffectParameter(
project, eventsFunction.GetEvents(), "My layer");
auto &wrongLayerAction = CreateActionWithLayerEffectParameter(
project, eventsFunction.GetEvents(), "My layer 2");
auto &expression = CreateExpressionWithLayerEffectParameter(
project, eventsFunction.GetEvents(), "My layer");
auto &wrongLayerExpression = CreateExpressionWithLayerEffectParameter(
project, eventsFunction.GetEvents(), "My layer 2");
gd::WholeProjectRefactorer::RenameLayerEffectInEventsBasedObject(
project, eventsExtension, eventsBasedObject, layer, "My effect",
"My renamed effect");
REQUIRE(action.GetParameter(2).GetPlainString() == "\"My renamed effect\"");
REQUIRE(wrongLayerAction.GetParameter(2).GetPlainString() ==
"\"My effect\"");
REQUIRE(expression.GetParameter(0).GetPlainString() ==
"MyExtension::LayerEffectParameter(\"My layer\", \"My renamed "
"effect\") + "
"MyExtension::LayerEffectParameter(\"My layer\", \"My renamed "
"effect\")");
REQUIRE(
wrongLayerExpression.GetParameter(0).GetPlainString() ==
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\") + "
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\")");
}
}
TEST_CASE("RemoveLayer", "[common]") {
SECTION("Can remove instances from a layer (in a scene and their associated external layouts)") {
SECTION("Can remove instances from a layer (in a scene and its associated external layouts)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
@@ -4086,7 +4256,7 @@ TEST_CASE("RemoveLayer", "[common]") {
}
TEST_CASE("MergeLayers", "[common]") {
SECTION("Can merge instances from a layer into another layer (in a scene and their associated external layouts)") {
SECTION("Can merge instances from a layer into another layer (in a scene and its associated external layouts)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);

View File

@@ -38,7 +38,7 @@ module.exports = {
.setName('Consent Cordova plugin')
.setDependencyType('cordova')
.setExportName('cordova-plugin-consent')
.setVersion('2.4.0')
.setVersion('3.0.0-alpha.8')
.onlyIfOtherDependencyIsExported('AdMob Cordova plugin');
extension
@@ -58,7 +58,7 @@ module.exports = {
.setName('AdMob Cordova plugin')
.setDependencyType('cordova')
.setExportName('admob-plus-cordova')
.setVersion('1.28.0')
.setVersion('2.0.0-alpha.18')
.setExtraSetting(
'APP_ID_ANDROID',
new gd.PropertyDescriptor('AdMobAppIdAndroid').setType(
@@ -166,11 +166,11 @@ module.exports = {
)
.addParameter('string', _('Android app open ID'), '', false)
.setParameterLongDescription(
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/3419835294"` for loading a test app open.'
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/9257395921"` for loading a test app open.'
)
.addParameter('string', _('iOS app open ID'), '', false)
.setParameterLongDescription(
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5662855259"` for loading a test app open.'
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5575463023"` for loading a test app open.'
)
.addParameter(
'yesorno',

View File

@@ -7,8 +7,8 @@ namespace gdjs {
const testAdIds = {
appOpen: {
android: 'ca-app-pub-3940256099942544/3419835294',
ios: 'ca-app-pub-3940256099942544/5662855259',
android: 'ca-app-pub-3940256099942544/9257395921',
ios: 'ca-app-pub-3940256099942544/5575463023',
},
banner: {
android: 'ca-app-pub-3940256099942544/6300978111',
@@ -67,6 +67,7 @@ namespace gdjs {
// Admob does not initialize automatically, so we store a flag to know if it's initialized.
let admobStarted = false;
let isStarting = false;
let isUsingTestAds = false;
// Banner
@@ -115,9 +116,13 @@ namespace gdjs {
async () => {
// Obtain user consent ?
logger.info('Starting AdMob.');
isStarting = true;
await admob.start();
logger.info('AdMob successfully started.');
isStarting = false;
admobStarted = true;
},
false
@@ -126,15 +131,32 @@ namespace gdjs {
/**
* Helper to know if we are on mobile and admob is correctly initialized.
*/
const checkIfAdMobIsAvailable = () => {
const checkIfAdMobIsAvailable = async () => {
if (typeof cordova === 'undefined') {
logger.warn('We are not on mobile, AdMob will not be available.');
return false;
}
if (typeof admob === 'undefined' || !admobStarted) {
logger.warn('AdMob has not been configured or started properly.');
if (typeof admob === 'undefined') {
logger.warn('AdMob has not been configured properly.');
return false;
}
if (!admobStarted) {
if (isStarting) {
// Delay the call until AdMob is started, up to 5 seconds.
let time = 0;
while (!admobStarted && time < 5000) {
await new Promise((resolve) => setTimeout(resolve, 100));
time += 100;
}
}
if (!admobStarted) {
logger.warn('AdMob is not started.');
return false;
}
}
return true;
};
@@ -164,8 +186,10 @@ namespace gdjs {
* charging advertisers. If you click on too many ads without being in test mode, you risk your
* account being flagged for invalid activity.
*/
export const setTestMode = (enable: boolean) => {
if (!checkIfAdMobIsAvailable()) return;
export const setTestMode = async (enable: boolean) => {
if (!(await checkIfAdMobIsAvailable())) return;
logger.info('Setting AdMob test mode to:', enable);
isUsingTestAds = enable;
};
@@ -185,7 +209,7 @@ namespace gdjs {
displayLandscape,
displayWhenLoaded
) => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
// If an appOpen is already loading or showing, we don't stop it.
if (appOpenLoading || appOpenShowing) {
return;
@@ -242,7 +266,7 @@ namespace gdjs {
/** Show the loaded appOpen. */
export const showAppOpen = async () => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (!appOpen) {
logger.warn('App Open has not been set up, call loadAppOpen first.');
@@ -293,7 +317,7 @@ namespace gdjs {
* If a banner is already set up, it will be hidden and replaced by the new one.
*/
export const setupBanner = async (androidAdUnitId, iosAdUnitId, atTop) => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
const adUnitId = getAdUnitId(androidAdUnitId, iosAdUnitId, 'banner');
if (!adUnitId) return;
@@ -354,7 +378,7 @@ namespace gdjs {
/** Hide the banner shown on screen. */
export const hideBanner = async () => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (!banner || !bannerShowing) {
logger.warn('No banner is being shown.');
@@ -381,7 +405,7 @@ namespace gdjs {
iosAdUnitId,
displayWhenLoaded
) => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
// If an interstitial is already loading or showing, we don't stop it.
if (interstitialLoading || interstitialShowing) {
return;
@@ -440,7 +464,7 @@ namespace gdjs {
/** Show the loaded interstitial. */
export const showInterstitial = async () => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (!interstitial) {
logger.warn(
@@ -495,7 +519,7 @@ namespace gdjs {
iosAdUnitID,
displayWhenLoaded
) => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (rewardedInterstitialLoading || rewardedInterstitialShowing) {
return;
}
@@ -557,7 +581,7 @@ namespace gdjs {
/** Show the loaded reward interstitial. */
export const showRewardedInterstitial = async () => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (!rewardedInterstitial) {
logger.warn(
@@ -614,7 +638,7 @@ namespace gdjs {
iosAdUnitID,
displayWhenLoaded
) => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (rewardedVideoLoading || rewardedVideoShowing) {
return;
}
@@ -672,7 +696,7 @@ namespace gdjs {
/** Show the loaded reward video. */
export const showRewardedVideo = async () => {
if (!checkIfAdMobIsAvailable()) return;
if (!(await checkIfAdMobIsAvailable())) return;
if (!rewardedVideo) {
logger.warn('Video has not been set up, call loadRewardedVideo first.');

View File

@@ -417,7 +417,7 @@ module.exports = {
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerLeft');
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustLeft');
extension
.addCondition(
@@ -443,7 +443,108 @@ module.exports = {
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerLeft');
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustLeft');
extension
.addExpression(
'LastLeftPlayerNumber',
_('Last left player number'),
_('Returns the number of the player that has just left the lobby.'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft'
);
extension
.addCondition(
'HasAnyPlayerJoined',
_('Any player has joined'),
_('Check if any player has joined the lobby.'),
_('Any player has joined'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustJoined');
extension
.addCondition(
'HasPlayerJoined',
_('Player has joined'),
_('Check if the player has joined the lobby.'),
_('Player _PARAM0_ has joined'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.addParameter('number', _('Player number'), '', false)
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustJoined');
extension
.addExpression(
'LastJoinedPlayerNumber',
_('Last joined player number'),
_('Returns the number of the player that has just joined the lobby.'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined'
);
extension
.addStrExpression(
@@ -520,6 +621,37 @@ module.exports = {
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('gdjs.multiplayer.getPlayersInLobbyCount');
extension
.addCondition(
'IsPlayerConnected',
_('Player is connected'),
_('Check if the specified player is connected to the lobby.'),
_('Player _PARAM0_ is connected'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter(
'number',
_('The position of the player in the lobby (1, 2, ...)'),
'',
false
)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.isPlayerConnected');
extension
.addExpressionAndCondition(
'number',

View File

@@ -34,8 +34,66 @@ namespace gdjs {
this.cache.add(key);
this.keys.push(key);
}
clear = () => {
this.cache.clear();
this.keys = [];
};
}
class SavedSyncDataUpdates<T> {
private _updates: T[] = [];
store(update: T) {
this._updates.push(update);
if (this._updates.length > 10) {
this._updates.shift();
}
}
getUpdates() {
return this._updates;
}
remove(update: T) {
const index = this._updates.indexOf(update);
if (index !== -1) {
this._updates.splice(index, 1);
}
}
clear() {
this._updates = [];
}
}
/**
* Helper function to clone an object without reassigning the target object.
* It's mainly helpful for tests, where multiple instances of the MultiplayerMessageManager are created,
* and prevents keeping references to the same object.
*/
const cloneObjectWithoutOverwriting = ({
target,
source,
}: {
target: Object;
source: Object;
}) => {
// Add the new properties.
for (const key in source) {
if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
target[key] = source[key];
}
}
// Remove the properties that are not in the source.
for (const key in target) {
if (target.hasOwnProperty(key) && !source.hasOwnProperty(key)) {
delete target[key];
}
}
};
export type MultiplayerMessageManager = ReturnType<
typeof makeMultiplayerMessageManager
>;
@@ -91,24 +149,39 @@ namespace gdjs {
let lastSceneSyncTimestamp = 0;
let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;
let numberOfForcedSceneUpdates = 0;
let lastReceivedSceneSyncDataUpdates = new SavedSyncDataUpdates<
LayoutNetworkSyncData
>();
// The number of times per second the game data should be synchronized.
const gameSyncDataTickRate = 1;
let lastGameSyncTimestamp = 0;
let lastSentGameSyncData: GameNetworkSyncData | null = null;
let numberOfForcedGameUpdates = 0;
let lastReceivedGameSyncDataUpdates = new SavedSyncDataUpdates<
GameNetworkSyncData
>();
// Send heartbeat messages to host to ensure the connection is still alive.
// Send heartbeat messages from host to players, ensuring their connection is still alive,
// measure the ping, and send other useful info.
const heartbeatTickRate = 1;
let lastHeartbeatTimestamp = 0;
let _playersLastHeartbeatInfo: {
[playerNumber: number]: {
lastRoundTripTimes: number[];
};
let _playersLastRoundTripTimes: {
[playerNumber: number]: number[];
} = {};
let _peerIdToPlayerNumber: { [peerId: string]: number } = {};
let _playersPings: { [playerNumber: number]: number } = { 1: 0 };
let _playersInfo: {
[playerNumber: number]: {
ping: number;
playerId: string;
username: string;
};
} = {};
let _playerNumbersWhoJustLeft: number[] = [];
let _playerNumbersWhoJustJoined: number[] = [];
let _temporaryPlayerNumberToUsername: {
[playerNumber: number]: string;
} = {};
const addExpectedMessageAcknowledgement = ({
originalMessageName,
@@ -157,11 +230,6 @@ namespace gdjs {
});
};
const clearExpectedMessageAcknowledgements = () => {
expectedMessageAcknowledgements = {};
_lastClockReceivedByInstanceByScene = {};
};
const getLastClockReceivedForInstanceOnScene = ({
sceneNetworkId,
instanceNetworkId,
@@ -290,27 +358,6 @@ namespace gdjs {
(instance) => instance.networkId === instanceNetworkId
) || null;
if (!instance) {
debugLogger.info(
`instance ${objectName} ${instanceNetworkId} not found with network ID, trying to find it with persistent UUID.`
);
instance =
instances.find(
(instance) =>
// For objects created from the start, the network ID is not set yet.
instance.persistentUuid &&
instance.persistentUuid.substring(0, 8) === instanceNetworkId
) || null;
if (instance) {
debugLogger.info(
`instance ${objectName} ${instanceNetworkId} found with persistent UUID. Assigning network ID.`
);
// Set the network ID, as it was not set yet.
instance.networkId = instanceNetworkId;
}
}
// If we know the position of the object, we can try to find the closest instance not synchronized yet.
if (!instance && instanceX !== undefined && instanceY !== undefined) {
debugLogger.info(
@@ -411,6 +458,12 @@ namespace gdjs {
const handleChangeInstanceOwnerMessagesReceived = (
runtimeScene: gdjs.RuntimeScene
) => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
// the instance when receiving an update message with a different owner.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
@@ -563,6 +616,12 @@ namespace gdjs {
const handleUpdateInstanceMessagesReceived = (
runtimeScene: gdjs.RuntimeScene
) => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Update instance messages do not need to be saved for later use, as the updates are sent pretty often,
// a new one will be received very quickly.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
@@ -732,6 +791,12 @@ namespace gdjs {
const handleChangeVariableOwnerMessagesReceived = (
runtimeScene: gdjs.RuntimeScene
) => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
// the variable when receiving an update message with a different owner.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
@@ -884,6 +949,12 @@ namespace gdjs {
};
const handleAcknowledgeMessagesReceived = () => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
// which are not sent when the game is not ready.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
// When we receive acknowledgement messages, save it in the extension, to avoid sending the message again.
@@ -958,6 +1029,12 @@ namespace gdjs {
const resendClearOrCancelAcknowledgedMessages = (
runtimeScene: gdjs.RuntimeScene
) => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
// which are not sent when the game is not ready.
return;
}
// When all acknowledgments are received for an message, we can clear the message from our
// list of expected acknowledgments.
const expectedMessageNames = Object.keys(expectedMessageAcknowledgements);
@@ -1179,6 +1256,12 @@ namespace gdjs {
const handleDestroyInstanceMessagesReceived = (
runtimeScene: gdjs.RuntimeScene
) => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Destroy messages do not need to be saved for later use, as the game will automatically destroy
// the instance if it does not receive an update message from it. So we return early.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
const destroyInstanceMessageNames = messageNamesArray.filter(
@@ -1453,6 +1536,11 @@ namespace gdjs {
};
const handleCustomMessagesReceived = (): void => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Assume that the custom messages are not worth saving for later use.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const messageNamesArray = Array.from(p2pMessagesMap.keys());
const customMessageNames = messageNamesArray.filter((messageName) =>
@@ -1569,6 +1657,11 @@ namespace gdjs {
const handleUpdateSceneMessagesToSend = (
runtimeScene: gdjs.RuntimeScene
): void => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Don't send messages if the multiplayer is not ready.
return;
}
const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
});
@@ -1622,16 +1715,26 @@ namespace gdjs {
const messageSender = message.getSender();
const sceneNetworkId = messageData.id;
if (sceneNetworkId !== runtimeScene.networkId) {
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
if (sceneNetworkId !== runtimeScene.networkId) {
debugLogger.info(
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
);
// The scene is not the current scene.
return;
}
runtimeScene.updateFromNetworkSyncData(messageData);
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
debugLogger.info(
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
`Saving scene ${sceneNetworkId} update message for later use.`
);
// The scene is not the current scene.
lastReceivedSceneSyncDataUpdates.store(messageData);
return;
}
runtimeScene.updateFromNetworkSyncData(messageData);
// If we are are the host,
// we need to relay the scene update to others except the player who sent the update message.
if (gdjs.multiplayer.isPlayerHost()) {
@@ -1717,6 +1820,11 @@ namespace gdjs {
const handleUpdateGameMessagesToSend = (
runtimeScene: gdjs.RuntimeScene
): void => {
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
// Don't send messages if the multiplayer is not ready.
return;
}
const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
});
@@ -1768,7 +1876,15 @@ namespace gdjs {
messages.forEach((message) => {
const messageData = message.getData();
const messageSender = message.getSender();
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
debugLogger.info(`Saving game update message for later use.`);
lastReceivedGameSyncDataUpdates.store(messageData);
return;
}
// If we are are the host,
// we need to relay the game update to others except the player who sent the update message.
@@ -1785,23 +1901,59 @@ namespace gdjs {
});
};
const handleSavedUpdateMessages = (runtimeScene: gdjs.RuntimeScene) => {
// Reapply the game saved updates.
lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {
debugLogger.info(`Reapplying saved update of game.`);
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
});
// Game updates are always applied properly, so we can clear them.
lastReceivedGameSyncDataUpdates.clear();
// Then reapply the scene saved updates.
lastReceivedSceneSyncDataUpdates.getUpdates().forEach((messageData) => {
const sceneNetworkId = messageData.id;
if (sceneNetworkId !== runtimeScene.networkId) {
debugLogger.info(
`Trying to apply saved update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
);
// The scene is not the current scene.
return;
}
debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);
runtimeScene.updateFromNetworkSyncData(messageData);
// We only remove the message if it was successfully applied, so it can be reapplied later,
// in case we were not on the right scene.
lastReceivedSceneSyncDataUpdates.remove(messageData);
});
};
const heartbeatMessageNamePrefix = '#heartbeat';
const heartbeastMessageRegex = /#heartbeat#(.+)/;
const createHeartbeatMessage = (): {
messageName: string;
messageData: any;
} => {
const playersPings = {
1: 0, // Player 1 is the host, so we don't need to compute the ping.
// Ensure player 1 is correctly set when the first heartbeat is sent.
_playersInfo[1] = {
ping: 0, // Player 1 is the host, so we don't need to compute the ping.
playerId: gdjs.playerAuthentication.getUserId(),
username: gdjs.playerAuthentication.getUsername(),
};
for (const playerNumber in _playersLastHeartbeatInfo) {
playersPings[playerNumber] = getPlayerPing(parseInt(playerNumber, 10));
for (const playerNumber in _playersInfo) {
_playersInfo[playerNumber] = {
..._playersInfo[playerNumber],
ping: getPlayerPing(parseInt(playerNumber, 10)),
};
}
return {
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
messageData: {
now: getTimeNow(), // we send the current time to compute the ping.
playersPings,
playersInfo: _playersInfo,
},
};
};
@@ -1817,11 +1969,16 @@ namespace gdjs {
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
messageData: {
sentAt: heartbeatSentAt,
playerId: gdjs.playerAuthentication.getUserId(),
username: gdjs.playerAuthentication.getUsername(),
},
};
};
const hasSentHeartbeatRecently = () => {
return getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate;
return (
!!lastHeartbeatTimestamp &&
getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate
);
};
const handleHeartbeatsToSend = () => {
// Only host sends heartbeats to all players regularly:
@@ -1865,10 +2022,53 @@ namespace gdjs {
// Ensure we know who is who.
_peerIdToPlayerNumber[messageSender] = playerNumber;
// If we are not the host, save what the host told us about the pings and respond
// with a heartbeat immediately.
// If we are not the host, save what the host told us about the other players info
// and respond with a heartbeat immediately, informing the host of our playerId and username.
if (!gdjs.multiplayer.isPlayerHost()) {
_playersPings = messageData.playersPings;
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const currentlyKnownPlayerNumbers = Object.keys(
_playersInfo
).map((playerNumber) => parseInt(playerNumber, 10));
const receivedPlayerNumbers = Object.keys(
messageData.playersInfo
).map((playerNumber) => parseInt(playerNumber, 10));
const currentlyKnownPingForCurrentUser =
_playersInfo[currentPlayerNumber] &&
_playersInfo[currentPlayerNumber].ping;
// If there are no players info yet, we're probably just connecting.
// This can happen when joining a game that is already running.
// Do not handle this case to avoid displaying too many notifications.
if (!!currentlyKnownPlayerNumbers.length) {
// Look at the players info received to know if there are new players who just connected.
const newPlayerNumbers = receivedPlayerNumbers.filter(
(playerNumber) =>
!currentlyKnownPlayerNumbers.includes(playerNumber) &&
playerNumber !== currentPlayerNumber // Do not consider ourselves as a new player.
);
_playerNumbersWhoJustJoined.push(...newPlayerNumbers);
// Or players who have disconnected.
const playerNumbersWhoHaveDisconnected = currentlyKnownPlayerNumbers.filter(
(playerNumber) => !receivedPlayerNumbers.includes(playerNumber)
);
_playerNumbersWhoJustLeft.push(
...playerNumbersWhoHaveDisconnected
);
for (const playerNumber of playerNumbersWhoHaveDisconnected) {
// Temporarily save the username in another variable to be used for the notification,
// as we're deleting its playerInfo just after.
_temporaryPlayerNumberToUsername[
playerNumber
] = getPlayerUsername(playerNumber);
}
}
// Save the players info received from the host.
// Avoid overwriting the whole object as it can mess up tests that rely on the object reference.
cloneObjectWithoutOverwriting({
source: messageData.playersInfo,
target: _playersInfo,
});
const {
messageName: answerMessageName,
messageData: answerMessageData,
@@ -1876,27 +2076,44 @@ namespace gdjs {
heartbeatSentAt: messageData.now, // We send back the time we received, so that the host can compute the ping.
});
sendDataTo([messageSender], answerMessageName, answerMessageData);
// We have received a heartbeat from the host, informing us of our ping,
// so we can consider the connection as working.
if (
_playersInfo[currentPlayerNumber] !== undefined &&
_playersInfo[currentPlayerNumber].ping !== undefined
) {
gdjs.multiplayer.markConnectionAsConnected();
if (currentlyKnownPingForCurrentUser === undefined) {
// We just connected, let's add ourselves to the list of players who just connected,
// for the notification and the events.
_playerNumbersWhoJustJoined.push(currentPlayerNumber);
}
}
return;
}
// If we are the host, compute the pings based on:
// If we are the host.
// If this is a new player, we're about to send them their ping, so we can consider them connected.
if (!_playersInfo[playerNumber]) {
_playerNumbersWhoJustJoined.push(playerNumber);
}
// compute the pings based on:
// - the time we received the heartbeat.
// - the time the heartbeat was sent.
const now = getTimeNow();
const heartbeatSentAt = messageData.sentAt;
const roundTripTime = Math.round(now - heartbeatSentAt);
const playerLastHeartbeatInfo =
_playersLastHeartbeatInfo[playerNumber] || {};
const playerLastRoundTripTimes =
playerLastHeartbeatInfo.lastRoundTripTimes || [];
_playersLastRoundTripTimes[playerNumber] || [];
playerLastRoundTripTimes.push(roundTripTime);
if (playerLastRoundTripTimes.length > 5) {
// Keep only the last 5 RTT to compute the average.
playerLastRoundTripTimes.shift();
}
_playersLastHeartbeatInfo[playerNumber] = {
lastRoundTripTimes: playerLastRoundTripTimes,
};
_playersLastRoundTripTimes[playerNumber] = playerLastRoundTripTimes;
let sum = 0;
for (const lastRoundTripTime of playerLastRoundTripTimes) {
@@ -1905,19 +2122,30 @@ namespace gdjs {
const averagePing = Math.round(
sum / playerLastRoundTripTimes.length / 2 // Divide by 2 to get the one way ping.
);
_playersPings[playerNumber] = averagePing;
_playersInfo[playerNumber] = {
ping: averagePing,
playerId: messageData.playerId,
username: messageData.username,
};
// If there are new players, let's resend a heartbeat right away so that everyone is aware of them
// on approximately the same frame.
if (_playerNumbersWhoJustJoined.length) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const { messageName, messageData } = createHeartbeatMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
lastHeartbeatTimestamp = getTimeNow();
}
});
});
};
const getPlayerPing = (playerNumber: number) => {
if (playerNumber < 1) {
// Player 1 is the host, so we don't need to compute the ping.
// Any negative number is invalid.
const playerInfo = _playersInfo[playerNumber];
if (!playerInfo) {
return 0;
}
return _playersPings[playerNumber] || 0;
return playerInfo.ping || 0;
};
const getCurrentPlayerPing = () => {
@@ -1928,36 +2156,55 @@ namespace gdjs {
const markPlayerAsDisconnected = (playerNumber: number) => {
logger.info(`Marking player ${playerNumber} as disconnected.`);
_playerNumbersWhoJustLeft.push(playerNumber);
// Temporarily save the username in another variable to be used for the notification,
// as we're deleting its playerInfo just after.
_temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(
playerNumber
);
// If Player 1 has disconnected, just end the game.
if (playerNumber === 1) {
logger.info('Host has disconnected, ending the game.');
_playersLastHeartbeatInfo = {};
_playersPings = {};
clearAllMessagesTempData();
gdjs.multiplayer.handleLobbyGameEnded();
return;
}
// Remove the player from the list of players.
// This will cause the next hearbeat to not include this player
// and the others will consider them as disconnected.
delete _playersLastHeartbeatInfo[playerNumber];
delete _playersPings[playerNumber];
clearPlayerTempData(playerNumber);
// If we are the host, send a heartbeat right away so that everyone is aware of the disconnection
// on approximately the same frame.
if (gdjs.multiplayer.isPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const { messageName, messageData } = createHeartbeatMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
lastHeartbeatTimestamp = getTimeNow();
}
};
const handleDisconnectedPeers = (runtimeScene: RuntimeScene) => {
const getPlayerUsername = (playerNumber: number) => {
return (
(_playersInfo[playerNumber] || {}).username ||
_temporaryPlayerNumberToUsername[playerNumber] ||
`Player ${playerNumber}`
);
};
const getPlayerId = (playerNumber: number) => {
return (_playersInfo[playerNumber] || {}).playerId || '';
};
const handleJustDisconnectedPeers = (runtimeScene: RuntimeScene) => {
// If the game is not running, we don't need to handle disconnected peers.
if (!gdjs.multiplayer.isLobbyGameRunning()) {
return;
}
// Players can disconnect if the P2P connection disconnects
// or if we don't receive heartbeats for a while.
const disconnectedPlayerNumbers: number[] = [];
// We rely on the p2p helper to know who has disconnected.
const justDisconnectedPlayerNumbers: number[] = [];
const disconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
if (disconnectedPeers.length) {
for (const disconnectedPeer of disconnectedPeers) {
const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
if (justDisconnectedPeers.length) {
for (const disconnectedPeer of justDisconnectedPeers) {
const disconnectedPlayerNumber =
_peerIdToPlayerNumber[disconnectedPeer];
if (!disconnectedPlayerNumber) {
@@ -1965,11 +2212,11 @@ namespace gdjs {
return;
}
logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);
disconnectedPlayerNumbers.push(disconnectedPlayerNumber);
justDisconnectedPlayerNumbers.push(disconnectedPlayerNumber);
}
}
for (const playerNumber of disconnectedPlayerNumbers) {
for (const playerNumber of justDisconnectedPlayerNumbers) {
// When a player disconnects, as the host, we look at all the instances
// they own and decide what to do with them.
if (gdjs.multiplayer.isPlayerHost()) {
@@ -2000,29 +2247,60 @@ namespace gdjs {
}
};
const clearDisconnectedPeers = () => {
_playerNumbersWhoJustLeft = [];
};
const hasAnyPlayerLeft = () => {
const hasAnyPlayerJustLeft = (): boolean => {
return _playerNumbersWhoJustLeft.length > 0;
};
const hasPlayerLeft = (playerNumber: number) => {
const hasPlayerJustLeft = (playerNumber: number): boolean => {
return _playerNumbersWhoJustLeft.includes(playerNumber);
};
const getDisconnectedPlayers = () => {
const getPlayersWhoJustLeft = (): number[] => {
return _playerNumbersWhoJustLeft;
};
const getNumberOfConnectedPlayers = () => {
// Look at the player pings as a way to know how many players are in the lobby.
return Object.keys(_playersPings).length;
const getLatestPlayerWhoJustLeft = (): number => {
return _playerNumbersWhoJustLeft[0] || 0;
};
const removePlayerWhoJustLeft = (): void => {
// Avoid using shift for test purposes, as it modifies the reference.
const playerNumberWhoLeft = _playerNumbersWhoJustLeft[0];
if (playerNumberWhoLeft !== undefined) {
_playerNumbersWhoJustLeft = _playerNumbersWhoJustLeft.slice(1);
delete _temporaryPlayerNumberToUsername[playerNumberWhoLeft];
}
};
const hasAnyPlayerJustJoined = () => {
return _playerNumbersWhoJustJoined.length > 0;
};
const hasPlayerJustJoined = (playerNumber: number): boolean => {
return _playerNumbersWhoJustJoined.includes(playerNumber);
};
const getPlayersWhoJustJoined = () => {
return _playerNumbersWhoJustJoined;
};
const getLatestPlayerWhoJustJoined = (): number => {
return _playerNumbersWhoJustJoined[0] || 0;
};
const removePlayerWhoJustJoined = (): void => {
// Avoid using shift for test purposes, as it modifies the reference.
const playerNumberWhoJoined = _playerNumbersWhoJustJoined[0];
if (playerNumberWhoJoined !== undefined) {
_playerNumbersWhoJustJoined = _playerNumbersWhoJustJoined.slice(1);
}
};
const getConnectedPlayers = () => {
return Object.keys(_playersInfo).map((playerNumber) => ({
playerNumber: parseInt(playerNumber, 10),
playerId: _playersInfo[playerNumber].playerId,
}));
};
const getNumberOfConnectedPlayers = () => {
// Look at the player info as a way to know how many players are in the lobby.
// This object is updated when heartbeats are sent and received.
return Object.keys(_playersInfo).length;
};
const isPlayerConnected = (playerNumber: number) => {
return _playersPings[playerNumber] !== undefined;
return _playersInfo[playerNumber] !== undefined;
};
const endGameMessageName = '#endGame';
@@ -2066,20 +2344,34 @@ namespace gdjs {
// If the message is received more than 1 time, we just ignore it and end the game.
_playersLastHeartbeatInfo = {};
_playersPings = {};
clearAllMessagesTempData();
gdjs.multiplayer.handleLobbyGameEnded();
};
const updatePlayersPingsForTests = (playersPings) => {
_playersPings = playersPings;
const clearAllMessagesTempData = () => {
_playersLastRoundTripTimes = {};
_playersInfo = {};
lastReceivedGameSyncDataUpdates.clear();
lastReceivedSceneSyncDataUpdates.clear();
processedCustomMessagesCache.clear();
_playerNumbersWhoJustLeft = [];
_playerNumbersWhoJustJoined = [];
expectedMessageAcknowledgements = {};
_lastClockReceivedByInstanceByScene = {};
};
const clearPlayerTempData = (playerNumber: number) => {
// Remove the player from the list of players.
// This will cause the next hearbeat to not include this player
// and the others will consider them as disconnected.
delete _playersLastRoundTripTimes[playerNumber];
delete _playersInfo[playerNumber];
};
return {
sendDataTo,
// Acks.
addExpectedMessageAcknowledgement,
clearExpectedMessageAcknowledgements,
handleAcknowledgeMessagesReceived,
resendClearOrCancelAcknowledgedMessages,
// Instance ownership.
@@ -2113,23 +2405,36 @@ namespace gdjs {
createUpdateGameMessage,
handleUpdateGameMessagesToSend,
handleUpdateGameMessagesReceived,
handleSavedUpdateMessages,
// Heartbeats.
handleHeartbeatsToSend,
handleHeartbeatsReceived,
// Connection/Disonnection.
// Pings & usernames.
getPlayerPing,
getCurrentPlayerPing,
updatePlayersPingsForTests,
handleDisconnectedPeers,
clearDisconnectedPeers,
hasAnyPlayerLeft,
hasPlayerLeft,
getDisconnectedPlayers,
getPlayerUsername,
getPlayerId,
// Connected players.
handleJustDisconnectedPeers,
getConnectedPlayers,
getNumberOfConnectedPlayers,
isPlayerConnected,
// Leaving players.
hasAnyPlayerJustLeft,
hasPlayerJustLeft,
getPlayersWhoJustLeft,
getLatestPlayerWhoJustLeft,
removePlayerWhoJustLeft,
// Joining players.
hasAnyPlayerJustJoined,
hasPlayerJustJoined,
getPlayersWhoJustJoined,
getLatestPlayerWhoJustJoined,
removePlayerWhoJustJoined,
// End game.
sendEndGameMessage,
handleEndGameMessages,
clearAllMessagesTempData,
};
};

View File

@@ -208,7 +208,10 @@ namespace gdjs {
};
const handleChangeVariableOwnerMessagesToSend = function () {
if (!gdjs.multiplayer.isLobbyGameRunning()) {
if (
!gdjs.multiplayer.isLobbyGameRunning() ||
!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()
) {
return;
}

View File

@@ -11,6 +11,8 @@ namespace gdjs {
let canLobbyBeClosed = true;
const notificationContainerIds: string[] = [];
export const getDomElementContainer = (
runtimeScene: gdjs.RuntimeScene
): HTMLDivElement | null => {
@@ -387,7 +389,7 @@ namespace gdjs {
// to allow the player to leave the lobby.
setTimeout(() => {
closeContainer.style.visibility = 'inherit';
}, 5000);
}, 10000);
};
/**
@@ -398,7 +400,6 @@ namespace gdjs {
) {
showNotification(
runtimeScene,
'error-notification',
'An error occurred while displaying the game lobbies, please try again.',
'error'
);
@@ -411,37 +412,82 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene,
playerName: string
) {
showNotification(
runtimeScene,
'player-left-notification',
`${playerName} has left the game.`,
'warning'
);
showNotification(runtimeScene, `${playerName} left.`, 'warning');
};
/**
* Create, display, and hide a notification when a player leaves the game.
* Create, display, and hide a notification when a player joins the game.
*/
export const displayPlayerJoinedNotification = function (
runtimeScene: gdjs.RuntimeScene,
playerName: string
) {
showNotification(runtimeScene, `${playerName} joined.`, 'success');
};
/**
* Create, display, and hide a notification when an error happens on connection.
*/
export const displayConnectionErrorNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
showNotification(
runtimeScene,
'connection-error-notification',
'Could not connect to other players.',
'error'
);
};
const removeNotificationAndShiftOthers = function (
notificationContainerId: string
) {
const notification = document.getElementById(notificationContainerId);
if (!notification) {
logger.error('Notification not found.');
return;
}
const index = notificationContainerIds.indexOf(notificationContainerId);
if (index !== -1) {
notificationContainerIds.splice(index, 1);
}
notification.remove();
// Shift the other notifications up.
for (let i = 0; i < notificationContainerIds.length; i++) {
const notification = document.getElementById(
notificationContainerIds[i]
);
if (!notification) {
logger.error('Notification not found.');
continue;
}
notification.style.top = `${12 + i * 32}px`;
}
};
/**
* Helper to show a notification to the user, that disappears automatically.
*/
export const showNotification = function (
runtimeScene: gdjs.RuntimeScene,
id: string,
content: string,
type: 'success' | 'warning' | 'error'
) {
// When we show a notification, we add it below the other ones.
// We also remove the oldest one if there are too many > 5.
if (notificationContainerIds.length > 5) {
const oldestNotificationId = notificationContainerIds.shift();
if (!oldestNotificationId) {
logger.error('No oldest notification ID found.');
return;
}
removeNotificationAndShiftOthers(oldestNotificationId);
}
// We generate a random ID for the notification, so they can stack.
const id = `notification-${Math.random().toString(36).substring(7)}`;
const domContainer = runtimeScene
.getGame()
.getRenderer()
@@ -461,7 +507,8 @@ namespace gdjs {
: type === 'warning'
? '#FFA500'
: '#FF0000';
divContainer.style.top = '12px';
// Space the notifications vertically, based on how many there are.
divContainer.style.top = `${12 + notificationContainerIds.length * 32}px`;
divContainer.style.right = '16px';
divContainer.style.padding = '6px 32px 6px 6px';
// Use zIndex 1 to make sure it is below the iframe.
@@ -496,8 +543,9 @@ namespace gdjs {
loggedText.style.margin = '0px';
divContainer.appendChild(loggedText);
domContainer.appendChild(divContainer);
notificationContainerIds.push(id);
const animationTime = 700;
const notificationTime = 5000;
setTimeout(() => {
@@ -518,7 +566,7 @@ namespace gdjs {
}, notificationTime);
// Use timeout because onanimationend listener does not work.
setTimeout(() => {
divContainer.remove();
removeNotificationAndShiftOthers(id);
}, notificationTime + animationTime);
};

View File

@@ -92,9 +92,14 @@ namespace gdjs {
// To handle this case and avoid having an object not synchronized, we set a timeout to destroy the object
// if it has not been assigned a networkId after a short delay.
this._destroyInstanceTimeoutId = setTimeout(() => {
if (!owner.networkId && gdjs.multiplayer.isLobbyGameRunning()) {
const sceneNetworkId = this.owner.getRuntimeScene().networkId;
if (
!owner.networkId &&
gdjs.multiplayer.isLobbyGameRunning() &&
sceneNetworkId
) {
debugLogger.info(
`Lobby game is running and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
`Lobby game is running on a synced scene and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
);
owner.deleteFromScene(instanceContainer);
}
@@ -162,12 +167,9 @@ namespace gdjs {
private _getOrCreateInstanceNetworkId() {
if (!this.owner.networkId) {
// no ID for this object, let's generate one so it can be identified by other players.
// Either use the persistentUuid if it exists, or generate a new one.
// No ID for this object, let's generate one so it can be identified by other players.
// Keep it short to avoid sending too much data.
const newID = this.owner.persistentUuid
? this.owner.persistentUuid.substring(0, 8)
: gdjs.makeUuid().substring(0, 8);
const newID = gdjs.makeUuid().substring(0, 8);
this.owner.networkId = newID;
}
@@ -250,7 +252,8 @@ namespace gdjs {
return;
}
// If game is running and object belong to a player that is not connected, destroy the object.
// If game is running and the object belongs to a player who is not connected, destroy the object.
// As the game may create objects before the lobby game starts, we don't want to destroy them if it's not running.
if (
this.playerNumber !== 0 && // Host is always connected.
!gdjs.multiplayerMessageManager.isPlayerConnected(this.playerNumber)
@@ -405,8 +408,7 @@ namespace gdjs {
this._destroyInstanceTimeoutId = null;
}
// If the lobby game is not running, do not try to destroy the object,
// as the game may create objects before the lobby game starts, and we don't want to destroy them.
// If the lobby game is not running, no need to send a message to destroy the object.
if (!gdjs.multiplayer.isLobbyGameRunning()) {
return;
}

View File

@@ -1,16 +1,12 @@
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
type Lobby = {
id: string;
name: string;
status: string;
players: { playerId: string; status: string }[];
};
export namespace multiplayer {
/** Set to true in testing to avoid relying on the multiplayer extension. */
export let disableMultiplayerForTesting = false;
export let _isReadyToSendOrReceiveGameUpdateMessages = false;
let _isGameRegistered: boolean | null = null;
let _isCheckingIfGameIsRegistered = false;
let _isWaitingForLogin = false;
@@ -20,8 +16,6 @@ namespace gdjs {
let _hasLobbyGameJustEnded = false;
let _lobbyId: string | null = null;
let _connectionId: string | null = null;
export let _lobby: Lobby | null = null;
let _playerPublicProfiles: { id: string; username?: string }[] = [];
// Communication methods.
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
@@ -46,6 +40,11 @@ namespace gdjs {
if (disableMultiplayerForTesting) return;
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
gdjs.multiplayerMessageManager.handleJustDisconnectedPeers(
runtimeScene
);
gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(
runtimeScene
);
@@ -60,14 +59,21 @@ namespace gdjs {
gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(
runtimeScene
);
// In case we're joining an existing lobby, it's possible we haven't
// fully caught up with the game state yet, especially if a scene is loading.
// We look at them every frame, from the moment the lobby has started,
// to ensure we don't miss any.
if (_isLobbyGameRunning) {
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(
runtimeScene
);
}
gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
gdjs.multiplayerMessageManager.handleDisconnectedPeers(runtimeScene);
}
);
@@ -75,6 +81,13 @@ namespace gdjs {
(runtimeScene: gdjs.RuntimeScene) => {
if (disableMultiplayerForTesting) return;
// Handle joining and leaving players to show notifications accordingly.
handleLeavingPlayer(runtimeScene);
handleJoiningPlayer(runtimeScene);
// Then look at the heartbeats received to know if a new player has joined/left.
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
runtimeScene
);
@@ -85,9 +98,6 @@ namespace gdjs {
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(
runtimeScene
);
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
handleLeavingPlayer(runtimeScene);
gdjs.multiplayerMessageManager.clearDisconnectedPeers();
}
);
@@ -144,6 +154,9 @@ namespace gdjs {
if (playerToken) {
url.searchParams.set('playerToken', playerToken);
}
// Increment this value when a new feature is introduced so we can
// adapt the interface of the lobbies.
url.searchParams.set('multiplayerVersion', '2');
return url.toString();
};
@@ -156,6 +169,9 @@ namespace gdjs {
export const isLobbyGameRunning = () => _isLobbyGameRunning;
export const isReadyToSendOrReceiveGameUpdateMessages = () =>
_isReadyToSendOrReceiveGameUpdateMessages;
/**
* Returns true if the game has just ended,
* useful to switch back to to the main menu.
@@ -166,17 +182,16 @@ namespace gdjs {
* Returns the number of players in the lobby.
*/
export const getPlayersInLobbyCount = () => {
// If the game has not started yet, look at the lobby.
if (!_isLobbyGameRunning && _lobby) {
return _lobby.players.length;
}
// Whether the lobby game has started or not, the number of players in the lobby
// is the number of connected players.
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
};
// If the game has started, look at the pings received from the players.
if (_isLobbyGameRunning) {
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
}
return 0;
/**
* Returns true if the player at this position is connected to the lobby.
*/
export const isPlayerConnected = (playerNumber: number) => {
return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);
};
/**
@@ -195,38 +210,12 @@ namespace gdjs {
return playerNumber === 1;
};
/**
* Returns the player ID of the player at the given number in the lobby.
* The number is shifted by one, so that the first player has number 1.
*/
const getPlayerId = (playerNumber: number) => {
if (!_lobby) {
return '';
}
const index = playerNumber - 1;
if (index < 0 || index >= _lobby.players.length) {
return '';
}
return _lobby.players[index].playerId;
};
/**
* Returns the player username at the given number in the lobby.
* The number is shifted by one, so that the first player has number 1.
*/
export const getPlayerUsername = (playerNumber: number) => {
const playerId = getPlayerId(playerNumber);
if (!playerId) {
return '';
}
const playerPublicProfile = _playerPublicProfiles.find(
(profile) => profile.id === playerId
);
return playerPublicProfile
? playerPublicProfile.username
: `Player ${playerNumber}`;
return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);
};
/**
@@ -238,30 +227,39 @@ namespace gdjs {
};
const handleLeavingPlayer = (runtimeScene: gdjs.RuntimeScene) => {
const disconnectedPlayers = gdjs.multiplayerMessageManager.getDisconnectedPlayers();
if (disconnectedPlayers.length > 0) {
for (const playerNumber of disconnectedPlayers) {
const playerLeftId = getPlayerId(playerNumber);
const lastestPlayerWhoJustLeft = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft();
if (lastestPlayerWhoJustLeft) {
const playerUsername = getPlayerUsername(lastestPlayerWhoJustLeft);
gdjs.multiplayerComponents.displayPlayerLeftNotification(
runtimeScene,
playerUsername
);
// We remove the players who just left 1 by 1, so that they can be treated in different frames.
// This is especially important if the expression to know the latest player who just left is used,
// to avoid missing a player leaving.
gdjs.multiplayerMessageManager.removePlayerWhoJustLeft();
if (!playerLeftId) {
return;
}
const playerLeftPublicProfile = _playerPublicProfiles.find(
(profile) => profile.id === playerLeftId
);
if (playerLeftPublicProfile) {
gdjs.multiplayerComponents.displayPlayerLeftNotification(
runtimeScene,
(playerLeftPublicProfile && playerLeftPublicProfile.username) ||
'Player'
);
}
}
// When a player leaves, we send a heartbeat to the backend so that they're aware of the players in the lobby.
// Do not await as we want don't want to block the execution of the of the rest of the logic.
sendHeartbeatToBackend();
}
};
const handleJoiningPlayer = (runtimeScene: gdjs.RuntimeScene) => {
const lastestPlayerWhoJustJoined = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined();
if (lastestPlayerWhoJustJoined) {
const playerUsername = getPlayerUsername(lastestPlayerWhoJustJoined);
gdjs.multiplayerComponents.displayPlayerJoinedNotification(
runtimeScene,
playerUsername
);
}
// We remove the players who just joined 1 by 1, so that they can be treated in different frames.
// This is especially important if the expression to know the latest player who just joined is used,
// to avoid missing a player joining.
gdjs.multiplayerMessageManager.removePlayerWhoJustJoined();
};
/**
* Returns true if the game is registered, false otherwise.
* Useful to display a message to the user to register the game before logging in.
@@ -298,61 +296,7 @@ namespace gdjs {
);
};
const getUserPublicProfile = async (
userId: string,
isDev: boolean
): Promise<{ id: string; username?: string }> => {
const rootApi = isDev
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const url = `${rootApi}/user/user-public-profile/${userId}`;
const response = await fetch(url);
return response.json();
};
const updatePlayerPublicProfiles = async (isDev: boolean) => {
if (!_lobby) {
return;
}
const playerIds = _lobby.players.map((player) => player.playerId);
const currentPlayerPublicProfileIds = _playerPublicProfiles.map(
(profile) => profile.id
);
const addedPlayerIds = playerIds.filter(
(id) => !currentPlayerPublicProfileIds.includes(id)
);
const removedPlayerIds = currentPlayerPublicProfileIds.filter(
(id) => !playerIds.includes(id)
);
if (addedPlayerIds.length === 0 && removedPlayerIds.length === 0) {
return;
}
if (addedPlayerIds.length > 0) {
const addedPlayerPublicProfiles = await Promise.all(
addedPlayerIds.map(async (id) => {
const userPublicProfile = await getUserPublicProfile(id, isDev);
return userPublicProfile;
})
);
_playerPublicProfiles = [
..._playerPublicProfiles,
...addedPlayerPublicProfiles,
];
}
if (removedPlayerIds.length > 0) {
const updatedPlayerPublicProfiles = _playerPublicProfiles.filter(
(profile) => !removedPlayerIds.includes(profile.id)
);
_playerPublicProfiles = updatedPlayerPublicProfiles;
}
};
const handleLobbyJoinEvent = function (
const handleJoinLobbyEvent = function (
runtimeScene: gdjs.RuntimeScene,
lobbyId: string
) {
@@ -367,7 +311,6 @@ namespace gdjs {
_connectionId = null;
playerNumber = null;
_lobbyId = null;
_lobby = null;
_websocket = null;
}
@@ -460,15 +403,9 @@ namespace gdjs {
}
case 'lobbyUpdated': {
const messageData = messageContent.data;
const lobby = messageData.lobby;
const positionInLobby = messageData.positionInLobby;
if (!lobby) {
logger.error('No lobby received');
return;
}
handleLobbyUpdatedEvent({
runtimeScene,
updatedLobby: lobby,
positionInLobby,
});
break;
@@ -502,12 +439,13 @@ namespace gdjs {
return;
}
const peerId = messageData.peerId;
if (!peerId) {
const compressionMethod = messageData.compressionMethod;
if (!peerId || !compressionMethod) {
logger.error('Malformed message received');
return;
}
handlePeerIdEvent({ peerId });
handlePeerIdEvent({ peerId, compressionMethod });
break;
}
}
@@ -632,37 +570,25 @@ namespace gdjs {
);
};
const handleLobbyLeaveEvent = function () {
const handleLeaveLobbyEvent = function () {
if (_websocket) {
_websocket.close();
}
_connectionId = null;
playerNumber = null;
_lobbyId = null;
_lobby = null;
_websocket = null;
};
const handleLobbyUpdatedEvent = function ({
runtimeScene,
updatedLobby,
positionInLobby,
}: {
runtimeScene: gdjs.RuntimeScene;
updatedLobby: Lobby;
positionInLobby: number;
}) {
// Update the object representing the lobby in the extension.
_lobby = updatedLobby;
// If the lobby is playing, do not update anything.
if (updatedLobby.status === 'playing') {
return;
}
// Update the profiles so we can use the usernames of the players.
updatePlayerPublicProfiles(isUsingGDevelopDevelopmentEnvironment);
// This is mainly useful when joining a lobby, or when the lobby is updated before the game starts.
// The position in lobby should never change after the game has started (the WS is closed anyway).
playerNumber = positionInLobby;
// If the player is in the lobby, tell the lobbies window that the lobby has been updated,
@@ -672,7 +598,6 @@ namespace gdjs {
);
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
logger.info('The lobbies iframe is not opened, not sending message.');
return;
}
@@ -725,6 +650,55 @@ namespace gdjs {
);
};
const sendHeartbeatToBackend = async function () {
const gameId = gdjs.projectData.properties.projectUuid;
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (!gameId || !playerId || !playerToken || !_lobbyId) {
logger.error(
'Cannot keep the lobby playing without the game ID or player ID.'
);
return;
}
const rootApi = isUsingGDevelopDevelopmentEnvironment
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const headers = {
'Content-Type': 'application/json',
};
let heartbeatUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
headers['Authorization'] = `player-game-token ${playerToken}`;
heartbeatUrl += `?playerId=${playerId}`;
const players = gdjs.multiplayerMessageManager.getConnectedPlayers();
try {
await fetch(heartbeatUrl, {
method: 'POST',
headers,
body: JSON.stringify({
players,
}),
});
} catch (error) {
logger.error('Error while sending heartbeat, retrying:', error);
try {
await fetch(heartbeatUrl, {
method: 'POST',
headers,
body: JSON.stringify({
players,
}),
});
} catch (error) {
logger.error(
'Error while sending heartbeat a second time. Giving up:',
error
);
}
}
};
/**
* When the game receives the information that the game has started, close the
* lobbies window, focus on the game, and set the flag to true.
@@ -745,7 +719,7 @@ namespace gdjs {
runtimeScene
);
// Do as if the player left the lobby.
handleLobbyLeaveEvent();
handleLeaveLobbyEvent();
removeLobbiesContainer(runtimeScene);
focusOnGame(runtimeScene);
return;
@@ -753,36 +727,16 @@ namespace gdjs {
// If we are the host, start pinging the backend to let it know the lobby is running.
if (isPlayerHost()) {
const gameId = gdjs.projectData.properties.projectUuid;
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (!gameId || !playerId || !playerToken || !_lobbyId) {
logger.error(
'Cannot keep the lobby playing without the game ID or player ID.'
);
return;
}
_lobbyHeartbeatInterval = setInterval(async () => {
const rootApi = isUsingGDevelopDevelopmentEnvironment
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const headers = {
'Content-Type': 'application/json',
};
let heartbeatUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
headers['Authorization'] = `player-game-token ${playerToken}`;
heartbeatUrl += `?playerId=${playerId}`;
await fetch(heartbeatUrl, {
method: 'POST',
headers,
});
await sendHeartbeatToBackend();
}, heartbeatInterval);
}
// If we are connected to players, then the game can start.
logger.info('Lobby game has started.');
// In case we're joining an existing lobby, read the saved messages to catch-up with the game state.
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(runtimeScene);
_isReadyToSendOrReceiveGameUpdateMessages = true;
_hasLobbyGameJustStarted = true;
_isLobbyGameRunning = true;
removeLobbiesContainer(runtimeScene);
@@ -802,8 +756,8 @@ namespace gdjs {
_hasLobbyGameJustEnded = true;
_isLobbyGameRunning = false;
_lobbyId = null;
_lobby = null;
playerNumber = null;
_isReadyToSendOrReceiveGameUpdateMessages = false;
if (_lobbyHeartbeatInterval) {
clearInterval(_lobbyHeartbeatInterval);
}
@@ -812,15 +766,22 @@ namespace gdjs {
gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();
// Clear the expected acknowledgments, as the game is ending.
gdjs.multiplayerMessageManager.clearExpectedMessageAcknowledgements();
gdjs.multiplayerMessageManager.clearAllMessagesTempData();
};
/**
* When the game receives the information of the peerId, then
* the player can connect to the peer.
*/
const handlePeerIdEvent = function ({ peerId }: { peerId: string }) {
// When a peerId is received, trigger a P2P connection with the peer.
const handlePeerIdEvent = function ({
peerId,
compressionMethod,
}: {
peerId: string;
compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;
}) {
// When a peerId is received, trigger a P2P connection with the peer, just after setting the compression method.
gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);
const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
if (!currentPeerId) {
logger.error(
@@ -842,7 +803,7 @@ namespace gdjs {
* players in the lobby via the websocket.
* It will then trigger an event from the websocket to all players in the lobby.
*/
const handleGameCountdownStartMessage = function () {
const handleStartGameCountdownMessage = function () {
if (!_websocket) {
logger.error(
'No connection to send the start countdown message. Are you connected to a lobby?'
@@ -863,7 +824,7 @@ namespace gdjs {
* players in the lobby via the websocket.
* It will then trigger an event from the websocket to all players in the lobby.
*/
const handleGameStartMessage = function () {
const handleStartGameMessage = function () {
if (!_websocket) {
logger.error(
'No connection to send the start countdown message. Are you connected to a lobby?'
@@ -877,6 +838,48 @@ namespace gdjs {
connectionType: 'lobby',
})
);
// As the host, start sending messages to the players.
_isReadyToSendOrReceiveGameUpdateMessages = true;
};
/**
* When the game receives a join game message from the lobby, send it via the WS
* waiting for a peerId to be received and that the connection happens automatically.
*/
const handleJoinGameMessage = function () {
if (!_websocket) {
logger.error(
'No connection to send the start countdown message. Are you connected to a lobby?'
);
return;
}
_websocket.send(
JSON.stringify({
action: 'joinGame',
connectionType: 'lobby',
})
);
};
/**
* When the first heartbeat is received, we consider the connection to the host as working,
* we inform the backend services that the connection is ready, so it can start the game when
* everyone is ready.
*/
export const markConnectionAsConnected = function () {
if (!_websocket) {
return;
}
_websocket.send(
JSON.stringify({
action: 'updateConnection',
connectionType: 'lobby',
status: 'connected',
})
);
};
/**
@@ -941,7 +944,7 @@ namespace gdjs {
* Helper to send the ID from PeerJS to the lobby players.
*/
const sendPeerId = function () {
if (!_websocket || !_lobby) {
if (!_websocket) {
logger.error(
'No connection to send the message. Are you connected to a lobby?'
);
@@ -997,19 +1000,23 @@ namespace gdjs {
throw new Error('Malformed message.');
}
handleLobbyJoinEvent(runtimeScene, event.data.lobbyId);
handleJoinLobbyEvent(runtimeScene, event.data.lobbyId);
break;
}
case 'startGameCountdown': {
handleGameCountdownStartMessage();
handleStartGameCountdownMessage();
break;
}
case 'startGame': {
handleGameStartMessage();
handleStartGameMessage();
break;
}
case 'leaveLobby': {
handleLobbyLeaveEvent();
handleLeaveLobbyEvent();
break;
}
case 'joinGame': {
handleJoinGameMessage();
break;
}
}
@@ -1247,9 +1254,9 @@ namespace gdjs {
/**
* Action to allow the player to leave the lobby in-game.
*/
export const leaveGameLobby = async (runtimeScene: gdjs.RuntimeScene) => {
export const leaveGameLobby = async () => {
// Handle the case where the game has not started yet, so the player is in the lobby.
handleLobbyLeaveEvent();
handleLeaveLobbyEvent();
// Handle the case where the game has started, so the player is in the game and connected to other players.
handleLobbyGameEnded();
};

View File

@@ -254,6 +254,11 @@ namespace gdjs {
connection.on('close', () => {
_onDisconnect(connection.peer);
});
connection.on('iceStateChanged', (state) => {
if (state === 'disconnected') {
_onDisconnect(connection.peer);
}
});
// Regularly check for disconnection as the built in way is not reliable.
(function disconnectChecker() {
@@ -428,7 +433,7 @@ namespace gdjs {
export const isReady = () => ready;
/**
* Return any disconnected peers.
* Return peers that have disconnected in the frame.
*/
export const getJustDisconnectedPeers = () => justDisconnectedPeers;

File diff suppressed because it is too large Load Diff

View File

@@ -340,7 +340,7 @@ namespace gdjs {
setAdditiveRendering(enabled: boolean): void {
// Access private members of the behavior to apply changes right away.
const behavior: any = this.emitter.getBehavior('blendMode');
behavior.blendMode = enabled ? 'ADD' : 'NORMAL';
behavior.value = enabled ? 'ADD' : 'NORMAL';
}
setAlpha(alpha1: number, alpha2: number): void {

View File

@@ -1395,6 +1395,7 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('getLinearVelocityLength');
// Deprecated
aut
.addCondition(
'LinearVelocityAngle',
@@ -1413,9 +1414,27 @@ module.exports = {
_('Angle to compare to (in degrees)')
)
)
.setHidden()
.getCodeExtraInformation()
.setFunctionName('getLinearVelocityAngle');
aut
.addScopedCondition(
'IsLinearVelocityAngleAround',
_('Linear velocity angle'),
_('Compare the linear velocity angle of the object.'),
_('Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°'),
_('Velocity'),
'res/physics32.png',
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Angle (in degrees)'))
.addParameter('expression', _('Tolerance (in degrees)'))
.getCodeExtraInformation()
.setFunctionName('isLinearVelocityAngleAround');
aut
.addAction(
'LinearVelocityAngle',

View File

@@ -1528,6 +1528,17 @@ namespace gdjs {
);
}
isLinearVelocityAngleAround(degreeAngle: float, tolerance: float) {
return (
Math.abs(
gdjs.evtTools.common.angleDifference(
this.getLinearVelocityAngle(),
degreeAngle
)
) <= tolerance
);
}
getAngularVelocity(): float {
// If there is no body, set a new one
if (this._body === null) {

View File

@@ -397,7 +397,6 @@ module.exports = {
'telephone number',
'url',
'search',
'email',
])
)
)

View File

@@ -106,7 +106,9 @@ namespace gdjs {
}
onSceneResumed() {
this._createElement();
// The input must have been destroyed when the scene was paused,
// in case it still exists, skip recreation.
if (!this._input) this._createElement();
}
onDestroy() {

View File

@@ -715,7 +715,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
const object = extension
.addObject(
'SimpleTileMap',
_('Tilemap'),
_('Tile map'),
_('Displays a tiled-based map.'),
'JsPlatform/Extensions/tile_map.svg',
objectSimpleTileMap
@@ -734,78 +734,326 @@ const defineSimpleTileMap = function (extension, _, gd) {
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
object
.addCondition(
'Tile',
_('Tile id'),
_('Check tile id at coordinates.'),
_('The tile id at coordinates _PARAM3_;_PARAM3_'),
.addExpression(
'TilesetColumnCount',
_('Tileset column count'),
_('Get the number of column in the tileset.'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tilemap'), '', false)
.useStandardRelationalOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
)
.addParameter('number', _('X'), '', false)
.addParameter('number', _('Y'), '', false)
.setFunctionName('getTileAt');
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.setFunctionName('getTilesetColumnCount');
object
.addExpression(
'Tile',
_('Tile id'),
_('Check tile id at coordinates.'),
'TilesetRowCount',
_('Tileset row count'),
_('Get the number of row in the tileset.'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tilemap'), '', false)
.addParameter('number', _('X'), '', false)
.addParameter('number', _('Y'), '', false)
.setFunctionName('getTileAt');
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.setFunctionName('getTilesetRowCount');
object
.addAction(
'Tile',
_('Tile id'),
_('Set tile id at coordinates.'),
.addExpression(
'TileCenterX',
_('Scene X coordinate of tile'),
_('Get the scene X position of the center of the tile.'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.setFunctionName('getSceneXCoordinateOfTileCenter');
object
.addExpression(
'TileCenterY',
_('Scene Y coordinate of tile'),
_('Get the scene Y position of the center of the tile.'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.setFunctionName('getSceneYCoordinateOfTileCenter');
object
.addExpression(
'GridX',
_('Tile map grid column coordinate'),
_(
'tile at coordinates _PARAM3_;_PARAM4_ (flip horizontally _PARAM5_, flip vertically _PARAM6_)'
'Get the grid column coordinates in the tile map corresponding to the scene coordinates.'
),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tilemap'), '', false)
.useStandardOperatorParameters(
'number',
gd.ParameterOptions.makeNewOptions()
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.setFunctionName('getColumnIndexAtPosition');
object
.addExpression(
'GridY',
_('Tile map grid row coordinate'),
_(
'Get the grid row coordinates in the tile map corresponding to the scene coordinates.'
),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('number', _('X'), '', false)
.addParameter('number', _('Y'), '', false)
.addParameter('yesorno', _('Flip horizontally'), '', false)
.setDefaultValue('false')
.addParameter('yesorno', _('Flip vertically'), '', false)
.setDefaultValue('false')
.setFunctionName('setTileAt')
.setGetter('getTileAt');
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.setFunctionName('getRowIndexAtPosition');
object
.addExpressionAndConditionAndAction(
'number',
'TileIdAtPosition',
_('Tile (at position)'),
_('the id of the tile at the scene coordinates'),
_('the tile id at scene coordinates _PARAM3_ ; _PARAM4_'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.setFunctionName('setTileAtPosition')
.setGetter('getTileAtPosition');
object
.addAction(
'removeTileAt',
_('Remove tile'),
_('Remove the tile at the given coordinates'),
_('Remove tile at coordinates _PARAM1_;_PARAM2_'),
'FlipTileOnYAtPosition',
_('Flip tile vertically (at position)'),
_('Flip tile vertically at scene coordinates.'),
_(
'Flip tile vertically at scene coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
),
_('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.addParameter('yesorno', _('Flip vertically'), '', false)
.setDefaultValue('false')
.setFunctionName('flipTileOnYAtPosition');
object
.addAction(
'FlipTileOnXAtPosition',
_('Flip tile horizontally (at position)'),
_('Flip tile horizontally at scene coordinates.'),
_(
'Flip tile horizontally at scene coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
),
_('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.addParameter('yesorno', _('Flip horizontally'), '', false)
.setDefaultValue('false')
.setFunctionName('flipTileOnXAtPosition');
object
.addAction(
'RemoveTileAtPosition',
_('Remove tile (at position)'),
_('Remove the tile at the scene coordinates.'),
_('Remove tile at scene coordinates _PARAM1_ ; _PARAM2_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('X'), '', false)
.addParameter('number', _('Y'), '', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('removeTileAt');
.setFunctionName('removeTileAtPosition');
object
.addExpressionAndConditionAndAction(
'number',
'TileIdAtGrid',
_('Tile (on the grid)'),
_('the id of the tile at the grid coordinates'),
_('the tile id at grid coordinates _PARAM2_ ; _PARAM3_'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.setFunctionName('setTileAtGridCoordinates')
.setGetter('getTileAtGridCoordinates');
object
.addAction(
'FlipTileOnYAtGridCoordinates',
_('Flip tile vertically (on the grid)'),
_('Flip tile vertically at grid coordinates.'),
_(
'Flip tile vertically at grid coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
),
_('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.addParameter('yesorno', _('Flip vertically'), '', false)
.setDefaultValue('false')
.setFunctionName('flipTileOnYAtGridCoordinates');
object
.addAction(
'FlipTileOnXAtGridCoordinates',
_('Flip tile horizontally (on the grid)'),
_('Flip tile horizontally at grid coordinates.'),
_(
'Flip tile horizontally at grid coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
),
_('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.addParameter('yesorno', _('Flip horizontally'), '', false)
.setDefaultValue('false')
.setFunctionName('flipTileOnXAtGridCoordinates');
object
.addAction(
'RemoveTileAtGridCoordinates',
_('Remove tile (on the grid)'),
_('Remove the tile at the grid coordinates.'),
_('Remove tile at grid coordinates _PARAM1_ ; _PARAM2_'),
'',
'JsPlatform/Extensions/tile_map.svg',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('removeTileAtGridCoordinates');
object
.addCondition(
'IsTileFlippedOnXAtPosition',
_('Tile flipped horizontally (at position)'),
_('Check if tile at scene coordinates is flipped horizontally.'),
_(
'The tile at scene coordinates _PARAM1_ ; _PARAM2_ is flipped horizontally'
),
_('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTileFlippedOnXAtPosition');
object
.addCondition(
'IsTileFlippedOnYAtPosition',
_('Tile flipped vertically (at position)'),
_('Check if tile at scene coordinates is flipped vertically.'),
_(
'The tile at scene coordinates _PARAM1_ ; _PARAM2_ is flipped vertically'
),
_('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTileFlippedOnYAtPosition');
object
.addCondition(
'IsTileFlippedOnXAtGridCoordinates',
_('Tile flipped horizontally (on the grid)'),
_('Check if tile at grid coordinates is flipped horizontally.'),
_(
'The tile at grid coordinates _PARAM1_ ; _PARAM2_ is flipped horizontally'
),
_('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTileFlippedOnXAtGridCoordinates');
object
.addCondition(
'IsTileFlippedOnYAtGridCoordinates',
_('Tile flipped vertically (on the grid)'),
_('Check if tile at grid coordinates is flipped vertically.'),
_(
'The tile at grid coordinates _PARAM1_ ; _PARAM2_ is flipped vertically'
),
_('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.addParameter('number', _('Grid X'), '', false)
.addParameter('number', _('Grid Y'), '', false)
.getCodeExtraInformation()
.setFunctionName('isTileFlippedOnYAtGridCoordinates');
object
.addExpressionAndConditionAndAction(
'number',
'GridRowCount',
_('Grid row count'),
_('the grid row count in the tile map'),
_('the grid row count'),
_('Size'),
'res/actions/scaleHeight24_black.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('setGridRowCount')
.setGetter('getGridRowCount');
object
.addExpressionAndConditionAndAction(
'number',
'GridColumnCount',
_('Grid column count'),
_('the grid column count in the tile map'),
_('the grid column count'),
_('Size'),
'res/actions/scaleWidth24_black.png'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('setGridColumnCount')
.setGetter('getGridColumnCount');
};
/**
@@ -1237,7 +1485,7 @@ module.exports = {
extension
.setExtensionInformation(
'TileMap',
_('Tilemap'),
_('Tile map'),
_(
"The Tilemap object can be used to display tile-based objects. It's a good way to create maps for RPG, strategy games or create objects by assembling tiles, useful for platformer, retro-looking games, etc..."
),
@@ -1309,12 +1557,6 @@ module.exports = {
helpPagePath: '/objects/tilemap',
})
);
objectsEditorService.registerEditorConfiguration(
'TileMap::SimpleTileMap',
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
helpPagePath: '/objects/tilemap',
})
);
},
/**
* Register renderers for instance of objects on the scene editor.
@@ -1638,15 +1880,21 @@ module.exports = {
* Renderer for instances of SimpleTileMap inside the IDE.
*/
class RenderedSimpleTileMapInstance extends RenderedInstance {
_placeholderPixiObject = new PIXI.Text(
'Select the instance\nand start painting',
_placeholderTextPixiObject = new PIXI.Text(
'Select this instance\nto start painting',
new PIXI.TextStyle({
fontFamily: 'Arial',
fontSize: 20,
fontSize: 16,
align: 'center',
padding: 5,
})
);
_placeholderImagePixiObject = new PIXI.Sprite(
PIXI.Texture.from(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAADFBMVEUAAAAkMoYsfqH///8FP6xgAAAAAXRSTlMAQObYZgAAAAFiS0dEAxEMTPIAAAAjSURBVBjTpcYxAQAADIMwTGISlTsmoVcCQClzSmvNo2ueGnMajGpBwI5BnwAAAABJRU5ErkJggg=='
)
);
_placeholderPixiObject = new PIXI.Container();
constructor(
project,
@@ -1683,7 +1931,7 @@ module.exports = {
localPosition
);
} else {
this._placeholderPixiObject.worldTransform.applyInverse(
this._placeholderImagePixiObject.worldTransform.applyInverse(
position,
localPosition
);
@@ -1696,9 +1944,15 @@ module.exports = {
localPosition.y < this.height
);
};
this._placeholderPixiObject.interactive = true;
this._placeholderPixiObject.anchor.x = 0.5;
this._placeholderPixiObject.anchor.y = 0.5;
this._placeholderTextPixiObject.interactive = true;
this._placeholderImagePixiObject.interactive = true;
this._placeholderTextPixiObject.anchor.x = 0.5;
this._placeholderTextPixiObject.anchor.y = 0.5;
this._placeholderTextPixiObject.y = 30;
this._placeholderImagePixiObject.y = -30;
this._placeholderImagePixiObject.x = -16;
this._placeholderPixiObject.addChild(this._placeholderTextPixiObject);
this._placeholderPixiObject.addChild(this._placeholderImagePixiObject);
this._pixiObject.addChild(this._placeholderPixiObject);
this._pixiContainer.addChild(this._pixiObject);
this.width = 48;

View File

@@ -80,7 +80,7 @@ namespace gdjs {
}
getOrLoadSimpleTileMap(
tileMapAsJsObject: object,
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
columnCount: number,

View File

@@ -1,6 +1,7 @@
import {
EditableTileMap,
EditableTileMapLayer,
EditableTileMapAsJsObject,
PixiTileMapHelper,
TileDefinition,
TileMapFileContent,
@@ -14,6 +15,7 @@ declare global {
export {
EditableTileMap,
EditableTileMapLayer,
EditableTileMapAsJsObject,
PixiTileMapHelper,
TileDefinition,
TileMapFileContent,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,16 @@ export declare type integer = number;
export declare type float = number;
export type FloatPoint = [float, float];
export type PolygonVertices = FloatPoint[];
export type EditableTileMapLayerAsJsObject = {
id: number;
alpha: number;
tiles: number[][];
};
export type EditableTileMapAsJsObject = {
tileWidth: number;
tileHeight: number;
dimX: number;
dimY: number;
layers: EditableTileMapLayerAsJsObject[];
};
//# sourceMappingURL=CommonTypes.d.ts.map

View File

@@ -1 +1 @@
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC"}
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC;AAE3C,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,8BAA8B,EAAE,CAAC;CAC1C,CAAC"}

View File

@@ -1,4 +1,10 @@
import { PolygonVertices, integer, float } from './CommonTypes';
import {
PolygonVertices,
integer,
float,
EditableTileMapAsJsObject,
EditableTileMapLayerAsJsObject,
} from './CommonTypes';
/**
* A tile map model.
*
@@ -27,6 +33,10 @@ export declare class EditableTileMap {
* The number of tile rows in the map.
*/
private dimY;
/**
* True if is allowed to set a tile out of the tile map's bounds.
* Useful when editing the tile map easily.
*/
/**
* @param tileWidth The width of a tile.
* @param tileHeight The height of a tile.
@@ -50,7 +60,7 @@ export declare class EditableTileMap {
* @param objectConfiguration
*/
static from(
editableTileMapAsJsObject: any,
editableTileMapAsJsObject: EditableTileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
@@ -87,10 +97,24 @@ export declare class EditableTileMap {
*/
getDimensionY(): integer;
/**
* Changes the number of columns in the tile map by adding/removing
* columns at the end.
* @param dim The number of tile columns in the map.
*/
setDimensionX(dim: integer): void;
/**
* Increases dimensions of the tile map by adding columns and rows
* at the start and/or at the end of the grid.
*/
increaseDimensions(
columnsToAppend: number,
columnsToUnshift: number,
rowsToAppend: number,
rowsToUnshift: number
): void;
/**
* Changes the number of row in the tile map by adding/removing
* rows at the end.
* @param dim The number of tile rows in the map.
*/
setDimensionY(dim: integer): void;
@@ -107,11 +131,11 @@ export declare class EditableTileMap {
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addTileLayer(id: integer): EditableTileMapLayer;
addNewTileLayer(id: integer): EditableTileMapLayer;
/**
* @param layer the new layer to set.
*/
setTileLayer(layer: EditableTileMapLayer): void;
addTileLayer(layer: EditableTileMapLayer): void;
getTileLayer(id: integer): EditableTileMapLayer | null;
/**
* @param id The identifier of the new layer.
@@ -146,6 +170,23 @@ export declare class EditableTileMap {
* Returns true if all layers contain no defined tiled.
*/
isEmpty(): boolean;
getTileId(x: integer, y: integer, layerId: integer): integer;
setTile(x: integer, y: integer, layerId: integer, tileId: number): void;
flipTileOnY(x: integer, y: integer, layerId: integer, flip: boolean): void;
flipTileOnX(x: integer, y: integer, layerId: integer, flip: boolean): void;
isTileFlippedOnX(x: integer, y: integer, layerId: integer): boolean;
isTileFlippedOnY(x: integer, y: integer, layerId: integer): boolean;
removeTile(x: integer, y: integer, layerId: integer): void;
trimEmptyColumnsAndRowToFitLayer(
layerId: integer
):
| {
poppedRows: number;
poppedColumns: number;
shiftedRows: number;
shiftedColumns: number;
}
| undefined;
}
/**
* A tile map layer.
@@ -241,7 +282,7 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
constructor(tileMap: EditableTileMap, id: integer);
buildEmptyLayer(dimensionX: number, dimensionY: number): void;
static from(
editableTileMapLayerAsJsObject: any,
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer;
@@ -271,38 +312,19 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile.
* @param options Flipping options.
*/
setTile(
x: integer,
y: integer,
tileId: integer,
options?:
| {
flipVertically: boolean;
flipHorizontally: boolean;
flipDiagonally: boolean;
}
| undefined
):
| {
unshiftedRows: number;
unshiftedColumns: number;
appendedRows: number;
appendedColumns: number;
}
| undefined;
setTile(x: integer, y: integer, tileId: integer): void;
/**
* @param x The layer column.
* @param y The layer row.
* @param tileGID The tile GID.
*/
setTileGID(x: integer, y: integer, tileGID: integer): void;
trimEmptyColumnsAndRow(): {
shiftedRows: number;
shiftedColumns: number;
poppedRows: number;
poppedColumns: number;
getTrimmingData(): {
rowsToShift: number;
columnsToShift: number;
rowsToPop: number;
columnsToPop: number;
};
/**
* @param x The layer column.

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
import { EditableTileMap } from '../model/TileMapModel';
import { TileTextureCache } from './TileTextureCache';
import { TileMapFileContent } from '../load/TileMapFileContent';
import { EditableTileMapAsJsObject } from '../model/CommonTypes';
/**
* A holder to share tile maps across the 2 extension objects.
*
@@ -44,7 +45,7 @@ export declare class TileMapManager {
callback: (tileMap: EditableTileMap | null) => void
): void;
getOrLoadSimpleTileMap(
tileMapAsJsObject: object,
tileMapAsJsObject: EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
tileSetColumnCount: number,

View File

@@ -1 +1 @@
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP,sBAAsB,CACpB,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EAGvB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAC3C,IAAI;IAYP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAqBP,WAAW,IAAI,IAAI;CAIpB"}
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP,sBAAsB,CACpB,iBAAiB,EAAE,yBAAyB,EAC5C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EAGvB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAC3C,IAAI;IAeP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAqBP,WAAW,IAAI,IAAI;CAIpB"}

View File

@@ -1 +1 @@
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;OAQG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAiBlB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;OAQG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAqBlB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}

View File

@@ -40,15 +40,16 @@ namespace gdjs {
_atlasImage: string;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
_rowCount: number;
_columnCount: number;
_tileSize: number;
readonly _rowCount: number;
readonly _columnCount: number;
readonly _tileSize: number;
_displayMode = 'all';
_layerIndex = 0;
_initialTileMapAsJsObject: object | null = null;
_initialTilesWithHitBox: number[];
_initialTileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject | null = null;
readonly _initialTilesWithHitBox: number[];
_isTileMapDirty: boolean = false;
_sceneToTileMapTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
_tileMapToSceneTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap | null = null;
_hitBoxTag: string = 'collision';
private _transformationIsUpToDate: boolean = false;
@@ -130,6 +131,7 @@ namespace gdjs {
// TODO: support changing the atlas texture
return false;
}
// Map content is updated at hot-reload by extraInitializationFromInitialInstance.
return true;
}
@@ -193,10 +195,18 @@ namespace gdjs {
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
if (this._collisionTileMap) {
// If collision tile map is already defined, there's a good chance it means
// extraInitializationFromInitialInstance is called when hot reloading the
// scene so the collision is tile map is updated instead of being re-created.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
}
this.updateTransformation();
});
}
@@ -396,15 +406,9 @@ namespace gdjs {
updateHitBoxes(): void {
this.updateTransformation();
if (!this._collisionTileMap) return;
// Update the RuntimeObject hitboxes attribute.
for (const _ of this._collisionTileMap.getAllHitboxes(this._hitBoxTag)) {
// RuntimeObject.hitBoxes contains the same polygons instances as the
// hitboxes from the tiles.
//
// When hitboxes for a tile is asked to the model, they are updated
// according to the new object location if needed.
// Iterating over all the tiles forces them to update their hitboxes.
}
this.hitBoxes = Array.from(
this._collisionTileMap.getAllHitboxes(this._hitBoxTag)
);
this.hitBoxesDirty = false;
this.updateAABB();
}
@@ -508,38 +512,73 @@ namespace gdjs {
const absScaleX = Math.abs(this._renderer.getScaleX());
const absScaleY = Math.abs(this._renderer.getScaleY());
this._sceneToTileMapTransformation.setToIdentity();
this._tileMapToSceneTransformation.setToIdentity();
// Translation
this._sceneToTileMapTransformation.translate(this.getX(), this.getY());
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
// Rotation
const angleInRadians = (this.getAngle() * Math.PI) / 180;
this._sceneToTileMapTransformation.rotateAround(
this._tileMapToSceneTransformation.rotateAround(
angleInRadians,
this.getCenterX(),
this.getCenterY()
);
// Scale
this._sceneToTileMapTransformation.scale(absScaleX, absScaleY);
this._tileMapToSceneTransformation.scale(absScaleX, absScaleY);
if (this._collisionTileMap) {
const collisionTileMapTransformation = this._collisionTileMap.getTransformation();
collisionTileMapTransformation.copyFrom(
this._sceneToTileMapTransformation
this._tileMapToSceneTransformation
);
this._collisionTileMap.setTransformation(
collisionTileMapTransformation
);
}
this._sceneToTileMapTransformation.copyFrom(
this._tileMapToSceneTransformation
);
this._sceneToTileMapTransformation.invert();
this._transformationIsUpToDate = true;
}
getSceneXCoordinateOfTileCenter(
columnIndex: integer,
rowIndex: integer
): float {
const sceneCoordinates: FloatPoint =
SimpleTileMapRuntimeObject.workingPoint;
this._tileMapToSceneTransformation.transform(
[
(columnIndex + 0.5) * this._tileSize,
(rowIndex + 0.5) * this._tileSize,
],
sceneCoordinates
);
return sceneCoordinates[0];
}
getSceneYCoordinateOfTileCenter(
columnIndex: integer,
rowIndex: integer
): float {
const sceneCoordinates: FloatPoint =
SimpleTileMapRuntimeObject.workingPoint;
this._tileMapToSceneTransformation.transform(
[
(columnIndex + 0.5) * this._tileSize,
(rowIndex + 0.5) * this._tileSize,
],
sceneCoordinates
);
return sceneCoordinates[1];
}
getGridCoordinatesFromSceneCoordinates(
x: number,
y: number
): [number, number] {
x: float,
y: float
): [integer, integer] {
this.updateTransformation();
const gridCoordinates: FloatPoint =
@@ -552,87 +591,150 @@ namespace gdjs {
return [columnIndex, rowIndex];
}
getTileAt(x: number, y: number): integer {
getColumnIndexAtPosition(x: float, y: float): integer {
return this.getGridCoordinatesFromSceneCoordinates(x, y)[0];
}
getRowIndexAtPosition(x: float, y: float): integer {
return this.getGridCoordinatesFromSceneCoordinates(x, y)[1];
}
getTileAtPosition(x: float, y: float): integer {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
return this.getTileAtGridCoordinates(columnIndex, rowIndex);
}
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
return this._renderer.getTileId(columnIndex, rowIndex, 0);
}
setTileAt(
tileId: number,
x: number,
y: number,
flipHorizontally: boolean,
flipVertically: boolean
) {
setTileAtPosition(tileId: number, x: float, y: float) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
const addedData = this._renderer.setTileId(
columnIndex,
rowIndex,
0,
tileId,
{ flipHorizontally, flipVertically, flipDiagonally: false }
);
this._isTileMapDirty = true;
if (addedData) {
const {
unshiftedRows,
unshiftedColumns,
appendedColumns,
appendedRows,
} = addedData;
const scaleX = this.getScaleX();
const scaleY = this.getScaleY();
this.setX(this.getX() - unshiftedColumns * (this._tileSize * scaleX));
this.setY(this.getY() - unshiftedRows * (this._tileSize * scaleY));
if (
unshiftedColumns > 0 ||
unshiftedRows > 0 ||
appendedColumns > 0 ||
appendedRows > 0
) {
this.invalidateHitboxes();
}
}
this._transformationIsUpToDate = false;
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
}
removeTileAt(x: number, y: number) {
setTileAtGridCoordinates(
tileId: number,
columnIndex: integer,
rowIndex: integer
) {
this._renderer.setTileId(columnIndex, rowIndex, 0, tileId);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
flipTileOnYAtPosition(x: float, y: float, flip: boolean) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
this.flipTileOnYAtGridCoordinates(columnIndex, rowIndex, flip);
}
flipTileOnXAtPosition(x: float, y: float, flip: boolean) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
this.flipTileOnXAtGridCoordinates(columnIndex, rowIndex, flip);
}
flipTileOnYAtGridCoordinates(
columnIndex: integer,
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnY(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
}
flipTileOnXAtGridCoordinates(
columnIndex: integer,
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnX(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
}
isTileFlippedOnXAtPosition(x: float, y: float) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtPosition(x: float, y: float) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
removeTileAtPosition(x: float, y: float) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
this.removeTileAtGridCoordinates(columnIndex, rowIndex);
}
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
this._renderer.removeTile(columnIndex, rowIndex, 0);
this._isTileMapDirty = true;
const removedData = this._renderer.trimEmptyColumnsAndRows(0);
if (removedData) {
const {
shiftedRows,
shiftedColumns,
poppedColumns,
poppedRows,
} = removedData;
this.setX(
this.getX() + shiftedColumns * (this._tileSize * this.getScaleX())
);
this.setY(
this.getY() + shiftedRows * (this._tileSize * this.getScaleY())
);
if (
shiftedColumns > 0 ||
shiftedRows > 0 ||
poppedColumns > 0 ||
poppedRows > 0
) {
this.invalidateHitboxes();
}
}
this._transformationIsUpToDate = false;
this.invalidateHitboxes();
}
setGridRowCount(targetRowCount: integer) {
if (targetRowCount <= 0) return;
this._renderer.setGridRowCount(targetRowCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
setGridColumnCount(targetColumnCount: integer) {
if (targetColumnCount <= 0) return;
this._renderer.setGridColumnCount(targetColumnCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
getGridRowCount(): integer {
return this._renderer.getGridRowCount();
}
getGridColumnCount(): integer {
return this._renderer.getGridColumnCount();
}
getTilesetColumnCount(): integer {
return this._columnCount;
}
getTilesetRowCount(): integer {
return this._rowCount;
}
}
gdjs.registerObject(

View File

@@ -93,6 +93,9 @@ namespace gdjs {
}
updateOpacity(): void {
// TODO: Currently, the renderer does not use the object alpha to set
// opacity. Setting alpha on each layer tile might not be useful as
// each layer would be separately transparent instead of the whole tilemap.
this._pixiObject.alpha = this._object._opacity / 255;
const tileMap = this._tileMap;
if (!tileMap) return;
@@ -167,9 +170,7 @@ namespace gdjs {
getTileId(x: integer, y: integer, layerIndex: integer): integer {
const tileMap = this._tileMap;
if (!tileMap) return -1;
const layer = tileMap.getTileLayer(layerIndex);
if (!layer) return -1;
return layer.getTileId(x, y) || -1;
return tileMap.getTileId(x, y, layerIndex);
}
/**
@@ -178,24 +179,56 @@ namespace gdjs {
* @param layerIndex The layer index.
* @param tileId The tile's id.
*/
setTileId(
x: integer,
y: integer,
layerIndex: integer,
tileId: number,
options?:
| {
flipVertically: boolean;
flipHorizontally: boolean;
flipDiagonally: boolean;
}
| undefined
) {
setTileId(x: integer, y: integer, layerIndex: integer, tileId: number) {
const tileMap = this._tileMap;
if (!tileMap) return;
const layer = tileMap.getTileLayer(layerIndex);
if (!layer) return;
return layer.setTile(x, y, tileId, options);
return tileMap.setTile(x, y, layerIndex, tileId);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
if (!tileMap) return;
tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
if (!tileMap) return;
tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnY(x, y, layerIndex);
}
/**
@@ -206,19 +239,36 @@ namespace gdjs {
removeTile(x: integer, y: integer, layerIndex: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
const layer = tileMap.getTileLayer(layerIndex);
if (!layer) return;
return layer.removeTile(x, y);
return tileMap.removeTile(x, y, layerIndex);
}
/**
* @param layerIndex The layer index.
* @param targetRowCount The number of rows to have.
*/
trimEmptyColumnsAndRows(layerIndex: integer) {
setGridRowCount(targetRowCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
const layer = tileMap.getTileLayer(layerIndex);
if (!layer) return;
return layer.trimEmptyColumnsAndRow();
return tileMap.setDimensionY(targetRowCount);
}
/**
* @param targetColumnCount The number of rows to have.
*/
setGridColumnCount(targetColumnCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.setDimensionX(targetColumnCount);
}
getGridRowCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionY();
}
getGridColumnCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionX();
}
destroy(): void {

View File

@@ -413,7 +413,6 @@ namespace gdjs {
setOpacity(opacity: float): void {
this._opacity = opacity;
this._renderer.updateOpacity();
// TODO: Set all tile layers alpha and re-render tilemap.
}
getOpacity(): float {

View File

@@ -871,8 +871,13 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
// Create call
gd::String call;
if (instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
instrInfos.codeExtraInformation.type == "boolean") {
instrInfos.codeExtraInformation.type == "string" ||
// Boolean actions declared with addExpressionAndConditionAndAction uses
// MutatorAndOrAccessor even though they don't declare an operator parameter.
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
(instrInfos.codeExtraInformation.type == "boolean" &&
instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
if (instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
call = GenerateOperatorCall(
@@ -932,9 +937,14 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
// Create call
gd::String call;
if ((instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
instrInfos.codeExtraInformation.type == "boolean")) {
if (instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
// Boolean actions declared with addExpressionAndConditionAndAction uses
// MutatorAndOrAccessor even though they don't declare an operator parameter.
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
(instrInfos.codeExtraInformation.type == "boolean" &&
instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
if (instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
call = GenerateOperatorCall(

View File

@@ -892,22 +892,28 @@ void CommonInstructionsExtension::GenerateLocalVariableInitializationCode(
code += variableCodeName + ".setString(" +
EventsCodeGenerator::ConvertToStringExplicit(variable.GetString()) +
");\n";
} else if (variable.GetType() == gd::Variable::Structure ||
variable.GetType() == gd::Variable::Array) {
} else if (variable.GetType() == gd::Variable::Structure) {
const auto &childrenNames = variable.GetAllChildrenNames();
for (const auto& childName : variable.GetAllChildrenNames()) {
for (const auto &childName : variable.GetAllChildrenNames()) {
auto &child = variable.GetChild(childName);
code += "{\n";
GenerateLocalVariableInitializationCode(child, code, depth + 1);
auto childCodeName = "variable" + gd::String::From(depth + 1);
code += variableCodeName + ".addChild(" +
EventsCodeGenerator::ConvertToStringExplicit(childName) +
", " + childCodeName + ");\n";
EventsCodeGenerator::ConvertToStringExplicit(childName) + ", " +
childCodeName + ");\n";
code += "}\n";
}
if (variable.GetType() == gd::Variable::Array) {
code += variableCodeName + ".castTo('array');\n";
} else if (variable.GetType() == gd::Variable::Array) {
for (std::size_t i = 0; i < variable.GetChildrenCount(); i++) {
auto &child = variable.GetAtIndex(i);
code += "{\n";
GenerateLocalVariableInitializationCode(child, code, depth + 1);
auto childCodeName = "variable" + gd::String::From(depth + 1);
code += variableCodeName + "._pushVariable(" + childCodeName + ");\n";
code += "}\n";
}
}
}

View File

@@ -1,6 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="GDJS_PACKAGENAME" version="GDJS_PROJECTVERSION" xmlns="http://www.w3.org/ns/widgets"
xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:cdv="http://cordova.apache.org/ns/1.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<name>GDJS_PROJECTNAME</name>
<content src="index.html" />
<plugin name="cordova-plugin-whitelist" version="1" />
@@ -13,19 +14,24 @@
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served via https. -->
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served
via https. -->
<allow-navigation href="*" />
<platform name="android">
<allow-intent href="market:*" />
<!-- Required by admob-plus plugin > version 2 -->
<preference name="GradlePluginKotlinEnabled" value="true" />
<!-- Increase timeout value for low-end android devices -->
<preference name="loadUrlTimeoutValue" value="60000" />
<!-- GDJS_ICONS_ANDROID -->
<preference name="AndroidWindowSplashScreenBackground" value="#000000" />
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on Android. -->
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on
Android. -->
<config-file target="AndroidManifest.xml" parent="/manifest">
<queries>
<intent>
@@ -40,6 +46,9 @@
<preference name="SwiftVersion" value="5.3" />
<!-- Required by admob-plus plugin > version 2 -->
<preference name="deployment-target" value="12.0" />
<!-- GDJS_ICONS_IOS -->
</platform>

View File

@@ -395,9 +395,10 @@ namespace gdjs {
let minX = 0;
if (this._forcedDefaultSize) {
minX = this._forcedDefaultSize.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
} else {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
minX = this._unrotatedAABB.min[0];
}
const absScaleX = this.getScaleX();
@@ -416,9 +417,10 @@ namespace gdjs {
let minY = 0;
if (this._forcedDefaultSize) {
minY = this._forcedDefaultSize.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
} else {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
minY = this._unrotatedAABB.min[1];
}
const absScaleY = this.getScaleY();

View File

@@ -37,6 +37,18 @@ namespace gdjs {
if (this._isNextLayoutLoading || this._stack.length === 0) {
return false;
}
const hasMadeChangeToStack = this.applyUpdateFromNetworkSyncDataIfAny();
if (hasMadeChangeToStack) {
debugLogger.info(
'Scene stack has been updated from network sync data, skipping step.'
);
// If we have made changes to the stack as part of the network sync,
// we trust the network to be the source of truth for the scene stack,
// and skip the scene rendering (and so any other request to change the scene stack from it)
return true;
}
const currentScene = this._stack[this._stack.length - 1];
if (currentScene.renderAndStep(elapsedTime)) {
const request = currentScene.getRequestedChange();
@@ -58,8 +70,6 @@ namespace gdjs {
}
}
this.applyUpdateFromNetworkSyncDataIfAny();
return true;
}
@@ -232,9 +242,10 @@ namespace gdjs {
this._sceneStackSyncDataToApply = sceneStackSyncData;
}
applyUpdateFromNetworkSyncDataIfAny(): void {
applyUpdateFromNetworkSyncDataIfAny(): boolean {
const sceneStackSyncData = this._sceneStackSyncDataToApply;
if (!sceneStackSyncData) return;
let hasMadeChangeToStack = false;
if (!sceneStackSyncData) return hasMadeChangeToStack;
this._sceneStackSyncDataToApply = null;
@@ -251,11 +262,12 @@ namespace gdjs {
debugLogger.info(
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
);
// We have less scenes in the stack than the host, let's add the scene.
// We have fewer scenes in the stack than the host, let's add the scene.
const newScene = this.push(sceneSyncData.name);
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
hasMadeChangeToStack = true;
// Continue to the next scene in the stack received from the host.
continue;
}
@@ -275,6 +287,7 @@ namespace gdjs {
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
hasMadeChangeToStack = true;
// Continue to the next scene in the stack received from the host.
continue;
}
@@ -317,6 +330,7 @@ namespace gdjs {
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
hasMadeChangeToStack = true;
// Continue to the next scene in the stack received from the host.
continue;
}
@@ -330,7 +344,10 @@ namespace gdjs {
if (this._stack.length > sceneStackSyncData.length) {
const popCount = this._stack.length - sceneStackSyncData.length;
this.pop(popCount);
hasMadeChangeToStack = true;
}
return hasMadeChangeToStack;
}
}
}

View File

@@ -620,6 +620,16 @@ namespace gdjs {
this._childrenArray.push(variable.clone());
}
/**
* Pushes a variable into the array without duplicating it first.
* This should only be used by generated code.
*/
_pushVariable(variable: gdjs.Variable) {
if (this._type !== 'array') this.castTo('array');
this._childrenArray.push(variable);
}
/**
* Pushes a value into the array.
*/

View File

@@ -521,6 +521,7 @@ namespace gdjs {
return [];
},
pushVariableCopy: () => {},
_pushVariable: () => {},
pushValue: () => {},
removeAtIndex: function () {
return;

View File

@@ -226,6 +226,7 @@ interface PairStringVariable {
enum Variable_Type {
"Variable::Unknown",
"Variable::MixedTypes",
"Variable::String",
"Variable::Number",
"Variable::Boolean",
@@ -264,6 +265,7 @@ interface Variable {
double GetValue();
void SetBool([Const] boolean val);
boolean GetBool();
boolean HasMixedValues();
void SetFolded(boolean val);
boolean IsFolded();
@@ -352,6 +354,16 @@ interface ObjectGroup {
void UnserializeFrom([Const, Ref] SerializerElement element);
};
interface GroupVariableHelper {
[Value] VariablesContainer STATIC_MergeVariableContainers(
[Const, Ref] ObjectsContainersList objectsContainersList,
[Const, Ref] ObjectGroup objectGroup);
void STATIC_FillAnyVariableBetweenObjects(
[Ref] ObjectsContainer globalObjectsContainer,
[Ref] ObjectsContainer objectsContainer,
[Const, Ref] ObjectGroup objectGroup);
};
interface ObjectGroupsContainer {
void ObjectGroupsContainer();
@@ -2346,6 +2358,14 @@ interface WholeProjectRefactorer {
[Ref] VariablesContainer newVariablesContainer,
[Const, Ref] VariablesChangeset changeset,
[Const, Ref] SerializerElement originalSerializedVariables);
void STATIC_ApplyRefactoringForGroupVariablesContainer(
[Ref] Project project,
[Ref] ObjectsContainer globalObjectsContainer,
[Ref] ObjectsContainer objectsContainer,
[Const, Ref] VariablesContainer groupVariablesContainer,
[Const, Ref] ObjectGroup objectGroup,
[Const, Ref] VariablesChangeset changeset,
[Const, Ref] SerializerElement originalSerializedVariables);
void STATIC_RenameEventsFunctionsExtension(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
@@ -2433,43 +2453,82 @@ interface WholeProjectRefactorer {
[Ref] Project project,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameLayer(
void STATIC_RenameLayerInScene(
[Ref] Project project,
[Ref] Layout layout,
[Ref] Layout scene,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameLayerEffect(
void STATIC_RenameLayerInEventsBasedObject(
[Ref] Project project,
[Ref] Layout layout,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Ref] EventsBasedObject eventsBasedObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameLayerEffectInScene(
[Ref] Project project,
[Ref] Layout scene,
[Ref] Layer layer,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectAnimation(
void STATIC_RenameLayerEffectInEventsBasedObject(
[Ref] Project project,
[Ref] Layout layout,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Ref] EventsBasedObject eventsBasedObject,
[Ref] Layer layer,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectAnimationInScene(
[Ref] Project project,
[Ref] Layout scene,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectPoint(
void STATIC_RenameObjectAnimationInEventsBasedObject(
[Ref] Project project,
[Ref] Layout layout,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Ref] EventsBasedObject eventsBasedObject,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectEffect(
void STATIC_RenameObjectPointInScene(
[Ref] Project project,
[Ref] Layout layout,
[Ref] Layout scene,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_ObjectOrGroupRenamedInLayout([Ref] Project project, [Ref] Layout layout, [Const] DOMString oldName, [Const] DOMString newName, boolean isObjectGroup);
void STATIC_ObjectRemovedInLayout(
void STATIC_RenameObjectPointInEventsBasedObject(
[Ref] Project project,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Ref] EventsBasedObject eventsBasedObject,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectEffectInScene(
[Ref] Project project,
[Ref] Layout scene,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_RenameObjectEffectInEventsBasedObject(
[Ref] Project project,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Ref] EventsBasedObject eventsBasedObject,
[Ref] gdObject gdObject,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_ObjectOrGroupRenamedInScene(
[Ref] Project project,
[Ref] Layout layout,
[Ref] Layout scene,
[Const] DOMString oldName,
[Const] DOMString newName,
boolean isObjectGroup);
void STATIC_ObjectRemovedInScene(
[Ref] Project project,
[Ref] Layout scene,
[Const] DOMString objectName);
void STATIC_BehaviorsAddedToObjectInLayout(
void STATIC_BehaviorsAddedToObjectInScene(
[Ref] Project project,
[Ref] Layout layout,
[Ref] Layout scene,
[Const] DOMString objectName);
void STATIC_ObjectOrGroupRenamedInEventsFunction(
[Ref] Project project,

View File

@@ -47,6 +47,7 @@
#include <GDCore/IDE/Events/TextFormatting.h>
#include <GDCore/IDE/Events/UsedExtensionsFinder.h>
#include <GDCore/IDE/EventsFunctionTools.h>
#include <GDCore/IDE/GroupVariableHelper.h>
#include <GDCore/IDE/Project/ArbitraryResourceWorker.h>
#include <GDCore/IDE/Project/ArbitraryObjectsWorker.h>
#include <GDCore/IDE/Project/ObjectsUsingResourceCollector.h>
@@ -646,9 +647,9 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_Year Year
#define STATIC_Month Month
#define STATIC_Date Date
#define STATIC_ObjectOrGroupRenamedInLayout ObjectOrGroupRenamedInLayout
#define STATIC_ObjectRemovedInLayout ObjectRemovedInLayout
#define STATIC_BehaviorsAddedToObjectInLayout BehaviorsAddedToObjectInLayout
#define STATIC_ObjectOrGroupRenamedInScene ObjectOrGroupRenamedInScene
#define STATIC_ObjectRemovedInScene ObjectRemovedInScene
#define STATIC_BehaviorsAddedToObjectInScene BehaviorsAddedToObjectInScene
#define STATIC_ObjectRemovedInEventsFunction \
ObjectRemovedInEventsFunction
#define STATIC_ObjectOrGroupRenamedInEventsFunction \
@@ -723,8 +724,12 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_GetBehaviorFullType GetBehaviorFullType
#define STATIC_ApplyRefactoringForVariablesContainer \
ApplyRefactoringForVariablesContainer
#define STATIC_ApplyRefactoringForGroupVariablesContainer \
ApplyRefactoringForGroupVariablesContainer
#define STATIC_ComputeChangesetForVariablesContainer \
ComputeChangesetForVariablesContainer
#define STATIC_MergeVariableContainers MergeVariableContainers
#define STATIC_FillAnyVariableBetweenObjects FillAnyVariableBetweenObjects
#define STATIC_RenameEventsFunctionsExtension RenameEventsFunctionsExtension
#define STATIC_UpdateExtensionNameInEventsBasedBehavior \
UpdateExtensionNameInEventsBasedBehavior
@@ -746,11 +751,16 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_RenameLayout RenameLayout
#define STATIC_RenameExternalLayout RenameExternalLayout
#define STATIC_RenameExternalEvents RenameExternalEvents
#define STATIC_RenameLayer RenameLayer
#define STATIC_RenameLayerEffect RenameLayerEffect
#define STATIC_RenameObjectAnimation RenameObjectAnimation
#define STATIC_RenameObjectPoint RenameObjectPoint
#define STATIC_RenameObjectEffect RenameObjectEffect
#define STATIC_RenameLayerInScene RenameLayerInScene
#define STATIC_RenameLayerEffectInScene RenameLayerEffectInScene
#define STATIC_RenameObjectAnimationInScene RenameObjectAnimationInScene
#define STATIC_RenameObjectPointInScene RenameObjectPointInScene
#define STATIC_RenameObjectEffectInScene RenameObjectEffectInScene
#define STATIC_RenameLayerInEventsBasedObject RenameLayerInEventsBasedObject
#define STATIC_RenameLayerEffectInEventsBasedObject RenameLayerEffectInEventsBasedObject
#define STATIC_RenameObjectAnimationInEventsBasedObject RenameObjectAnimationInEventsBasedObject
#define STATIC_RenameObjectPointInEventsBasedObject RenameObjectPointInEventsBasedObject
#define STATIC_RenameObjectEffectInEventsBasedObject RenameObjectEffectInEventsBasedObject
#define STATIC_GetBehaviorPropertyGetterName GetBehaviorPropertyGetterName
#define STATIC_GetBehaviorPropertySetterName GetBehaviorPropertySetterName

View File

@@ -252,6 +252,13 @@ class Variable {
this._childrenArray.push(variable.clone());
return this;
}
/**
* @param {Variable} variable
*/
_pushVariable(variable) {
this._childrenArray.push(variable);
}
/**
*

View File

@@ -3643,7 +3643,7 @@ describe('libGD.js', function () {
instance1.setObjectName('Object1');
instance2.setObjectName('Object2');
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
gd.WholeProjectRefactorer.objectOrGroupRenamedInScene(
project,
layout,
'Object1',
@@ -3660,7 +3660,7 @@ describe('libGD.js', function () {
true
);
gd.WholeProjectRefactorer.objectRemovedInLayout(
gd.WholeProjectRefactorer.objectRemovedInScene(
project,
layout,
'Object3',

File diff suppressed because it is too large Load Diff

View File

@@ -420,7 +420,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
).toBe(1);
});
it('can generate a local child-variable condition', function () {
it('can generate a local child-variable condition on a structure', function () {
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
const runtimeScene = generateAndRunEventsForFunction([
{
@@ -455,6 +455,41 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
).toBe(1);
});
it('can generate a local child-variable condition on an array', function () {
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
const runtimeScene = generateAndRunEventsForFunction([
{
type: 'BuiltinCommonInstructions::Standard',
variables: [
{
name: 'MyLocalVariable',
type: 'array',
children: [{ name: '0', type: 'number', value: 123 }],
},
],
conditions: [
{
type: { inverted: false, value: 'NumberVariable' },
parameters: ['MyLocalVariable[0]', '=', '123'],
},
],
actions: [
{
type: { value: 'SetNumberVariable' },
parameters: ['SuccessVariable', '=', '1'],
},
],
events: [],
},
]);
expect(
runtimeScene
.getVariablesForExtension('Extension')
.get('SuccessVariable')
.getAsNumber()
).toBe(1);
});
it('can generate a local variable condition giving precedence to the closest local variable', function () {
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);

View File

@@ -346,7 +346,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
).toBe(1);
});
it('can generate a local child-variable condition', function () {
it('can generate a local child-variable condition on a structure', function () {
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
const runtimeScene = generateAndRunEventsForLayout([
{
@@ -378,6 +378,38 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
).toBe(1);
});
it('can generate a local child-variable condition on an array', function () {
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
const runtimeScene = generateAndRunEventsForLayout([
{
type: 'BuiltinCommonInstructions::Standard',
variables: [
{
name: 'MyLocalVariable',
type: 'array',
children: [{ name: '0', type: 'number', value: 123 }],
},
],
conditions: [
{
type: { inverted: false, value: 'NumberVariable' },
parameters: ['MyLocalVariable[0]', '=', '123'],
},
],
actions: [
{
type: { value: 'SetNumberVariable' },
parameters: ['SuccessVariable', '=', '1'],
},
],
events: [],
},
]);
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(1);
});
it('can generate a local variable condition giving precedence to the closest local variable', function () {
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);

View File

@@ -105,7 +105,7 @@ type EventsFunctionsContainer_FunctionOwner = 0 | 1 | 2`
fs.writeFileSync(
'types/variable_type.js',
`// Automatically generated by GDevelop.js/scripts/generate-types.js
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5`
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5 | 6`
);
shell.sed(
'-i',
@@ -113,11 +113,12 @@ type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5`
[
'declare class gdVariable {',
' static Unknown: 0;',
' static String: 1;',
' static Number: 2;',
' static Boolean: 3;',
' static Structure: 4;',
' static Array: 5;',
' static MixedTypes: 1;',
' static String: 2;',
' static Number: 3;',
' static Boolean: 4;',
' static Structure: 5;',
' static Array: 6;',
].join('\n'),
'types/gdvariable.js'
);

View File

@@ -21,11 +21,12 @@ declare class EmscriptenObject {
export enum Variable_Type {
Unknown = 0,
String = 1,
Number = 2,
Boolean = 3,
Structure = 4,
Array = 5,
MixedTypes = 1,
String = 2,
Number = 3,
Boolean = 4,
Structure = 5,
Array = 6,
}
export enum VariablesContainer_SourceType {
@@ -279,6 +280,7 @@ export class Variable extends EmscriptenObject {
getValue(): number;
setBool(val: boolean): void;
getBool(): boolean;
hasMixedValues(): boolean;
setFolded(val: boolean): void;
isFolded(): boolean;
getChildrenCount(): number;
@@ -346,6 +348,11 @@ export class ObjectGroup extends EmscriptenObject {
unserializeFrom(element: SerializerElement): void;
}
export class GroupVariableHelper extends EmscriptenObject {
static mergeVariableContainers(objectsContainersList: ObjectsContainersList, objectGroup: ObjectGroup): VariablesContainer;
static fillAnyVariableBetweenObjects(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, objectGroup: ObjectGroup): void;
}
export class ObjectGroupsContainer extends EmscriptenObject {
constructor();
has(name: string): boolean;
@@ -1794,6 +1801,7 @@ export class VariablesChangeset extends EmscriptenObject {
export class WholeProjectRefactorer extends EmscriptenObject {
static computeChangesetForVariablesContainer(oldSerializedVariablesContainer: SerializerElement, newVariablesContainer: VariablesContainer): VariablesChangeset;
static applyRefactoringForVariablesContainer(project: Project, newVariablesContainer: VariablesContainer, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
static applyRefactoringForGroupVariablesContainer(project: Project, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, groupVariablesContainer: VariablesContainer, objectGroup: ObjectGroup, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
static renameEventsFunctionsExtension(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
static updateExtensionNameInEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedBehavior: EventsBasedBehavior, sourceExtensionName: string): void;
static renameEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
@@ -1810,14 +1818,19 @@ export class WholeProjectRefactorer extends EmscriptenObject {
static renameLayout(project: Project, oldName: string, newName: string): void;
static renameExternalLayout(project: Project, oldName: string, newName: string): void;
static renameExternalEvents(project: Project, oldName: string, newName: string): void;
static renameLayer(project: Project, layout: Layout, oldName: string, newName: string): void;
static renameLayerEffect(project: Project, layout: Layout, layer: Layer, oldName: string, newName: string): void;
static renameObjectAnimation(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPoint(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffect(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static objectOrGroupRenamedInLayout(project: Project, layout: Layout, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInLayout(project: Project, layout: Layout, objectName: string): void;
static behaviorsAddedToObjectInLayout(project: Project, layout: Layout, objectName: string): void;
static renameLayerInScene(project: Project, scene: Layout, oldName: string, newName: string): void;
static renameLayerInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, oldName: string, newName: string): void;
static renameLayerEffectInScene(project: Project, scene: Layout, layer: Layer, oldName: string, newName: string): void;
static renameLayerEffectInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, layer: Layer, oldName: string, newName: string): void;
static renameObjectAnimationInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectAnimationInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPointInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPointInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffectInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffectInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static objectOrGroupRenamedInScene(project: Project, scene: Layout, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInScene(project: Project, scene: Layout, objectName: string): void;
static behaviorsAddedToObjectInScene(project: Project, scene: Layout, objectName: string): void;
static objectOrGroupRenamedInEventsFunction(project: Project, projectScopedContainers: ProjectScopedContainers, eventsFunction: EventsFunction, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInEventsFunction(project: Project, eventsFunction: EventsFunction, objectName: string): void;
static objectOrGroupRenamedInEventsBasedObject(project: Project, projectScopedContainers: ProjectScopedContainers, eventsBasedObject: EventsBasedObject, oldName: string, newName: string, isObjectGroup: boolean): void;

View File

@@ -0,0 +1,7 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdGroupVariableHelper {
static mergeVariableContainers(objectsContainersList: gdObjectsContainersList, objectGroup: gdObjectGroup): gdVariablesContainer;
static fillAnyVariableBetweenObjects(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectGroup: gdObjectGroup): void;
delete(): void;
ptr: number;
};

View File

@@ -1,11 +1,12 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdVariable {
static Unknown: 0;
static String: 1;
static Number: 2;
static Boolean: 3;
static Structure: 4;
static Array: 5;
static MixedTypes: 1;
static String: 2;
static Number: 3;
static Boolean: 4;
static Structure: 5;
static Array: 6;
constructor(): void;
static isPrimitive(type: Variable_Type): boolean;
getType(): Variable_Type;
@@ -16,6 +17,7 @@ declare class gdVariable {
getValue(): number;
setBool(val: boolean): void;
getBool(): boolean;
hasMixedValues(): boolean;
setFolded(val: boolean): void;
isFolded(): boolean;
getChildrenCount(): number;

View File

@@ -2,6 +2,7 @@
declare class gdWholeProjectRefactorer {
static computeChangesetForVariablesContainer(oldSerializedVariablesContainer: gdSerializerElement, newVariablesContainer: gdVariablesContainer): gdVariablesChangeset;
static applyRefactoringForVariablesContainer(project: gdProject, newVariablesContainer: gdVariablesContainer, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
static applyRefactoringForGroupVariablesContainer(project: gdProject, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, groupVariablesContainer: gdVariablesContainer, objectGroup: gdObjectGroup, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
static renameEventsFunctionsExtension(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
static updateExtensionNameInEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, sourceExtensionName: string): void;
static renameEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
@@ -18,14 +19,19 @@ declare class gdWholeProjectRefactorer {
static renameLayout(project: gdProject, oldName: string, newName: string): void;
static renameExternalLayout(project: gdProject, oldName: string, newName: string): void;
static renameExternalEvents(project: gdProject, oldName: string, newName: string): void;
static renameLayer(project: gdProject, layout: gdLayout, oldName: string, newName: string): void;
static renameLayerEffect(project: gdProject, layout: gdLayout, layer: gdLayer, oldName: string, newName: string): void;
static renameObjectAnimation(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPoint(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffect(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static objectOrGroupRenamedInLayout(project: gdProject, layout: gdLayout, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInLayout(project: gdProject, layout: gdLayout, objectName: string): void;
static behaviorsAddedToObjectInLayout(project: gdProject, layout: gdLayout, objectName: string): void;
static renameLayerInScene(project: gdProject, scene: gdLayout, oldName: string, newName: string): void;
static renameLayerInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string): void;
static renameLayerEffectInScene(project: gdProject, scene: gdLayout, layer: gdLayer, oldName: string, newName: string): void;
static renameLayerEffectInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, layer: gdLayer, oldName: string, newName: string): void;
static renameObjectAnimationInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectAnimationInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPointInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectPointInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffectInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
static renameObjectEffectInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
static objectOrGroupRenamedInScene(project: gdProject, scene: gdLayout, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInScene(project: gdProject, scene: gdLayout, objectName: string): void;
static behaviorsAddedToObjectInScene(project: gdProject, scene: gdLayout, objectName: string): void;
static objectOrGroupRenamedInEventsFunction(project: gdProject, projectScopedContainers: gdProjectScopedContainers, eventsFunction: gdEventsFunction, oldName: string, newName: string, isObjectGroup: boolean): void;
static objectRemovedInEventsFunction(project: gdProject, eventsFunction: gdEventsFunction, objectName: string): void;
static objectOrGroupRenamedInEventsBasedObject(project: gdProject, projectScopedContainers: gdProjectScopedContainers, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string, isObjectGroup: boolean): void;

View File

@@ -71,6 +71,7 @@ declare class libGDevelop {
VariablesContainer: Class<gdVariablesContainer>;
VariablesContainersList: Class<gdVariablesContainersList>;
ObjectGroup: Class<gdObjectGroup>;
GroupVariableHelper: Class<gdGroupVariableHelper>;
ObjectGroupsContainer: Class<gdObjectGroupsContainer>;
PlatformSpecificAssets: Class<gdPlatformSpecificAssets>;
LoadingScreen: Class<gdLoadingScreen>;

View File

@@ -1,2 +1,2 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5 | 6

View File

@@ -75,7 +75,7 @@ export namespace LDtkTileMapLoader {
gridSize,
dimX,
dimY,
tileSet
tileSet,
);
const composedTileMap = new Map<string, TileDefinition>();
let nextComposedTileId = 0xfffffff;
@@ -89,7 +89,7 @@ export namespace LDtkTileMapLoader {
const gridSize = layer.__gridSize;
const tilesetId = layer.__tilesetDefUid;
const editableTileLayer = editableTileMap.addTileLayer(iLayer);
const editableTileLayer = editableTileMap.addNewTileLayer(iLayer);
editableTileLayer.setAlpha(layer.__opacity);
editableTileLayer.setVisible(layer.visible);

View File

@@ -117,7 +117,7 @@ export namespace TiledTileMapLoader {
tiledTileMap.tileheight,
tiledTileMap.width,
tiledTileMap.height,
definitions
definitions,
);
for (const tiledLayer of tiledTileMap.layers) {
@@ -156,7 +156,7 @@ export namespace TiledTileMapLoader {
layerData = tiledLayer.data as integer[];
}
if (layerData) {
const collisionTileLayer = collisionTileMap.addTileLayer(
const collisionTileLayer = collisionTileMap.addNewTileLayer(
tiledLayer.id
);
collisionTileLayer.setAlpha(tiledLayer.opacity);

View File

@@ -3,3 +3,17 @@ export declare type float = number;
export type FloatPoint = [float, float];
export type PolygonVertices = FloatPoint[];
export type EditableTileMapLayerAsJsObject = {
id: number;
alpha: number;
tiles: number[][];
};
export type EditableTileMapAsJsObject = {
tileWidth: number;
tileHeight: number;
dimX: number;
dimY: number;
layers: EditableTileMapLayerAsJsObject[];
};

View File

@@ -1,4 +1,10 @@
import { PolygonVertices, integer, float } from "./CommonTypes";
import {
PolygonVertices,
integer,
float,
EditableTileMapAsJsObject,
EditableTileMapLayerAsJsObject,
} from "./CommonTypes";
import { FlippingHelper } from "./GID";
/**
@@ -29,6 +35,10 @@ export class EditableTileMap {
* The number of tile rows in the map.
*/
private dimY: integer;
/**
* True if is allowed to set a tile out of the tile map's bounds.
* Useful when editing the tile map easily.
*/
/**
* @param tileWidth The width of a tile.
@@ -63,12 +73,16 @@ export class EditableTileMap {
* @param objectConfiguration
*/
static from(
editableTileMapAsJsObject: any,
editableTileMapAsJsObject: EditableTileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
tileSetRowCount,
}: { tileSize: number; tileSetColumnCount: number; tileSetRowCount: number }
}: {
tileSize: number;
tileSetColumnCount: number;
tileSetRowCount: number;
}
): EditableTileMap {
const tileSet = new Map<number, TileDefinition>();
@@ -89,7 +103,7 @@ export class EditableTileMap {
if (editableTileMapAsJsObject.layers) {
editableTileMapAsJsObject.layers.forEach((layerAsJsObject: any) => {
tileMap.setTileLayer(
tileMap.addTileLayer(
EditableTileMapLayer.from(
layerAsJsObject,
tileMap,
@@ -98,7 +112,7 @@ export class EditableTileMap {
);
});
} else {
tileMap.addTileLayer(0);
tileMap.addNewTileLayer(0);
}
return tileMap;
@@ -157,16 +171,81 @@ export class EditableTileMap {
}
/**
* Changes the number of columns in the tile map by adding/removing
* columns at the end.
* @param dim The number of tile columns in the map.
*/
setDimensionX(dim: integer): void {
if (dim === this.dimX) return;
const columnDelta = dim - this.dimX;
for (const layer of this.getLayers()) {
// TODO: Implement dimensions changes for EditableObjectLayer.
if (layer instanceof EditableTileMapLayer) {
if (columnDelta > 0) {
layer.increaseDimensions(columnDelta, 0, 0, 0);
} else {
layer.reduceDimensions(-columnDelta, 0, 0, 0);
}
}
}
this.dimX = dim;
}
/**
* Increases dimensions of the tile map by adding columns and rows
* at the start and/or at the end of the grid.
*/
increaseDimensions(
columnsToAppend: number,
columnsToUnshift: number,
rowsToAppend: number,
rowsToUnshift: number
): void {
if (
columnsToAppend < 0 ||
columnsToUnshift < 0 ||
rowsToAppend < 0 ||
rowsToUnshift < 0 ||
(columnsToAppend === 0 &&
columnsToUnshift === 0 &&
rowsToAppend === 0 &&
rowsToUnshift === 0)
) {
return;
}
for (const layer of this.getLayers()) {
// TODO: Implement dimensions changes for EditableObjectLayer.
if (layer instanceof EditableTileMapLayer) {
layer.increaseDimensions(
columnsToAppend,
columnsToUnshift,
rowsToAppend,
rowsToUnshift
);
}
}
this.dimX = this.dimX + columnsToAppend + columnsToUnshift;
this.dimY = this.dimY + rowsToAppend + rowsToUnshift;
}
/**
* Changes the number of row in the tile map by adding/removing
* rows at the end.
* @param dim The number of tile rows in the map.
*/
setDimensionY(dim: integer): void {
if (dim === this.dimY) return;
const rowDelta = dim - this.dimY;
for (const layer of this.getLayers()) {
// TODO: Implement dimensions changes for EditableObjectLayer.
if (layer instanceof EditableTileMapLayer) {
if (rowDelta > 0) {
layer.increaseDimensions(0, 0, rowDelta, 0);
} else {
layer.reduceDimensions(0, 0, -rowDelta, 0);
}
}
}
this.dimY = dim;
}
@@ -189,7 +268,7 @@ export class EditableTileMap {
* @param id The identifier of the new layer.
* @returns The new layer.
*/
addTileLayer(id: integer): EditableTileMapLayer {
addNewTileLayer(id: integer): EditableTileMapLayer {
const layer = new EditableTileMapLayer(this, id);
this._layers.push(layer);
return layer;
@@ -198,7 +277,7 @@ export class EditableTileMap {
/**
* @param layer the new layer to set.
*/
setTileLayer(layer: EditableTileMapLayer): void {
addTileLayer(layer: EditableTileMapLayer): void {
this._layers.push(layer);
}
@@ -276,6 +355,105 @@ export class EditableTileMap {
isEmpty(): boolean {
return this._layers.every((layer) => layer.isEmpty());
}
getTileId(x: integer, y: integer, layerId: integer): integer {
const layer = this.getTileLayer(layerId);
if (!layer) return -1;
const tileId = layer.getTileId(x, y);
return tileId === undefined ? -1 : tileId;
}
setTile(x: integer, y: integer, layerId: integer, tileId: number) {
const layer = this.getTileLayer(layerId);
if (!layer) return;
layer.setTile(x, y, tileId);
}
flipTileOnY(x: integer, y: integer, layerId: integer, flip: boolean) {
const layer = this.getTileLayer(layerId);
if (!layer) return;
layer.setFlippedVertically(x, y, flip);
}
flipTileOnX(x: integer, y: integer, layerId: integer, flip: boolean) {
const layer = this.getTileLayer(layerId);
if (!layer) return;
layer.setFlippedHorizontally(x, y, flip);
}
isTileFlippedOnX(x: integer, y: integer, layerId: integer): boolean {
const layer = this.getTileLayer(layerId);
if (!layer) return false;
return layer.isFlippedHorizontally(x, y);
}
isTileFlippedOnY(x: integer, y: integer, layerId: integer): boolean {
const layer = this.getTileLayer(layerId);
if (!layer) return false;
return layer.isFlippedVertically(x, y);
}
removeTile(x: integer, y: integer, layerId: integer) {
if (x < 0 || x >= this.dimX || y < 0 || y >= this.dimY) {
return;
}
const layer = this.getTileLayer(layerId);
if (!layer) return;
layer.removeTile(x, y);
}
trimEmptyColumnsAndRowToFitLayer(
layerId: integer
):
| {
poppedRows: number;
poppedColumns: number;
shiftedRows: number;
shiftedColumns: number;
}
| undefined {
const layer = this.getTileLayer(layerId);
if (!layer) return;
const initialRowCount = this.dimY;
const initialColumnCount = this.dimX;
if (layer.isEmpty() && this._layers.length === 1) {
// The tile map is empty. Instead of having an object with null width and height,
// the tile map is resized to have a size of 1x1 with an empty tile. This is useful
// in the editor. It might need to have a different behavior in the runtime.
layer.buildEmptyLayer(1, 1);
this.dimX = 1;
this.dimY = 1;
return {
shiftedRows: 0,
shiftedColumns: 0,
poppedRows: initialRowCount - 1,
poppedColumns: initialColumnCount - 1,
};
}
const trimmingData = layer.getTrimmingData();
for (const layer of this.getLayers()) {
// TODO: Implement dimensions changes for EditableObjectLayer.
if (layer instanceof EditableTileMapLayer) {
layer.reduceDimensions(
trimmingData.columnsToPop,
trimmingData.columnsToShift,
trimmingData.rowsToPop,
trimmingData.rowsToShift
);
}
}
this.dimX =
initialColumnCount -
trimmingData.columnsToPop -
trimmingData.columnsToShift;
this.dimY =
initialRowCount - trimmingData.rowsToPop - trimmingData.rowsToShift;
return {
poppedRows: trimmingData.rowsToPop,
poppedColumns: trimmingData.columnsToPop,
shiftedRows: trimmingData.rowsToShift,
shiftedColumns: trimmingData.columnsToShift,
};
}
}
/**
@@ -452,7 +630,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
}
static from(
editableTileMapLayerAsJsObject: any,
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer {
@@ -461,7 +639,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
editableTileMapLayerAsJsObject.id
);
layer.setAlpha(editableTileMapLayerAsJsObject.alpha);
editableTileMapLayerAsJsObject.tiles.forEach((row: Int32Array, y: number) =>
editableTileMapLayerAsJsObject.tiles.forEach((row: number[], y: number) =>
row.forEach((tileGID, x) => {
const tileId = FlippingHelper.getTileId(tileGID);
if (isTileIdValid(tileId)) {
@@ -480,11 +658,12 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
// Array.from is needed to convert Int32Array to Array. Otherwise, JSON.stringify
// serializes it as an object with index as keys.
Array.from(
row.map(
(_, x) =>
// -1 corresponds to null value
this.getTileGID(x, y) || -1
)
row.map((_, x) => {
const tileGID = this.getTileGID(x, y);
// -1 corresponds to null value
if (tileGID === undefined) return -1;
return tileGID;
})
)
),
};
@@ -514,9 +693,6 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
rowsToPop: number,
rowsToShift: number
) {
const initialRowCount = this._tiles.length;
const initialColumnCount = this._tiles[0].length;
if (rowsToPop > 0 || rowsToShift > 0) {
this._tiles = this._tiles.slice(
rowsToShift,
@@ -531,12 +707,6 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
);
});
}
// TODO: Instead of setting the dimensions directly, should it call a method on
// EditableTileMap that will iterates over all the layers to change their dimensions?
this.tileMap.setDimensionX(
initialColumnCount - columnsToPop - columnsToShift
);
this.tileMap.setDimensionY(initialRowCount - rowsToPop - rowsToShift);
}
increaseDimensions(
@@ -557,6 +727,10 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
});
}
if (rowsToAppend > 0 || rowsToUnshift > 0) {
// TODO: Consider over-provisioning columns and rows to avoid this operation being made
// too often, especially in a case where tiles are added towards the outside.
// Beware of over-provisioning rows above and/or columns on the left as it is supposed
// to change the object position.
this._tiles.unshift(
...new Array(rowsToUnshift)
.fill(0)
@@ -579,86 +753,27 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
).fill(0);
}
}
// TODO: Instead of setting the dimensions directly, should it call a method on
// EditableTileMap that will iterates over all the layers to change their dimensions?
this.tileMap.setDimensionX(
initialColumnCount + columnsToAppend + columnsToUnshift
);
this.tileMap.setDimensionY(initialRowCount + rowsToAppend + rowsToUnshift);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile.
* @param options Flipping options.
*/
setTile(
x: integer,
y: integer,
tileId: integer,
options:
| {
flipVertically: boolean;
flipHorizontally: boolean;
flipDiagonally: boolean;
}
| undefined = {
flipVertically: false,
flipHorizontally: false,
flipDiagonally: false,
}
):
| {
unshiftedRows: number;
unshiftedColumns: number;
appendedRows: number;
appendedColumns: number;
}
| undefined {
setTile(x: integer, y: integer, tileId: integer) {
const definition = this.tileMap.getTileDefinition(tileId);
if (!definition) {
console.error(`Invalid tile definition index: ${tileId}`);
return;
}
const rowsToAdd = Math.max(0, y - (this._tiles.length - 1));
const columnsToAdd = Math.max(0, x - (this._tiles[0].length - 1));
const rowsToUnshift = Math.abs(Math.min(0, y));
const columnsToUnshift = Math.abs(Math.min(0, x));
if (rowsToAdd || columnsToAdd || rowsToUnshift || columnsToUnshift) {
this.increaseDimensions(
columnsToAdd,
columnsToUnshift,
rowsToAdd,
rowsToUnshift
);
}
// Dimensions have been changed to support setting tiles in positions below 0.
// So we adapt the indices.
const newX = x + columnsToUnshift;
const newY = y + rowsToUnshift;
const tilesRow = this._tiles[newY];
const tilesRow = this._tiles[y];
if (!tilesRow || x >= tilesRow.length) {
// Coordinates are out of bounds, don't do anything.
return;
}
tilesRow[newX] =
FlippingHelper.setFlippedHorizontally(
FlippingHelper.setFlippedVertically(
FlippingHelper.setFlippedDiagonally(tileId, options.flipDiagonally),
options.flipVertically
),
options.flipHorizontally
) +
// +1 because 0 mean null
1;
return {
unshiftedRows: rowsToUnshift,
unshiftedColumns: columnsToUnshift,
appendedRows: rowsToAdd,
appendedColumns: columnsToAdd,
};
// +1 because 0 means null
tilesRow[x] = tileId + 1;
}
/**
@@ -673,15 +788,15 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
return;
}
// +1 because 0 mean null
// +1 because 0 means null
tilesRow[x] = tileGID + 1;
}
trimEmptyColumnsAndRow(): {
shiftedRows: number;
shiftedColumns: number;
poppedRows: number;
poppedColumns: number;
getTrimmingData(): {
rowsToShift: number;
columnsToShift: number;
rowsToPop: number;
columnsToPop: number;
} {
let rowsToShift = 0,
rowsToPop = 0;
@@ -717,29 +832,20 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
}
}
if (!isFirstNonEmptyRowFound) {
// The tile map is empty. Instead of having an object with null width and height,
// the tile map is resized to have a size of 1x1 with an empty tile. This is useful
// in the editor. It might need to have a different behavior in the runtime.
this.buildEmptyLayer(1, 1);
// TODO: Instead of setting the dimensions directly, should it call a method on
// EditableTileMap that will iterates over all the layers to change their dimensions?
this.tileMap.setDimensionX(1);
this.tileMap.setDimensionY(1);
return {
shiftedColumns: 0,
shiftedRows: 0,
poppedColumns: initialDimensionX - 1,
poppedRows: initialDimensionY - 1,
columnsToShift: 0,
rowsToShift: 0,
columnsToPop: initialDimensionX - 1,
rowsToPop: initialDimensionY - 1,
};
}
const columnsToShift = Math.min(...columnsToShiftByRow);
const columnsToPop = Math.min(...columnsToPopByRow);
this.reduceDimensions(columnsToPop, columnsToShift, rowsToPop, rowsToShift);
return {
shiftedRows: rowsToShift,
shiftedColumns: columnsToShift,
poppedRows: rowsToPop,
poppedColumns: columnsToPop,
rowsToShift,
columnsToShift,
rowsToPop,
columnsToPop,
};
}
@@ -754,7 +860,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
return;
}
// 0 mean null
// 0 means null
tilesRow[x] = 0;
}

View File

@@ -4,6 +4,7 @@ import { TileTextureCache } from "./TileTextureCache";
import { PixiTileMapHelper } from "./TileMapPixiHelper";
import { TileMapLoader } from "../load/TileMapLoader";
import { TileMapFileContent } from "../load/TileMapFileContent";
import { EditableTileMapAsJsObject } from "../model/CommonTypes";
/**
* A holder to share tile maps across the 2 extension objects.
@@ -118,7 +119,7 @@ export class TileMapManager {
}
getOrLoadSimpleTileMap(
tileMapAsJsObject: object,
tileMapAsJsObject: EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
tileSetColumnCount: number,
@@ -130,11 +131,14 @@ export class TileMapManager {
// TODO: Is it useful to cache the tilemap since it belongs to an instance?
// const key = `${objectName}|${tileSize}|${tileSetColumnCount}|${tileSetRowCount}`;
const editableTileMap = EditableTileMap.from(tileMapAsJsObject, {
tileSize,
tileSetColumnCount,
tileSetRowCount,
});
const editableTileMap = EditableTileMap.from(
tileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
tileSetRowCount,
},
);
callback(editableTileMap);
}

View File

@@ -77,7 +77,11 @@ export namespace PixiTileMapHelper {
const texture = new PIXI.Texture(atlasTexture, rect);
textureCache.setTexture(rowCount * x + y, texture);
textureCache.setTexture(
// Id of the tile
columnCount * y + x,
texture
);
}
}
return textureCache;

View File

@@ -164,7 +164,21 @@ export const useShopNavigation = (): NavigationState => {
previousHistory.previousPages[
previousHistory.previousPages.length - 1
];
if (!isSearchResultPage(currentPage)) {
if (isSearchResultPage(currentPage)) {
const updatedCurrentPage = {
...currentPage,
pageBreakIndex: 0,
scrollPosition: 0,
};
return {
...previousHistory,
previousPages: [
// All pages except the last one
...previousHistory.previousPages.slice(0, -1),
updatedCurrentPage,
],
};
} else {
return {
...previousHistory,
previousPages: [
@@ -173,8 +187,6 @@ export const useShopNavigation = (): NavigationState => {
],
};
}
return previousHistory;
});
},
openTagPage: (tag: string) => {

View File

@@ -224,6 +224,7 @@ const PageBreakNavigation = ({
export type AssetsListInterface = {|
getScrollPosition: () => number,
scrollToPosition: (y: number) => void,
setPageBreakIndex: (index: number) => void,
|};
type Props = {|
@@ -306,6 +307,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
isNavigatingInsideFolder,
setIsNavigatingInsideFolder,
] = React.useState<boolean>(false);
const [pageBreakIndex, setPageBreakIndex] = React.useState<number>(
(currentPage && currentPage.pageBreakIndex) || 0
);
const { openedAssetPack, selectedFolders } = React.useMemo(
() => {
if (!currentPage) {
@@ -333,6 +337,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
scrollViewElement.scrollToPosition(y);
},
setPageBreakIndex: (index: number) => {
setPageBreakIndex(0);
},
}));
const fetchAssetsAndGameTemplates = React.useCallback(
@@ -458,10 +465,6 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
[allPrivateAssetPackListingDatas, openedAssetPack]
);
const [pageBreakIndex, setPageBreakIndex] = React.useState<number>(
(currentPage && currentPage.pageBreakIndex) || 0
);
const assetTiles = React.useMemo(
() => {
// Loading

View File

@@ -574,23 +574,27 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
hideGameTemplates ? t`Search assets` : `Search the shop`
}
value={searchText}
onChange={
isOnSearchResultPage
? // An existing search is already being done: just update the
// search text and the store will update the search results.
setSearchText
: (newValue: string) => {
setSearchText(newValue);
// A new search is being initiated: navigate to the search page,
// and clear the history as a new search was launched.
if (!!newValue) {
shopNavigationState.clearHistory();
shopNavigationState.openSearchResultPage();
openFiltersPanelIfAppropriate();
}
}
}
onChange={(newValue: string) => {
setSearchText(newValue);
if (isOnSearchResultPage) {
// An existing search is already being done: just move to the
// top search results.
shopNavigationState.openSearchResultPage();
const assetsListInterface = assetsList.current;
if (assetsListInterface) {
assetsListInterface.scrollToPosition(0);
assetsListInterface.setPageBreakIndex(0);
}
} else {
// A new search is being initiated: navigate to the search page,
// and clear the history as a new search was launched.
if (!!newValue) {
shopNavigationState.clearHistory();
shopNavigationState.openSearchResultPage();
openFiltersPanelIfAppropriate();
}
}
}}
onRequestSearch={() => {}}
ref={searchBar}
id="asset-store-search-bar"

View File

@@ -14,7 +14,7 @@ type Props = {|
value: string,
onChange: string => void,
disabled?: boolean,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|};
type State = {|
behaviorMetadata: Array<EnumeratedBehaviorMetadata>,

View File

@@ -18,7 +18,7 @@ export type EnumeratedBehaviorMetadata = {|
export const enumerateBehaviorsMetadata = (
platform: gdPlatform,
project: gdProject,
eventsFunctionsExtension?: ?gdEventsFunctionsExtension
eventsFunctionsExtension: gdEventsFunctionsExtension | null
): Array<EnumeratedBehaviorMetadata> => {
const extensionsList = platform.getAllPlatformExtensions();

View File

@@ -27,7 +27,7 @@ const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
eventsFunctionsExtension?: ?gdEventsFunctionsExtension,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
objectType: string,
objectBehaviorsTypes: Array<string>,
open: boolean,

View File

@@ -269,7 +269,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
type Props = {|
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
object: gdObject,
onUpdateBehaviorsSharedData: () => void,
onSizeUpdated?: ?() => void,

View File

@@ -39,7 +39,6 @@ export type CommandName =
| 'TOGGLE_LAYERS_PANEL'
| 'SCENE_EDITOR_UNDO'
| 'SCENE_EDITOR_REDO'
| 'RENAME_SCENE_OBJECT'
| 'DELETE_INSTANCES'
| 'TOGGLE_WINDOW_MASK'
| 'TOGGLE_GRID'
@@ -243,10 +242,6 @@ const commandsList: { [CommandName]: CommandMetadata } = {
displayText: t`Redo the last changes`,
noShortcut: true,
},
RENAME_SCENE_OBJECT: {
area: 'SCENE',
displayText: t`Rename the selected object`,
},
DELETE_INSTANCES: {
area: 'SCENE',
displayText: t`Delete the selected instances from the scene`,

View File

@@ -731,6 +731,7 @@ export default function EventsBasedBehaviorPropertiesEditor({
{property.getType() === 'Behavior' && (
<BehaviorTypeSelector
project={project}
eventsFunctionsExtension={extension}
objectType={
behaviorObjectType || ''
}

View File

@@ -283,7 +283,9 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
object={this.state.editedObjectWithContext.object}
initialTab={this.state.editedObjectInitialTab}
project={project}
layout={null}
eventsFunctionsExtension={eventsFunctionsExtension}
eventsBasedObject={eventsBasedObject}
projectScopedContainersAccessor={
this.props.projectScopedContainersAccessor
}

View File

@@ -0,0 +1,105 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import { type EventsBasedObjectCreationParameters } from '../EventsFunctionsList/EventsBasedObjectTreeViewItemContent';
import FlatButton from '../UI/FlatButton';
import { List, ListItem } from '../UI/List';
import Dialog from '../UI/Dialog';
import HelpButton from '../UI/HelpButton';
import Object2DIcon from '../UI/CustomSvgIcons/Object2d';
import Object3DIcon from '../UI/CustomSvgIcons/Object3d';
type Props = {|
onCancel: () => void,
onChoose: (parameters: EventsBasedObjectCreationParameters) => void,
|};
const styles = {
icon: { width: 40, height: 40 },
disabledItem: { opacity: 0.6 },
};
const FunctionListItem = ({
icon,
disabled,
onChoose,
name,
description,
}: {|
icon: React.Node,
disabled?: boolean,
onChoose: () => void,
name: React.Node,
description: React.Node,
|}) => {
return (
<ListItem
leftIcon={icon}
primaryText={name}
secondaryText={description}
secondaryTextLines={2}
onClick={onChoose}
style={disabled ? styles.disabledItem : undefined}
disabled={disabled}
/>
);
};
export default function EventsBasedObjectSelectorDialog({
onChoose,
onCancel,
}: Props) {
return (
<Dialog
title={<Trans>Choose a new object type</Trans>}
actions={[
<FlatButton
label={<Trans>Cancel</Trans>}
keyboardFocused={true}
onClick={onCancel}
key={'close'}
/>,
]}
secondaryActions={[
<HelpButton
key="help"
helpPagePath="/objects/custom-objects-prefab-template"
/>,
]}
open
onRequestClose={onCancel}
>
<List>
<FunctionListItem
icon={<Object2DIcon style={styles.icon} />}
name={<Trans>2D object</Trans>}
onChoose={() =>
onChoose({
isRenderedIn3D: false,
})
}
description={
<Trans>
An object that can be moved, rotated and scaled in 2D.
</Trans>
}
/>
<FunctionListItem
icon={<Object3DIcon style={styles.icon} />}
name={<Trans>3D object</Trans>}
onChoose={() =>
onChoose({
isRenderedIn3D: true,
})
}
description={
<Trans>
An object that can be moved, rotated and scaled in 3D.
</Trans>
}
/>
</List>
</Dialog>
);
}

View File

@@ -28,9 +28,10 @@ const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
eventsFunction: gdEventsFunction,
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject,
eventsFunctionsContainer: ?gdEventsFunctionsContainer,
eventsBasedBehavior: gdEventsBasedBehavior | null,
eventsBasedObject: gdEventsBasedObject | null,
eventsFunctionsContainer: gdEventsFunctionsContainer | null,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
onParametersUpdated: () => void,
helpPagePath?: string,
freezeParameters?: boolean,
@@ -68,6 +69,7 @@ export const EventsFunctionParametersEditor = ({
eventsBasedBehavior,
eventsBasedObject,
eventsFunctionsContainer,
eventsFunctionsExtension,
onParametersUpdated,
helpPagePath,
freezeParameters,
@@ -400,6 +402,7 @@ export const EventsFunctionParametersEditor = ({
<ColumnStackLayout expand noMargin>
<ValueTypeEditor
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
valueTypeMetadata={parameter.getValueTypeMetadata()}
disabled={isParameterDisabled(i)}
isTypeSelectorShown={isParameterTypeShown(i)}

View File

@@ -29,9 +29,10 @@ const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
eventsFunction: gdEventsFunction,
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject,
eventsFunctionsContainer: ?gdEventsFunctionsContainer,
eventsBasedBehavior: gdEventsBasedBehavior | null,
eventsBasedObject: gdEventsBasedObject | null,
eventsFunctionsContainer: gdEventsFunctionsContainer | null,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
helpPagePath?: string,
onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void,
renderConfigurationHeader?: () => React.Node,
@@ -143,6 +144,7 @@ const getDescriptionHintText = (
export const EventsFunctionPropertiesEditor = ({
project,
eventsFunctionsExtension,
eventsFunction,
freezeEventsFunctionType,
onConfigurationUpdated,
@@ -500,6 +502,7 @@ export const EventsFunctionPropertiesEditor = ({
<ValueTypeEditor
isExpressionType
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
valueTypeMetadata={eventsFunction.getExpressionType()}
isTypeSelectorShown={true}
onTypeUpdated={() => {

View File

@@ -14,6 +14,7 @@ import useForceUpdate from '../../Utils/UseForceUpdate';
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
valueTypeMetadata: gdValueTypeMetadata,
onTypeUpdated: () => void,
disabled?: boolean,
@@ -44,6 +45,7 @@ const getIdentifierName = (scopedIdentifier: string) =>
export default function ValueTypeEditor({
project,
eventsFunctionsExtension,
valueTypeMetadata,
disabled,
isTypeSelectorShown,
@@ -191,6 +193,7 @@ export default function ValueTypeEditor({
{valueTypeMetadata.isBehavior() && (
<BehaviorTypeSelector
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
objectType={getLastObjectParameterObjectType()}
value={valueTypeMetadata.getExtraInfo()}
onChange={(value: string) => {

View File

@@ -21,9 +21,10 @@ type Props = {|
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
objectsContainer: gdObjectsContainer,
eventsFunction: gdEventsFunction,
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject,
eventsBasedBehavior: gdEventsBasedBehavior | null,
eventsBasedObject: gdEventsBasedObject | null,
eventsFunctionsContainer: gdEventsFunctionsContainer,
eventsFunctionsExtension: gdEventsFunctionsExtension,
onParametersOrGroupsUpdated: () => void,
helpPagePath?: string,
onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void,
@@ -132,6 +133,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
render() {
const {
project,
projectScopedContainersAccessor,
objectsContainer,
eventsFunction,
eventsBasedBehavior,
@@ -147,6 +149,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
onMoveObjectEventsParameter,
getFunctionGroupNames,
eventsFunctionsContainer,
eventsFunctionsExtension,
} = this.props;
return (
@@ -182,6 +185,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
eventsBasedBehavior={eventsBasedBehavior}
eventsBasedObject={eventsBasedObject}
eventsFunctionsContainer={eventsFunctionsContainer}
eventsFunctionsExtension={eventsFunctionsExtension}
helpPagePath={helpPagePath}
onConfigurationUpdated={onConfigurationUpdated}
renderConfigurationHeader={renderConfigurationHeader}
@@ -200,6 +204,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
eventsBasedBehavior={eventsBasedBehavior}
eventsBasedObject={eventsBasedObject}
eventsFunctionsContainer={eventsFunctionsContainer}
eventsFunctionsExtension={eventsFunctionsExtension}
onParametersUpdated={onParametersOrGroupsUpdated}
helpPagePath={helpPagePath}
freezeParameters={freezeParameters}
@@ -214,6 +219,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
{this.state.currentTab === 'groups' ? (
<ObjectGroupsListWithObjectGroupEditor
project={project}
projectScopedContainersAccessor={projectScopedContainersAccessor}
globalObjectsContainer={null}
objectsContainer={objectsContainer}
globalObjectGroups={null}

View File

@@ -16,6 +16,7 @@ import EventsFunctionsListWithErrorBoundary, {
type EventsFunctionsListInterface,
} from '../EventsFunctionsList';
import { type EventsFunctionCreationParameters } from '../EventsFunctionsList/EventsFunctionTreeViewItemContent';
import { type EventsBasedObjectCreationParameters } from '../EventsFunctionsList/EventsBasedObjectTreeViewItemContent';
import Background from '../UI/Background';
import OptionsEditorDialog from './OptionsEditorDialog';
import EventsBasedBehaviorEditorPanel from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel';
@@ -24,6 +25,7 @@ import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog';
import ObjectMethodSelectorDialog from './ObjectMethodSelectorDialog';
import ExtensionFunctionSelectorDialog from './ExtensionFunctionSelectorDialog';
import EventsBasedObjectSelectorDialog from './EventsBasedObjectSelectorDialog';
import { ResponsiveWindowMeasurer } from '../UI/Responsive/ResponsiveWindowMeasurer';
import EditorNavigator, {
type EditorNavigatorInterface,
@@ -85,10 +87,14 @@ type State = {|
behaviorMethodSelectorDialogOpen: boolean,
objectMethodSelectorDialogOpen: boolean,
extensionFunctionSelectorDialogOpen: boolean,
eventsBasedObjectSelectorDialogOpen: boolean,
variablesEditorOpen: { isGlobalTabInitiallyOpen: boolean } | null,
onAddEventsFunctionCb: ?(
parameters: ?EventsFunctionCreationParameters
) => void,
onAddEventsBasedObjectCb: ?(
parameters: ?EventsBasedObjectCreationParameters
) => void,
|};
const extensionEditIconReactNode = <ExtensionEditIcon />;
@@ -121,8 +127,10 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
behaviorMethodSelectorDialogOpen: false,
objectMethodSelectorDialogOpen: false,
extensionFunctionSelectorDialogOpen: false,
eventsBasedObjectSelectorDialogOpen: false,
variablesEditorOpen: null,
onAddEventsFunctionCb: null,
onAddEventsBasedObjectCb: null,
};
editor: ?EventsSheetInterface;
eventsFunctionList: ?EventsFunctionsListInterface;
@@ -772,6 +780,32 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
);
};
_onCloseEventsBasedObjectSelectorDialog = (
parameters: ?EventsBasedObjectCreationParameters
) => {
const { onAddEventsBasedObjectCb } = this.state;
this.setState(
{
eventsBasedObjectSelectorDialogOpen: false,
onAddEventsBasedObjectCb: null,
},
() => {
if (onAddEventsBasedObjectCb) onAddEventsBasedObjectCb(parameters);
}
);
};
_onAddEventsBasedObject = (
onAddEventsBasedObjectCb: (
parameters: ?EventsBasedObjectCreationParameters
) => void
) => {
this.setState({
eventsBasedObjectSelectorDialogOpen: true,
onAddEventsBasedObjectCb,
});
};
_onAddEventsFunction = (
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject,
@@ -1156,6 +1190,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
behaviorMethodSelectorDialogOpen,
objectMethodSelectorDialogOpen,
extensionFunctionSelectorDialogOpen,
eventsBasedObjectSelectorDialogOpen,
variablesEditorOpen,
} = this.state;
@@ -1197,6 +1232,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
selectedEventsBasedEntity.getEventsFunctions()) ||
eventsFunctionsExtension
}
eventsFunctionsExtension={eventsFunctionsExtension}
objectsContainer={this._objectsContainer}
onConfigurationUpdated={this._onConfigurationUpdated}
helpPagePath={
@@ -1384,6 +1420,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
i18n
)}
onEventsBasedObjectRenamed={this._onEventsBasedObjectRenamed}
onAddEventsBasedObject={this._onAddEventsBasedObject}
onSelectExtensionProperties={() => this._editOptions(true)}
onSelectExtensionGlobalVariables={() =>
this._editVariables({ isGlobalTabInitiallyOpen: true })
@@ -1516,6 +1553,14 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
}
/>
)}
{eventsBasedObjectSelectorDialogOpen && (
<EventsBasedObjectSelectorDialog
onCancel={() => this._onCloseEventsBasedObjectSelectorDialog(null)}
onChoose={parameters =>
this._onCloseEventsBasedObjectSelectorDialog(parameters)
}
/>
)}
</React.Fragment>
);
}

View File

@@ -19,6 +19,10 @@ import Add from '../UI/CustomSvgIcons/Add';
const EVENTS_BASED_OBJECT_CLIPBOARD_KIND = 'Events Based Object';
export type EventsBasedObjectCreationParameters = {|
isRenderedIn3D: boolean,
|};
export type EventsBasedObjectCallbacks = {|
onSelectEventsBasedObject: (eventsBasedObject: ?gdEventsBasedObject) => void,
onDeleteEventsBasedObject: (
@@ -30,6 +34,9 @@ export type EventsBasedObjectCallbacks = {|
newName: string,
cb: (boolean) => void
) => void,
onAddEventsBasedObject: (
(parameters: ?EventsBasedObjectCreationParameters) => void
) => void,
onEventsBasedObjectRenamed: (eventsBasedObject: gdEventsBasedObject) => void,
onOpenCustomObjectEditor: (eventsBasedObject: gdEventsBasedObject) => void,
|};

View File

@@ -45,6 +45,7 @@ import {
getObjectTreeViewItemId,
type EventsBasedObjectProps,
type EventsBasedObjectCallbacks,
type EventsBasedObjectCreationParameters,
} from './EventsBasedObjectTreeViewItemContent';
import { type HTMLDataset } from '../Utils/HTMLDataset';
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
@@ -538,6 +539,7 @@ const EventsFunctionsList = React.forwardRef<
onDeleteEventsBasedObject,
onRenameEventsBasedObject,
onEventsBasedObjectRenamed,
onAddEventsBasedObject,
selectedEventsFunction,
selectedEventsBasedBehavior,
selectedEventsBasedObject,
@@ -774,47 +776,57 @@ const EventsFunctionsList = React.forwardRef<
const addNewEventsBasedObject = React.useCallback(
() => {
const eventBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
onAddEventsBasedObject(
(parameters: ?EventsBasedObjectCreationParameters) => {
if (!parameters) {
return;
}
const name = newNameGenerator('MyObject', name =>
eventBasedObjects.has(name)
const eventBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
const name = newNameGenerator('MyObject', name =>
eventBasedObjects.has(name)
);
const newEventsBasedObject = eventBasedObjects.insertNew(
name,
eventBasedObjects.getCount()
);
newEventsBasedObject.markAsRenderedIn3D(parameters.isRenderedIn3D);
if (unsavedChanges) {
unsavedChanges.triggerUnsavedChanges();
}
forceUpdate();
const objectItemId = getObjectTreeViewItemId(newEventsBasedObject);
if (treeViewRef.current) {
treeViewRef.current.openItems([
objectItemId,
extensionObjectsRootFolderId,
]);
}
// Scroll to the new function.
// Ideally, we'd wait for the list to be updated to scroll, but
// to simplify the code, we just wait a few ms for a new render
// to be done.
setTimeout(() => {
scrollToItem(objectItemId);
}, 100); // A few ms is enough for a new render to be done.
// We focus it so the user can edit the name directly.
onSelectEventsBasedObject(newEventsBasedObject);
editName(objectItemId);
}
);
const newEventsBasedObject = eventBasedObjects.insertNew(
name,
eventBasedObjects.getCount()
);
if (unsavedChanges) {
unsavedChanges.triggerUnsavedChanges();
}
forceUpdate();
const objectItemId = getObjectTreeViewItemId(newEventsBasedObject);
if (treeViewRef.current) {
treeViewRef.current.openItems([
objectItemId,
extensionObjectsRootFolderId,
]);
}
// Scroll to the new function.
// Ideally, we'd wait for the list to be updated to scroll, but
// to simplify the code, we just wait a few ms for a new render
// to be done.
setTimeout(() => {
scrollToItem(objectItemId);
}, 100); // A few ms is enough for a new render to be done.
// We focus it so the user can edit the name directly.
onSelectEventsBasedObject(newEventsBasedObject);
editName(objectItemId);
},
[
editName,
onAddEventsBasedObject,
eventsFunctionsExtension,
forceUpdate,
scrollToItem,
onSelectEventsBasedObject,
unsavedChanges,
forceUpdate,
onSelectEventsBasedObject,
editName,
scrollToItem,
]
);
@@ -986,6 +998,7 @@ const EventsFunctionsList = React.forwardRef<
onDeleteEventsBasedObject,
onRenameEventsBasedObject,
onEventsBasedObjectRenamed,
onAddEventsBasedObject,
addNewEventsFunction,
selectedEventsBasedBehavior,
selectedEventsBasedObject,
@@ -1009,6 +1022,7 @@ const EventsFunctionsList = React.forwardRef<
onDeleteEventsBasedObject,
onRenameEventsBasedObject,
onEventsBasedObjectRenamed,
onAddEventsBasedObject,
addNewEventsFunction,
selectedEventsBasedBehavior,
selectedEventsBasedObject,

View File

@@ -294,6 +294,7 @@ export default class EventsFunctionExtractorDialog extends React.Component<
eventsBasedBehavior={null}
eventsBasedObject={null}
eventsFunctionsContainer={null}
eventsFunctionsExtension={null}
onConfigurationUpdated={() => {
// Force re-running logic to see if Create button is disabled.
this.forceUpdate();
@@ -307,6 +308,7 @@ export default class EventsFunctionExtractorDialog extends React.Component<
eventsBasedBehavior={null}
eventsBasedObject={null}
eventsFunctionsContainer={null}
eventsFunctionsExtension={null}
onParametersUpdated={() => {
// Force the dialog to adapt its size
this.forceUpdate();

View File

@@ -427,7 +427,7 @@ const InstructionEditorDialog = ({
{newBehaviorDialogOpen && chosenObject && (
<NewBehaviorDialog
project={project}
eventsFunctionsExtension={scope.eventsFunctionsExtension}
eventsFunctionsExtension={scope.eventsFunctionsExtension || null}
open={newBehaviorDialogOpen}
objectType={chosenObject.getType()}
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}

View File

@@ -33,13 +33,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
focus,
}));
const { scope, instruction, expression, parameterIndex } = props;
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
// We don't memo/callback this, as we want to recompute it every time something changes.
// Because of the function getPreviousParameterValue.
const getEffectNames = () => {
const { layout } = scope;
if (!layout) return [];
const layersSource = layout || eventsBasedObject;
if (!layersSource) return [];
const layerName =
tryExtractStringLiteralContent(
getPreviousParameterValue({
@@ -48,8 +48,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
parameterIndex,
})
) || ''; // If no layer name is provided, this is the Base layer.
if (!layout.hasLayerNamed(layerName)) return [];
const layer = layout.getLayer(layerName);
const layersContainer = layersSource.getLayers();
if (!layersContainer.hasLayerNamed(layerName)) return [];
const layer = layersContainer.getLayer(layerName);
return enumerateEffectNames(layer.getEffects()).sort();
};
@@ -60,10 +61,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
effectName => `"${effectName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInEffectNamesList) ||
props.scope.eventsFunctionsExtension
(!!props.value && !isCurrentValueInEffectNamesList) || !canAutocomplete
);
const switchFieldType = () => {
@@ -133,25 +135,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -35,12 +35,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
}));
const { project, scope, instruction, expression, parameterIndex } = props;
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
// We don't memo/callback this, as we want to recompute it every time something changes.
// Because of the function getPreviousParameterValue.
const getEffectParameterNames = () => {
const { layout } = scope;
if (!layout || !project) return [];
const layersSource = layout || eventsBasedObject;
if (!layersSource || !project) return [];
const layerName =
tryExtractStringLiteralContent(
@@ -50,8 +51,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
parameterIndex: parameterIndex ? parameterIndex - 1 : null,
})
) || ''; // If no layer name is provided, this is the Base layer.
if (!layout.hasLayerNamed(layerName)) return [];
const layer = layout.getLayer(layerName);
const layersContainer = layersSource.getLayers();
if (!layersContainer.hasLayerNamed(layerName)) return [];
const layer = layersContainer.getLayer(layerName);
const effectName = tryExtractStringLiteralContent(
getPreviousParameterValue({
@@ -82,10 +84,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
effectParameterName => `"${effectParameterName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInEffectParameterNamesList) ||
props.scope.eventsFunctionsExtension
!canAutocomplete
);
const switchFieldType = () => {
@@ -159,25 +163,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect property</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect property</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -31,12 +31,16 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
focus,
}));
const { layout } = props.scope;
const { layout, eventsFunctionsExtension, eventsBasedObject } = props.scope;
// The list is not kept with a memo because layers could be changed by
// another component without this one to know.
const layerNames = layout
? mapFor(0, layout.getLayersCount(), i => {
const layer = layout.getLayerAt(i);
const layersSource = layout || eventsBasedObject;
const layersContainer = layersSource ? layersSource.getLayers() : null;
const layerNames = layersContainer
? mapFor(0, layersContainer.getLayersCount(), i => {
const layer = layersContainer.getLayerAt(i);
return layer.getName();
})
: [];
@@ -45,10 +49,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
layerName => `"${layerName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list of layers, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInLayersList) ||
props.scope.eventsFunctionsExtension
(!!props.value && !isCurrentValueInLayersList) || !canAutocomplete
);
const switchFieldType = () => {
@@ -122,25 +127,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select a layer</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select a layer</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -43,6 +43,8 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
parameterIndex,
} = props;
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
// We don't memo/callback this, as we want to recompute it every time something changes.
// Because of the function getLastObjectParameterValue.
const getAnimationNames = () => {
@@ -60,7 +62,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
const object = getObjectByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectName
);
if (!object) {
@@ -129,10 +131,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
animationName => `"${animationName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list of animation names, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInAnimationNamesList) ||
props.scope.eventsFunctionsExtension
(!!props.value && !isCurrentValueInAnimationNamesList) || !canAutocomplete
);
const switchFieldType = () => {
@@ -202,25 +205,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an animation</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an animation</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -42,6 +42,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
expression,
parameterIndex,
} = props;
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
// We don't memo/callback this, as we want to recompute it every time something changes.
// Because of the function getLastObjectParameterValue.
@@ -59,7 +60,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
const object = getObjectByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectOrGroupName
);
if (object) {
@@ -67,7 +68,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
}
const group = getObjectGroupByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectOrGroupName
);
if (group) {
@@ -76,7 +77,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
objectName => {
const object = getObjectByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectName
);
if (!object) {
@@ -97,10 +98,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
effectName => `"${effectName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInEffectNamesList) ||
props.scope.eventsFunctionsExtension
(!!props.value && !isCurrentValueInEffectNamesList) || !canAutocomplete
);
const switchFieldType = () => {
@@ -170,25 +172,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -38,12 +38,14 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
focus,
}));
const { scope } = props;
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
// We don't memo/callback this, as we want to recompute it every time something changes.
// Because of the function getLastObjectParameterValue.
const getEffectParameterNames = (): Array<string> => {
const {
project,
scope,
instructionMetadata,
instruction,
expressionMetadata,
@@ -72,7 +74,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
let effectType: string | null = null;
const object = getObjectByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectOrGroupName
);
if (object && object.getEffects().hasEffectNamed(effectName)) {
@@ -85,7 +87,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
if (!effectType) {
const group = getObjectGroupByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectOrGroupName
);
if (group) {
@@ -96,7 +98,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
objectName => {
const object = getObjectByName(
project.getObjects(),
scope.layout ? scope.layout.getObjects() : null,
layout ? layout.getObjects() : null,
objectName
);
if (!object) {
@@ -139,10 +141,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
effectParameterName => `"${effectParameterName}"` === props.value
);
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
// If the current value is not in the list, display an expression field.
const [isExpressionField, setIsExpressionField] = React.useState(
(!!props.value && !isCurrentValueInEffectParameterNamesList) ||
props.scope.eventsFunctionsExtension
!canAutocomplete
);
const switchFieldType = () => {
@@ -216,25 +220,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
)
}
renderButton={style =>
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect property</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
canAutocomplete ? (
isExpressionField ? (
<FlatButton
id="switch-expression-select"
leftIcon={<TypeCursorSelect />}
style={style}
primary
label={<Trans>Select an effect property</Trans>}
onClick={switchFieldType}
/>
) : (
<RaisedButton
id="switch-expression-select"
icon={<Functions />}
style={style}
primary
label={<Trans>Use an expression</Trans>}
onClick={switchFieldType}
/>
)
) : null
}
/>
);

View File

@@ -7,6 +7,7 @@ import VariableField, {
type VariableDialogOpeningProps,
} from './VariableField';
import ObjectVariablesDialog from '../../VariablesList/ObjectVariablesDialog';
import ObjectGroupVariablesDialog from '../../VariablesList/ObjectGroupVariablesDialog';
import {
type ParameterFieldProps,
type ParameterFieldInterface,
@@ -18,6 +19,7 @@ import getObjectGroupByName from '../../Utils/GetObjectGroupByName';
import ObjectVariableIcon from '../../UI/CustomSvgIcons/ObjectVariable';
import { enumerateVariables } from './EnumerateVariables';
import { intersectionBy } from 'lodash';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
const gd: libGDevelop = global.gd;
@@ -94,6 +96,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
expression,
parameterIndex,
});
const objectGroup = objectName
? getObjectGroupByName(
globalObjectsContainer,
objectsContainer,
objectName
)
: null;
const { layout } = scope;
const variablesContainers = React.useMemo<Array<gdVariablesContainer>>(
@@ -132,6 +141,20 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
[onChange, onInstructionTypeChanged, value]
);
const onComputeAllVariableNames = React.useCallback(
() => {
if (!project || !layout || !objectName) return [];
return EventsRootVariablesFinder.findAllObjectVariables(
project.getCurrentPlatform(),
project,
layout, // TODO: Handle this for custom objects?
objectName
);
},
[layout, objectName, project]
);
return (
<React.Fragment>
<VariableField
@@ -153,8 +176,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
onRequestClose={props.onRequestClose}
onApply={props.onApply}
ref={field}
// There is no variable editor for groups.
onOpenDialog={variablesContainers.length === 1 ? setEditorOpen : null}
onOpenDialog={setEditorOpen}
globalObjectsContainer={props.globalObjectsContainer}
objectsContainer={props.objectsContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
@@ -166,10 +188,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
}
onInstructionTypeChanged={onInstructionTypeChanged}
/>
{editorOpen && project && (
{editorOpen && project && !objectGroup && (
<ObjectVariablesDialog
project={project}
layout={layout}
projectScopedContainersAccessor={projectScopedContainersAccessor}
objectName={objectName}
variablesContainer={variablesContainers[0]}
@@ -179,6 +200,22 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
preventRefactoringToDeleteInstructions
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
onComputeAllVariableNames={onComputeAllVariableNames}
/>
)}
{editorOpen && project && objectGroup && (
<ObjectGroupVariablesDialog
project={project}
projectScopedContainersAccessor={projectScopedContainersAccessor}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
objectGroup={objectGroup}
onCancel={() => setEditorOpen(null)}
onApply={onVariableEditorApply}
open
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
onComputeAllVariableNames={onComputeAllVariableNames}
/>
)}
</React.Fragment>

View File

@@ -1275,6 +1275,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
},
() => {
if (this._eventsTree) this._eventsTree.forceEventsUpdate();
// The focus is lost because the button "(or paste conditions/actions)"
// no long exist.
// Ideally, we'd wait for the events to be updated, but
// to simplify the code, we just wait a few ms for a new render
// to be done.
setTimeout(() => {
this._ensureFocused();
}, 100); // A few ms is enough for a new render to be done.
}
);
};

Some files were not shown because too many files have changed in this diff Show More