• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Action [TFS 1.x] Item 'loot seller'

Like click 1 BoH and it sells all BoHs in same BP? or all in all player's BPs? or sell all sellable items from BP?
For 1 BP - often 20 slots - it's fine, but running it on 'all BPs in BPs' (all player's BPs) would lag OTS, if some player wear ex. 2000 items.

@Gesior.pl

You don't need to go through all of the player's backpacks, it can be a defined type of backpack, for example on Brazilian servers they use the Gold Pouch for Auto Loot and then this action item sells everything that is inside the Gold Pouch only. But it sells all configurable items at once and the money is sent to the bank. Being 5000 or 10 I never noticed lag when using.

They use, for example, 15 minutes of exhaust so this should alleviate the lag if exists.
 
Last edited:
@Gesior.pl

You don't need to go through all of the player's backpacks, it can be a defined type of backpack, for example on Brazilian servers they use the Gold Pouch for Auto Loot and then this action item sells everything that is inside the Gold Pouch only. But it sells all configurable items at once and the money is sent to the bank. Being 5000 or 10 I never noticed lag when using.

They use, for example, 15 minutes of exhaust so this should alleviate the lag if exists.
Version that allows usage on containers and sell all sellable items from given container:
Lua:
-- rest of config (item prices) is under function, paste there your items list from npc
local config = {
	containerAllowedIds = {2000, 2001, 2002}, -- if item is used on given container ID, script sells all items inside
	containerMaxWeight = 321000, -- (321k) ignore all items in containers with higher weight
	containerMaxItemsToProcess = 10000, -- process only first X items in container
	containerMaxItemsToSell = 500, -- sell only first X sellable items from container
	price_percent = 90, -- how many % of shop price player receive when sell by 'item seller'
	cash_to_bank = true -- send money to bank, not add to player BP
}

local allowedContainers = {}
for _, itemId in pairs(config.containerAllowedIds) do
	allowedContainers[itemId] = true
end
local shopItems = {}

local function sellAllItemsFromContainer(player, container)
	if not container.getItems then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'Invalid configuration of Loot Seller items. Contact with GM.')
		return
	end

	if container:getWeight() > config.containerMaxWeight * 100 then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'Container weight is above ' .. config.containerMaxWeight .. '!')
		return
	end

	local sellableItemsInContainer = {}
	local totalSellableItems = 0
	for i, item in pairs(container:getItems(true)) do
		if shopItems[item.itemid] and not (item:getUniqueId() < 65535 or item:getActionId() > 0) then
			if not sellableItemsInContainer[item.itemid] then
				sellableItemsInContainer[item.itemid] = {}
			end
			table.insert(sellableItemsInContainer[item.itemid], item)
			totalSellableItems = totalSellableItems + 1

			if totalSellableItems == config.containerMaxItemsToSell then
				break
			end
		end

		if i == config.containerMaxItemsToProcess then
			break
		end
	end

	local totalItemCount = 0
	local totalItemValue = 0
	for itemId, items in pairs(sellableItemsInContainer) do
		local itemCount = 0
		local itemType = ItemType(itemId)
		for _, item in pairs(items) do
			if itemType:isStackable() then
				itemCount = itemCount + item.type
			else
				itemCount = itemCount + 1
			end
			item:remove()
		end
		totalItemCount = totalItemCount + itemCount

		local itemValue = math.ceil(shopItems[items[1].itemid] * itemCount / 100 * config.price_percent)
		totalItemValue = totalItemValue + itemValue

		local message = 'You sold ' .. itemCount .. ' ' .. itemType:getName() .. ' for ' .. itemValue .. ' gold coins.'
		if config.cash_to_bank then
			player:setBankBalance(player:getBankBalance() + itemValue)
			message = message .. ' Money was added to your bank account.'
		else
			player:addMoney(itemValue)
		end
		player:sendTextMessage(MESSAGE_INFO_DESCR, message)
	end

	local itemName = container:getName()

	if totalItemCount > 0 then
		player:sendTextMessage(MESSAGE_INFO_DESCR, totalItemCount .. ' items from ' .. itemName .. ' sold for ' .. totalItemValue .. ' gold coins.')
	else
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'Items inside ' .. itemName .. ' are worthless.')
	end

end

