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

Feature [TFS 1.X] (C++/Lua/XML) Token Currency + Shop Module

Aeluu

Member
Joined
Apr 12, 2017
Messages
47
Reaction score
22
Let's say you wanted to add a new in-game token as a separate economy from gold/platinum/crystal.

First, pick a stackable item that you want to use as the token.

Changes to TFS Source:

items.xml:

XML:
    <item id="6527" article="a" name="Tharian token">
        <attribute key="weight" value="5" />
    </item>

Now add the token in the c++ source as a constant.
const.h:
C++:
    ITEM_GOLD_COIN = 2148,
    ITEM_PLATINUM_COIN = 2152,
    ITEM_CRYSTAL_COIN = 2160,
    ITEM_THARIAN_TOKEN = 6527 // Token to add "Tharian Token"

Then, add the functions to remove/add the new tokens.

1. Search for "removeMoney"
2. Add this after the function:

game.cpp:
C++:
bool Game::removeTokens(Cylinder* cylinder, uint64_t tokens, uint32_t flags /*= 0*/)
{
    if (cylinder == nullptr) {
        return false;
    }

    if (tokens == 0) {
        return true;
    }   
    std::vector<Container*> containers;

    std::multimap<uint32_t, Item*> tokenMap;
    uint64_t tokenCount = 0;

    for (size_t i = cylinder->getFirstIndex(), j = cylinder->getLastIndex(); i < j; ++i) {
        Thing* thing = cylinder->getThing(i);
        if (!thing) {
            continue;
        }

        Item* item = thing->getItem();
        if (!item) {
            continue;
        }

        Container* container = item->getContainer();
        if (container) {
            containers.push_back(container);
        } else {
            const uint32_t worth = item->getTokenWorth();
            if (worth != 0) {
                tokenCount += worth;
                tokenMap.emplace(worth, item);
            }
        }
    }

    size_t i = 0;
    while (i < containers.size()) {
        Container* container = containers[i++];
        for (Item* item : container->getItemList()) {
            Container* tmpContainer = item->getContainer();
            if (tmpContainer) {
                containers.push_back(tmpContainer);
            } else {
                const uint32_t worth = item->getTokenWorth();
                if (worth != 0) {
                    tokenCount += worth;
                    tokenMap.emplace(worth, item);
                }
            }
        }
    }

    if (tokenCount < tokens) {
        return false;
    }

    for (const auto& tokenEntry : tokenMap) {
        Item* item = tokenEntry.second;
        if (tokenEntry.first < tokens) {
            internalRemoveItem(item);
            tokens -= tokenEntry.first;
        } else if (tokenEntry.first > tokens) {
            const uint32_t worth = tokenEntry.first / item->getItemCount();
            const uint32_t removeCount = std::ceil(tokens / static_cast<double>(worth));

            addTokens(cylinder, (worth * removeCount) - tokens, flags);
            internalRemoveItem(item, removeCount);
            break;
        } else {
            internalRemoveItem(item);
            break;
        }
    }
    return true;
}

void Game::addTokens(Cylinder *cylinder, uint64_t tokens, uint32_t flags /*= 0*/)
{
    if (tokens == 0)
    {
        return;
    }

    if (tokens != 0)
    {
        Item *remaindItem = Item::CreateItem(ITEM_THARIAN_TOKEN, tokens);

        ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags);
        if (ret != RETURNVALUE_NOERROR)
        {
            internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT);
        }
    }
}

Add declarations in the header:

game.h:
C++:
bool removeTokens(Cylinder* cylinder, uint64_t tokens, uint32_t flags = 0);
void addTokens(Cylinder* cylinder, uint64_t tokens, uint32_t flags = 0);

Inside item, add function to get the worth of the token.

1. Search for "getWorth"
2. Add this after the function.

item.cpp:
C++:
uint32_t Item::getTokenWorth() const
{
    switch (id) {
        case ITEM_THARIAN_TOKEN:
            return count;

        default:
            return 0;
    }
}

Add declaration in header.

item.h:
C++:
uint32_t getTokenWorth() const;

Register token enum for lua script.

luascript.cpp:
C++:
registerEnum(ITEM_THARIAN_TOKEN)

Register methods.

luascript.cpp:
C++:
registerMethod("Player", "getTokens", LuaScriptInterface::luaPlayerGetTokens);
registerMethod("Player", "addTokens", LuaScriptInterface::luaPlayerAddTokens);
registerMethod("Player", "removeTokens", LuaScriptInterface::luaPlayerRemoveTokens);

and then add functions for above declarations

luascript.cpp:
C++:
int LuaScriptInterface::luaPlayerGetTokens(lua_State* L)
{
    // player:getTokens()
    Player* player = getUserdata<Player>(L, 1);
    if (player) {
        lua_pushnumber(L, player->getTokens());
    } else {
        lua_pushnil(L);
    }
    return 1;
}

int LuaScriptInterface::luaPlayerAddTokens(lua_State* L)
{
    // player:addTokens(tokens)
    uint64_t tokens = getNumber<uint64_t>(L, 2);
    Player* player = getUserdata<Player>(L, 1);
    if (player) {
        g_game.addTokens(player, tokens);
        pushBoolean(L, true);
    } else {
        lua_pushnil(L);
    }
    return 1;
}

