Skip to content

[GEN][ZH] Fix object deselection logic in BuildAssistant::sellObject()#1216

Merged
xezon merged 1 commit into
TheSuperHackers:mainfrom
Mauller:fix-buildassistant-sellObject
Jul 9, 2025
Merged

[GEN][ZH] Fix object deselection logic in BuildAssistant::sellObject()#1216
xezon merged 1 commit into
TheSuperHackers:mainfrom
Mauller:fix-buildassistant-sellObject

Conversation

@Mauller

@Mauller Mauller commented Jun 29, 2025

Copy link
Copy Markdown

This PR fixes the ordering of deselection logic within the build assistants sellObject() function.

I came across this while resolving #1200 and it corrects the object handling for removing the required object from the current selected objects.

In the original ordering, the object being sold was not being removed from it's original AiGroup and put into a new AIGroup within Player::getCurrentSelectionAsAIGroup() when it was called by GameLogic::deselectObject().

This is because getCurrentSelectionAsAiGroup tests if an object is selectable before placing it in an AiGroup.

This meant that subsequent code within GameLogic::deselectObject() was not working as intended.

At the moment there should not be any user facing changes with this.

TODO

  • Test against many Replays

@Mauller Mauller self-assigned this Jun 29, 2025
@Mauller Mauller added Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels Jun 29, 2025
@xezon

xezon commented Jun 29, 2025

Copy link
Copy Markdown

In the original ordering, the object being sold was not being removed from it's original AiGroup and put into a new AIGroup within Player::getCurrentSelectionAsAIGroup() when it was called by GameLogic::deselectObject().

This is because getCurrentSelectionAsAiGroup tests if an object is selectable before placing it in an AiGroup.

This meant that subsequent code within GameLogic::deselectObject() was not working as intended.

I dont fully understand this. What do the AIGroups look like before and what do they look like afterwards?

Also if the AIGroup membership changes now, then this will mismatch with the Retail version, right?

@Mauller

Mauller commented Jun 29, 2025

Copy link
Copy Markdown
Author

In the original ordering, the object being sold was not being removed from it's original AiGroup and put into a new AIGroup within Player::getCurrentSelectionAsAIGroup() when it was called by GameLogic::deselectObject().
This is because getCurrentSelectionAsAiGroup tests if an object is selectable before placing it in an AiGroup.
This meant that subsequent code within GameLogic::deselectObject() was not working as intended.

I dont fully understand this. What do the AIGroups look like before and what do they look like afterwards?

Also if the AIGroup membership changes now, then this will mismatch with the Retail version, right?

Before the change the object deselect is being called on remains in the current group it is a part of, if it was within one.
After the change it gets properly transfered to and then deleted from the new AIGroup that is created when updating the current selection.

This showed up in the groupSell as the first object on the list remaining in the member list on the calling AIGroup while everything else got removed. Since the other objects got transfered to the new AIGroup and set as the currently selected objects / buildings etc.

This is more about making the code function as it was intended, the way this gets called in game by players means the AIGroups were already being cleaned up during a game. And the subsequent code only functioned by chance and not by intention.

@xezon xezon changed the title [GEN][ZH] fix object deselection logic in BuildAssistant::sellObject() [GEN][ZH] Fix object deselection logic in BuildAssistant::sellObject() Jun 30, 2025
@xezon

xezon commented Jun 30, 2025

Copy link
Copy Markdown

This needs to be tested for Retail mismatching.

@Skyaero42

Copy link
Copy Markdown

This needs to be tested for Retail mismatching.

I'll pick up the testing against replays

@Skyaero42 Skyaero42 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.

Code looks good

Tested against 1,000 replays without issues.

@xezon xezon 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.

I do not quite understand what this change is supposed to fix. I sold single buildings, stepped through the code, and saw no difference in behaviour with and without this change.

Does this only concern logic that would sell multiple buildings at once? If yes, how? And was this tested for Mismatch? As far as I am aware in regular play it is not possible to select and sell multiple buildings with one action.

Blocking merge until situation is clear.

@Mauller

Mauller commented Jul 2, 2025

Copy link
Copy Markdown
Author

I do not quite understand what this change is supposed to fix. I sold single buildings, stepped through the code, and saw no difference in behaviour with and without this change.

Does this only concern logic that would sell multiple buildings at once? If yes, how? And was this tested for Mismatch? As far as I am aware in regular play it is not possible to select and sell multiple buildings with one action.

Blocking merge until situation is clear.

So the change allows the correct behaviour of object deselection to occur within GameLogic::deselectObject when deselecting a building that is being sold.

When the object is set to be unselectable, the call to Player::getCurrentSelectionAsAIGroup -> Squad::aiGroupFromSquad does not remove the object the deselection was called on from its original AIGroup.

This occurs because Squad::getLiveObjects, that is called within aiGroupFromSquad, filters based on if an object is selectable or not. If the object is not selectable it leaves the object in it's prior AIgroup. Which when using groupSell, it shows up as the object remaining in the callees AIGroup member list when it should have been removed from it.

Once the logic returns back into GameLogic::deselectObject the AIGroup it is acting on implicitly lacks the object that deselectObject was called for, so the following essentially doesn't do what it was meant to be doing.