function onUse(player, item, fromPosition, target, toPosition, isHotkey)
	if toPosition.x ~= 65535 and getDistanceBetween(player:getPosition(), toPosition) > 1 then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'This item is too far away.')
		return true
	end

	local targetTile = Tile(toPosition)
	if targetTile then
		-- this is to prevent selling item that lays in house doors
		local targetHouse = targetTile:getHouse()
		if targetHouse then
			-- this blocks only selling items laying on house floor
			-- if player open BP that lays on house floor, he can sell items inside it
			player:sendTextMessage(MESSAGE_INFO_DESCR, 'You cannot sell items in house.')
			return true
		end
	end

	local itemEx = Item(target.uid)
	if not itemEx then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'This is not an item.')
		return true
	end

	if allowedContainers[itemEx.itemid] then
		sellAllItemsFromContainer(player, itemEx)
		return true
	end

	if itemEx:getUniqueId() < 65535 or itemEx:getActionId() > 0 then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'You cannot sell quest item.')
		return true
	end

	if not shopItems[itemEx.itemid] then
		player:sendTextMessage(MESSAGE_INFO_DESCR, 'This is not sellable item.')
		return true
	end

	local itemCount = 1
	local itemType = ItemType(itemEx.itemid)
	if itemType:isStackable() then
		itemCount = itemEx.type
	end

	local itemName = itemEx:getName()
	local itemValue = math.ceil(shopItems[itemEx.itemid] * itemCount / 100 * config.price_percent)
	if itemValue > 0 then
		itemEx:getPosition():sendMagicEffect(CONST_ME_GIFT_WRAPS)
		itemEx:remove()

		local message = 'You sold ' .. itemCount .. ' ' .. itemName .. ' for ' .. itemValue .. ' gold coins.'
		if config.cash_to_bank then
			player:setBankBalance(player:getBankBalance() + itemValue)
			message = message .. ' Money was added to your bank account.'
		else
			player:addMoney(itemValue)
		end
		player:sendTextMessage(MESSAGE_INFO_DESCR, message)
	else
		player:sendTextMessage(MESSAGE_INFO_DESCR, itemName .. ' is worthless.')
	end

	return true
end

local shopModule = {}
function shopModule:addBuyableItemContainer()
end
function shopModule:addBuyableItem()
end
function shopModule:addSellableItem(names, itemid, cost, realName)
	shopItems[itemid] = cost
end

function shopModule:parseList(data)
	for item in string.gmatch(data, "[^;]+") do
		local i = 1
		local itemid = -1
		local cost = 0
		for temp in string.gmatch(item, "[^,]+") do
			if i == 2 then
				itemid = tonumber(temp)
			elseif i == 3 then
				cost = tonumber(temp)
			end
			i = i + 1
		end

		shopItems[itemid] = cost
	end
end

-- here paste list of items from NPC lua file
shopModule:addSellableItem({ 'poison arrow' }, 2545, 5, 'poison arrow')
shopModule:addSellableItem({ 'hota' }, 2342, 500, 'helmet of the ancients')
shopModule:addSellableItem({ 'magic sword' }, 2400, 1300)

-- here paste list from .xml file
shopModule:parseList('crossbow,2455,150;bow,2456,130')
shopModule:parseList('knight armor, 2476, 10000')
Example message after usage:
Code:
18:58 You sold 250 sword for 57375 gold coins. Money was added to your bank account.
18:58 You sold 250 magic sword for 292500 gold coins. Money was added to your bank account.
18:58 500 items from red backpack sold for 349875 gold coins.

There are 4 new configs:
Lua:
    containerAllowedIds = {2000, 2001, 2002}, -- if item is used on given container ID, script sells all items inside
    containerMaxWeight = 321000, -- (321k) ignore all items in containers with higher weight
    containerMaxItemsToProcess = 10000, -- process only first X items in container
    containerMaxItemsToSell = 500, -- sell only first X sellable items from container
Container with 4k Magic Sword, 4k Sword and 420 Red BPs weights 315k, so 321k weight limit is pretty high.

