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

TFS 1.X+ Shared Exp taking into account distance and professions

ZiumZium

Member
Joined
Sep 27, 2024
Messages
106
Solutions
1
Reaction score
11
Hi, I thought I would start a new topic considering what has already been done. By the way, someone may directly benefit from the results :)
original thread: C++ - Party shared experience issue (https://otland.net/threads/party-shared-experience-issue.286392/page-3#posts) (3 page)

Thanks to @dchampag who took the time to get to this point

TFS 1.4.2

(changes compared to TFS 1.4.2 freshly downloaded from otland)
Here's what we managed to do and the results:

LUA:
void Party::shareExperience(uint64_t experience, Creature* source/* = nullptr*/, const Position& monsterPosition)
{
    // Create a temporary list to hold eligible members for experience sharing
    std::vector<Player*> eligibleMembers;

    // Check if the leader can receive experience, range of monster killed, not in PZ
    if (leader->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, leader->getPosition())) {
        eligibleMembers.push_back(leader);
    }

    // Check if party members can receive experience, range of monster killed, not in PZ
    for (Player* member : memberList) {
        if (member->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, member->getPosition())) {
            eligibleMembers.push_back(member);
        }
    }

    uint64_t shareExperience = experience;
    g_events->eventPartyOnShareExperience(this, shareExperience);

    if (std::find(eligibleMembers.begin(), eligibleMembers.end(), leader) != eligibleMembers.end()) {
        leader->onGainSharedExperience(shareExperience, source);
    }

    for (auto it = memberList.begin(); it != memberList.end(); ++it) {
        if (std::find(eligibleMembers.begin(), eligibleMembers.end(), (*it)) != eligibleMembers.end()) {
            shareExperience = shareExperience; //This ensures that the expShare amount is not multiplied in any way
            (*it)->onGainSharedExperience(shareExperience, source);
        }
    }
}

LUA:
    if (!Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(leader->getPosition(), player->getPosition())) {
        return SHAREDEXP_TOOFARAWAY;
    }

    if (!player->hasFlag(PlayerFlag_NotGainInFight)) {
        //check if the player has healed/attacked anything recently
        auto it = ticksMap.find(player->getID());
        if (it == ticksMap.end()) {
            return SHAREDEXP_MEMBERINACTIVE;
        }

        uint64_t timeDiff = OTSYS_TIME() - it->second;
        if (timeDiff > static_cast<uint64_t>(g_config.getNumber(ConfigManager::PZ_LOCKED))) {
            return SHAREDEXP_MEMBERINACTIVE;
        }
    }

LUA:
void shareExperience(uint64_t experience, Creature* source = nullptr, const Position& monsterPosition = Position());
LUA:
void Player::onGainExperience(uint64_t gainExp, Creature* target, const Position& monsterPosition)
{
    if (hasFlag(PlayerFlag_NotGainExperience)) {
        return;
    }

    if (target && !target->getPlayer() && party) {
        // && party->isSharedExperienceActive() && party->isSharedExperienceEnabled() is party share enable and active, remove this section to always share
        party->shareExperience(gainExp, target, monsterPosition);
        //We will get a share of the experience through the sharing mechanism
        return;
    }

    Creature::onGainExperience(gainExp, target, monsterPosition);
    gainExperience(gainExp, target);
}

LUA:
void onGainExperience(uint64_t gainExp, Creature* target, const Position& monsterPosition) override;

LUA:
function Party:onJoin(player)
    if hasEventCallback(EVENT_CALLBACK_ONJOIN) then
        return EventCallback(EVENT_CALLBACK_ONJOIN, self, player)
    else
        return true
    end
end

function Party:onLeave(player)
    if hasEventCallback(EVENT_CALLBACK_ONLEAVE) then
        return EventCallback(EVENT_CALLBACK_ONLEAVE, self, player)
    else
        return true
    end
end

function Party:onDisband()
    if hasEventCallback(EVENT_CALLBACK_ONDISBAND) then
        return EventCallback(EVENT_CALLBACK_ONDISBAND, self)
    else
        return true
    end
end

