Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All loot corpses otimization #3472

Open
wants to merge 44 commits into
base: main
Choose a base branch
from

Conversation

LeoTKBR
Copy link
Contributor

@LeoTKBR LeoTKBR commented Mar 28, 2025

This is an optimization for when autoloot is enabled to grab all loot when creatures die.

In the video, the loot pouch was open, the cipsoft client crashed causing "lag", so after closing everything went perfectly.

2025-03-28-16-39-38_NSzleKlN.mp4

@Meth28
Copy link

Meth28 commented Mar 31, 2025

thanks!

edit: causes lag in certain areas of the map

@beats-dh
Copy link
Collaborator

beats-dh commented Apr 1, 2025

void Game::playerQuickLootCorpse(const std::shared_ptr<Player> &player, const std::shared_ptr<Container> &corpse, const Position &position) {
    if (!player || !corpse) {
        return;
    }

    bool ignoreListItems = (player->quickLootFilter == QUICKLOOTFILTER_SKIPPEDLOOT);
    bool missedAnyGold = false;
    bool missedAnyItem = false;
    bool shouldNotifyCapacity = false;
    ObjectCategory_t shouldNotifyNotEnoughRoom = OBJECTCATEGORY_NONE;
    uint32_t totalLootedGold = 0;
    uint32_t totalLootedItems = 0;
    
    for (ContainerIterator it = corpse->iterator(); it.hasNext(); it.advance()) {
        const auto &item = *it;
        bool listed = player->isQuickLootListedItem(item);

        if ((listed && ignoreListItems) || (!listed && !ignoreListItems)) {
            if (item->getWorth() != 0) {
                missedAnyGold = true;
            } else {
                missedAnyItem = true;
            }
            continue;
        }

        uint32_t worth = item->getWorth();
        uint16_t baseCount = item->getItemCount();
        ObjectCategory_t category = getObjectCategory(item);

        ReturnValue ret = internalCollectManagedItems(player, item, category);
        if (ret == RETURNVALUE_NOTENOUGHCAPACITY) {
            shouldNotifyCapacity = true;
        } else if (ret == RETURNVALUE_CONTAINERNOTENOUGHROOM) {
            shouldNotifyNotEnoughRoom = category;
        }

        bool success = ret == RETURNVALUE_NOERROR;
        if (worth != 0) {
            missedAnyGold = missedAnyGold || !success;
            if (success) {
                player->sendLootStats(item, baseCount);
                totalLootedGold += worth;
            } else {
                totalLootedGold += worth - item->getWorth();
            }
        } else {
            missedAnyItem = missedAnyItem || !success;
            if (success || item->getItemCount() != baseCount) {
                totalLootedItems++;
                player->sendLootStats(item, item->getItemCount());
            }
        }
    }

    if (!(totalLootedGold != 0 || missedAnyGold || totalLootedItems != 0 || missedAnyItem)) {
        player->sendTextMessage(MESSAGE_STATUS, "No loot.");
        return;
    }

    std::string message;
    message.reserve(128);
    
    bool lootedAllGold = totalLootedGold != 0 && !missedAnyGold;
    bool lootedAllItems = totalLootedItems != 0 && !missedAnyItem;
    
    if (lootedAllGold) {
        message = "You looted the complete " + std::to_string(totalLootedGold) + " gold";

        if (totalLootedItems != 0 || missedAnyItem) {
            if (lootedAllItems) {
                message += " and all dropped items";
            } else if (totalLootedItems != 0) {
                message += ", but you only looted some of the items";
            } else if (missedAnyItem) {
                message += " but none of the dropped items";
            }
        }
    } else if (lootedAllItems) {
        if (totalLootedItems == 1) {
            message = "You looted 1 item";
        } else if (totalLootedGold != 0 || missedAnyGold) {
            message = "You looted all of the dropped items";
        } else {
            message = "You looted all items";
        }

        if (totalLootedGold != 0) {
            message += ", but you only looted " + std::to_string(totalLootedGold) + " of the dropped gold";
        } else if (missedAnyGold) {
            message += " but none of the dropped gold";
        }
    } else if (totalLootedGold != 0) {
        message = "You only looted " + std::to_string(totalLootedGold) + " of the dropped gold";
        if (totalLootedItems != 0) {
            message += " and some of the dropped items";
        } else if (missedAnyItem) {
            message += " but none of the dropped items";
        }
    } else if (totalLootedItems != 0) {
        message = "You looted some of the dropped items";
        if (missedAnyGold) {
            message += " but none of the dropped gold";
        }
    } else if (missedAnyGold) {
        message = "You looted none of the dropped gold";
        if (missedAnyItem) {
            message += " and none of the items";
        }
    } else if (missedAnyItem) {
        message = "You looted none of the dropped items";
    }
    
    message += ".";
    player->sendTextMessage(MESSAGE_STATUS, message);

    if (shouldNotifyCapacity) {
        message = "Attention! The loot you are trying to pick up is too heavy for you to carry.";
    } else if (shouldNotifyNotEnoughRoom != OBJECTCATEGORY_NONE) {
        message = "Attention! The container assigned to category " + 
                  std::string(getObjectCategoryName(shouldNotifyNotEnoughRoom)) + " is full.";
    } else {
        return;
    }

    if (player->lastQuickLootNotification + 15000 < OTSYS_TIME()) {
        player->sendTextMessage(MESSAGE_GAME_HIGHLIGHT, message);
    } else {
        player->sendTextMessage(MESSAGE_EVENT_ADVANCE, message);
    }

    player->lastQuickLootNotification = OTSYS_TIME();
}

Copy link

sonarqubecloud bot commented Apr 1, 2025

@beats-dh
Copy link
Collaborator

beats-dh commented Apr 1, 2025

player.cpp and player.hpp:

// Adicione estas variáveis na classe Player em player.h:
private:
    bool inventoryUpdateScheduled = false;
    bool statsUpdateScheduled = false;
    uint64_t lastInventoryUpdate = 0;
    uint64_t lastStatsUpdate = 0;
    const uint64_t INVENTORY_UPDATE_THROTTLE = 2000; // ms - atualização a cada 2 segundos
    const uint64_t STATS_UPDATE_THROTTLE = 2000; // ms - atualização a cada 2 segundos

// Métodos atualizados com throttling:
public:
    void scheduleUpdateInventory() {
        inventoryUpdateScheduled = true;
    }
    
    void scheduleUpdateStats() {
        statsUpdateScheduled = true;
    }
    
    void flushScheduledUpdates() {
        uint64_t now = OTSYS_TIME();
        
        if (inventoryUpdateScheduled && (now - lastInventoryUpdate >= INVENTORY_UPDATE_THROTTLE)) {
            sendInventoryIds();
            inventoryUpdateScheduled = false;
            lastInventoryUpdate = now;
        }
        
        if (statsUpdateScheduled && (now - lastStatsUpdate >= STATS_UPDATE_THROTTLE)) {
            sendStats();
            statsUpdateScheduled = false;
            lastStatsUpdate = now;
        }
    }

// Modifique as funções postAddNotification e postRemoveNotification:
void Player::postAddNotification(const std::shared_ptr<Thing> &thing, const std::shared_ptr<Cylinder> &oldParent, int32_t index, CylinderLink_t link) {
    // Armazenar resultados de métodos em referências locais para evitar chamadas repetidas
    const auto &item = thing->getItem();
    
    if (link == LINK_OWNER) {
        // calling movement scripts
        g_moveEvents().onPlayerEquip(getPlayer(), item, static_cast<Slots_t>(index), false);
    }

    bool requireListUpdate = true;
    if (link == LINK_OWNER || link == LINK_TOPPARENT) {
        const auto &oldItem = oldParent ? oldParent->getItem() : nullptr;
        const auto &oldContainer = oldItem ? oldItem->getContainer() : nullptr;
        if (oldContainer) {
            requireListUpdate = oldContainer->getHoldingPlayer() != getPlayer();
        } else {
            requireListUpdate = oldParent != getPlayer();
        }

        updateInventoryWeight();
        updateItemsLight();
        
        // Agendar atualizações em vez de chamar diretamente
        scheduleUpdateInventory();
        scheduleUpdateStats();
    }

    if (item) {
        const auto &container = item->getContainer();
        if (container) {
            onSendContainer(container);
        }

        if (shopOwner && !scheduledSaleUpdate && requireListUpdate) {
            updateSaleShopList(item);
        }
    } else {
        const auto &creature = thing->getCreature();
        if (creature == getPlayer()) {
            // check containers
            std::vector<std::shared_ptr<Container>> containers;
            containers.reserve(openContainers.size());

            for (const auto &[containerId, containerInfo] : openContainers) {
                const auto &container = containerInfo.container;
                if (container == nullptr) {
                    continue;
                }

                if (!Position::areInRange<1, 1, 0>(container->getPosition(), getPosition())) {
                    containers.emplace_back(container);
                }
            }

            for (const auto &container : containers) {
                autoCloseContainers(container);
            }
        }
    }
}

