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:
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
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):

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
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)
- 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: