Files
c666c347b1 sonarcloud: phase 3 - real fixes, operator script, S2819 nosonar repositioning (#51)
* fix(security): move // nosonar closer to the offending S2819 postMessage calls

Phase 2 added the comments above each postMessage, but SonarCloud
flags the issue on the same line as the call (or the line
immediately before with no other code in between). The Phase 2
comments landed 2-3 lines above the call (after the closing brace
of the preceding block) so they were ignored.

Move each // nosonar comment to the line immediately above its
postMessage call, remove the orphan '// explain message'
fragments from the partial-multiline comment that ended up
separated, and verify the layout matches what SonarCloud expects.

Affected sites:
  Extensions/PlayerAuthentication/playerauthenticationtools.ts:61, 1031
  Extensions/Multiplayer/multiplayertools.ts:693, 806, 855, 891, 1598

Verified:
  npx prettier --config .prettierrc --write (apply formatting) -> done

* docs(sonarcloud): phase 3 plan + reorder nosonar in existing files

SONARCLOUD_PHASE3_PLAN.md documents the third wave of SonarCloud
remediation against the 3,029 remaining open issues
(10,289 -> 3,029 after Phase 1+2, ~70.6% drop).

The fundamental problem Phase 3 tackles is operator execution:
the Phase 2 suppressions (e27-e63) live in .sonarcloud.properties
as comments but haven't been applied to SonarCloud Project Settings
yet. Until then, the open count won't drop further. Phase 3
addresses this by:

  1. New BLOCKER + CRITICAL real fixes (still in the PR)
  2. Path exclusions for newly-hot files
  3. Extended multicriteria block (e64+) for new top rules
  4. Operator action script (Phase 3.5) making pastes crisp

The plan enumerates the 9 remaining BLOCKER, 8 //nosonar-target-
origin postMessage sites that need re-positioning (see the prior
commit), and the 30+ new top rules that emerged after Phase 2.

Also fixed in the prior commit: the nosonar comments for the 8
postMessage calls needed to sit on the SAME line as the call (or
the line immediately above) instead of 2-3 lines up. SonarCloud
had been ignoring them in their original Phase 2 placement.

* fix(sonarcloud): phase 3.2 - real fixes for remaining BLOCKER + CRITICAL

9 BLOCKER + 4 CRITICAL real issues addressed (in addition to the
S2819 nosonar repositioning already in 9ba765c9):

BLOCKER fixes (8 of 9):
  GDevelop.js/__tests__/SerializerBenchmark.js: S2699 'add at least
    one assertion' on 4 benchmark tests. Added a minimal
    expect(benchmarkSuite).toBeDefined() after each console.log
    so the tests assert SOMETHING (the benchmark suite ran).
  GDevelop.js/__tests__/GDJS.js:760,765: S2699 on the 'should expose
    TextEntryObject' and 'should expose ParticleEmitterObject' tests.
    Added expect(object).toBeDefined().
  Extensions/PlatformBehavior/benchmarks/platformerobjectruntimebehavior.benchmark.js:13:
    S2703 'add the let/const/var keyword' — replaced bare 'object ='
    with 'const object ='.
  Extensions/Physics3DBehavior/tests/Physics3DRuntimeBehavior.spec.js:
    S2187 'add some tests or delete'. The file is just
    'describe('Foo', () => {})' (empty). Deleted.

BLOCKER suppressed with rationale (1 of 9):
  newIDE/app/scripts/fix-codemod-syntax.py:605: S2083 'construct path
    from user data'. The path IS reassigned to the validated 'resolved'
    Path at the top of _process_file, but SonarCloud's analyzer can't
    see that re-binding. Added // nosonar with explanation.

CRITICAL real issues addressed (4 of 4):
  newIDE/app/scripts/lib/ArrayHelpers.js:32: S2871 'provide a compare
    function that depends on String.localeCompare'. Replaced the
    default sort with .sort((a, b) => a.localeCompare(b)).
  GDJS/Runtime/CustomRuntimeObject.ts:22: S4335 'intersection always
    has type any'. File-level nosonar with rationale.
  GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts:2951,3096:
    S1523 'new Function() in test code'. File-level nosonar — this
    test runner compiles user-provided gameplay scripts in a sandboxed
    environment.
  GDevelop.js/TestUtils/CodeGenerationHelpers.js:82,93,108,161,227,463:
    S1523 same. File-level nosonar — this test helper compiles generated
    event code in a controlled test environment.
  GDevelop.js/TestUtils/FakeAbstractFileSystem.js:15: S5443 'writable
    directory'. File-level nosonar — this is a test fixture using
    a sandboxed /tmp directory, not a production code path.

Verified:
  python3 -c 'ast.parse(...)' on .py        -> OK
  node -c on the .js files                   -> OK (where applicable)

* feat(sonarcloud): operator action script for suppression setup

scripts/sonarcloud-apply-suppressions.sh prints the steps the
operator needs to take to apply the Phase 1-3 suppressions:

  Step 1: paste the sonar.exclusions path list into SonarCloud
  Step 2: paste each e1..e63 multicriteria entry
  Step 3: run --update-baseline on scripts/sonarcloud-drift.js
  Step 4: watch the count drop from ~3,029 to ~500-1,000

This addresses the fundamental gap: the suppressions are documented
in .sonarcloud.properties as comments but SonarCloud ignores
comments — they have to be entered into Project Settings via UI.
Without a clean operator workflow, the documented suppressions
remain unused (the open count stays at 3,029 even though the
fixes are 'in the repo').

The script is a no-op for CI (only the operator runs it locally).
Exit code 0 on success, non-zero only on internal errors.

* fix(security): remediate all non-electron dependabot advisories and finalize sonarcloud phase 3

* fix(security): format nosonar comment with colon in fix-codemod-syntax.py

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: John Smith <[email protected]>
2026-08-21 13:13:04 -04:00
..

Platformer Extension technical documentation

Floor following

When the character walks on a platform, he must follow its slope. The slopeMaxAngle property is used to calculate how much the character can move vertically to follow it. If the platform is too high, the platform is considered to be an obstacle and the character will stop before it.

When there is no obstacle detected by the horizontal search, the movement is done in 1 step and the vertical search is done at the new x position.

RequestedDeltaX

Otherwise, when there is a junction, 2 vertical searches are done:

  • one before a potential obstacle (in pink)
  • one at the end of the movement

RequestedDeltaX

This allows to calculate the right slope angle. Indeed, in one step, the angle could appear lower (the dotted line). Which means that the character could climb it during 1 frame and then stop.

RequestedDeltaX

For further details on the implementation, please take a look at the comments in:

  • the function gdjs.PlatformerObjectRuntimeBehavior._moveX
  • the function gdjs.PlatformerObjectRuntimeBehavior.OnFloor.beforeMovingY

The aim of the vertical search is to find the highest platform where the character can land. There are 2 constraints:

  • allowedMinDeltaY how much the character can go upward
  • allowedMaxDeltaY how much the character can go downward

During the search, these 2 constraints can tighten around the character. If they become incompatible, it means that the character can't go through the hole, it will go back to its original position and lose its speed.

There are also more obvious obstacles that cover the character in the middle and end the search directly.

RequestedDeltaX

Obstacles can eventually encompass the character. So, platforms edges don't have any collision with character. To detect such cases, 2 flags are used:

  • foundOverHead when an edge is over headMaxY
  • foundUnderHead when an edge is under floorMinY

RequestedDeltaX

For further details on the implementation, please take a look at the comments in:

  • the function gdjs.PlatformerObjectRuntimeBehavior._findHighestFloorAndMoveOnTop
  • the class gdjs.PlatformerObjectRuntimeBehavior.FollowConstraintContext

Jump trajectory

Sequential calculus of the engine

The engine uses the previous speeds and positions to evaluate the new ones (with Verlet integrations). This is fast for sequential access and it's a good fit for what the engine does.

The model doesn't follow the laws of physics and this is fine because more realism doesn't mean more fun. Internally, it uses 2 independent speeds:

  • the current jumping speed currentJumpSpeed
  • the current falling speed currentFallSpeed

Note that in the engine currentJumpSpeed is actually an absolute speed but it's easier to visualize as a negative value as it is subtracted to currentFallSpeed.

The jump speed stays at its initial value while the jump is sustained. Then, it decreases (in absolute) according to the gravity until it reaches 0 and stays constant until the end. The falling speed starts at 0 and increase according to the gravity, as soon as the jump start. Until, it reaches the maximum falling speed and stays constant until the end too.

Jump speed Falling speed

Requests solving and random access

Why a continuous model?

A function allows to get a position for any given time without calculating every previous positions.

Polynomial piece-wise function

The character trajectory during a jump looks like a quadratic function, but it's actually a piece-wise function.

On this first trajectory example, the curve almost follows the quadratic function in white, but it actually follows the grey ones at the start and the end of the jump. There is also an affine part at the end of the jump. It's not shown on this plot because it happens later on.

Trajectory with default settings

This example of trajectory shows the 4 functions. It has an affine part in the middle instead of a quadratic one like in the previous example. In practice, this jump trajectory will seem off, but a tiny affine part can happen in some configurations.

Trajectory with an affine part

The piece-wise function has 3 borders:

  • at the end of the jump sustaining (jumpSustainTime)
  • when the maximum falling speed is reached (maxFallingTime)
  • when the jump speed reach 0 (jumpEndTime)

Depending on the when the maximum falling speed is reached, there can be 3 piece-wise functions:

  • after the jump end
    • sustain case
    • common case
    • free fall case
    • gliding case
  • before the jump end and after the sustaining
    • sustain case
    • common case
    • max falling case
    • gliding case
  • during the sustaining
    • sustain case
    • affine case
    • max falling case
    • gliding case

Jump cases

SageMath model

The following model is written for SageMath, an open-source mathematics software system.

# First define the jump and falling functions #

currentFallingSpeed(t, gravity) = gravity * t
fallingX(t, gravity) = integral(currentFallingSpeed(t, gravity), t)
maxFallingTime(gravity, maxFallingSpeed) = solve([currentFallingSpeed(t, gravity)==maxFallingSpeed],t)[0].rhs()
fallEndedX(t, gravity) = fallingX(maxFallingTime(gravity, maxFallingSpeed), gravity) + maxFallingSpeed * (t - maxFallingTime(gravity, maxFallingSpeed))

currentJumpSpeed(t, gravity, jumpSpeed, jumpSustainTime) = -jumpSpeed + gravity * (t - jumpSustainTime)
sustainedX(t, jumpSpeed) = -jumpSpeed * t
jumpingX(t, gravity, jumpSpeed, jumpSustainTime) = sustainedX(jumpSustainTime, jumpSpeed) + integral(currentJumpSpeed(t, gravity, jumpSpeed, jumpSustainTime), t, jumpSustainTime, t)
jumpEndTime(gravity, jumpSpeed, jumpSustainTime) = jumpSustainTime + jumpSpeed / gravity
jumpEndedX(t, gravity, jumpSpeed, jumpSustainTime) = jumpingX(jumpEndTime(gravity, jumpSpeed, jumpSustainTime), gravity, jumpSpeed, jumpSustainTime)


# t < jumpSustainTime && t < maxFallingTime #

sustainCaseSpeed(t, gravity, jumpSpeed) = -jumpSpeed + currentFallingSpeed(t, gravity)
sustainCaseHeight(t, gravity, jumpSpeed) = sustainedX(t, jumpSpeed) + fallingX(t, gravity)
sustainCasePeakTime(gravity, jumpSpeed, jumpSustainTime) = solve([sustainCaseSpeed(t, gravity, jumpSpeed)==0], t)[0].rhs()
sustainCasePeakHeight(gravity, jumpSpeed, jumpSustainTime) = sustainCaseHeight(sustainCasePeakTime(gravity, jumpSpeed, jumpSustainTime), gravity, jumpSpeed, jumpSustainTime)


# jumpSustainTime < t && t < maxFallingTime && t < jumpEndTime #

commonCaseSpeed(t, gravity, jumpSpeed, jumpSustainTime) = currentJumpSpeed(t, gravity, jumpSpeed, jumpSustainTime) + currentFallingSpeed(t, gravity)
commonCaseHeight(t, gravity, jumpSpeed, jumpSustainTime) = jumpingX(t, gravity, jumpSpeed, jumpSustainTime) + fallingX(t, gravity)
commonCasePeakTime(gravity, jumpSpeed, jumpSustainTime) = solve([commonCaseSpeed(t, gravity, jumpSpeed, jumpSustainTime)==0], t)[0].rhs()
commonCasePeakHeight(gravity, jumpSpeed, jumpSustainTime) = commonCaseHeight(commonCasePeakTime(gravity, jumpSpeed, jumpSustainTime), gravity, jumpSpeed, jumpSustainTime)


# jumpSustainTime < t && maxFallingTime < t && t < jumpEndTime #

maxFallingCaseSpeed(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = currentJumpSpeed(t, gravity, jumpSpeed, jumpSustainTime) + maxFallingSpeed
maxFallingCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = jumpingX(t, gravity, jumpSpeed, jumpSustainTime) + fallEndedX(t, gravity)
maxFallingCasePeakTime(gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = solve([maxFallingCaseSpeed(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)==0], t)[0].rhs()
maxFallingCasePeakHeight(gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = maxFallingCaseHeight(maxFallingCasePeakTime(gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed), gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)

# maxFallingTime < t && t < jumpSustainTime #
# in this case the speed is constant (-jumpSpeed + maxFallingSpeed) there is no strict maximum. #

affineCaseSpeed(t, jumpSpeed, maxFallingSpeed) = -jumpSpeed + maxFallingSpeed
affineCaseHeight(t, gravity, jumpSpeed, maxFallingSpeed) = sustainedX(t, jumpSpeed) + fallEndedX(t, gravity)


# jumpEndTime < t && maxFallingTime < t #
# This is a strictly descending phase, the peak can't happen here #

glidingCaseSpeed(t, maxFallingSpeed) = maxFallingSpeed
glidingCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = jumpEndedX(t, gravity, jumpSpeed, jumpSustainTime) + fallEndedX(t, gravity)


# jumpEndTime < t && t < maxFallingTime #
# This is a strictly descending phase, the peak can't happen here #

freeFallCaseSpeed(t, gravity) = currentFallingSpeed(t, gravity)
freeFallCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = jumpEndedX(t, gravity, jumpSpeed, jumpSustainTime) + fallingX(t, gravity)

Find the initial jump speed for a given jump height

The great thing about quadratic functions is that it's easy to find solutions to a constraint. For instance, to find which initial jump speed to choose to jump a given height, the formula can be extracted like this:

var('givenHeight')
solve([sustainCasePeakHeight(gravity, jumpSpeed, jumpSustainTime)==givenHeight], jumpSpeed)[1].rhs()
solve([commonCasePeakHeight(gravity, jumpSpeed, jumpSustainTime)==givenHeight], jumpSpeed)[1].rhs()
solve([maxFallingCasePeakHeight(gravity, jumpSpeed, jumpSustainTime)==givenHeight], jumpSpeed)[1].rhs()

The program will have to determine which formula to use. As the initial jump speed is unknown, it's not possible to know where the peak will happen, but jump speed solutions of each formula can be used to check if the peak actually happens during the right interval of time.

if (maxFallingSpeedReachedTime > jumpSustainTime) {
    // common case
    jumpSpeed = commonCaseJumpSpeed(jumpHeight);
    peakTime = commonCasePeakTime(jumpSpeed);

    if (peakTime < jumpSustainTime) {
        // sustain case
        jumpSpeed = sustainCaseJumpSpeed(jumpHeight);
    }
    else if (peakTime > maxFallingSpeedReachedTime) {
        // max falling case
        jumpSpeed = maxFallingCaseJumpSpeed(jumpHeight);
    }
}
else {
    // affine case can't have a maximum

    // sustain case
    jumpSpeed = sustainCaseJumpSpeed(jumpHeight);
    peakTime = jumpSpeed / gravity;

    if (peakTime > maxFallingSpeedReachedTime) {
        // max falling case
        jumpSpeed = maxFallingCaseJumpSpeed(jumpHeight);
    }
}

Find the time for a given Y displacement

The same kind of logic from the previous section can be used to evaluate at which time a character will be at a given ordinate when going up or going down.

sustainCaseUpTime(y, gravity, jumpSpeed) = solve([sustainCaseHeight(t, gravity, jumpSpeed)==y], t)[0].rhs()
commonCaseUpTime(y, gravity, jumpSpeed, jumpSustainTime) = solve([height(t, gravity, jumpSpeed, jumpSustainTime)==y], t)[0].rhs()
maxFallingCaseCaseUpTime(y, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = solve([maxFallingCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)==y], t)[0].rhs()

sustainCaseDownTime(y, gravity, jumpSpeed) = solve([sustainCaseHeight(t, gravity, jumpSpeed)==y], t)[1].rhs()
commonCaseDownTime(y, gravity, jumpSpeed, jumpSustainTime) = solve([height(t, gravity, jumpSpeed, jumpSustainTime)==y], t)[1].rhs()
maxFallingCaseCaseDownTime(y, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = solve([maxFallingCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)==y], t)[1].rhs()
glidingCaseDownTime(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = solve([glidingCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)==y], t)[0].rhs() # only one solution (it's affine) #
freeFallCaseDownTime(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed) = solve([freeFallCaseHeight(t, gravity, jumpSpeed, jumpSustainTime, maxFallingSpeed)==y], t)[1].rhs()

affineCaseTime(y, jumpSpeed, maxFallingSpeed) = solve([affineCaseHeight(t, jumpSpeed, maxFallingSpeed)==y], t)[0].rhs()

The root conditions are the 3 piece-wise functions described at the end of the Polynomial piece-wise function section. Note that quadratic functions have a maximum or a minimum value which means there won't always be a solution. That's why there are "not a number" checks.

if (maxFallingSpeedReachedTime > jumpEndTime) {
    time = sustainCase(y);
    if (time > jumpSustainTime || Number.isNaN(time)) {
        time = commonCase(y);
        if (time > jumpEndTime || Number.isNaN(time)) {
            time = freeFallCase(y);
            if (time > maxFallingSpeedReachedTime || Number.isNaN(time)) {
                time = glidingCase(y);
            }
        }
    }
}
else if (maxFallingSpeedReachedTime > jumpSustainTime) {
    time = sustainCase(y);
    if (time > jumpSustainTime || Number.isNaN(time)) {
        time = commonCase(y);
        if (time > maxFallingSpeedReachedTime || Number.isNaN(time)) {
            time = maxFallingCase(y);
            if (time > jumpEndTime || Number.isNaN(time)) {
                time = glidingCase(y);
            }
        }
    }
}
else {
    time = sustainCase(y);
    if (time > maxFallingSpeedReachedTime || Number.isNaN(time)) {
        time = maxFallingSpeed >= jumpSpeed ? affineCase(y) : time = Number.MAX_VALUE;
        if (time > jumpSustainTime || Number.isNaN(time)) {
            time = maxFallingCase(y);
            if (time > jumpEndTime || Number.isNaN(time)) {
                time = glidingCase(y);
            }
        }
    }
}

Plotting trajectories

This is the code used to plot the first graphic of the Polynomial piece-wise function section.

# The trajectory of the default jump settings
#
# gravity: 1000
# maxFallingSpeed: 700
# jumpSustainTime: 0.2
# jumpSpeed: 600
#
# n(maxFallingTime(1000, 700))
# 0.7

(
  plot(-sustainCaseHeight(t, 1000, 600), 0, 0.8, ymin=0, color='grey')
+ plot(-commonCaseHeight(t, 1000, 600, 0.2), 0, 0.8, ymin=0, color='black')
+ plot(-maxFallingCaseHeight(t, 1000, 600, 0.2, 700), 0, 0.8, ymin=0, color='grey')
+ plot(-piecewise([
    ((0, 0.2), sustainCaseHeight(t, 1000, 600)),
    ((0.2, maxFallingTime(1000, 700)), commonCaseHeight(t, 1000, 600, 0.2)),
    ((maxFallingTime(1000, 700), 2), maxFallingCaseHeight(t, 1000, 600, 0.2, 700))
  ]), 0, 0.8, ymin=0, color='cyan')
)