Compare commits

...

47 Commits

Author SHA1 Message Date
Florian Rival
e9b4de2ca9 Fix tilemap instance error when no image set (#6923)
Only show in developer changelog
2024-09-05 16:08:59 +02:00
Clément Pasteau
75a4114ce8 Fix extension generation for docs (#6924) 2024-09-05 16:04:48 +02:00
Clément Pasteau
20abb9b45a Bump to 5.4.210 (#6922) 2024-09-05 11:54:52 +02:00
github-actions[bot]
56436fd44a Update translations [skip ci] (#6921)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-09-05 11:54:35 +02:00
AlexandreS
0dd5fc55c9 Improve subscriptions dialog to display current plan at the top (#6918)
* Mainly useful for old subscriptions that are not available in the list of available ones anymore.
2024-09-05 09:37:43 +02:00
github-actions[bot]
93db4cb508 Update translations [skip ci] (#6919)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-09-05 08:43:06 +02:00
D8H
f7888abf45 Allow custom objects to use the anchor behavior on their children (#6917) 2024-09-04 19:00:49 +02:00
github-actions[bot]
c8144da704 Update translations [skip ci] (#6909)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-09-04 15:25:50 +02:00
Giordane Oliveira
832e8cd593 Update AdMob banner so they cover part of the game screen instead of reducing it (#6900)
* In most games, it's better to design the game with the size of the ad banner in mind - which will cover either part of the top or bottom of the screen. Previously, the game size was reduced which was making it difficult to properly design a game and would introduce bad looking black borders.
2024-09-04 09:14:12 +02:00
AlexandreS
ef66a9f1a4 Add support for math formula in instance properties panel fields (#6914)
* For example, you can enter `100 + 50` or `100 + 70/2` or `ToDeg(sin(2.3))` in the instance editor fields
2024-09-03 18:41:38 +02:00
Florian Rival
5efbaa8c58 Add automated crash reporting from previews (#6915)
* This will allow to detect any bugs or crash in the game engine without relying on manual reports from users on GitHub. Note that exceptions and errors in JavaScript code blocks won't be reported. This can be deactivated in preferences if you prefer not to have GDevelop send these crash reports at all.
2024-09-03 16:52:53 +02:00
AlexandreS
ecbf38ccda Remove effects tab (and actions and conditions) wrongly shown for Tilemap objects (#6916) 2024-09-03 16:33:57 +02:00
AlexandreS
b3fcfc3f55 Avoid exponential loop when scrolling out of view on the Scene editor (#6912) 2024-09-03 09:33:45 +02:00
D8H
a515836add Avoid to duplicate custom object data in the project files (#6904)
- Fix hot-reload of sprite object animations
2024-09-02 18:52:27 +02:00
D8H
a7c81b47b2 Fix text input not properly deleted when used in a Custom Object (#6910) 2024-09-02 17:59:39 +02:00
D8H
0f22e462ad Keep clearing object lists before the events too (#6913)
- Don't show in changelog
2024-09-02 17:43:47 +02:00
AlexandreS
e6e4d9048f Add action to set Text object font (#6911) 2024-09-02 16:25:41 +02:00
github-actions[bot]
12f5f95d0c Update translations [skip ci] (#6902)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-09-02 13:53:57 +02:00
D8H
c52168a967 Fix memory clearing/leak when a scene is paused or stopped (#6123) 2024-09-02 13:52:37 +02:00
AlexandreS
1e33a13cc5 Improve tilemap preview rendering (#6908)
- Remove FPS limitation
- Avoid useless computations
2024-09-02 12:10:43 +02:00
Vladyslav Pohorielov
505debd60c Add expressions to get Spine point attachments positions (#6907) 2024-09-02 10:35:30 +02:00
Aurélien Vivet
e3b7109154 Add a default shortcut for adding a comment in the Events Sheet (#6879) 2024-09-02 09:22:13 +02:00
D8H
9e25899d3e Remove an "only" in GDJS tests (#6906)
- Don't show in changelog
2024-08-31 00:28:10 +02:00
AlexandreS
87cb8f0d47 Activate user after creating it (#6901)
Don't show in changelog
2024-08-30 09:57:38 +02:00
github-actions[bot]
481c6da992 Update translations [skip ci] (#6886)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-08-30 09:28:33 +02:00
AlexandreS
7cbebbb82f Fix some tilemap UX paintpoints (#6899)
- When using a mouse, use left click only to paint. Middle click can be used to pan the view.
- Fix issues with undo/redo tile setting
2024-08-29 20:08:05 +02:00
AlexandreS
fcf668788b Add interface for teacher to manage education users (#6891) 2024-08-29 18:42:25 +02:00
D8H
0cc844a77f Make the experimental custom object visual editor accessible (#6897)
* This is an *experimental**, *still work-in-progress*, editor to build objects inside GDevelop - from small, reusable UI objects to larger parts of your game.
2024-08-29 18:33:25 +02:00
Clément Pasteau
a234d9bd35 Show game ads earnings (#6896)
* You can now see the earnings of the games you've published on gd.games, thanks to ads, at the top of your Games dashboard
* You can cash out that amount if you reach the threshold or exchange it with GDevelop credits
2024-08-29 18:27:40 +02:00
D8H
465a6ce2ab Fix custom objects hot-reloading (#6887)
- Don't show in changelog
2024-08-29 18:13:54 +02:00
D8H
7e2e19eb33 Improve tile map collision precision (#6895) 2024-08-29 18:13:08 +02:00
D8H
95101763f7 Hide the drop-down list about quick customization visibility (#6893) 2024-08-28 16:59:24 +02:00
D8H
d4bd5fc671 Hide required behavior properties in the object editor (#6889) 2024-08-28 14:07:36 +02:00
D8H
c7fcf48ba5 Hide a duplicated expression to get the text of a text input (#6892) 2024-08-28 14:07:23 +02:00
D8H
8926d4406f Fix to forbid default parameters from being dragged and dropped (#6888) 2024-08-27 19:20:45 +02:00
Clément Pasteau
9ed2173038 Multiplayer game host migration (#6878)
* Host of a multiplayer game is now automatically migrated to a new one, picked from the list of players, with the lowest ping.
* A new condition is available "Is host migrating" in order to adapt the game while the migration happens. This can take up to a few seconds. If your game has no interactions between players, you can leave it like this. Otherwise, you may want to pause the game while the host is changing!
* A new action is available to configure the lobby game to end instead of migrating when the host leaves. (This is useful if you prefer the host not changing during your game)
2024-08-27 14:30:36 +02:00
github-actions[bot]
2fc3bc337f Update translations [skip ci] (#6885)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-08-27 11:49:01 +02:00
Clément Pasteau
0b7cac79ef Allow changing Sync Rate of objects in a multiplayer game (#6884)
* Default sync rate of objects is now set to 30 times per second (was 60 before, but 30 is enough for most games)
* A new action can be used to update this value depending on the type of your game, at the cost of bandwidth. Fast-paced competitive? go for 60. Slow turn-based game? 10 is probably enough, or even less!
2024-08-26 18:27:27 +02:00
D8H
8721c0099e [Physics2] Merge the 2 world scale properties into one (#6865) 2024-08-26 15:16:01 +02:00
Aurélien Vivet
4453eee3b9 Fix default values for the Adjustement effect (#6883) 2024-08-26 14:11:44 +02:00
github-actions[bot]
0215ab7dbb Update translations [skip ci] (#6874)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-08-26 14:10:00 +02:00
AlexandreS
87f6d5b99f Fix update notifications translations on Desktop app (#6882) 2024-08-26 10:57:39 +02:00
D8H
a440b16f84 Fix a memory issue with default custom object configurations (#6881)
- Don't show in changelog
2024-08-23 20:30:45 +02:00
D8H
f3822ba0df Allow to center objects with the anchor behavior (#6880) 2024-08-23 19:02:31 +02:00
D8H
6c5813affd Fix hot-reload for variables (#6877) 2024-08-23 15:03:40 +02:00
D8H
be4fe62bb6 Allow to override custom objects default property values or to follow them (#6861) 2024-08-23 14:12:13 +02:00
AlexandreS
0a29999894 Add possibility to drop variable after last item (#6875) 2024-08-22 09:59:07 +02:00
259 changed files with 9405 additions and 2855 deletions

View File

@@ -107,7 +107,7 @@
"description": "Define a parameter in a GDevelop extension definition.",
"prefix": "gdparam",
"body": [
".addParameter('${1|string,expression,object,behavior,yesorno,stringWithSelector,scenevar,globalvar,objectvar,objectList,objectListWithoutPicking,color,key,sceneName,file,layer,relationalOperator,operator,trueorfalse,musicfile,soundfile,police,mouse,passwordjoyaxis,camera,objectPtr,forceMultiplier|}', '${2:Parameter description}', '${3:Optional parameter data}', /*parameterIsOptional=*/${4|false,true|})"
".addParameter('${1|string,expression,object,behavior,yesorno,stringWithSelector,scenevar,globalvar,objectvar,objectList,objectListWithoutPicking,color,key,sceneName,file,layer,relationalOperator,operator,trueorfalse,musicfile,soundfile,mouse,passwordjoyaxis,camera,objectPtr,forceMultiplier|}', '${2:Parameter description}', '${3:Optional parameter data}', /*parameterIsOptional=*/${4|false,true|})"
]
},
"Add code only parameter": {

View File

@@ -77,11 +77,11 @@ gd::String EventsCodeGenerator::GenerateRelationalOperatorCall(
/**
* @brief Generate a relational operation
*
*
* @param relationalOperator the operator
* @param lhs the left hand operand
* @param rhs the right hand operand
* @return gd::String
* @return gd::String
*/
gd::String EventsCodeGenerator::GenerateRelationalOperation(
const gd::String& relationalOperator,
@@ -828,7 +828,7 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
metadata.GetType() == "spineResource" ||
// Deprecated, old parameter names:
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
metadata.GetType() == "soundfile" || metadata.GetType() == "police") {
metadata.GetType() == "soundfile") {
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
} else if (metadata.GetType() == "mouse") {
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
@@ -1007,7 +1007,7 @@ gd::String EventsCodeGenerator::GenerateEventsListCode(
output += "\n" + scopeBegin + "\n" + declarationsCode + "\n" +
eventCoreCode + "\n" + scopeEnd + "\n";
if (event.HasVariables()) {
GetProjectScopedContainers().GetVariablesContainersList().Pop();
}

View File

@@ -528,7 +528,7 @@ protected:
parameter -> string
* - operator : Used to update a value using a setter and a getter -> string
* - key, mouse, objectvar, scenevar, globalvar, password, musicfile,
soundfile, police -> string
soundfile -> string
* - trueorfalse, yesorno -> boolean ( See GenerateTrue/GenerateFalse ).
*
* <br><br>
@@ -849,7 +849,7 @@ protected:
instructionUniqueIds; ///< The unique ids generated for instructions.
size_t eventsListNextUniqueId; ///< The next identifier to use for an events
///< list function name.
gd::DiagnosticReport* diagnosticReport;
};

View File

@@ -225,9 +225,7 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi
size_t parameterIndex,
const gd::String& lastObjectName) {
const String& parameterValue = parameterExpression.GetPlainString();
if (parameterMetadata.GetType() ==
"police" || // Should be renamed fontResource
parameterMetadata.GetType() == "fontResource") {
if (parameterMetadata.GetType() == "fontResource") {
gd::String updatedParameterValue = parameterValue;
worker.ExposeFont(updatedParameterValue);
instruction.SetParameter(parameterIndex, updatedParameterValue);

View File

@@ -39,16 +39,16 @@ void CustomConfigurationHelper::InitializeContent(
std::map<gd::String, gd::PropertyDescriptor> CustomConfigurationHelper::GetProperties(
const gd::PropertiesContainer &properties,
const gd::SerializerElement &configurationContent) {
auto behaviorProperties = std::map<gd::String, gd::PropertyDescriptor>();
auto objectProperties = std::map<gd::String, gd::PropertyDescriptor>();
for (auto &property : properties.GetInternalVector()) {
const auto &propertyName = property->GetName();
const auto &propertyType = property->GetType();
// Copy the property
behaviorProperties[propertyName] = *property;
objectProperties[propertyName] = *property;
auto &newProperty = behaviorProperties[propertyName];
auto &newProperty = objectProperties[propertyName];
if (configurationContent.HasChild(propertyName)) {
if (propertyType == "String" || propertyType == "Choice" ||
@@ -71,7 +71,7 @@ std::map<gd::String, gd::PropertyDescriptor> CustomConfigurationHelper::GetPrope
}
}
return behaviorProperties;
return objectProperties;
}
bool CustomConfigurationHelper::UpdateProperty(

View File

@@ -21,6 +21,9 @@ void CustomObjectConfiguration::Init(const gd::CustomObjectConfiguration& object
project = objectConfiguration.project;
objectContent = objectConfiguration.objectContent;
animations = objectConfiguration.animations;
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
objectConfiguration
.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
// There is no default copy for a map of unique_ptr like childObjectConfigurations.
childObjectConfigurations.clear();
@@ -42,6 +45,26 @@ const gd::EventsBasedObject* CustomObjectConfiguration::GetEventsBasedObject() c
return &project->GetEventsBasedObject(GetType());
}
bool CustomObjectConfiguration::
IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const {
const auto *eventsBasedObject = GetEventsBasedObject();
if (!eventsBasedObject) {
// True is safer because nothing will be lost when serializing.
return true;
}
return eventsBasedObject->GetInitialInstances().GetInstancesCount() == 0;
}
bool CustomObjectConfiguration::
IsOverridingEventsBasedObjectChildrenConfiguration() const {
return isMarkedAsOverridingEventsBasedObjectChildrenConfiguration ||
IsForcedToOverrideEventsBasedObjectChildrenConfiguration();
}
void CustomObjectConfiguration::ClearChildrenConfiguration() {
childObjectConfigurations.clear();
}
gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(const gd::String &objectName) {
const auto *eventsBasedObject = GetEventsBasedObject();
if (!eventsBasedObject) {
@@ -55,6 +78,18 @@ gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(
}
auto &childObject = eventsBasedObject->GetObjects().GetObject(objectName);
if (!IsOverridingEventsBasedObjectChildrenConfiguration()) {
// It should be fine because the editor doesn't allow to edit values when
// the default values from the events-based object is used.
//
// Resource refactor operations may modify it but they will do the same
// thing on the custom object as on the event-based object children so it
// shouldn't have any side effect.
return const_cast<gd::ObjectConfiguration &>(
childObject.GetConfiguration());
}
auto configurationPosition = childObjectConfigurations.find(objectName);
if (configurationPosition == childObjectConfigurations.end()) {
childObjectConfigurations.insert(std::make_pair(
@@ -67,7 +102,7 @@ gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(
auto &configuration = pair.second;
return *configuration;
}
}
}
std::map<gd::String, gd::PropertyDescriptor> CustomObjectConfiguration::GetProperties() const {
auto objectProperties = std::map<gd::String, gd::PropertyDescriptor>();
@@ -128,26 +163,14 @@ void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const
animations.SerializeTo(animatableElement);
}
auto &childrenContentElement = element.AddChild("childrenContent");
for (auto &pair : childObjectConfigurations) {
auto &childName = pair.first;
auto &childConfiguration = pair.second;
auto &childElement = childrenContentElement.AddChild(childName);
childConfiguration->SerializeTo(childElement);
}
const auto *eventsBasedObject = GetEventsBasedObject();
if (eventsBasedObject) {
eventsBasedObject->GetInitialInstances().SerializeTo(
element.AddChild("instances"));
eventsBasedObject->GetLayers().SerializeLayersTo(
element.AddChild("layers"));
element.SetIntAttribute("areaMinX", eventsBasedObject->GetAreaMinX());
element.SetIntAttribute("areaMinY", eventsBasedObject->GetAreaMinY());
element.SetIntAttribute("areaMinZ", eventsBasedObject->GetAreaMinZ());
element.SetIntAttribute("areaMaxX", eventsBasedObject->GetAreaMaxX());
element.SetIntAttribute("areaMaxY", eventsBasedObject->GetAreaMaxY());
element.SetIntAttribute("areaMaxZ", eventsBasedObject->GetAreaMaxZ());
if (IsOverridingEventsBasedObjectChildrenConfiguration()) {
auto &childrenContentElement = element.AddChild("childrenContent");
for (auto &pair : childObjectConfigurations) {
auto &childName = pair.first;
auto &childConfiguration = pair.second;
auto &childElement = childrenContentElement.AddChild(childName);
childConfiguration->SerializeTo(childElement);
}
}
}
void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
@@ -159,12 +182,16 @@ void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
animations.UnserializeFrom(animatableElement);
}
auto &childrenContentElement = element.GetChild("childrenContent");
for (auto &pair : childrenContentElement.GetAllChildren()) {
auto &childName = pair.first;
auto &childElement = pair.second;
auto &childConfiguration = GetChildObjectConfiguration(childName);
childConfiguration.UnserializeFrom(project, *childElement);
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
element.HasChild("childrenContent");
if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
auto &childrenContentElement = element.GetChild("childrenContent");
for (auto &pair : childrenContentElement.GetAllChildren()) {
auto &childName = pair.first;
auto &childElement = pair.second;
auto &childConfiguration = GetChildObjectConfiguration(childName);
childConfiguration.UnserializeFrom(project, *childElement);
}
}
}

View File

@@ -30,7 +30,7 @@ namespace gd {
class CustomObjectConfiguration : public gd::ObjectConfiguration {
public:
CustomObjectConfiguration(const Project& project_, const String& type_)
: project(&project_) {
: project(&project_), isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(false) {
SetType(type_);
}
std::unique_ptr<gd::ObjectConfiguration> Clone() const override;
@@ -65,7 +65,22 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
void ExposeResources(gd::ArbitraryResourceWorker& worker) override;
gd::ObjectConfiguration &GetChildObjectConfiguration(const gd::String& objectName);
bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const;
bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const {
return isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
}
void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
bool isOverridingEventsBasedObjectChildrenConfiguration_) {
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
isOverridingEventsBasedObjectChildrenConfiguration_;
}
void ClearChildrenConfiguration();
gd::ObjectConfiguration &
GetChildObjectConfiguration(const gd::String &objectName);
std::size_t GetAnimationsCount() const override;
@@ -90,10 +105,14 @@ protected:
private:
const gd::EventsBasedObject* GetEventsBasedObject() const;
bool IsOverridingEventsBasedObjectChildrenConfiguration() const;
const Project* project; ///< The project is used to get the
///< EventBasedObject from the fullType.
gd::SerializerElement objectContent;
std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;
bool isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
mutable std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;
static gd::ObjectConfiguration badObjectConfiguration;

View File

@@ -16,6 +16,7 @@ EventsBasedObject::EventsBasedObject()
isRenderedIn3D(false),
isAnimatable(false),
isTextContainer(false),
isInnerAreaFollowingParentSize(false),
areaMinX(0),
areaMinY(0),
areaMinZ(0),
@@ -37,6 +38,9 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const {
if (isTextContainer) {
element.SetBoolAttribute("isTextContainer", true);
}
if (isInnerAreaFollowingParentSize) {
element.SetBoolAttribute("isInnerAreaFollowingParentSize", true);
}
element.SetIntAttribute("areaMinX", areaMinX);
element.SetIntAttribute("areaMinY", areaMinY);
element.SetIntAttribute("areaMinZ", areaMinZ);
@@ -59,6 +63,8 @@ void EventsBasedObject::UnserializeFrom(gd::Project& project,
isRenderedIn3D = element.GetBoolAttribute("is3D", false);
isAnimatable = element.GetBoolAttribute("isAnimatable", false);
isTextContainer = element.GetBoolAttribute("isTextContainer", false);
isInnerAreaFollowingParentSize =
element.GetBoolAttribute("isInnerAreaFollowingParentSize", false);
areaMinX = element.GetIntAttribute("areaMinX", 0);
areaMinY = element.GetIntAttribute("areaMinY", 0);
areaMinZ = element.GetIntAttribute("areaMinZ", 0);

View File

@@ -101,11 +101,35 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
/**
* \brief Declare a TextContainer capability.
*/
EventsBasedObject& MarkAsTextContainer(bool isTextContainer_) {
EventsBasedObject &MarkAsTextContainer(bool isTextContainer_) {
isTextContainer = isTextContainer_;
return *this;
}
/**
* \brief Declare that the parent scale will always be 1 and children will
* adapt there size. This is removing the ScalableCapability.
*/
EventsBasedObject &
MarkAsInnerAreaExpandingWithParent(bool isInnerAreaExpandingWithParent_) {
isInnerAreaFollowingParentSize = isInnerAreaExpandingWithParent_;
return *this;
}
/**
* \brief Return true if objects handle size changes on their own and
* don't have the ScalableCapability.
*
* When the parent dimensions change:
* - if `false`, the object is stretch proportionally while children local
* positions stay the same.
* - if `true`, the children local positions need to be adapted by events
* to follow their parent size.
*/
bool IsInnerAreaFollowingParentSize() const {
return isInnerAreaFollowingParentSize;
}
/**
* \brief Return true if the object needs a TextContainer capability.
*/
@@ -279,6 +303,7 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
bool isRenderedIn3D;
bool isAnimatable;
bool isTextContainer;
bool isInnerAreaFollowingParentSize;
gd::InitialInstancesContainer initialInstances;
gd::LayersContainer layers;
gd::ObjectsContainer objectsContainer;

View File

@@ -589,4 +589,13 @@ std::vector<gd::String> ObjectsContainersList::GetAnimationNamesOfObject(
return animationNames;
}
const gd::ObjectsContainer &
ObjectsContainersList::GetObjectsContainer(std::size_t index) const {
return *objectsContainers[index];
}
std::size_t ObjectsContainersList::GetObjectsContainersCount() const {
return objectsContainers.size();
}
} // namespace gd

View File

@@ -173,6 +173,16 @@ class GD_CORE_API ObjectsContainersList {
std::function<void(const gd::String& variableName,
const gd::Variable& variable)> fn) const;
/**
* \brief Return a the objects container at position \a index.
*/
const gd::ObjectsContainer &GetObjectsContainer(std::size_t index) const;
/**
* \brief Return the number of objects containers.
*/
std::size_t GetObjectsContainersCount() const;
/** Do not use - should be private but accessible to let Emscripten create a
* temporary. */
ObjectsContainersList(){};

View File

@@ -304,8 +304,15 @@ namespace gdjs {
*/
setDepth(depth: float): void {
const unscaledDepth = this.getUnscaledDepth();
if (unscaledDepth !== 0) {
this.setScaleZ(depth / unscaledDepth);
if (unscaledDepth === 0) {
return;
}
const scaleZ = depth / unscaledDepth;
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
this._innerArea.min[2] *= scaleZ;
this._innerArea.max[2] *= scaleZ;
} else {
this.setScaleZ(scaleZ);
}
}
@@ -325,6 +332,10 @@ namespace gdjs {
* @param newScale The new scale (must be greater than 0).
*/
setScaleZ(newScale: number): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}

View File

@@ -333,6 +333,7 @@ namespace gdjs {
adUnitId,
position: atTop ? 'top' : 'bottom',
size: bannerRequestedAdSizeType,
offset: 0,
});
banner.on('load', () => {

View File

@@ -25,7 +25,6 @@ void AnchorBehavior::InitializeContent(gd::SerializerElement& content) {
content.SetAttribute("useLegacyBottomAndRightAnchors", false);
}
#if defined(GD_IDE_ONLY)
namespace {
gd::String GetAnchorAsString(AnchorBehavior::HorizontalAnchor anchor) {
if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_LEFT)
@@ -34,6 +33,8 @@ gd::String GetAnchorAsString(AnchorBehavior::HorizontalAnchor anchor) {
return _("Window right");
else if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_PROPORTIONAL)
return _("Proportional");
else if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_CENTER)
return _("Window center");
else
return _("No anchor");
}
@@ -45,6 +46,8 @@ gd::String GetAnchorAsString(AnchorBehavior::VerticalAnchor anchor) {
return _("Window bottom");
else if (anchor == AnchorBehavior::ANCHOR_VERTICAL_PROPORTIONAL)
return _("Proportional");
else if (anchor == AnchorBehavior::ANCHOR_VERTICAL_WINDOW_CENTER)
return _("Window center");
else
return _("No anchor");
}
@@ -69,6 +72,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.SetType("Choice")
.AddExtraInfo(_("No anchor"))
.AddExtraInfo(_("Window left"))
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window right"))
.AddExtraInfo(_("Proportional"))
.SetDescription(_("Anchor the left edge of the object on X axis."));
@@ -79,6 +83,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.SetType("Choice")
.AddExtraInfo(_("No anchor"))
.AddExtraInfo(_("Window left"))
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window right"))
.AddExtraInfo(_("Proportional"))
.SetDescription(_("Anchor the right edge of the object on X axis."));
@@ -89,6 +94,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.SetType("Choice")
.AddExtraInfo(_("No anchor"))
.AddExtraInfo(_("Window top"))
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window bottom"))
.AddExtraInfo(_("Proportional"))
.SetDescription(_("Anchor the top edge of the object on Y axis."));
@@ -99,6 +105,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.SetType("Choice")
.AddExtraInfo(_("No anchor"))
.AddExtraInfo(_("Window top"))
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window bottom"))
.AddExtraInfo(_("Proportional"))
.SetDescription(_("Anchor the bottom edge of the object on Y axis."));
@@ -127,6 +134,8 @@ AnchorBehavior::HorizontalAnchor GetHorizontalAnchorFromString(
return AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_RIGHT;
else if (value == _("Proportional"))
return AnchorBehavior::ANCHOR_HORIZONTAL_PROPORTIONAL;
else if (value == _("Window center"))
return AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_CENTER;
else
return AnchorBehavior::ANCHOR_HORIZONTAL_NONE;
}
@@ -139,6 +148,8 @@ AnchorBehavior::VerticalAnchor GetVerticalAnchorFromString(
return AnchorBehavior::ANCHOR_VERTICAL_WINDOW_BOTTOM;
else if (value == _("Proportional"))
return AnchorBehavior::ANCHOR_VERTICAL_PROPORTIONAL;
else if (value == _("Window center"))
return AnchorBehavior::ANCHOR_VERTICAL_WINDOW_CENTER;
else
return AnchorBehavior::ANCHOR_VERTICAL_NONE;
}
@@ -172,4 +183,3 @@ bool AnchorBehavior::UpdateProperty(gd::SerializerElement& behaviorContent,
return true;
}
#endif

View File

@@ -3,8 +3,8 @@ GDevelop - Anchor Behavior Extension
Copyright (c) 2016 Victor Levasseur (victorlevasseur52@gmail.com)
This project is released under the MIT License.
*/
#ifndef ANCHORBEHAVIOR_H
#define ANCHORBEHAVIOR_H
#pragma once
#include <vector>
#include "GDCore/Project/Behavior.h"
#include "GDCore/Project/Object.h"
@@ -22,14 +22,16 @@ class GD_EXTENSION_API AnchorBehavior : public gd::Behavior {
ANCHOR_HORIZONTAL_NONE = 0,
ANCHOR_HORIZONTAL_WINDOW_LEFT = 1,
ANCHOR_HORIZONTAL_WINDOW_RIGHT = 2,
ANCHOR_HORIZONTAL_PROPORTIONAL = 3
ANCHOR_HORIZONTAL_PROPORTIONAL = 3,
ANCHOR_HORIZONTAL_WINDOW_CENTER = 4
};
enum VerticalAnchor {
ANCHOR_VERTICAL_NONE = 0,
ANCHOR_VERTICAL_WINDOW_TOP = 1,
ANCHOR_VERTICAL_WINDOW_BOTTOM = 2,
ANCHOR_VERTICAL_PROPORTIONAL = 3
ANCHOR_VERTICAL_PROPORTIONAL = 3,
ANCHOR_VERTICAL_WINDOW_CENTER = 4
};
AnchorBehavior() {};
@@ -47,5 +49,3 @@ class GD_EXTENSION_API AnchorBehavior : public gd::Behavior {
virtual void InitializeContent(
gd::SerializerElement& behaviorContent) override;
};
#endif // ANCHORBEHAVIOR_H

View File

@@ -4,10 +4,25 @@ Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const enum HorizontalAnchor {
None = 0,
WindowLeft,
WindowRight,
Proportional,
WindowCenter,
}
const enum VerticalAnchor {
None = 0,
WindowTop,
WindowBottom,
Proportional,
WindowCenter,
}
export class AnchorRuntimeBehavior extends gdjs.RuntimeBehavior {
_relativeToOriginalWindowSize: any;
_leftEdgeAnchor: any;
_rightEdgeAnchor: any;
_leftEdgeAnchor: HorizontalAnchor;
_rightEdgeAnchor: HorizontalAnchor;
_topEdgeAnchor: any;
_bottomEdgeAnchor: any;
_invalidDistances: boolean = true;
@@ -74,14 +89,25 @@ namespace gdjs {
gdjs.AnchorRuntimeBehavior.prototype.doStepPreEvents
) as FloatPoint;
// TODO EBO Make it work with event based objects or hide this behavior for them.
const game = instanceContainer.getGame();
let rendererWidth = game.getGameResolutionWidth();
let rendererHeight = game.getGameResolutionHeight();
let parentMinX = instanceContainer.getUnrotatedViewportMinX();
let parentMinY = instanceContainer.getUnrotatedViewportMinY();
let parentMaxX = instanceContainer.getUnrotatedViewportMaxX();
let parentMaxY = instanceContainer.getUnrotatedViewportMaxY();
let parentCenterX = (parentMaxX + parentMinX) / 2;
let parentCenterY = (parentMaxY + parentMinY) / 2;
let parentWidth = parentMaxX - parentMinX;
let parentHeight = parentMaxY - parentMinY;
const layer = instanceContainer.getLayer(this.owner.getLayer());
if (this._invalidDistances) {
if (this._relativeToOriginalWindowSize) {
rendererWidth = game.getOriginalWidth();
rendererHeight = game.getOriginalHeight();
parentMinX = instanceContainer.getInitialUnrotatedViewportMinX();
parentMinY = instanceContainer.getInitialUnrotatedViewportMinY();
parentMaxX = instanceContainer.getInitialUnrotatedViewportMaxX();
parentMaxY = instanceContainer.getInitialUnrotatedViewportMaxY();
parentCenterX = (parentMaxX + parentMinX) / 2;
parentCenterY = (parentMaxY + parentMinY) / 2;
parentWidth = parentMaxX - parentMinX;
parentHeight = parentMaxY - parentMinY;
}
//Calculate the distances from the window's bounds.
@@ -92,49 +118,28 @@ namespace gdjs {
workingPoint
);
//Left edge
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
) {
this._leftEdgeDistance = topLeftPixel[0];
} else {
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
) {
this._leftEdgeDistance = rendererWidth - topLeftPixel[0];
} else {
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
) {
this._leftEdgeDistance = topLeftPixel[0] / rendererWidth;
}
}
// Left edge
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
this._leftEdgeDistance = topLeftPixel[0] - parentMinX;
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
this._leftEdgeDistance = topLeftPixel[0] - parentMaxX;
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
this._leftEdgeDistance = (topLeftPixel[0] - parentMinX) / parentWidth;
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
this._leftEdgeDistance = topLeftPixel[0] - parentCenterX;
}
//Top edge
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
) {
this._topEdgeDistance = topLeftPixel[1];
} else {
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
) {
this._topEdgeDistance = rendererHeight - topLeftPixel[1];
} else {
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
) {
this._topEdgeDistance = topLeftPixel[1] / rendererHeight;
}
}
// Top edge
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
this._topEdgeDistance = topLeftPixel[1] - parentMinY;
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
this._topEdgeDistance = topLeftPixel[1] - parentMaxY;
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
this._topEdgeDistance = (topLeftPixel[1] - parentMinY) / parentHeight;
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
this._topEdgeDistance = topLeftPixel[1] - parentCenterY;
}
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
const bottomRightPixel = layer.convertCoords(
this.owner.getDrawableX() + this.owner.getWidth(),
@@ -143,49 +148,30 @@ namespace gdjs {
workingPoint
);
//Right edge
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
) {
this._rightEdgeDistance = bottomRightPixel[0];
} else {
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
) {
this._rightEdgeDistance = rendererWidth - bottomRightPixel[0];
} else {
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
) {
this._rightEdgeDistance = bottomRightPixel[0] / rendererWidth;
}
}
// Right edge
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
this._rightEdgeDistance = bottomRightPixel[0] - parentMinX;
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
this._rightEdgeDistance = bottomRightPixel[0] - parentMaxX;
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
this._rightEdgeDistance =
(bottomRightPixel[0] - parentMinX) / parentWidth;
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
this._rightEdgeDistance = bottomRightPixel[0] - parentCenterX;
}
//Bottom edge
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
) {
this._bottomEdgeDistance = bottomRightPixel[1];
} else {
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
) {
this._bottomEdgeDistance = rendererHeight - bottomRightPixel[1];
} else {
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
) {
this._bottomEdgeDistance = bottomRightPixel[1] / rendererHeight;
}
}
// Bottom edge
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
this._bottomEdgeDistance = bottomRightPixel[1] - parentMinY;
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
this._bottomEdgeDistance = bottomRightPixel[1] - parentMaxY;
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
this._bottomEdgeDistance =
(bottomRightPixel[1] - parentMinY) / parentHeight;
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
this._bottomEdgeDistance = bottomRightPixel[1] - parentCenterY;
}
this._invalidDistances = false;
} else {
//Move and resize the object if needed
@@ -194,93 +180,50 @@ namespace gdjs {
let rightPixel = 0;
let bottomPixel = 0;
//Left edge
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
) {
leftPixel = this._leftEdgeDistance;
} else {
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
) {
leftPixel = rendererWidth - this._leftEdgeDistance;
} else {
if (
this._leftEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
) {
leftPixel = this._leftEdgeDistance * rendererWidth;
}
}
// Left edge
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
leftPixel = parentMinX + this._leftEdgeDistance;
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
leftPixel = parentMaxX + this._leftEdgeDistance;
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
leftPixel = parentMinX + this._leftEdgeDistance * parentWidth;
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
leftPixel = parentCenterX + this._leftEdgeDistance;
}
//Top edge
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
) {
topPixel = this._topEdgeDistance;
} else {
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
) {
topPixel = rendererHeight - this._topEdgeDistance;
} else {
if (
this._topEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
) {
topPixel = this._topEdgeDistance * rendererHeight;
}
}
// Top edge
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
topPixel = parentMinY + this._topEdgeDistance;
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
topPixel = parentMaxY + this._topEdgeDistance;
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
topPixel = parentMinY + this._topEdgeDistance * parentHeight;
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
topPixel = parentCenterY + this._topEdgeDistance;
}
//Right edge
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
) {
rightPixel = this._rightEdgeDistance;
} else {
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
) {
rightPixel = rendererWidth - this._rightEdgeDistance;
} else {
if (
this._rightEdgeAnchor ===
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
) {
rightPixel = this._rightEdgeDistance * rendererWidth;
}
}
// Right edge
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
rightPixel = parentMinX + this._rightEdgeDistance;
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
rightPixel = parentMaxX + this._rightEdgeDistance;
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
rightPixel = parentMinX + this._rightEdgeDistance * parentWidth;
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
rightPixel = parentCenterX + this._rightEdgeDistance;
}
//Bottom edge
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
) {
bottomPixel = this._bottomEdgeDistance;
} else {
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
) {
bottomPixel = rendererHeight - this._bottomEdgeDistance;
} else {
if (
this._bottomEdgeAnchor ===
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
) {
bottomPixel = this._bottomEdgeDistance * rendererHeight;
}
}
// Bottom edge
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
bottomPixel = parentMinY + this._bottomEdgeDistance;
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
bottomPixel = parentMaxY + this._bottomEdgeDistance;
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
bottomPixel = parentMinY + this._bottomEdgeDistance * parentHeight;
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
bottomPixel = parentCenterY + this._bottomEdgeDistance;
}
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
const topLeftCoord = layer.convertInverseCoords(
leftPixel,
@@ -303,27 +246,18 @@ namespace gdjs {
// Compatibility with GD <= 5.0.133
if (this._useLegacyBottomAndRightAnchors) {
//Move and resize the object according to the anchors
if (
this._rightEdgeAnchor !==
AnchorRuntimeBehavior.HorizontalAnchor.NONE
) {
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
this.owner.setWidth(right - left);
}
if (
this._bottomEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
) {
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
this.owner.setHeight(bottom - top);
}
if (
this._leftEdgeAnchor !== AnchorRuntimeBehavior.HorizontalAnchor.NONE
) {
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
this.owner.setX(
left + this.owner.getX() - this.owner.getDrawableX()
);
}
if (
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
) {
if (this._topEdgeAnchor !== VerticalAnchor.None) {
this.owner.setY(
top + this.owner.getY() - this.owner.getDrawableY()
);
@@ -333,25 +267,18 @@ namespace gdjs {
else {
// Resize if right and left anchors are set
if (
this._rightEdgeAnchor !==
AnchorRuntimeBehavior.HorizontalAnchor.NONE &&
this._leftEdgeAnchor !== AnchorRuntimeBehavior.HorizontalAnchor.NONE
this._rightEdgeAnchor !== HorizontalAnchor.None &&
this._leftEdgeAnchor !== HorizontalAnchor.None
) {
this.owner.setWidth(right - left);
this.owner.setX(left);
} else {
if (
this._leftEdgeAnchor !==
AnchorRuntimeBehavior.HorizontalAnchor.NONE
) {
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
this.owner.setX(
left + this.owner.getX() - this.owner.getDrawableX()
);
}
if (
this._rightEdgeAnchor !==
AnchorRuntimeBehavior.HorizontalAnchor.NONE
) {
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
this.owner.setX(
right +
this.owner.getX() -
@@ -362,24 +289,18 @@ namespace gdjs {
}
// Resize if top and bottom anchors are set
if (
this._bottomEdgeAnchor !==
AnchorRuntimeBehavior.VerticalAnchor.NONE &&
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
this._bottomEdgeAnchor !== VerticalAnchor.None &&
this._topEdgeAnchor !== VerticalAnchor.None
) {
this.owner.setHeight(bottom - top);
this.owner.setY(top);
} else {
if (
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
) {
if (this._topEdgeAnchor !== VerticalAnchor.None) {
this.owner.setY(
top + this.owner.getY() - this.owner.getDrawableY()
);
}
if (
this._bottomEdgeAnchor !==
AnchorRuntimeBehavior.VerticalAnchor.NONE
) {
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
this.owner.setY(
bottom +
this.owner.getY() -
@@ -393,19 +314,6 @@ namespace gdjs {
}
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
static HorizontalAnchor = {
NONE: 0,
WINDOW_LEFT: 1,
WINDOW_RIGHT: 2,
PROPORTIONAL: 3,
};
static VerticalAnchor = {
NONE: 0,
WINDOW_TOP: 1,
WINDOW_BOTTOM: 2,
PROPORTIONAL: 3,
};
}
gdjs.registerBehavior(
'AnchorBehavior::AnchorBehavior',

View File

@@ -36,6 +36,13 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
usedExtensionsWithVariablesData: [],
});
const setGameResolutionSizeAndStep = (width, height) => {
runtimeGame.setGameResolutionSize(width, height);
// This method is called by the main loop:
runtimeScene.onGameResolutionResized();
runtimeScene.renderAndStep(1000 / 60);
};
function createObject(behaviorProperties) {
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
@@ -67,13 +74,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window left (fixed)`, function () {
const object = createObject({ [objectEdge]: 1 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(500);
@@ -83,29 +87,36 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window right (fixed)`, function () {
const object = createObject({ [objectEdge]: 2 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
setGameResolutionSizeAndStep(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.equal(1500);
expect(object.getY()).to.equal(500);
expect(object.getWidth()).to.equal(10);
});
});
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window center (fixed)`, function () {
const object = createObject({ [objectEdge]: 4 });
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(1000);
expect(object.getY()).to.equal(500);
expect(object.getWidth()).to.equal(10);
});
});
it('anchors the right and left edge of object (fixed)', function () {
const object = createObject({ leftEdgeAnchor: 1, rightEdgeAnchor: 2 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(500);
@@ -114,13 +125,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
it('anchors the left edge of object (proportional)', function () {
const object = createObject({ leftEdgeAnchor: 3 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(1000);
expect(object.getY()).to.equal(500);
@@ -132,13 +140,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window top (fixed)`, function () {
const object = createObject({ [objectEdge]: 1 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(500);
@@ -148,29 +153,36 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window bottom (fixed)`, function () {
const object = createObject({ [objectEdge]: 2 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(1500);
expect(object.getWidth()).to.equal(10);
});
});
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window center (fixed)`, function () {
const object = createObject({ [objectEdge]: 4 });
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(1000);
expect(object.getWidth()).to.equal(10);
});
});
it('anchors the top and bottom edge of object (fixed)', function () {
const object = createObject({ topEdgeAnchor: 1, bottomEdgeAnchor: 2 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(500);
@@ -179,13 +191,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
it('anchors the top edge of object (proportional)', function () {
const object = createObject({ topEdgeAnchor: 3 });
runtimeGame.setGameResolutionSize(1000, 1000);
object.setPosition(500, 500);
runtimeScene.renderAndStep(1000 / 60);
runtimeGame.setGameResolutionSize(2000, 2000);
runtimeScene.renderAndStep(1000 / 60);
setGameResolutionSizeAndStep(2000, 2000);
expect(object.getX()).to.equal(500);
expect(object.getY()).to.equal(1000);

View File

@@ -52,7 +52,7 @@ module.exports = {
.setType('number');
adjustmentProperties
.getOrCreate('saturation')
.setValue('2')
.setValue('1')
.setLabel(_('Saturation (between 0 and 5)'))
.setType('number');
adjustmentProperties
@@ -77,7 +77,7 @@ module.exports = {
.setType('number');
adjustmentProperties
.getOrCreate('blue')
.setValue('0.6')
.setValue('1')
.setLabel(_('Blue (between 0 and 5)'))
.setType('number');
adjustmentProperties

View File

@@ -369,6 +369,36 @@ module.exports = {
'gdjs.multiplayerMessageManager.hasCustomMessageBeenReceived'
);
extension
.addExpressionAndConditionAndAction(
'number',
'ObjectsSynchronizationRate',
_('Objects synchronization rate'),
_(
'objects synchronization rate (between 1 and 60, default is 30 times per second)'
),
_('objects synchronization rate'),
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg'
)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(_('Sync rate'))
)
.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.setObjectsSynchronizationRate')
.setGetter('gdjs.multiplayer.getObjectsSynchronizationRate');
extension
.addCondition(
'IsPlayerHost',
@@ -392,13 +422,13 @@ module.exports = {
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.isPlayerHost');
.setFunctionName('gdjs.multiplayer.isCurrentPlayerHost');
extension
.addCondition(
'HasAnyPlayerLeft',
_('Any player has left'),
_('Check if any player has left the lobby.'),
_('Check if any player has left the lobby game.'),
_('Any player has left'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
@@ -423,7 +453,7 @@ module.exports = {
.addCondition(
'HasPlayerLeft',
_('Player has left'),
_('Check if the player has left the lobby.'),
_('Check if the player has left the lobby game.'),
_('Player _PARAM0_ has left'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
@@ -448,8 +478,10 @@ module.exports = {
extension
.addExpression(
'LastLeftPlayerNumber',
_('Last left player number'),
_('Returns the number of the player that has just left the lobby.'),
_('Player number that just left'),
_(
'Returns the player number of the player that has just left the lobby.'
),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -524,8 +556,10 @@ module.exports = {
extension
.addExpression(
'LastJoinedPlayerNumber',
_('Last joined player number'),
_('Returns the number of the player that has just joined the lobby.'),
_('Player number that just joined'),
_(
'Returns the player number of the player that has just joined the lobby.'
),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -546,6 +580,61 @@ module.exports = {
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined'
);
extension
.addCondition(
'IsMigratingHost',
_('Host is migrating'),
_(
'Check if the host is migrating, in order to adapt the game state (like pausing the game).'
),
_('Host is migrating'),
_('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.multiplayer.isMigratingHost');
extension
.addAction(
'EndLobbyWhenHostLeaves',
_('Configure lobby game to end when host leaves'),
_(
'Configure the lobby game to end when the host leaves. This will trigger the "Lobby game has just ended" condition. (Default behavior is to migrate the host)'
),
_('Configure lobby game to end when host leaves'),
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('yesorno', _('End lobby game when host leaves'), '', 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.endLobbyWhenHostLeaves');
extension
.addStrExpression(
'MessageData',

View File

@@ -145,7 +145,7 @@ namespace gdjs {
} = {};
// The number of times per second the scene data should be synchronized.
const sceneSyncDataTickRate = 1;
const sceneSyncDataSyncRate = 1;
let lastSceneSyncTimestamp = 0;
let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;
let numberOfForcedSceneUpdates = 0;
@@ -154,7 +154,7 @@ namespace gdjs {
>();
// The number of times per second the game data should be synchronized.
const gameSyncDataTickRate = 1;
const gameSyncDataSyncRate = 1;
let lastGameSyncTimestamp = 0;
let lastSentGameSyncData: GameNetworkSyncData | null = null;
let numberOfForcedGameUpdates = 0;
@@ -164,8 +164,8 @@ namespace gdjs {
// 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;
const heartbeatSyncRate = 1;
let lastHeartbeatSentTimestamp = 0;
let _playersLastRoundTripTimes: {
[playerNumber: number]: number[];
} = {};
@@ -531,7 +531,10 @@ namespace gdjs {
currentPlayerObjectOwnership === previousOwner ||
// the object is already owned by the new owner. (may have been changed by another player faster)
currentPlayerObjectOwnership === newOwner;
if (gdjs.multiplayer.isPlayerHost() && !ownershipChangeIsCoherent) {
if (
gdjs.multiplayer.isCurrentPlayerHost() &&
!ownershipChangeIsCoherent
) {
// We received an ownership change message for an object which is in an unexpected state.
// There may be some lag, and multiple ownership changes may have been sent by the other players.
// As the host, let's not change the ownership and let the player revert it.
@@ -560,7 +563,7 @@ namespace gdjs {
// If we are the host,
// so we need to relay the ownership change to others,
// and expect an acknowledgment from them.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
// We don't need to send the message to the player who sent the ownership change message.
const otherPeerIds = connectedPeerIds.filter(
@@ -738,7 +741,7 @@ namespace gdjs {
// If we are are the host,
// we need to relay the position to others except the player who sent the update message.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const otherPeerIds = connectedPeerIds.filter(
(peerId) => peerId !== messageSender
@@ -863,7 +866,10 @@ namespace gdjs {
currentPlayerVariableOwnership === previousOwner ||
// the variable is already owned by the new owner. (may have been changed by another player faster)
currentPlayerVariableOwnership === newOwner;
if (gdjs.multiplayer.isPlayerHost() && !ownershipChangeIsCoherent) {
if (
gdjs.multiplayer.isCurrentPlayerHost() &&
!ownershipChangeIsCoherent
) {
// We received an ownership change message for a variable which is in an unexpected state.
// There may be some lag, and multiple ownership changes may have been sent by the other players.
// As the host, let's not change the ownership and let the player revert it.
@@ -892,7 +898,7 @@ namespace gdjs {
// If we are the host,
// we need to relay the ownership change to others,
// and expect an acknowledgment from them.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
// We don't need to send the message to the player who sent the ownership change message.
const otherPeerIds = connectedPeerIds.filter(
@@ -1336,7 +1342,7 @@ namespace gdjs {
// If we are the host, we need to relay the destruction to others.
// And expect an acknowledgment from everyone else as well.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
// We don't need to send the message to the player who sent the destroy message.
const otherPeerIds = connectedPeerIds.filter(
@@ -1429,7 +1435,7 @@ namespace gdjs {
// If we are the host, we can consider this messaged as received
// and add it to the list of custom messages to process on top of the messages received.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const messagesList = gdjs.multiplayerPeerJsHelper.getOrCreateMessagesList(
messageName
);
@@ -1592,7 +1598,7 @@ namespace gdjs {
// If we are the host,
// so we need to relay the message to others.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
// In the case of custom messages, we relay the message to all players, including the sender.
// This allows the sender to process it the same way others would, when they receive the event.
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
@@ -1650,7 +1656,7 @@ namespace gdjs {
const hasSceneBeenSyncedRecently = () => {
return (
getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataTickRate
getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataSyncRate
);
};
@@ -1664,6 +1670,7 @@ namespace gdjs {
const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
isHost: gdjs.multiplayer.isCurrentPlayerHost(),
});
if (!sceneNetworkSyncData) {
return;
@@ -1737,7 +1744,7 @@ namespace gdjs {
// 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()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
// We don't need to send the message to the player who sent the update message.
const otherPeerIds = connectedPeerIds.filter(
@@ -1814,7 +1821,7 @@ namespace gdjs {
};
const hasGameBeenSyncedRecently = () => {
return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataTickRate;
return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataSyncRate;
};
const handleUpdateGameMessagesToSend = (
@@ -1827,6 +1834,7 @@ namespace gdjs {
const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
isHost: gdjs.multiplayer.isCurrentPlayerHost(),
});
if (!gameNetworkSyncData) {
return;
@@ -1888,7 +1896,7 @@ namespace gdjs {
// If we are are the host,
// we need to relay the game update to others except the player who sent the update message.
if (gdjs.multiplayer.isPlayerHost()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
// We don't need to send the message to the player who sent the update message.
const otherPeerIds = connectedPeerIds.filter(
@@ -1937,9 +1945,10 @@ namespace gdjs {
messageName: string;
messageData: any;
} => {
// 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.
// If we create the heartbeat meassage, we are the host,
// Ensure our player number is correctly set when the first heartbeat is sent.
_playersInfo[gdjs.multiplayer.getCurrentPlayerNumber()] = {
ping: 0, // we are the host, so we don't need to compute the ping.
playerId: gdjs.playerAuthentication.getUserId(),
username: gdjs.playerAuthentication.getUsername(),
};
@@ -1976,15 +1985,15 @@ namespace gdjs {
};
const hasSentHeartbeatRecently = () => {
return (
!!lastHeartbeatTimestamp &&
getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate
!!lastHeartbeatSentTimestamp &&
getTimeNow() - lastHeartbeatSentTimestamp < 1000 / heartbeatSyncRate
);
};
const handleHeartbeatsToSend = () => {
// Only host sends heartbeats to all players regularly:
// - it allows them to send a heartbeat back immediately so that the host can compute the ping.
// - it allows to pass along the pings of all players to all players.
if (!gdjs.multiplayer.isPlayerHost()) {
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
return;
}
@@ -1997,7 +2006,7 @@ namespace gdjs {
const { messageName, messageData } = createHeartbeatMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
lastHeartbeatTimestamp = getTimeNow();
lastHeartbeatSentTimestamp = getTimeNow();
};
const handleHeartbeatsReceived = () => {
@@ -2024,7 +2033,7 @@ namespace gdjs {
// 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()) {
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const currentlyKnownPlayerNumbers = Object.keys(
_playersInfo
@@ -2134,12 +2143,20 @@ namespace gdjs {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const { messageName, messageData } = createHeartbeatMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
lastHeartbeatTimestamp = getTimeNow();
lastHeartbeatSentTimestamp = getTimeNow();
}
});
});
};
const hasReceivedHeartbeatFromPlayer = (playerNumber: number) => {
// Consider that a player has sent a heartbeat if we have been able to calculate
// at least one round trip time for them.
const playerLastRoundTripTimes =
_playersLastRoundTripTimes[playerNumber] || [];
return playerLastRoundTripTimes.length > 0;
};
const getPlayerPing = (playerNumber: number) => {
const playerInfo = _playersInfo[playerNumber];
if (!playerInfo) {
@@ -2153,7 +2170,15 @@ namespace gdjs {
return getPlayerPing(currentPlayerNumber);
};
const markPlayerAsDisconnected = (playerNumber: number) => {
const markPlayerAsDisconnected = ({
runtimeScene,
playerNumber,
peerId,
}: {
runtimeScene: gdjs.RuntimeScene;
playerNumber: number;
peerId?: string;
}) => {
logger.info(`Marking player ${playerNumber} as disconnected.`);
_playerNumbersWhoJustLeft.push(playerNumber);
// Temporarily save the username in another variable to be used for the notification,
@@ -2161,23 +2186,31 @@ namespace gdjs {
_temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(
playerNumber
);
clearPlayerTempData(playerNumber);
// If Player 1 has disconnected, just end the game.
if (playerNumber === 1) {
logger.info('Host has disconnected, ending the game.');
clearAllMessagesTempData();
gdjs.multiplayer.handleLobbyGameEnded();
return;
// If Host has disconnected, either switch host or stop the game.
if (peerId && peerId === gdjs.multiplayer.hostPeerId) {
const shouldEndLobbyGame = gdjs.multiplayer.shouldEndLobbyWhenHostLeaves();
if (shouldEndLobbyGame) {
logger.info('Host has disconnected, ending the game.');
clearAllMessagesTempData();
gdjs.multiplayer.handleLobbyGameEnded();
} else {
logger.info('Host has disconnected, switching host.');
gdjs.multiplayer.handleHostDisconnected({ runtimeScene });
return;
}
}
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()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const { messageName, messageData } = createHeartbeatMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
lastHeartbeatTimestamp = getTimeNow();
lastHeartbeatSentTimestamp = getTimeNow();
}
};
@@ -2200,7 +2233,10 @@ namespace gdjs {
}
// We rely on the p2p helper to know who has disconnected.
const justDisconnectedPlayerNumbers: number[] = [];
const justDisconnectedPlayers: {
playerNumber: number;
peerId: string;
}[] = [];
const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
if (justDisconnectedPeers.length) {
@@ -2212,14 +2248,17 @@ namespace gdjs {
return;
}
logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);
justDisconnectedPlayerNumbers.push(disconnectedPlayerNumber);
justDisconnectedPlayers.push({
playerNumber: disconnectedPlayerNumber,
peerId: disconnectedPeer,
});
}
}
for (const playerNumber of justDisconnectedPlayerNumbers) {
for (const { playerNumber, peerId } of justDisconnectedPlayers) {
// 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()) {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
const instances = runtimeScene.getAdhocListOfAllInstances();
for (const instance of instances) {
const behavior = instance.getBehavior(
@@ -2243,7 +2282,7 @@ namespace gdjs {
}
}
markPlayerAsDisconnected(playerNumber);
markPlayerAsDisconnected({ runtimeScene, playerNumber, peerId });
}
};
@@ -2303,6 +2342,10 @@ namespace gdjs {
return _playersInfo[playerNumber] !== undefined;
};
const getPlayersInfo = () => {
return _playersInfo;
};
const endGameMessageName = '#endGame';
const createEndGameMessage = (): {
messageName: string;
@@ -2315,7 +2358,7 @@ namespace gdjs {
};
const sendEndGameMessage = () => {
// Only the host can end the game.
if (!gdjs.multiplayer.isPlayerHost()) {
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
return;
}
@@ -2327,8 +2370,8 @@ namespace gdjs {
sendDataTo(connectedPeerIds, messageName, messageData);
};
const handleEndGameMessages = () => {
if (gdjs.multiplayer.isPlayerHost()) {
const handleEndGameMessagesReceived = () => {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
// Only other players need to react to the end game message.
return;
}
@@ -2348,6 +2391,50 @@ namespace gdjs {
gdjs.multiplayer.handleLobbyGameEnded();
};
const resumeGameMessageName = '#resumeGame';
const createResumeGameMessage = (): {
messageName: string;
messageData: any;
} => {
return {
messageName: resumeGameMessageName,
messageData: {},
};
};
const sendResumeGameMessage = () => {
// Only the host can inform others that the game is resuming.
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
return;
}
debugLogger.info(`Sending resumeGame message.`);
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const { messageName, messageData } = createResumeGameMessage();
sendDataTo(connectedPeerIds, messageName, messageData);
};
const handleResumeGameMessagesReceived = (
runtimeScene: gdjs.RuntimeScene
) => {
if (gdjs.multiplayer.isCurrentPlayerHost()) {
// Only other players need to react to resume game message.
return;
}
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
const resumeGameMessagesList = p2pMessagesMap.get(resumeGameMessageName);
if (!resumeGameMessagesList) {
return; // No resume game message received.
}
const messages = resumeGameMessagesList.getMessages();
if (!messages.length) return; // No messages to process.
logger.info(`Received resumeGame message.`);
gdjs.multiplayer.resumeGame(runtimeScene);
};
const clearAllMessagesTempData = () => {
_playersLastRoundTripTimes = {};
_playersInfo = {};
@@ -2409,6 +2496,7 @@ namespace gdjs {
// Heartbeats.
handleHeartbeatsToSend,
handleHeartbeatsReceived,
hasReceivedHeartbeatFromPlayer,
// Pings & usernames.
getPlayerPing,
getCurrentPlayerPing,
@@ -2419,12 +2507,14 @@ namespace gdjs {
getConnectedPlayers,
getNumberOfConnectedPlayers,
isPlayerConnected,
getPlayersInfo,
// Leaving players.
hasAnyPlayerJustLeft,
hasPlayerJustLeft,
getPlayersWhoJustLeft,
getLatestPlayerWhoJustLeft,
removePlayerWhoJustLeft,
markPlayerAsDisconnected,
// Joining players.
hasAnyPlayerJustJoined,
hasPlayerJustJoined,
@@ -2433,8 +2523,11 @@ namespace gdjs {
removePlayerWhoJustJoined,
// End game.
sendEndGameMessage,
handleEndGameMessages,
handleEndGameMessagesReceived,
clearAllMessagesTempData,
// Resume game after migration.
sendResumeGameMessage,
handleResumeGameMessagesReceived,
};
};

View File

@@ -398,11 +398,12 @@ namespace gdjs {
export const displayErrorNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
showNotification(
showNotification({
runtimeScene,
'An error occurred while displaying the game lobbies, please try again.',
'error'
);
content:
'An error occurred while displaying the game lobbies, please try again.',
type: 'error',
});
};
/**
@@ -412,7 +413,11 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene,
playerName: string
) {
showNotification(runtimeScene, `${playerName} left.`, 'warning');
showNotification({
runtimeScene,
content: `${playerName} left.`,
type: 'warning',
});
};
/**
@@ -422,7 +427,11 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene,
playerName: string
) {
showNotification(runtimeScene, `${playerName} joined.`, 'success');
showNotification({
runtimeScene,
content: `${playerName} joined.`,
type: 'success',
});
};
/**
@@ -431,11 +440,48 @@ namespace gdjs {
export const displayConnectionErrorNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
showNotification(
showNotification({
runtimeScene,
'Could not connect to other players.',
'error'
);
content: 'Could not connect to other players.',
type: 'error',
});
};
/**
* Create, display, and hide a notification when a player leaves the game.
*/
export const displayHostMigrationNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
showNotification({
runtimeScene,
content: `Migrating host...`,
type: 'warning',
id: 'migrating-host',
persist: true,
});
};
export const showHostMigrationFinishedNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
removeNotificationAndShiftOthers('migrating-host');
showNotification({
runtimeScene,
content: `Host migrated!`,
type: 'success',
});
};
export const showHostMigrationFailedNotification = function (
runtimeScene: gdjs.RuntimeScene
) {
removeNotificationAndShiftOthers('migrating-host');
showNotification({
runtimeScene,
content: `Host migration failed.`,
type: 'error',
});
};
const removeNotificationAndShiftOthers = function (
@@ -443,7 +489,9 @@ namespace gdjs {
) {
const notification = document.getElementById(notificationContainerId);
if (!notification) {
logger.error('Notification not found.');
logger.warn(
`Notification ${notificationContainerId} not found. skipping`
);
return;
}
const index = notificationContainerIds.indexOf(notificationContainerId);
@@ -452,8 +500,8 @@ namespace gdjs {
}
notification.remove();
// Shift the other notifications up.
for (let i = 0; i < notificationContainerIds.length; i++) {
// Shift the notifications that are below the one that was removed up.
for (let i = index; i < notificationContainerIds.length; i++) {
const notification = document.getElementById(
notificationContainerIds[i]
);
@@ -468,11 +516,19 @@ namespace gdjs {
/**
* Helper to show a notification to the user, that disappears automatically.
*/
export const showNotification = function (
runtimeScene: gdjs.RuntimeScene,
content: string,
type: 'success' | 'warning' | 'error'
) {
export const showNotification = function ({
runtimeScene,
content,
type,
id,
persist,
}: {
runtimeScene: gdjs.RuntimeScene;
content: string;
type: 'success' | 'warning' | 'error';
id?: string;
persist?: boolean;
}) {
// 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) {
@@ -486,7 +542,8 @@ namespace gdjs {
}
// We generate a random ID for the notification, so they can stack.
const id = `notification-${Math.random().toString(36).substring(7)}`;
const notificationId =
id || `notification-${Math.random().toString(36).substring(7)}`;
const domContainer = runtimeScene
.getGame()
@@ -498,7 +555,7 @@ namespace gdjs {
}
const divContainer = document.createElement('div');
divContainer.id = id;
divContainer.id = notificationId;
divContainer.style.position = 'absolute';
divContainer.style.pointerEvents = 'all';
divContainer.style.backgroundColor =
@@ -544,10 +601,14 @@ namespace gdjs {
divContainer.appendChild(loggedText);
domContainer.appendChild(divContainer);
notificationContainerIds.push(id);
notificationContainerIds.push(notificationId);
if (persist) {
return;
}
const animationTime = 700;
const notificationTime = 5000;
const notificationTime = 3000;
setTimeout(() => {
try {
divContainer.animate(
@@ -566,7 +627,7 @@ namespace gdjs {
}, notificationTime);
// Use timeout because onanimationend listener does not work.
setTimeout(() => {
removeNotificationAndShiftOthers(id);
removeNotificationAndShiftOthers(notificationId);
}, notificationTime + animationTime);
};

View File

@@ -24,48 +24,46 @@ namespace gdjs {
actionOnPlayerDisconnect: string;
// The last time the object has been synchronized.
// This is to avoid synchronizing the object too often, see _objectMaxTickRate.
// This is to avoid synchronizing the object too often, see _objectMaxSyncRate.
_lastObjectSyncTimestamp: number = 0;
// The number of times per second the object should be synchronized if it keeps changing.
_objectMaxTickRate: number = 60;
// The last time the basic object info has been synchronized.
_lastBasicObjectSyncTimestamp: number = 0;
// The number of times per second the object basic info should be synchronized when it doesn't change.
_objectBasicInfoTickRate: number = 5;
_objectBasicInfoSyncRate: number = 5;
// The last data sent to synchronize the basic info of the object.
_lastSentBasicObjectSyncData: BasicObjectNetworkSyncData | undefined;
// When we know that the basic info of the object has been updated, we can force sending them
// on the max tickrate for a number of times to ensure they are received, without the need of an acknowledgment.
// on the max SyncRate for a number of times to ensure they are received, without the need of an acknowledgment.
_numberOfForcedBasicObjectUpdates: number = 0;
// The last time the variables have been synchronized.
_lastVariablesSyncTimestamp: number = 0;
// The number of times per second the variables should be synchronized.
_variablesTickRate: number = 1;
_variablesSyncRate: number = 1;
// The last data sent to synchronize the variables.
_lastSentVariableSyncData: VariableNetworkSyncData[] | undefined;
// When we know that the variables have been updated, we can force sending them
// on the same tickrate as the object update for a number of times
// on the same syncRate as the object update for a number of times
// to ensure they are received, without the need of an acknowledgment.
_numberOfForcedVariablesUpdates: number = 0;
// The last time the effects have been synchronized.
_lastEffectsSyncTimestamp: number = 0;
// The number of times per second the effects should be synchronized.
_effectsTickRate: number = 1;
_effectsSyncRate: number = 1;
// The last data sent to synchronize the effects.
_lastSentEffectSyncData:
| { [effectName: string]: EffectNetworkSyncData }
| undefined;
// When we know that the effects have been updated, we can force sending them
// on the same tickrate as the object update for a number of times
// on the same syncRate as the object update for a number of times
// to ensure they are received, without the need of an acknowledgment.
_numberOfForcedEffectsUpdates: number = 0;
// To avoid seeing too many logs.
_lastLogTimestamp: number = 0;
_logTickRate: number = 1;
_logSyncRate: number = 1;
// Clock to be incremented every time we send a message, to ensure they are ordered
// and old messages are ignored.
_clock: number = 0;
@@ -131,35 +129,35 @@ namespace gdjs {
}
private _hasObjectBeenSyncedWithinMaxRate() {
const objectMaxSyncRate = gdjs.multiplayer.getObjectsSynchronizationRate();
return (
getTimeNow() - this._lastObjectSyncTimestamp <
1000 / this._objectMaxTickRate
getTimeNow() - this._lastObjectSyncTimestamp < 1000 / objectMaxSyncRate
);
}
private _hasObjectBasicInfoBeenSyncedRecently() {
return (
getTimeNow() - this._lastBasicObjectSyncTimestamp <
1000 / this._objectBasicInfoTickRate
1000 / this._objectBasicInfoSyncRate
);
}
private _haveVariablesBeenSyncedRecently() {
return (
getTimeNow() - this._lastVariablesSyncTimestamp <
1000 / this._variablesTickRate
1000 / this._variablesSyncRate
);
}
private _haveEffectsBeenSyncedRecently() {
return (
getTimeNow() - this._lastEffectsSyncTimestamp <
1000 / this._effectsTickRate
1000 / this._effectsSyncRate
);
}
// private _logToConsoleWithThrottle(message: string) {
// if (getTimeNow() - this._lastLogTimestamp > 1000 / this._logTickRate) {
// if (getTimeNow() - this._lastLogTimestamp > 1000 / this._logSyncRate) {
// logger.info(message);
// this._lastLogTimestamp = getTimeNow();
// }
@@ -415,7 +413,10 @@ namespace gdjs {
// For destruction of objects, we allow the host to destroy the object even if it is not the owner.
// This is particularly helpful when a player disconnects, so the host can destroy the object they were owning.
if (!this._isOwnerAsPlayerOrHost() && !gdjs.multiplayer.isPlayerHost()) {
if (
!this._isOwnerAsPlayerOrHost() &&
!gdjs.multiplayer.isCurrentPlayerHost()
) {
return;
}

View File

@@ -1,6 +1,82 @@
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
type LobbyChangeHostRequest = {
lobbyId: string;
gameId: string;
peerId: string;
playerId: string;
ping: number;
createdAt: number;
ttl: number;
newLobbyId?: string;
newHostPeerId?: string;
newPlayers?: {
playerNumber: number;
playerId: string;
}[];
};
const getTimeNow =
window.performance && typeof window.performance.now === 'function'
? window.performance.now.bind(window.performance)
: Date.now;
const fetchAsPlayer = async ({
relativeUrl,
method,
body,
dev,
}: {
relativeUrl: string;
method: 'GET' | 'POST';
body?: string;
dev: boolean;
}) => {
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (!playerId || !playerToken) {
logger.warn('Cannot fetch as a player if the player is not connected.');
throw new Error(
'Cannot fetch as a player if the player is not connected.'
);
}
const rootApi = dev
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const url = new URL(`${rootApi}${relativeUrl}`);
url.searchParams.set('playerId', playerId);
const formattedUrl = url.toString();
const headers = {
'Content-Type': 'application/json',
Authorization: `player-game-token ${playerToken}`,
};
const response = await fetch(formattedUrl, {
method,
headers,
body,
});
if (!response.ok) {
throw new Error(
`Error while fetching as a player: ${response.status} ${response.statusText}`
);
}
// Response can either be 'OK' or a JSON object. Get the content before trying to parse it.
const responseText = await response.text();
if (responseText === 'OK') {
return;
}
try {
return JSON.parse(responseText);
} catch (error) {
throw new Error(`Error while parsing the response: ${error}`);
}
};
export namespace multiplayer {
/** Set to true in testing to avoid relying on the multiplayer extension. */
export let disableMultiplayerForTesting = false;
@@ -17,20 +93,38 @@ namespace gdjs {
let _lobbyId: string | null = null;
let _connectionId: string | null = null;
let _shouldEndLobbyWhenHostLeaves = false;
let _lobbyChangeHostRequest: LobbyChangeHostRequest | null = null;
let _lobbyChangeHostRequestInitiatedAt: number | null = null;
let _isChangingHost = false;
let _lobbyNewHostPickedAt: number | null = null;
// Communication methods.
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
let _websocket: WebSocket | null = null;
let _websocketHeartbeatInterval: NodeJS.Timeout | null = null;
let _lobbyHeartbeatInterval: NodeJS.Timeout | null = null;
let _websocketHeartbeatIntervalFunction: NodeJS.Timeout | null = null;
let _lobbyHeartbeatIntervalFunction: NodeJS.Timeout | null = null;
const DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL = 10000;
const DEFAULT_LOBBY_HEARTBEAT_INTERVAL = 30000;
const DEFAULT_COUNTDOWN_SECONDS_TO_START = 5;
let currentLobbyHeartbeatInterval = DEFAULT_LOBBY_HEARTBEAT_INTERVAL;
const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL = 1000;
// 10 seconds to be safe, but the backend will answer in less.
const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT = 10000;
const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL = 1000;
const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT = 10000;
let _resumeTimeout: NodeJS.Timeout | null = null;
const DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT = 12000;
export const DEFAULT_OBJECT_MAX_SYNC_RATE = 30;
// The number of times per second an object should be synchronized if it keeps changing.
export let _objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;
// Save if we are on dev environment so we don't need to use the runtimeGame every time.
let isUsingGDevelopDevelopmentEnvironment = false;
export let playerNumber: number | null = null;
export let hostPeerId: string | null = null;
gdjs.registerRuntimeScenePreEventsCallback(
(runtimeScene: gdjs.RuntimeScene) => {
@@ -88,6 +182,11 @@ namespace gdjs {
// Then look at the heartbeats received to know if a new player has joined/left.
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
gdjs.multiplayerMessageManager.handleEndGameMessagesReceived();
gdjs.multiplayerMessageManager.handleResumeGameMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
runtimeScene
);
@@ -161,6 +260,19 @@ namespace gdjs {
return url.toString();
};
export const setObjectsSynchronizationRate = (rate: number) => {
if (rate < 1 || rate > 60) {
logger.warn(
`Invalid rate ${rate} for object synchronization. Defaulting to ${DEFAULT_OBJECT_MAX_SYNC_RATE}.`
);
_objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;
} else {
_objectMaxSyncRate = rate;
}
};
export const getObjectsSynchronizationRate = () => _objectMaxSyncRate;
/**
* Returns true if the game has just started,
* useful to switch to the game scene.
@@ -181,7 +293,7 @@ namespace gdjs {
/**
* Returns the number of players in the lobby.
*/
export const getPlayersInLobbyCount = () => {
export const getPlayersInLobbyCount = (): number => {
// 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();
@@ -190,7 +302,7 @@ namespace gdjs {
/**
* Returns true if the player at this position is connected to the lobby.
*/
export const isPlayerConnected = (playerNumber: number) => {
export const isPlayerConnected = (playerNumber: number): boolean => {
return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);
};
@@ -199,29 +311,52 @@ namespace gdjs {
* Return 0 if the player is not in the lobby.
* Returns 1, 2, 3, ... if the player is in the lobby.
*/
export const getCurrentPlayerNumber = () => {
export const getCurrentPlayerNumber = (): number => {
return playerNumber || 0;
};
/**
* Returns true if the player is the host in the lobby. Here, player 1.
* Returns true if the player is the host in the lobby.
* This can change during the game.
*/
export const isPlayerHost = () => {
return playerNumber === 1;
export const isCurrentPlayerHost = (): boolean => {
return (
!!hostPeerId &&
hostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()
);
};
/**
* Returns true if the host left and the game is either:
* - picking a new host
* - waiting for everyone to connect to the new host
*/
export const isMigratingHost = (): boolean => {
return !!_isChangingHost;
};
/**
* If this is set, instead of migrating the host, the lobby will end when the host leaves.
*/
export const endLobbyWhenHostLeaves = (enable: boolean) => {
_shouldEndLobbyWhenHostLeaves = enable;
};
export const shouldEndLobbyWhenHostLeaves = () =>
_shouldEndLobbyWhenHostLeaves;
/**
* 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) => {
export const getPlayerUsername = (playerNumber: number): string => {
return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);
};
/**
* Returns the player username of the current player in the lobby.
*/
export const getCurrentPlayerUsername = () => {
export const getCurrentPlayerUsername = (): string => {
const currentPlayerNumber = getCurrentPlayerNumber();
return getPlayerUsername(currentPlayerNumber);
};
@@ -241,7 +376,12 @@ namespace gdjs {
// 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();
if (
isCurrentPlayerHost() &&
isReadyToSendOrReceiveGameUpdateMessages()
) {
sendHeartbeatToBackend();
}
}
};
@@ -253,6 +393,15 @@ namespace gdjs {
runtimeScene,
playerUsername
);
// We also send a heartbeat to the backend right away, 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.
if (
isCurrentPlayerHost() &&
isReadyToSendOrReceiveGameUpdateMessages()
) {
sendHeartbeatToBackend();
}
}
// 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,
@@ -310,6 +459,7 @@ namespace gdjs {
_websocket.close();
_connectionId = null;
playerNumber = null;
hostPeerId = null;
_lobbyId = null;
_websocket = null;
}
@@ -339,7 +489,7 @@ namespace gdjs {
_websocket.onopen = () => {
logger.info('Connected to the lobby.');
// Register a heartbeat to keep the connection alive.
_websocketHeartbeatInterval = setInterval(() => {
_websocketHeartbeatIntervalFunction = setInterval(() => {
if (_websocket) {
_websocket.send(
JSON.stringify({
@@ -413,23 +563,21 @@ namespace gdjs {
case 'gameCountdownStarted': {
const messageData = messageContent.data;
const compressionMethod = messageData.compressionMethod || 'none';
const secondsToStart =
messageData.secondsToStart ||
DEFAULT_COUNTDOWN_SECONDS_TO_START;
handleGameCountdownStartedEvent({
runtimeScene,
compressionMethod,
secondsToStart,
});
break;
}
case 'gameStarted': {
const messageData = messageContent.data;
const heartbeatInterval =
currentLobbyHeartbeatInterval =
messageData.heartbeatInterval ||
DEFAULT_LOBBY_HEARTBEAT_INTERVAL;
handleGameStartedEvent({ runtimeScene, heartbeatInterval });
handleGameStartedEvent({
runtimeScene,
});
break;
}
case 'peerId': {
@@ -452,14 +600,14 @@ namespace gdjs {
}
};
_websocket.onclose = () => {
logger.info(
'Disconnected from the lobby. Either manually or game started.'
);
if (!_isLobbyGameRunning) {
logger.info('Disconnected from the lobby.');
}
_connectionId = null;
_websocket = null;
if (_websocketHeartbeatInterval) {
clearInterval(_websocketHeartbeatInterval);
if (_websocketHeartbeatIntervalFunction) {
clearInterval(_websocketHeartbeatIntervalFunction);
}
// If the game is running, then all good.
@@ -576,6 +724,7 @@ namespace gdjs {
}
_connectionId = null;
playerNumber = null;
hostPeerId = null;
_lobbyId = null;
_websocket = null;
};
@@ -613,15 +762,14 @@ namespace gdjs {
const handleGameCountdownStartedEvent = function ({
runtimeScene,
compressionMethod,
secondsToStart,
}: {
runtimeScene: gdjs.RuntimeScene;
compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;
secondsToStart: number;
}) {
gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);
// When the countdown starts, if we are player number 1, then send the peerId to others so they can connect via P2P.
// When the countdown starts, if we are player number 1, we are chosen as the host.
// We then send the peerId to others so they can connect via P2P.
if (getCurrentPlayerNumber() === 1) {
sendPeerId();
}
@@ -639,7 +787,6 @@ namespace gdjs {
lobbiesIframe.contentWindow.postMessage(
{
id: 'gameCountdownStarted',
secondsToStart,
},
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
);
@@ -652,43 +799,34 @@ 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) {
if (!gameId || !_lobbyId) {
logger.error(
'Cannot keep the lobby playing without the game ID or player ID.'
'Cannot keep the lobby playing without the game ID or lobby 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 heartbeatRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
const players = gdjs.multiplayerMessageManager.getConnectedPlayers();
try {
await fetch(heartbeatUrl, {
await fetchAsPlayer({
relativeUrl: heartbeatRelativeUrl,
method: 'POST',
headers,
body: JSON.stringify({
players,
}),
dev: isUsingGDevelopDevelopmentEnvironment,
});
} catch (error) {
logger.error('Error while sending heartbeat, retrying:', error);
try {
await fetch(heartbeatUrl, {
await fetchAsPlayer({
relativeUrl: heartbeatRelativeUrl,
method: 'POST',
headers,
body: JSON.stringify({
players,
}),
dev: isUsingGDevelopDevelopmentEnvironment,
});
} catch (error) {
logger.error(
@@ -705,16 +843,14 @@ namespace gdjs {
*/
const handleGameStartedEvent = function ({
runtimeScene,
heartbeatInterval,
}: {
runtimeScene: gdjs.RuntimeScene;
heartbeatInterval: number;
}) {
// It is possible the connection to other players didn't work.
// If that's the case, show an error message and leave the lobby.
// If we are the host, still start the game, as this allows a player to test the game alone.
const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();
if (!isPlayerHost() && allConnectedPeers.length === 0) {
if (!isCurrentPlayerHost() && allConnectedPeers.length === 0) {
gdjs.multiplayerComponents.displayConnectionErrorNotification(
runtimeScene
);
@@ -726,10 +862,10 @@ namespace gdjs {
}
// If we are the host, start pinging the backend to let it know the lobby is running.
if (isPlayerHost()) {
_lobbyHeartbeatInterval = setInterval(async () => {
if (isCurrentPlayerHost()) {
_lobbyHeartbeatIntervalFunction = setInterval(async () => {
await sendHeartbeatToBackend();
}, heartbeatInterval);
}, currentLobbyHeartbeatInterval);
}
// If we are connected to players, then the game can start.
@@ -757,9 +893,11 @@ namespace gdjs {
_isLobbyGameRunning = false;
_lobbyId = null;
playerNumber = null;
hostPeerId = null;
_isReadyToSendOrReceiveGameUpdateMessages = false;
if (_lobbyHeartbeatInterval) {
clearInterval(_lobbyHeartbeatInterval);
if (_lobbyHeartbeatIntervalFunction) {
clearInterval(_lobbyHeartbeatIntervalFunction);
_lobbyHeartbeatIntervalFunction = null;
}
// Disconnect from any P2P connections.
@@ -795,6 +933,7 @@ namespace gdjs {
return;
}
hostPeerId = peerId;
gdjs.multiplayerPeerJsHelper.connect(peerId);
};
@@ -878,10 +1017,315 @@ namespace gdjs {
action: 'updateConnection',
connectionType: 'lobby',
status: 'connected',
peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),
})
);
};
const clearChangeHostRequestData = function (
runtimeScene: gdjs.RuntimeScene
) {
_lobbyChangeHostRequest = null;
_lobbyChangeHostRequestInitiatedAt = null;
_lobbyNewHostPickedAt = null;
if (_resumeTimeout) {
clearTimeout(_resumeTimeout);
_resumeTimeout = null;
}
_isChangingHost = false;
if (hostPeerId) {
gdjs.multiplayerComponents.showHostMigrationFinishedNotification(
runtimeScene
);
} else {
gdjs.multiplayerComponents.showHostMigrationFailedNotification(
runtimeScene
);
}
};
export const resumeGame = async function (runtimeScene: gdjs.RuntimeScene) {
if (isCurrentPlayerHost()) {
// Send message to other players to indicate the game is resuming.
gdjs.multiplayerMessageManager.sendResumeGameMessage();
// Start sending heartbeats to the backend.
await sendHeartbeatToBackend();
_lobbyHeartbeatIntervalFunction = setInterval(async () => {
await sendHeartbeatToBackend();
}, currentLobbyHeartbeatInterval);
}
// Migration is finished.
clearChangeHostRequestData(runtimeScene);
};
/**
* When a host is being changed, multiple cases can happen:
* - We are the new host and the only one in the lobby. Unpause the game right away.
* - We are the new host and there are other players in the new lobby. Wait for them to connect:
* - if they are all connected, unpause the game.
* - if we reach a timeout, a player may have disconnected at the same time, unpause the game.
* - We are not the new host. Connect to the new host peerId.
* - If we cannot connect, leave the lobby.
* - when we receive a message to unpause the game, unpause it.
* - if we reach a timeout without the message, leave the lobby, something wrong happened.
*/
const checkHostChangeRequestRegularly = async function ({
runtimeScene,
}: {
runtimeScene: gdjs.RuntimeScene;
}) {
if (!_lobbyChangeHostRequest || !_lobbyChangeHostRequestInitiatedAt) {
return;
}
// Refresh the request to get the latest information.
try {
const changeHostRelativeUrl = `/play/game/${
_lobbyChangeHostRequest.gameId
}/public-lobby/${
_lobbyChangeHostRequest.lobbyId
}/lobby-change-host-request?peerId=${gdjs.multiplayerPeerJsHelper.getCurrentId()}`;
const lobbyChangeHostRequest = await fetchAsPlayer({
relativeUrl: changeHostRelativeUrl,
method: 'GET',
dev: isUsingGDevelopDevelopmentEnvironment,
});
_lobbyChangeHostRequest = lobbyChangeHostRequest;
} catch (error) {
logger.error(
'Error while trying to retrieve the lobby change host request:',
error
);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
return;
}
if (!_lobbyChangeHostRequest) {
throw new Error('No lobby change host request received.');
}
const newHostPeerId = _lobbyChangeHostRequest.newHostPeerId;
if (!newHostPeerId) {
logger.info('No new host picked yet.');
if (
getTimeNow() - _lobbyChangeHostRequestInitiatedAt >
DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT
) {
logger.error(
'Timeout while waiting for the lobby host change. Giving up.'
);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
return;
}
logger.info('Retrying...');
setTimeout(() => {
checkHostChangeRequestRegularly({ runtimeScene });
}, DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL);
return;
}
try {
const newLobbyId = _lobbyChangeHostRequest.newLobbyId;
const newPlayers = _lobbyChangeHostRequest.newPlayers;
if (!newLobbyId || !newPlayers) {
logger.error(
'Change host request is incomplete. Cannot change host.'
);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
return;
}
hostPeerId = newHostPeerId;
_lobbyNewHostPickedAt = getTimeNow();
_lobbyId = newLobbyId;
if (newHostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()) {
logger.info(
`We are the new host. Switching to lobby ${newLobbyId} and awaiting for ${
newPlayers.length - 1
} player(s) to connect.`
);
await checkExpectedConnectedPlayersRegularly({
runtimeScene,
});
} else {
logger.info(
`Connecting to new host and switching lobby to ${newLobbyId}.`
);
gdjs.multiplayerPeerJsHelper.connect(newHostPeerId);
_resumeTimeout = setTimeout(() => {
logger.error(
'Timeout while waiting for the game to resume. Leaving the lobby.'
);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
}, DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT);
}
} catch (error) {
logger.error('Error while trying to change host:', error);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
}
};
/**
* Helper for the new host, to check if they have all the expected players connected.
*/
const checkExpectedConnectedPlayersRegularly = async function ({
runtimeScene,
}: {
runtimeScene: gdjs.RuntimeScene;
}) {
if (!_lobbyChangeHostRequest) {
return;
}
const expectedNewPlayers = _lobbyChangeHostRequest.newPlayers;
if (!expectedNewPlayers) {
logger.error('No expected players in the lobby change host request.');
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
return;
}
const expectedNewOtherPlayerNumbers = expectedNewPlayers.map(
(player) => player.playerNumber
);
// First look for players who left during the migration.
const playerNumbersConnectedBeforeMigration = gdjs.multiplayerMessageManager
.getConnectedPlayers()
.map((player) => player.playerNumber);
const playerNumbersWhoLeftDuringMigration = playerNumbersConnectedBeforeMigration.filter(
(playerNumberBeforeMigration) =>
!expectedNewOtherPlayerNumbers.includes(playerNumberBeforeMigration)
);
playerNumbersWhoLeftDuringMigration.map((playerNumberWhoLeft) => {
logger.info(
`Player ${playerNumberWhoLeft} left during the host migration. Marking as disconnected.`
);
gdjs.multiplayerMessageManager.markPlayerAsDisconnected({
runtimeScene,
playerNumber: playerNumberWhoLeft,
});
});
// Then check if all expected players are connected.
const playerNumbersWhoDidNotConnect = expectedNewOtherPlayerNumbers.filter(
(otherPlayerNumber) =>
otherPlayerNumber !== playerNumber && // We don't look for ourselves
!gdjs.multiplayerMessageManager.hasReceivedHeartbeatFromPlayer(
otherPlayerNumber
)
);
if (playerNumbersWhoDidNotConnect.length === 0) {
logger.info('All expected players are connected. Resuming the game.');
await resumeGame(runtimeScene);
return;
}
if (
_lobbyNewHostPickedAt &&
getTimeNow() - _lobbyNewHostPickedAt >
DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT &&
playerNumbersWhoDidNotConnect.length > 0
) {
logger.error(
`Timeout while waiting for players ${playerNumbersWhoDidNotConnect.join(
', '
)} to connect. Assume they disconnected.`
);
playerNumbersWhoDidNotConnect.map((missingPlayerNumber) => {
gdjs.multiplayerMessageManager.markPlayerAsDisconnected({
runtimeScene,
playerNumber: missingPlayerNumber,
});
});
await resumeGame(runtimeScene);
return;
}
setTimeout(() => {
checkExpectedConnectedPlayersRegularly({
runtimeScene,
});
}, DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL);
};
/**
* When the host disconnects, we inform the backend we lost the connection and we need a new lobby/host.
*/
export const handleHostDisconnected = async function ({
runtimeScene,
}: {
runtimeScene: gdjs.RuntimeScene;
}) {
if (!_isLobbyGameRunning) {
// This can happen when the game ends. Nothing to do here.
return;
}
if (_lobbyChangeHostRequest) {
// The new host disconnected while we are already changing host.
// Let's end the lobby game to avoid weird situations.
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
}
const gameId = gdjs.projectData.properties.projectUuid;
if (!gameId || !_lobbyId) {
logger.error(
'Cannot ask for a host change without the game ID or lobby ID.'
);
return;
}
try {
_isChangingHost = true;
gdjs.multiplayerComponents.displayHostMigrationNotification(
runtimeScene
);
const changeHostRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/lobby-change-host-request`;
const playersInfo = gdjs.multiplayerMessageManager.getPlayersInfo();
const playersInfoForHostChange = Object.keys(playersInfo).map(
(playerNumber) => {
return {
playerNumber: parseInt(playerNumber, 10),
playerId: playersInfo[playerNumber].playerId,
ping: playersInfo[playerNumber].ping,
};
}
);
const body = JSON.stringify({
playersInfo: playersInfoForHostChange,
peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),
});
const lobbyChangeHostRequest = await fetchAsPlayer({
relativeUrl: changeHostRelativeUrl,
method: 'POST',
body,
dev: isUsingGDevelopDevelopmentEnvironment,
});
_lobbyChangeHostRequest = lobbyChangeHostRequest;
_lobbyChangeHostRequestInitiatedAt = getTimeNow();
await checkHostChangeRequestRegularly({ runtimeScene });
} catch (error) {
logger.error('Error while trying to change host:', error);
handleLobbyGameEnded();
clearChangeHostRequestData(runtimeScene);
}
};
/**
* Action to end the lobby game.
* This will update the lobby status and inform everyone in the lobby that the game has ended.
@@ -891,7 +1335,7 @@ namespace gdjs {
return;
}
if (!isPlayerHost()) {
if (!isCurrentPlayerHost()) {
logger.error('Only the host can end the game.');
return;
}
@@ -906,31 +1350,18 @@ namespace gdjs {
// Also call backend to end the game.
const gameId = gdjs.projectData.properties.projectUuid;
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (!gameId || !playerId || !playerToken || !_lobbyId) {
logger.error('Cannot end the lobby without the game ID or player ID.');
if (!gameId || !_lobbyId) {
logger.error('Cannot end the lobby without the game ID or lobby ID.');
return;
}
const rootApi = isUsingGDevelopDevelopmentEnvironment
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const headers = {
'Content-Type': 'application/json',
};
let endGameUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;
headers['Authorization'] = `player-game-token ${playerToken}`;
endGameUrl += `?playerId=${playerId}`;
const endGameRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;
try {
await fetch(endGameUrl, {
await fetchAsPlayer({
relativeUrl: endGameRelativeUrl,
method: 'POST',
headers,
body: JSON.stringify({
gameId,
lobbyId: _lobbyId,
}),
body: JSON.stringify({}),
dev: isUsingGDevelopDevelopmentEnvironment,
});
} catch (error) {
logger.error('Error while ending the game:', error);
@@ -966,6 +1397,8 @@ namespace gdjs {
peerId,
})
);
// We are the host.
hostPeerId = peerId;
};
/**

View File

@@ -313,9 +313,14 @@ describe('Multiplayer', () => {
/**
* Helper to fast forward a bit of time in players games, so that heartbeats
* are sent and all players are aware of each other.
* @param {{ playerNumber: number, peerId: string}[]} players
* @param {{ playerNumber: number, peerId: string, isHost?: boolean }[]} players
*/
const initiateGameWithPlayers = (players) => {
// Find the host.
const host = players.find((player) => player.isHost);
if (!host)
throw new Error('No host defined in players, cannot initiate game.');
// Create the instances of the MultiplayerMessageManager and MultiplayerVariablesManager
// for each player.
for (const player of players) {
@@ -325,6 +330,9 @@ describe('Multiplayer', () => {
peerMultiplayerVariablesManager[
player.peerId
] = gdjs.makeMultiplayerVariablesManager();
// Define the host for everyone.
gdjs.multiplayer.hostPeerId = host.peerId;
}
// Use a scene to simulate the game loop moving forward.
@@ -395,12 +403,16 @@ describe('Multiplayer', () => {
gdjs.multiplayer.disableMultiplayerForTesting = false;
gdjs.multiplayer._isLobbyGameRunning = true;
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = true;
// Sync as fast as possible for tests.
gdjs.multiplayer._objectMaxSyncRate = Infinity;
});
afterEach(() => {
gdjs.multiplayerPeerJsHelper = _originalP2pIfAny;
gdjs.multiplayer.disableMultiplayerForTesting = true;
gdjs.multiplayer._isLobbyGameRunning = false;
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = false;
gdjs.multiplayer._objectMaxSyncRate =
gdjs.multiplayer.DEFAULT_OBJECT_MAX_SYNC_RATE;
});
describe('Single scene tests', () => {
@@ -411,7 +423,7 @@ describe('Multiplayer', () => {
initiateGameWithPlayers,
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -607,7 +619,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -693,7 +705,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -830,7 +842,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -1026,7 +1038,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -1081,13 +1093,11 @@ describe('Multiplayer', () => {
const {
object: p1SpriteObject,
behavior: p1SpriteObjectBehavior,
} = getObjectAndMultiplayerBehaviorsFromScene(
p1RuntimeScene,
'MySpriteObject'
)[0];
p1SpriteObjectBehavior._objectMaxTickRate = Infinity;
p1SpriteObject.setX(242);
p1SpriteObject.setY(243);
p1RuntimeScene.renderAndStep(1000 / 60);
@@ -1155,7 +1165,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -1237,13 +1247,11 @@ describe('Multiplayer', () => {
const {
object: p2SpriteObject,
behavior: p2SpriteObjectBehavior,
} = getObjectAndMultiplayerBehaviorsFromScene(
p2RuntimeScene,
'MySpriteObject'
)[0];
p2SpriteObjectBehavior._objectMaxTickRate = Infinity;
p2SpriteObject.setX(242);
p2SpriteObject.setY(243);
p2RuntimeScene.renderAndStep(1000 / 60);
@@ -1358,7 +1366,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -1630,7 +1638,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -1840,7 +1848,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -1899,7 +1907,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -1941,7 +1949,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -2140,12 +2148,10 @@ describe('Multiplayer', () => {
const {
object: p2SpriteObject,
behavior: p2SpriteMultiplayerObjectBehavior,
} = getObjectAndMultiplayerBehaviorsFromScene(
p2RuntimeScene,
'MySpriteObject'
)[0];
p2SpriteMultiplayerObjectBehavior._objectMaxTickRate = Infinity;
p2SpriteObject.setX(242);
p2SpriteObject.setY(243);
p2RuntimeScene.renderAndStep(1000 / 60);
@@ -2202,7 +2208,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -2365,7 +2371,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -2450,7 +2456,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -2582,7 +2588,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -2676,7 +2682,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];
@@ -2685,7 +2691,7 @@ describe('Multiplayer', () => {
// Player 2 leaves.
const newConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 3, peerId: 'player-3' },
];
// Host sees the player 2 leaving.
@@ -2712,7 +2718,7 @@ describe('Multiplayer', () => {
} = createMultiplayerManagersMock();
const allConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 3, peerId: 'player-3' },
];
initiateGameWithPlayers(allConnectedPlayers);
@@ -2723,7 +2729,7 @@ describe('Multiplayer', () => {
// Player 2 joins.
const newConnectedPlayers = [
{ playerNumber: 1, peerId: 'player-1' },
{ playerNumber: 1, peerId: 'player-1', isHost: true },
{ playerNumber: 2, peerId: 'player-2' },
{ playerNumber: 3, peerId: 'player-3' },
];

View File

@@ -394,16 +394,15 @@ module.exports = {
return true;
}
if (propertyName === 'scaleX') {
if (propertyName === 'worldScale') {
const newValueAsNumber = parseInt(newValue, 10);
if (newValueAsNumber !== newValueAsNumber) return false;
if (!sharedContent.hasChild('worldScale')) {
sharedContent.addChild('worldScale');
}
sharedContent.getChild('worldScale').setDoubleValue(newValueAsNumber);
// Set deprecated properties for compatibility with 5.4.209-
sharedContent.getChild('scaleX').setDoubleValue(newValueAsNumber);
return true;
}
if (propertyName === 'scaleY') {
const newValueAsNumber = parseInt(newValue, 10);
if (newValueAsNumber !== newValueAsNumber) return false;
sharedContent.getChild('scaleY').setDoubleValue(newValueAsNumber);
return true;
}
@@ -427,16 +426,22 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getNewton());
if (!sharedContent.hasChild('worldScale')) {
sharedContent.addChild('worldScale');
sharedContent
.getChild('worldScale')
.setDoubleValue(
Math.sqrt(
sharedContent.getChild('scaleX').getDoubleValue() *
sharedContent.getChild('scaleY').getDoubleValue()
)
);
}
sharedProperties
.getOrCreate('scaleX')
.getOrCreate('worldScale')
.setValue(
sharedContent.getChild('scaleX').getDoubleValue().toString(10)
)
.setType('Number');
sharedProperties
.getOrCreate('scaleY')
.setValue(
sharedContent.getChild('scaleY').getDoubleValue().toString(10)
sharedContent.getChild('worldScale').getDoubleValue().toString(10)
)
.setType('Number');
@@ -445,6 +450,8 @@ module.exports = {
sharedData.initializeContent = function (behaviorContent) {
behaviorContent.addChild('gravityX').setDoubleValue(0);
behaviorContent.addChild('gravityY').setDoubleValue(9.8);
behaviorContent.addChild('worldScale').setDoubleValue(100);
// Set deprecated properties for compatibility with 5.4.209-
behaviorContent.addChild('scaleX').setDoubleValue(100);
behaviorContent.addChild('scaleY').setDoubleValue(100);
};
@@ -472,6 +479,19 @@ module.exports = {
);
// Global
aut
.addExpression(
'WorldScale',
_('World scale'),
_('Return the world scale.'),
_('Global'),
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.getCodeExtraInformation()
.setFunctionName('getWorldScale');
aut
.addCondition(
'GravityX',
@@ -1757,7 +1777,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Angular impulse (N·m·s'))
.addParameter('expression', _('Angular impulse (N·m·s)'))
.setParameterLongDescription(
_(
'An impulse is like a rotation speed addition but depends on the mass.'
@@ -1784,7 +1804,7 @@ module.exports = {
'Inertia',
_('Inertia'),
_(
'Return the rotational inertia of the object (in kilograms * meters * meters)'
'Return the rotational inertia of the object (in kilograms · meters²)'
),
'',
'res/physics32.png'

View File

@@ -15,6 +15,8 @@ namespace gdjs {
lvy: number | undefined;
av: number | undefined;
aw: boolean | undefined;
layers: number;
masks: number;
}
export interface Physics2NetworkSyncData extends BehaviorNetworkSyncData {
@@ -23,9 +25,15 @@ namespace gdjs {
export class Physics2SharedData {
gravityX: float;
gravityY: float;
worldScale: float;
worldInvScale: float;
/** @deprecated Use `worldScale` instead */
scaleX: float;
/** @deprecated Use `worldScale` instead */
scaleY: float;
/** @deprecated Use `worldInvScale` instead */
invScaleX: float;
/** @deprecated Use `worldInvScale` instead */
invScaleY: float;
timeStep: float;
frameTime: float = 0;
@@ -52,10 +60,13 @@ namespace gdjs {
this._registeredBehaviors = new Set();
this.gravityX = sharedData.gravityX;
this.gravityY = sharedData.gravityY;
this.scaleX = sharedData.scaleX === 0 ? 100 : sharedData.scaleX;
this.scaleY = sharedData.scaleY === 0 ? 100 : sharedData.scaleY;
this.scaleX = sharedData.scaleX || 100;
this.scaleY = sharedData.scaleY || 100;
this.invScaleX = 1 / this.scaleX;
this.invScaleY = 1 / this.scaleY;
this.worldScale =
sharedData.worldScale || Math.sqrt(this.scaleX * this.scaleY);
this.worldInvScale = 1 / this.worldScale;
this.timeStep = 1 / 60;
this.world = new Box2D.b2World(
new Box2D.b2Vec2(this.gravityX, this.gravityY)
@@ -513,6 +524,8 @@ namespace gdjs {
...super.getNetworkSyncData(),
props: {
...bodyProps,
layers: this.layers,
masks: this.masks,
},
};
}
@@ -556,6 +569,14 @@ namespace gdjs {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
}
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
}
}
onDeActivate() {
@@ -642,10 +663,10 @@ namespace gdjs {
createShape(): Box2D.b2FixtureDef {
// Get the scaled offset
const offsetX = this.shapeOffsetX
? this.shapeOffsetX * this.shapeScale * this._sharedData.invScaleX
? this.shapeOffsetX * this.shapeScale * this._sharedData.worldInvScale
: 0;
const offsetY = this.shapeOffsetY
? this.shapeOffsetY * this.shapeScale * this._sharedData.invScaleY
? this.shapeOffsetY * this.shapeScale * this._sharedData.worldInvScale
: 0;
// Generate the base shape
@@ -657,12 +678,14 @@ namespace gdjs {
// Average radius from width and height
if (this.shapeDimensionA > 0) {
shape.set_m_radius(
this.shapeDimensionA * this.shapeScale * this._sharedData.invScaleX
this.shapeDimensionA *
this.shapeScale *
this._sharedData.worldInvScale
);
} else {
const radius =
(this.owner.getWidth() * this._sharedData.invScaleX +
this.owner.getHeight() * this._sharedData.invScaleY) /
(this.owner.getWidth() * this._sharedData.worldInvScale +
this.owner.getHeight() * this._sharedData.worldInvScale) /
4;
shape.set_m_radius(radius > 0 ? radius : 1);
}
@@ -680,10 +703,10 @@ namespace gdjs {
) {
let width =
(this.owner.getWidth() > 0 ? this.owner.getWidth() : 1) *
this._sharedData.invScaleX;
this._sharedData.worldInvScale;
let height =
(this.owner.getHeight() > 0 ? this.owner.getHeight() : 1) *
this._sharedData.invScaleY;
this._sharedData.worldInvScale;
// Set the shape box
shape.SetAsBox(
@@ -728,12 +751,12 @@ namespace gdjs {
Box2D.HEAPF32[(this._verticesBuffer + offset) >> 2] =
(this.polygon.vertices[i][0] * this.shapeScale +
originOffsetX) *
this._sharedData.invScaleX +
this._sharedData.worldInvScale +
offsetX;
Box2D.HEAPF32[(this._verticesBuffer + (offset + 4)) >> 2] =
(this.polygon.vertices[i][1] * this.shapeScale +
originOffsetY) *
this._sharedData.invScaleY +
this._sharedData.worldInvScale +
offsetY;
offset += 8;
}
@@ -755,10 +778,10 @@ namespace gdjs {
? this.shapeDimensionA * this.shapeScale
: this.owner.getWidth() > 0
? this.owner.getWidth()
: 1) * this._sharedData.invScaleX;
: 1) * this._sharedData.worldInvScale;
let height =
this.owner.getHeight() > 0
? this.owner.getHeight() * this._sharedData.invScaleY
? this.owner.getHeight() * this._sharedData.worldInvScale
: 0;
// Angle from custom dimension, otherwise is 0
@@ -787,13 +810,13 @@ namespace gdjs {
? this.shapeDimensionA * this.shapeScale
: this.owner.getWidth() > 0
? this.owner.getWidth()
: 1) * this._sharedData.invScaleX;
: 1) * this._sharedData.worldInvScale;
let height =
(this.shapeDimensionB > 0
? this.shapeDimensionB * this.shapeScale
: this.owner.getHeight() > 0
? this.owner.getHeight()
: 1) * this._sharedData.invScaleY;
: 1) * this._sharedData.worldInvScale;
// Set the shape box, the offset must be added here too
shape.SetAsBox(
@@ -878,9 +901,9 @@ namespace gdjs {
bodyDef.set_position(
this.b2Vec2(
(this.owner.getDrawableX() + this.owner.getWidth() / 2) *
this._sharedData.invScaleX,
this._sharedData.worldInvScale,
(this.owner.getDrawableY() + this.owner.getHeight() / 2) *
this._sharedData.invScaleY
this._sharedData.worldInvScale
)
);
bodyDef.set_angle(gdjs.toRad(this.owner.getAngle()));
@@ -934,13 +957,13 @@ namespace gdjs {
// don't do anything (but still run the physics simulation - this is independent).
if (this._body !== null) {
this.owner.setX(
this._body.GetPosition().get_x() * this._sharedData.scaleX -
this._body.GetPosition().get_x() * this._sharedData.worldScale -
this.owner.getWidth() / 2 +
this.owner.getX() -
this.owner.getDrawableX()
);
this.owner.setY(
this._body.GetPosition().get_y() * this._sharedData.scaleY -
this._body.GetPosition().get_y() * this._sharedData.worldScale -
this.owner.getHeight() / 2 +
this.owner.getY() -
this.owner.getDrawableY()
@@ -993,15 +1016,19 @@ namespace gdjs {
) {
const pos = this.b2Vec2(
(this.owner.getDrawableX() + this.owner.getWidth() / 2) *
this._sharedData.invScaleX,
this._sharedData.worldInvScale,
(this.owner.getDrawableY() + this.owner.getHeight() / 2) *
this._sharedData.invScaleY
this._sharedData.worldInvScale
);
body.SetTransform(pos, gdjs.toRad(this.owner.getAngle()));
body.SetAwake(true);
}
}
getWorldScale(): float {
return this._sharedData.worldScale;
}
getGravityX(): float {
return this._sharedData.gravityX;
}
@@ -1435,7 +1462,7 @@ namespace gdjs {
const body = this._body!;
// Get the linear velocity on X
return body.GetLinearVelocity().get_x() * this._sharedData.scaleX;
return body.GetLinearVelocity().get_x() * this._sharedData.worldScale;
}
setLinearVelocityX(linearVelocityX: float): void {
@@ -1448,7 +1475,7 @@ namespace gdjs {
// Set the linear velocity on X
body.SetLinearVelocity(
this.b2Vec2(
linearVelocityX * this._sharedData.invScaleX,
linearVelocityX * this._sharedData.worldInvScale,
body.GetLinearVelocity().get_y()
)
);
@@ -1462,7 +1489,7 @@ namespace gdjs {
const body = this._body!;
// Get the linear velocity on Y
return body.GetLinearVelocity().get_y() * this._sharedData.scaleY;
return body.GetLinearVelocity().get_y() * this._sharedData.worldScale;
}
setLinearVelocityY(linearVelocityY: float): void {
@@ -1476,7 +1503,7 @@ namespace gdjs {
body.SetLinearVelocity(
this.b2Vec2(
body.GetLinearVelocity().get_x(),
linearVelocityY * this._sharedData.invScaleY
linearVelocityY * this._sharedData.worldInvScale
)
);
}
@@ -1490,8 +1517,8 @@ namespace gdjs {
// Get the linear velocity length
return this.b2Vec2(
body.GetLinearVelocity().get_x() * this._sharedData.scaleX,
body.GetLinearVelocity().get_y() * this._sharedData.scaleY
body.GetLinearVelocity().get_x() * this._sharedData.worldScale,
body.GetLinearVelocity().get_y() * this._sharedData.worldScale
).Length();
}
@@ -1505,8 +1532,8 @@ namespace gdjs {
// Get the linear velocity angle
return gdjs.toDegrees(
Math.atan2(
body.GetLinearVelocity().get_y() * this._sharedData.scaleY,
body.GetLinearVelocity().get_x() * this._sharedData.scaleX
body.GetLinearVelocity().get_y() * this._sharedData.worldScale,
body.GetLinearVelocity().get_x() * this._sharedData.worldScale
)
);
}
@@ -1522,8 +1549,8 @@ namespace gdjs {
angle = gdjs.toRad(angle);
body.SetLinearVelocity(
this.b2Vec2(
linearVelocity * Math.cos(angle) * this._sharedData.invScaleX,
linearVelocity * Math.sin(angle) * this._sharedData.invScaleY
linearVelocity * Math.cos(angle) * this._sharedData.worldInvScale,
linearVelocity * Math.sin(angle) * this._sharedData.worldInvScale
)
);
}
@@ -1580,8 +1607,8 @@ namespace gdjs {
body.ApplyForce(
this.b2Vec2(forceX, forceY),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1608,8 +1635,8 @@ namespace gdjs {
body.ApplyForce(
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1632,16 +1659,17 @@ namespace gdjs {
// Wake up the object
body.SetAwake(true);
// TODO Optimize this using a unit vector instead of trigonometry.
// Apply the force
const angle = Math.atan2(
towardY * this._sharedData.invScaleY - body.GetPosition().get_y(),
towardX * this._sharedData.invScaleX - body.GetPosition().get_x()
towardY * this._sharedData.worldInvScale - body.GetPosition().get_y(),
towardX * this._sharedData.worldInvScale - body.GetPosition().get_x()
);
body.ApplyForce(
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1667,8 +1695,8 @@ namespace gdjs {
body.ApplyLinearImpulse(
this.b2Vec2(impulseX, impulseY),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1695,8 +1723,8 @@ namespace gdjs {
body.ApplyLinearImpulse(
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1719,16 +1747,17 @@ namespace gdjs {
// Wake up the object
body.SetAwake(true);
// TODO Optimize this using a unit vector instead of trigonometry.
// Apply the impulse
const angle = Math.atan2(
towardY * this._sharedData.invScaleY - body.GetPosition().get_y(),
towardX * this._sharedData.invScaleX - body.GetPosition().get_x()
towardY * this._sharedData.worldInvScale - body.GetPosition().get_y(),
towardX * this._sharedData.worldInvScale - body.GetPosition().get_x()
);
body.ApplyLinearImpulse(
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
this.b2Vec2Sec(
positionX * this._sharedData.invScaleX,
positionY * this._sharedData.invScaleY
positionX * this._sharedData.worldInvScale,
positionY * this._sharedData.worldInvScale
),
// TODO Should let Box2d awake the object itself.
false
@@ -1805,7 +1834,7 @@ namespace gdjs {
const body = this._body!;
// Get the mass center on X
return body.GetWorldCenter().get_x() * this._sharedData.scaleX;
return body.GetWorldCenter().get_x() * this._sharedData.worldScale;
}
getMassCenterY(): float {
@@ -1816,7 +1845,7 @@ namespace gdjs {
const body = this._body!;
// Get the mass center on Y
return body.GetWorldCenter().get_y() * this._sharedData.scaleY;
return body.GetWorldCenter().get_y() * this._sharedData.worldScale;
}
// Joints
@@ -1983,8 +2012,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -1992,17 +2021,17 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
jointDef.set_length(
length > 0
? length * this._sharedData.invScaleX
? length * this._sharedData.worldInvScale
: this.b2Vec2(
(x2 - x1) * this._sharedData.invScaleX,
(y2 - y1) * this._sharedData.invScaleY
(x2 - x1) * this._sharedData.worldInvScale,
(y2 - y1) * this._sharedData.worldInvScale
).Length()
);
jointDef.set_frequencyHz(frequency >= 0 ? frequency : 0);
@@ -2031,7 +2060,7 @@ namespace gdjs {
}
// Get the joint length
return joint.GetLength() * this._sharedData.scaleX;
return joint.GetLength() * this._sharedData.worldScale;
}
setDistanceJointLength(jointId: integer | string, length: float): void {
@@ -2049,7 +2078,7 @@ namespace gdjs {
}
// Set the joint length
joint.SetLength(length * this._sharedData.invScaleX);
joint.SetLength(length * this._sharedData.worldInvScale);
// Awake the bodies
joint.GetBodyA().SetAwake(true);
@@ -2148,8 +2177,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
this._sharedData.staticBody.GetLocalPoint(
this.b2Vec2(
x * this._sharedData.invScaleX,
y * this._sharedData.invScaleY
x * this._sharedData.worldInvScale,
y * this._sharedData.worldInvScale
)
)
);
@@ -2157,8 +2186,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
body.GetLocalPoint(
this.b2Vec2(
x * this._sharedData.invScaleX,
y * this._sharedData.invScaleY
x * this._sharedData.worldInvScale,
y * this._sharedData.worldInvScale
)
)
);
@@ -2233,8 +2262,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -2242,8 +2271,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
@@ -2534,8 +2563,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -2543,8 +2572,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
@@ -2564,13 +2593,17 @@ namespace gdjs {
// The translation range must include zero
jointDef.set_lowerTranslation(
lowerTranslation < 0 ? lowerTranslation * this._sharedData.invScaleX : 0
lowerTranslation < 0
? lowerTranslation * this._sharedData.worldInvScale
: 0
);
jointDef.set_upperTranslation(
upperTranslation > 0 ? upperTranslation * this._sharedData.invScaleX : 0
upperTranslation > 0
? upperTranslation * this._sharedData.worldInvScale
: 0
);
jointDef.set_enableMotor(enableMotor);
jointDef.set_motorSpeed(motorSpeed * this._sharedData.invScaleX);
jointDef.set_motorSpeed(motorSpeed * this._sharedData.worldInvScale);
jointDef.set_maxMotorForce(maxMotorForce);
jointDef.set_collideConnected(collideConnected);
@@ -2633,7 +2666,7 @@ namespace gdjs {
}
// Get the joint current translation
return joint.GetJointTranslation() * this._sharedData.scaleX;
return joint.GetJointTranslation() * this._sharedData.worldScale;
}
getPrismaticJointSpeed(jointId: integer | string): float {
@@ -2648,7 +2681,7 @@ namespace gdjs {
}
// Get the joint speed
return joint.GetJointSpeed() * this._sharedData.scaleX;
return joint.GetJointSpeed() * this._sharedData.worldScale;
}
isPrismaticJointLimitsEnabled(jointId: integer | string): boolean {
@@ -2696,7 +2729,7 @@ namespace gdjs {
}
// Get the joint lower limit
return joint.GetLowerLimit() * this._sharedData.scaleX;
return joint.GetLowerLimit() * this._sharedData.worldScale;
}
getPrismaticJointMaxTranslation(jointId: integer | string): float {
@@ -2711,7 +2744,7 @@ namespace gdjs {
}
// Get the joint upper angle
return joint.GetUpperLimit() * this._sharedData.scaleX;
return joint.GetUpperLimit() * this._sharedData.worldScale;
}
setPrismaticJointLimits(
@@ -2742,8 +2775,8 @@ namespace gdjs {
// Set the joint limits
joint.SetLimits(
lowerTranslation * this._sharedData.invScaleX,
upperTranslation * this._sharedData.invScaleX
lowerTranslation * this._sharedData.worldInvScale,
upperTranslation * this._sharedData.worldInvScale
);
}
@@ -2792,7 +2825,7 @@ namespace gdjs {
}
// Get the joint motor speed
return joint.GetMotorSpeed() * this._sharedData.scaleX;
return joint.GetMotorSpeed() * this._sharedData.worldScale;
}
setPrismaticJointMotorSpeed(jointId: integer | string, speed): void {
@@ -2807,7 +2840,7 @@ namespace gdjs {
}
// Set the joint motor speed
joint.SetMotorSpeed(speed * this._sharedData.invScaleX);
joint.SetMotorSpeed(speed * this._sharedData.worldInvScale);
}
getPrismaticJointMaxMotorForce(jointId: integer | string): float {
@@ -2904,8 +2937,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -2913,37 +2946,37 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
jointDef.set_groundAnchorA(
this.b2Vec2(
groundX1 * this._sharedData.invScaleX,
groundY1 * this._sharedData.invScaleY
groundX1 * this._sharedData.worldInvScale,
groundY1 * this._sharedData.worldInvScale
)
);
jointDef.set_groundAnchorB(
this.b2Vec2(
groundX2 * this._sharedData.invScaleX,
groundY2 * this._sharedData.invScaleY
groundX2 * this._sharedData.worldInvScale,
groundY2 * this._sharedData.worldInvScale
)
);
jointDef.set_lengthA(
lengthA > 0
? lengthA * this._sharedData.invScaleX
? lengthA * this._sharedData.worldInvScale
: this.b2Vec2(
(groundX1 - x1) * this._sharedData.invScaleX,
(groundY1 - y1) * this._sharedData.invScaleY
(groundX1 - x1) * this._sharedData.worldInvScale,
(groundY1 - y1) * this._sharedData.worldInvScale
).Length()
);
jointDef.set_lengthB(
lengthB > 0
? lengthB * this._sharedData.invScaleX
? lengthB * this._sharedData.worldInvScale
: this.b2Vec2(
(groundX2 - x2) * this._sharedData.invScaleX,
(groundY2 - y2) * this._sharedData.invScaleY
(groundX2 - x2) * this._sharedData.worldInvScale,
(groundY2 - y2) * this._sharedData.worldInvScale
).Length()
);
jointDef.set_ratio(ratio > 0 ? ratio : 1);
@@ -2971,7 +3004,7 @@ namespace gdjs {
}
// Get the joint ground anchor
return joint.GetGroundAnchorA().get_x() * this._sharedData.scaleX;
return joint.GetGroundAnchorA().get_x() * this._sharedData.worldScale;
}
getPulleyJointFirstGroundAnchorY(jointId: integer | string): float {
@@ -2984,7 +3017,7 @@ namespace gdjs {
}
// Get the joint ground anchor
return joint.GetGroundAnchorA().get_y() * this._sharedData.scaleY;
return joint.GetGroundAnchorA().get_y() * this._sharedData.worldScale;
}
getPulleyJointSecondGroundAnchorX(jointId: integer | string): float {
@@ -2997,7 +3030,7 @@ namespace gdjs {
}
// Get the joint ground anchor
return joint.GetGroundAnchorB().get_x() * this._sharedData.scaleX;
return joint.GetGroundAnchorB().get_x() * this._sharedData.worldScale;
}
getPulleyJointSecondGroundAnchorY(jointId: integer | string): float {
@@ -3010,7 +3043,7 @@ namespace gdjs {
}
// Get the joint ground anchor
return joint.GetGroundAnchorB().get_y() * this._sharedData.scaleY;
return joint.GetGroundAnchorB().get_y() * this._sharedData.worldScale;
}
getPulleyJointFirstLength(jointId: integer | string): float {
@@ -3023,7 +3056,7 @@ namespace gdjs {
}
// Get the joint length
return joint.GetCurrentLengthA() * this._sharedData.scaleX;
return joint.GetCurrentLengthA() * this._sharedData.worldScale;
}
getPulleyJointSecondLength(jointId: integer | string): float {
@@ -3036,7 +3069,7 @@ namespace gdjs {
}
// Get the joint length
return joint.GetCurrentLengthB() * this._sharedData.scaleX;
return joint.GetCurrentLengthB() * this._sharedData.worldScale;
}
getPulleyJointRatio(jointId: integer | string): float {
@@ -3188,8 +3221,8 @@ namespace gdjs {
jointDef.set_bodyB(body);
jointDef.set_target(
this.b2Vec2(
targetX * this._sharedData.invScaleX,
targetY * this._sharedData.invScaleY
targetX * this._sharedData.worldInvScale,
targetY * this._sharedData.worldInvScale
)
);
jointDef.set_maxForce(maxForce >= 0 ? maxForce : 0);
@@ -3217,7 +3250,7 @@ namespace gdjs {
}
// Get the joint target X
return joint.GetTarget().get_x() * this._sharedData.scaleX;
return joint.GetTarget().get_x() * this._sharedData.worldScale;
}
getMouseJointTargetY(jointId: integer | string): float {
@@ -3229,7 +3262,7 @@ namespace gdjs {
}
// Get the joint target Y
return joint.GetTarget().get_y() * this._sharedData.scaleY;
return joint.GetTarget().get_y() * this._sharedData.worldScale;
}
setMouseJointTarget(
@@ -3247,8 +3280,8 @@ namespace gdjs {
// Set the joint target
joint.SetTarget(
this.b2Vec2(
targetX * this._sharedData.invScaleX,
targetY * this._sharedData.invScaleY
targetX * this._sharedData.worldInvScale,
targetY * this._sharedData.worldInvScale
)
);
@@ -3386,8 +3419,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -3395,8 +3428,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
@@ -3451,7 +3484,7 @@ namespace gdjs {
}
// Get the joint current translation
return joint.GetJointTranslation() * this._sharedData.scaleX;
return joint.GetJointTranslation() * this._sharedData.worldScale;
}
getWheelJointSpeed(jointId: integer | string): float {
@@ -3668,8 +3701,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -3677,8 +3710,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
@@ -3817,8 +3850,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -3826,17 +3859,17 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
jointDef.set_maxLength(
maxLength > 0
? maxLength * this._sharedData.invScaleX
? maxLength * this._sharedData.worldInvScale
: this.b2Vec2(
(x2 - x1) * this._sharedData.invScaleX,
(y2 - y1) * this._sharedData.invScaleY
(x2 - x1) * this._sharedData.worldInvScale,
(y2 - y1) * this._sharedData.worldInvScale
).Length()
);
jointDef.set_collideConnected(collideConnected);
@@ -3863,7 +3896,7 @@ namespace gdjs {
}
// Get the joint maximum length
return joint.GetMaxLength() * this._sharedData.scaleX;
return joint.GetMaxLength() * this._sharedData.worldScale;
}
setRopeJointMaxLength(jointId: integer | string, maxLength: float): void {
@@ -3881,7 +3914,7 @@ namespace gdjs {
}
// Set the joint maximum length
joint.SetMaxLength(maxLength * this._sharedData.invScaleX);
joint.SetMaxLength(maxLength * this._sharedData.worldInvScale);
// Awake the bodies
joint.GetBodyA().SetAwake(true);
@@ -3927,8 +3960,8 @@ namespace gdjs {
jointDef.set_localAnchorA(
body.GetLocalPoint(
this.b2Vec2(
x1 * this._sharedData.invScaleX,
y1 * this._sharedData.invScaleY
x1 * this._sharedData.worldInvScale,
y1 * this._sharedData.worldInvScale
)
)
);
@@ -3936,8 +3969,8 @@ namespace gdjs {
jointDef.set_localAnchorB(
otherBody.GetLocalPoint(
this.b2Vec2(
x2 * this._sharedData.invScaleX,
y2 * this._sharedData.invScaleY
x2 * this._sharedData.worldInvScale,
y2 * this._sharedData.worldInvScale
)
)
);
@@ -4061,8 +4094,8 @@ namespace gdjs {
jointDef.set_bodyB(otherBody);
jointDef.set_linearOffset(
this.b2Vec2(
offsetX * this._sharedData.invScaleX,
offsetY * this._sharedData.invScaleY
offsetX * this._sharedData.worldInvScale,
offsetY * this._sharedData.worldInvScale
)
);
jointDef.set_angularOffset(gdjs.toRad(offsetAngle));
@@ -4095,7 +4128,7 @@ namespace gdjs {
}
// Get the joint offset
return joint.GetLinearOffset().get_x() * this._sharedData.scaleX;
return joint.GetLinearOffset().get_x() * this._sharedData.worldScale;
}
getMotorJointOffsetY(jointId: integer | string): float {
@@ -4108,7 +4141,7 @@ namespace gdjs {
}
// Get the joint offset
return joint.GetLinearOffset().get_y() * this._sharedData.scaleY;
return joint.GetLinearOffset().get_y() * this._sharedData.worldScale;
}
setMotorJointOffset(
@@ -4127,8 +4160,8 @@ namespace gdjs {
// Set the joint offset
joint.SetLinearOffset(
this.b2Vec2(
offsetX * this._sharedData.invScaleX,
offsetY * this._sharedData.invScaleY
offsetX * this._sharedData.worldInvScale,
offsetY * this._sharedData.worldInvScale
)
);
}

View File

@@ -73,6 +73,38 @@ module.exports = {
.setFunctionName('setAnimationMixingDuration')
.setGetter('getAnimationMixingDuration');
object
.addExpressionAndCondition(
'number',
'PointAttachmentX',
_('Point attachment X position'),
_('x position of spine point attachment'),
_('x position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
_('Animations and images'),
'JsPlatform/Extensions/spine.svg'
)
.addParameter('object', _('Spine'), 'SpineObject')
.addParameter('string', _('Attachment name'))
.addParameter('string', _('Slot name (use "" if names are the same)'))
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('getPointAttachmentX');
object
.addExpressionAndCondition(
'number',
'PointAttachmentY',
_('Point attachment Y position'),
_('y position of spine point attachment'),
_('y position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
_('Animations and images'),
'JsPlatform/Extensions/spine.svg'
)
.addParameter('object', _('Spine'), 'SpineObject')
.addParameter('string', _('Attachment name'))
.addParameter('string', _('Slot name (use "" if names are the same)'))
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('getPointAttachmentY');
return extension;
},

View File

@@ -2,6 +2,21 @@ namespace gdjs {
const isSpine = (obj: any): obj is pixi_spine.Spine =>
obj instanceof pixi_spine.Spine;
// See https://github.com/pixijs/spine/issues/562
// IPointAttachment is not declared and exported but its implementation does exist and it is used in runtime
interface IPointAttachment extends pixi_spine.IVertexAttachment {
computeWorldPosition(
bone: pixi_spine.IBone,
point: pixi_spine.Vector2
): pixi_spine.Vector2;
computeWorldRotation(bone: pixi_spine.IBone): number;
}
const isPointAttachment = (
attachment: pixi_spine.IAttachment
): attachment is IPointAttachment =>
!!attachment && attachment.type === pixi_spine.AttachmentType.Point;
export class SpineRuntimeObjectPixiRenderer {
private _object: gdjs.SpineRuntimeObject;
private _rendererObject: pixi_spine.Spine | PIXI.Container;
@@ -182,6 +197,44 @@ namespace gdjs {
return this._isAnimationComplete;
}
getPointAttachmentPosition(
attachmentName: string,
slotName?: string
): pixi_spine.Vector2 {
if (!slotName) {
slotName = attachmentName;
}
if (!isSpine(this._rendererObject)) {
return new pixi_spine.Vector2(
this._rendererObject.x,
this._rendererObject.y
);
}
const slot = this._rendererObject.skeleton.findSlot(slotName);
if (!slot) {
throw new Error(
`Unable to find ${slotName} slot name for ${attachmentName} point attachment.`
);
}
const attachment = this._rendererObject.skeleton.getAttachmentByName(
slotName,
attachmentName
);
if (!isPointAttachment(attachment)) {
throw new Error(
`Unable to find ${attachmentName} point attachment with ${slotName} slot name.`
);
}
return new PIXI.Matrix()
.rotate(this._rendererObject.rotation)
.scale(this._rendererObject.scale.x, this._rendererObject.scale.y)
.translate(this._rendererObject.x, this._rendererObject.y)
.apply(
attachment.computeWorldPosition(slot.bone, new pixi_spine.Vector2())
);
}
private constructRendererObject(): pixi_spine.Spine | PIXI.Container {
const game = this.instanceContainer.getGame();
const spineManager = game.getSpineManager();

View File

@@ -67,7 +67,7 @@ namespace gdjs {
this._animations = objectData.content.animations;
this._originalScale = objectData.content.scale;
this.spineResourceName = objectData.content.spineResourceName;
this._animationMixingDuration = 0.1;
this._animationMixingDuration = 0;
this._renderer = new gdjs.SpineRuntimeObjectRenderer(
this,
instanceContainer
@@ -500,6 +500,16 @@ namespace gdjs {
this._isPausedFrameDirty = true;
}
getPointAttachmentX(attachmentName: string, slotName?: string): number {
return this._renderer.getPointAttachmentPosition(attachmentName, slotName)
.x;
}
getPointAttachmentY(attachmentName: string, slotName?: string): number {
return this._renderer.getPointAttachmentPosition(attachmentName, slotName)
.y;
}
getAnimationDuration(): number {
if (this._animations.length === 0) {
return 0;

View File

@@ -278,7 +278,7 @@ module.exports = {
// Properties expressions/conditions/actions:
// Deprecated
// Deprecated, see TextContainerCapability
object
.addExpressionAndConditionAndAction(
'string',
@@ -298,6 +298,7 @@ module.exports = {
.setFunctionName('setText')
.setGetter('getText');
// Deprecated, see TextContainerCapability
object
.addStrExpression(
'Text',
@@ -306,6 +307,7 @@ module.exports = {
'',
'res/conditions/text24_black.png'
)
.setHidden()
.addParameter('object', _('Text input'), 'TextInputObject', false)
.setFunctionName('getText');

View File

@@ -49,7 +49,7 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
"res/actions/font.png")
.AddParameter("object", _("Object"), "Text")
.AddParameter("police", _("Font"))
.AddParameter("fontResource", _("Font resource name"))
.SetFunctionName("ChangeFont");
obj.AddAction(

View File

@@ -38,6 +38,9 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllExpressionsForObject("TextObject::Text")["FontSize"]
.SetFunctionName("getCharacterSize");
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
.SetFunctionName("setFontName");
GetAllActionsForObject("TextObject::Text")["TextObject::SetBold"]
.SetFunctionName("setBold");
GetAllConditionsForObject("TextObject::Text")["TextObject::IsBold"]
@@ -188,8 +191,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setShadow");
// Unimplemented actions and conditions:
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
.SetFunctionName("");
GetAllActionsForObject("TextObject::Text")["TextObject::SetUnderlined"]
.SetFunctionName("");
GetAllConditionsForObject("TextObject::Text")["TextObject::IsUnderlined"]

View File

@@ -184,7 +184,6 @@ const defineTileMap = function (extension, _, gd) {
objectTileMap
)
.setCategoryFullName(_('Advanced'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
@@ -721,7 +720,6 @@ const defineSimpleTileMap = function (extension, _, gd) {
objectSimpleTileMap
)
.setCategoryFullName(_('General'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
@@ -1240,7 +1238,6 @@ const defineCollisionMask = function (extension, _, gd) {
collisionMaskObject
)
.setCategoryFullName(_('Advanced'))
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.setIncludeFile('Extensions/TileMap/tilemapcollisionmaskruntimeobject.js')

View File

@@ -96,6 +96,16 @@ namespace gdjs {
(this._transformationUpToDateCount + 1) % Number.MAX_SAFE_INTEGER;
}
invalidateTile(layerIndex: integer, x: integer, y: integer) {
const layer = this.getLayer(layerIndex);
if (layer) {
const tile = layer.get(x, y);
if (tile) {
tile.invalidate();
}
}
}
/**
* @returns The tile map width in pixels.
*/
@@ -690,6 +700,28 @@ namespace gdjs {
this.affineTransformationUpToDateCount = this.layer.tileMap._transformationUpToDateCount;
}
invalidate() {
this.affineTransformationUpToDateCount = -1;
// Also invalidate neighbors because their hit boxes may need to be
// extended differently.
let neighbor = this.layer.get(this.x - 1, this.y);
if (neighbor) {
neighbor.affineTransformationUpToDateCount = -1;
}
neighbor = this.layer.get(this.x + 1, this.y);
if (neighbor) {
neighbor.affineTransformationUpToDateCount = -1;
}
neighbor = this.layer.get(this.x, this.y - 1);
if (neighbor) {
neighbor.affineTransformationUpToDateCount = -1;
}
neighbor = this.layer.get(this.x, this.y + 1);
if (neighbor) {
neighbor.affineTransformationUpToDateCount = -1;
}
}
/**
* @returns The hitboxes of this tile in the scene basis.
*/
@@ -701,7 +733,6 @@ namespace gdjs {
const definition = this.getDefinition();
if (!definition) {
this._setHitboxesUpToDate();
// It should already be []
this.hitBoxes.length = 0;
return this.hitBoxes;
}
@@ -709,14 +740,100 @@ namespace gdjs {
const definitionHitboxes = definition.getHitBoxes(tag);
if (!definitionHitboxes) {
this._setHitboxesUpToDate();
// It should already be []
this.hitBoxes.length = 0;
return this.hitBoxes;
}
const layerTransformation = this.layer.tileMap.getTransformation();
const width = this.layer.tileMap.getTileWidth();
const height = this.layer.tileMap.getTileHeight();
const tileMap = this.layer.tileMap;
const width = tileMap.getTileWidth();
const height = tileMap.getTileHeight();
// Extend the hit boxes.
// It avoids small objects to be pushed side way into a wall when they
// should be pop out of the wall.
const hasFullHitBox =
definitionHitboxes.length === 1 && definition.hasFullHitBox(tag);
if (hasFullHitBox) {
const isLeftFull = this._hasNeighborFullHitBox(-1, 0);
const isRightFull = this._hasNeighborFullHitBox(1, 0);
const isTopFull = this._hasNeighborFullHitBox(0, -1);
const isBottomFull = this._hasNeighborFullHitBox(0, 1);
let hitBoxesCount = 0;
if (isLeftFull || isRightFull) {
let minX = isLeftFull ? -width : 0;
let maxX = isRightFull ? 2 * width : width;
if (hitBoxesCount >= this.hitBoxes.length) {
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
}
TransformedCollisionTile.setRectangle(
this.hitBoxes[hitBoxesCount],
minX,
0,
maxX,
height
);
hitBoxesCount++;
}
if (isTopFull || isBottomFull) {
let minY = isTopFull ? -height : 0;
let maxY = isBottomFull ? 2 * height : height;
if (hitBoxesCount >= this.hitBoxes.length) {
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
}
TransformedCollisionTile.setRectangle(
this.hitBoxes[hitBoxesCount],
0,
minY,
width,
maxY
);
hitBoxesCount++;
}
if (hitBoxesCount === 0) {
if (this.hitBoxes.length === 0) {
this.hitBoxes[0] = gdjs.Polygon.createRectangle(0, 0);
}
TransformedCollisionTile.setRectangle(
this.hitBoxes[0],
0,
0,
width,
height
);
hitBoxesCount++;
}
this.hitBoxes.length = hitBoxesCount;
} else {
for (
let polygonIndex = 0;
polygonIndex < definitionHitboxes.length;
polygonIndex++
) {
const defPolygon = definitionHitboxes[polygonIndex];
if (polygonIndex >= this.hitBoxes.length) {
// This can't happen in practice as only the simple tile map can be
// modify and it only contains full hit boxes.
this.hitBoxes[polygonIndex] = gdjs.Polygon.createRectangle(0, 0);
}
const polygon = this.hitBoxes[polygonIndex];
for (
let vertexIndex = 0;
vertexIndex < polygon.vertices.length;
vertexIndex++
) {
const defVertex = defPolygon[vertexIndex];
const vertex = polygon.vertices[vertexIndex];
vertex[0] = defVertex[0];
vertex[1] = defVertex[1];
}
}
}
// Transform the hit boxes.
const tileTransformation =
TransformedCollisionTile.workingTransformation;
@@ -731,16 +848,12 @@ namespace gdjs {
tileTransformation.flipX(width / 2);
tileTransformation.rotateAround(Math.PI / 2, width / 2, height / 2);
}
tileTransformation.preConcatenate(layerTransformation);
// The tile map can't change at runtime so the existing arrays can be
// reused safely.
tileTransformation.preConcatenate(tileMap.getTransformation());
for (
let polygonIndex = 0;
polygonIndex < this.hitBoxes.length;
polygonIndex++
) {
const defPolygon = definitionHitboxes[polygonIndex];
const polygon = this.hitBoxes[polygonIndex];
for (
@@ -748,15 +861,42 @@ namespace gdjs {
vertexIndex < polygon.vertices.length;
vertexIndex++
) {
const defVertex = defPolygon[vertexIndex];
const vertex = polygon.vertices[vertexIndex];
tileTransformation.transform(defVertex, vertex);
tileTransformation.transform(vertex, vertex);
}
}
this._setHitboxesUpToDate();
return this.hitBoxes;
}
private _hasNeighborFullHitBox(deltaX: integer, deltaY: integer) {
const sourceLayer = this.layer._source;
const tileId = sourceLayer.getTileId(this.x + deltaX, this.y + deltaY);
const tileDefinition =
tileId && this.layer.tileMap.getTileDefinition(tileId);
return (
tileDefinition && tileDefinition.hasFullHitBox(this.layer.tileMap.tag)
);
}
private static setRectangle(
polygon: gdjs.Polygon,
minX: float,
minY: float,
maxX: float,
maxY: float
) {
const vertices = polygon.vertices;
vertices[0][0] = minX;
vertices[0][1] = minY;
vertices[1][0] = maxX;
vertices[1][1] = minY;
vertices[2][0] = maxX;
vertices[2][1] = maxY;
vertices[3][0] = minX;
vertices[3][1] = maxY;
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"TiledTileMapLoader.d.ts","sourceRoot":"","sources":["../../../src/load/tiled/TiledTileMapLoader.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO7C;;GAEG;AACH,yBAAiB,kBAAkB,CAAC;IAClC;;;;;;OAMG;IACH,SAAgB,IAAI,CAClB,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,GAAG,GACR,eAAe,GAAG,IAAI,CA2KxB;CACF"}
{"version":3,"file":"TiledTileMapLoader.d.ts","sourceRoot":"","sources":["../../../src/load/tiled/TiledTileMapLoader.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO7C;;GAEG;AACH,yBAAiB,kBAAkB,CAAC;IAClC;;;;;;OAMG;IACH,SAAgB,IAAI,CAClB,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,GAAG,GACR,eAAe,GAAG,IAAI,CAkLxB;CACF"}

View File

@@ -431,8 +431,13 @@ export declare class TileDefinition {
* Add a polygon for the collision layer
* @param tag The tag to allow collision layer filtering.
* @param polygon The polygon to use for collisions.
* @param hasFullHitBox Set to `true` when the hitBox cover the whole tile.
*/
addHitBox(tag: string, polygon: PolygonVertices): void;
addHitBox(
tag: string,
polygon: PolygonVertices,
hasFullHitBox: boolean
): void;
/**
* This property is used by {@link TransformedCollisionTileMap}
* to make collision classes.
@@ -446,6 +451,12 @@ export declare class TileDefinition {
* @returns The hit boxes for this tile.
*/
getHitBoxes(tag: string): PolygonVertices[] | undefined;
/**
* Return `true` if the hit-box cover the whole tile.
* @param tag The tag to allow collision layer filtering.
* @returns `true` if the hit-box cover the whole tile.
*/
hasFullHitBox(tag: string): boolean;
/**
* Animated tiles have a limitation:
* they are only able to use frames arranged horizontally one next

File diff suppressed because one or more lines are too long

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;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"}
{"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;IAYP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwBP,WAAW,IAAI,IAAI;CAIpB"}

View File

@@ -231,12 +231,16 @@ namespace gdjs {
);
return;
}
tileDefinition.addHitBox(this._hitBoxTag, [
[0, 0],
[0, tileMap.getTileHeight()],
[tileMap.getTileWidth(), tileMap.getTileHeight()],
[tileMap.getTileWidth(), 0],
]);
tileDefinition.addHitBox(
this._hitBoxTag,
[
[0, 0],
[0, tileMap.getTileHeight()],
[tileMap.getTileWidth(), tileMap.getTileHeight()],
[tileMap.getTileWidth(), 0],
],
true
);
});
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
@@ -624,7 +628,38 @@ namespace gdjs {
columnIndex: integer,
rowIndex: integer
) {
this._renderer.setTileId(columnIndex, rowIndex, 0, tileId);
const tileMap = this._renderer._tileMap;
if (!tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
const oldTileId = layer.getTileId(columnIndex, rowIndex);
if (tileId === oldTileId) {
return;
}
layer.setTile(columnIndex, rowIndex, tileId);
if (this._collisionTileMap) {
const oldTileDefinition =
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
const newTileDefinition = tileMap.getTileDefinition(tileId);
const hadFullHitBox =
!!oldTileDefinition &&
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
const haveFullHitBox =
!!newTileDefinition &&
newTileDefinition.hasFullHitBox(this._hitBoxTag);
if (hadFullHitBox !== haveFullHitBox) {
this._collisionTileMap.invalidateTile(
this._layerIndex,
columnIndex,
rowIndex
);
}
}
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
@@ -702,7 +737,26 @@ namespace gdjs {
}
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
this._renderer.removeTile(columnIndex, rowIndex, 0);
const tileMap = this._renderer._tileMap;
if (!tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
const oldTileId = layer.getTileId(columnIndex, rowIndex);
if (oldTileId === undefined) {
return;
}
layer.removeTile(columnIndex, rowIndex);
if (this._collisionTileMap) {
this._collisionTileMap.invalidateTile(
this._layerIndex,
columnIndex,
rowIndex
);
}
this._isTileMapDirty = true;
this.invalidateHitboxes();
}

View File

@@ -10,7 +10,8 @@ namespace gdjs {
private _object:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject;
private _tileMap: TileMapHelper.EditableTileMap | null = null;
// TODO Move this attribute in the object as it's a model.
_tileMap: TileMapHelper.EditableTileMap | null = null;
private _pixiObject: PIXI.tilemap.CompositeTilemap;
@@ -173,18 +174,6 @@ namespace gdjs {
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.
@@ -231,17 +220,6 @@ namespace gdjs {
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.
*/

View File

@@ -87,6 +87,7 @@ gd::String EventsCodeGenerator::GenerateEventsListCompleteFunctionCode(
functionPreEventsCode + "\n" +
globalObjectListsReset + "\n" +
wholeEventsCode + "\n" +
globalObjectListsReset + "\n" +
functionPostEventsCode + "\n" +
functionReturnCode + "\n" +
"}\n";

View File

@@ -131,8 +131,11 @@ gd::ObjectMetadata &MetadataDeclarationHelper::DeclareObjectMetadata(
// PlatformExtension but this line will be removed soon.
.SetCategoryFullName(extension.GetCategory())
.AddDefaultBehavior("ResizableCapability::ResizableBehavior")
.AddDefaultBehavior("ScalableCapability::ScalableBehavior")
.AddDefaultBehavior("FlippableCapability::FlippableBehavior");
if (!eventsBasedObject.IsInnerAreaFollowingParentSize()) {
objectMetadata
.AddDefaultBehavior("ScalableCapability::ScalableBehavior");
}
if (eventsBasedObject.IsRenderedIn3D()) {
objectMetadata
.MarkAsRenderedIn3D()

View File

@@ -101,6 +101,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
usedExtensionsResult.Has3DObjects(),
/*includeWebsocketDebuggerClient=*/false,
/*includeWindowMessageDebuggerClient=*/false,
/*includeMinimalDebuggerClient=*/false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);

View File

@@ -96,7 +96,7 @@ static gd::String CleanProjectName(gd::String projectName) {
ExporterHelper::ExporterHelper(gd::AbstractFileSystem &fileSystem,
gd::String gdjsRoot_,
gd::String codeOutputDir_)
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_){};
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_) {};
bool ExporterHelper::ExportProjectForPixiPreview(
const PreviewExportOptions &options) {
@@ -154,6 +154,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
!options.websocketDebuggerServerAddress.empty(),
/*includeWindowMessageDebuggerClient=*/
options.useWindowMessageDebuggerClient,
/*includeMinimalDebuggerClient=*/
options.useMinimalDebuggerClient,
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
@@ -248,6 +250,24 @@ bool ExporterHelper::ExportProjectForPixiPreview(
runtimeGameOptions.AddChild("playerToken")
.SetStringValue(options.playerToken);
}
if (!options.crashReportUploadLevel.empty()) {
runtimeGameOptions.AddChild("crashReportUploadLevel")
.SetStringValue(options.crashReportUploadLevel);
}
if (!options.previewContext.empty()) {
runtimeGameOptions.AddChild("previewContext")
.SetStringValue(options.previewContext);
}
runtimeGameOptions.AddChild("gdevelopVersionWithHash")
.SetStringValue(options.gdevelopVersionWithHash);
if (!options.projectTemplateSlug.empty()) {
runtimeGameOptions.AddChild("projectTemplateSlug")
.SetStringValue(options.projectTemplateSlug);
}
if (!options.sourceGameId.empty()) {
runtimeGameOptions.AddChild("sourceGameId")
.SetStringValue(options.sourceGameId);
}
// Pass in the options the list of scripts files - useful for hot-reloading.
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
@@ -735,6 +755,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool pixiInThreeRenderers,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles) {
// First, do not forget common includes (they must be included before events
@@ -809,6 +830,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles,
"debugger-client/window-message-debugger-client.js");
}
if (includeMinimalDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/minimal-debugger-client.js");
}
if (pixiInThreeRenderers) {
InsertUnique(includesFiles, "pixi-renderers/three.js");

View File

@@ -37,6 +37,7 @@ struct PreviewExportOptions {
: project(project_),
exportPath(exportPath_),
useWindowMessageDebuggerClient(false),
useMinimalDebuggerClient(false),
nativeMobileApp(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
@@ -93,6 +94,14 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set that the game should have a minimal debugger client.
*/
PreviewExportOptions &UseMinimalDebuggerClient() {
useMinimalDebuggerClient = true;
return *this;
}
/**
* \brief Set that the preview is launched from a GDevelop native mobile app
* (iOS or Android).
@@ -203,11 +212,56 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set the level of crash reports to be sent to GDevelop APIs.
*/
PreviewExportOptions &SetCrashReportUploadLevel(
const gd::String& crashReportUploadLevel_) {
crashReportUploadLevel = crashReportUploadLevel_;
return *this;
}
/**
* \brief Set the context of the preview.
*/
PreviewExportOptions &SetPreviewContext(
const gd::String& previewContext_) {
previewContext = previewContext_;
return *this;
}
/**
* \brief Set the GDevelop version so the game is aware of it.
*/
PreviewExportOptions &SetGDevelopVersionWithHash(
const gd::String& gdevelopVersionWithHash_) {
gdevelopVersionWithHash = gdevelopVersionWithHash_;
return *this;
}
/**
* \brief Set the template slug that was used to create the project.
*/
PreviewExportOptions &SetProjectTemplateSlug(
const gd::String& projectTemplateSlug_) {
projectTemplateSlug = projectTemplateSlug_;
return *this;
}
/**
* \brief Set the source game id that was used to create the project.
*/
PreviewExportOptions &SetSourceGameId(const gd::String& sourceGameId_) {
sourceGameId = sourceGameId_;
return *this;
}
gd::Project &project;
gd::String exportPath;
gd::String websocketDebuggerServerAddress;
gd::String websocketDebuggerServerPort;
bool useWindowMessageDebuggerClient;
bool useMinimalDebuggerClient;
gd::String layoutName;
gd::String externalLayoutName;
gd::String fallbackAuthorUsername;
@@ -224,6 +278,11 @@ struct PreviewExportOptions {
gd::String electronRemoteRequirePath;
gd::String gdevelopResourceToken;
bool allowAuthenticationUsingIframeForPreview;
gd::String crashReportUploadLevel;
gd::String previewContext;
gd::String gdevelopVersionWithHash;
gd::String projectTemplateSlug;
gd::String sourceGameId;
};
/**
@@ -326,6 +385,7 @@ class ExporterHelper {
bool pixiInThreeRenderers,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles);

View File

@@ -4,6 +4,8 @@
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('CustomRuntimeObject');
export type ObjectConfiguration = {
content: any;
};
@@ -11,24 +13,6 @@ namespace gdjs {
export type CustomObjectConfiguration = ObjectConfiguration & {
animatable?: SpriteAnimationData[];
childrenContent: { [objectName: string]: ObjectConfiguration & any };
instances: InstanceData[];
layers: LayerData[];
// The flat representation of defaultSize.
areaMinX: float;
areaMinY: float;
areaMinZ: float;
areaMaxX: float;
areaMaxY: float;
areaMaxZ: float;
/**
* A value shared by every object instances.
*
* @see gdjs.CustomRuntimeObject._forcedDefaultSize
**/
defaultSize: {
min: [float, float, float];
max: [float, float, float];
} | null;
};
/**
@@ -56,14 +40,21 @@ namespace gdjs {
/** The dimension of this object is calculated from its children AABBs. */
private _unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
/**
* The default size defined by users in the custom object initial instances editor.
*
* Don't modify it as it would affect every instance.
* The bounds of the object content before any transformation.
* @see gdjs.CustomRuntimeObjectInstanceContainer._initialInnerArea
**/
private _forcedDefaultSize: {
protected _innerArea: {
min: [float, float, float];
max: [float, float, float];
} | null = null;
/**
* When the parent dimensions change:
* - if `false`, the object is stretch proportionally while children local
* positions stay the same ({@link gdjs.CustomRuntimeObject._innerArea} don't change).
* - if `true`, the children local positions need to be adapted by events
* to follow their parent size.
*/
protected _isInnerAreaFollowingParentSize = false;
private _scaleX: float = 1;
private _scaleY: float = 1;
private _flippedX: boolean = false;
@@ -73,6 +64,7 @@ namespace gdjs {
private _localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
private _localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
private _isLocalTransformationDirty: boolean = true;
_type: string;
/**
* @param parent The container the object belongs to
@@ -83,19 +75,50 @@ namespace gdjs {
objectData: ObjectData & CustomObjectConfiguration
) {
super(parent, objectData);
this._type = objectData.type;
this._instanceContainer = new gdjs.CustomRuntimeObjectInstanceContainer(
parent,
this
);
this._renderer = this._createRender();
this._createDefaultSizeIfNeeded(objectData);
this._instanceContainer.loadFrom(objectData);
this._initializeFromObjectData(objectData);
// The generated code calls onCreated at the constructor end
// and onCreated calls its super implementation at its end.
}
private _initializeFromObjectData(
objectData: ObjectData & CustomObjectConfiguration
) {
const eventsBasedObjectData = this._runtimeScene
.getGame()
.getEventsBasedObjectData(objectData.type);
if (!eventsBasedObjectData) {
logger.error(
`A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${objectData.type}".`
);
return;
}
this._isInnerAreaFollowingParentSize =
eventsBasedObjectData.isInnerAreaFollowingParentSize;
if (eventsBasedObjectData.instances.length > 0) {
if (!this._innerArea) {
this._innerArea = {
min: [0, 0, 0],
max: [0, 0, 0],
};
}
this._innerArea.min[0] = eventsBasedObjectData.areaMinX;
this._innerArea.min[1] = eventsBasedObjectData.areaMinY;
this._innerArea.min[2] = eventsBasedObjectData.areaMinZ;
this._innerArea.max[0] = eventsBasedObjectData.areaMaxX;
this._innerArea.max[1] = eventsBasedObjectData.areaMaxY;
this._innerArea.max[2] = eventsBasedObjectData.areaMaxZ;
}
this._instanceContainer.loadFrom(objectData, eventsBasedObjectData);
}
protected abstract _createRender():
| gdjs.CustomRuntimeObject2DRenderer
| gdjs.CustomRuntimeObject3DRenderer;
@@ -104,38 +127,13 @@ namespace gdjs {
reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
super.reinitialize(objectData);
this._createDefaultSizeIfNeeded(objectData);
this._instanceContainer.loadFrom(objectData);
this._initializeFromObjectData(objectData);
this._reinitializeRenderer();
// The generated code calls the onCreated super implementation at the end.
this.onCreated();
}
/**
* Initialize `defaultSize` if it doesn't exist.
* `defaultSize` is shared by every instance to save memory.
*/
private _createDefaultSizeIfNeeded(objectData: CustomObjectConfiguration) {
if (objectData.instances.length > 0) {
if (!objectData.defaultSize) {
objectData.defaultSize = {
min: [
objectData.areaMinX,
objectData.areaMinY,
objectData.areaMinZ,
],
max: [
objectData.areaMaxX,
objectData.areaMaxY,
objectData.areaMaxZ,
],
};
}
this._forcedDefaultSize = objectData.defaultSize;
}
}
updateFromObjectData(
oldObjectData: ObjectData & CustomObjectConfiguration,
newObjectData: ObjectData & CustomObjectConfiguration
@@ -147,7 +145,7 @@ namespace gdjs {
newObjectData.animatable || []
);
}
return this._instanceContainer.updateFrom(oldObjectData, newObjectData);
return true;
}
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
@@ -228,6 +226,10 @@ namespace gdjs {
return this._renderer;
}
getChildrenContainer(): gdjs.RuntimeInstanceContainer {
return this._instanceContainer;
}
onChildrenLocationChanged() {
this._isUntransformedHitBoxesDirty = true;
this.invalidateHitboxes();
@@ -393,8 +395,8 @@ namespace gdjs {
getDrawableX(): float {
let minX = 0;
if (this._forcedDefaultSize) {
minX = this._forcedDefaultSize.min[0];
if (this._innerArea) {
minX = this._innerArea.min[0];
} else {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -415,8 +417,8 @@ namespace gdjs {
getDrawableY(): float {
let minY = 0;
if (this._forcedDefaultSize) {
minY = this._forcedDefaultSize.min[1];
if (this._innerArea) {
minY = this._innerArea.min[1];
} else {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -435,12 +437,64 @@ namespace gdjs {
}
}
/**
* @return the internal left bound of the object according to its children.
*/
getInnerAreaMinX(): number {
if (this._innerArea) {
return this._innerArea.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[0];
}
/**
* @return the internal top bound of the object according to its children.
*/
getInnerAreaMinY(): number {
if (this._innerArea) {
return this._innerArea.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[1];
}
/**
* @return the internal right bound of the object according to its children.
*/
getInnerAreaMaxX(): number {
if (this._innerArea) {
return this._innerArea.max[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[0];
}
/**
* @return the internal bottom bound of the object according to its children.
*/
getInnerAreaMaxY(): number {
if (this._innerArea) {
return this._innerArea.max[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[1];
}
/**
* @return the internal width of the object according to its children.
*/
getUnscaledWidth(): float {
if (this._forcedDefaultSize) {
return this._forcedDefaultSize.max[0] - this._forcedDefaultSize.min[0];
if (this._innerArea) {
return this._innerArea.max[0] - this._innerArea.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -452,8 +506,8 @@ namespace gdjs {
* @return the internal height of the object according to its children.
*/
getUnscaledHeight(): float {
if (this._forcedDefaultSize) {
return this._forcedDefaultSize.max[1] - this._forcedDefaultSize.min[1];
if (this._innerArea) {
return this._innerArea.max[1] - this._innerArea.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -468,10 +522,8 @@ namespace gdjs {
if (this._customCenter) {
return this._customCenter[0];
}
if (this._forcedDefaultSize) {
return (
(this._forcedDefaultSize.min[0] + this._forcedDefaultSize.max[0]) / 2
);
if (this._innerArea) {
return (this._innerArea.min[0] + this._innerArea.max[0]) / 2;
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -486,10 +538,8 @@ namespace gdjs {
if (this._customCenter) {
return this._customCenter[1];
}
if (this._forcedDefaultSize) {
return (
(this._forcedDefaultSize.min[1] + this._forcedDefaultSize.max[1]) / 2
);
if (this._innerArea) {
return (this._innerArea.min[1] + this._innerArea.max[1]) / 2;
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
@@ -547,15 +597,29 @@ namespace gdjs {
setWidth(newWidth: float): void {
const unscaledWidth = this.getUnscaledWidth();
if (unscaledWidth !== 0) {
this.setScaleX(newWidth / unscaledWidth);
if (unscaledWidth === 0) {
return;
}
const scaleX = newWidth / unscaledWidth;
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
this._innerArea.min[0] *= scaleX;
this._innerArea.max[0] *= scaleX;
} else {
this.setScaleX(scaleX);
}
}
setHeight(newHeight: float): void {
const unscaledHeight = this.getUnscaledHeight();
if (unscaledHeight !== 0) {
this.setScaleY(newHeight / unscaledHeight);
if (unscaledHeight === 0) {
return;
}
const scaleY = newHeight / unscaledHeight;
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
this._innerArea.min[1] *= scaleY;
this._innerArea.max[1] *= scaleY;
} else {
this.setScaleY(scaleY);
}
}
@@ -606,6 +670,10 @@ namespace gdjs {
* @param newScale The new scale (must be greater than 0).
*/
setScale(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}
@@ -628,6 +696,10 @@ namespace gdjs {
* @param newScale The new scale (must be greater than 0).
*/
setScaleX(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}
@@ -646,6 +718,10 @@ namespace gdjs {
* @param newScale The new scale (must be greater than 0).
*/
setScaleY(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}

View File

@@ -4,8 +4,6 @@
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('CustomRuntimeObject');
/**
* The instance container of a custom object, containing instances of objects rendered on screen.
*
@@ -19,6 +17,17 @@ namespace gdjs {
/** The object that is built with the instances of this container. */
_customObject: gdjs.CustomRuntimeObject;
_isLoaded: boolean = false;
/**
* The default size defined by users in the custom object initial instances editor.
*
* Don't modify it as it would affect every instance.
*
* @see gdjs.CustomRuntimeObject._innerArea
**/
private _initialInnerArea: {
min: [float, float, float];
max: [float, float, float];
} | null = null;
/**
* @param parent the parent container that contains the object associated
@@ -54,18 +63,15 @@ namespace gdjs {
* @param customObjectData An object containing the container data.
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
*/
loadFrom(customObjectData: ObjectData & CustomObjectConfiguration) {
loadFrom(
customObjectData: ObjectData & CustomObjectConfiguration,
eventsBasedObjectData: EventsBasedObjectData
) {
if (this._isLoaded) {
this.onDestroyFromScene(this._parent);
}
const eventsBasedObjectData = this._runtimeScene
.getGame()
.getEventsBasedObjectData(customObjectData.type);
if (!eventsBasedObjectData) {
logger.error('loadFrom was called without an events-based object');
return;
}
this._setOriginalInnerArea(eventsBasedObjectData);
// Registering objects
for (
@@ -74,16 +80,26 @@ namespace gdjs {
++i
) {
const childObjectData = eventsBasedObjectData.objects[i];
this.registerObject({
...childObjectData,
...customObjectData.childrenContent[childObjectData.name],
});
if (customObjectData.childrenContent) {
this.registerObject({
...childObjectData,
// The custom object overrides its events-based object configuration.
...customObjectData.childrenContent[childObjectData.name],
});
} else {
// The custom object follows its events-based object configuration.
this.registerObject(childObjectData);
}
}
if (customObjectData.layers.length > 0) {
if (eventsBasedObjectData.layers.length > 0) {
// Load layers
for (let i = 0, len = customObjectData.layers.length; i < len; ++i) {
this.addLayer(customObjectData.layers[i]);
for (
let i = 0, len = eventsBasedObjectData.layers.length;
i < len;
++i
) {
this.addLayer(eventsBasedObjectData.layers[i]);
}
} else {
// Add a default layer
@@ -112,7 +128,7 @@ namespace gdjs {
}
this.createObjectsFrom(
customObjectData.instances,
eventsBasedObjectData.instances,
0,
0,
0,
@@ -127,48 +143,29 @@ namespace gdjs {
}
/**
* Called when the container must be updated using the specified
* objectData. This is the case during hot-reload, and is only called if
* the object was modified.
*
* @param oldCustomObjectData The previous data for the object.
* @param newCustomObjectData The new data for the object.
* @returns true if the object was updated, false if it could not
* (i.e: hot-reload is not supported).
* Initialize `_initialInnerArea` if it doesn't exist.
* `_initialInnerArea` is shared by every instance to save memory.
*/
updateFrom(
oldCustomObjectData: ObjectData & CustomObjectConfiguration,
newCustomObjectData: ObjectData & CustomObjectConfiguration
): boolean {
const eventsBasedObjectData = this._runtimeScene
.getGame()
.getEventsBasedObjectData(newCustomObjectData.type);
if (!eventsBasedObjectData) {
logger.error('updateFrom was called without an events-based object');
return false;
}
for (
let i = 0, len = eventsBasedObjectData.objects.length;
i < len;
++i
) {
const childName = eventsBasedObjectData.objects[i].name;
const oldChildData = {
...eventsBasedObjectData.objects[i],
...oldCustomObjectData.childrenContent[childName],
};
const newChildData = {
...eventsBasedObjectData.objects[i],
...newCustomObjectData.childrenContent[childName],
};
this.updateObject(newChildData);
for (const child of this.getInstancesOf(childName)) {
child.updateFromObjectData(oldChildData, newChildData);
private _setOriginalInnerArea(
eventsBasedObjectData: EventsBasedObjectData
) {
if (eventsBasedObjectData.instances.length > 0) {
if (!eventsBasedObjectData._initialInnerArea) {
eventsBasedObjectData._initialInnerArea = {
min: [
eventsBasedObjectData.areaMinX,
eventsBasedObjectData.areaMinY,
eventsBasedObjectData.areaMinZ,
],
max: [
eventsBasedObjectData.areaMaxX,
eventsBasedObjectData.areaMaxY,
eventsBasedObjectData.areaMaxZ,
],
};
}
this._initialInnerArea = eventsBasedObjectData._initialInnerArea;
}
return true;
}
/**
@@ -187,8 +184,10 @@ namespace gdjs {
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const object = allInstancesList[i];
object.onDeletedFromScene(this);
// The object can free all its resource directly...
object.onDestroyed();
}
// ...as its container cache `_instancesRemoved` is also destroy.
this._destroy();
this._isLoaded = false;
@@ -291,6 +290,38 @@ namespace gdjs {
return this._runtimeScene;
}
getUnrotatedViewportMinX(): float {
return this._customObject.getInnerAreaMinX();
}
getUnrotatedViewportMinY(): float {
return this._customObject.getInnerAreaMinY();
}
getUnrotatedViewportMaxX(): float {
return this._customObject.getInnerAreaMaxX();
}
getUnrotatedViewportMaxY(): float {
return this._customObject.getInnerAreaMaxY();
}
getInitialUnrotatedViewportMinX(): float {
return this._initialInnerArea ? this._initialInnerArea.min[0] : 0;
}
getInitialUnrotatedViewportMinY(): float {
return this._initialInnerArea ? this._initialInnerArea.min[1] : 0;
}
getInitialUnrotatedViewportMaxX(): float {
return this._initialInnerArea ? this._initialInnerArea.max[0] : 0;
}
getInitialUnrotatedViewportMaxY(): float {
return this._initialInnerArea ? this._initialInnerArea.max[1] : 0;
}
getViewportWidth(): float {
return this._customObject.getUnscaledWidth();
}

View File

@@ -102,6 +102,70 @@ namespace gdjs {
result: FloatPoint
): FloatPoint;
/**
* @return the left bound of:
* - the game resolution for a {@link gdjs.RuntimeScene}
* - the default dimensions (the AABB of all its children) for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getUnrotatedViewportMinX(): float;
/**
* @return the top bound of:
* - the game resolution for a {@link gdjs.RuntimeScene}
* - the default dimensions (the AABB of all its children) for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getUnrotatedViewportMinY(): float;
/**
* @return the right bound of:
* - the game resolution for a {@link gdjs.RuntimeScene}
* - the default dimensions (the AABB of all its children) for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getUnrotatedViewportMaxX(): float;
/**
* @return the bottom bound of:
* - the game resolution for a {@link gdjs.RuntimeScene}
* - the default dimensions (the AABB of all its children) for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getUnrotatedViewportMaxY(): float;
/**
* @return the left bound of:
* - the initial game resolution for a {@link gdjs.RuntimeScene}
* - the initial default dimensions (inner area) set in the editor for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getInitialUnrotatedViewportMinX(): float;
/**
* @return the top bound of:
* - the initial game resolution for a {@link gdjs.RuntimeScene}
* - the initial default dimensions (inner area) set in the editor for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getInitialUnrotatedViewportMinY(): float;
/**
* @return the right bound of:
* - the initial game resolution for a {@link gdjs.RuntimeScene}
* - the initial default dimensions (inner area) set in the editor for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getInitialUnrotatedViewportMaxX(): float;
/**
* @return the bottom bound of:
* - the initial game resolution for a {@link gdjs.RuntimeScene}
* - the initial default dimensions (inner area) set in the editor for a
* {@link gdjs.CustomRuntimeObject}.
*/
abstract getInitialUnrotatedViewportMaxY(): float;
/**
* @return the width of:
* - the game resolution for a {@link gdjs.RuntimeScene}

View File

@@ -123,6 +123,9 @@ namespace gdjs {
frameData: SpriteFrameData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.image = frameData.image;
this.texture = textureManager.getAnimationFrameTexture(this.image);
this.points.clear();
for (let i = 0, len = frameData.points.length; i < len; ++i) {
const ptData = frameData.points[i];

View File

@@ -79,14 +79,6 @@ namespace gdjs {
},
};
const isErrorComingFromJavaScriptCode = (
exception: Error | null
): boolean => {
if (!exception || !exception.stack) return false;
return exception.stack.includes('GDJSInlineCode');
};
/**
* Displays uncaught exceptions on top of the game.
* Could be reworked in the future to support a minimal debugger inside the game.
@@ -120,7 +112,7 @@ namespace gdjs {
}
if (this._uncaughtException) {
const errorIsInJs = isErrorComingFromJavaScriptCode(
const errorIsInJs = gdjs.AbstractDebuggerClient.isErrorComingFromJavaScriptCode(
this._uncaughtException
);
this._uncaughtExceptionElement = (

View File

@@ -88,6 +88,69 @@ namespace gdjs {
);
};
/** Replacer function for JSON.stringify to convert Error objects into plain objects that can be logged. */
const errorReplacer = (_, value: any) => {
if (value instanceof Error) {
// See https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify
const errorObject = {};
Object.getOwnPropertyNames(value).forEach((prop) => {
errorObject[prop] = value[prop];
});
return errorObject;
}
// Return the value unchanged if it's not an Error object.
return value;
};
const buildGameCrashReport = (
exception: Error,
runtimeGame: gdjs.RuntimeGame
) => {
const sceneNames = runtimeGame.getSceneStack().getAllSceneNames();
const currentScene = runtimeGame.getSceneStack().getCurrentScene();
return {
type: 'javascript-uncaught-exception',
exception,
platformInfo: runtimeGame.getPlatformInfo(),
playerId: runtimeGame.getPlayerId(),
sessionId: runtimeGame.getSessionId(),
isPreview: runtimeGame.isPreview(),
gdevelop: {
previewContext: runtimeGame.getAdditionalOptions().previewContext,
isNativeMobileApp: runtimeGame.getAdditionalOptions().nativeMobileApp,
versionWithHash: runtimeGame.getAdditionalOptions()
.gdevelopVersionWithHash,
environment: runtimeGame.getAdditionalOptions().environment,
},
game: {
gameId: gdjs.projectData.properties.projectUuid,
name: runtimeGame.getGameData().properties.name || '',
packageName: runtimeGame.getGameData().properties.packageName || '',
version: runtimeGame.getGameData().properties.version || '',
location: window.location.href,
projectTemplateSlug: runtimeGame.getAdditionalOptions()
.projectTemplateSlug,
sourceGameId: runtimeGame.getAdditionalOptions().sourceGameId,
},
gameState: {
sceneNames,
isWebGLSupported: runtimeGame.getRenderer().isWebGLSupported(),
hasPixiRenderer: !!runtimeGame.getRenderer().getPIXIRenderer(),
hasThreeRenderer: !!runtimeGame.getRenderer().getThreeRenderer(),
resourcesTotalCount: runtimeGame.getGameData().resources.resources
.length,
antialiasingMode: runtimeGame.getAntialiasingMode(),
isAntialisingEnabledOnMobile: runtimeGame.isAntialisingEnabledOnMobile(),
scriptFiles: runtimeGame.getAdditionalOptions().scriptFiles,
currentSceneTimeFromStart: currentScene
? currentScene.getTimeManager().getTimeFromStart()
: null,
gdjsKeys: Object.keys(gdjs).slice(0, 1000),
},
};
};
/**
* The base class describing a debugger client, that can be used to inspect
* a runtime game (dump its state) or alter it.
@@ -98,6 +161,8 @@ namespace gdjs {
_originalConsole = originalConsole;
_inGameDebugger: gdjs.InGameDebugger;
_hasLoggedUncaughtException = false;
constructor(runtimeGame: RuntimeGame) {
this._runtimegame = runtimeGame;
this._hotReloader = new gdjs.HotReloader(runtimeGame);
@@ -213,10 +278,68 @@ namespace gdjs {
*/
protected abstract _sendMessage(message: string): void;
static isErrorComingFromJavaScriptCode(exception: Error | null): boolean {
if (!exception || !exception.stack) return false;
return exception.stack.includes('GDJSInlineCode');
}
async _reportCrash(exception: Error) {
const gameCrashReport = buildGameCrashReport(
exception,
this._runtimegame
);
// Let a debugger server know about the crash.
this._sendMessage(
circularSafeStringify(
{
command: 'game.crashed',
payload: gameCrashReport,
},
errorReplacer
)
);
// Send the report to the APIs, if allowed.
if (
!this._runtimegame.getAdditionalOptions().crashReportUploadLevel ||
this._runtimegame.getAdditionalOptions().crashReportUploadLevel ===
'none' ||
(this._runtimegame.getAdditionalOptions().crashReportUploadLevel ===
'exclude-javascript-code-events' &&
AbstractDebuggerClient.isErrorComingFromJavaScriptCode(exception))
) {
return;
}
const rootApi = this._runtimegame.isUsingGDevelopDevelopmentEnvironment()
? 'https://api-dev.gdevelop.io'
: 'https://api.gdevelop.io';
const baseUrl = `${rootApi}/analytics`;
try {
await fetch(`${baseUrl}/game-crash-report`, {
body: circularSafeStringify(gameCrashReport, errorReplacer),
method: 'POST',
});
} catch (error) {
logger.error('Error while sending the crash report:', error);
}
}
onUncaughtException(exception: Error): void {
logger.error('Uncaught exception: ' + exception);
this._inGameDebugger.setUncaughtException(exception);
if (!this._hasLoggedUncaughtException) {
// Only log an uncaught exception once, to avoid spamming the debugger server
// in case of an exception at each frame.
this._hasLoggedUncaughtException = true;
this._reportCrash(exception);
}
}
/**

View File

@@ -41,6 +41,17 @@ namespace gdjs {
}, {});
}
static indexByName<E extends { name: string | null }>(
objectsWithName: E[]
): Map<string, E> {
return objectsWithName.reduce(function (objectsMap, object) {
if (object.name) {
objectsMap.set(object.name, object);
}
return objectsMap;
}, new Map<string, E>());
}
_canReloadScriptFile(srcFilename: string): boolean {
function endsWith(str: string, suffix: string): boolean {
const suffixPosition = str.indexOf(suffix);
@@ -193,19 +204,22 @@ namespace gdjs {
if (errorTarget instanceof HTMLScriptElement) {
this._logs.push({
kind: 'fatal',
message: 'Unable to reload script:' + errorTarget.src,
message: 'Unable to reload script: ' + errorTarget.src,
});
} else {
this._logs.push({
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading:' +
'Unexpected error happened while hot-reloading: ' +
error.message,
});
}
})
.then(() => {
logger.info('Hot reload finished with logs:', this._logs);
logger.info(
'Hot reload finished with logs:',
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
);
this._runtimeGame.pause(false);
return this._logs;
});
@@ -338,28 +352,90 @@ namespace gdjs {
runtimeGame.getVariables()
);
// Update extension's global variables.
for (const newExtensionData of newProjectData.eventsFunctionsExtensions) {
const oldExtensionData = oldProjectData.eventsFunctionsExtensions.find(
(oldExtensionData) => oldExtensionData.name === newExtensionData.name
);
const oldGlobalVariables = oldExtensionData
? oldExtensionData.globalVariables
: [];
const newGlobalVariables = newExtensionData.globalVariables;
if (oldGlobalVariables.length > 0 || newGlobalVariables.length > 0) {
const currentVariables = runtimeGame.getVariablesForExtension(
newExtensionData.name
);
if (currentVariables) {
this._hotReloadVariablesContainer(
oldGlobalVariables,
newGlobalVariables,
currentVariables
);
} else {
runtimeGame._variablesByExtensionName.set(
newExtensionData.name,
new gdjs.VariablesContainer(newGlobalVariables)
);
}
}
}
const oldlayoutDataMap = HotReloader.indexByName(oldProjectData.layouts);
const newlayoutDataMap = HotReloader.indexByName(newProjectData.layouts);
// Reload runtime scenes
sceneStack._stack.forEach((runtimeScene) => {
const oldLayoutData = oldProjectData.layouts.filter(
(layoutData) => layoutData.name === runtimeScene.getName()
)[0];
const newLayoutData = newProjectData.layouts.filter(
(layoutData) => layoutData.name === runtimeScene.getName()
)[0];
const oldLayoutData = oldlayoutDataMap.get(runtimeScene.getName());
const newLayoutData = newlayoutDataMap.get(runtimeScene.getName());
if (oldLayoutData && newLayoutData) {
this._hotReloadRuntimeScene(
oldProjectData,
newProjectData,
oldLayoutData,
newLayoutData,
changedRuntimeBehaviors,
runtimeScene
);
// Update extension's scene variables.
for (const newExtensionData of newProjectData.eventsFunctionsExtensions) {
const oldExtensionData = oldProjectData.eventsFunctionsExtensions.find(
(oldExtensionData) =>
oldExtensionData.name === newExtensionData.name
);
const oldSceneVariables = oldExtensionData
? oldExtensionData.sceneVariables
: [];
const newSceneVariables = newExtensionData.sceneVariables;
if (oldSceneVariables.length > 0 || newSceneVariables.length > 0) {
const currentVariables = runtimeScene.getVariablesForExtension(
newExtensionData.name
);
if (currentVariables) {
this._hotReloadVariablesContainer(
oldSceneVariables,
newSceneVariables,
currentVariables
);
} else {
runtimeScene._variablesByExtensionName.set(
newExtensionData.name,
new gdjs.VariablesContainer(newSceneVariables)
);
}
}
}
} else {
// A scene was removed. Not hot-reloading this.
this._logs.push({
kind: 'error',
message:
'Scene ' +
oldLayoutData.name +
(oldLayoutData && oldLayoutData.name) +
' was removed. A fresh preview should be launched.',
});
}
@@ -377,8 +453,20 @@ namespace gdjs {
// hot-reload all the scenes.
!HotReloader.deepEqual(oldExternalLayoutData, newExternalLayoutData)
) {
const oldLayoutData = oldlayoutDataMap.get(
oldExternalLayoutData.associatedLayout
);
const newLayoutData = newlayoutDataMap.get(
newExternalLayoutData.associatedLayout
);
sceneStack._stack.forEach((runtimeScene) => {
this._hotReloadRuntimeSceneInstances(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
oldLayoutData ? oldLayoutData.objects : [],
newLayoutData ? newLayoutData.objects : [],
oldExternalLayoutData.instances,
newExternalLayoutData.instances,
runtimeScene
@@ -455,6 +543,7 @@ namespace gdjs {
variablesContainer.remove(oldVariableData.name);
}
});
variablesContainer.rebuildIndexFrom(newVariablesData);
}
_hotReloadStructureVariable(
@@ -535,6 +624,8 @@ namespace gdjs {
}
_hotReloadRuntimeScene(
oldProjectData: ProjectData,
newProjectData: ProjectData,
oldLayoutData: LayoutData,
newLayoutData: LayoutData,
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
@@ -562,25 +653,12 @@ namespace gdjs {
runtimeScene
);
// Re-instantiate any gdjs.RuntimeBehavior that was changed.
this._reinstantiateRuntimeSceneRuntimeBehaviors(
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldLayoutData,
newLayoutData,
changedRuntimeBehaviors,
newLayoutData.objects,
runtimeScene
);
this._hotReloadRuntimeSceneObjects(
oldLayoutData.objects,
newLayoutData.objects,
runtimeScene
);
this._hotReloadRuntimeSceneInstances(
oldLayoutData.instances,
newLayoutData.instances,
runtimeScene
);
this._hotReloadRuntimeSceneLayers(
oldLayoutData.layers,
newLayoutData.layers,
runtimeScene
);
@@ -591,6 +669,101 @@ namespace gdjs {
runtimeScene.setEventsGeneratedCodeFunction(newLayoutData);
}
static resolveCustomObjectConfigurations(
projectData: ProjectData,
objectDatas: ObjectData[]
): ObjectData[] {
return objectDatas.map((objectData) => {
const [extensionName, eventsBasedObjectName] = objectData.type.split(
'::'
);
const extensionData = projectData.eventsFunctionsExtensions.find(
(extension) => extension.name === extensionName
);
if (!extensionData) {
return objectData;
}
const eventsBasedObjectData =
extensionData &&
extensionData.eventsBasedObjects.find(
(object) => object.name === eventsBasedObjectName
);
if (!eventsBasedObjectData) {
return objectData;
}
const customObjectConfiguration = objectData as ObjectData &
CustomObjectConfiguration;
const mergedChildObjectDataList = customObjectConfiguration.childrenContent
? eventsBasedObjectData.objects.map((objectData) => ({
...objectData,
...customObjectConfiguration.childrenContent[objectData.name],
}))
: eventsBasedObjectData.objects;
const mergedObjectConfiguration = {
...eventsBasedObjectData,
...objectData,
// ObjectData doesn't have an `objects` attribute.
// This is a small optimization to avoid to create an
// InstanceContainerData for each instance to hot-reload their inner
// scene (see `_hotReloadRuntimeInstanceContainer` call from
// `_hotReloadRuntimeSceneInstances`).
objects: mergedChildObjectDataList,
childrenContent: mergedChildObjectDataList,
};
return mergedObjectConfiguration;
});
}
_hotReloadRuntimeInstanceContainer(
oldProjectData: ProjectData,
newProjectData: ProjectData,
oldLayoutData: InstanceContainerData,
newLayoutData: InstanceContainerData,
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations(
oldProjectData,
oldLayoutData.objects
);
const newObjectDataList = HotReloader.resolveCustomObjectConfigurations(
newProjectData,
newLayoutData.objects
);
// Re-instantiate any gdjs.RuntimeBehavior that was changed.
this._reinstantiateRuntimeSceneRuntimeBehaviors(
changedRuntimeBehaviors,
newObjectDataList,
runtimeInstanceContainer
);
this._hotReloadRuntimeSceneObjects(
oldObjectDataList,
newObjectDataList,
runtimeInstanceContainer
);
this._hotReloadRuntimeSceneInstances(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
oldObjectDataList,
newObjectDataList,
oldLayoutData.instances,
newLayoutData.instances,
runtimeInstanceContainer
);
this._hotReloadRuntimeSceneLayers(
oldLayoutData.layers,
newLayoutData.layers,
runtimeInstanceContainer
);
}
_hotReloadRuntimeSceneBehaviorsSharedData(
oldBehaviorsSharedData: BehaviorSharedData[],
newBehaviorsSharedData: BehaviorSharedData[],
@@ -637,12 +810,12 @@ namespace gdjs {
_reinstantiateRuntimeSceneRuntimeBehaviors(
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
newObjects: ObjectData[],
runtimeScene: gdjs.RuntimeScene
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
newObjects.forEach((newObjectData) => {
const objectName = newObjectData.name;
const newBehaviors = newObjectData.behaviors;
const runtimeObjects = runtimeScene.getObjects(objectName)!;
const runtimeObjects = runtimeInstanceContainer.getObjects(objectName)!;
changedRuntimeBehaviors.forEach((changedRuntimeBehavior) => {
const behaviorTypeName = changedRuntimeBehavior.behaviorTypeName;
@@ -727,7 +900,7 @@ namespace gdjs {
_hotReloadRuntimeSceneObjects(
oldObjects: ObjectData[],
newObjects: ObjectData[],
runtimeScene: gdjs.RuntimeScene
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
oldObjects.forEach((oldObjectData) => {
const name = oldObjectData.name;
@@ -742,13 +915,13 @@ namespace gdjs {
// its name (it's not expected to change).
if (!newObjectData || oldObjectData.type !== newObjectData.type) {
// Object was removed or object type was changed (considered as a removal of the old object)
runtimeScene.unregisterObject(name);
runtimeInstanceContainer.unregisterObject(name);
} else {
if (runtimeScene.isObjectRegistered(name)) {
if (runtimeInstanceContainer.isObjectRegistered(name)) {
this._hotReloadRuntimeSceneObject(
oldObjectData,
newObjectData,
runtimeScene
runtimeInstanceContainer
);
}
}
@@ -760,10 +933,10 @@ namespace gdjs {
)[0];
if (
(!oldObjectData || oldObjectData.type !== newObjectData.type) &&
!runtimeScene.isObjectRegistered(name)
!runtimeInstanceContainer.isObjectRegistered(name)
) {
// Object was added or object type was changed (considered as adding the new object)
runtimeScene.registerObject(newObjectData);
runtimeInstanceContainer.registerObject(newObjectData);
}
});
}
@@ -771,7 +944,7 @@ namespace gdjs {
_hotReloadRuntimeSceneObject(
oldObjectData: ObjectData,
newObjectData: ObjectData,
runtimeScene: gdjs.RuntimeScene
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
let hotReloadSucceeded = true;
if (!HotReloader.deepEqual(oldObjectData, newObjectData)) {
@@ -784,10 +957,12 @@ namespace gdjs {
});
// Register the updated object data, used for new instances.
runtimeScene.updateObject(newObjectData);
runtimeInstanceContainer.updateObject(newObjectData);
// Update existing instances
const runtimeObjects = runtimeScene.getObjects(newObjectData.name)!;
const runtimeObjects = runtimeInstanceContainer.getObjects(
newObjectData.name
)!;
// Update instances state
runtimeObjects.forEach((runtimeObject) => {
@@ -1005,7 +1180,7 @@ namespace gdjs {
_hotReloadRuntimeSceneLayers(
oldLayers: LayerData[],
newLayers: LayerData[],
runtimeScene: gdjs.RuntimeScene
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
oldLayers.forEach((oldLayerData) => {
const name = oldLayerData.name;
@@ -1014,10 +1189,10 @@ namespace gdjs {
)[0];
if (!newLayerData) {
// Layer was removed
runtimeScene.removeLayer(name);
runtimeInstanceContainer.removeLayer(name);
} else {
if (runtimeScene.hasLayer(name)) {
const layer = runtimeScene.getLayer(name);
if (runtimeInstanceContainer.hasLayer(name)) {
const layer = runtimeInstanceContainer.getLayer(name);
this._hotReloadRuntimeLayer(oldLayerData, newLayerData, layer);
}
}
@@ -1027,13 +1202,13 @@ namespace gdjs {
const oldLayerData = oldLayers.filter(
(layerData) => layerData.name === name
)[0];
if (!oldLayerData && !runtimeScene.hasLayer(name)) {
if (!oldLayerData && !runtimeInstanceContainer.hasLayer(name)) {
// Layer was added
runtimeScene.addLayer(newLayerData);
runtimeInstanceContainer.addLayer(newLayerData);
}
});
newLayers.forEach((newLayerData, index) => {
runtimeScene.setLayerIndex(newLayerData.name, index);
runtimeInstanceContainer.setLayerIndex(newLayerData.name, index);
});
}
@@ -1168,11 +1343,16 @@ namespace gdjs {
}
_hotReloadRuntimeSceneInstances(
oldProjectData: ProjectData,
newProjectData: ProjectData,
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
oldObjects: ObjectData[],
newObjects: ObjectData[],
oldInstances: InstanceData[],
newInstances: InstanceData[],
runtimeScene: gdjs.RuntimeScene
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
const runtimeObjects = runtimeScene.getAdhocListOfAllInstances();
const runtimeObjects = runtimeInstanceContainer.getAdhocListOfAllInstances();
const groupedOldInstances: {
[key: number]: InstanceData;
} = HotReloader.groupByPersistentUuid(oldInstances);
@@ -1182,30 +1362,73 @@ namespace gdjs {
const groupedRuntimeObjects: {
[key: number]: gdjs.RuntimeObject;
} = HotReloader.groupByPersistentUuid(runtimeObjects);
for (let persistentUuid in groupedOldInstances) {
const oldObjectsMap = HotReloader.indexByName(oldObjects);
const newObjectsMap = HotReloader.indexByName(newObjects);
for (const persistentUuid in groupedOldInstances) {
const oldInstance = groupedOldInstances[persistentUuid];
const newInstance = groupedNewInstances[persistentUuid];
const runtimeObject = groupedRuntimeObjects[persistentUuid];
if (
oldInstance &&
(!newInstance || oldInstance.name !== newInstance.name)
) {
// Instance was deleted (or object name changed, in which case it will be re-created later)
if (runtimeObject) {
runtimeObject.deleteFromScene(runtimeScene);
runtimeObject.deleteFromScene(runtimeInstanceContainer);
}
} else {
if (oldInstance && newInstance && runtimeObject) {
// Instance was not deleted nor created, maybe modified (or not):
this._hotReloadRuntimeInstance(
oldInstance,
newInstance,
runtimeObject
);
}
}
}
for (let persistentUuid in groupedNewInstances) {
for (const persistentUuid in groupedRuntimeObjects) {
const runtimeObject = groupedRuntimeObjects[persistentUuid];
const oldObjectData = oldObjectsMap.get(runtimeObject.getName());
const newObjectData = newObjectsMap.get(runtimeObject.getName());
if (!runtimeObject || !oldObjectData || !newObjectData) {
// New objects or deleted objects can't have instances to hot-reload.
continue;
}
const oldInstance = groupedOldInstances[persistentUuid];
const newInstance = groupedNewInstances[persistentUuid];
if (oldInstance && newInstance) {
// Instance was not deleted nor created, maybe modified (or not):
this._hotReloadRuntimeInstance(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
oldObjectData,
newObjectData,
oldInstance,
newInstance,
runtimeObject
);
} else if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
const oldCustomObjectData = oldObjectData as ObjectData &
CustomObjectConfiguration &
InstanceContainerData;
const newCustomObjectData = newObjectData as ObjectData &
CustomObjectConfiguration &
InstanceContainerData;
// Reload the content of custom objects that were created at runtime.
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
}
}
for (const persistentUuid in groupedNewInstances) {
const oldInstance = groupedOldInstances[persistentUuid];
const newInstance = groupedNewInstances[persistentUuid];
const runtimeObject = groupedRuntimeObjects[persistentUuid];
@@ -1216,7 +1439,7 @@ namespace gdjs {
) {
// Instance was created (or object name changed, in which case it was destroyed previously)
// and we verified that runtimeObject does not exist.
runtimeScene.createObjectsFrom(
runtimeInstanceContainer.createObjectsFrom(
[newInstance],
0,
0,
@@ -1229,6 +1452,11 @@ namespace gdjs {
}
_hotReloadRuntimeInstance(
oldProjectData: ProjectData,
newProjectData: ProjectData,
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
oldObjectData: ObjectData,
newObjectData: ObjectData,
oldInstance: InstanceData,
newInstance: InstanceData,
runtimeObject: gdjs.RuntimeObject
@@ -1326,6 +1554,26 @@ namespace gdjs {
sizeChanged = true;
}
}
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
const oldCustomObjectData = oldObjectData as ObjectData &
CustomObjectConfiguration &
InstanceContainerData;
const newCustomObjectData = newObjectData as ObjectData &
CustomObjectConfiguration &
InstanceContainerData;
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
}
// Update variables
this._hotReloadVariablesContainer(

View File

@@ -0,0 +1,16 @@
namespace gdjs {
/**
* Does nothing apart from allowing to reporting errors.
*/
export class MinimalDebuggerClient extends gdjs.AbstractDebuggerClient {
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
}
protected _sendMessage(message: string) {}
}
//Register the class to let the engine use it.
// @ts-ignore
export const DebuggerClient = WindowMessageDebuggerClient;
}

View File

@@ -8,9 +8,6 @@ namespace gdjs {
export class WebsocketDebuggerClient extends gdjs.AbstractDebuggerClient {
_ws: WebSocket | null;
/**
* @param path - The path of the property to modify, starting from the RuntimeGame.
*/
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
this._ws = null;

View File

@@ -8,9 +8,6 @@ namespace gdjs {
export class WindowMessageDebuggerClient extends gdjs.AbstractDebuggerClient {
_opener: Window | null = null;
/**
* @param path - The path of the property to modify, starting from the RuntimeGame.
*/
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);

View File

@@ -72,7 +72,7 @@ namespace gdjs {
electronRemoteRequirePath?: string;
/**
* the token to use by the game engine when requiring any resource stored on
* The token to use by the game engine when requiring any resource stored on
* GDevelop Cloud buckets. Note that this is only useful during previews.
*/
gdevelopResourceToken?: string;
@@ -85,6 +85,21 @@ namespace gdjs {
*/
allowAuthenticationUsingIframeForPreview?: boolean;
/** If set, the game will send crash reports to GDevelop APIs. */
crashReportUploadLevel?: 'all' | 'exclude-javascript-code-events' | 'none';
/** Arbitrary string explaining in which context the game is being played. */
previewContext?: string;
/** The GDevelop version used to build the game. */
gdevelopVersionWithHash?: string;
/** The template slug that was used to create the project. */
projectTemplateSlug?: string;
/** The source game id that was used to create the project. */
sourceGameId?: string;
/**
* If set, this data is used to authenticate automatically when launching the game.
* This is only useful during previews.
@@ -189,6 +204,7 @@ namespace gdjs {
);
}
}
this._eventsBasedObjectDatas = new Map<String, EventsBasedObjectData>();
this._data = data;
this._updateSceneAndExtensionsData();
@@ -250,18 +266,6 @@ namespace gdjs {
}
}
this._eventsBasedObjectDatas = new Map<String, EventsBasedObjectData>();
if (this._data.eventsFunctionsExtensions) {
for (const extension of this._data.eventsFunctionsExtensions) {
for (const eventsBasedObject of extension.eventsBasedObjects) {
this._eventsBasedObjectDatas.set(
extension.name + '::' + eventsBasedObject.name,
eventsBasedObject
);
}
}
}
if (this.isUsingGDevelopDevelopmentEnvironment()) {
logger.info(
'This game will run on the development version of GDevelop APIs.'
@@ -292,6 +296,18 @@ namespace gdjs {
sceneData,
usedExtensionsWithVariablesData,
}));
this._eventsBasedObjectDatas.clear();
if (this._data.eventsFunctionsExtensions) {
for (const extension of this._data.eventsFunctionsExtensions) {
for (const eventsBasedObject of extension.eventsBasedObjects) {
this._eventsBasedObjectDatas.set(
extension.name + '::' + eventsBasedObject.name,
eventsBasedObject
);
}
}
}
}
/**

View File

@@ -605,6 +605,38 @@ namespace gdjs {
return this;
}
getUnrotatedViewportMinX(): float {
return 0;
}
getUnrotatedViewportMinY(): float {
return 0;
}
getUnrotatedViewportMaxX(): float {
return this._cachedGameResolutionWidth;
}
getUnrotatedViewportMaxY(): float {
return this._cachedGameResolutionHeight;
}
getInitialUnrotatedViewportMinX(): float {
return 0;
}
getInitialUnrotatedViewportMinY(): float {
return 0;
}
getInitialUnrotatedViewportMaxX(): float {
return this.getGame().getOriginalWidth();
}
getInitialUnrotatedViewportMaxY(): float {
return this.getGame().getOriginalHeight();
}
getViewportWidth(): float {
return this._cachedGameResolutionWidth;
}

View File

@@ -208,11 +208,16 @@ namespace gdjs {
return this._wasFirstSceneLoaded;
}
getAllSceneNames(): Array<string> {
return this._stack.map((scene) => scene.getName());
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SceneStackNetworkSyncData | null {
const syncedPlayerNumber = syncOptions.playerNumber;
if (syncedPlayerNumber !== undefined && syncedPlayerNumber !== 1) {
const isHost = syncOptions.isHost;
if (syncedPlayerNumber !== undefined && !isHost) {
// If we are getting sync data of a specific player,
// and they are not the host, we don't sync the scene stack.
return null;

View File

@@ -109,6 +109,7 @@ namespace gdjs {
newObjectData.animations
);
this._updateIfNotVisible = !!newObjectData.updateIfNotVisible;
this._updateAnimationFrame();
this.invalidateHitboxes();
return true;
}

View File

@@ -39,7 +39,10 @@ declare type ObjectData = {
effects: Array<EffectData>;
};
declare type GetNetworkSyncDataOptions = { playerNumber?: number };
declare type GetNetworkSyncDataOptions = {
playerNumber?: number;
isHost?: boolean;
};
/** Object containing basic properties for all objects synchronizing over the network. */
declare type BasicObjectNetworkSyncData = {
@@ -149,7 +152,14 @@ declare interface GdVersionData {
revision: number;
}
declare interface LayoutData {
declare interface InstanceContainerData {
variables: RootVariableData[];
instances: InstanceData[];
objects: ObjectData[];
layers: LayerData[];
}
declare interface LayoutData extends InstanceContainerData {
r: number;
v: number;
b: number;
@@ -157,10 +167,6 @@ declare interface LayoutData {
name: string;
stopSoundsOnStartup: boolean;
title: string;
variables: RootVariableData[];
instances: InstanceData[];
objects: ObjectData[];
layers: LayerData[];
behaviorsSharedData: BehaviorSharedData[];
usedResources: ResourceReference[];
}
@@ -200,9 +206,25 @@ declare interface SceneAndExtensionsData {
usedExtensionsWithVariablesData: EventsFunctionsExtensionData[];
}
declare interface EventsBasedObjectData {
declare interface EventsBasedObjectData extends InstanceContainerData {
name: string;
objects: Array<ObjectData & any>;
isInnerAreaFollowingParentSize: boolean;
// The flat representation of defaultSize.
areaMinX: float;
areaMinY: float;
areaMinZ: float;
areaMaxX: float;
areaMaxY: float;
areaMaxZ: float;
/**
* A value shared by every object instances.
*
* @see gdjs.CustomRuntimeObjectInstanceContainer._originalInnerArea
**/
_initialInnerArea: {
min: [float, float, float];
max: [float, float, float];
} | null;
}
declare interface BehaviorSharedData {
@@ -212,6 +234,7 @@ declare interface BehaviorSharedData {
declare interface ExternalLayoutData {
name: string;
associatedLayout: string;
instances: InstanceData[];
}

View File

@@ -86,6 +86,16 @@ namespace gdjs {
}
}
rebuildIndexFrom(data: VariableData[]) {
this._variablesArray.length = 0;
for (const variableData of data) {
if (variableData.name) {
const variable = this._variables.get(variableData.name);
this._variablesArray.push(variable);
}
}
}
/**
* Declare a new variable.
* This should only be used by generated code.
@@ -224,6 +234,7 @@ namespace gdjs {
syncOptions: GetNetworkSyncDataOptions
): VariableNetworkSyncData[] {
const syncedPlayerNumber = syncOptions.playerNumber;
const isHost = syncOptions.isHost;
const networkSyncData: VariableNetworkSyncData[] = [];
const variableNames = [];
this._variables.keys(variableNames);
@@ -237,10 +248,10 @@ namespace gdjs {
variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Owned by host but we are not player 1.
// Variable is owned by host but this player number is not the host.
variableOwner === 0 &&
syncedPlayerNumber !== 1) ||
// Owned by a player but we are not this player.
!isHost) ||
// Variable is owned by a player but not getting sync data for this player number.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
) {
// In those cases, the variable should not be synchronized.
@@ -442,6 +453,9 @@ namespace gdjs {
getVariableNameInContainerByLoopingThroughAllVariables: function () {
return '';
},
rebuildIndexFrom: function () {
return;
},
};
/**

View File

@@ -161,6 +161,7 @@ module.exports = function (config) {
'./GDJS/tests/tests-utils/init.js',
'./GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js',
'./GDJS/tests/tests-utils/init.pixiruntimegame.js',
'./GDJS/tests/tests-utils/MockedCustomObject.js',
// Test helpers
'./Extensions/PlatformBehavior/tests/PlatformerTestHelper.js',

View File

@@ -0,0 +1,52 @@
gdjs.evtsExt__MyExtension__MyCustomObject = gdjs.evtsExt__MyExtension__MyCustomObject || {};
/**
* Mocked generated custom object for MyExtension::MyCustomObject.
*/
gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject = class MyCustomObject extends gdjs.CustomRuntimeObject2D {
constructor(parentInstanceContainer, objectData) {
super(parentInstanceContainer, objectData);
this._parentInstanceContainer = parentInstanceContainer;
this._onceTriggers = new gdjs.OnceTriggers();
this._objectData = {};
this._objectData.MyProperty = objectData.content.MyProperty !== undefined ? objectData.content.MyProperty : Number("123") || 0;
// It calls the onCreated super implementation at the end.
this.onCreated();
}
// Hot-reload:
updateFromObjectData(oldObjectData, newObjectData) {
super.updateFromObjectData(oldObjectData, newObjectData);
if (oldObjectData.content.MyProperty !== newObjectData.content.MyProperty)
this._objectData.MyProperty = newObjectData.content.MyProperty;
this.onHotReloading(this._parentInstanceContainer);
return true;
}
// Properties:
_getMyProperty() {
return this._objectData.MyProperty !== undefined ? this._objectData.MyProperty : Number("123") || 0;
}
_setMyProperty(newValue) {
this._objectData.MyProperty = newValue;
}
}
// Methods:
gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject.prototype.doStepPreEvents = function() {
this._onceTriggers.startNewFrame();
};
gdjs.registerObject("MyExtension::MyCustomObject", gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject);

View File

@@ -4,10 +4,10 @@
* Create and return a minimum working game.
* @internal
* @param {{layouts?: LayoutData[], resources?: ResourcesData, propertiesOverrides?: Partial<ProjectPropertiesData>}=} settings
* @returns {gdjs.RuntimeGame}
* @returns {ProjectData}
*/
gdjs.getPixiRuntimeGame = (settings) => {
const runtimeGame = new gdjs.RuntimeGame({
gdjs.createProjectData = (settings) => {
return {
variables: [],
properties: {
adaptGameResolutionAtRuntime: true,
@@ -65,7 +65,14 @@ gdjs.getPixiRuntimeGame = (settings) => {
resources: (settings && settings.resources) || { resources: [] },
eventsFunctionsExtensions: [],
usedResources: [],
});
return runtimeGame;
};
};
/**
* Create and return a minimum working game.
* @internal
* @param {{layouts?: LayoutData[], resources?: ResourcesData, propertiesOverrides?: Partial<ProjectPropertiesData>}=} settings
* @returns {gdjs.RuntimeGame}
*/
gdjs.getPixiRuntimeGame = (settings) =>
new gdjs.RuntimeGame(gdjs.createProjectData(settings));

View File

@@ -104,13 +104,24 @@ gdjs.getPixiRuntimeGameWithAssets = () => {
{
name: 'MySprite',
type: 'Sprite',
updateIfNotVisible: false,
variables: [],
behaviors: [],
animations: [],
effects: [],
// @ts-ignore This is the object configuration.
updateIfNotVisible: false,
// @ts-ignore This is the object configuration.
animations: [],
},
],
instances: [],
layers: [],
areaMinX: 0,
areaMinY: 0,
areaMinZ: 0,
areaMaxX: 0,
areaMaxY: 0,
areaMaxZ: 0,
_initialInnerArea: null,
},
],
sceneVariables: [],

View File

@@ -18,16 +18,7 @@ describe('gdjs.CustomRuntimeObject', function () {
variables: [],
behaviors: [],
effects: [],
instances: [],
layers: [],
content: {},
areaMinX: 0,
areaMinY: 0,
areaMinZ: 0,
areaMaxX: 0,
areaMaxY: 0,
areaMaxZ: 0,
defaultSize: null,
childrenContent: {
MySprite: {
updateIfNotVisible: false,

File diff suppressed because it is too large Load Diff

View File

@@ -662,8 +662,10 @@ interface ObjectsContainersList {
[Value] VectorString GetAnimationNamesOfObject([Const] DOMString name);
[Const, Value] DOMString GetTypeOfBehaviorInObjectOrGroup([Const] DOMString objectOrGroupName, [Const] DOMString behaviorName, boolean searchInGroups);
boolean HasObjectOrGroupNamed([Const] DOMString name);
ObjectsContainersList_VariableExistence HasObjectOrGroupWithVariableNamed([Const] DOMString objectName, [Const] DOMString variableName);
[Const, Ref] ObjectsContainer GetObjectsContainer(unsigned long index);
unsigned long GetObjectsContainersCount();
};
interface ProjectScopedContainers {
@@ -875,6 +877,12 @@ interface ObjectJsImplementation {
interface CustomObjectConfiguration {
[Value] UniquePtrObjectConfiguration Clone();
boolean IsForcedToOverrideEventsBasedObjectChildrenConfiguration();
boolean IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration();
void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
boolean isOverridingEventsBasedObjectChildrenConfiguration);
void ClearChildrenConfiguration();
[Ref] ObjectConfiguration GetChildObjectConfiguration([Const] DOMString objectName);
[Value] MapStringPropertyDescriptor GetProperties();
@@ -2948,6 +2956,8 @@ interface EventsBasedObject {
boolean IsAnimatable();
[Ref] EventsBasedObject MarkAsTextContainer(boolean isTextContainer);
boolean IsTextContainer();
[Ref] EventsBasedObject MarkAsInnerAreaExpandingWithParent(boolean value);
boolean IsInnerAreaFollowingParentSize();
[Ref] InitialInstancesContainer GetInitialInstances();
[Ref] LayersContainer GetLayers();
@@ -3712,6 +3722,7 @@ interface PreviewExportOptions {
void PreviewExportOptions([Ref] Project project, [Const] DOMString outputPath);
[Ref] PreviewExportOptions UseWebsocketDebuggerClientWithServerAddress([Const] DOMString address, [Const] DOMString port);
[Ref] PreviewExportOptions UseWindowMessageDebuggerClient();
[Ref] PreviewExportOptions UseMinimalDebuggerClient();
[Ref] PreviewExportOptions SetLayoutName([Const] DOMString layoutName);
[Ref] PreviewExportOptions SetFallbackAuthor([Const] DOMString id, [Const] DOMString username);
[Ref] PreviewExportOptions SetAuthenticatedPlayer([Const] DOMString playerId, [Const] DOMString playerUsername, [Const] DOMString playerToken);
@@ -3725,6 +3736,11 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);
[Ref] PreviewExportOptions SetAllowAuthenticationUsingIframeForPreview(boolean enable);
[Ref] PreviewExportOptions SetCrashReportUploadLevel([Const] DOMString crashReportUploadLevel);
[Ref] PreviewExportOptions SetPreviewContext([Const] DOMString previewContext);
[Ref] PreviewExportOptions SetGDevelopVersionWithHash([Const] DOMString gdevelopVersionWithHash);
[Ref] PreviewExportOptions SetProjectTemplateSlug([Const] DOMString projectTemplateSlug);
[Ref] PreviewExportOptions SetSourceGameId([Const] DOMString sourceGameId);
};
[Prefix="gdjs::"]

View File

@@ -599,6 +599,8 @@ export class ObjectsContainersList extends EmscriptenObject {
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
hasObjectOrGroupNamed(name: string): boolean;
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
getObjectsContainer(index: number): ObjectsContainer;
getObjectsContainersCount(): number;
}
export class ProjectScopedContainers extends EmscriptenObject {
@@ -729,6 +731,10 @@ export class ObjectJsImplementation extends ObjectConfiguration {
export class CustomObjectConfiguration extends ObjectConfiguration {
clone(): UniquePtrObjectConfiguration;
isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
clearChildrenConfiguration(): void;
getChildObjectConfiguration(objectName: string): ObjectConfiguration;
getProperties(): MapStringPropertyDescriptor;
updateProperty(name: string, value: string): boolean;
@@ -2146,6 +2152,8 @@ export class EventsBasedObject extends AbstractEventsBasedEntity {
isAnimatable(): boolean;
markAsTextContainer(isTextContainer: boolean): EventsBasedObject;
isTextContainer(): boolean;
markAsInnerAreaExpandingWithParent(value: boolean): EventsBasedObject;
isInnerAreaFollowingParentSize(): boolean;
getInitialInstances(): InitialInstancesContainer;
getLayers(): LayersContainer;
getObjects(): ObjectsContainer;
@@ -2759,6 +2767,7 @@ export class PreviewExportOptions extends EmscriptenObject {
constructor(project: Project, outputPath: string);
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): PreviewExportOptions;
useWindowMessageDebuggerClient(): PreviewExportOptions;
useMinimalDebuggerClient(): PreviewExportOptions;
setLayoutName(layoutName: string): PreviewExportOptions;
setFallbackAuthor(id: string, username: string): PreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): PreviewExportOptions;
@@ -2772,6 +2781,11 @@ export class PreviewExportOptions extends EmscriptenObject {
setElectronRemoteRequirePath(electronRemoteRequirePath: string): PreviewExportOptions;
setGDevelopResourceToken(gdevelopResourceToken: string): PreviewExportOptions;
setAllowAuthenticationUsingIframeForPreview(enable: boolean): PreviewExportOptions;
setCrashReportUploadLevel(crashReportUploadLevel: string): PreviewExportOptions;
setPreviewContext(previewContext: string): PreviewExportOptions;
setGDevelopVersionWithHash(gdevelopVersionWithHash: string): PreviewExportOptions;
setProjectTemplateSlug(projectTemplateSlug: string): PreviewExportOptions;
setSourceGameId(sourceGameId: string): PreviewExportOptions;
}
export class ExportOptions extends EmscriptenObject {

View File

@@ -1,6 +1,10 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdCustomObjectConfiguration extends gdObjectConfiguration {
clone(): gdUniquePtrObjectConfiguration;
isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
clearChildrenConfiguration(): void;
getChildObjectConfiguration(objectName: string): gdObjectConfiguration;
getProperties(): gdMapStringPropertyDescriptor;
updateProperty(name: string, value: string): boolean;

View File

@@ -15,6 +15,8 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity {
isAnimatable(): boolean;
markAsTextContainer(isTextContainer: boolean): gdEventsBasedObject;
isTextContainer(): boolean;
markAsInnerAreaExpandingWithParent(value: boolean): gdEventsBasedObject;
isInnerAreaFollowingParentSize(): boolean;
getInitialInstances(): gdInitialInstancesContainer;
getLayers(): gdLayersContainer;
getObjects(): gdObjectsContainer;

View File

@@ -14,6 +14,8 @@ declare class gdObjectsContainersList {
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
hasObjectOrGroupNamed(name: string): boolean;
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
getObjectsContainer(index: number): gdObjectsContainer;
getObjectsContainersCount(): number;
delete(): void;
ptr: number;
};

View File

@@ -3,6 +3,7 @@ declare class gdPreviewExportOptions {
constructor(project: gdProject, outputPath: string): void;
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): gdPreviewExportOptions;
useWindowMessageDebuggerClient(): gdPreviewExportOptions;
useMinimalDebuggerClient(): gdPreviewExportOptions;
setLayoutName(layoutName: string): gdPreviewExportOptions;
setFallbackAuthor(id: string, username: string): gdPreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): gdPreviewExportOptions;
@@ -16,6 +17,11 @@ declare class gdPreviewExportOptions {
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;
setAllowAuthenticationUsingIframeForPreview(enable: boolean): gdPreviewExportOptions;
setCrashReportUploadLevel(crashReportUploadLevel: string): gdPreviewExportOptions;
setPreviewContext(previewContext: string): gdPreviewExportOptions;
setGDevelopVersionWithHash(gdevelopVersionWithHash: string): gdPreviewExportOptions;
setProjectTemplateSlug(projectTemplateSlug: string): gdPreviewExportOptions;
setSourceGameId(sourceGameId: string): gdPreviewExportOptions;
delete(): void;
ptr: number;
};

View File

@@ -27,23 +27,23 @@
"pixi.js": "7.3.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-node-resolve": "13.3.0",
"@rollup/plugin-typescript": "8.3.3",
"@types/expect.js": "^0.3.29",
"@types/mocha": "^5.2.7",
"expect.js": "^0.3.1",
"karma": "^6.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^2.0.0",
"karma-mocha": "^1.3.0",
"karma-typescript": "latest",
"mocha": "^6.2.0",
"@types/expect.js": "0.3.32",
"@types/mocha": "5.2.7",
"expect.js": "0.3.1",
"karma": "6.4.3",
"karma-chrome-launcher": "3.2.0",
"karma-firefox-launcher": "2.1.3",
"karma-mocha": "1.3.0",
"karma-typescript": "5.5.4",
"mocha": "6.2.3",
"prettier": "2.1.2",
"rollup": "^2.66.1",
"rollup-plugin-terser": "^7.0.2",
"ts-loader": "^9.2.3",
"tslib": "^2.3.1",
"typescript": "latest"
"rollup": "2.79.1",
"rollup-plugin-terser": "7.0.2",
"ts-loader": "9.5.1",
"tslib": "2.6.2",
"typescript": "5.4.5"
},
"repository": {
"type": "git",

View File

@@ -75,7 +75,7 @@ export namespace LDtkTileMapLoader {
gridSize,
dimX,
dimY,
tileSet,
tileSet
);
const composedTileMap = new Map<string, TileDefinition>();
let nextComposedTileId = 0xfffffff;

View File

@@ -51,6 +51,7 @@ export namespace TiledTileMapLoader {
continue;
}
let polygon: PolygonVertices | null = null;
let hasFullHitBox = false;
if (object.polygon) {
const angle = (object.rotation * Math.PI) / 180;
let cos = Math.cos(angle);
@@ -67,6 +68,7 @@ export namespace TiledTileMapLoader {
object.y + point.x * sin + point.y * cos,
]);
//TODO check that polygons are convex or split them?
// TODO Set hasFullHitBox to true if the polygon covers the whole tile.
}
// TODO handle ellipses by creating a polygon?
// Make an object property for the number of vertices or always create 8 ones?
@@ -83,9 +85,14 @@ export namespace TiledTileMapLoader {
[object.x + object.width, object.y + object.height],
[object.x + object.width, object.y],
];
hasFullHitBox =
object.x === 0 &&
object.y === 0 &&
object.width === tiledTileMap.tilewidth &&
object.height === tiledTileMap.tileheight;
}
if (polygon) {
tileDefinition.addHitBox(tag, polygon);
tileDefinition.addHitBox(tag, polygon, hasFullHitBox);
}
}
} else if (tileClass) {
@@ -96,7 +103,7 @@ export namespace TiledTileMapLoader {
[tiledTileMap.tilewidth, tiledTileMap.tileheight],
[tiledTileMap.tilewidth, 0],
];
tileDefinition.addHitBox(tileClass, polygon);
tileDefinition.addHitBox(tileClass, polygon, true);
}
definitions.set(
getTileIdFromTiledGUI(firstGid + tile.id),
@@ -117,7 +124,7 @@ export namespace TiledTileMapLoader {
tiledTileMap.tileheight,
tiledTileMap.width,
tiledTileMap.height,
definitions,
definitions
);
for (const tiledLayer of tiledTileMap.layers) {

View File

@@ -1056,6 +1056,7 @@ export class TileDefinition {
private readonly taggedHitBoxes: {
tag: string;
polygons: PolygonVertices[];
hasFullHitBox: boolean;
}[];
private readonly animationLength: integer;
@@ -1078,13 +1079,19 @@ export class TileDefinition {
* Add a polygon for the collision layer
* @param tag The tag to allow collision layer filtering.
* @param polygon The polygon to use for collisions.
* @param hasFullHitBox Set to `true` when the hitBox cover the whole tile.
*/
addHitBox(tag: string, polygon: PolygonVertices): void {
addHitBox(
tag: string,
polygon: PolygonVertices,
hasFullHitBox: boolean
): void {
let taggedHitBox = this.taggedHitBoxes.find((hitbox) => hitbox.tag === tag);
if (!taggedHitBox) {
taggedHitBox = { tag, polygons: [] };
taggedHitBox = { tag, polygons: [], hasFullHitBox: false };
this.taggedHitBoxes.push(taggedHitBox);
}
taggedHitBox.hasFullHitBox ||= hasFullHitBox;
taggedHitBox.polygons.push(polygon);
}
@@ -1110,6 +1117,18 @@ export class TileDefinition {
return taggedHitBox && taggedHitBox.polygons;
}
/**
* Return `true` if the hit-box cover the whole tile.
* @param tag The tag to allow collision layer filtering.
* @returns `true` if the hit-box cover the whole tile.
*/
hasFullHitBox(tag: string): boolean {
const taggedHitBox = this.taggedHitBoxes.find(
(hitbox) => hitbox.tag === tag
);
return taggedHitBox && taggedHitBox.hasFullHitBox;
}
/**
* Animated tiles have a limitation:
* they are only able to use frames arranged horizontally one next

View File

@@ -131,14 +131,11 @@ export class TileMapManager {
// TODO: Is it useful to cache the tilemap since it belongs to an instance?
// const key = `${objectName}|${tileSize}|${tileSetColumnCount}|${tileSetRowCount}`;
const editableTileMap = EditableTileMap.from(
tileMapAsJsObject,
{
tileSize,
tileSetColumnCount,
tileSetRowCount,
},
);
const editableTileMap = EditableTileMap.from(tileMapAsJsObject, {
tileSize,
tileSetColumnCount,
tileSetRowCount,
});
callback(editableTileMap);
}
@@ -224,9 +221,12 @@ export class TileMapManager {
this._textureCacheCaches.getOrLoad(
key,
(textureCacheLoadingCallback) => {
const atlasTexture = atlasImageResourceName
? getTexture(atlasImageResourceName)
: null;
if (!atlasImageResourceName) {
textureCacheLoadingCallback(null);
return;
}
const atlasTexture = getTexture(atlasImageResourceName);
const textureCache = PixiTileMapHelper.parseSimpleTileMapAtlas(
atlasTexture,
columnCount,

View File

@@ -1,5 +1,5 @@
// @ts-check
const { mapVector } = require('./MapFor');
const { mapFor } = require('./MapFor');
const { generateReadMoreLink } = require('./WikiHelpLink');
// Types definitions used in this script:
@@ -261,10 +261,11 @@ const generateExpressionReferenceRowsText = ({
}) => {
let parameterRows = [];
let parameterStrings = [];
mapVector(expressionMetadata.getParameters(), (parameterMetadata, index) => {
mapFor(0, expressionMetadata.getParameters(), index => {
if ((!!objectMetadata && index < 1) || (!!behaviorMetadata && index < 2)) {
return; // Skip the first (or first twos) parameters by convention.
}
const parameterMetadata = expressionMetadata.getParameter(index);
if (parameterMetadata.isCodeOnly()) return;
const sanitizedDescription = sanitizeExpressionDescription(

View File

@@ -539,6 +539,10 @@ export default function EventsBasedBehaviorPropertiesEditor({
>
<SelectField
margin="none"
disabled={
property.getType() === 'Behavior' &&
!property.isHidden()
}
value={
property.isHidden()
? 'Hidden'

View File

@@ -19,9 +19,12 @@ import Checkbox from '../UI/Checkbox';
import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import Window from '../Utils/Window';
const gd: libGDevelop = global.gd;
const isDev = Window.isDev();
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension,
@@ -140,32 +143,34 @@ export default function EventsBasedBehaviorEditor({
</Trans>
</AlertMessage>
)}
<SelectField
floatingLabelText={
<Trans>Visibility in quick customization dialog</Trans>
}
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
onChange={(e, i, valueString: string) => {
// $FlowFixMe
const value: QuickCustomization_Visibility = valueString;
eventsBasedBehavior.setQuickCustomizationVisibility(value);
onChange();
}}
fullWidth
>
<SelectOption
value={gd.QuickCustomization.Default}
label={t`Default (visible)`}
/>
<SelectOption
value={gd.QuickCustomization.Visible}
label={t`Always visible`}
/>
<SelectOption
value={gd.QuickCustomization.Hidden}
label={t`Hidden`}
/>
</SelectField>
{isDev && (
<SelectField
floatingLabelText={
<Trans>Visibility in quick customization dialog</Trans>
}
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
onChange={(e, i, valueString: string) => {
// $FlowFixMe
const value: QuickCustomization_Visibility = valueString;
eventsBasedBehavior.setQuickCustomizationVisibility(value);
onChange();
}}
fullWidth
>
<SelectOption
value={gd.QuickCustomization.Default}
label={t`Default (visible)`}
/>
<SelectOption
value={gd.QuickCustomization.Visible}
label={t`Always visible`}
/>
<SelectOption
value={gd.QuickCustomization.Hidden}
label={t`Hidden`}
/>
</SelectField>
)}
<Checkbox
label={<Trans>Private</Trans>}
checked={eventsBasedBehavior.isPrivate()}

View File

@@ -1,347 +0,0 @@
// @flow
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { Line } from '../UI/Grid';
import ObjectsList, { type ObjectsListInterface } from '../ObjectsList';
import ObjectsRenderingService from '../ObjectsRendering/ObjectsRenderingService';
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import ObjectEditorDialog from '../ObjectEditor/ObjectEditorDialog';
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
import newNameGenerator from '../Utils/NewNameGenerator';
import {
getObjectFolderOrObjectUnifiedName,
type ObjectFolderOrObjectWithContext,
} from '../ObjectsList/EnumerateObjectFolderOrObject';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension,
eventsBasedObject: gdEventsBasedObject,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
unsavedChanges?: ?UnsavedChanges,
|};
type State = {|
editedObjectWithContext: ?ObjectWithContext,
editedObjectInitialTab: ?ObjectEditorTab,
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
|};
export default class EventBasedObjectChildrenEditor extends React.Component<
Props,
State
> {
_objectsList: ?ObjectsListInterface;
// TODO Reset selectedObjectFolderOrObjectsWithContext when a different eventsBasedObject is passed.
// It will avoid to add objects in the tree of the wrong ObjectsContainer.
state = {
editedObjectWithContext: null,
editedObjectInitialTab: 'properties',
selectedObjectFolderOrObjectsWithContext: [],
};
_onDeleteObjects = (i18n: I18nType) => (
objectsWithContext: ObjectWithContext[],
done: boolean => void
) => {
const { project, eventsBasedObject } = this.props;
objectsWithContext.forEach(objectWithContext => {
const { object } = objectWithContext;
gd.WholeProjectRefactorer.objectRemovedInEventsBasedObject(
project,
eventsBasedObject,
object.getName()
);
});
done(true);
};
_getValidatedObjectOrGroupName = (newName: string) => {
const { eventsBasedObject } = this.props;
const safeAndUniqueNewName = newNameGenerator(
gd.Project.getSafeName(newName),
tentativeNewName => {
if (
eventsBasedObject.getObjects().hasObjectNamed(tentativeNewName) ||
eventsBasedObject
.getObjects()
.getObjectGroups()
.has(tentativeNewName) ||
// TODO EBO Use a constant instead a hard coded value "Object".
tentativeNewName === 'Object'
) {
return true;
}
return false;
}
);
return safeAndUniqueNewName;
};
_onRenameEditedObject = (newName: string) => {
const { editedObjectWithContext } = this.state;
if (editedObjectWithContext) {
this._onRenameObject(editedObjectWithContext, newName);
}
};
_onRenameObject = (objectWithContext: ObjectWithContext, newName: string) => {
const { object } = objectWithContext;
const {
project,
eventsBasedObject,
projectScopedContainersAccessor,
} = this.props;
// newName is supposed to have been already validated
// Avoid triggering renaming refactoring if name has not really changed
if (object.getName() !== newName) {
gd.WholeProjectRefactorer.objectOrGroupRenamedInEventsBasedObject(
project,
projectScopedContainersAccessor.get(),
eventsBasedObject,
object.getName(),
newName,
/* isObjectGroup=*/ false
);
}
object.setName(newName);
};
_onRenameObjectFolderOrObjectFinish = (
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
newName: string,
done: boolean => void
) => {
const { objectFolderOrObject, global } = objectFolderOrObjectWithContext;
const unifiedName = getObjectFolderOrObjectUnifiedName(
objectFolderOrObject
);
// Avoid triggering renaming refactoring if name has not really changed
if (unifiedName === newName) {
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
done(false);
return;
}
// newName is supposed to have been already validated.
if (objectFolderOrObject.isFolder()) {
objectFolderOrObject.setFolderName(newName);
done(true);
return;
}
const object = objectFolderOrObject.getObject();
this._onRenameObject({ object, global }, newName);
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
done(true);
};
editObject = (editedObject: ?gdObject, initialTab: ?ObjectEditorTab) => {
if (editedObject) {
this.setState({
editedObjectWithContext: {
object: editedObject,
global: false,
},
editedObjectInitialTab: initialTab || 'properties',
});
} else {
this.setState({
editedObjectWithContext: null,
editedObjectInitialTab: 'properties',
});
}
};
_onObjectFolderOrObjectWithContextSelected = (
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
) => {
const selectedObjectFolderOrObjectsWithContext = [];
if (objectFolderOrObjectWithContext) {
selectedObjectFolderOrObjectsWithContext.push(
objectFolderOrObjectWithContext
);
}
this.setState(
{
selectedObjectFolderOrObjectsWithContext,
},
() => {
this.forceUpdateObjectsList();
}
);
};
updateBehaviorsSharedData = () => {
// TODO EBO Decide how BehaviorsSharedData of child-objects should work.
// - Use a shared data per object instance
// BehaviorsSharedData is configured on the CustomObject instead of the
// scene and each CustomObject instance will have it own data.
// - Use the layout shared data
// Find all layouts that are using this object and update them
// (something a bit like UsedExtensionsFinder, but the other way around).
// const { project, eventsBasedObject } = this.props;
// const layout = eventsBasedObject.getLayout();
// layout.updateBehaviorsSharedData(project);
};
forceUpdateObjectsList = () => {
if (this._objectsList) this._objectsList.forceUpdateList();
};
render() {
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
const { selectedObjectFolderOrObjectsWithContext } = this.state;
// TODO EBO Add a button icon to mark some objects solid or not.
return (
<I18n>
{({ i18n }) => (
<React.Fragment>
<Line expand useFullHeight>
<ObjectsList
getThumbnail={ObjectsRenderingService.getThumbnail.bind(
ObjectsRenderingService
)}
project={project}
eventsBasedObject={eventsBasedObject}
unsavedChanges={this.props.unsavedChanges}
objectsContainer={eventsBasedObject.getObjects()}
globalObjectsContainer={null}
layout={null}
// TODO EBO Allow to use project resources as place holders
resourceManagementProps={{
resourceSources: [],
resourceExternalEditors: [],
onChooseResource: async () => [],
getStorageProvider: () => emptyStorageProvider,
onFetchNewlyAddedResources: async () => {},
getStorageProviderResourceOperations: () => null,
canInstallPrivateAsset: () => false,
}}
selectedObjectFolderOrObjectsWithContext={
selectedObjectFolderOrObjectsWithContext
}
onEditObject={this.editObject}
// Don't allow export as there is no assets.
onExportAssets={() => {}}
onDeleteObjects={this._onDeleteObjects(i18n)}
getValidatedObjectOrGroupName={
this._getValidatedObjectOrGroupName
}
// Nothing special to do.
onObjectCreated={() => {}}
// Nothing special to do.
onObjectEdited={() => {}}
onObjectFolderOrObjectWithContextSelected={
this._onObjectFolderOrObjectWithContextSelected
}
onRenameObjectFolderOrObjectWithContextFinish={
this._onRenameObjectFolderOrObjectFinish
}
// Instances can't be created from this context.
onAddObjectInstance={() => {}}
onObjectPasted={() => this.updateBehaviorsSharedData()}
ref={objectsList => (this._objectsList = objectsList)}
// TODO EBO Hide the preview button or implement it.
// Note that it will be hard to do hot reload as extensions need
// to be generated.
hotReloadPreviewButtonProps={{
hasPreviewsRunning: false,
launchProjectDataOnlyPreview: async () => {},
launchProjectWithLoadingScreenPreview: async () => {},
}}
canSetAsGlobalObject={false}
/>
</Line>
{this.state.editedObjectWithContext && (
<ObjectEditorDialog
open
object={this.state.editedObjectWithContext.object}
initialTab={this.state.editedObjectInitialTab}
project={project}
layout={null}
eventsFunctionsExtension={eventsFunctionsExtension}
eventsBasedObject={eventsBasedObject}
projectScopedContainersAccessor={
this.props.projectScopedContainersAccessor
}
resourceManagementProps={{
resourceSources: [],
resourceExternalEditors: [],
onChooseResource: async () => [],
getStorageProvider: () => emptyStorageProvider,
onFetchNewlyAddedResources: async () => {},
getStorageProviderResourceOperations: () => null,
canInstallPrivateAsset: () => false,
}}
onComputeAllVariableNames={() => {
return [];
// TODO EBO Find undeclared variables in the parent events.
// const { editedObjectWithContext } = this.state;
// if (!editedObjectWithContext) return [];
// return EventsRootVariablesFinder.findAllObjectVariables(
// project.getCurrentPlatform(),
// project,
// eventsBasedObject,
// editedObjectWithContext.object.getName()
// );
}}
onCancel={() => {
this.editObject(null);
}}
getValidatedObjectOrGroupName={
this._getValidatedObjectOrGroupName
}
onRename={this._onRenameEditedObject}
onApply={() => {
this.editObject(null);
this.updateBehaviorsSharedData();
this.forceUpdateObjectsList();
}}
// TODO EBO Hide the preview button or implement it.
// Note that it will be hard to do hot reload as extensions need
// to be generated.
hotReloadPreviewButtonProps={{
hasPreviewsRunning: false,
launchProjectDataOnlyPreview: async () => {},
launchProjectWithLoadingScreenPreview: async () => {},
}}
onUpdateBehaviorsSharedData={() =>
this.updateBehaviorsSharedData()
}
// TODO EBO Go to the behavior extension tab.
openBehaviorEvents={() => {}}
/>
)}
</React.Fragment>
)}
</I18n>
);
}
}

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import EventsBasedObjectEditor from './index';
import { Tabs } from '../UI/Tabs';
import EventsBasedObjectPropertiesEditor from './EventsBasedObjectPropertiesEditor';
import EventBasedObjectChildrenEditor from './EventBasedObjectChildrenEditor';
import Background from '../UI/Background';
import { Column, Line } from '../UI/Grid';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
@@ -61,10 +60,6 @@ export default function EventsBasedObjectEditorPanel({
value: 'properties',
label: <Trans>Properties</Trans>,
},
{
value: 'children',
label: <Trans>Children</Trans>,
},
]}
/>
</Column>
@@ -86,14 +81,6 @@ export default function EventsBasedObjectEditorPanel({
onEventsFunctionsAdded={onEventsFunctionsAdded}
/>
)}
{currentTab === 'children' && (
<EventBasedObjectChildrenEditor
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
eventsBasedObject={eventsBasedObject}
projectScopedContainersAccessor={projectScopedContainersAccessor}
/>
)}
</Column>
</Background>
);

View File

@@ -13,12 +13,9 @@ import HelpButton from '../UI/HelpButton';
import { Line } from '../UI/Grid';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import RaisedButton from '../UI/RaisedButton';
import Window from '../Utils/Window';
const gd: libGDevelop = global.gd;
const isDev = Window.isDev();
type Props = {|
eventsBasedObject: gdEventsBasedObject,
onOpenCustomObjectEditor: () => void,
@@ -119,15 +116,21 @@ export default function EventsBasedObjectEditor({
onChange();
}}
/>
{isDev && (
<Line noMargin justifyContent="center">
<RaisedButton
label={<Trans>Open visual editor for the object</Trans>}
primary
onClick={onOpenCustomObjectEditor}
/>
</Line>
)}
<Checkbox
label={<Trans>Expand inner area with parent</Trans>}
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
onCheck={(e, checked) => {
eventsBasedObject.markAsInnerAreaExpandingWithParent(checked);
onChange();
}}
/>
<Line noMargin justifyContent="center">
<RaisedButton
label={<Trans>Open visual editor for the object</Trans>}
primary
onClick={onOpenCustomObjectEditor}
/>
</Line>
<Line noMargin>
<HelpButton
key="help"

View File

@@ -182,6 +182,7 @@ export const EventsFunctionParametersEditor = ({
parameters.insertNewParameter(newName, index).setType('objectList');
forceUpdate();
onParametersUpdated();
setJustAddedParameterName(newName);
},
[eventsFunction, forceUpdate, onParametersUpdated]
);
@@ -550,8 +551,8 @@ export const EventsFunctionParametersEditor = ({
draggedParameter.current = parameter;
return {};
}}
canDrag={() => true}
canDrop={() => true}
canDrag={() => !isParameterDisabled(i)}
canDrop={() => !isParameterDisabled(i)}
drop={() => {
moveParameterBefore(parameter);
}}
@@ -581,7 +582,9 @@ export const EventsFunctionParametersEditor = ({
{connectDragSource(
<span>
<Column>
<DragHandleIcon />
<DragHandleIcon
disabled={isParameterDisabled(i)}
/>
</Column>
</span>
)}
@@ -629,7 +632,7 @@ export const EventsFunctionParametersEditor = ({
label: i18n._(
t`Add a parameter below`
),
enabled: !isParameterDisabled(i),
enabled: !isParameterDisabled(i + 1),
click: () => addParameterAt(i + 1),
},
{

View File

@@ -42,6 +42,7 @@ import Mark from '../UI/CustomSvgIcons/Mark';
import newNameGenerator from '../Utils/NewNameGenerator';
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
const gd: libGDevelop = global.gd;
@@ -75,6 +76,7 @@ type Props = {|
initiallyFocusedObjectName: ?string,
unsavedChanges?: ?UnsavedChanges,
onOpenCustomObjectEditor: gdEventsBasedObject => void,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|};
type State = {|
@@ -1313,6 +1315,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
settingsIcon={extensionEditIconReactNode}
unsavedChanges={this.props.unsavedChanges}
isActive={true}
hotReloadPreviewButtonProps={
this.props.hotReloadPreviewButtonProps
}
/>
</Background>
) : selectedEventsBasedBehavior ? (
@@ -1527,6 +1532,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
open
onCancel={() => this._editVariables(null)}
onApply={() => this._editVariables(null)}
hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
/>
)}
{objectMethodSelectorDialogOpen && selectedEventsBasedObject && (

View File

@@ -16,9 +16,6 @@ import {
extensionObjectsRootFolderId,
} from '.';
import Add from '../UI/CustomSvgIcons/Add';
import Window from '../Utils/Window';
const isDev = Window.isDev();
const EVENTS_BASED_OBJECT_CLIPBOARD_KIND = 'Events Based Object';
@@ -146,13 +143,11 @@ export class EventsBasedObjectTreeViewItemContent
buildMenuTemplate(i18n: I18nType, index: number) {
return [
isDev
? {
label: i18n._(t`Open visual editor`),
click: () =>
this.props.onOpenCustomObjectEditor(this.eventsBasedObject),
}
: null,
{
label: i18n._(t`Open visual editor`),
click: () =>
this.props.onOpenCustomObjectEditor(this.eventsBasedObject),
},
{
label: i18n._(t`Add a function`),
click: () => this.addFunctionAtSelection(),

View File

@@ -123,6 +123,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
isGlobalTabInitiallyOpen={isGlobal}
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
hotReloadPreviewButtonProps={null}
/>
)}
</React.Fragment>

View File

@@ -82,6 +82,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
preventRefactoringToDeleteInstructions
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
hotReloadPreviewButtonProps={null}
/>
)}
</React.Fragment>

View File

@@ -201,6 +201,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
onComputeAllVariableNames={onComputeAllVariableNames}
hotReloadPreviewButtonProps={null}
/>
)}
{editorOpen && project && objectGroup && (

View File

@@ -91,6 +91,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
shouldCreateInitiallySelectedVariable={
editorOpen.shouldCreate || false
}
hotReloadPreviewButtonProps={null}
/>
)}
</React.Fragment>

View File

@@ -103,7 +103,6 @@ const components = {
atlasResource: AtlasResourceField,
spineResource: SpineResourceField,
color: ColorExpressionField,
police: DefaultField, //TODO
forceMultiplier: ForceMultiplierField,
sceneName: SceneNameField,
layerEffectName: LayerEffectNameField,

View File

@@ -115,6 +115,7 @@ import {
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
import LocalVariablesDialog from '../VariablesList/LocalVariablesDialog';
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
const gd: libGDevelop = global.gd;
@@ -149,6 +150,7 @@ type Props = {|
onBeginCreateEventsFunction: () => void,
unsavedChanges?: ?UnsavedChanges,
isActive: boolean,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|};
type ComponentProps = {|
@@ -1832,6 +1834,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
resourceManagementProps,
onCreateEventsFunction,
tutorials,
hotReloadPreviewButtonProps,
} = this.props;
if (!project) return null;
@@ -2156,6 +2159,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
open
onCancel={() => this.editLayoutVariables(false)}
onApply={() => this.editLayoutVariables(false)}
hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
/>
)}
{this.state.textEditedEvent && (

View File

@@ -4,7 +4,10 @@ import BrowserPreviewErrorDialog from './BrowserPreviewErrorDialog';
import BrowserS3FileSystem from '../BrowserS3FileSystem';
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
import assignIn from 'lodash/assignIn';
import { type PreviewOptions } from '../../PreviewLauncher.flow';
import {
type PreviewOptions,
type PreviewLauncherProps,
} from '../../PreviewLauncher.flow';
import { getBaseUrl } from '../../../Utils/GDevelopServices/Preview';
import { makeTimestampedId } from '../../../Utils/TimestampedId';
import {
@@ -16,17 +19,13 @@ import Window from '../../../Utils/Window';
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
import { isNativeMobileApp } from '../../../Utils/Platform';
import { getIDEVersionWithHash } from '../../../Version';
const gd: libGDevelop = global.gd;
type State = {|
error: ?Error,
|};
type Props = {|
getIncludeFileHashs: () => { [string]: number },
onExport?: () => void,
|};
let nextPreviewWindowId = 0;
/**
@@ -60,7 +59,7 @@ export const immediatelyOpenNewPreviewWindow = (
};
export default class BrowserS3PreviewLauncher extends React.Component<
Props,
PreviewLauncherProps,
State
> {
canDoNetworkPreview = () => false;
@@ -163,7 +162,11 @@ export default class BrowserS3PreviewLauncher extends React.Component<
previewExportOptions.setExternalLayoutName(externalLayout.getName());
}
previewExportOptions.useWindowMessageDebuggerClient();
if (isNativeMobileApp()) {
previewExportOptions.useMinimalDebuggerClient();
} else {
previewExportOptions.useWindowMessageDebuggerClient();
}
// Scripts generated from extensions keep the same URL even after being modified.
// Use a cache bursting parameter to force the browser to reload them.
@@ -174,6 +177,13 @@ export default class BrowserS3PreviewLauncher extends React.Component<
);
previewExportOptions.setNativeMobileApp(isNativeMobileApp());
previewExportOptions.setGDevelopVersionWithHash(getIDEVersionWithHash());
previewExportOptions.setCrashReportUploadLevel(
this.props.crashReportUploadLevel
);
previewExportOptions.setPreviewContext(this.props.previewContext);
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
previewExportOptions.setSourceGameId(this.props.sourceGameId);
if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(

View File

@@ -8,7 +8,10 @@ import { timeFunction } from '../../../Utils/TimeFunction';
import { findGDJS } from '../../../GameEngineFinder/LocalGDJSFinder';
import LocalNetworkPreviewDialog from './LocalNetworkPreviewDialog';
import assignIn from 'lodash/assignIn';
import { type PreviewOptions } from '../../PreviewLauncher.flow';
import {
type PreviewOptions,
type PreviewLauncherProps,
} from '../../PreviewLauncher.flow';
import SubscriptionChecker, {
type SubscriptionCheckerInterface,
} from '../../../Profile/Subscription/SubscriptionChecker';
@@ -17,16 +20,12 @@ import {
localPreviewDebuggerServer,
} from './LocalPreviewDebuggerServer';
import Window from '../../../Utils/Window';
import { getIDEVersionWithHash } from '../../../Version';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const ipcRenderer = electron ? electron.ipcRenderer : null;
const gd: libGDevelop = global.gd;
type Props = {|
getIncludeFileHashs: () => { [string]: number },
onExport?: () => void,
|};
type State = {|
networkPreviewDialogOpen: boolean,
networkPreviewHost: ?string,
@@ -47,7 +46,7 @@ type State = {|
|};
export default class LocalPreviewLauncher extends React.Component<
Props,
PreviewLauncherProps,
State
> {
canDoNetworkPreview = () => true;
@@ -249,6 +248,17 @@ export default class LocalPreviewLauncher extends React.Component<
previewExportOptions.setFullLoadingScreen(
previewOptions.fullLoadingScreen
);
previewExportOptions.setGDevelopVersionWithHash(
getIDEVersionWithHash()
);
previewExportOptions.setCrashReportUploadLevel(
this.props.crashReportUploadLevel
);
previewExportOptions.setPreviewContext(this.props.previewContext);
previewExportOptions.setProjectTemplateSlug(
project.getTemplateSlug()
);
previewExportOptions.setSourceGameId(this.props.sourceGameId);
if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(

View File

@@ -22,6 +22,9 @@ export type PreviewOptions = {|
/** The props that PreviewLauncher must support */
export type PreviewLauncherProps = {|
crashReportUploadLevel: string,
previewContext: string,
sourceGameId: string,
getIncludeFileHashs: () => { [string]: number },
onExport: () => void,
|};

View File

@@ -1,8 +1,5 @@
// @flow
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import Avatar from '@material-ui/core/Avatar';
import { getGravatarUrl } from '../../UI/GravatarUrl';
import * as React from 'react';
import { ColumnStackLayout } from '../../UI/Layout';
import RaisedButton from '../../UI/RaisedButton';
@@ -27,70 +24,14 @@ import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
import FlatButton from '../../UI/FlatButton';
import LeftLoader from '../../UI/LeftLoader';
import TextField from '../../UI/TextField';
import IconButton from '../../UI/IconButton';
import Trash from '../../UI/CustomSvgIcons/Trash';
import useAlertDialog from '../../UI/Alert/useAlertDialog';
import SelectField from '../../UI/SelectField';
import SelectOption from '../../UI/SelectOption';
import Form from '../../UI/Form';
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
export const emailRegex = /^(.+)@(.+)$/;
const getTranslatableLevel = (level: Level) => {
switch (level) {
case 'owner':
return t`Owner`;
case 'writer':
return t`Read & Write`;
case 'reader':
return t`Read only`;
default:
return level;
}
};
const UserLine = ({
username,
email,
level,
onDelete,
disabled,
}: {|
username: ?string,
email: string,
level: ?Level,
onDelete?: () => void,
disabled?: boolean,
|}) => (
<I18n>
{({ i18n }) => (
<Line justifyContent="space-between">
<Line noMargin expand>
<Avatar src={getGravatarUrl(email, { size: 40 })} />
<Column expand justifyContent="flex-end">
{username && <Text noMargin>{username}</Text>}
<Text noMargin color="secondary">
{email}
</Text>
</Column>
</Line>
<Column>
{!!level && (
<Text color="secondary">{i18n._(getTranslatableLevel(level))}</Text>
)}
</Column>
{onDelete && (
<Column noMargin>
<IconButton size="small" onClick={onDelete} disabled={disabled}>
<Trash />
</IconButton>
</Column>
)}
</Line>
)}
</I18n>
);
import UserLine from '../../UI/User/UserLine';
import { getTranslatableLevel } from '../../Utils/AclUtils';
import { emailRegex } from '../../Utils/EmailUtils';
const getEmailErrorText = (addError: ?string) => {
switch (addError) {
@@ -449,7 +390,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
</ColumnStackLayout>
{showCollaboratorAddDialog && (
<Dialog
title="Add a collaborator"
title={<Trans>Add a collaborator</Trans>}
actions={[
<FlatButton
label={<Trans>Back</Trans>}

View File

@@ -20,11 +20,12 @@ import {
import IconButton from '../UI/IconButton';
import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
import ChevronArrowRight from '../UI/CustomSvgIcons/ChevronArrowRight';
import { Column, Line } from '../UI/Grid';
import { Column, Line, Spacer } from '../UI/Grid';
import Text from '../UI/Text';
import Paper from '../UI/Paper';
import BackgroundText from '../UI/BackgroundText';
import { getDefaultRegisterGamePropertiesFromProject } from '../Utils/UseGameAndBuildsManager';
import UserEarnings from './Monetization/UserEarnings';
const pageSize = 10;
@@ -220,13 +221,13 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
return (
<ColumnStackLayout noMargin>
{!isGameRegistering && (
<GameRegistration
project={project}
hideLoader
onGameRegistered={onRefreshGames}
/>
)}
<UserEarnings />
<Spacer />
<Line noMargin>
<Text size="section-title" noMargin>
<Trans>Published games</Trans>
</Text>
</Line>
<Line noMargin alignItems="center">
<Column noMargin expand>
<SearchBar
@@ -257,7 +258,13 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
<ChevronArrowRight />
</IconButton>
</Line>
{!isGameRegistering && (
<GameRegistration
project={project}
hideLoader
onGameRegistered={onRefreshGames}
/>
)}
{displayedGames.length > 0 ? (
displayedGames.map(game => (
<GameCard

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