Compare commits

...

64 Commits

Author SHA1 Message Date
Arthur Pacaud
058b73db66 Replace electron-is with electron-is-dev 2024-08-07 20:10:55 +02:00
D8H
75566c9f38 Avoid to use an intermediate ThreeGroup for custom objects in the editor (#6837)
- 2D custom objects displayed in the editor now also use the origin point of their 1st child unless an hidden property force a location.
2024-08-07 18:24:39 +02:00
AlexandreS
8db8cbe3c2 Display the number of tilemap objects available in premium asset packs before purchase (#6851) 2024-08-07 16:40:50 +02:00
D8H
dd8c5dce2e Fix access to the prefab editor by hiding the drop-down menu item (#6850) 2024-08-06 11:47:49 +02:00
Arthur Pacaud (arthuro555)
2810056626 Allow hot-reloading of 3D models (#6848) 2024-08-05 21:33:14 +02:00
D8H
d082d4f5a4 Fix child object recycling in custom objects (#6846) 2024-08-05 10:08:58 +02:00
Florian Rival
eaa0f4077f Bump newIDE version (#6844) 2024-08-02 12:01:32 +02:00
D8H
7d73f58bc4 Fix a crash when renaming a child variable (#6839) 2024-08-01 16:13:51 +02:00
AlexandreS
9171b40429 Fix sentence in tile id action/condition (#6836) 2024-07-31 17:44:28 +02:00
AlexandreS
242fc49342 Bump newIDE version (#6834) 2024-07-31 14:18:56 +02:00
github-actions[bot]
7081fd570c Update translations [skip ci] (#6831) 2024-07-31 14:10:55 +02:00
D8H
34ecdce7fa Fix a regression on the virtual joystick position in the editor (#6832) 2024-07-31 12:03:22 +02:00
Joseph Spagnuolo
3a0903a19f Fix: correctly update version numbers after extension update (#6828) 2024-07-31 11:56:30 +02:00
AlexandreS
e1a4f88ba6 Implement new design for education marketing form (#6829) 2024-07-31 11:48:07 +02:00
AlexandreS
ed59498835 Remove unused imports (#6830) 2024-07-31 11:41:08 +02:00
D8H
4b70653a0b Add animation autocompletion for object groups (#6817) 2024-07-31 10:19:31 +02:00
github-actions[bot]
48dd91043a Update translations [skip ci] (#6825) 2024-07-31 09:14:58 +02:00
Aurélien Vivet
30e7ef5865 Fix some languages names (#6779)
Update iso-639-1 dependency 

---------

Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-07-30 18:13:14 +02:00
D8H
289c555fe4 Fix a crash in the object group editor of events-functions (#6823) 2024-07-30 18:01:43 +02:00
Florian Rival
803a55869c Forbid usage of constructor.name in the codebase (#6819)
Only show in developer changelog
2024-07-30 17:33:22 +02:00
AlexandreS
282f4c184f Add distraction-free mode for students (#6821) 2024-07-30 17:31:59 +02:00
AlexandreS
875237cc8b Fix global objects not being rendered on the scene editor (#6822) 2024-07-30 17:05:58 +02:00
github-actions[bot]
7aeb44d05e Update translations [skip ci] (#6820) 2024-07-30 17:03:25 +02:00
Florian Rival
e445ff710e Fix tilemap drawing blocked if touch going out of the canvas
Don't show in changelog
2024-07-30 11:05:36 +02:00
Florian Rival
e016babf71 Increase cloud project names max length (#6818) 2024-07-29 19:54:12 +02:00
github-actions[bot]
6a34e3c1db Update translations [skip ci] (#6802) 2024-07-29 16:50:52 +02:00
AlexandreS
82e819c033 Fix a few things in the new object group dialog (#6814)
Don't show in changelog
2024-07-29 16:49:38 +02:00
AlexandreS
72e5deeef3 Fix mobile users experience with tilemap painting (#6813)
Don't show in changelog
2024-07-29 15:53:53 +02:00
D8H
a91cc4557f Fix to make platformer wall collisions with tile maps more precise (#6810) 2024-07-29 15:35:52 +02:00
D8H
b2c953a31d Fix behavior autocompletion to be case insensitive (#6812) 2024-07-29 15:32:38 +02:00
D8H
b29fbaa1f8 Fix name collision between group variables and object variables (#6808)
- Don't show in changelog
2024-07-29 10:50:32 +02:00
D8H
e8faa17e12 Add a search bar in the properties editor (#6807) 2024-07-27 18:16:26 +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
304 changed files with 15514 additions and 5758 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

@@ -116,6 +116,13 @@ void SpriteAnimationList::ExposeResources(gd::ArbitraryResourceWorker& worker) {
}
}
bool SpriteAnimationList::HasAnimationNamed(const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const Animation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
const Animation& SpriteAnimationList::GetAnimation(std::size_t nb) const {
if (nb >= animations.size()) return badAnimation;

View File

@@ -51,6 +51,11 @@ class GD_CORE_API SpriteAnimationList {
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
/**
* \brief Return true if an animation exists for a given name.
*/
bool HasAnimationNamed(const gd::String &name) const;
/**
* \brief Add an animation at the end of the existing ones.
*/

View File

@@ -87,6 +87,19 @@ bool SpriteObject::UpdateInitialInstanceProperty(
return true;
}
size_t SpriteObject::GetAnimationsCount() const {
return animations.GetAnimationsCount();
}
const gd::String &SpriteObject::GetAnimationName(size_t index) const {
return animations.GetAnimation(index).GetName();
}
bool SpriteObject::HasAnimationNamed(
const gd::String &name) const {
return animations.HasAnimationNamed(name);
}
const SpriteAnimationList& SpriteObject::GetAnimations() const {
return animations;
}

View File

@@ -52,6 +52,12 @@ class GD_CORE_API SpriteObject : public gd::ObjectConfiguration {
const gd::String& name,
const gd::String& value) override;
size_t GetAnimationsCount() const override;
const gd::String &GetAnimationName(size_t index) const override;
bool HasAnimationNamed(const gd::String &animationName) const override;
/**
* \brief Return the animation configuration.
*/

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,196 @@
/*
* 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) {
if (variablesContainer.Has(variableName)) {
// It can happens if an object already had the variable but it was not
// shared by other object of the group.
continue;
}
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;
if (variablesContainer.Has(newVariableName)) {
// It can happens if an object already had the variable but it was not
// shared by other object of the group.
variablesContainer.Remove(oldVariableName);
} else {
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

@@ -224,6 +224,20 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
}
}
std::size_t CustomObjectConfiguration::GetAnimationsCount() const {
return animations.GetAnimationsCount();
}
const gd::String &
CustomObjectConfiguration::GetAnimationName(size_t index) const {
return animations.GetAnimation(index).GetName();
}
bool CustomObjectConfiguration::HasAnimationNamed(
const gd::String &name) const {
return animations.HasAnimationNamed(name);
}
const SpriteAnimationList& CustomObjectConfiguration::GetAnimations() const {
return animations;
}

View File

@@ -67,6 +67,12 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
gd::ObjectConfiguration &GetChildObjectConfiguration(const gd::String& objectName);
std::size_t GetAnimationsCount() const override;
const gd::String &GetAnimationName(size_t index) const override;
bool HasAnimationNamed(const gd::String &animationName) const override;
/**
* \brief Return the animation configuration for Animatable custom objects.
*/

View File

@@ -14,6 +14,7 @@
#include "GDCore/Tools/Log.h"
namespace gd {
gd::String ObjectConfiguration::badAnimationName;
ObjectConfiguration::~ObjectConfiguration() {}

View File

@@ -165,7 +165,36 @@ class GD_CORE_API ObjectConfiguration {
void UnserializeFrom(gd::Project& project, const SerializerElement& element);
///@}
protected:
/** \name Animations
* Members functions related to object animations
*/
///@{
/**
* \brief Return the number of animations declared in this object
* configuration.
*/
virtual size_t GetAnimationsCount() const {
return 0;
};
/**
* \brief Return the name of an animation declared in this object
* configuration.
*/
virtual const gd::String &GetAnimationName(size_t index) const {
return badAnimationName;
}
/**
* \brief Return true if an animation is declared in this object
* configuration for a given name.
*/
virtual bool HasAnimationNamed(const gd::String &animationName) const {
return false;
}
///@}
protected:
gd::String type; ///< Which type of object is represented by this
///< configuration.
@@ -181,6 +210,9 @@ class GD_CORE_API ObjectConfiguration {
* custom attributes.
*/
virtual void DoSerializeTo(SerializerElement& element) const {};
private:
static gd::String badAnimationName;
};
} // namespace gd

View File

@@ -73,6 +73,15 @@ bool ObjectsContainersList::HasObjectNamed(const gd::String& name) const {
return false;
}
const gd::Object* ObjectsContainersList::GetObject(const gd::String& name) const {
for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend();
++it) {
if ((*it)->HasObjectNamed(name)) return &(*it)->GetObject(name);
}
return nullptr;
}
ObjectsContainersList::VariableExistence
ObjectsContainersList::HasObjectOrGroupWithVariableNamed(
const gd::String& objectOrGroupName, const gd::String& variableName) const {
@@ -368,7 +377,7 @@ std::vector<gd::String> ObjectsContainersList::ExpandObjectName(
}
// Ensure that all returned objects actually exists (i.e: if some groups have
// names refering to non existing objects, don't return them).
// names referring to non existing objects, don't return them).
for (std::size_t i = 0; i < realObjects.size();) {
if (!HasObjectNamed(realObjects[i]))
realObjects.erase(realObjects.begin() + i);
@@ -521,4 +530,63 @@ std::vector<gd::String> ObjectsContainersList::GetBehaviorsOfObject(
*objectsContainers[0], *objectsContainers[1], objectName, searchInGroups);
}
std::vector<gd::String> ObjectsContainersList::GetAnimationNamesOfObject(
const gd::String &objectOrGroupName) const {
std::vector<gd::String> animationNames;
for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend();
++it) {
if ((*it)->HasObjectNamed(objectOrGroupName)) {
const auto &objectConfiguration =
(*it)->GetObject(objectOrGroupName).GetConfiguration();
for (size_t index = 0; index < objectConfiguration.GetAnimationsCount();
index++) {
animationNames.push_back(objectConfiguration.GetAnimationName(index));
}
return animationNames;
}
if ((*it)->GetObjectGroups().Has(objectOrGroupName)) {
const auto &objectGroup = (*it)->GetObjectGroups().Get(objectOrGroupName);
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 (!HasObjectNamed(objectName)) {
continue;
}
isFirstObjectFound = true;
const auto &objectConfiguration =
GetObject(objectName)->GetConfiguration();
for (size_t index = 0; index < objectConfiguration.GetAnimationsCount();
index++) {
animationNames.push_back(objectConfiguration.GetAnimationName(index));
}
}
for (; objectIndex < objectNames.size(); objectIndex++) {
const gd::String &objectName = objectNames[objectIndex];
if (!HasObjectNamed(objectName)) {
continue;
}
const auto &objectConfiguration =
GetObject(objectName)->GetConfiguration();
for (size_t animationIndex = 0; animationIndex < animationNames.size();
animationIndex++) {
if (!objectConfiguration.HasAnimationNamed(
animationNames[animationIndex])) {
animationNames.erase(animationNames.begin() + animationIndex);
animationIndex--;
}
}
}
return animationNames;
}
}
return animationNames;
}
} // namespace gd

View File

@@ -129,7 +129,17 @@ class GD_CORE_API ObjectsContainersList {
const gd::String& objectName, bool searchInGroups = true) const;
/**
* \brief Return a list containing all objects refered to by the group.
* \brief Get the animation names of an object/group.
* \note The animation names of a group are the animation names common to
* every object of the group.
*
* @return The names of animations
*/
std::vector<gd::String>
GetAnimationNamesOfObject(const gd::String &objectOrGroupName) const;
/**
* \brief Return a list containing all objects referred to by the group.
* If an object name is passed, then only this object name is returned.
*
* If \a onlyObjectToSelectIfPresent is set and present in the group(s),
@@ -170,6 +180,8 @@ class GD_CORE_API ObjectsContainersList {
private:
bool HasObjectNamed(const gd::String& name) const;
const gd::Object* GetObject(const gd::String& name) const;
bool HasObjectWithVariableNamed(const gd::String& objectName,
const gd::String& variableName) const;

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 == variable.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

@@ -1,136 +0,0 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering layout content helper methods.
*/
#include "GDCore/Project/Layout.h"
#include "DummyPlatform.h"
#include "GDCore/CommonTools.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/Project/Project.h"
#include "catch.hpp"
using namespace gd;
TEST_CASE("Layout", "[common]") {
SECTION("Find the type of a behavior in a object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyObject", "MyBehavior", true) == "MyExtension::MyBehavior");
}
SECTION("Give an empty type for an object that doesn't have the behavior") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyObject", "MyBehavior", true) == "");
}
SECTION("Find the type of a behavior in a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto &group =
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyGroup", "MyBehavior", true) == "MyExtension::MyBehavior");
}
SECTION(
"Give an empty type for a group with an object missing the behavior") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
// object2 doesn't have the behavior.
auto &group =
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyGroup", "MyBehavior", true) == "");
}
SECTION("Give an empty type for a group with behaviors of same name but "
"different types") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
"MyBehavior");
auto &group =
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyGroup", "MyBehavior", true) == "");
}
SECTION("Give an empty type for an empty group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
auto &group =
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
project.GetObjects(), layout.GetObjects(),
"MyGroup", "MyBehavior", true) == "");
}
}

View File

@@ -0,0 +1,488 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering layout content helper methods.
*/
#include "GDCore/Project/ObjectsContainersList.h"
#include "DummyPlatform.h"
#include "GDCore/CommonTools.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Tools/MakeUnique.h"
#include "catch.hpp"
#include <algorithm>
#include <vector>
using namespace gd;
TEST_CASE("ObjectContainersList (HasObjectOrGroupNamed)", "[common]") {
SECTION("Can check an object exists") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasObjectOrGroupNamed("MyObject"));
REQUIRE(!objectsContainersList.HasObjectOrGroupNamed("MyWrongObject"));
}
SECTION("Can check a group exists") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasObjectOrGroupNamed("MyGroup"));
}
}
TEST_CASE("ObjectContainersList (HasObjectOrGroupWithVariableNamed)", "[common]") {
SECTION("Can check a variable exists in an object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.GetVariables().InsertNew("MyVariable", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasObjectOrGroupWithVariableNamed("MyObject", "MyVariable"));
REQUIRE(!objectsContainersList.HasObjectOrGroupWithVariableNamed("MyObject", "MyWrongVariable"));
}
SECTION("Can check a variable exists in a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.GetVariables().InsertNew("MyVariable", 0);
// This variable is only in one of the 2 objects.
object1.GetVariables().InsertNew("MyOtherVariable", 0);
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object1.GetVariables().InsertNew("MyVariable", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasObjectOrGroupWithVariableNamed("MyGroup", "MyVariable"));
REQUIRE(!objectsContainersList.HasObjectOrGroupWithVariableNamed("MyGroup", "MyWrongVariable"));
}
}
TEST_CASE("ObjectContainersList (GetTypeOfObject)", "[common]") {
SECTION("Find the type of an object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("MyObject") == "MyExtension::Sprite");
}
SECTION("Find the object type of a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("MyGroup") == "MyExtension::Sprite");
}
SECTION("Give an empty type for groups with mixed object types") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "FakeObjectWithDefaultBehavior", "MyObject2", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("MyGroup") == "");
}
SECTION("Give an empty type for an empty group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject(
"MyGroup") == "");
}
}
TEST_CASE("ObjectContainersList (GetTypeOfBehaviorInObjectOrGroup)",
"[common]") {
SECTION("Find the type of a behavior in an object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyObject", "MyBehavior", true) == "MyExtension::MyBehavior");
}
SECTION("Give an empty type for an object that doesn't have the behavior") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyObject", "MyBehavior", true) == "");
}
SECTION("Find the type of a behavior in a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyGroup", "MyBehavior", true) == "MyExtension::MyBehavior");
}
SECTION(
"Give an empty type for a group with an object missing the behavior") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
// object2 doesn't have the behavior.
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyGroup", "MyBehavior", true) == "");
}
SECTION("Give an empty type for a group with behaviors of same name but different types") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
"MyBehavior");
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyGroup", "MyBehavior", true) == "");
}
SECTION("Give an empty type for an empty group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"MyGroup", "MyBehavior", true) == "");
}
}
TEST_CASE("ObjectContainersList (HasBehaviorInObjectOrGroup)", "[common]") {
SECTION("Can check a behavior exists in an object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasBehaviorInObjectOrGroup("MyObject", "MyBehavior"));
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("MyObject", "MyWrongBehavior"));
}
SECTION("Can check a behavior exists in a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
// This behavior is only in one of the 2 objects.
object1.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
"MyOtherBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.HasBehaviorInObjectOrGroup("MyGroup", "MyBehavior"));
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("MyGroup", "MyOtherBehavior"));
}
}
TEST_CASE("ObjectContainersList (GetBehaviorsOfObject)", "[common]") {
SECTION("Find the behaviors in an object") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject", 0);
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
const auto behaviors =
objectsContainersList.GetBehaviorsOfObject("MyObject", true);
REQUIRE(behaviors.size() == 1);
REQUIRE(behaviors[0] == "MyBehavior");
}
SECTION("Find the behaviors in a group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object1 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject1", 0);
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
// This behavior is only in one of the 2 objects.
object1.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
"MyOtherBehavior");
gd::Object &object2 = layout.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "MyObject2", 0);
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
const auto behaviors =
objectsContainersList.GetBehaviorsOfObject("MyGroup", true);
REQUIRE(behaviors.size() == 1);
REQUIRE(behaviors[0] == "MyBehavior");
}
}
namespace {
gd::SpriteObject BuildSpriteWithAnimations(gd::String animationName1 = "!",
gd::String animationName2 = "!",
gd::String animationName3 = "!") {
gd::SpriteObject configuration;
gd::SpriteAnimationList &animations = configuration.GetAnimations();
if (animationName1 != "!") {
gd::Animation animation;
animation.SetName(animationName1);
animations.AddAnimation(animation);
if (animationName2 != "!") {
gd::Animation animation;
animation.SetName(animationName2);
animations.AddAnimation(animation);
}
if (animationName3 != "!") {
gd::Animation animation;
animation.SetName(animationName3);
animations.AddAnimation(animation);
}
}
return configuration;
}
bool Contains(const std::vector<gd::String> &vector, const gd::String &value) {
return std::find(vector.begin(), vector.end(), value) !=
vector.end();
}
} // namespace
TEST_CASE("ObjectContainersList (GetAnimationNamesOfObject)", "[common]") {
SECTION("Find the animation names in a sprite") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object object("MyObject", "Sprite",
gd::make_unique<gd::SpriteObject>(
BuildSpriteWithAnimations("Idle", "Run")));
layout.GetObjects().InsertObject(object, 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
const auto animationNames =
objectsContainersList.GetAnimationNamesOfObject("MyObject");
REQUIRE(Contains(animationNames, "Idle"));
REQUIRE(Contains(animationNames, "Run"));
REQUIRE(animationNames.size() == 2);
}
SECTION("Find the animation names in a group of sprite") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object object1("MyObject1", "Sprite",
gd::make_unique<gd::SpriteObject>(
BuildSpriteWithAnimations("Idle", "Jump", "Run")));
layout.GetObjects().InsertObject(object1, 0);
gd::Object object2("MyObject2", "Sprite",
gd::make_unique<gd::SpriteObject>(
BuildSpriteWithAnimations("Run", "Idle", "Climb")));
layout.GetObjects().InsertObject(object2, 0);
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
group.AddObject(object1.GetName());
group.AddObject(object2.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
const auto animationNames =
objectsContainersList.GetAnimationNamesOfObject("MyGroup");
REQUIRE(Contains(animationNames, "Idle"));
REQUIRE(Contains(animationNames, "Run"));
REQUIRE(animationNames.size() == 2);
}
}

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