int LuaScriptInterface::luaPlayerRemoveTokens(lua_State* L)
{
    // player:removeTokens(tokens)
    Player* player = getUserdata<Player>(L, 1);
    if (player) {
        uint64_t tokens = getNumber<uint64_t>(L, 2);
        pushBoolean(L, g_game.removeTokens(player, tokens));
    } else {
        lua_pushnil(L);
    }
    return 1;
}

Add in header.

luascript.h:
C++:
static int luaPlayerGetTokens(lua_State* L);
static int luaPlayerAddTokens(lua_State* L);
static int luaPlayerRemoveTokens(lua_State* L);

In player, we add a few tweaks to some functions:

player.cpp (postAddNotification):
C++:
...
        if (shopOwner && requireListUpdate) {
            updateSaleShopList(item);
            updateTokenShopList(item); // <--- Add this
        }
...

player.cpp (postRemoveNotification):
C++:
...
        if (shopOwner && requireListUpdate) {
            updateSaleShopList(item);
            updateTokenShopList(item); // <--- Add this
        }
...

Add this function:

player.cpp:
C++:
bool Player::updateTokenShopList(const Item* item)
{
    uint16_t itemId = item->getID();
    if (itemId != ITEM_THARIAN_TOKEN) {
        auto it = std::find_if(shopItemList.begin(), shopItemList.end(), [itemId](const ShopInfo& shopInfo) { return shopInfo.itemId == itemId && shopInfo.sellPrice != 0; });
        if (it == shopItemList.end()) {
            const Container* container = item->getContainer();
            if (!container) {
                return false;
            }

            const auto& items = container->getItemList();
            return std::any_of(items.begin(), items.end(), [this](const Item* containerItem) {
                return updateTokenShopList(containerItem);
            });
        }
    }

    if (client) {
        client->sendSaleItemList(shopItemList);
    }
    return true;
}

as well as this

player.cpp:
C++:
uint64_t Player::getTokens() const
{
    std::vector<const Container*> containers;
    uint64_t tokenCount = 0;

    for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
        Item* item = inventory[i];
        if (!item) {
            continue;
        }

        const Container* container = item->getContainer();
        if (container) {
            containers.push_back(container);
        } else {
            tokenCount += item->getTokenWorth();
        }
    }

    size_t i = 0;
    while (i < containers.size()) {
        const Container* container = containers[i++];
        for (const Item* item : container->getItemList()) {
            const Container* tmpContainer = item->getContainer();
            if (tmpContainer) {
                containers.push_back(tmpContainer);
            } else {
                tokenCount += item->getTokenWorth();
            }
        }
    }
    return tokenCount;
}

add functions to header.

player.h:
C++:
uint64_t getTokens() const;
bool updateTokenShopList(const Item* item);

Continued....
Post automatically merged:

Changes to TFS Data Folder:

Search for "getPlayerMoney" and add this underneath:

compat.lua:
Lua:
function getPlayerTokens(cid) local p = Player(cid) return p and p:getTokens() or false end

search for "doPlayerAddMoney" and add these underneath:

compat.lua:
Lua:
function doPlayerAddTokens(cid, tokens) local p = Player(cid) return p and p:addTokens(tokens) or false end
function doPlayerRemoveTokens(cid, tokens) local p = Player(cid) return p and p:removeTokens(tokens) or false end

Next, go to player.lua and add edit "canCarryMoney" to this.

player.lua:
Lua:
function Player.canCarryMoney(self, amount)
    -- Anyone can carry as much imaginary money as they desire
    if amount == 0 then
        return true
    end

    -- The 3 below loops will populate these local variables
    local totalWeight = 0
    local inventorySlots = 0

    -- Add crystal coins to totalWeight and inventorySlots
    local type_crystal = ItemType(ITEM_CRYSTAL_COIN)
    local crystalCoins = math.floor(amount / 10000)
    if crystalCoins > 0 then
        amount = amount - (crystalCoins * 10000)
        while crystalCoins > 0 do
            local count = math.min(100, crystalCoins)
            totalWeight = totalWeight + type_crystal:getWeight(count)
            crystalCoins = crystalCoins - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add platinum coins to totalWeight and inventorySlots
    local type_platinum = ItemType(ITEM_PLATINUM_COIN)
    local platinumCoins = math.floor(amount / 100)
    if platinumCoins > 0 then
        amount = amount - (platinumCoins * 100)
        while platinumCoins > 0 do
            local count = math.min(100, platinumCoins)
            totalWeight = totalWeight + type_platinum:getWeight(count)
            platinumCoins = platinumCoins - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add gold coins to totalWeight and inventorySlots
    local type_gold = ItemType(ITEM_GOLD_COIN)
    if amount > 0 then
        while amount > 0 do
            local count = math.min(100, amount)
            totalWeight = totalWeight + type_gold:getWeight(count)
            amount = amount - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add Tharian tokens to totalWeight and inventorySlots
    local type_tharian_token = ItemType(ITEM_THARIAN_TOKEN)
    if amount > 0 then
        while amount > 0 do
            local count = math.min(100, amount)
            totalWeight = totalWeight + type_tharian_token:getWeight(count)
            amount = amount - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- If player don't have enough capacity to carry this money
    if self:getFreeCapacity() < totalWeight then
        return false
    end

    -- If player don't have enough available inventory slots to carry this money
    local backpack = self:getSlotItem(CONST_SLOT_BACKPACK)
    if not backpack or backpack:getEmptySlots(true) < inventorySlots then
        return false
    end
    return true
