Skip to content

unify(worldbuilder): Align script import and export - #3409

Open
OmarAglan wants to merge 1 commit into
TheSuperHackers:mainfrom
OmarAglan:unify/worldbuilder-script-import-export
Open

OmarAglan wants to merge 1 commit into
TheSuperHackers:mainfrom
OmarAglan:unify/worldbuilder-script-import-export

Conversation

@OmarAglan

Copy link
Copy Markdown

Follow up for #3368.
Relates to #555.

Aligns the import/export portions of ScriptDialog and ExportScriptsOptions with Zero Hour.

Generals inherits side-dictionary import, team-owner selection handling and the export-side option. Both editors use the aligned export dialog, sized to contain its controls.

Compatibility:

  • Retail-compatible Generals continues writing ScriptsPlayers v1, with side export disabled.
  • Generals without RETAIL_COMPATIBLE_DATA and Zero Hour write v2.
  • Both readers accept v1 and v2.
  • Script-bundle PolygonTriggers remain v3.

The player-matching correction is kept in a separate bugfix branch. Script-warning changes and Core moves remain outside this PR.

Validation:

  • Both VC6 Release WorldBuilder targets build.

  • All 16 targeted VC6 compilations pass across both games, debug/release and DATA=0/1.

  • 1,904 extracted writer/reader fixture cases pass.

  • Focused team-owner and export-dialog checks pass.

  • test how the world builder looks (WIP)

AI assistance was used for implementation and local verification.

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Walkthrough

WorldBuilder adds a control for optional side-data export. Script export writes versioned player data, with Generals retail-compatible builds retaining version 1. Import handling reads side dictionaries from version-2-or-later data and updates side lists and team ownership.

Changes

Side-data export and import

Layer / File(s) Summary
Side-data export option
Generals/Code/Tools/WorldBuilder/include/ExportScriptsOptions.h, Generals/Code/Tools/WorldBuilder/src/ExportScriptsOptions.cpp, Generals/Code/Tools/WorldBuilder/res/WorldBuilder.rc, Generals/Code/Tools/WorldBuilder/res/resource.h, GeneralsMD/Code/Tools/WorldBuilder/src/ExportScriptsOptions.cpp, GeneralsMD/Code/Tools/WorldBuilder/res/WorldBuilder.rc
The dialog adds an “Include sides” checkbox and stores its state. Generals retail-compatible builds clear and disable the option.
Player-data export
Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp, GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
Exports use version 2 and write the side-export flag outside Generals retail-compatible builds. Those builds use version 1 and disable side export.
Player-data import
Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
Version-2-or-later imports read side dictionaries and add sides whose player names are not present. The dialog refreshes script lists and side icons after import. Team imports preserve a valid selected owner or use the neutral player.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Merge Risk: 🟡 Moderate · up to fc902

Importing scripts with sides into a map that already has sides can show the imported side under the wrong tree entry or not at all. Fix the index passed to addPlayer before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to fc902

The new Generals importer can terminate the editor on inconsistent or capacity-exceeding side records. Failed imports can also leave partially imported sides available for later acceptance. Exposure requires opening a script bundle in the editor; retail-compatible export remains protected.

Retained concerns

  • Medium · security · inferred: The new Generals v2 importer crosses from file-controlled side records into a capacity-limited model without enforcing insertion and identity invariants. It checks duplicates using the separate player-name field, initializes the side from the dictionary, then looks it up using that separate name. A mismatched dictionary identity can therefore produce no matching side. At capacity, addSide refuses insertion, producing the same outcome. Both paths subsequently use the missing side, allowing an imported bundle to terminate the editor. The per-file record limit does not constrain existing sides plus newly imported sides.
  • Medium · reliability · inferred: New side creation occurs directly in the dialog model during parsing, without a failure checkpoint. If a later dictionary contains an unsupported type, readDict throws; OnLoad catches the exception without restoring previously imported sides. The successful-load undo operation has not yet been created, and a subsequent OnOK commits the retained model. Consequently, rejecting a bundle does not reliably reject its side-state changes, weakening failure containment for file-controlled map data.