function Party:onShareExperience(exp)
    local sharedExperienceMultiplier = 1.00
    local vocationsIds = {}
    local rawExp = exp

    local vocationId = self:getLeader():getVocation():getBase():getId()
    if vocationId ~= VOCATION_NONE then
        table.insert(vocationsIds, vocationId)
    end

    for _, member in ipairs(self:getMembers()) do
        vocationId = member:getVocation():getBase():getId()
        if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then
            table.insert(vocationsIds, vocationId)
        end
    end

    local size = #vocationsIds
 
    if size == 2 then  -- size represents number of different vocations
        sharedExperienceMultiplier = 1.0 + (15 / 100) -- 10 should represent 10%
    elseif size == 3 then
        sharedExperienceMultiplier = 1.0 + (30 / 100) -- 30 should represent 30%
    elseif size >= 4 then -- in case you add more vocations this wont cause issues.
        sharedExperienceMultiplier = 1.0 + (50 / 100) -- 50 should represent 50%
    end

    exp = math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1))
    return hasEventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE, self, exp, rawExp) or exp
end
LUA:
function Player:onBrowseField(position)
    if hasEventCallback(EVENT_CALLBACK_ONBROWSEFIELD) then
        return EventCallback(EVENT_CALLBACK_ONBROWSEFIELD, self, position)
    end
    return true
end

function Player:onLook(thing, position, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOK) then
        description = EventCallback(EVENT_CALLBACK_ONLOOK, self, thing, position, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInBattleList(creature, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST, self, creature, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInTrade(partner, item, distance)
    local description = "You see " .. item:getDescription(distance)
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINTRADE) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINTRADE, self, partner, item, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInShop(itemType, count, description)
    local description = "You see " .. description
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINSHOP) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINSHOP, self, itemType, count, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONMOVEITEM) then
        return EventCallback(EVENT_CALLBACK_ONMOVEITEM, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
    return RETURNVALUE_NOERROR
end

function Player:onItemMoved(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONITEMMOVED) then
        EventCallback(EVENT_CALLBACK_ONITEMMOVED, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
end

function Player:onMoveCreature(creature, fromPosition, toPosition)
    if hasEventCallback(EVENT_CALLBACK_ONMOVECREATURE) then
        return EventCallback(EVENT_CALLBACK_ONMOVECREATURE, self, creature, fromPosition, toPosition)
    end
    return true
end

function Player:onReportRuleViolation(targetName, reportType, reportReason, comment, translation)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION) then
        EventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION, self, targetName, reportType, reportReason, comment, translation)
    end
end

function Player:onReportBug(message, position, category)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTBUG) then
        return EventCallback(EVENT_CALLBACK_ONREPORTBUG, self, message, position, category)
    end
    return true
end

function Player:onTurn(direction)
    if hasEventCallback(EVENT_CALLBACK_ONTURN) then
        return EventCallback(EVENT_CALLBACK_ONTURN, self, direction)
    end
    return true
end

function Player:onTradeRequest(target, item)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEREQUEST) then
        return EventCallback(EVENT_CALLBACK_ONTRADEREQUEST, self, target, item)
    end
    return true
end

function Player:onTradeAccept(target, item, targetItem)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEACCEPT) then
        return EventCallback(EVENT_CALLBACK_ONTRADEACCEPT, self, target, item, targetItem)
    end
    return true
end

function Player:onTradeCompleted(target, item, targetItem, isSuccess)
    if hasEventCallback(EVENT_CALLBACK_ONTRADECOMPLETED) then
        EventCallback(EVENT_CALLBACK_ONTRADECOMPLETED, self, target, item, targetItem, isSuccess)
    end
end

local soulCondition = Condition(CONDITION_SOUL, CONDITIONID_DEFAULT)
soulCondition:setTicks(4 * 60 * 1000)
soulCondition:setParameter(CONDITION_PARAM_SOULGAIN, 1)

local function useStamina(player)
    local staminaMinutes = player:getStamina()
    if staminaMinutes == 0 then
        return
    end

    local playerId = player:getId()
    if not nextUseStaminaTime[playerId] then
        nextUseStaminaTime[playerId] = 0
    end

    local currentTime = os.time()
    local timePassed = currentTime - nextUseStaminaTime[playerId]
    if timePassed <= 0 then
        return
    end

    if timePassed > 60 then
        if staminaMinutes > 2 then
            staminaMinutes = staminaMinutes - 2
        else
            staminaMinutes = 0
        end
        nextUseStaminaTime[playerId] = currentTime + 120
    else
        staminaMinutes = staminaMinutes - 1
        nextUseStaminaTime[playerId] = currentTime + 60
    end
    player:setStamina(staminaMinutes)
end

