X2CommunityCore / X2WOTCCommunityHighlander

https://steamcommunity.com/workshop/filedetails/?id=1134256495
MIT License
60 stars 69 forks source link

Black Market sell UI is laggy #1397

Open faanlez opened 1 month ago

faanlez commented 1 month ago

UIBlackMarket_Sell has multiple functions that cause it to fetch GameStates from History too often. This is the main cause of it getting laggy with high number of items.

The main culprits are these two functions but there are many other improvements possible as well:

function int SortByInterest(BlackMarketItemPrice BuyPriceA, BlackMarketItemPrice BuyPriceB)
{
    local XComGameState_BlackMarket BlackMarketState;
    local XComGameState_Item ItemA, ItemB;

    History = `XCOMHISTORY;
    BlackMarketState = XComGameState_BlackMarket(History.GetGameStateForObjectID(BlackMarketReference.ObjectID));
    ItemA = XComGameState_Item(History.GetGameStateForObjectID(BuyPriceA.ItemRef.ObjectID));
    ItemB = XComGameState_Item(History.GetGameStateForObjectID(BuyPriceB.ItemRef.ObjectID));

    if(BlackMarketState.InterestTemplates.Find(ItemB.GetMyTemplateName()) != INDEX_NONE &&
       BlackMarketState.InterestTemplates.Find(ItemA.GetMyTemplateName()) == INDEX_NONE)
    {
        return -1;
    }

    return 0;
}

SortByInterest() is used inside PopulateData() as the sorting function to sort all sellable items. Due to the array being sorted only containing object references it has to fetch the item states from history.

simulated function bool IsInterested(X2ItemTemplate ItemTemplate)
{
    local array<XComGameState_Item> Interests;
    local int i;

    Interests = class'UIUtilities_Strategy'.static.GetBlackMarket().GetInterests();

    for( i = 0; i < Interests.Length; i++ )
    {
        if( Interests[i].GetMyTemplate() == ItemTemplate )
            return true;
    }
    return false;
}

IsInterested() gets called from PopulateItemCard() every time selection changes (which also triggers when you click the +/- buttons) and gets the list of interests again every time which fetches GameStates for all sellable items from history.

I had very good results from caching Interests as a class variable and using that for both of these functions. CachedInterests can be fetched in InitScreen() which will also help with clicking the sell button resulting in another call of PopulateData().

var array<XComGameState_Item> CachedInterests;

simulated function InitScreen(XComPlayerController InitController, UIMovie InitMovie, optional name InitName)
{
    super.InitScreen(InitController, InitMovie, InitName);

    History = `XCOMHISTORY;

    CachedInterests = XComGameState_BlackMarket(History.GetGameStateForObjectID(BlackMarketReference.ObjectID)).GetInterests();

    BuildScreen();
    MC.FunctionVoid("AnimateIn");
    Show();
}
function int SortByInterest(BlackMarketItemPrice BuyPriceA, BlackMarketItemPrice BuyPriceB)
{
    local int i;
    local bool FoundItemB;

    FoundItemB = false;

    for (i = 0; i < CachedInterests.Length; i++)
    {
        if (CachedInterests[i].ObjectID == BuyPriceA.ItemRef.ObjectID)
        {
            return 0;
        }

        if (CachedInterests[i].ObjectID == BuyPriceB.ItemRef.ObjectID)
        {
            FoundItemB = true;
        }
    }

    if (FoundItemB)
    {
        return -1;
    }

    return 0;
}
simulated function bool IsInterested(X2ItemTemplate ItemTemplate)
{
    local int i;

    for( i = 0; i < CachedInterests.Length; i++ )
    {
        if( CachedInterests[i].GetMyTemplate() == ItemTemplate )
            return true;
    }
    return false;
}
faanlez commented 1 month ago

Forgot to mention UIBlackMarket_SellItem also doing things that could be cached in its PopulateData(). This all could get moved to InitListItem() and making Item a class variable.

ItemPrice = BuyPrice;
ItemRef = BuyPrice.ItemRef;
Price = BuyPrice.Price + BuyPrice.Price * (class'UIUtilities_Strategy'.static.GetBlackMarket().BuyPricePercentIncrease / 100.0f);
Item = GetItem();
ItemTemplate = Item.GetMyTemplate();

ItemCost = class'UIUtilities_Strategy'.static.GetCostQuantity(ItemTemplate.Cost, 'Supplies');
if (ItemCost > 0) // Ensure that the sell price of the item is not more than its cost from engineering
{
    Price = Min(Price, ItemCost);
}
faanlez commented 1 month ago

Something that would also help is XComGameState_BlackMarket caching the output of GetInterests() instead of the array being built every time the function is called. This should be done whenever InterestTemplates also gets built.

faanlez commented 1 month ago

Also in PopulateData() the UI has to get item states to validate item quantities because it has been given a list that can have zero quantity item states.

foreach Items(Item)
{
    // Don't display if none in your inventory to sell
    InventoryItem = XComGameState_Item(`XCOMHISTORY.GetGameStateForObjectID(Item.ItemRef.ObjectID));
    if( InventoryItem.Quantity > 0 )
    {
        Spawn(class'UIBlackMarket_SellItem', List.itemContainer).InitListItem(Item);
        if(Item == PrevItem)
            List.SetSelectedIndex(List.GetItemCount() - 1);
    }
}

What should be done instead is that XComGameState_HeadquartersXCom should skip the zero quantity items already.

function array<StateObjectReference> GetTradingPostItems()
{
    local XComGameStateHistory History;
    local XComGameState_Item ItemState;
    local array<StateObjectReference> TradingPostItems;
    local int idx;

    History = `XCOMHISTORY;
    TradingPostItems.Length = 0;

    for(idx = 0; idx < Inventory.Length; idx++)
    {
        ItemState = XComGameState_Item(History.GetGameStateForObjectID(Inventory[idx].ObjectID));

        if(ItemState != none)
        {
            if(ItemState.GetMyTemplate().TradingPostValue > 0 && !ItemState.GetMyTemplate().bInfiniteItem && !ItemState.IsNeededForGoldenPath())
            {
                TradingPostItems.AddItem(ItemState.GetReference());
            }
        }
    }

    //<workshop> BLACK_MARKET_FIXES, BET, 2016-04-22
    //INS:
    class'XComGameState_Item'.static.FilterOutGoldenPathItems(TradingPostItems);
    //</workshop>

    return TradingPostItems;
}

This way it would cut down on fetching the same GameStates from History twice which is slowing down the initial opening of the sell screen and the update after clicking the sell button a lot.

faanlez commented 1 month ago

I have put the changes so far into a mod: https://steamcommunity.com/sharedfiles/filedetails/?id=3349050094

faanlez commented 4 weeks ago

Related; If you sell your entire inventory off, you can't open the sell screen again until the next month. This is because the main UIBlackMarket screen checks BuyPrices from XComGameState_BlackMarket, but XComGameState_BlackMarket::UpdateBuyPrices() is only called on the sell screen and monthly reset. Same situation happens if the monthly reset happened while you had nothing to sell in your inventory.