Why is there weight limit?
OTS can tell weight of any container without checking items inside (it's used in C++ to check 'free cap' of player).
To get count of all items inside all backpacks, it has to iterate over all items in all BPs inside.
Checking weight of BP with 168k items takes less than 1 ms (around 0, it reads 'int' from RAM), but checking items count takes 21 ms.

I've benchmarked execution time of script with different BPs and configs:

1. Max items to process: 10.000
Container contains 8420 items, with 4000 sellable items inside (every 1 of 2 items is sellable + 420 BPs)
Time with different 'containerMaxItemsToSell' configs:
Code:
- sell 500: 7-15 ms, avg 12 ms
- sell 1000: 22-26 ms, avg 24 ms
- sell 2000: 29-49 ms, avg 38 ms

2. Max items to process: 2.000
Container contains 8420 items, with 4000 sellable items inside (every 1 of 2 items is sellable + 420 BPs):
Time with different 'containerMaxItemsToSell' configs:
Code:
- sell 500: 7-15 ms, avg 12 ms
- sell 1000 (max. sell 790, cannot find more items with 2k process limit): 22-26 ms, avg 24 ms
- sell 2000: max. sell 790, so score is the same as for 1000

3. (How would it work without limits) Max items to process: 100.000, weight limit 10kk
Container contains 168.420 items, with 80.000 sellable items inside:
Time with different 'containerMaxItemsToSell' configs:
Code:
- sell 500: 35 ms
- sell 2000: 41-66 ms
- sell 10.000: 480 ms
Benchmarked on TFS 1.4 with Feature - [TFS 1.4] OTS statistics (C++/Lua/SQL) by kondra (https://otland.net/threads/tfs-1-4-ots-statistics-c-lua-sql-by-kondra.283717/)
and big containers with items generated using [TFS 1.x+] Generate big depot – HALP! (https://halp.skalski.pro/2019/10/04/tfs-1-x-generate-big-depot/)
All tested on Intel i9-13900K. On your dedic, it will be way slower.
 
Thinking about a scenario at rates 100x where there will be players at maximum level 2000, a knight's capacity will be around 50k...


I tried to use it but it didn't work:
"15:49 Items inside Loot Pouch are worthless."

1703875821811.png

-- here paste list from .xml file
shopModule:parseList('crossbow,2455,150;bow,2456,130')
shopModule:parseList('knight armor, 2476, 10000')

am I doing something wrong? @Gesior.pl

I'm using canary src
 
shopModule:parseList('knight armor, 2476, 10000')
Name in config is ignored. Script only reads second parameter as ITEM_ID and third as PRICE.
ID 2476 is knight armor in TFS - server that uses items.otb. Canary does not use items.otb. It uses IDs from Tibia Client 13+, which are different.
Ex. according to juggernaut.lua loot list from canary:
Lua:
	{ id = 3370, chance = 4990 }, -- knight armor
ID of Knight Armor is 3370.

This script config is compatible with TFS (Jiddo NPC system), which loads config of sellable items from 2 formats. Lua script:
Lua:
shopModule:addSellableItem({ 'poison arrow' }, 2545, 5, 'poison arrow')
and from NPC .xml files:
XML:
<parameter key="shop_sellable" value="crossbow,2455,150;bow,2456,130" />

on canary it's in Lua in format:
Lua:
npcConfig.shop = {
	{ itemName = "amber", clientId = 32626, sell = 20000 },
	{ itemName = "amber with a bug", clientId = 32624, sell = 41000 },
	{ itemName = "amber with a dragonfly", clientId = 32625, sell = 56000 },
	{ itemName = "ancient coin", clientId = 24390, sell = 350 },
	{ itemName = "black pearl", clientId = 3027, buy = 560, sell = 280 },
}

If you want to load config copied from canary NPC. Add at end of script:
Lua:
function parseCanaryConfig(npcConfig)
	for _, data in pairs(npcConfig) do
		if data.clientId and data.sell and data.sell > 0 then
			shopItems[data.clientId] = data.sell
		end
	end
end
Then you can use it:
Lua:
parseCanaryConfig(
{
	{ itemName = "amber", clientId = 32626, sell = 20000 },
	{ itemName = "amber with a bug", clientId = 32624, sell = 41000 },
	{ itemName = "amber with a dragonfly", clientId = 32625, sell = 56000 },
	{ itemName = "ancient coin", clientId = 24390, sell = 350 },
	{ itemName = "black pearl", clientId = 3027, buy = 560, sell = 280 },
}
)
 
Worked!

@Gesior.pl
One question, how could adapt it so don't need to use "Use with..." on the backpack?
To be able to automatically sell items from the configured backpack when using the item?
 
Worked!

@Gesior.pl
One question, how could adapt it so don't need to use "Use with..." on the backpack?
To be able to automatically sell items from the configured backpack when using the item?
This script ignores parameters item and fromPosition as it only cares about 'on what item was used'.
If you set this script to execute for BP ID, not use extra item 'Item Seller' ID with 'Use with..'.

You can replace:
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
with:
Lua:
function onUse(player, target, toPosition)
and it should work. Item with only Use parameters passed to first 2 variables after player should be the same as Use with.. item 3rd and 4th parameters.[/CODE]
 
Last edited:
This script ignores parameters item and fromPosition as it only cares about 'on what item was used'.
If you set this script to execute for BP ID, not use extra item 'Item Seller' ID with 'Use with..'.

You can replace:
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
with:
Lua:
function onUse(player, target, toPosition)
and it should work. Item with only Use parameters passed to first 2 variables after player should be the same as Use with.. item 3rd and 4th parameters.[/CODE]

I tried with
function onUse(player, target, toPosition)

But it didn't work, it seems he's trying to sell it himself
21:25 This is not sellable item.
 
I tried with
function onUse(player, target, toPosition)

But it didn't work, it seems he's trying to sell it himself
21:25 This is not sellable item.
Message me on Discord: gesior.pl
We will find out what is wrong with script/canary and then I will post how to fix it.
 
Back
Top