function Player:onGainExperience(source, exp, rawExp)
    if not source or source:isPlayer() then
        return exp
    end

    -- Soul regeneration
    local vocation = self:getVocation()
    if self:getSoul() < vocation:getMaxSoul() and exp >= self:getLevel() then
        soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000)
        self:addCondition(soulCondition)
    end

    -- Apply experience stage multiplier
    exp = exp * Game.getExperienceStage(self:getLevel())

    -- Stamina modifier
    if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then
        useStamina(self)

        local staminaMinutes = self:getStamina()
        if staminaMinutes > 2400 and self:isPremium() then
            exp = exp * 1.0
        elseif staminaMinutes <= 840 then
            exp = exp * 0.5
        end
    end

    return hasEventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE, self, source, exp, rawExp) or exp
end

function Player:onLoseExperience(exp)
    return hasEventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE, self, exp) or exp
end

function Player:onGainSkillTries(skill, tries)
    if APPLY_SKILL_MULTIPLIER == false then
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end

    if skill == SKILL_MAGLEVEL then
        tries = tries * configManager.getNumber(configKeys.RATE_MAGIC)
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end
    tries = tries * configManager.getNumber(configKeys.RATE_SKILL)
    return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
end

function Player:onWrapItem(item)
    local topCylinder = item:getTopParent()
    if not topCylinder then
        return
    end

    local tile = Tile(topCylinder:getPosition())
    if not tile then
        return
    end

    local house = tile:getHouse()
    if not house then
        self:sendCancelMessage("You can only wrap and unwrap this item inside a house.")
        return
    end

    if house ~= self:getHouse() and not string.find(house:getAccessList(SUBOWNER_LIST):lower(), "%f[%a]" .. self:getName():lower() .. "%f[%A]") then
        self:sendCancelMessage("You cannot wrap or unwrap items from a house, which you are only guest to.")
        return
    end

    local wrapId = item:getAttribute("wrapid")
    if wrapId == 0 then
        return
    end

    if not hasEventCallback(EVENT_CALLBACK_ONWRAPITEM) or EventCallback(EVENT_CALLBACK_ONWRAPITEM, self, item) then
        local oldId = item:getId()
        item:remove(1)
        local item = tile:addItem(wrapId)
        if item then
            item:setAttribute("wrapid", oldId)
        end
    end
end
The test object is a dragon 700 xp
Effect:


If 4 different vocations kill the dragon - 700 XP: 4 (players) + 50% (bonus vocations)

If 3 different vocations kill the dragon - 700 XP: 3 (players) + 30% (bonus vocations)

etc as in player.lua

Problem:


If 4 vocations are together, they get 262.5 XP and one person is out of Shared range or in PZ does not receive XP for the kill.

However, the 3 remaining people still receive 262.5 (because it still counts as if 4 vocations were together). It doesn't matter/removes if someone is out of party or PZ range
Post automatically merged:

Here's what I ultimately want to do:

I would like to divide it equally among the number of party members and add a bonus depending on the number of professions:

Dragon (700 XP): Number of party members (2) = 350 + bonus ✅
Dragon (700 XP): Number of party members (3) = 233 + bonus ✅
... ✅

Bonus based on the number of professions in the party (shared):

  • 1 profession – bonus 10% (e.g., 2x sorcerer) ✅
  • 2 professions – bonus 15% (e.g., sorcerer + druid) ✅
  • 3 professions – bonus 30% (e.g., sorcerer + druid + knight) ✅
  • 4 professions – bonus 50% (all professions) ✅
If it is possible to activate Shared XP independently of joining the party:

  • For Shared XP to work for a given player, they must hit a monster at least once every 3 minutes (in that case, without attacking other monsters, Shared XP remains active for the player). After 3 minutes without an attack, Shared XP stops working for that specific player – for the rest of the party, Shared XP treats them as if they were not present (the experience and bonus are calculated without them). ❌

  • Shared XP works even if someone is on another floor or far away (Hence the 3-minute limit) or if an alternative can be done - if the player is out of range (or in the PZ) removes him from shared ❌

  • Allowed level difference in Shared XP: 50 – but if you have a better idea to make it work well, I’m open to suggestions. ❌
    (Currently, if there are 3 professions at lvl 588 and 1 profession (the last one for the bonus) at lvl 1 - everyone gets 700: 4 + 50% bonus)
 
Last edited:
What he is asking is, if there is a way to use the areInRange function available from CPP in lua, as well as monsterPosition that is already working in CPP in the .lua how would we go about adding that if it's not already accessible?