end

search for "removeTotalTokens" and add this above:

player.lua:
Lua:
function Player.removeTotalTokens(self, amount)
    local tokensCount = self:getTokens()
    if amount <= tokensCount then
        self:removeTokens(amount)
        return true
    end
    return false
end

Next, the NPC additions.

Go to npc.lua and search for "doPlayerSellItem" and add this under the function.

npc.lua:
Lua:
function doPlayerBuyTokenItemContainer(cid, containerid, itemid, count, cost, charges)
    local player = Player(cid)
    if not player:removeTotalTokens(cost) then
        return false
    end

    for i = 1, count do
        local container = Game.createItem(containerid, 1)
        for x = 1, ItemType(containerid):getCapacity() do
            container:addItem(itemid, charges)
        end

        if player:addItemEx(container, true) ~= RETURNVALUE_NOERROR then
            return false
        end
    end
    return true
end

right under the function you just added, you'll see "getCount", add this under neath that function.

npc.lua:
Lua:
function Player.getTotalTokens(self)
    return self:getTokens()
end

function isValidTokens(tokens)
    return isNumber(tokens) and tokens > 0
end

function getTokensWeight(tokens)
    local t = tokens
    local tharian = math.floor(t / 100)
    t = t - tharian * 100
    return (ItemType(ITEM_THARIAN_TOKEN):getWeight() * t)
end

function getTokensCount(string)
    local b, e = string:find("%d+")
    local tokens = b and e and tonumber(string:sub(b, e)) or -1
    if isValidTokens(tokens) then
        return tokens
    end
    return -1
end
Post automatically merged:

in modules, add the Token Shop Module.

modules.lua (too big to post):

and on the npc handler side, add some messages:

npchandler.lua:
Lua:
    MESSAGE_MISSINGTOKENS = 24 -- When the player does not have enough tokens.
    MESSAGE_NEEDTOKENS = 25 -- Same as above, used for shop window.
    MESSAGE_TS_BUY = 26 -- When the npc asks the player if he wants to buy something.
    MESSAGE_TS_ONBUY = 27 -- When the player successfully buys something via talk.
    MESSAGE_TS_BOUGHT = 28 -- When the player bought something through the shop window.
    MESSAGE_TS_SELL = 29 -- When the npc asks the player if he wants to sell something.
    MESSAGE_TS_ONSELL = 30 -- When the player successfully sells something via talk.
    MESSAGE_TS_SOLD = 31 -- When the player sold something through the shop window.

further into npchandler, write messages.

npchandler.lua:
Lua:
            [MESSAGE_MISSINGTOKENS] = "You don't have enough tokens.",
            [MESSAGE_NEEDTOKENS] = "You don't have enough tokens.",
            [MESSAGE_TS_BUY] = "Do you want to buy |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| tokens?",
            [MESSAGE_TS_ONBUY] = "Here you are.",
            [MESSAGE_TS_BOUGHT] = "Bought |ITEMCOUNT|x |ITEMNAME| for |TOTALCOST| tokens.",
            [MESSAGE_TS_SELL] = "Do you want to sell |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| tokens?",
            [MESSAGE_TS_ONSELL] = "Here you are, |TOTALCOST| tokens.",
            [MESSAGE_TS_SOLD] = "Sold |ITEMCOUNT|x |ITEMNAME| for |TOTALCOST| tokens.",


Now, just edit a NPC to buy/sell with the new Token Shop Module:

Riona.xml:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<npc name="Riona" script="default.lua" walkinterval="2000" floorchange="0">
    <health now="100" max="100" />
    <look type="138" head="57" body="59" legs="40" feet="76" addons="0" />
    <parameters>
        <parameter key="module_token_shop" value="1" />
        <parameter key="token_shop_buyable" value="backpack,1988,20;pick,2553,10;shovel,2554,20;rope,2120,50" />
        <parameter key="token_shop_sellable" value="backpack,1988,20;pick,2553,10;shovel,2554,20;rope,2120,50" />

    </parameters>
</npc>

And that's it!
 
Does this has support for several tokens or just one Id?
 
Does this has support for several tokens or just one Id?
It can theoretically support more than 1 token, but it would require a system like the current gold system has.

Like this:

aToken = 100
bToken = 1000
cToken = 10000

and so forth. That way it'll be able to have a "value" compared to the other tokens you'd be adding. Look at how the switch case is formed, I did this in the style of the actual gold system so that others would be able to build off it, exactly how you're describing.

So yes, in theory, you could have 2 separate "gold" systems. One being the Tibian gold system, and the other being a new currency that's used on top of the original.

A cool idea would be supporting multiple currency systems, and each "region" of the map contains its own unique currency where the monsters in that region drop that coin, and you are only able to buy particular things in particular cities with those specific coins. Like, you kill a troll near Carlin, it drops 15 "Carlin Gold" and you can then go to Carlin to buy a "Carlin Backpack". If you were to try to buy a "Thais Backpack" in Thais, your money would be declined as it required "Thais Gold" and so forth.
Post automatically merged:

Does this has support for several tokens or just one Id?
So yes, I just confirmed you are able to add multiple "tokens" in a separate economy.

Like this:
C++:
    registerEnum(ITEM_THARIAN_GEM_FRAGMENT)
    registerEnum(ITEM_THARIAN_GEM_SHARD)
    registerEnum(ITEM_THARIAN_GEM_CLUSTER)
    registerEnum(ITEM_THARIAN_TOKEN)

C++:
function Player.canCarryMoney(self, amount)
    -- Anyone can carry as much imaginary money as they desire
    if amount == 0 then
        return true
    end

    -- The 3 below loops will populate these local variables
    local totalWeight = 0
    local inventorySlots = 0

    -- Add crystal coins to totalWeight and inventorySlots
    local type_crystal = ItemType(ITEM_CRYSTAL_COIN)
    local crystalCoins = math.floor(amount / 10000)
    if crystalCoins > 0 then
        amount = amount - (crystalCoins * 10000)
        while crystalCoins > 0 do
            local count = math.min(100, crystalCoins)
            totalWeight = totalWeight + type_crystal:getWeight(count)
            crystalCoins = crystalCoins - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add platinum coins to totalWeight and inventorySlots
    local type_platinum = ItemType(ITEM_PLATINUM_COIN)
    local platinumCoins = math.floor(amount / 100)
    if platinumCoins > 0 then
        amount = amount - (platinumCoins * 100)
        while platinumCoins > 0 do
            local count = math.min(100, platinumCoins)
            totalWeight = totalWeight + type_platinum:getWeight(count)
            platinumCoins = platinumCoins - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add gold coins to totalWeight and inventorySlots
    local type_gold = ItemType(ITEM_GOLD_COIN)
    if amount > 0 then
        while amount > 0 do
            local count = math.min(100, amount)
            totalWeight = totalWeight + type_gold:getWeight(count)
            amount = amount - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add tharian tokens to totalWeight and inventorySlots
    local type_tharian = ItemType(ITEM_THARIAN_TOKEN)
    local tharianTokens = math.floor(amount / 1000000)
    if tharianTokens > 0 then
        amount = amount - (tharianTokens * 1000000)
        while platinumCoins > 0 do
            local count = math.min(100, tharianTokens)
            totalWeight = totalWeight + type_tharian:getWeight(count)
            tharianTokens = tharianTokens - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add tharian gem clusters to totalWeight and inventorySlots
    local type_tharian_clusters = ItemType(ITEM_THARIAN_GEM_CLUSTER)
    local tharianClusters = math.floor(amount / 10000)
    if tharianClusters > 0 then
        amount = amount - (tharianClusters * 10000)
        while tharianClusters > 0 do
            local count = math.min(100, tharianClusters)
            totalWeight = totalWeight + type_tharian_clusters:getWeight(count)
            tharianClusters = tharianClusters - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add tharian shards to totalWeight and inventorySlots
    local type_tharian_shards = ItemType(ITEM_THARIAN_GEM_SHARD)
    local tharianShards = math.floor(amount / 100)
    if tharianShards > 0 then
        amount = amount - (tharianShards * 100)
        while tharianShards > 0 do
            local count = math.min(100, tharianShards)
            totalWeight = totalWeight + type_tharian_shards:getWeight(count)
            tharianShards = tharianShards - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- Add tharian fragments to totalWeight and inventorySlots
    local type_tharian_fragments = ItemType(ITEM_THARIAN_GEM_FRAGMENT)
    if amount > 0 then
        while amount > 0 do
            local count = math.min(100, amount)
            totalWeight = totalWeight + type_tharian_fragments:getWeight(count)
            amount = amount - count
            inventorySlots = inventorySlots + 1
        end
    end

    -- If player don't have enough capacity to carry this money
    if self:getFreeCapacity() < totalWeight then
        return false
    end

    -- If player don't have enough available inventory slots to carry this money
    local backpack = self:getSlotItem(CONST_SLOT_BACKPACK)
    if not backpack or backpack:getEmptySlots(true) < inventorySlots then
        return false
    end
    return true
end


C++:
    ITEM_THARIAN_GEM_FRAGMENT = 18418, // Value = 1
    ITEM_THARIAN_GEM_SHARD = 18419, // Value = 1000
    ITEM_THARIAN_GEM_CLUSTER = 18413, // Value = 10000
    ITEM_THARIAN_TOKEN = 18423, // Value = 1000000

C++:
function getTokensWeight(tokens)
    local t = tokens
    local tharianTokens = math.floor(t / 1000000)
    t = t - tharianTokens * 1000000
    local tharianClusters = math.floor(t / 10000)
    t = t - tharianClusters * 10000
    local tharianShards = math.floor(t / 100)
    t = t - tharianShards * 100
    return (ItemType(ITEM_THARIAN_TOKEN):getWeight() * tharianTokens) + (ItemType(ITEM_THARIAN_GEM_CLUSTER):getWeight() * tharianClusters) + (ItemType(ITEM_THARIAN_GEM_SHARD):getWeight() * tharianShards) + (ItemType(ITEM_THARIAN_GEM_FRAGMENT):getWeight() * t)
end