void Player::postRemoveNotification(const std::shared_ptr<Thing> &thing, const std::shared_ptr<Cylinder> &newParent, int32_t index, CylinderLink_t link) {
    if (!thing) {
        return;
    }

    // Copiar ponteiros como no código original - isso era necessário?
    const auto copyThing = thing;
    const auto copyNewParent = newParent;

    if (link == LINK_OWNER) {
        const auto &item = copyThing->getItem();
        if (item) {
            g_moveEvents().onPlayerDeEquip(getPlayer(), item, static_cast<Slots_t>(index));
        }
    }
    bool requireListUpdate = true;

    if (link == LINK_OWNER || link == LINK_TOPPARENT) {
        const auto &item = copyNewParent ? copyNewParent->getItem() : nullptr;
        const auto &container = item ? item->getContainer() : nullptr;
        if (container) {
            requireListUpdate = container->getHoldingPlayer() != getPlayer();
        } else {
            requireListUpdate = copyNewParent != getPlayer();
        }

        updateInventoryWeight();
        updateItemsLight();
        
        // Agendar atualizações em vez de chamar diretamente
        scheduleUpdateInventory();
        scheduleUpdateStats();
    }

    const auto &item = copyThing->getItem();
    if (item) {
        const auto &container = item->getContainer();
        if (container) {
            checkLootContainers(container);

            if (container->isRemoved() || !Position::areInRange<1, 1, 0>(getPosition(), container->getPosition())) {
                autoCloseContainers(container);
            } else if (container->getTopParent() == getPlayer()) {
                onSendContainer(container);
            } else {
                const auto &topContainer = std::dynamic_pointer_cast<Container>(container->getTopParent());
                if (topContainer) {
                    const auto &depotChest = std::dynamic_pointer_cast<DepotChest>(topContainer);
                    if (depotChest) {
                        bool isOwner = false;

                        for (const auto &[depotId, depotChestMap] : depotChests) {
                            if (depotId == 0) {
                                continue;
                            }

                            if (depotChestMap == depotChest) {
                                isOwner = true;
                                onSendContainer(container);
                                break;
                            }
                        }

                        if (!isOwner) {
                            autoCloseContainers(container);
                        }
                    } else {
                        onSendContainer(container);
                    }
                } else {
                    autoCloseContainers(container);
                }
            }
        }

        if (item->getTier() > 0 && !requireListUpdate) {
            requireListUpdate = true;
        }

        if (shopOwner && !scheduledSaleUpdate && requireListUpdate) {
            updateSaleShopList(item);
        }
    }
}

void Player::onThink(uint32_t interval) {
	Creature::onThink(interval);

	sendPing();
        flushScheduledUpdates();

	MessageBufferTicks += interval;
	if (MessageBufferTicks >= 1500) {
		MessageBufferTicks = 0;
		addMessageBuffer();
	}

	// Transcendance (avatar trigger)
	triggerTranscendance();
	// Momentum (cooldown resets)
	triggerMomentum();
	const auto &playerTile = getTile();
	const bool vipStaysOnline = isVip() && g_configManager().getBoolean(VIP_STAY_ONLINE);
	idleTime += interval;
	if (playerTile && !playerTile->hasFlag(TILESTATE_NOLOGOUT) && !isAccessPlayer() && !isExerciseTraining() && !vipStaysOnline) {
		const int32_t kickAfterMinutes = g_configManager().getNumber(KICK_AFTER_MINUTES);
		if (idleTime > (kickAfterMinutes * 60000) + 60000) {
			removePlayer(true);
		} else if (client && idleTime == 60000 * kickAfterMinutes) {
			std::ostringstream ss;
			ss << "There was no variation in your behaviour for " << kickAfterMinutes << " minutes. You will be disconnected in one minute if there is no change in your actions until then.";
			client->sendTextMessage(TextMessage(MESSAGE_ADMINISTRATOR, ss.str()));
		}
	}

	if (g_game().getWorldType() != WORLD_TYPE_PVP_ENFORCED) {
		checkSkullTicks(interval / 1000);
	}

	addOfflineTrainingTime(interval);
	if (lastStatsTrainingTime != getOfflineTrainingTime() / 60 / 1000) {
		sendStats();
	}

	// Wheel of destiny major spells
	wheel().onThink();

	g_callbacks().executeCallback(EventCallback_t::playerOnThink, &EventCallback::playerOnThink, getPlayer(), interval);
}

@lBaah
Copy link

lBaah commented Apr 1, 2025

@beats-dh some updates gotta be 50ms, other can be delayed more, so is better to use a dispatcher schedule.

I suggest to use SayiansKing scheduled updates. I use it for 4 years already. (use smart pointers instead of that loop to get the player)
https://github.com/opentibiabr/optimized_forgottenserver/blob/master/src/game.cpp#L6495

Other suggestions to @LeoTKBR is to insert items in the back of the loot pouch list. This way the client doesn't need to relocate tons of looted items in the container.

You can also cache the containers to be updated and completely ignore some container item updates from loot pouch. 1 container update is just enough to update everything.

Please take a look on these functions `sendAddContainerItem, sendUpdateContainerItem, sendRemoveContainerItem, which are located in player.cpp
https://github.com/opentibiabr/canary/blob/main/src/creatures/players/player.cpp#L8000

Basically copy/paste the browsefield code, then schedule just one update to the container (onSendContainer) that you are adding, updating or removing the item. The container will be updated just once instead of O(n) times. (n = the number of items added)

You can also schedule checkLootContainers to avoid multiple calls to isHoldingItem that uses container iterator and is very expensive in performance. Just in case.

This is the result: (video too big to post directly here)
https://drive.google.com/file/d/1NB7f_vK6CTlrLkAAfbHtNnqvNV0XG-W7/view?usp=sharing

The frame drop is mostly from monsters dying/being removed. The server lag is due to containter iterator checking O(n) times how much items the container holds, and how much container the loot container holds (n = number of items looted). So if you have 2000 items, you can see where this goes.

Yeah, I went a little nuts with all this.

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.

5 participants