if (group) {
    Bool emptied = group->remove(obj);

The code only worked by happenstance as the original AIGroup the building belonged to would get deleted in the command handling function that GroupSell was being called on in this case. And if other objects were part of the selection they would be in the new AIGroup to be set as the current selection.

It also removes the object ID for the building being sold from the current selection cache.

This change is more about making sure the code is doing what it was meant to so that when we make future changes we don't come across unexpected behaviour.

@xezon

xezon commented Jul 2, 2025

Copy link
Copy Markdown

Can you perhaps give repro steps how the wrong behavior can be triggered?

I stepped through the code on single object selling and the behaviour was the exact same.

@Mauller

Mauller commented Jul 2, 2025

Copy link
Copy Markdown
Author

Can you perhaps give repro steps how the wrong behavior can be triggered?

I stepped through the code on single object selling and the behaviour was the exact same.

Just sell a building, breakpoint inside the GameLogic::deselectObject just before the call to Player::getCurrentSelectionAsAIGroup then check back up the call stack and check the status of m_memberlist within the call to groupSell it should contain the building being sold.

In the incorrect behavour, the moment you step over Player::getCurrentSelectionAsAIGroup the building will still be in the m_memberlist and the AIGroup returned from Player::getCurrentSelectionAsAIGroup will be empty or destroyed.

in the corrected behaviour, the object should be removed from the m_memberlist and be contained in the AIGroup returned from Player::getCurrentSelectionAsAIGroup, the call Bool emptied = group->remove(obj); further on within GameLogic::deselectObject will then remove the object and set emptied to true.

Externally the behaviour appears to be the same since beyond the deselect function the object being sold is no longer in the AIGroup that overwrites the current selection, but from what i remember the player cached selection can still contain the ID of the building that was being sold. you can see the cached selection if you go down the rabbit hole of what is going on within Player::getCurrentSelectionAsAIGroup.

@xezon

xezon commented Jul 4, 2025

Copy link
Copy Markdown

In the incorrect behavour, the moment you step over Player::getCurrentSelectionAsAIGroup the building will still be in the m_memberlist and the AIGroup returned from Player::getCurrentSelectionAsAIGroup will be empty or destroyed.

in the corrected behaviour, the object should be removed from the m_memberlist and be contained in the AIGroup returned from Player::getCurrentSelectionAsAIGroup, the call Bool emptied = group->remove(obj); further on within GameLogic::deselectObject will then remove the object and set emptied to true.

I tested this and cannot confirm this observation. m_memberlist is 1 and new group is 0 in both scenarios.

If what you described was true, then I would expect retail mismatch, because any difference in AIGroup lifetime will cause mismatch.

@Mauller

Mauller commented Jul 5, 2025

Copy link
Copy Markdown
Author

In the incorrect behavour, the moment you step over Player::getCurrentSelectionAsAIGroup the building will still be in the m_memberlist and the AIGroup returned from Player::getCurrentSelectionAsAIGroup will be empty or destroyed.
in the corrected behaviour, the object should be removed from the m_memberlist and be contained in the AIGroup returned from Player::getCurrentSelectionAsAIGroup, the call Bool emptied = group->remove(obj); further on within GameLogic::deselectObject will then remove the object and set emptied to true.

I tested this and cannot confirm this observation. m_memberlist is 1 and new group is 0 in both scenarios.

If what you described was true, then I would expect retail mismatch, because any difference in AIGroup lifetime will cause mismatch.

I think it's memory manager shenanigans, if you run without the memory manager you see the behaviour and the game will crash due to the original group being destroyed and not overwritten yet.

I always tend to run under debug and without the memory manager since it catches far more problems.

Mismatching also won't be an issue in this instance since the groups are updated and destroyed within the same frame under both scenarios.

@xezon

xezon commented Jul 5, 2025

Copy link
Copy Markdown

I think it's memory manager shenanigans, if you run without the memory manager you see the behaviour and the game will crash due to the original group being destroyed and not overwritten yet.

I always tend to run under debug and without the memory manager since it catches far more problems.

Mismatching also won't be an issue in this instance since the groups are updated and destroyed within the same frame under both scenarios.

I have tested this with Null Game Memory and the behavior is still the same. There is no crash on Sell either. Can you please test again and verify that your initial observation is correct? If it is, I suggest we do a screen sharing and you show me. Because I cannot reproduce this issue at all.

@Mauller Mauller force-pushed the fix-buildassistant-sellObject branch from 83ee195 to ad4fc6e Compare July 9, 2025 19:29
@Mauller

Mauller commented Jul 9, 2025

Copy link
Copy Markdown
Author

just a rebase

@Mauller Mauller force-pushed the fix-buildassistant-sellObject branch from ad4fc6e to 1700676 Compare July 9, 2025 20:18
@Mauller

Mauller commented Jul 9, 2025

Copy link
Copy Markdown
Author

Tweaked to make the tweaked behaviour locked behind the fixed AIGroup handling while keeping current behaviour as the active behaviour

@xezon xezon 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.

Looks good.

@xezon xezon added the NoRetail This fix or change is not applicable with Retail game compatibility label Jul 9, 2025
@xezon xezon merged commit 1a8475d into TheSuperHackers:main Jul 9, 2025
14 checks passed
@xezon xezon deleted the fix-buildassistant-sellObject branch July 9, 2025 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker NoRetail This fix or change is not applicable with Retail game compatibility ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants