Fix/Perf/Refactor: buildableUnits and many related code#3220
Fix/Perf/Refactor: buildableUnits and many related code#3220VariableVince wants to merge 28 commits intomainfrom
Conversation
…e needs both canAttack and buildableUnits
…yerBuildableTypes which includes Transport Ship. Accept only readonly which includes existing enum UnitType. buildableUnits only returns PlayerBuildableTypes that the player .. can build while Executions keep using canBuild directly
…and now becoming unused)
…leUnits can never ask for eg UnitType.Train when it doesn't return data for that type. In order to make type safety work in StructureIconsLayer for GhostUnit.buildableUnit.type too, changed type of interface BuildableUnit to PlayerBuildableType too. Which is only more accurate. Same for uiState.ghostStructure and with that, renderUnitItem in UnitDisplay and setGhostStructure in InputHandler. Have all callers ask for their type of units, not only StructureIconsLayer and NukeTrajectoryPreviewLayer which already did, but also ClientGameRunner, BuildMenu and PlayerPanel. Only MainRadialMenu needs all player buildable unit types including transport ship, so it can stay undefined there. Added a 'null' argument so that PlayerPanel and one function in ClientGameRunner can ask for no buildableUnits at all, which will stop GameRunner from calling PlayerImpl buildableUnits. Just like GameRunner already doesn't call canAttack when tile is null.
WalkthroughAdds BuildableUnit and PlayerBuildableUnitType + BuildMenuTypes; changes player.actions to accept readonly PlayerBuildableUnitType[] | null; adds playerBuildables RPC and GameRunner.playerBuildables; removes several UnitInfo flags; replaces territoryBound checks with isStructureType/StructureTypes; updates many client call sites and UI types to use buildables. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Client UI
participant CGR as ClientGameRunner
participant WC as WorkerClient
participant WK as Worker
participant GR as GameRunner
participant P as PlayerImpl
UI->>CGR: request buildables(tile, units?)
CGR->>WC: worker.playerBuildables(playerID, x, y, units)
WC->>WK: post "player_buildables"
WK->>GR: gameRunner.playerBuildables(playerID, x, y, units)
GR->>P: player.buildableUnits(tile, units?)
P-->>GR: BuildableUnit[] (type, cost, canBuild/canUpgrade)
GR-->>WK: player_buildables_result
WK-->>WC: reply
WC-->>CGR: resolve BuildableUnit[]
CGR-->>UI: provide buildables to UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
…improvements for BuildMenu and RadialMenuElements.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/core/game/PlayerImpl.ts (1)
983-991: Cost is still recomputed insidecanUpgradeUnit(line 953).You precompute
costat line 983 and pass it intocanBuild— nice. ButfindUnitToUpgradecallscanUpgradeUnit, which independently recomputesthis.mg.config().unitInfo(unit.type()).cost(this.mg, this)at line 953 to check gold. That is the same value ascosthere.Consider threading
knownCostthroughfindUnitToUpgrade→canUpgradeUnitthe same way you did forcanBuild, so you compute cost exactly once per unit type.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/game/PlayerImpl.ts` around lines 983 - 991, Precompute cost once and thread it into the upgrade checks: change findUnitToUpgrade to accept a knownCost (or pass the already computed cost variable) and forward that into canUpgradeUnit, then modify canUpgradeUnit to accept an optional knownCost parameter and use it instead of calling this.mg.config().unitInfo(...).cost(...) internally; update all call sites of findUnitToUpgrade and canUpgradeUnit to pass the cost where available and fall back to computing cost only when knownCost is not provided to preserve existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/core/game/PlayerImpl.ts`:
- Around line 983-991: Precompute cost once and thread it into the upgrade
checks: change findUnitToUpgrade to accept a knownCost (or pass the already
computed cost variable) and forward that into canUpgradeUnit, then modify
canUpgradeUnit to accept an optional knownCost parameter and use it instead of
calling this.mg.config().unitInfo(...).cost(...) internally; update all call
sites of findUnitToUpgrade and canUpgradeUnit to pass the cost where available
and fall back to computing cost only when knownCost is not provided to preserve
existing behavior.
|
I'm having trouble understanding this PR since it contains unrelated fixes, would it be possible to break this up into multiple PRs? |
Not that easy with my lacking git skills. Without those it will cost a lot of time to split up (or fix mistakes when i do try git magic and mess up) so may either take awhile or not happen alas |
Description:
After #3213 got merged, the change with largest impact in #3193 was done in such a different way that a new PR was needed
The idea in 3193 was to not always ask for Transport Ship from buildableUnits. In such a way that very little extra data was send to the worker. This had the biggest impact on performance (the idea was months older btw, see #2295). Now, we do it the other way around, by telling buildableUnits all unit types we want. Or we want them all (undefined). The downside is more data is send in the worker message. The upside is we have more options and can add more in this PR.
This PR implements some of the leftovers in 3193 on top of 3213 and adds more.
GameRunner: Adds option to ask for no buildable untis (null) in playerActions.
GameRunner: Fixes wrong assumption in Add units filter on playeractions for performance #3213: that only if units was undefined, we have to know canAttack. ClientGameRunner wants to know both, in case of a click on non-bordering land, to decide if it should auto-boat using a Transport Ship. So units is not undefined (we only ask for Transport Ship now which has a positive effect on performance for each click/tap) but we need canAttack still.
Solved by removing the unit === undefined check before canAttack in playerActions. playerActions now has 3 modes: get all actions and all buildings (units undefined), get all actions and no buildings (units null), or get all actions and specific building (units contains Unit Types).
GameRunner: But with above solved, there was still no option to only get buildable units. While StructureIconsLayer, NukeTrajectoryPreviewLayer, BuildMenu and UnitsDisplay need only that. To not make playerActions more convoluted with more params or so, i've added a new function playerBuildables to only get buildable units. playerBuildables has 2 modes: get all buildings (units undefined) or get specific buildings (units contains Unit Types).
Have all callers of GameView .actions or .buildables, ultimately GameRunner playerActions and playerBuildables, ask for their type of units. Not only StructureIconsLayer and NukeTrajectoryPreviewLayer but also ClientGameRunner, BuildMenu, UnitsDisplay and PlayerPanel. Only MainRadialMenu needs all player buildable unit types including Transport Ship, so it can stay undefined there.
PlayerImpl: validStructureSpawnTiles did a filter on unit types to get isTerroritoryBound units, on every call again. It read this from unit info in DefaultConfig so while that's good for maintainability, other code already uses hardcoded StructureTypes and isisStructureType from Game.ts. Which has the same purpose and thus contains the same unit types. StructureTypes and isisStructureType do need manual maintainance outside of DefaultConfig. And are more bug prone/less type safe. Still, using them gives more speed compared to putting it in some get function in GameImpl for example (tested with buildableUnits and MIRVPerf.ts. So went with StructureTypes in validStructureSpawnTiles.
PlayerImpl: Early return for non-upgradable unit types in findUnitToUpgrade. If it is upgradable, it will again check in canUpgradeUnit if the unit type is upgradable alas. Since canUpgradeUnit is also used by other code, this double check cannot be removed in a simple way. Still the early return makes the code faster as there are a number of units that can't be upgraded so we can skip all that happens in findUnitToUpgrade.
PlayerImpl: buildableUnits would do Object.values(UnitTypes) on every call. Now only get player buildable units, exclude MIRVWarhead, TradeShip, Train, SamMissile and Shell. Since a player doesn't build those by themselves, they are only build by Executions which use canBuild directly instead of buildableUnits. Added PlayerBuildableTypes to Game.ts in the same fashion as existing StructureTypes and isStructureType.
PlayerImpl: buildableUnits would check twice for tile!==null to decide to call findUnitToUpgrade and canBuild. Now once.
PlayerImpl: buildableUnits would call canBuild which checks Cost, but then it fetches the Cost itself too. Now just do this once with canBuild accepting it as optional param. This does not affect other code that calls canBuild.
Game / PlayerImpl: Typesafety: adds new type PlayerBuildableUnitType so callers of buidableUnits can never ask for the wrong type like e.g. UnitType.Train because it doesn't return data for that type.
StructureIconsLayer: In order to make type safety work in StructureIconsLayer for GhostUnit.buildableUnit.type too, changed type of interface BuildableUnit to PlayerBuildableType. Which is only more accurate. Same for uiState.ghostStructure and with that, renderUnitItem in UnitDisplay and setGhostStructure in InputHandler. All Structures are of PlayerBuildableType (there are even some in PlayerBuildableType that aren't Structures, but it is much more confined than UnitType).
RadialMenuElements: replace non-central ATTACK_UNIT_TYPES in RadialMenuElements with centralized BuildableAttackTypes too. Use PlayerBuildableUnitType for more type safety (can't by mistake add UnitType.Train to its build menu). Make use of StructureTypes and BuildableAttackTypes instead of adding items hardcoded line by line in getAllEnabledUnits. Don't call canBuildOrUpgrade 3x in CreateMenuElements for the .map on flattenedBuildTable, instead do it once.
BuildMenu: Fix some comments. Make playerBuildables have default value of null (null was already set as one of the possible values). Now we can remove the "?? []" check in canBuildOrUpgrade, since we already checked if playerBuildables is null when we get there.
Some related (perf) changes:
NationStructureBehavior: removed canUpgradeUnit check from maybeUpgradeStructure. Since it already checked this right before in findBestStructureToUpgrade, so only upgradable units are returned. Furthermore, UpgradeStructureExecution is called right after which also again checks canUpgradeUnit. So we're going from 3 times to 2 times canUpgradeUnit, small perf win on its own.
NationStructureBehavior: instead of hardcoded ruling out Defense Post for upgrade check in maybeSpawnStructure, check dynamically if type is upgradable. That way if defense posts ever do become upgradable, we don't run into a bug right away.
NationsStructureBehavior: in getTotalStructureDensity, we can pass an array of types as argument so we only have to get an array length once. It needs to ignore levels so we can't make use of other pre-defined functions in PlayerImpl (which were created to avoid array length calls), but at least this saves a few.
GameImpl/GameView nearbyUnits: it accepted "UnitType | UnitType[]" for tiles, but called UnitGrid nearbyUnits which mandates "UnitType | readonly UnitType[]". Made the requirement the same for GameImpl/GameView nearbyUnits. This way, we don't have make a shallow copy of the StructureTypes array everytime we want to send it as an argument, from validStructureSpawnTiles and Util.ts. Other callers aren't affected.
ClientGameRunner: removed two redundant myPlayer===null checks since that was already done right above.
PlayerExecution: now validStructureSpawnTiles no longer needs isTerritoryBound, PlayerExecution is the last place where it was used. Replaced it for isStructureType here too (since it has the same meaning and outcome).
Game.ts and DefaultConfig unitInfo: removed canBuildTrainStation and expirimental properties, as they already weren't used anywhere anymore. Removed isTerritoryBound too as it was only used in validStructureSpawnTiles and PlayerExecution and has been replaced in both.
PlayerActionHandler: remove unused getPlayerActions, the only potential caller MainRadialMenu already just calls myPlayer.actions via GameView directly.
StructureIconsLayer: remove already unused PlayerActions
Worker.worker: correct some existing error messages
And more
Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
tryout33