• 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!

Shop NPC with Dynamic Pricing?

Tysoch86

New Member
Joined
Jan 8, 2016
Messages
81
Reaction score
1
Hello everyone!

I am designing a server for a small amount of players and since there won't exactly be a a lot of trading due to the small player count I was wondering about a NPC that could do the work.

If there could be a shop NPC that gathered the information on how many of a certain item there was on the server (Players, Depots, Houses), that number could be a modifier to how much an item is worth.
Much like a real economy, this NPC would be able to look at the current supply of and object and adjust his price accordingly.
An added benefit would be that players who were above and below the average level would indirectly receive a bonus when selling goods to the NPC (by obtaining items in higher demand). This would create more competition when competing for the top of the server, as well as, creating a catch up mechanic for players starting out on the server.


I don't know how possible something like this is, so, as a back up and NPC that randomized the modifier number would create some kind of a stock market feel when selling items.

Thank you for you time! Please let me know your thoughts.
 
I use to play runescape too :p

If someone were to write this, they would need to consider these tables in the database.
Code:
market_history
market_offers
player_depotitems
player_inboxitems
player_items

They wouldn't have to concern themselves with items on the ground because of server save, which normally cleans the map...

For all you dirty servers you are out of luck :p
 
Last edited:
Haha I didn't play much runescape, I guess Tibia beat it out for more enjoyable game.

Would this be overly complicated to design? Should I be looking for a randomized NPC instead?
Not at all, I've had this very idea in mind for some time, I just never got around to writing the code, I think it is an excellent idea.

For many reasons one of which is it prevents price gouging also it allows you to keep track of statistical data or item duplication, you can even go as far as set the rarity of the drop rate, a lot of good things can come out of a feature/system like this.
 
In TFS 1.2 I have a function