Security review details

Security Blast Radius

  • inferred — The demonstrated attack path requires a user to import a supplied script bundle into Generals WorldBuilder. Its established outcomes are editor availability loss and unintended side-state changes in the open map. No privilege escalation, cross-tenant reach, or code-execution outcome was established.

Security Findings and Attack Paths

  • inferred — A crafted v2 record can name a nonexistent player while its dictionary stores a different identity; alternatively, a valid new identity can exceed the destination map's remaining side capacity. The newly added Generals reader assumes a matching side exists afterward and uses it. This establishes a local denial-of-service path without establishing memory corruption or code execution.

Trust Boundaries and Controls

  • observed — The relevant trust boundary is script-file content entering the local editor model. Existing-name deduplication prevents ordinary replacement of matching side dictionaries, and the shared model prevents capacity overflow. These controls do not enforce agreement between serialized names and dictionary identities or communicate insertion failure to the importer.

Resilience and Maintainability Implications

  • inferred — A bundle can add a valid side before a later invalid dictionary type raises an exception. Since the catch path does not reset the dialog model, later acceptance can persist part of the rejected bundle. Successful-load undo is useful counterevidence, but does not cover this pre-commit failure state.

Hardening Proposals

  • proposed — Stage imported side records separately, validate one canonical identity and destination capacity, and make side creation return an explicit result. Apply model changes only after the complete import validates, with recovery covering any subsequently applied changes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: aligning WorldBuilder script import and export.
Description check ✅ Passed The description explains the import/export changes, compatibility behavior, and validation. It is directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

[Medium risk] Adds optional sides export to the world builder script tool.

The PR is not safe to merge until the Generals import crash and incorrect player-tree indexing are fixed.

Findings

  1. P1 Security Imported side can crash editor ▶
  2. P1 New player uses wrong index ▶
Summary

The PR aligns Generals script-bundle import and export with Zero Hour, adds the side-export option, preserves selected team ownership, and enlarges both export dialogs. The new Generals v2 import path needs correction before merge because malformed side data can crash the editor and newly added players can be attached to the wrong tree index.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Export options] --> B{Retail-compatible Generals?}
  B -->|Yes| C[ScriptsPlayers v1: names]
  B -->|No| D[ScriptsPlayers v2: side flag, names, optional dictionaries]
  C --> E[Bundle import]
  D --> E
  E --> F[Read players and optional sides]
  F --> G[Read teams and scripts]
  G --> H[Refresh player tree]
Loading

Reviews (1) · Last reviewed commit: "unify(worldbuilder): Align script import..."

Comment on lines +1691 to +1693
SidesInfo* sides = pThis->m_sides.findSideInfo(pThis->m_readPlayerNames[i]);
// A script list must be created.
sides->setScriptList(pList);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Imported side can crash editor

A v2 script bundle can provide a player-name string that differs from the name in its side dictionary. The code adds the dictionary unchanged, then searches using the separate string and dereferences a null result, crashing WorldBuilder. The same dereference is possible when the map is already at the side limit and no new side can be added. Validate the name and confirm the side was added before using it.

How this was verified: The bundle supplies the two names separately, and the side lookup returns null when no stored side matches the player-name string.

Knowledge Base Used: Content pipeline tools

// A script list must be created.
sides->setScriptList(pList);
// Update the dialog.
pThis->addPlayer(i);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 New player uses wrong index

When a v2 bundle adds a player to a map that already has sides, the new side is appended after those sides, but addPlayer(i) receives its position in the bundle instead. The tree node then points to an existing player, so the imported player's scripts do not appear under the correct player and the later refresh cannot reliably find its node. Use the new side's actual index.

Suggested change
pThis->addPlayer(i);
pThis->addPlayer(pThis->m_sides.getNumSides() - 1);

Knowledge Base Used: Content pipeline tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 01404c5e-443a-4e11-bd76-3f3b6bf198af
📥 Commits

