Skip to content

Fix ScriptComponent clone dropping scripts awaiting their script type - #9200

Merged
mvaligursky merged 1 commit into
mainfrom
mv-script-clone-awaiting
Aug 21, 2026
Merged

Fix ScriptComponent clone dropping scripts awaiting their script type#9200
mvaligursky merged 1 commit into
mainfrom
mv-script-clone-awaiting

Conversation

@mvaligursky

@mvaligursky mvaligursky commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

ScriptComponentSystem#cloneComponent iterated _scriptsIndex with for...in and checked key.awaiting, where key is the script name string — so the check was always undefined and the whole block was dead code:

for (const key in entity.script._scriptsIndex) {
    if (key.awaiting) {              // always falsy
        order.splice(key.ind, 0, key);
    }
}

Cloning an entity therefore silently dropped any script whose script type was not in the registry yet. When the type was later registered, the deferred sweep in ScriptRegistry#add created the script on the original entity, but the clone had no awaiting entry so it never got one:

source: _scriptsIndex = [scriptA, loadedLater(awaiting,ind=1), scriptB]
clone : _scriptsIndex = [scriptA, scriptB]
--> after loadedLater is registered
source.script.loadedLater: EXISTS
clone.script.loadedLater : MISSING

Fix

Dereferencing the index entry is not enough on its own:

  • The awaiting script also needs an entry in scripts. initializeComponentData looks every name from order up in scripts (data.scripts[data.order[i]].enabled), so a name in order but missing from scripts throws TypeError: Cannot read properties of undefined (reading 'enabled') — a hard throw out of entity.clone() in place of the silent drop. The data comes from _scriptsData, which is also where the registry sweep reads the attributes from when it eventually creates the script.

  • The awaiting script needs a well-defined slot. It is placed directly after the script it was declared after, which is where the deferred creation puts it on the source entity (see Create scripts added to the registry later at their declared index #9202), so source and clone converge on the same order.

That needs the declaration order of all scripts, created or not. Neither existing record is that: _scripts only holds created scripts and is reordered by move(), and the key order of _scriptsIndex is not declaration order for every accepted script name (integer-like keys are enumerated first, so ['scriptA', '10', '2', 'scriptB'] enumerates as 2, 10, scriptA, scriptB). So it is recorded explicitly as _declarationOrder, maintained in create() / destroy().

Supersedes #8264, which is the one-line dereference and hits the TypeError above.

Fixes #2796

Testing

Five tests in test/framework/components/script/component.test.mjs, all five fail on main:

  • the clone keeps the awaiting entry, its ind, and its declared enabled / attribute data
  • consecutive awaiting scripts keep their relative order
  • ...including with integer-like script names
  • an awaiting script is placed after the script it was declared after, when that has been moved
  • the clone gets the instance, in the right slot, once the script type is registered

Checklist

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2371.8 KB (+0.4 KB, +0.02%) 609.5 KB (+0.1 KB, +0.02%) 473.4 KB (+0.1 KB, +0.01%)
playcanvas.min.mjs 2369.2 KB (+0.4 KB, +0.02%) 608.4 KB (+0.1 KB, +0.02%) 472.6 KB (+0.0 KB, +0.01%)

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review by Codex (GPT-5).

No actionable issues found in this PR in isolation.

The fix correctly dereferences each _scriptsIndex entry, reconstructs the missing scripts data required by component initialization, copies awaiting attribute data, and offsets equal captured indices so consecutive awaiting declarations are not reversed during cloning. The clone then retains an awaiting entry and participates in the later registry sweep.

Local verification on the exact head: the full focused ScriptComponent suite passed (91 tests), ESLint passed for both changed files, and git diff --check passed. All current GitHub checks are green.

`cloneComponent` checked `key.awaiting` while iterating `_scriptsIndex` with
`for...in`, where `key` is the script name string, so the check was always
false and the block was dead code. Cloning an entity therefore dropped any
script whose script type was not yet in the registry: when the type was later
registered, the deferred registry sweep created the script on the original
entity but the clone had no awaiting entry, so it never got it.

Restoring the name into `order` is not enough on its own, the awaiting script
also needs an entry in `scripts` - `initializeComponentData` looks every name
coming from `order` up in there, so the clone would throw a TypeError instead
of silently dropping the script.

Place the awaiting script directly after the script it was declared after,
which is where the deferred creation will put it on the source entity. That
needs the declaration order of all scripts, created or not, which neither
`_scripts` (reordered by move()) nor the key order of `_scriptsIndex`
(integer-like script names are enumerated first) is, so record it explicitly
as `_declarationOrder`.

Fixes #2796

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated re-review by Codex (GPT-5).

Re-reviewed the changes since commit cc49f49. No actionable issues found.

The explicit _declarationOrder record removes the integer-like-key enumeration problem and gives clone reconstruction the same predecessor rule used by the stacked ordering fix. It is maintained across create/destroy, protected as a reserved script name, and reconstructs moved-plus-awaiting clones consistently. The expanded focused ScriptComponent suite passed (93 tests); changed-file ESLint, diff validation, and all current CI checks are green.

@mvaligursky
mvaligursky merged commit 79dd9d3 into main Aug 21, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the mv-script-clone-awaiting branch August 21, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ScriptComponent clone awaiting

1 participant