@@ -235,6 +235,19 @@ void Model3DObjectConfiguration::ExposeResources(
worker.ExposeModel3D(modelResourceName);
}
const gd::String &
Model3DObjectConfiguration::GetAnimationName(size_t index) const {
return GetAnimation(index).GetName();
}
bool Model3DObjectConfiguration::HasAnimationNamed(
const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const Model3DAnimation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
Model3DAnimation Model3DObjectConfiguration::badAnimation;
const Model3DAnimation &
@@ -252,14 +265,6 @@ Model3DAnimation &Model3DObjectConfiguration::GetAnimation(std::size_t nb) {
return animations[nb];
}
bool Model3DObjectConfiguration::HasAnimationNamed(
const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const Model3DAnimation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
void Model3DObjectConfiguration::AddAnimation(
const Model3DAnimation &animation) {
animations.push_back(animation);

View File

@@ -85,6 +85,12 @@ public:
* Methods related to animations management
*/
///@{
std::size_t GetAnimationsCount() const override { return animations.size(); };
const gd::String &GetAnimationName(size_t index) const override;
bool HasAnimationNamed(const gd::String &animationName) const override;
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
@@ -97,16 +103,6 @@ public:
*/
Model3DAnimation &GetAnimation(std::size_t nb);
/**
* \brief Return the number of animations this object has.
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
/**
* \brief Return true if the animation called "name" exists.
*/
bool HasAnimationNamed(const gd::String& name) const;
/**
* \brief Add an animation at the end of the existing ones.
*/

View File

@@ -118,6 +118,19 @@ namespace gdjs {
this._materialType = this._convertMaterialType(
objectData.content.materialType
);
this.onModelChanged(objectData);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
/**
* To be called after the renderer loaded a Model resource:
* - After the renderer was instantiated
* - After reloading the model
*/
private onModelChanged(objectData) {
this._updateModel(objectData);
if (this._animations.length > 0) {
this._renderer.playAnimation(
@@ -125,9 +138,6 @@ namespace gdjs {
this._animations[0].loop
);
}
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
updateFromObjectData(
@@ -135,18 +145,7 @@ namespace gdjs {
newObjectData: Model3DObjectData
): boolean {
super.updateFromObjectData(oldObjectData, newObjectData);
if (
oldObjectData.content.width !== newObjectData.content.width ||
oldObjectData.content.height !== newObjectData.content.height ||
oldObjectData.content.depth !== newObjectData.content.depth ||
oldObjectData.content.rotationX !== newObjectData.content.rotationX ||
oldObjectData.content.rotationY !== newObjectData.content.rotationY ||
oldObjectData.content.rotationZ !== newObjectData.content.rotationZ ||
oldObjectData.content.keepAspectRatio !==
newObjectData.content.keepAspectRatio
) {
this._updateModel(newObjectData);
}
if (
oldObjectData.content.materialType !==
newObjectData.content.materialType
@@ -154,6 +153,24 @@ namespace gdjs {
this._materialType = this._convertMaterialType(
newObjectData.content.materialType
);
}
if (
oldObjectData.content.modelResourceName !==
newObjectData.content.modelResourceName
) {
this._reloadModel(newObjectData);
} else if (
oldObjectData.content.width !== newObjectData.content.width ||
oldObjectData.content.height !== newObjectData.content.height ||
oldObjectData.content.depth !== newObjectData.content.depth ||
oldObjectData.content.rotationX !== newObjectData.content.rotationX ||
oldObjectData.content.rotationY !== newObjectData.content.rotationY ||
oldObjectData.content.rotationZ !== newObjectData.content.rotationZ ||
oldObjectData.content.keepAspectRatio !==
newObjectData.content.keepAspectRatio ||
oldObjectData.content.materialType !==
newObjectData.content.materialType
) {
this._updateModel(newObjectData);
}
if (
@@ -218,6 +235,12 @@ namespace gdjs {
}
}
_reloadModel(objectData: Model3DObjectData) {
this._modelResourceName = objectData.content.modelResourceName;
this._renderer._reloadModel(this, this._runtimeScene);
this.onModelChanged(objectData);
}
_updateModel(objectData: Model3DObjectData) {
const rotationX = objectData.content.rotationX || 0;
const rotationY = objectData.content.rotationY || 0;

View File

@@ -236,6 +236,20 @@ namespace gdjs {
}
}
/**
* `_updateModel` should always be called after this method.
* Ideally, use `Model3DRuntimeObject#_reloadModel` instead.
*/
_reloadModel(
runtimeObject: Model3DRuntimeObject,
instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._originalModel = instanceContainer
.getGame()
.getModel3DManager()
.getModel(runtimeObject._modelResourceName);
}
_updateModel(
rotationX: float,
rotationY: float,

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

@@ -7,7 +7,8 @@ namespace gdjs {
* Returned by _findHighestFloorAndMoveOnTop
*/
type PlatformSearchResult = {
highestGround: gdjs.PlatformRuntimeBehavior | null;
highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null;
highestGroundPolygon: gdjs.Polygon | null;
isCollidingAnyPlatform: boolean;
};
@@ -74,7 +75,8 @@ namespace gdjs {
* Returned by _findHighestFloorAndMoveOnTop
*/
private static readonly _platformSearchResult: PlatformSearchResult = {
highestGround: null,
highestGroundPlatform: null,
highestGroundPolygon: null,
isCollidingAnyPlatform: false,
};
@@ -612,7 +614,8 @@ namespace gdjs {
this._potentialCollidingObjects,
floorPlatformId,
/*excludeJumpthrus=*/
true
true,
this._onFloor.getFloorPolygon()
)
) {
if (
@@ -648,12 +651,12 @@ namespace gdjs {
// This is to be consistent on all floor collision.
// The object will land right on floor.
const { highestGround } = this._findHighestFloorAndMoveOnTop(
const { highestGroundPlatform } = this._findHighestFloorAndMoveOnTop(
this._potentialCollidingObjects,
0,
this._requestedDeltaY
);
if (!highestGround) {
if (!highestGroundPlatform) {
object.setY(object.getY() + this._requestedDeltaY);
}
} else {
@@ -706,10 +709,13 @@ namespace gdjs {
this._falling.enter(from);
}
_setOnFloor(collidingPlatform: gdjs.PlatformRuntimeBehavior) {
_setOnFloor(
collidingPlatform: gdjs.PlatformRuntimeBehavior,
floorPolygon: gdjs.Polygon
) {
this._state.leave();
this._state = this._onFloor;
this._onFloor.enter(collidingPlatform);
this._onFloor.enter(collidingPlatform, floorPolygon);
}
private _setJumping() {
@@ -803,22 +809,28 @@ namespace gdjs {
// The interval could be smaller.
// It's just for rounding errors.
const { highestGround } = this._findHighestFloorAndMoveOnTop(
const {
highestGroundPlatform,
highestGroundPolygon,
} = this._findHighestFloorAndMoveOnTop(
this._potentialCollidingObjects,
-1,
1
);
// don't fall if GrabbingPlatform or OnLadder
if (this._state === this._onFloor) {
if (!highestGround) {
if (!highestGroundPlatform || !highestGroundPolygon) {
this._setFalling();
} else if (highestGround === this._onFloor.getFloorPlatform()) {
} else if (
highestGroundPlatform === this._onFloor.getFloorPlatform() &&
highestGroundPolygon === this._onFloor.getFloorPolygon()
) {
this._onFloor.updateFloorPosition();
} else {
this._setOnFloor(highestGround);
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
}
} else if (highestGround && canLand) {
this._setOnFloor(highestGround);
} else if (highestGroundPlatform && highestGroundPolygon && canLand) {
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
} else {
// The object can't land.
object.setY(oldY);
@@ -930,13 +942,15 @@ namespace gdjs {
*/
_isCollidingWithOneOf(
candidates: gdjs.PlatformRuntimeBehavior[],
exceptThisOne?: number | null,
excludeJumpThrus?: boolean
ignoredPlatformId?: number | null,
excludeJumpThrus?: boolean,
ignoredPolygon?: gdjs.Polygon | null
) {
excludeJumpThrus = !!excludeJumpThrus;
for (let i = 0; i < candidates.length; ++i) {
const platform = candidates[i];
if (platform.owner.id === exceptThisOne) {
const isPlatformIgnored = platform.owner.id === ignoredPlatformId;
if (isPlatformIgnored && !ignoredPolygon) {
continue;
}
if (
@@ -952,9 +966,10 @@ namespace gdjs {
}
if (
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
this.owner,
this._ignoreTouchingEdges,
isPlatformIgnored ? ignoredPolygon : null
)
) {
return true;
@@ -982,7 +997,8 @@ namespace gdjs {
context.initializeBeforeSearch(this, upwardDeltaY, downwardDeltaY);
let totalHighestY = Number.MAX_VALUE;
let highestGround: gdjs.PlatformRuntimeBehavior | null = null;
let highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null = null;
let highestGroundPolygon: gdjs.Polygon | null = null;
let isCollidingAnyPlatform = false;
for (const platform of candidates) {
if (
@@ -1032,7 +1048,8 @@ namespace gdjs {
// and is too high for the character to walk on.
// This will still be an obstacle even if there
// are other platforms that fit the requirements.
highestGround = null;
highestGroundPlatform = null;
highestGroundPolygon = null;
break;
}
@@ -1041,16 +1058,18 @@ namespace gdjs {
highestRelativeY < totalHighestY
) {
totalHighestY = highestRelativeY;
highestGround = platform;
highestGroundPlatform = platform;
highestGroundPolygon = context.highestFloorPolygon;
}
}
if (highestGround) {
if (highestGroundPlatform) {
const object = this.owner;
object.setY(object.getY() + totalHighestY);
}
const returnValue =
gdjs.PlatformerObjectRuntimeBehavior._platformSearchResult;
returnValue.highestGround = highestGround;
returnValue.highestGroundPlatform = highestGroundPlatform;
returnValue.highestGroundPolygon = highestGroundPolygon;
returnValue.isCollidingAnyPlatform = isCollidingAnyPlatform;
return returnValue;
}
@@ -1115,7 +1134,7 @@ namespace gdjs {
(vertex[0] === context.ownerMaxX &&
(previousVertex[0] < vertex[0] || nextVertex[0] < vertex[0]))
) {
context.addPointConstraint(vertex[1]);
context.addPointConstraint(vertex[1], hitbox);
}
const deltaX = vertex[0] - previousVertex[0];
@@ -1133,7 +1152,7 @@ namespace gdjs {
previousVertex[1] +
((context.ownerMinX - previousVertex[0]) * deltaY) / deltaX;
context.addPointConstraint(intersectionY);
context.addPointConstraint(intersectionY, hitbox);
}
// Check intersection on the right side of owner
if (
@@ -1147,7 +1166,7 @@ namespace gdjs {
previousVertex[1] +
((context.ownerMaxX - previousVertex[0]) * deltaY) / deltaX;
context.addPointConstraint(intersectionY);
context.addPointConstraint(intersectionY, hitbox);
}
}
if (context.floorIsTooHigh()) {
@@ -1867,6 +1886,7 @@ namespace gdjs {
class OnFloor implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
private _floorPlatform: gdjs.PlatformRuntimeBehavior | null = null;
private _floorPolygon: gdjs.Polygon | null = null;
private _floorLastX: float = 0;
private _floorLastY: float = 0;
_oldHeight: float = 0;
@@ -1879,8 +1899,16 @@ namespace gdjs {
return this._floorPlatform;
}
enter(floorPlatform: gdjs.PlatformRuntimeBehavior) {
getFloorPolygon() {
return this._floorPolygon;
}
enter(
floorPlatform: gdjs.PlatformRuntimeBehavior,
floorPolygon: gdjs.Polygon
) {
this._floorPlatform = floorPlatform;
this._floorPolygon = floorPolygon;
this.updateFloorPosition();
this._behavior._canJump = true;
this._behavior._currentFallSpeed = 0;
@@ -1888,6 +1916,7 @@ namespace gdjs {
leave() {
this._floorPlatform = null;
this._floorPolygon = null;
}
updateFloorPosition() {
@@ -2000,17 +2029,23 @@ namespace gdjs {
behavior._requestedDeltaX * behavior._slopeClimbingFactor
);
const {
highestGround,
highestGroundPlatform,
highestGroundPolygon,
isCollidingAnyPlatform,
} = behavior._findHighestFloorAndMoveOnTop(
behavior._potentialCollidingObjects,
-deltaMaxY,
deltaMaxY
);
if (highestGround && highestGround !== this._floorPlatform) {
behavior._setOnFloor(highestGround);
if (
highestGroundPlatform &&
highestGroundPolygon &&
(highestGroundPlatform !== this._floorPlatform ||
highestGroundPolygon !== this._floorPolygon)
) {
behavior._setOnFloor(highestGroundPlatform, highestGroundPolygon);
}
if (highestGround === null && isCollidingAnyPlatform) {
if (highestGroundPlatform === null && isCollidingAnyPlatform) {
// Unable to follow the floor (too steep): go back to the original position.
behavior.owner.setX(oldX);
}
@@ -2020,7 +2055,7 @@ namespace gdjs {
// Try to follow the platform until the obstacle.
const {
highestGround: highestGroundOnPlatform,
highestGroundPlatform: highestGroundOnPlatform,
isCollidingAnyPlatform,
} = behavior._findHighestFloorAndMoveOnTop(
behavior._potentialCollidingObjects,
@@ -2055,7 +2090,7 @@ namespace gdjs {
// 1. Try to move 1 pixel on the X axis to climb the junction.
object.setX(object.getX() + Math.sign(requestedDeltaX));
const {
highestGround: highestGroundAtJunction,
highestGroundPlatform: highestGroundAtJunction,
} = behavior._findHighestFloorAndMoveOnTop(
behavior._potentialCollidingObjects,
// Look up from at least 1 pixel to bypass not perfectly aligned floors.
@@ -2075,23 +2110,27 @@ namespace gdjs {
);
object.setX(object.getX() + deltaX);
const {
highestGround: highestGroundOnObstacle,
highestGroundPlatform: highestGroundOnObstacle,
highestGroundPolygon,
} = behavior._findHighestFloorAndMoveOnTop(
behavior._potentialCollidingObjects,
// Do an exact slope angle check.
-Math.abs(deltaX) * behavior._slopeClimbingFactor,
0
);
if (highestGroundOnObstacle) {
if (highestGroundOnObstacle && highestGroundPolygon) {
// The obstacle slope can be climbed.
if (Math.abs(remainingDeltaX) >= 2) {
behavior._setOnFloor(highestGroundOnObstacle);
behavior._setOnFloor(
highestGroundOnObstacle,
highestGroundPolygon
);
} else {
// We went too far in order to check that.
// Now, find the right position on the obstacles.
object.setPosition(oldX + requestedDeltaX, beforeObstacleY);
const {
highestGround: highestGroundOnObstacle,
highestGroundPlatform: highestGroundOnObstacle,
} = behavior._findHighestFloorAndMoveOnTop(
behavior._potentialCollidingObjects,
// requestedDeltaX can be small when the object start moving.
@@ -2103,8 +2142,11 @@ namespace gdjs {
0
);
// Should always be true
if (highestGroundOnObstacle) {
behavior._setOnFloor(highestGroundOnObstacle);
if (highestGroundOnObstacle && highestGroundPolygon) {
behavior._setOnFloor(
highestGroundOnObstacle,
highestGroundPolygon
);
}
}
} else {
@@ -2578,6 +2620,8 @@ namespace gdjs {
*/
foundUnderBottom: boolean = false;
highestFloorPolygon: gdjs.Polygon | null = null;
initializeBeforeSearch(
behavior: PlatformerObjectRuntimeBehavior,
upwardDeltaY: float,
@@ -2653,7 +2697,7 @@ namespace gdjs {
* and update the context with this new constraint.
* @param y
*/
addPointConstraint(y: float): void {
addPointConstraint(y: float, sourcePolygon: gdjs.Polygon): void {
if (y < this.floorMinY) {
// The platform is too high to walk on...
if (y > this.headMaxY) {
@@ -2693,6 +2737,7 @@ namespace gdjs {
this.allowedMaxDeltaY,
y - this.ownerMaxY
);
this.highestFloorPolygon = sourcePolygon;
}
}
}

View File

@@ -1644,8 +1644,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
behavior = object.getBehavior('auto1');
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
// The object is in the corner of the platform.
object.setPosition(80 - 10, 80 - 20);
});
[
@@ -1672,6 +1670,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
it(`can jump while moving against a wall ${wallBeing}`, function () {
createPlatforms(runtimeScene);
// The object is in the corner of the platform.
object.setPosition(80 - 10, 80 - 20);
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
@@ -1696,6 +1697,32 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getY()).to.be.lessThan(oldY);
}
});
it(`can move against a wall ${wallBeing}`, function () {
createPlatforms(runtimeScene);
// The object is 20 pixels from the wall.
object.setPosition(80 - 10 - 10, 80 - 20);
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(60 - epsilon, 60 + epsilon);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
// It's important the character moves from several pixels per frame.
behavior.setCurrentSpeed(behavior.getMaxSpeed());
for (let i = 0; i < 10; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
// The object is against the wall.
expect(object.getX()).to.be(80 - 10);
});
});
});

View File

@@ -86,6 +86,12 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
return platform;
};
/**
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
* @param {float} upwardDeltaY
* @param {float} downwardDeltaY
*/
const checkMoveOn = (
characterBehavior,
platformBehavior,
@@ -97,9 +103,15 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(platformBehavior);
expect(result.highestGroundPlatform).to.be(platformBehavior);
};
/**
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
* @param {float} upwardDeltaY
* @param {float} downwardDeltaY
*/
const checkNoFloor = (
characterBehavior,
platformBehavior,
@@ -112,11 +124,17 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.highestGroundPlatform).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(false);
expect(characterBehavior.owner.getY()).to.be(oldY);
};
/**
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
* @param {float} upwardDeltaY
* @param {float} downwardDeltaY
*/
const checkObstacle = (
characterBehavior,
platformBehavior,
@@ -129,7 +147,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.highestGroundPlatform).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(true);
expect(characterBehavior.owner.getY()).to.be(oldY);
};
@@ -293,11 +311,13 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
/** @type gdjs.PlatformerObjectRuntimeBehavior */
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
/** @type gdjs.PlatformRuntimeBehavior */
const platformBehavior = platform.getBehavior('Platform');
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
runtimeScene
@@ -365,11 +385,13 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
/** @type gdjs.PlatformerObjectRuntimeBehavior */
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
/** @type gdjs.PlatformRuntimeBehavior */
const platformBehavior = platform.getBehavior('Platform');
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
runtimeScene

View File

@@ -116,6 +116,18 @@ void SpineObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker &work
worker.ExposeEmbeddeds(spineResourceName);
}
const gd::String &
SpineObjectConfiguration::GetAnimationName(size_t index) const {
return GetAnimation(index).GetName();
}
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const SpineAnimation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
const SpineAnimation &
SpineObjectConfiguration::GetAnimation(std::size_t nb) const {
if (nb >= animations.size()) return badAnimation;
@@ -129,13 +141,6 @@ SpineAnimation &SpineObjectConfiguration::GetAnimation(std::size_t nb) {
return animations[nb];
}
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const SpineAnimation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
void SpineObjectConfiguration::AddAnimation(const SpineAnimation &animation) {
animations.push_back(animation);
}

View File

@@ -93,15 +93,11 @@ public:
*/
SpineAnimation &GetAnimation(std::size_t nb);
/**
* \brief Return the number of animations this object has.
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
std::size_t GetAnimationsCount() const override { return animations.size(); };
/**
* \brief Return true if the animation called "name" exists.
*/
bool HasAnimationNamed(const gd::String& name) const;
const gd::String &GetAnimationName(size_t index) const override;
bool HasAnimationNamed(const gd::String &animationName) const override;
/**
* \brief Add an animation at the end of the existing ones.

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

@@ -176,7 +176,7 @@ const defineTileMap = function (extension, _, gd) {
const object = extension
.addObject(
'TileMap',
_('Tilemap'),
_('External Tilemap (Tiled/LDtk)'),
_(
'Displays a tiled-based map, made with the Tiled editor (https://www.mapeditor.org/) or the LDtk editor (https://ldtk.io/).'
),
@@ -591,6 +591,471 @@ const defineTileMap = function (extension, _, gd) {
.setFunctionName('setHeight');
};
/**
* @param {gd.PlatformExtension} extension
* @param {(translationSource: string) => string} _
* @param {GDNamespace} gd
*/
const defineSimpleTileMap = function (extension, _, gd) {
var objectSimpleTileMap = new gd.ObjectJsImplementation();
objectSimpleTileMap.updateProperty = function (
objectContent,
propertyName,
newValue
) {
if (propertyName === 'atlasImage') {
objectContent.atlasImage = newValue;
return true;
}
if (propertyName === 'columnCount') {
objectContent.columnCount = parseFloat(newValue);
return true;
}
if (propertyName === 'rowCount') {
objectContent.rowCount = parseFloat(newValue);
return true;
}
if (propertyName === 'tileSize') {
objectContent.tileSize = parseFloat(newValue);
return true;
}
if (propertyName === 'tilesWithHitBox') {
objectContent.tilesWithHitBox = newValue;
return true;
}
return false;
};
objectSimpleTileMap.getProperties = function (objectContent) {
var objectProperties = new gd.MapStringPropertyDescriptor();
objectProperties.set(
'columnCount',
new gd.PropertyDescriptor((objectContent.columnCount || 4).toString())
.setType('number')
.setLabel(_('Columns'))
.setDescription(_('Number of columns.'))
.setHidden(true)
);
objectProperties.set(
'rowCount',
new gd.PropertyDescriptor((objectContent.rowCount || 4).toString())
.setType('number')
.setLabel(_('Rows'))
.setDescription(_('Number of rows.'))
.setHidden(true)
);
objectProperties.set(
'tileSize',
new gd.PropertyDescriptor((objectContent.tileSize || 8).toString())
.setType('number')
.setLabel(_('Tile size'))
.setDescription(_('Tile size in pixels.'))
);
objectProperties.set(
'tilesWithHitBox',
new gd.PropertyDescriptor(objectContent.tilesWithHitBox || '')
.setType('string')
.setLabel(_('Tile ids with hit box'))
.setDescription(
_('The list of tile ids with a hit box (separated by commas).')
)
.setHidden(true)
);
objectProperties.set(
'atlasImage',
new gd.PropertyDescriptor(objectContent.atlasImage)
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Atlas image'))
.setDescription(_('The Atlas image containing the tileset.'))
);
return objectProperties;
};
objectSimpleTileMap.setRawJSONContent(
JSON.stringify({
atlasImage: '',
rowCount: 1,
columnCount: 1,
tileSize: 8,
tilesWithHitBox: '',
})
);
objectSimpleTileMap.updateInitialInstanceProperty = function (
instance,
propertyName,
newValue
) {
if (propertyName === 'tilemap') {
instance.setRawStringProperty('tilemap', newValue);
return true;
}
return false;
};
objectSimpleTileMap.getInitialInstanceProperties = function (
objectContent,
instance
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
instanceProperties
.getOrCreate('tilemap')
.setValue(instance.getRawStringProperty('tileMap'))
.setType('string')
.setLabel('Tilemap')
.setHidden(true);
return instanceProperties;
};
const object = extension
.addObject(
'SimpleTileMap',
_('Tile map'),
_('Displays a tiled-based map.'),
'JsPlatform/Extensions/tile_map.svg',
objectSimpleTileMap
)
.setCategoryFullName(_('General'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
.setIncludeFile('Extensions/TileMap/simpletilemapruntimeobject.js')
.addIncludeFile('Extensions/TileMap/TileMapRuntimeManager.js')
.addIncludeFile('Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js')
.addIncludeFile('Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js')
.addIncludeFile('Extensions/TileMap/collision/TransformedTileMap.js')
.addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
object
.addExpression(
'TilesetColumnCount',
_('Tileset column count'),
_('Get the number of column in the tileset.'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.setFunctionName('getTilesetColumnCount');
object
.addExpression(
'TilesetRowCount',
_('Tileset row count'),
_('Get the number of row in the tileset.'),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
.setFunctionName('getTilesetRowCount');
object
.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'),
_(
'Get the grid column coordinates in the tile map corresponding to the scene coordinates.'
),
'',
'JsPlatform/Extensions/tile_map.svg'
)
.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('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(
'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', _('Position X'), '', false)
.addParameter('number', _('Position Y'), '', false)
.getCodeExtraInformation()
.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 _PARAM3_ ; _PARAM4_'),
'',
'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');
};
/**
* @param {gd.PlatformExtension} extension
* @param {(translationSource: string) => string} _
@@ -769,7 +1234,7 @@ const defineCollisionMask = function (extension, _, gd) {
const object = extension
.addObject(
'CollisionMask',
_('Tilemap collision mask'),
_('External Tilemap (Tiled/LDtk) collision mask'),
_('Invisible object handling collisions with parts of a tilemap.'),
'JsPlatform/Extensions/tile_map_collision_mask32.svg',
collisionMaskObject
@@ -1020,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..."
),
@@ -1035,6 +1500,7 @@ module.exports = {
.setIcon('JsPlatform/Extensions/tile_map.svg');
defineTileMap(extension, _, gd);
defineSimpleTileMap(extension, _, gd);
defineCollisionMask(extension, _, gd);
return extension;
@@ -1411,7 +1877,367 @@ module.exports = {
);
/**
* Renderer for instances of TileMap inside the IDE.
* Renderer for instances of SimpleTileMap inside the IDE.
*/
class RenderedSimpleTileMapInstance extends RenderedInstance {
_placeholderTextPixiObject = new PIXI.Text(
'Select this instance\nto start painting',
new PIXI.TextStyle({
fontFamily: 'Arial',
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,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
) {
super(
project,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
);
// This setting allows tile maps with more than 16K tiles.
Tilemap.settings.use32bitIndex = true;
this.tileMapPixiObject = new Tilemap.CompositeTilemap();
this._pixiObject = new PIXI.Container();
this._pixiObject.addChild(this.tileMapPixiObject);
this._editableTileMap = null;
// Implement `containsPoint` so that we can set `interactive` to true and
// the Tilemap will properly emit events when hovered/clicked.
// By default, this is not implemented in pixi-tilemap.
this._pixiObject.containsPoint = (position) => {
// Turns the world position to the local object coordinates
const localPosition = new PIXI.Point();
if (this.tileMapPixiObject.visible) {
this.tileMapPixiObject.worldTransform.applyInverse(
position,
localPosition
);
} else {
this._placeholderImagePixiObject.worldTransform.applyInverse(
position,
localPosition
);
}
return (
localPosition.x >= 0 &&
localPosition.x < this.width &&
localPosition.y >= 0 &&
localPosition.y < this.height
);
};
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;
this.height = 48;
this._objectName = instance.getObjectName();
this.update();
this.updateTileMap();
}
onRemovedFromScene() {
super.onRemovedFromScene();
// Keep textures because they are shared by all tile maps.
this._pixiObject.destroy(false);
}
onLoadingError() {
this.errorPixiObject =
this.errorPixiObject ||
new PIXI.Sprite(this._pixiResourcesLoader.getInvalidPIXITexture());
this._pixiContainer.addChild(this.errorPixiObject);
this._pixiObject = this.errorPixiObject;
}
onLoadingSuccess() {
if (this.errorPixiObject) {
this._pixiContainer.removeChild(this.errorPixiObject);
this.errorPixiObject = null;
this._pixiObject = this.tileMapPixiObject;
}
}
/**
* Return the path to the thumbnail of the specified object.
*/
static getThumbnail(project, resourcesLoader, objectConfiguration) {
const atlasImageResourceName = objectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
return resourcesLoader.getResourceFullUrl(
project,
atlasImageResourceName,
{}
);
}
getEditableTileMap() {
return this._editableTileMap;
}
/**
* This is used to reload the Tilemap
*/
updateTileMap() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const tilemapAsJSObject = JSON.parse(
this._instance.getRawStringProperty('tilemap') || '{}'
);
const tileSize = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('tileSize')
.getValue(),
10
);
const columnCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('columnCount')
.getValue(),
10
);
const rowCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('rowCount')
.getValue(),
10
);
const atlasTexture = this._pixiResourcesLoader.getPIXITexture(
this._project,
atlasImageResourceName
);
const loadTileMap = () => {
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(
this._project
);
manager.getOrLoadSimpleTileMap(
tilemapAsJSObject,
this._objectName,
tileSize,
columnCount,
rowCount,
(tileMap) => {
if (!tileMap) {
this.onLoadingError();
console.error('Could not parse tilemap.');
return;
}
this._editableTileMap = tileMap;
manager.getOrLoadSimpleTileMapTextureCache(
(textureName) =>
this._pixiResourcesLoader.getPIXITexture(
this._project,
textureName
),
atlasImageResourceName,
tileSize,
columnCount,
rowCount,
(
/** @type {TileMapHelper.TileTextureCache | null} */
textureCache
) => {
this.onLoadingSuccess();
if (!this._editableTileMap) return;
this.width = this._editableTileMap.getWidth();
this.height = this._editableTileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
this.tileMapPixiObject,
this._editableTileMap,
textureCache,
'all', // No notion of visibility on simple tile maps.
0 // Only one layer is used on simple tile maps.
);
}
);
}
);
};
if (atlasTexture.valid) {
loadTileMap();
} else {
// Wait for the atlas image to load.
atlasTexture.once('update', () => {
loadTileMap();
});
}
}
updatePixiTileMap() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const tileSize = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('tileSize')
.getValue(),
10
);
const columnCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('columnCount')
.getValue(),
10
);
const rowCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('rowCount')
.getValue(),
10
);
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(this._project);
manager.getOrLoadSimpleTileMapTextureCache(
(textureName) =>
this._pixiResourcesLoader.getPIXITexture(
this._project,
textureName
),
atlasImageResourceName,
tileSize,
columnCount,
rowCount,
(
/** @type {TileMapHelper.TileTextureCache | null} */
textureCache
) => {
this.onLoadingSuccess();
if (!this._editableTileMap) return;
this.width = this._editableTileMap.getWidth();
this.height = this._editableTileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
this.tileMapPixiObject,
this._editableTileMap,
textureCache,
'all',
0
);
}
);
}
/**
* This is called to update the PIXI object on the scene editor
*/
update() {
const isTileMapEmpty = this._editableTileMap
? this._editableTileMap.isEmpty()
: false;
let objectToChange;
if (isTileMapEmpty) {
this._placeholderPixiObject.visible = true;
this.tileMapPixiObject.visible = false;
objectToChange = this._placeholderPixiObject;
} else {
this._placeholderPixiObject.visible = false;
this.tileMapPixiObject.visible = true;
objectToChange = this.tileMapPixiObject;
}
if (!isTileMapEmpty) {
// Don't change size of placeholder object.
if (this._instance.hasCustomSize()) {
objectToChange.scale.x = this.getCustomWidth() / this.width;
objectToChange.scale.y = this.getCustomHeight() / this.height;
} else {
objectToChange.scale.x = 1;
objectToChange.scale.y = 1;
}
// Place the center of rotation in the center of the object. Because pivot position in Pixi
// is in the **local coordinates of the object**, we need to find back the original width
// and height of the object before scaling (then divide by 2 to find the center)
const originalWidth = this.width;
const originalHeight = this.height;
objectToChange.pivot.x = originalWidth / 2;
objectToChange.pivot.y = originalHeight / 2;
}
// Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
// of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
// object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
objectToChange.x =
this._instance.getX() +
objectToChange.pivot.x * objectToChange.scale.x;
objectToChange.y =
this._instance.getY() +
objectToChange.pivot.y * objectToChange.scale.y;
// Rotation works as intended because we put the pivot in the center
objectToChange.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
}
/**
* Return the width of the instance, when it's not resized.
*/
getDefaultWidth() {
return this.width;
}
/**
* Return the height of the instance, when it's not resized.
*/
getDefaultHeight() {
return this.height;
}
}
objectsRenderingService.registerInstanceRenderer(
'TileMap::SimpleTileMap',
RenderedSimpleTileMapInstance
);
/**
* Renderer for instances of TileMap collision mask inside the IDE.
*/
class RenderedCollisionMaskInstance extends RenderedInstance {
constructor(

View File

@@ -79,6 +79,24 @@ namespace gdjs {
);
}
getOrLoadSimpleTileMap(
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
columnCount: number,
rowCount: number,
callback: (tileMap: TileMapHelper.EditableTileMap) => void
): void {
this._manager.getOrLoadSimpleTileMap(
tileMapAsJsObject,
objectName,
tileSize,
columnCount,
rowCount,
callback
);
}
/**
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
@@ -106,6 +124,32 @@ namespace gdjs {
);
}
/**
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileSize
* @param columnCount
* @param rowCount
* @param callback A function called when the tiles textures are split.
*/
getOrLoadSimpleTileMapTextureCache(
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileSize: number,
columnCount: number,
rowCount: number,
callback: (textureCache: TileMapHelper.TileTextureCache | null) => void
): void {
this._manager.getOrLoadSimpleTileMapTextureCache(
getTexture,
atlasImageResourceName,
tileSize,
columnCount,
rowCount,
callback
);
}
/**
* Parse both JSON and set the content of the tile set in the right
* attribute in the tile map to merge both parsed data.

View File

@@ -30,7 +30,7 @@ namespace gdjs {
*/
_transformationUpToDateCount: integer = 1;
/**
* An reusable Point to avoid allocations.
* A reusable Point to avoid allocations.
*/
private static readonly workingPoint: FloatPoint = [0, 0];
@@ -41,14 +41,28 @@ namespace gdjs {
this._source = source;
this.tag = tag;
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
for (const sourceLayer of source.getLayers()) {
this._buildLayersFromTileMap(source, this._layers);
}
updateFromTileMap(tileMap: TileMapHelper.EditableTileMap) {
this._source = tileMap;
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
this._buildLayersFromTileMap(tileMap, this._layers);
}
_buildLayersFromTileMap(
tileMap: TileMapHelper.EditableTileMap,
layers: Map<integer, TransformedCollisionTileMapLayer>
) {
for (const sourceLayer of tileMap.getLayers()) {
// TODO A visitor could be used to avoid a cast.
if (!(sourceLayer instanceof TileMapHelper.EditableTileMapLayer)) {
// TODO Collision mask for object layers is not handled.
continue;
}
const tileLayer = sourceLayer as TileMapHelper.EditableTileMapLayer;
this._layers.set(
layers.set(
tileLayer.id,
new TransformedCollisionTileMapLayer(this, tileLayer)
);
@@ -64,7 +78,7 @@ namespace gdjs {
}
/**
* @param transformation the transformation from the time map coordinate
* @param transformation the transformation from the tile map coordinate
* (in pixels) to the scene coordinate (in pixels).
*/
setTransformation(transformation: gdjs.AffineTransformation) {

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.
*
@@ -22,11 +28,15 @@ export declare class EditableTileMap {
/**
* The number of tile columns in the map.
*/
private readonly dimX;
private dimX;
/**
* The number of tile rows in the map.
*/
private readonly dimY;
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.
@@ -41,6 +51,27 @@ export declare class EditableTileMap {
dimY: integer,
tileSet: Map<integer, TileDefinition>
);
/**
* Loads EditableTileMap from serialized data.
* Uses object configuration as the source of truth as the serialized data
* might contain expired data (if the tile set configuration has changed and
* the serialized data was not updated).
* @param editableTileMapAsJsObject Serialized editable tile map object
* @param objectConfiguration
*/
static from(
editableTileMapAsJsObject: EditableTileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
tileSetRowCount,
}: {
tileSize: number;
tileSetColumnCount: number;
tileSetRowCount: number;
}
): EditableTileMap;
toJSObject(): Object;
/**
* @returns The tile map width in pixels.
*/
@@ -65,6 +96,28 @@ export declare class EditableTileMap {
* @returns The number of tile rows in the map.
*/
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;
/**
* @param tileId The tile identifier
* @returns The tile definition form the tile set.
@@ -78,7 +131,12 @@ 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.
*/
addTileLayer(layer: EditableTileMapLayer): void;
getTileLayer(id: integer): EditableTileMapLayer | null;
/**
* @param id The identifier of the new layer.
* @returns The new layer.
@@ -108,6 +166,27 @@ export declare class EditableTileMap {
* @param resourceName The name of the resource
*/
setBackgroundResourceName(resourceName: string): void;
/**
* 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.
@@ -128,10 +207,12 @@ declare abstract class AbstractEditableLayer {
*/
constructor(tileMap: EditableTileMap, id: integer);
setVisible(visible: boolean): void;
toJSObject(): Object;
/**
* @returns true if the layer is visible.
*/
isVisible(): boolean;
isEmpty(): boolean;
}
/**
* A layer where tiles are placed with pixel coordinates.
@@ -144,6 +225,7 @@ export declare class EditableObjectLayer extends AbstractEditableLayer {
*/
constructor(tileMap: EditableTileMap, id: integer);
add(object: TileObject): void;
isEmpty(): boolean;
}
/**
* A tile that is placed with pixel coordinates.
@@ -191,13 +273,20 @@ export declare class TileObject {
* A tile map layer with tile organized in grid.
*/
export declare class EditableTileMapLayer extends AbstractEditableLayer {
private readonly _tiles;
private _tiles;
private _alpha;
/**
* @param tileMap The layer tile map.
* @param id The layer identifier.
*/
constructor(tileMap: EditableTileMap, id: integer);
buildEmptyLayer(dimensionX: number, dimensionY: number): void;
static from(
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer;
toJSObject(): Object;
/**
* The opacity (between 0-1) of the layer
*/
@@ -206,12 +295,37 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
* @param alpha The opacity between 0-1
*/
setAlpha(alpha: float): void;
isEmpty(): boolean;
reduceDimensions(
columnsToPop: number,
columnsToShift: number,
rowsToPop: number,
rowsToShift: number
): void;
increaseDimensions(
columnsToAppend: number,
columnsToUnshift: number,
rowsToAppend: number,
rowsToUnshift: number
): void;
/**
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile.
*/
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;
getTrimmingData(): {
rowsToShift: number;
columnsToShift: number;
rowsToPop: number;
columnsToPop: number;
};
/**
* @param x The layer column.
* @param y The layer row.

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.
*
@@ -43,6 +44,14 @@ export declare class TileMapManager {
pako: any,
callback: (tileMap: EditableTileMap | null) => void
): void;
getOrLoadSimpleTileMap(
tileMapAsJsObject: EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
tileSetColumnCount: number,
tileSetRowCount: number,
callback: (tileMap: EditableTileMap) => void
): void;
/**
* @param loadTileMap The method that loads the Tiled JSON file in memory.
* @param getTexture The method that loads the atlas image file in memory.
@@ -65,6 +74,22 @@ export declare class TileMapManager {
levelIndex: number,
callback: (textureCache: TileTextureCache | null) => void
): void;
/**
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileSize
* @param columnCount
* @param rowCount
* @param callback A function called when the tiles textures are split.
*/
getOrLoadSimpleTileMapTextureCache(
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileSize: number,
columnCount: number,
rowCount: number,
callback: (textureCache: TileTextureCache | null) => void
): void;
clearCaches(): void;
}
//# sourceMappingURL=TileMapManager.d.ts.map

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;;;;;;;;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,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

@@ -6,7 +6,7 @@ export declare namespace PixiTileMapHelper {
/**
* Split an atlas image into Pixi textures.
*
* @param tiledMap A tile map exported from Tiled.
* @param tiledMap A tile map exported from Tiled or LDtk.
* @param levelIndex The level of the tile map to load from.
* @param atlasTexture The texture containing the whole tile set.
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
@@ -18,6 +18,21 @@ export declare namespace PixiTileMapHelper {
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
): TileTextureCache | null;
/**
* Split an atlas image into Pixi textures.
*
* @param atlasTexture The texture containing the whole tile set.
* @param columnCount The number of columns.
* @param rowCount The number of rows.
* @param tileSize The squared tile size.
* @returns A textures cache.
*/
function parseSimpleTileMapAtlas(
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
columnCount: number,
rowCount: number,
tileSize: number
): TileTextureCache;
/**
* Re-renders the tile map whenever its rendering settings have been changed
*

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;;;;;;;;;;;;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

@@ -0,0 +1,744 @@
/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
export type SimpleTileMapObjectDataType = {
content: {
opacity: number;
atlasImage: string;
rowCount: number;
columnCount: number;
tileSize: number;
tilesWithHitBox: string;
};
};
export type SimpleTileMapObjectData = ObjectData &
SimpleTileMapObjectDataType;
export type SimpleTileMapNetworkSyncDataType = {
op: number;
ai: string;
wid: number;
hei: number;
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
};
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
SimpleTileMapNetworkSyncDataType;
/**
* Displays a SimpleTileMap object.
*/
export class SimpleTileMapRuntimeObject
extends gdjs.RuntimeObject
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler {
/**
* A reusable Point to avoid allocations.
*/
private static readonly workingPoint: FloatPoint = [0, 0];
_opacity: float;
_atlasImage: string;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
readonly _rowCount: number;
readonly _columnCount: number;
readonly _tileSize: number;
_displayMode = 'all';
_layerIndex = 0;
_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;
// TODO: Add a debug mode like for TileMapCollisionMaskRuntimeObject to draw?
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: SimpleTileMapObjectDataType
) {
super(instanceContainer, objectData);
this._opacity = objectData.content.opacity;
this._atlasImage = objectData.content.atlasImage;
this._rowCount = objectData.content.rowCount;
this._columnCount = objectData.content.columnCount;
this._tileSize = objectData.content.tileSize;
this._initialTilesWithHitBox = (objectData.content
.tilesWithHitBox as string)
.split(',')
.filter((id) => !!id)
.map((idAsString) => parseInt(idAsString, 10));
this._tileMapManager = gdjs.TileMap.TileMapRuntimeManager.getManager(
instanceContainer
);
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
this,
instanceContainer
);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
getRendererObject() {
return this._renderer.getRendererObject();
}
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
if (this._isTileMapDirty) {
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
(textureName) => {
return (this.getInstanceContainer()
.getGame()
.getImageManager()
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
PIXI.Resource
>;
},
this._atlasImage,
this._tileSize,
this._columnCount,
this._rowCount,
(textureCache: TileMapHelper.TileTextureCache | null) => {
if (!textureCache) {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.refreshPixiTileMap(textureCache);
}
);
if (this._collisionTileMap) {
const tileMap = this._renderer.getTileMap();
if (tileMap) this._collisionTileMap.updateFromTileMap(tileMap);
}
this._isTileMapDirty = false;
}
}
updateFromObjectData(
oldObjectData: SimpleTileMapObjectData,
newObjectData: SimpleTileMapObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
) {
// TODO: support changing the atlas texture
return false;
}
// Map content is updated at hot-reload by extraInitializationFromInitialInstance.
return true;
}
getNetworkSyncData(): SimpleTileMapNetworkSyncData {
return {
...super.getNetworkSyncData(),
op: this._opacity,
ai: this._atlasImage,
wid: this.getWidth(),
hei: this.getHeight(),
};
}
updateFromNetworkSyncData(
networkSyncData: SimpleTileMapNetworkSyncData
): void {
super.updateFromNetworkSyncData(networkSyncData);
if (
networkSyncData.op !== undefined &&
networkSyncData.op !== this._opacity
) {
this.setOpacity(networkSyncData.op);
}
if (
networkSyncData.wid !== undefined &&
networkSyncData.wid !== this.getWidth()
) {
this.setWidth(networkSyncData.wid);
}
if (
networkSyncData.hei !== undefined &&
networkSyncData.hei !== this.getHeight()
) {
this.setHeight(networkSyncData.hei);
}
if (networkSyncData.ai !== undefined) {
// TODO: support changing the atlas texture
}
}
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {
// 1. load the tilemap from the instance.
for (const property of initialInstanceData.stringProperties) {
if (property.name === 'tilemap') {
this._initialTileMapAsJsObject = JSON.parse(property.value);
}
}
// 2. Update the renderer so that it updates the tilemap object
// (used for width and position calculations).
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
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();
});
}
private _loadInitialTileMap(
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
): void {
if (!this._initialTileMapAsJsObject) return;
this._tileMapManager.getOrLoadSimpleTileMap(
this._initialTileMapAsJsObject,
this.name,
this._tileSize,
this._columnCount,
this._rowCount,
(tileMap: TileMapHelper.EditableTileMap) => {
this._initialTilesWithHitBox.forEach((tileId) => {
const tileDefinition = tileMap.getTileDefinition(tileId);
if (!tileDefinition) {
console.warn(
`Could not set hit box for tile with id ${tileId}. Continuing.`
);
return;
}
tileDefinition.addHitBox(this._hitBoxTag, [
[0, 0],
[0, tileMap.getTileHeight()],
[tileMap.getTileWidth(), tileMap.getTileHeight()],
[tileMap.getTileWidth(), 0],
]);
});
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
(textureName) => {
return (this.getInstanceContainer()
.getGame()
.getImageManager()
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
PIXI.Resource
>;
},
this._atlasImage,
this._tileSize,
this._columnCount,
this._rowCount,
(textureCache: TileMapHelper.TileTextureCache | null) => {
if (!textureCache) {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
tileMapLoadingCallback(tileMap);
}
);
}
);
}
onDestroyed(): void {
super.onDestroyed();
this._renderer.destroy();
}
setWidth(width: float): void {
if (this.getWidth() === width) return;
this._transformationIsUpToDate = false;
this._renderer.setWidth(width);
this.invalidateHitboxes();
}
setHeight(height: float): void {
if (this.getHeight() === height) return;
this._transformationIsUpToDate = false;
this._renderer.setHeight(height);
this.invalidateHitboxes();
}
setSize(newWidth: float, newHeight: float): void {
this.setWidth(newWidth);
this.setHeight(newHeight);
this._transformationIsUpToDate = false;
}
/**
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*/
getScale(): float {
const scaleX = this.getScaleX();
const scaleY = this.getScaleY();
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
}
/**
* Change the scale on X and Y axis of the object.
*
* @param scale The new scale (must be greater than 0).
*/
setScale(scale: float): void {
this.setScaleX(scale);
this.setScaleY(scale);
this._transformationIsUpToDate = false;
}
/**
* Change the scale on X axis of the object (changing its width).
*
* @param scaleX The new scale (must be greater than 0).
*/
setScaleX(scaleX: float): void {
if (scaleX < 0) {
scaleX = 0;
}
if (this.getScaleX() === scaleX) return;
this._renderer.setScaleX(scaleX);
this.invalidateHitboxes();
this._transformationIsUpToDate = false;
}
/**
* Change the scale on Y axis of the object (changing its width).
*
* @param scaleY The new scale (must be greater than 0).
*/
setScaleY(scaleY: float): void {
if (scaleY < 0) {
scaleY = 0;
}
if (this.getScaleY() === scaleY) return;
this._renderer.setScaleY(scaleY);
this.invalidateHitboxes();
this._transformationIsUpToDate = false;
}
setX(x: float): void {
super.setX(x);
this._renderer.updatePosition();
this._transformationIsUpToDate = false;
}
setY(y: float): void {
super.setY(y);
this._renderer.updatePosition();
this._transformationIsUpToDate = false;
}
setAngle(angle: float): void {
super.setAngle(angle);
this._renderer.updateAngle();
this._transformationIsUpToDate = false;
}
setOpacity(opacity: float): void {
this._opacity = opacity;
this._renderer.updateOpacity();
this._isTileMapDirty = true;
}
getOpacity(): float {
return this._opacity;
}
getWidth(): float {
return this._renderer.getWidth();
}
getHeight(): float {
return this._renderer.getHeight();
}
getScaleX(): float {
return this._renderer.getScaleX();
}
getScaleY(): float {
return this._renderer.getScaleY();
}
/**
* This method is expensive and should not be called.
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
*/
getHitBoxes(): gdjs.Polygon[] {
if (this.hitBoxesDirty) {
this.updateHitBoxes();
this.updateAABB();
this.hitBoxesDirty = false;
}
return this.hitBoxes;
}
updateHitBoxes(): void {
this.updateTransformation();
if (!this._collisionTileMap) return;
this.hitBoxes = Array.from(
this._collisionTileMap.getAllHitboxes(this._hitBoxTag)
);
this.hitBoxesDirty = false;
this.updateAABB();
}
// This implementation doesn't use updateHitBoxes.
// It's important for good performances.
updateAABB(): void {
if (this.getAngle() === 0) {
// Fast computation of AABB for non rotated object
this.aabb.min[0] = this.x;
this.aabb.min[1] = this.y;
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
} else {
if (!this._collisionTileMap) return;
const affineTransformation = this._collisionTileMap.getTransformation();
const left = 0;
const right = this._collisionTileMap.getWidth();
const top = 0;
const bottom = this._collisionTileMap.getHeight();
const workingPoint = this.aabb.min;
workingPoint[0] = left;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topLeftX = workingPoint[0];
const topLeftY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topRightX = workingPoint[0];
const topRightY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomRightX = workingPoint[0];
const bottomRightY = workingPoint[1];
workingPoint[0] = left;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomLeftX = workingPoint[0];
const bottomLeftY = workingPoint[1];
this.aabb.min[0] = Math.min(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.max[0] = Math.max(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.min[1] = Math.min(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
this.aabb.max[1] = Math.max(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
}
}
getHitBoxesAround(
left: float,
top: float,
right: float,
bottom: float
): Iterable<gdjs.Polygon> {
// This implementation doesn't call updateHitBoxes.
// It's important for good performances because there is no need to
// update the whole collision mask where only a few hitboxes must be
// checked.
this.updateTransformation();
if (!this._collisionTileMap) return [];
return this._collisionTileMap.getHitboxesAround(
this._hitBoxTag,
left,
top,
right,
bottom
);
}
updateTransformation() {
if (this._transformationIsUpToDate) {
return;
}
const absScaleX = Math.abs(this._renderer.getScaleX());
const absScaleY = Math.abs(this._renderer.getScaleY());
this._tileMapToSceneTransformation.setToIdentity();
// Translation
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
// Rotation
const angleInRadians = (this.getAngle() * Math.PI) / 180;
this._tileMapToSceneTransformation.rotateAround(
angleInRadians,
this.getCenterX(),
this.getCenterY()
);
// Scale
this._tileMapToSceneTransformation.scale(absScaleX, absScaleY);
if (this._collisionTileMap) {
const collisionTileMapTransformation = this._collisionTileMap.getTransformation();
collisionTileMapTransformation.copyFrom(
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: float,
y: float
): [integer, integer] {
this.updateTransformation();
const gridCoordinates: FloatPoint =
SimpleTileMapRuntimeObject.workingPoint;
this._sceneToTileMapTransformation.transform([x, y], gridCoordinates);
const columnIndex = Math.floor(gridCoordinates[0] / this._tileSize);
const rowIndex = Math.floor(gridCoordinates[1] / this._tileSize);
return [columnIndex, rowIndex];
}
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);
}
setTileAtPosition(tileId: number, x: float, y: float) {
const [
columnIndex,
rowIndex,
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
}
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;
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(
'TileMap::SimpleTileMap',
gdjs.SimpleTileMapRuntimeObject
);
}

View File

@@ -7,7 +7,9 @@ namespace gdjs {
* @class TileMapRuntimeObjectPixiRenderer
*/
export class TileMapRuntimeObjectPixiRenderer {
private _object: any;
private _object:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject;
private _tileMap: TileMapHelper.EditableTileMap | null = null;
private _pixiObject: PIXI.tilemap.CompositeTilemap;
@@ -17,7 +19,9 @@ namespace gdjs {
* @param instanceContainer The gdjs.RuntimeScene in which the object is
*/
constructor(
runtimeObject: gdjs.TileMapRuntimeObject,
runtimeObject:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject,
instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._object = runtimeObject;
@@ -55,11 +59,28 @@ namespace gdjs {
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
if (!this._tileMap) return;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
this._tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
updatePosition(): void {
this._pixiObject.pivot.x = this.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this.getTileMapHeight() / 2;
@@ -72,7 +93,24 @@ 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;
for (const layer of tileMap.getLayers()) {
if (
(this._object._displayMode === 'index' &&
this._object._layerIndex !== layer.id) ||
(this._object._displayMode === 'visible' && !layer.isVisible())
) {
continue;
}
if (layer instanceof TileMapHelper.EditableTileMapLayer) {
layer.setAlpha(this._pixiObject.alpha);
}
}
}
getTileMapWidth() {
@@ -123,6 +161,116 @@ namespace gdjs {
return this._pixiObject.scale.y;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
const tileMap = this._tileMap;
if (!tileMap) return -1;
return tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param tileId The tile's id.
*/
setTileId(x: integer, y: integer, layerIndex: integer, tileId: number) {
const tileMap = this._tileMap;
if (!tileMap) return;
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);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
removeTile(x: integer, y: integer, layerIndex: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.removeTile(x, y, layerIndex);
}
/**
* @param targetRowCount The number of rows to have.
*/
setGridRowCount(targetRowCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
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 {
// Keep textures because they are shared by all tile maps.
this._pixiObject.destroy(false);

View File

@@ -33,7 +33,7 @@ namespace gdjs {
TilemapNetworkSyncDataType;
/**
* Displays a Tilemap object (mapeditor.org supported).
* Displays a Tilemap object (LDtk and Tiled).
*/
export class TileMapRuntimeObject
extends gdjs.RuntimeObject

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

@@ -318,7 +318,10 @@ namespace gdjs {
this.layer = '';
this._livingOnScene = true;
//@ts-ignore Reinitialize is like a constructor, it can overwrite the readonly property.
this.id = runtimeScene.createNewUniqueId();
this.id = runtimeScene
//
.getScene()
.createNewUniqueId();
this.persistentUuid = null;
this.networkId = null;
this.pick = false;
@@ -2541,12 +2544,14 @@ namespace gdjs {
* @param obj1 The first runtimeObject
* @param obj2 The second runtimeObject
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
* @param ignoredObject1Polygon A polygon from the `obj1` collision mask to ignore
* @return true if obj1 and obj2 are in collision
*/
static collisionTest(
obj1: gdjs.RuntimeObject,
obj2: gdjs.RuntimeObject,
ignoreTouchingEdges: boolean
ignoreTouchingEdges: boolean,
ignoredObject1Polygon: gdjs.Polygon | null = null
): boolean {
//First check if bounding circle are too far.
const o1centerX = obj1.getCenterX();
@@ -2600,6 +2605,9 @@ namespace gdjs {
);
for (const hitBox1 of hitBoxes1) {
if (hitBox1 === ignoredObject1Polygon) {
continue;
}
for (const hitBox2 of hitBoxes2) {
if (
gdjs.Polygon.collisionTest(hitBox1, hitBox2, ignoreTouchingEdges)

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();
@@ -647,6 +659,7 @@ interface ObjectsContainersList {
[Const, Value] DOMString GetTypeOfObject([Const] DOMString objectName);
[Const, Value] DOMString GetTypeOfBehavior([Const] DOMString name, boolean searchInGroups);
[Value] VectorString GetBehaviorsOfObject([Const] DOMString name, boolean searchInGroups);
[Value] VectorString GetAnimationNamesOfObject([Const] DOMString name);
[Const, Value] DOMString GetTypeOfBehaviorInObjectOrGroup([Const] DOMString objectOrGroupName, [Const] DOMString behaviorName, boolean searchInGroups);
boolean HasObjectOrGroupNamed([Const] DOMString name);
@@ -2346,6 +2359,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 +2454,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;
@@ -583,6 +590,7 @@ export class ObjectsContainersList extends EmscriptenObject {
getTypeOfObject(objectName: string): string;
getTypeOfBehavior(name: string, searchInGroups: boolean): string;
getBehaviorsOfObject(name: string, searchInGroups: boolean): VectorString;
getAnimationNamesOfObject(name: string): VectorString;
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
hasObjectOrGroupNamed(name: string): boolean;
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
@@ -1794,6 +1802,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 +1819,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

@@ -10,6 +10,7 @@ declare class gdObjectsContainersList {
getTypeOfObject(objectName: string): string;
getTypeOfBehavior(name: string, searchInGroups: boolean): string;
getBehaviorsOfObject(name: string, searchInGroups: boolean): gdVectorString;
getAnimationNamesOfObject(name: string): gdVectorString;
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
hasObjectOrGroupNamed(name: string): boolean;
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;

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";
/**
@@ -24,11 +30,15 @@ export class EditableTileMap {
/**
* The number of tile columns in the map.
*/
private readonly dimX: integer;
private dimX: integer;
/**
* The number of tile rows in the map.
*/
private readonly dimY: integer;
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.
@@ -54,6 +64,70 @@ export class EditableTileMap {
this._layers = [];
}
/**
* Loads EditableTileMap from serialized data.
* Uses object configuration as the source of truth as the serialized data
* might contain expired data (if the tile set configuration has changed and
* the serialized data was not updated).
* @param editableTileMapAsJsObject Serialized editable tile map object
* @param objectConfiguration
*/
static from(
editableTileMapAsJsObject: EditableTileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
tileSetRowCount,
}: {
tileSize: number;
tileSetColumnCount: number;
tileSetRowCount: number;
}
): EditableTileMap {
const tileSet = new Map<number, TileDefinition>();
// TODO: Actually save and load tile set when useful.
new Array(tileSetColumnCount * tileSetRowCount)
.fill(0)
.forEach((_, index) => {
tileSet.set(index, new TileDefinition(0));
});
const tileMap = new EditableTileMap(
tileSize || editableTileMapAsJsObject.tileWidth,
tileSize || editableTileMapAsJsObject.tileHeight,
editableTileMapAsJsObject.dimX || 1,
editableTileMapAsJsObject.dimY || 1,
tileSet
);
if (editableTileMapAsJsObject.layers) {
editableTileMapAsJsObject.layers.forEach((layerAsJsObject: any) => {
tileMap.addTileLayer(
EditableTileMapLayer.from(
layerAsJsObject,
tileMap,
(tileId) => tileId < tileSetColumnCount * tileSetRowCount
)
);
});
} else {
tileMap.addNewTileLayer(0);
}
return tileMap;
}
toJSObject(): Object {
return {
tileWidth: this.tileWidth,
tileHeight: this.tileHeight,
dimX: this.dimX,
dimY: this.dimY,
layers: this._layers.map((layer) => layer.toJSObject()),
};
}
/**
* @returns The tile map width in pixels.
*/
@@ -96,6 +170,85 @@ export class EditableTileMap {
return this.dimY;
}
/**
* 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;
}
/**
* @param tileId The tile identifier
* @returns The tile definition form the tile set.
@@ -115,12 +268,25 @@ 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;
}
/**
* @param layer the new layer to set.
*/
addTileLayer(layer: EditableTileMapLayer): void {
this._layers.push(layer);
}
getTileLayer(id: integer): EditableTileMapLayer | null {
const matchingLayer = this._layers.find((layer) => layer.id === id);
if (!(matchingLayer instanceof EditableTileMapLayer)) return null;
return matchingLayer;
}
/**
* @param id The identifier of the new layer.
* @returns The new layer.
@@ -182,6 +348,112 @@ export class EditableTileMap {
setBackgroundResourceName(resourceName: string): void {
this._backgroundResourceName = resourceName;
}
/**
* Returns true if all layers contain no defined tiled.
*/
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,
};
}
}
/**
@@ -211,12 +483,20 @@ abstract class AbstractEditableLayer {
this.visible = visible;
}
toJSObject(): Object {
return {};
}
/**
* @returns true if the layer is visible.
*/
isVisible(): boolean {
return this.visible;
}
isEmpty(): boolean {
return true;
}
}
/**
@@ -237,6 +517,10 @@ export class EditableObjectLayer extends AbstractEditableLayer {
add(object: TileObject): void {
this.objects.push(object);
}
isEmpty(): boolean {
return this.objects.length === 0;
}
}
/**
@@ -321,7 +605,7 @@ export class TileObject {
* A tile map layer with tile organized in grid.
*/
export class EditableTileMapLayer extends AbstractEditableLayer {
private readonly _tiles: Int32Array[];
private _tiles: Int32Array[];
private _alpha: float;
/**
@@ -330,14 +614,61 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
*/
constructor(tileMap: EditableTileMap, id: integer) {
super(tileMap, id);
this._tiles = [];
this._tiles.length = this.tileMap.getDimensionY();
for (let index = 0; index < this._tiles.length; index++) {
this._tiles[index] = new Int32Array(this.tileMap.getDimensionX());
}
this.buildEmptyLayer(
this.tileMap.getDimensionX(),
this.tileMap.getDimensionY()
);
this._alpha = 1;
}
buildEmptyLayer(dimensionX: number, dimensionY: number) {
this._tiles = [];
this._tiles.length = dimensionY;
for (let index = 0; index < this._tiles.length; index++) {
this._tiles[index] = new Int32Array(dimensionX);
}
}
static from(
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer {
const layer = new EditableTileMapLayer(
tileMap,
editableTileMapLayerAsJsObject.id
);
layer.setAlpha(editableTileMapLayerAsJsObject.alpha);
editableTileMapLayerAsJsObject.tiles.forEach((row: number[], y: number) =>
row.forEach((tileGID, x) => {
const tileId = FlippingHelper.getTileId(tileGID);
if (isTileIdValid(tileId)) {
layer.setTileGID(x, y, tileGID);
}
})
);
return layer;
}
toJSObject(): Object {
return {
id: this.id,
alpha: this._alpha,
tiles: this._tiles.map((row, y) =>
// 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) => {
const tileGID = this.getTileGID(x, y);
// -1 corresponds to null value
if (tileGID === undefined) return -1;
return tileGID;
})
)
),
};
}
/**
* The opacity (between 0-1) of the layer
*/
@@ -352,12 +683,84 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
this._alpha = alpha;
}
isEmpty(): boolean {
return this._tiles.every((row) => row.every((cell) => cell === 0));
}
reduceDimensions(
columnsToPop: number,
columnsToShift: number,
rowsToPop: number,
rowsToShift: number
) {
if (rowsToPop > 0 || rowsToShift > 0) {
this._tiles = this._tiles.slice(
rowsToShift,
rowsToPop ? -rowsToPop : undefined
);
}
if (columnsToPop > 0 || columnsToShift > 0) {
this._tiles.forEach((row, rowIndex) => {
this._tiles[rowIndex] = this._tiles[rowIndex].slice(
columnsToShift,
columnsToPop ? -columnsToPop : undefined
);
});
}
}
increaseDimensions(
columnsToAppend: number,
columnsToUnshift: number,
rowsToAppend: number,
rowsToUnshift: number
) {
const initialRowCount = this._tiles.length;
const initialColumnCount = this._tiles[0].length;
if (columnsToAppend > 0 || columnsToUnshift > 0) {
this._tiles.forEach((row, rowIndex) => {
const newRow = new Int32Array(
initialColumnCount + columnsToAppend + columnsToUnshift
).fill(0);
newRow.set(row, columnsToUnshift);
this._tiles[rowIndex] = newRow;
});
}
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)
.map(() =>
new Int32Array(
initialColumnCount + columnsToAppend + columnsToUnshift
).fill(0)
)
);
this._tiles.length = initialRowCount + rowsToAppend + rowsToUnshift;
for (
let rowIndex = initialRowCount + rowsToUnshift;
rowIndex < this._tiles.length;
rowIndex++
) {
this._tiles[rowIndex] = new Int32Array(
initialColumnCount + columnsToAppend + columnsToUnshift
).fill(0);
}
}
}
/**
* @param x The layer column.
* @param y The layer row.
* @param tileId The tile.
*/
setTile(x: integer, y: integer, tileId: integer): void {
setTile(x: integer, y: integer, tileId: integer) {
const definition = this.tileMap.getTileDefinition(tileId);
if (!definition) {
console.error(`Invalid tile definition index: ${tileId}`);
@@ -369,10 +772,83 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
return;
}
// +1 because 0 mean null
// +1 because 0 means null
tilesRow[x] = tileId + 1;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param tileGID The tile GID.
*/
setTileGID(x: integer, y: integer, tileGID: integer): void {
const tilesRow = this._tiles[y];
if (!tilesRow || x >= tilesRow.length) {
// Coordinates are out of bounds, don't do anything.
return;
}
// +1 because 0 means null
tilesRow[x] = tileGID + 1;
}
getTrimmingData(): {
rowsToShift: number;
columnsToShift: number;
rowsToPop: number;
columnsToPop: number;
} {
let rowsToShift = 0,
rowsToPop = 0;
const initialDimensionX = this.getDimensionX();
const initialDimensionY = this.getDimensionY();
const columnsToShiftByRow = new Array(this._tiles.length).fill(
this._tiles[0].length
);
const columnsToPopByRow = new Array(this._tiles.length).fill(
this._tiles[0].length
);
let isFirstNonEmptyRowFound = false;
for (let y = 0; y < this._tiles.length; y++) {
const row = this._tiles[y];
let isFirstNonEmptyColumnFound = false;
for (let x = 0; x < row.length; x++) {
const cell = row[x];
if (cell !== 0) {
columnsToPopByRow[y] = row.length - 1 - x;
if (!isFirstNonEmptyColumnFound) {
columnsToShiftByRow[y] = x;
isFirstNonEmptyColumnFound = true;
}
}
}
const isRowEmpty = !isFirstNonEmptyColumnFound;
if (!isRowEmpty) {
rowsToPop = this._tiles.length - 1 - y;
if (!isFirstNonEmptyRowFound) {
rowsToShift = y;
isFirstNonEmptyRowFound = true;
}
}
}
if (!isFirstNonEmptyRowFound) {
return {
columnsToShift: 0,
rowsToShift: 0,
columnsToPop: initialDimensionX - 1,
rowsToPop: initialDimensionY - 1,
};
}
const columnsToShift = Math.min(...columnsToShiftByRow);
const columnsToPop = Math.min(...columnsToPopByRow);
return {
rowsToShift,
columnsToShift,
rowsToPop,
columnsToPop,
};
}
/**
* @param x The layer column.
* @param y The layer row.
@@ -384,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.
@@ -117,6 +118,30 @@ export class TileMapManager {
);
}
getOrLoadSimpleTileMap(
tileMapAsJsObject: EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
tileSetColumnCount: number,
tileSetRowCount: number,
// Logic using callback has been set up to mimic what has been done for other
// loading methods. But it could be refactored to directly return the tile map.
callback: (tileMap: EditableTileMap) => void
): void {
// 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,
},
);
callback(editableTileMap);
}
/**
* @param loadTileMap The method that loads the Tiled JSON file in memory.
* @param getTexture The method that loads the atlas image file in memory.
@@ -178,6 +203,42 @@ export class TileMapManager {
);
}
/**
* @param getTexture The method that loads the atlas image file in memory.
* @param atlasImageResourceName The resource name of the atlas image.
* @param tileSize
* @param columnCount
* @param rowCount
* @param callback A function called when the tiles textures are split.
*/
getOrLoadSimpleTileMapTextureCache(
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
atlasImageResourceName: string,
tileSize: number,
columnCount: number,
rowCount: number,
callback: (textureCache: TileTextureCache | null) => void
): void {
const key = `${atlasImageResourceName}|${tileSize}|${columnCount}|${rowCount}`;
this._textureCacheCaches.getOrLoad(
key,
(textureCacheLoadingCallback) => {
const atlasTexture = atlasImageResourceName
? getTexture(atlasImageResourceName)
: null;
const textureCache = PixiTileMapHelper.parseSimpleTileMapAtlas(
atlasTexture,
columnCount,
rowCount,
tileSize
);
textureCacheLoadingCallback(textureCache);
},
callback
);
}
clearCaches(): void {
this._tileMapCache = new ResourceCache<EditableTileMap>();
this._textureCacheCaches = new ResourceCache<TileTextureCache>();

View File

@@ -14,7 +14,7 @@ export namespace PixiTileMapHelper {
/**
* Split an atlas image into Pixi textures.
*
* @param tiledMap A tile map exported from Tiled.
* @param tiledMap A tile map exported from Tiled or LDtk.
* @param levelIndex The level of the tile map to load from.
* @param atlasTexture The texture containing the whole tile set.
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
@@ -50,6 +50,43 @@ export namespace PixiTileMapHelper {
return null;
}
/**
* Split an atlas image into Pixi textures.
*
* @param atlasTexture The texture containing the whole tile set.
* @param columnCount The number of columns.
* @param rowCount The number of rows.
* @param tileSize The squared tile size.
* @returns A textures cache.
*/
export function parseSimpleTileMapAtlas(
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
columnCount: number,
rowCount: number,
tileSize: number
): TileTextureCache {
const textureCache = new TileTextureCache();
for (let x = 0; x < columnCount; x++) {
for (let y = 0; y < rowCount; y++) {
const rect = new PIXI.Rectangle(
x * tileSize,
y * tileSize,
tileSize,
tileSize
);
const texture = new PIXI.Texture(atlasTexture, rect);
textureCache.setTexture(
// Id of the tile
columnCount * y + x,
texture
);
}
}
return textureCache;
}
/**
* Re-renders the tile map whenever its rendering settings have been changed
*

View File

@@ -86,7 +86,7 @@
"flow-bin": "0.131.0",
"flow-coverage-report": "^0.4.0",
"folder-hash": "^3.0.0",
"iso-639-1": "^2.0.3",
"iso-639-1": "^3.1.2",
"minimist": "1.2.5",
"patch-package": "^6.4.7",
"prettier": "1.15.3",
@@ -19848,9 +19848,10 @@
"integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
},
"node_modules/iso-639-1": {
"version": "2.1.4",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.2.tgz",
"integrity": "sha512-Le7BRl3Jt9URvaiEHJCDEdvPZCfhiQoXnFgLAWNRhzFMwRFdWO7/5tLRQbiPzE394I9xd7KdRCM7S6qdOhwG5A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
}

View File

@@ -24,7 +24,7 @@
"flow-bin": "0.131.0",
"flow-coverage-report": "^0.4.0",
"folder-hash": "^3.0.0",
"iso-639-1": "^2.0.3",
"iso-639-1": "^3.1.2",
"minimist": "1.2.5",
"patch-package": "^6.4.7",
"prettier": "1.15.3",
@@ -152,6 +152,13 @@
}
]
}
],
"no-restricted-syntax": [
"error",
{
"selector": "MemberExpression[object.property.name='constructor'][property.name='name']",
"message": "'constructor.name' is not reliable (can become 'E', 'P' and etc.) in production build."
}
]
},
"overrides": [

View File

@@ -34,7 +34,7 @@ const getLocaleSourceCatalogFiles = localeName => {
if (localeName === 'pseudo_LOCALE') return ['ide-messages.pot'];
return ['ide-messages.po', 'gdcore-gdcpp-gdjs-extensions-messages.po'];
}
};
const getLocalePath = localeName => {
return path.join(localesBasePath, localeName);

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