Jump to content

PSA: You have to garbage-collect your components yourself


Amineri

Recommended Posts

So I've been trying to trace down a particularly irksome bug in the OfficerPack mod these last few days, and I thought I'd share the results with the modding community.

 

At a high level, what happens is this :

After a dismissing a soldier with leader training, at the end of the next tactical mission, the game crashes. Loading the save without the OfficerPack prevents the crash from occurring.

 

Under the hood, what's going on is the following sequence of events :

  1. Unit gets dismissed. Unit State bRemoved is set true, but the LWOfficer component bRemoved remains false.
  2. At some point, Unit State gets obliterated from History
  3. During History validation (when switching from tactical to Avenger map), an assertion is thrown because there is a component with a reference to a parent that doesn't exist.
  4. The assertion triggers program exit.
A bit of info on Removed vs Obiliterated...
XComGameState.uc has this to say about Removing gamestates:
/// This method is intended to be the typical path for 'removing' a state object from the history. A new state is created, and the 'bRemoved' flag is set on the state object
/// indicating that it should no longer appear in lists of 'current' state objects. The state object will still show up if history frames are examined prior to its removal.
/// <param name="ObjectID">Unique indentifier associated with the object state to be marked as removed</param>
native final function RemoveStateObject(int ObjectID);
In contrast, Obliterate is in XComGameStateHistory.uc, and has this to say :
/// Removes all information related to the last Count states from the history.
/// <param name="Count">Number of states to pop from the history.</param>
native final function ObliterateGameStatesFromHistory(optional int Count = 1);
In theory deleted units should be hanging around with just their bRemoved flag set, so the past states can be accessed, but at some point the unit state is getting obliterated.
When forcing a load of the tactical save without the Officer mod, the LWOfficer component can't be deserialized, so it gets discarded from History, thus preventing the assertion from triggering and halting execution. I was able to get the same result without removing the mod by adding the following to Game.ini:
+TransientTacticalClassNames=XComGameState_Unit_LWOfficer
This forces a purge of all LWOfficer components from History on the end of tactical missions, which happens before the validation code that triggers the assertion. Unfortunately, this isn't a viable "fix" because it also wipes all officer-related data -- so all officers would lose their ranks, etc.
However, searching the History at the point of the tactical save fails to find the officer component that is still hanging around, so I can't really patch up saves that have gotten to that point. The native accessor in XComGameStateHistory apparently can't find a component gamestate whose parent has been obliterated. However, if I search History just after Dismissing a unit, I can find the Officer component, and can verify that the component bRemoved flag is not set, but that its owning Unit parent's bRemoved flag is set. So, looks like I'll have to garbage collect my own components, which is kind of a pain...
The search method I'm using to find my component states looks like this :
local XComGameState_Unit_LWOfficer OfficerState;

foreach History.IterateByClassType(class'XComGameState_Unit_LWOfficer', OfficerState,,true)
{ ... }

I created a static garbage collection and validation method to my LWOfficerUtilities class, which uses the above to look through the history for any LWOfficer component states that are attached to unit states that don't exist or have been flagged for removal, and then marks the component state for removal.

 

 

 

static function GCandValidationChecks()
{
    local XComGameStateHistory History;
    local XComGameState NewGameState;
    local XComGameState_Unit UnitState;
    local XComGameState_Unit_LWOfficer OfficerState;
 
    `LOG("LWOfficerUtilities: Starting Garbage Collection and Validation.");
 
    History = `XCOMHISTORY;
    NewGameState = class'XComGameStateContext_ChangeContainer'.static.CreateChangeState("Officer States cleanup");
    foreach History.IterateByClassType(class'XComGameState_Unit_LWOfficer', OfficerState,,true)
    {
        `LOG("LWOfficerUtilities: Found OfficerState, OwningObjectID=" $ OfficerState.OwningObjectId $ ", Deleted=" $ OfficerState.bRemoved);
        //check and see if the OwningObject is still alive and exists
        if(OfficerState.OwningObjectId > 0)
        {
            UnitState = XComGameState_Unit(History.GetGameStateForObjectID(OfficerState.OwningObjectID));
            if(UnitState == none)
            {
                `LOG("LWOfficerUtilities: Officer Component has no current owning unit, cleaning up state.");
                // Remove disconnected officer state
                NewGameState.RemoveStateObject(OfficerState.ObjectID);
            }
            else
            {
                `LOG("LWOfficerUtilities: Found Owning Unit=" $ UnitState.GetFullName() $ ", Deleted=" $ UnitState.bRemoved);
                if(UnitState.bRemoved)
                {
                    `LOG("LWOfficerUtilities: Owning Unit was removed, Removing OfficerState");
                    NewGameState.RemoveStateObject(OfficerState.ObjectID);
                }
            }
        }
    }
    if (NewGameState.GetNumGameStateObjects() > 0)
        `GAMERULES.SubmitGameState(NewGameState);
    else
        History.CleanupPendingGameState(NewGameState);
}

 

 

 

I had to do this in my case because there are no TriggerEvent calls that occur in the FireStaff call chain for me to listen to to call RemoveStateObject for. I also had rather expected that calling RemoveStateObject on a GameState would also remove all of its components, but this seems to not be the case.

 

For my particular case, the only situation in which units are removed is via the UIArmory_MainMenu dismiss button (killed units stick around so they can be displayed in the memorial), so I added a UIScreenListener that listens to UIArmory_MainMenu, and trigger the GC routine OnRemoved :

event OnRemoved(UIScreen Screen)
{
    //clear reference to UIScreen so it can be garbage collected
    ParentScreen = none;
 
    //garbage collect officer states in case one was dismissed
    class'LWOfficerUtilities'.static.GCandValidationChecks();
}

------------------------

 

I know that using gamestate components is kind of "bleeding edge" right now, but figured I'd toss this out there as a consideration for those of you considering using them.

Link to comment
Share on other sites

ahh Thanks for the PSA, would have been a shame to see saves crash when units are deleted. Using components for Hidden Potential where each unit holds it's own randomised stat progression through all the ranks. Gonna take that code of yours and configure it the same way on my end

Edited by Guest
Link to comment
Share on other sites

Manual garbage collection in UnrealScript? Heresy! :ninja:

 

Seriously though thank you for sharing. I'd be interested to know why Firaxis would wipe a unit from the history, given that their documentation implies that nothing unique should ever be deleted. Or maybe I just read it wrong.

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...