C++:
void Game::addTokens(Cylinder *cylinder, uint64_t tokens, uint32_t flags /*= 0*/)
{
    if (tokens == 0) {
        return;
    }

    uint32_t tharianTokens = tokens / 1000000;
    tokens -= tharianTokens * 1000000;
    while (tharianTokens > 0) {
        const uint16_t count = std::min<uint32_t>(100, tharianTokens);

        Item* remaindItem = Item::CreateItem(ITEM_THARIAN_TOKEN, count);

        ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags);
        if (ret != RETURNVALUE_NOERROR) {
            internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT);
        }

        tharianTokens -= count;
    }

    uint16_t tharianClusters = tokens / 10000;
    if (tharianClusters != 0) {
        Item* remaindItem = Item::CreateItem(ITEM_THARIAN_GEM_CLUSTER, tharianClusters);

        ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags);
        if (ret != RETURNVALUE_NOERROR) {
            internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT);
        }

        tokens -= tharianClusters * 100;
    }

    uint16_t tharianShards = tokens / 100;
    if (tharianShards != 0) {
        Item* remaindItem = Item::CreateItem(ITEM_THARIAN_GEM_SHARD, tharianShards);

        ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags);
        if (ret != RETURNVALUE_NOERROR) {
            internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT);
        }

        tokens -= tharianShards * 100;
    }

    if (tokens != 0) {
        Item* remaindItem = Item::CreateItem(ITEM_THARIAN_GEM_FRAGMENT, tokens);

        ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags);
        if (ret != RETURNVALUE_NOERROR) {
            internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT);
        }
    }
}

C++:
uint32_t Item::getTokenWorth() const
{
    switch (id) {
        case ITEM_THARIAN_GEM_FRAGMENT:
            return count;

        case ITEM_THARIAN_GEM_SHARD:
            return count * 100;

        case ITEM_THARIAN_GEM_CLUSTER:
            return count * 10000;

        case ITEM_THARIAN_TOKEN:
            return count * 1000000;

        default:
            return 0;
    }
}

C++:
bool Player::updateTokenShopList(const Item* item)
{
    uint16_t itemId = item->getID();
    if (itemId != ITEM_THARIAN_GEM_FRAGMENT && itemId != ITEM_THARIAN_GEM_SHARD && itemId != ITEM_THARIAN_GEM_CLUSTER && itemId != ITEM_THARIAN_TOKEN) {
        auto it = std::find_if(shopItemList.begin(), shopItemList.end(), [itemId](const ShopInfo& shopInfo) { return shopInfo.itemId == itemId && shopInfo.sellPrice != 0; });
        if (it == shopItemList.end()) {
            const Container* container = item->getContainer();
            if (!container) {
                return false;
            }

            const auto& items = container->getItemList();
            return std::any_of(items.begin(), items.end(), [this](const Item* containerItem) {
                return updateTokenShopList(containerItem);
            });
        }
    }

    if (client) {
        client->sendSaleItemList(shopItemList);
    }
    return true;
}

That would create a system such as:

Tharian Gem Fragment = 1
Tharian Gem Shard = 1000
Tharian Gem Cluster = 10000
Tharian Token = 1000000
 
Last edited:
exactly, like diferent kind of tokens that u can use on diferent npcs.
right now im using some islands maps where u have npcs that sells items based on the region.
that could be handy tho.

thks for the aditions!
 
exactly, like diferent kind of tokens that u can use on diferent npcs.
right now im using some islands maps where u have npcs that sells items based on the region.
that could be handy tho.

thks for the aditions!
I might also play around with something to use as a medium of exchange. Like this:

You kill a troll around Town A that gives you 10 aTokens.

You travel to Town B and want to buy some meat but they use bTokens.

You can then trade your aTokens for a "exchangeToken" that you can then use to trade for bTokens, cTokens, dTokens, or back to aTokens. You pick bTokens.

It charges you a 1-2% fee to exchange and then returns 98-99% bTokens to you.

You can then purchase meat with bTokens.

This is also a way to fight inflation in the games economy by simply increasing the fee of exchange when the eco is becoming too inflated. Decrease when back to normal.

It also forces the players to explore the entire map, as you can only get particular things with particular tokens from their respective zones.
 
fuck yeah
you can also exchange the token youll need for give 1 token in exchange for 2 (1:2)

i really like the idea
 
Currently playing around with the exchange currency and an in game economy that tracks exchanges between one token to another.

In theory it will work similarly to Forex. Will probably include a way to exchange via the website as well.. since it would be difficult to display exchange rates in game.

I will update as I progress.
 
I was able to make a lending/borrowing platform that includes gaining/losing principal due to interest. It's very very rough. I'm not sure if this is all of the code, I kind of got lost. Luckily, I think I was able to write most of it in Lua. Again, this is very rough and likely won't work, mainly meant to be a proof of concept:


Borrowing: (Obviously you want to borrow a specific amount, I just haven't gotten around to adding that to the borrowing section just yet)

borrow.png


Lending:
lending.png

Market funding info:
funding.png

Your own lending info: (to check rewards)
check.png

When you try to borrow money when there is no funding available:
error.png


Again (for the last time, I promise), it's very rough. So if anyone has any suggestions, please feel free to speak up / respond, I'd love to hear anyone's feedback.

Not going to be producing this for a server, so all of my code is for you guys and you guys alone. You can reuse/edit/sell/distribute it any way you please.

Lua:
-----------------------------------------------------------------------------------------------------------------------
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
NpcSystem.parseParameters(npcHandler)
-----------------------------------------------------------------------------------------------------------------------
local onPlayerBalance = {}
local interestRate = 0.0001
-----------------------------------------------------------------------------------------------------------------------
function onCreatureAppear(cid)              npcHandler:onCreatureAppear(cid)            end
function onCreatureDisappear(cid)           npcHandler:onCreatureDisappear(cid)         end
function onCreatureSay(cid, type, msg)      npcHandler:onCreatureSay(cid, type, msg)    end
function onThink()                          npcHandler:onThink()                        end
-----------------------------------------------------------------------------------------------------------------------
local function getCount(s)
    local b, e = s:find('%d+')
    return b and e and math.min(4294967295, tonumber(s:sub(b, e))) or -1
end

-----------------------------------------------------------------------------------------------------------------------
local function creatureSayCallback(cid, type, msg)
    if not npcHandler:isFocused(cid) then
        return false
    end

-----------------------------------------------------------------------------------------------------------------------
local function greetCallback(cid)
    onPlayerBalance[cid] = nil
    
    return true
end
-----------------------------------------------------------------------------------------------------------------------
    local player = Player(cid)
    local playerGuid = player:getGuid()
    onPlayerBalance[cid] = player:getMoney()
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: GET GLOBAL BORROWERS COUNT
    local function getGlobalBorrowersCount()
        local resultId = db.storeQuery("SELECT COUNT(borrowing_balance) FROM `margin_book` WHERE `ended` IS NULL AND `lending_balance` = 0 ")
        local targetGlobalLendersCount = result.getNumber(resultId, "COUNT(borrowing_balance)")

        return targetGlobalLendersCount
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: GET GLOBAL BORRWED FUND
    local function getGlobalBorrowedFund()
        local resultId = db.storeQuery("SELECT SUM(borrowing_balance) FROM `margin_book` WHERE `ended` IS NULL AND `lending_balance` = 0 ")
        local targetGlobalBorrwedFund = result.getNumber(resultId, "SUM(borrowing_balance)")

        return targetGlobalBorrwedFund
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: GET GLOBAL LENDERS COUNT
    local function getGlobalLendersCount()
        local resultId = db.storeQuery("SELECT COUNT(lending_balance) FROM `margin_book` WHERE `ended` IS NULL AND `borrowing_balance` = 0 ")
        local targetGlobalLendersCount = result.getNumber(resultId, "COUNT(lending_balance)")

        return targetGlobalLendersCount
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: GET GLOBAL LENDING FUND
    local function getGlobalLendingFund()
        local resultId = db.storeQuery("SELECT SUM(lending_balance) FROM `margin_book` WHERE `ended` IS NULL AND `borrowing_balance` = 0 ")
        local targetGlobalLendingFund = result.getNumber(resultId, "SUM(lending_balance)")

        return targetGlobalLendingFund
    end

-----------------------------------------------------------------------------------------------------------------------
    -- Getter: GET GLOBAL FUNDING
    local function getGlobalFunding()
        local globalFund = getGlobalLendingFund() - getGlobalBorrowedFund()

        return globalFund
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: BORROW START
    local function getBorrowStartTime()
        local resultId = db.storeQuery("SELECT `borrow_start` FROM `players` WHERE `id` = " .. playerGuid)
        local targetBorrowStartTime = result.getNumber(resultId, "borrow_start")

        return targetBorrowStartTime
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: BORROW END
    local function getBorrowEndTime()
        local resultId = db.storeQuery("SELECT `borrow_end` FROM `players` WHERE `id` = " .. playerGuid)
        local targetBorrowEndTime = result.getNumber(resultId, "borrow_end")

        return targetBorrowEndTime
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: BORROW TOTAL BALANCE
    local function getTotalBorrowBalance()
        local resultId = db.storeQuery("SELECT `borrow_balance` FROM `players` WHERE `id` = " .. playerGuid)
        local targetTotalBorrowBalance = result.getNumber(resultId, "borrow_balance")

        return targetTotalBorrowBalance
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: BORROW TOTAL ACCRUED INTEREST
    local function getTotalBorrowAccrued()
        local resultId = db.storeQuery("SELECT `borrow_accrued` FROM `players` WHERE `id` = " .. playerGuid)
        local targetTotalBorrowAccrued = result.getNumber(resultId, "borrow_accrued")

        return targetTotalBorrowAccrued
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Function: BORROW START
    local function startBorrowing(amount)
        if amount < getGlobalFunding() then
            local startTime = os.time()
            local newBalance = getTotalBorrowBalance() + amount
            npcHandler:say("Money: " .. onPlayerBalance[cid] .. " .", cid)
            npcHandler:say("Current Borrowing Balance: " .. getTotalBorrowBalance() .. " .", cid)
            npcHandler:say("Borrowing Amount: " .. amount .. " .", cid)
            npcHandler:say("New Balance: " .. newBalance .. " .", cid)
            npcHandler:say("Borrow Start Time: " .. startTime .. " .", cid)

            player:addMoney(amount)

            db.asyncQuery("UPDATE `players` SET `borrow_start` = " .. startTime .. " WHERE `id` = '" .. playerGuid .. "'")
            db.asyncQuery("UPDATE `players` SET `borrow_balance` = " .. newBalance .. " WHERE `id` = '" .. playerGuid .. "'")

            db.asyncQuery("INSERT INTO `margin_book` (`player_id`, `borrowing_balance`, `created`) VALUES (" .. playerGuid .. ", " .. newBalance .. ", " .. startTime .. ")")
        else if amount > getGlobalLendingFund() then
            npcHandler:say("Sorry, the global lending fund cannot afford this transaction. Please check available {funding}.", cid)
        end
    end
end
-----------------------------------------------------------------------------------------------------------------------
    -- Function: BORROW STOP
    local function stopBorrowing()
        local endTime = os.time()
        local borrowTime = endTime - getBorrowStartTime()
        local newBalance = getTotalBorrowBalance() - getTotalBorrowBalance()

        npcHandler:say("Money: " .. onPlayerBalance[cid] .. " .", cid)
        npcHandler:say("Current Borrowing Balance: " .. getTotalBorrowBalance() .. " .", cid)
        npcHandler:say("Removing Amount: " .. getTotalBorrowBalance() .. " .", cid)
        npcHandler:say("New Balance: " .. newBalance .. " .", cid)
        npcHandler:say("Started Borrowing: " .. getBorrowStartTime() .. " .", cid)
        npcHandler:say("Ended Borrowing: " .. endTime .. " .", cid)
        npcHandler:say("Time Spent Borrowing: " .. borrowTime .. " .", cid)
        npcHandler:say("Interest Rate: " .. interestRate .. " .", cid)

        borrowInterestMultiplier = (interestRate / borrowTime) * 100
        borrowInterest = borrowInterestMultiplier * getTotalBorrowBalance()

        npcHandler:say("Borrowing Interest Accrued: " .. borrowInterest .. " .", cid)
        npcHandler:say("Total Borrowing Interest: " .. getTotalBorrowAccrued() .. " .", cid)

        concBorrowInterest = getTotalBorrowAccrued() + borrowInterest
        local totalInterestCapital = getTotalBorrowBalance() + concBorrowInterest

        player:removeTotalMoney(totalInterestCapital)

        npcHandler:say("Total Repayment: " .. totalInterestCapital .. " .", cid)

        db.asyncQuery("UPDATE `players` SET `borrow_end` = " .. endTime.. " WHERE `id` = '" .. playerGuid .. "'")
        db.asyncQuery("UPDATE `players` SET `borrow_accrued` = " .. concBorrowInterest .. " WHERE `id` = '" .. playerGuid .. "'")
        db.asyncQuery("UPDATE `players` SET `borrow_balance` = " .. newBalance .. " WHERE `id` = '" .. playerGuid .. "'")

        db.asyncQuery("UPDATE `margin_book` SET `ended` = " .. endTime .. " WHERE `created` = " .. getBorrowStartTime() .. " AND `player_id` = " .. playerGuid )
        db.asyncQuery("UPDATE `margin_book` SET `loan_time` = " .. borrowTime .. " WHERE `created` = " .. getBorrowStartTime() .. " AND `player_id` = " .. playerGuid )
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: LEND START
    local function getLendStartTime()
        local resultId = db.storeQuery("SELECT `lend_start` FROM `players` WHERE `id` = " .. playerGuid)
        local targetLendStartTime = result.getNumber(resultId, "lend_start")

        return targetLendStartTime
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: LEND END
    local function getLendEndTime()
        local resultId = db.storeQuery("SELECT `lend_end` FROM `players` WHERE `id` = " .. playerGuid)
        local targetLendEndTime = result.getNumber(resultId, "lend_end")

        return targetLendEndTime
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: LEND TOTAL BALANCE
    local function getTotalLendingBalance()
        local resultId = db.storeQuery("SELECT `lend_balance` FROM `players` WHERE `id` = " .. playerGuid)
        local targetTotalLendingBalance = result.getNumber(resultId, "lend_balance")

        return targetTotalLendingBalance
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Getter: LEND TOTAL REWARDS
    local function getTotalLendingRewards()
        local resultId = db.storeQuery("SELECT `lend_rewards` FROM `players` WHERE `id` = " .. playerGuid)
        local targetTotalLendingRewards = result.getNumber(resultId, "lend_rewards")

        return targetTotalLendingRewards
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Function: LENDING START
    local function startLending(amount)
        if player:removeTotalMoney(amount) then
            local startTime = os.time()
            local newBalance = getTotalLendingBalance() + amount
            npcHandler:say("Money: " .. onPlayerBalance[cid] .. " .", cid)
            npcHandler:say("Current Lending Balance: " .. getTotalLendingBalance() .. " .", cid)
            npcHandler:say("Adding Amount: " .. amount .. " .", cid)
            npcHandler:say("New Balance: " .. newBalance .. " .", cid)
            npcHandler:say("Lend Start Time: " .. startTime .. " .", cid)

            db.asyncQuery("UPDATE `players` SET `lend_start` = " .. startTime .. " WHERE `id` = '" .. playerGuid .. "'")
            db.asyncQuery("UPDATE `players` SET `lend_balance` = " .. newBalance .. " WHERE `id` = '" .. playerGuid .. "'")

            db.asyncQuery("INSERT INTO `margin_book` (`player_id`, `lending_balance`, `created`) VALUES (" .. playerGuid .. ", " .. newBalance .. ", " .. startTime .. ")")
        else
            npcHandler:say("You don't have enough exchange tokens to support this order.", cid)
        end
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Function: LENDING STOP
    local function stopLending()
        local endTime = os.time()
        local lendTime = endTime - getLendStartTime()
        local newBalance = getTotalLendingBalance() - getTotalLendingBalance()

        npcHandler:say("Money: " .. onPlayerBalance[cid] .. " .", cid)
        npcHandler:say("Current Lending Balance: " .. getTotalLendingBalance() .. " .", cid)
        npcHandler:say("Removing Amount: " .. getTotalLendingBalance() .. " .", cid)
        npcHandler:say("New Balance: " .. newBalance .. " .", cid)
        npcHandler:say("Started Lending: " .. getLendStartTime() .. " .", cid)
        npcHandler:say("Ended Lending: " .. endTime .. " .", cid)
        npcHandler:say("Time Spent Lending: " .. lendTime .. " .", cid)
        npcHandler:say("Interest Rate: " .. interestRate .. " .", cid)

        lendRewardMultiplier = (interestRate / lendTime) * 100
        lendRewards = lendRewardMultiplier * getTotalLendingBalance()

        npcHandler:say("Lending Rewards Earned: " .. lendRewards .. " .", cid)
        npcHandler:say("Total Lending Rewards: " .. getTotalLendingRewards() .. " .", cid)

        concLendingRewards = getTotalLendingRewards() + lendRewards
        local totalInterestCapital = getTotalLendingBalance() + concLendingRewards

        player:addMoney(totalInterestCapital)
        npcHandler:say("Total Repayment: " .. totalInterestCapital .. " .", cid)

        db.asyncQuery("UPDATE `players` SET `lend_end` = " .. endTime.. " WHERE `id` = '" .. playerGuid .. "'")
        db.asyncQuery("UPDATE `players` SET `lend_rewards` = " .. concLendingRewards.. " WHERE `id` = '" .. playerGuid .. "'")
        db.asyncQuery("UPDATE `players` SET `lend_balance` = " .. newBalance .. " WHERE `id` = '" .. playerGuid .. "'")

        db.asyncQuery("UPDATE `margin_book` SET `ended` = " .. endTime .. " WHERE `created` = " .. getLendStartTime() .. " AND `player_id` = " .. playerGuid )
        db.asyncQuery("UPDATE `margin_book` SET `loan_time` = " .. lendTime .. " WHERE `created` = " .. getLendStartTime() .. " AND `player_id` = " .. playerGuid )
    end
-----------------------------------------------------------------------------------------------------------------------
    -- Function: NPC INTERACTION
    if msgcontains(msg, 'lend') then
        local amountToLend = getCount(msg)
        npcHandler:say("Got it. I'll lend " .. amountToLend .. "x exchange tokens for you.", cid)
            startLending(amountToLend)
    end

    if msgcontains(msg, 'check') then
        npcHandler:say("Current Lending Balance: " .. getTotalLendingBalance() .. " .", cid)
        npcHandler:say("Interest Rate: " .. interestRate .. " .", cid)
        npcHandler:say("Total Lending Rewards: " .. getTotalLendingRewards() .. " .", cid)
    end

    if msgcontains(msg, 'withdrawl') then
        stopLending()
    end

    if msgcontains(msg, 'borrow') then
        startBorrowing(50)
            else if msgcontains(msg, "repay") then
            stopBorrowing()
        end
    end

    if msgcontains(msg, 'funding') then
        local globalFund = getGlobalLendingFund() - getGlobalBorrowedFund()
        npcHandler:say("Total Lenders: " .. getGlobalLendersCount() .. ".", cid)
        npcHandler:say("Total Lending: " .. getGlobalLendingFund() .. ".", cid)
        npcHandler:say("Total Borrowers: " .. getGlobalBorrowersCount() .. ".", cid)
        npcHandler:say("Total Borrwed: " .. getGlobalBorrowedFund() .. ".", cid)
        npcHandler:say("Global Funding: " .. getGlobalFunding() .. ".", cid)
        return true
    end
end
-----------------------------------------------------------------------------------------------------------------------
npcHandler:setMessage(MESSAGE_GREET, "Welcome to the global lending market. Need some {help} getting started?")
npcHandler:setCallback(CALLBACK_GREET, greetCallback)
npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback)
npcHandler:addModule(FocusModule:new())
-----------------------------------------------------------------------------------------------------------------------
Post automatically merged:

You will also need to have these tables / columns / rows in your db that are mentioned in the script in order for it to actually grab that info.
 
sup @Aeluu is there a way to use your main script, with diferent npcs and a unique token for each of them?
 
Really impressive! I think adding another script to “use” currency to convert it would help alot
 
sup @Aeluu is there a way to use your main script, with diferent npcs and a unique token for each of them?
Yessir,

How I have that set up Is using different shop modules for separate currencies. I have them for three different in game currencies, so it works.

Really impressive! I think adding another script to “use” currency to convert it would help alot
Yeah that is ultra easy to setup. Essentially just changing the gold converter script to take the new currencies listed in the c code, like changing ITEM_CRYSTAL_COIN to ITEM_THARIAN_TOKEN and whatnot. If you get your currencies setup let me know their names and I can make you a script to convert them.
 
Back
Top