Code:
function ShopModule:addBuyableItem(names, itemid, cost, itemSubType, realName)
    if SHOPMODULE_MODE ~= SHOPMODULE_MODE_TALK then
          if itemSubType == nil then 
                itemSubType = 1 
           end 
    local shopItem = self:getShopItem(itemid, itemSubType)
    if shopItem == nil then 
          self.npcHandler.shopItems[#self.npcHandler.shopItems + 1] = {id = itemid, buy = cost, sell = -1, subType = itemSubType, name = realName or ItemType(itemid):getName()}
     else 
            shopItem.buy = cost
      end 
   end

  ... <Omitted Code>

inside of npc/../modules.lua

This function appears to update the cost of an item if it does already is selling it. This could probably be used to update item costs.

You might wanna only update the cost of every item at certain times during the day, not whenever someone asks the npc for a trade
 
You might wanna only update the cost of every item at certain times during the day, not whenever someone asks the npc for a trade
Why do you say that?
If requesting data from the database is a concern it shouldn't be as just about every function/method which exists makes calls to the database all the time.
 
Why do you say that?
If requesting data from the database is a concern it shouldn't be as just about every function/method which exists makes calls to the database all the time.

It seems like a lot of overhead to be counting all instances of sellable items every time a player says 'trade' to an npc.

I'm not pro OT developer but I thought most functions access objects stored in memory and the database was mostly only accessed to load/save the players and houses (when they log in/out of the server is saved).

I may be wrong how the server operates but I have spent some time working with it.
 
Well then, if overhead is an issue these items could always be stored in a global table, lots of ways to handle this.
 
Ya that would work, maybe a table of the form

item_counts = { <itemid> = { name = "Broadsword" , count = 194 , value = 12 }, ... }

and have a global event to count all the items, update the table, update all npcs sellables

This could allow all npcs to sell all items too.

Problem now is counting all the items in the server
 
Ya that would work, maybe a table of the form

item_counts = { <itemid> = { name = "Broadsword" , count = 194 , value = 12 }, ... }

and have a global event to count all the items, update the table, update all npcs sellables

This could allow all npcs to sell all items too.

Problem now is counting all the items in the server
You can use db.asyncStoreQuery to do that. It won't be easy tho. I will try to do it once I get some spare time but here is how it would work:

db.asyncStoreQuery("SELECT * FROM `players`;" callbackFunction)

And the callbackFunction should:
Check if the player is online by doing Player(name)
If he is online, get all depot items from all depot chests and his items in inventory and add to global table.

If he is offline, get all items from `player_items`and `player_depotitems` and add global table.

I would forget about loading house items. But you could try to load them from `tile_store`, you would need to get the functions to unserialize the items from it.
 
Last edited:
Ya that would work, maybe a table of the form

item_counts = { <itemid> = { name = "Broadsword" , count = 194 , value = 12 }, ... }

and have a global event to count all the items, update the table, update all npcs sellables

This could allow all npcs to sell all items too.

Problem now is counting all the items in the server
Normally I would just write the code and not even have a discussion on it, then the person would say whether it worked or not, i would either update the code or some one else would write a better or working version.. the problem would be resolved and maybe we would get likes for the scripts we produced for people.

I don't feel like writing scripts anymore for the public because eventually it leads to a lot of [insert obscenity here] waving...

I am trying something different, making suggestions.. although I do know 100% the suggestions I am making I could write the code for in only a few minutes, this is absolutely possible either using the database or a global table or both and it wouldn't be a problem... just ask @MatheusMkalo he seems to be writing code lately based on my suggestions, however not because I am suggesting it, but because he has something to prove... kids.. sigh

Take this thread as an example:
https://otland.net/threads/how-to-save-changes-made-in-game.241016/

I wrote this as a suggestion
https://otland.net/threads/how-to-save-changes-made-in-game.241016/#post-2330906

And he wrote the code to make it happen
https://otland.net/threads/how-to-save-changes-made-in-game.241016/#post-2331061

Edit: speak of the devil :p
 
Normally I would just write the code and not even have a discussion on it, then the person would say whether it worked or not, i would either update the code or some one else would write a better or working version.. the problem would be resolved and maybe we would get likes for the scripts we produced for people.

I don't feel like writing scripts anymore for the public because eventually it leads to a lot of [insert obscenity here] waving...

I am trying something different, making suggestions.. although I do know 100% the suggestions I am making I could write the code for in only a few minutes, this is absolutely possible either using the database or a global table or both and it wouldn't be a problem... just ask @MatheusMkalo he seems to be writing code lately based on my suggestions, however not because I am suggesting it, but because he has something to prove... kids.. sigh

Take this thread as an example:
https://otland.net/threads/how-to-save-changes-made-in-game.241016/

I wrote this as a suggestion
https://otland.net/threads/how-to-save-changes-made-in-game.241016/#post-2330906

And he wrote the code to make it happen
https://otland.net/threads/how-to-save-changes-made-in-game.241016/#post-2331061

Edit: speak of the devil :p
Did you invent the process of saving information to the database? Congratulations.
I should have saved those tile informations in some txt in plain text maybe, then it wouldn't be your suggestion :/

Why do you say that?
If requesting data from the database is a concern it shouldn't be as just about every function/method which exists makes calls to the database all the time.

Tell me which functions makes calls to the database when they are executed. (The database related ones doesn't count.)

Edit: Are you getting mad because I'm doing codes that you can just idealize but you cant make them? Sorry m8, you will learn from them some day.
 
You can use db.asyncStoreQuery to do that. It won't be easy tho. I will try to do it once I get some spare time but here is how it would work:

db.asyncStoreQuery("SELECT * FROM `players`;" callbackFunction)

And the callbackFunction should:
Check if the player is online by doing Player(name)
If he is online, get all depot items from all depot chests and his items in inventory and add to global table.

If he is offline, get all items from `player_items`and `player_depotitems` and add global table.

I would forget about loading house items. But you could try to load them from `tile_store`, you would need to get the functions to unserializate the items from it.

Ah, i thought all the items were stored in serialized format in the db.
So the queries for this are easy, for player_items, player_debotitems, and player_inboxitems we would have

Code:
SELECT itemtype, SUM(count)
FROM player_items
group by itemtype;

SELECT itemtype, SUM(count)
FROM player_inboxitems
group by itemtype;

SELECT itemtype, SUM(count)
FROM player_depotitems
group by itemtype;

But I think counting house items is necessary since people love to show off their fancy shit.
 
Ah, i thought all the items were stored in serialized format in the db.
So the queries for this are easy, for player_items, player_debotitems, and player_inboxitems we would have

Code:
SELECT itemtype, SUM(count)
FROM player_items
group by itemtype;

SELECT itemtype, SUM(count)
FROM player_inboxitems
group by itemtype;

SELECT itemtype, SUM(count)
FROM player_depotitems
group by itemtype;

But I think counting house items is necessary since people love to show off their fancy shit.
You can do it like this but it will give you an incorrect amount of items, because the items just get updated when the player logout or saved.

About the unserialization, you can do it with pure lua but it will be hard, you should try to copy this: https://github.com/otland/forgotten...1b927c19afdf65/src/iomapserialize.cpp#L28-L65 in C++ to get what you want.

You can use this if you don't wanna edit your source:
Code:
function toBits(num,bits)
  bits = bits or select(2,math.frexp(num))
  local t={}
  for b=bits,1,-1 do
  t[b]=math.fmod(num,2)
  num=(num-t[b])/2
  end
   local ret = {}
   for i = 1, 8-#t do
     table.insert(t, 1, 0)
   end
  return t
end

function dataToMessage(data)
   return {data}
end

function readUint16(messageT)
   msg = messageT[1]
   if msg then
     local ret = ""
     for i = 1, 2 do
       ret = table.concat(toBits(msg:sub(i, i):byte())) .. ret
     end
     if ret ~= "" then
       messageT[1] = msg:sub(3)
       return tonumber(ret, 2)
     end
   end
end

function readUint8(messageT)
   msg = messageT[1]
   if msg then
     local ret = msg:sub(1,1):byte()
     messageT[1] = msg:sub(2)
     return ret
   end
end

function readUint32(messageT)
   msg = messageT[1]
   if msg then
     local ret = ""
     for i = 1, 4 do
       ret = table.concat(toBits(msg:sub(i, i):byte())) .. ret
     end
     if ret ~= "" then
       messageT[1] = msg:sub(5)
       return tonumber(ret, 2)
     end
   end
end

local file = io.open("tile_store-data.bin", "rb")
local data = file:read("*a")
file:close()

local msg = dataToMessage(data)
local x = readUint16(msg)
local y = readUint16(msg)
local z = readUint8(msg)
local itemCount = readUint32(msg)
local itemType = readUint16(msg)
--local attributes = readUint8(msg)
--if ItemType(itemType):isContainer() then
    --loadContainer()
--

print(x, y, z, itemCount, itemType)

I tested it and it works, but I just can't do the attribute and container unserialization, it would take time to get all ATTR values, but you can get something with this atleast.
Instead of reading from the file you would use dataToMessage(result.getStream(resultId, "data"))

EDITED! There was some stupid errors, now its working :p
 
Last edited:
I didn't read what other wrote, but here is what I would do:

on the loot dropping and loot containers what refill or wherever I think I should keep record of item, I add a function: dynamicPricing_insert(itemID, count)
This function would add either to global table or database the amount of items added to game.

Every time item is sold to npc this function executes: dynamicPricing_sell(itemID, count)
This function would remove the values from table/db

This way has a flaw that not all items dropped are actually going to circulate, but then again if player didn't pick it up in first place, then it wasn't worth it anyway.

To counter the non-stop price increase, every now and then db could cut 5% of itemCount and delete them (if the count was over 1)
 
You can do it like this but it will give you an incorrect amount of items, because the items just get updated when the player logout or saved.

About the unserialization, you can do it with pure lua but it will be hard, you should try to copy this: https://github.com/otland/forgotten...1b927c19afdf65/src/iomapserialize.cpp#L28-L65 in C++ to get what you want.

You can use this if you don't wanna edit your source:
Code:
function toBits(num,bits)
  bits = bits or select(2,math.frexp(num))
  local t={}
  for b=bits,1,-1 do
  t[b]=math.fmod(num,2)
  num=(num-t[b])/2
  end
   local ret = {}
   for i = 1, 8-#t do
     table.insert(t, 1, 0)
   end
  return t
end

function dataToMessage(data)
   return {data}
end

function readUint16(messageT)
   msg = messageT[1]
   if msg then
     local ret = ""
     for i = 1, 2 do
       ret = table.concat(toBits(msg:sub(i, i):byte())) .. ret
     end
     if ret ~= "" then
       messageT[1] = msg:sub(3)
       print(ret)
       return tonumber(ret, 2)
     end
   end
end

function readUint8(messageT)
   msg = messageT[1]
   if msg then
     local ret = msg:sub(1,1):byte()
     messageT[1] = msg:sub(2)
     return ret
   end
end

function readUint32(messageT)
   msg = messageT[1]
   if msg then
     local ret = ""
     for i = 1, 4 do
       ret = table.concat(toBits(msg:sub(i, i):byte())) .. ret
     end
     if ret ~= "" then
       messageT[1] = msg:sub(5)
       return tonumber(ret, 2)
     end
   end
end

local file = io.open("tile_store-data.bin", "rb")
local data = file:read("*a")
file:close()

local msg = dataToMessage(data)
local x = readUint16(msg)
local y = readUint16(msg)
local z = readUint8(msg)
local itemCount = readUint32(msg)
local itemType = readUint16(msg)
--local attributes = readUint8(msg)
--if ItemType(itemType):isContainer() then
    --loadContainer()
--

print(x, y, z, itemCount, itemType)

I tested it and it works, but I just can't do the attribute and container unserialization, it would take time to get all ATTR values, but you can get something with this atleast.
Instead of reading from the file you would use dataToMessage(result.getStream(resultId, "data"))

EDITED! There was some stupid errors, now its working :p

Ya I was aware it wouldn't take into account online players unsaved items, thought it could be a good enough approximation to start out with at least.

So, this will unserialize all items not inside of containers? I'm kinda confused about the file reading 'io.open("tile_store-data.bin", "rb")' part. Why are you not querying the database?
 
Ya I was aware it wouldn't take into account online players unsaved items, thought it could be a good enough approximation to start out with at least.

So, this will unserialize all items not inside of containers? I'm kinda confused about the file reading 'io.open("tile_store-data.bin", "rb")' part. Why are you not querying the database?
"Instead of reading from the file you would use dataToMessage(result.getStream(resultId, "data"))"

I downloaded the data from the database to test it without using queries because I was not running with TFS and i don't have any lib for mysql to test it.
 
Ya I was aware it wouldn't take into account online players unsaved items, thought it could be a good enough approximation to start out with at least.

So, this will unserialize all items not inside of containers? I'm kinda confused about the file reading 'io.open("tile_store-data.bin", "rb")' part. Why are you not querying the database?
Code:
ATTR_COUNT = 15
ATTR_CONTAINER_ITEMS = 23

function getItemData(houseid)
   local ret = {}
   local resultId = db.storeQuery("SELECT * FROM `tile_store` WHERE `house_id` = " .. houseid)
   if resultId then
     repeat
       local data = result.getStream(resultId, "data")
       if data then
         table.insert(ret, data)
       end
     until not result.next(resultId)
   end
   return ret
end

function House:getItemData()
   local id = self:getId()
   return getItemData(id)
end

function NetworkMessage:getU8()
   local x = self:getByte()
   return x
end

function loadItem(msg)
   local id = msg:getU16()
   if not id then
     return false
   end

   local items = {}
   local itemType = ItemType(id)
   if itemType:isMovable() then
     --Here it would create the item but we just need the info so we wont do that.
     --We are also skipping any byte that is not ATTR_COUNT or ATTR_CONTAINER_ITEMS.
     local count = 1
     local attrtype = msg:getU8()
     while attrtype and attrtype ~= 0 do
       if attrtype == ATTR_COUNT and itemType:isStackable() then
         count = msg:getU8()
       elseif attrtype == ATTR_CONTAINER_ITEMS and itemType:isContainer() then
         local serializationCount = msg:getU32()
         for i = 1, serializationCount do
           local subitems = loadItem(msg)
           if subitems then
             for s, z in pairs(subitems) do
               items[s] = items[s] and items[s]+z or z
             end
           end
         end
       else
         msg:skipBytes(1)
       end
       attrtype = msg:getU8()
     end
     items[id] = items[id] and items[id]+count or count
   end
   return items
end

function readHouseItemData(data)
   local ret = {}
   local msg = NetworkMessage()
   for i = 1, #data do
     msg:addByte(data:sub(i, i):byte())
   end

   msg:skipBytes(-#data)
   local x = msg:getU16()
   local y = msg:getU16()
   local z = msg:getU8()
   local tile = Tile(x, y, z)
   if not tile then
     return false
   end
   local itemcount = msg:getU32()
   while itemcount > 0 do
     local items = loadItem(msg, tile)
     if items then
       for s, z in pairs(items) do
         ret[s] = ret[s] and ret[s]+z or z
       end
     end
     itemcount = itemcount-1
   end
   return ret
end

function onSay(player)
   saveServer()
   local items = getItemData(11) -- 11 houseid
   local totalcount = {}
   for i = 1, #items do
     local itemcounts = readHouseItemData(items[i])
     for s,z in pairs(itemcounts) do
       totalcount[s] = totalcount[s] and totalcount[s]+z or z
     end
   end

   for s, z in pairs(totalcount) do
     print(s, z)
   end
end

Here it is a better version of the functions (and its ready to use in otservers just copy it). This should put every single itemcount (even the ones inside containers) in the table totalcount.

If you have 10000 crystal coins inside the houseid 11 then, totalcount[2160] = 10000.

It can bug in the right conditions tho as i'm skiping 1 byte at time, to do it better you would have to make a table with all ATTR values and how many bytes each of those has to skip. (I tried to make it bug but couldn't do it so yep.)
 
Back
Top