example of what we need to work below. the Position function, under the vocationId check to add bonus.

LUA:
function Party:onShareExperience(exp)
    local sharedExperienceMultiplier = 1.00
    local vocationsIds = {}
    local rawExp = exp

    local vocationId = self:getLeader():getVocation():getBase():getId()
    if vocationId ~= VOCATION_NONE then
        if Position(self:getLeader):isInRange(monsterPosotition, x, y, z) then
            table.insert(vocationsIds, vocationId)
        end
    end

    for _, member in ipairs(self:getMembers()) do
        vocationId = member:getVocation():getBase():getId()
        if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then
            if Position(member):isInRange(monsterPosotition, x, y, z) then
                table.insert(vocationsIds, vocationId)
            end
        end
    end

    local size = #vocationsIds
 
    if size == 2 then  -- size represents number of different vocations
        sharedExperienceMultiplier = 1.0 + (15 / 100) -- 10 should represent 10%
    elseif size == 3 then
        sharedExperienceMultiplier = 1.0 + (30 / 100) -- 30 should represent 30%
    elseif size >= 4 then -- in case you add more vocations this wont cause issues.
        sharedExperienceMultiplier = 1.0 + (50 / 100) -- 50 should represent 50%
    end

    exp = math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1))
    return hasEventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE, self, exp, rawExp) or exp
end
Post automatically merged:

SOLVED

we added monsterPosition to event in cpp and header

party.cpp FROM
C++:
    uint64_t shareExperience = experience;
    g_events->eventPartyOnShareExperience(this, shareExperience);

TO
C++:
    uint64_t shareExperience = experience;
    g_events->eventPartyOnShareExperience(this, shareExperience, monsterPosition);

events.h
LUA:
        void eventPartyOnShareExperience(Party* party, uint64_t& exp, const Position& monsterPosition);

events.cpp
Code:
void Events::eventPartyOnShareExperience(Party* party, uint64_t& exp, const Position& monsterPosition)
{
    // Party:onShareExperience(exp, monsterPosition) or Party.onShareExperience(self, exp, monsterPosition)
    if (info.partyOnShareExperience == -1) {
        return;
    }

    if (!scriptInterface.reserveScriptEnv()) {
        std::cout << "[Error - Events::eventPartyOnShareExperience] Call stack overflow" << std::endl;
        return;
    }

    ScriptEnvironment* env = scriptInterface.getScriptEnv();
    env->setScriptId(info.partyOnShareExperience, &scriptInterface);

    lua_State* L = scriptInterface.getLuaState();
    scriptInterface.pushFunction(info.partyOnShareExperience);

    LuaScriptInterface::pushUserdata<Party>(L, party);
    LuaScriptInterface::setMetatable(L, -1, "Party");

    lua_pushnumber(L, exp);

    // Push the monsterPosition to the Lua stack
    LuaScriptInterface::pushPosition(L, monsterPosition);

    if (scriptInterface.protectedCall(L, 3, 1) != 0) {
        LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L));
    }
    else {
        exp = LuaScriptInterface::getNumber<uint64_t>(L, -1);
        lua_pop(L, 1);
    }

    scriptInterface.resetScriptEnv();
}


Then this was used to check range using this function

LUA:
local function checkRange(pos1, pos2, rangeX, rangeY, floors)
    -- Calculate the differences in the X and Y axes
    local dx = math.abs(pos1.x - pos2.x)
    local dy = math.abs(pos1.y - pos2.y)
  
    -- Calculate the difference in levels (floors)
    local dz = math.abs(pos1.z - pos2.z)

    -- Condition: the player must be within the horizontal range (X, Y) and acceptable floor difference
    return dx <= rangeX and dy <= rangeY and dz <= floors
end

example below will check if the player is in range of the monster killed and not in PZ
LUA:
local leaderPosition = self:getLeader():getPosition()
--checkRange(first position, second position, x, y, z)
--Position.getTile(tilePosition):hasFlag(TILESTATE_PROTECTIONZONE)
if checkRange(leaderPosition, monsterPosition, 10, 10, 1) and not
Position.getTile(leaderPosition):hasFlag(TILESTATE_PROTECTIONZONE) then
Post automatically merged:

Here I've attached the sources and data from basic 1.4 release with this system added, can be used to compare the code and add to your own servers. enjoy.
 

Attachments

Last edited:
Back
Top