Reviewing files that changed from the base of the PR and between 5ae042c and fc90243.

📒 Files selected for processing (8)
  • Generals/Code/Tools/WorldBuilder/include/ExportScriptsOptions.h
  • Generals/Code/Tools/WorldBuilder/res/WorldBuilder.rc
  • Generals/Code/Tools/WorldBuilder/res/resource.h
  • Generals/Code/Tools/WorldBuilder/src/ExportScriptsOptions.cpp
  • Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
  • GeneralsMD/Code/Tools/WorldBuilder/res/WorldBuilder.rc
  • GeneralsMD/Code/Tools/WorldBuilder/src/ExportScriptsOptions.cpp
  • GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

// A script list must be created.
sides->setScriptList(pList);
// Update the dialog.
pThis->addPlayer(i);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1655,1710p' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
rg -n 'addPlayer' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 2073


🏁 Script executed:

printf '%s\\n' '--- addPlayer ---'
sed -n '385,435p' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
printf '%s\\n' '--- side collection definitions ---'
rg -n 'SidesList::addSide|addSide\\s*\\(|class SidesList|SidesList::getNumSides' Generals/Code

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 1946


🏁 Script executed:

printf '%s\n' '--- addPlayer remainder ---'
sed -n '415,455p' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
printf '%s\n' '--- side collection symbol locations ---'
rg -n 'addSide|class SidesList' Generals/Code | head -80

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 3896


🏁 Script executed:

printf '%s\n' '--- SidesList and addSide definitions/usages ---'
rg -n 'class SidesList|struct SidesList|SidesList::|addSide[[:space:]]*\(' --glob '*.{h,cpp}' .
printf '%s\n' '--- ScriptDialog import/tree flow ---'
rg -n -C 5 'ParsePlayersDataChunk|DeleteAllItems|addPlayer[[:space:]]*\(|OnLoad|LoadScript' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 14035


🏁 Script executed:

printf '%s\n' '--- SidesList declaration and index lookup ---'
sed -n '132,226p' Core/GameEngine/Include/GameLogic/SidesList.h
sed -n '390,407p' Core/GameEngine/Source/GameLogic/Map/SidesList.cpp
printf '%s\n' '--- SidesList append implementation ---'
sed -n '598,614p' Core/GameEngine/Source/GameLogic/Map/SidesList.cpp
printf '%s\n' '--- OnLoad post-parse flow ---'
sed -n '1359,1495p' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp
printf '%s\n' '--- reviewed diff hunk ---'
git diff --unified=4 5ae042cafb4f08c5ec264acbdfc2db964a41b295 fc90243e3240e59f3849d82e10e1073f2e8fae0c -- Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp | rg -n -C 12 'addPlayer\(i\)|addSide\(&sideDict\)'

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 8860


🏁 Script executed:

rg -n -C 20 'ScriptDialog::reloadPlayer|reloadPlayer[[:space:]]*\(' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp Generals/Code/Tools/WorldBuilder/include/ScriptDialog.h

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 26581


🏁 Script executed:

sed -n '629,700p' Generals/Code/Tools/WorldBuilder/src/ScriptDialog.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 2053


Pass the appended side’s index to addPlayer.

If the destination already has sides, or earlier imported records were skipped, i can differ from the new side’s index. For example, with two existing sides, a new first imported record appends at index 2, but addPlayer(0) inserts another tree item for side 0. The later reloadPlayer(2) cannot find the new side’s item and returns.

Suggested fix
 				pThis->m_sides.addSide(&sideDict);
 				ScriptList* pList = newInstance(ScriptList);
-				SidesInfo* sides = pThis->m_sides.findSideInfo(pThis->m_readPlayerNames[i]);
+				Int sideIndex;
+				SidesInfo* sides = pThis->m_sides.findSideInfo(pThis->m_readPlayerNames[i], &sideIndex);
 				// A script list must be created.
 				sides->setScriptList(pList);
 				// Update the dialog.
-				pThis->addPlayer(i);
+				pThis->addPlayer(sideIndex);

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.

1 participant