player.cpp

main 4949 lines · 126.8 KB Raw
Alex Alex Commit Server upload 18/10/2026 23:38
1/**
2 * The Forgotten Server - a free and open-source MMORPG server emulator
3 * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "otpch.h"
21
22#include <bitset>
23
24#include "bed.h"
25#include "chat.h"
26#include "combat.h"
27#include "configmanager.h"
28#include "creatureevent.h"
29#include "events.h"
30#include "game.h"
31#include "iologindata.h"
32#include "monster.h"
33#include "movement.h"
34#include "outfit.h"
35#include "scheduler.h"
36#include "weapons.h"
37
38extern ConfigManager g_config;
39extern Game g_game;
40extern Chat *g_chat;
41extern Vocations g_vocations;
42extern MoveEvents *g_moveEvents;
43extern Weapons *g_weapons;
44extern CreatureEvents *g_creatureEvents;
45extern Events *g_events;
46
47MuteCountMap Player::muteCountMap;
48
49uint32_t Player::playerAutoID = 0x10000000;
50
51Player::Player(ProtocolGame_ptr p)
52 : Creature(), lastPing(OTSYS_TIME()), lastPong(lastPing),
53 inbox(new Inbox(ITEM_INBOX)), client(std::move(p)) {
54 inbox->incrementReferenceCounter();
55}
56
57Player::~Player() {
58 for (Item *item : inventory) {
59 if (item) {
60 item->setParent(nullptr);
61 item->decrementReferenceCounter();
62 }
63 }
64
65 for (const auto &it : depotLockerMap) {
66 it.second->removeInbox(inbox);
67 it.second->decrementReferenceCounter();
68 }
69
70 inbox->decrementReferenceCounter();
71
72 setWriteItem(nullptr);
73 setEditHouse(nullptr);
74}
75
76bool Player::setVocation(uint16_t vocId) {
77 Vocation *voc = g_vocations.getVocation(vocId);
78 if (!voc) {
79 return false;
80 }
81 vocation = voc;
82
83 Condition *condition =
84 getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT);
85 if (condition) {
86 condition->setParam(CONDITION_PARAM_HEALTHGAIN,
87 vocation->getHealthGainAmount());
88 condition->setParam(CONDITION_PARAM_HEALTHTICKS,
89 vocation->getHealthGainTicks() * 1000);
90 condition->setParam(CONDITION_PARAM_MANAGAIN,
91 vocation->getManaGainAmount());
92 condition->setParam(CONDITION_PARAM_MANATICKS,
93 vocation->getManaGainTicks() * 1000);
94 }
95 return true;
96}
97
98bool Player::isPushable() const {
99 if (hasFlag(PlayerFlag_CannotBePushed)) {
100 return false;
101 }
102 return Creature::isPushable();
103}
104
105std::string Player::getDescription(int32_t lookDistance) const {
106 std::ostringstream s;
107
108 if (lookDistance == -1) {
109 s << "yourself.";
110
111 if (group->access) {
112 s << " You are " << group->name << '.';
113 } else if (vocation->getId() != VOCATION_NONE) {
114 s << " You are " << vocation->getVocDescription() << '.';
115 } else {
116 s << " You have no vocation.";
117 }
118 } else {
119 s << name;
120 if (!group->access) {
121 s << " (Level " << level << ')';
122 }
123 s << '.';
124
125 if (sex == PLAYERSEX_FEMALE) {
126 s << " She";
127 } else {
128 s << " He";
129 }
130
131 if (group->access) {
132 s << " is " << group->name << '.';
133 } else if (vocation->getId() != VOCATION_NONE) {
134 s << " is " << vocation->getVocDescription() << '.';
135 } else {
136 s << " has no vocation.";
137 }
138 }
139
140 if (party) {
141 if (lookDistance == -1) {
142 s << " Your party has ";
143 } else if (sex == PLAYERSEX_FEMALE) {
144 s << " She is in a party with ";
145 } else {
146 s << " He is in a party with ";
147 }
148
149 size_t memberCount = party->getMemberCount() + 1;
150 if (memberCount == 1) {
151 s << "1 member and ";
152 } else {
153 s << memberCount << " members and ";
154 }
155
156 size_t invitationCount = party->getInvitationCount();
157 if (invitationCount == 1) {
158 s << "1 pending invitation.";
159 } else {
160 s << invitationCount << " pending invitations.";
161 }
162 }
163
164 if (!guild || !guildRank) {
165 return s.str();
166 }
167
168 if (lookDistance == -1) {
169 s << " You are ";
170 } else if (sex == PLAYERSEX_FEMALE) {
171 s << " She is ";
172 } else {
173 s << " He is ";
174 }
175
176 s << guildRank->name << " of the " << guild->getName();
177 if (!guildNick.empty()) {
178 s << " (" << guildNick << ')';
179 }
180
181 size_t memberCount = guild->getMemberCount();
182 if (memberCount == 1) {
183 s << ", which has 1 member, " << guild->getMembersOnline().size()
184 << " of them online.";
185 } else {
186 s << ", which has " << memberCount << " members, "
187 << guild->getMembersOnline().size() << " of them online.";
188 }
189 return s.str();
190}
191
192Item *Player::getInventoryItem(slots_t slot) const {
193 if (slot < CONST_SLOT_FIRST || slot > CONST_SLOT_LAST) {
194 return nullptr;
195 }
196 return inventory[slot];
197}
198
199void Player::addConditionSuppressions(uint32_t conditions) {
200 conditionSuppressions |= conditions;
201}
202
203void Player::removeConditionSuppressions(uint32_t conditions) {
204 conditionSuppressions &= ~conditions;
205}
206
207Item *Player::getWeapon(slots_t slot, bool ignoreAmmo) const {
208 Item *item = inventory[slot];
209 if (!item) {
210 return nullptr;
211 }
212
213 WeaponType_t weaponType = item->getWeaponType();
214 if (weaponType == WEAPON_NONE || weaponType == WEAPON_SHIELD ||
215 weaponType == WEAPON_AMMO) {
216 return nullptr;
217 }
218
219 if (!ignoreAmmo && weaponType == WEAPON_DISTANCE) {
220 const ItemType &it = Item::items[item->getID()];
221 if (it.ammoType != AMMO_NONE) {
222 Item *ammoItem = inventory[CONST_SLOT_AMMO];
223 if (!ammoItem || ammoItem->getAmmoType() != it.ammoType) {
224 return nullptr;
225 }
226 item = ammoItem;
227 }
228 }
229 return item;
230}
231
232Item *Player::getWeapon(bool ignoreAmmo /* = false*/) const {
233 Item *item = getWeapon(CONST_SLOT_LEFT, ignoreAmmo);
234 if (item) {
235 return item;
236 }
237
238 item = getWeapon(CONST_SLOT_RIGHT, ignoreAmmo);
239 if (item) {
240 return item;
241 }
242 return nullptr;
243}
244
245WeaponType_t Player::getWeaponType() const {
246 Item *item = getWeapon();
247 if (!item) {
248 return WEAPON_NONE;
249 }
250 return item->getWeaponType();
251}
252
253int32_t Player::getWeaponSkill(const Item *item) const {
254 if (!item) {
255 return getSkillLevel(SKILL_FIST);
256 }
257
258 int32_t attackSkill;
259
260 WeaponType_t weaponType = item->getWeaponType();
261 switch (weaponType) {
262 case WEAPON_SWORD: {
263 attackSkill = getSkillLevel(SKILL_SWORD);
264 break;
265 }
266
267 case WEAPON_CLUB: {
268 attackSkill = getSkillLevel(SKILL_CLUB);
269 break;
270 }
271
272 case WEAPON_AXE: {
273 attackSkill = getSkillLevel(SKILL_AXE);
274 break;
275 }
276
277 case WEAPON_DISTANCE: {
278 attackSkill = getSkillLevel(SKILL_DISTANCE);
279 break;
280 }
281
282 default: {
283 attackSkill = 0;
284 break;
285 }
286 }
287 return attackSkill;
288}
289
290int32_t Player::getArmor() const {
291 int32_t armor = 0;
292
293 static const slots_t armorSlots[] = {CONST_SLOT_HEAD, CONST_SLOT_NECKLACE,
294 CONST_SLOT_ARMOR, CONST_SLOT_LEGS,
295 CONST_SLOT_FEET, CONST_SLOT_RING};
296 for (slots_t slot : armorSlots) {
297 Item *inventoryItem = inventory[slot];
298 if (inventoryItem) {
299 armor += inventoryItem->getArmor();
300 }
301 }
302 return static_cast<int32_t>(armor * vocation->armorMultiplier);
303}
304
305void Player::getShieldAndWeapon(const Item *&shield,
306 const Item *&weapon) const {
307 shield = nullptr;
308 weapon = nullptr;
309
310 for (uint32_t slot = CONST_SLOT_RIGHT; slot <= CONST_SLOT_LEFT; slot++) {
311 Item *item = inventory[slot];
312 if (!item) {
313 continue;
314 }
315
316 switch (item->getWeaponType()) {
317 case WEAPON_NONE:
318 break;
319
320 case WEAPON_SHIELD: {
321 if (!shield || item->getDefense() > shield->getDefense()) {
322 shield = item;
323 }
324 break;
325 }
326
327 default: { // weapons that are not shields
328 weapon = item;
329 break;
330 }
331 }
332 }
333}
334
335int32_t Player::getDefense() const {
336 int32_t defenseSkill = getSkillLevel(SKILL_FIST);
337 int32_t defenseValue = 7;
338 const Item *weapon;
339 const Item *shield;
340 getShieldAndWeapon(shield, weapon);
341
342 if (weapon) {
343 defenseValue = weapon->getDefense() + weapon->getExtraDefense();
344 defenseSkill = getWeaponSkill(weapon);
345 }
346
347 if (shield) {
348 defenseValue = weapon != nullptr
349 ? shield->getDefense() + weapon->getExtraDefense()
350 : shield->getDefense();
351 defenseSkill = getSkillLevel(SKILL_SHIELD);
352 }
353
354 if (defenseSkill == 0) {
355 switch (fightMode) {
356 case FIGHTMODE_ATTACK:
357 case FIGHTMODE_BALANCED:
358 return 1;
359
360 case FIGHTMODE_DEFENSE:
361 return 2;
362 }
363 }
364
365 return (defenseSkill / 4. + 2.23) * defenseValue * 0.15 * getDefenseFactor() *
366 vocation->defenseMultiplier;
367}
368
369float Player::getAttackFactor() const {
370 switch (fightMode) {
371 case FIGHTMODE_ATTACK:
372 return 1.0f;
373 case FIGHTMODE_BALANCED:
374 return 1.2f;
375 case FIGHTMODE_DEFENSE:
376 return 2.0f;
377 default:
378 return 1.0f;
379 }
380}
381
382float Player::getDefenseFactor() const {
383 switch (fightMode) {
384 case FIGHTMODE_ATTACK:
385 return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.5f : 1.0f;
386 case FIGHTMODE_BALANCED:
387 return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.75f : 1.0f;
388 case FIGHTMODE_DEFENSE:
389 return 1.0f;
390 default:
391 return 1.0f;
392 }
393}
394
395uint16_t Player::getClientIcons() const {
396 uint16_t icons = 0;
397 for (Condition *condition : conditions) {
398 if (!isSuppress(condition->getType())) {
399 icons |= condition->getIcons();
400 }
401 }
402
403 if (pzLocked) {
404 icons |= ICON_REDSWORDS;
405 }
406
407 if (tile->hasFlag(TILESTATE_PROTECTIONZONE)) {
408 icons |= ICON_PIGEON;
409
410 // Don't show ICON_SWORDS if player is in protection zone.
411 if (hasBitSet(ICON_SWORDS, icons)) {
412 icons &= ~ICON_SWORDS;
413 }
414 }
415
416 // Game client debugs with 10 or more icons
417 // so let's prevent that from happening.
418 std::bitset<20> icon_bitset(static_cast<uint64_t>(icons));
419 for (size_t pos = 0, bits_set = icon_bitset.count(); bits_set >= 10; ++pos) {
420 if (icon_bitset[pos]) {
421 icon_bitset.reset(pos);
422 --bits_set;
423 }
424 }
425 return icon_bitset.to_ulong();
426}
427
428void Player::updateInventoryWeight() {
429 if (hasFlag(PlayerFlag_HasInfiniteCapacity)) {
430 return;
431 }
432
433 inventoryWeight = 0;
434 for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
435 const Item *item = inventory[i];
436 if (item) {
437 inventoryWeight += item->getWeight();
438 }
439 }
440}
441
442void Player::addSkillAdvance(skills_t skill, uint64_t count) {
443 uint64_t currReqTries =
444 vocation->getReqSkillTries(skill, skills[skill].level);
445 uint64_t nextReqTries =
446 vocation->getReqSkillTries(skill, skills[skill].level + 1);
447 if (currReqTries >= nextReqTries) {
448 // player has reached max skill
449 return;
450 }
451
452 g_events->eventPlayerOnGainSkillTries(this, skill, count);
453 if (count == 0) {
454 return;
455 }
456
457 bool sendUpdateSkills = false;
458 while ((skills[skill].tries + count) >= nextReqTries) {
459 count -= nextReqTries - skills[skill].tries;
460 skills[skill].level++;
461 skills[skill].tries = 0;
462 skills[skill].percent = 0;
463
464 std::ostringstream ss;
465 // ss << "You advanced to " << getSkillName(skill) << " level " <<
466 // skills[skill].level << '.'; sendTextMessage(MESSAGE_EVENT_ADVANCE,
467 // ss.str());
468
469 g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1),
470 skills[skill].level);
471
472 sendUpdateSkills = true;
473 currReqTries = nextReqTries;
474 nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1);
475 if (currReqTries >= nextReqTries) {
476 count = 0;
477 break;
478 }
479 }
480
481 skills[skill].tries += count;
482
483 uint32_t newPercent;
484 if (nextReqTries > currReqTries) {
485 newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries);
486 } else {
487 newPercent = 0;
488 }
489
490 if (skills[skill].percent != newPercent) {
491 skills[skill].percent = newPercent;
492 sendUpdateSkills = true;
493 }
494
495 if (sendUpdateSkills) {
496 sendSkills();
497 }
498}
499
500void Player::setVarStats(stats_t stat, int32_t modifier) {
501 varStats[stat] += modifier;
502
503 switch (stat) {
504 case STAT_MAXHITPOINTS: {
505 if (getHealth() > getMaxHealth()) {
506 Creature::changeHealth(getMaxHealth() - getHealth());
507 } else {
508 g_game.addCreatureHealth(this);
509 }
510 break;
511 }
512
513 case STAT_MAXMANAPOINTS: {
514 if (getMana() > getMaxMana()) {
515 changeMana(getMaxMana() - getMana());
516 }
517 break;
518 }
519
520 default: {
521 break;
522 }
523 }
524}
525
526int32_t Player::getDefaultStats(stats_t stat) const {
527 switch (stat) {
528 case STAT_MAXHITPOINTS:
529 return healthMax;
530 case STAT_MAXMANAPOINTS:
531 return manaMax;
532 case STAT_MAGICPOINTS:
533 return getBaseMagicLevel();
534 default:
535 return 0;
536 }
537}
538
539void Player::addContainer(uint8_t cid, Container *container) {
540 if (cid > 0xF) {
541 return;
542 }
543
544 if (container->getID() == ITEM_BROWSEFIELD) {
545 container->incrementReferenceCounter();
546 }
547
548 auto it = openContainers.find(cid);
549 if (it != openContainers.end()) {
550 OpenContainer &openContainer = it->second;
551 Container *oldContainer = openContainer.container;
552 if (oldContainer->getID() == ITEM_BROWSEFIELD) {
553 oldContainer->decrementReferenceCounter();
554 }
555
556 openContainer.container = container;
557 openContainer.index = 0;
558 } else {
559 OpenContainer openContainer;
560 openContainer.container = container;
561 openContainer.index = 0;
562 openContainers[cid] = openContainer;
563 }
564}
565
566void Player::closeContainer(uint8_t cid) {
567 auto it = openContainers.find(cid);
568 if (it == openContainers.end()) {
569 return;
570 }
571
572 OpenContainer openContainer = it->second;
573 Container *container = openContainer.container;
574 openContainers.erase(it);
575
576 if (container && container->getID() == ITEM_BROWSEFIELD) {
577 container->decrementReferenceCounter();
578 }
579}
580
581void Player::setContainerIndex(uint8_t cid, uint16_t index) {
582 auto it = openContainers.find(cid);
583 if (it == openContainers.end()) {
584 return;
585 }
586 it->second.index = index;
587}
588
589Container *Player::getContainerByID(uint8_t cid) {
590 auto it = openContainers.find(cid);
591 if (it == openContainers.end()) {
592 return nullptr;
593 }
594 return it->second.container;
595}
596
597int8_t Player::getContainerID(const Container *container) const {
598 for (const auto &it : openContainers) {
599 if (it.second.container == container) {
600 return it.first;
601 }
602 }
603 return -1;
604}
605
606uint16_t Player::getContainerIndex(uint8_t cid) const {
607 auto it = openContainers.find(cid);
608 if (it == openContainers.end()) {
609 return 0;
610 }
611 return it->second.index;
612}
613
614bool Player::canOpenCorpse(uint32_t ownerId) const {
615 return getID() == ownerId || (party && party->canOpenCorpse(ownerId));
616}
617
618uint16_t Player::getLookCorpse() const {
619 if (sex == PLAYERSEX_FEMALE) {
620 return ITEM_FEMALE_CORPSE;
621 } else {
622 return ITEM_MALE_CORPSE;
623 }
624}
625
626void Player::addStorageValue(const uint32_t key, const int32_t value,
627 const bool isLogin /* = false*/) {
628 if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) {
629 if (IS_IN_KEYRANGE(key, OUTFITS_RANGE)) {
630 outfits.emplace_back(value >> 16, value & 0xFF);
631 return;
632 } else if (IS_IN_KEYRANGE(key, MOUNTS_RANGE) ||
633 IS_IN_KEYRANGE(key, WINGS_RANGE) ||
634 IS_IN_KEYRANGE(key, AURAS_RANGE) ||
635 IS_IN_KEYRANGE(key, SHADERS_RANGE) ||
636 IS_IN_KEYRANGE(key, EFFECTS_RANGE)) {
637 // do nothing
638 } else {
639 std::cout << "Warning: unknown reserved key: " << key
640 << " player: " << getName() << std::endl;
641 return;
642 }
643 }
644
645 if (value != -1) {
646 int32_t oldValue;
647 getStorageValue(key, oldValue);
648
649 storageMap[key] = value;
650
651 if (!isLogin) {
652 auto currentFrameTime = g_dispatcher.getDispatcherCycle();
653 if (lastQuestlogUpdate != currentFrameTime &&
654 g_game.quests.isQuestStorage(key, value, oldValue)) {
655 lastQuestlogUpdate = currentFrameTime;
656 // sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your questlog has been
657 // updated.");
658 }
659 }
660 } else {
661 storageMap.erase(key);
662 }
663}
664
665bool Player::getStorageValue(const uint32_t key, int32_t &value) const {
666 auto it = storageMap.find(key);
667 if (it == storageMap.end()) {
668 value = -1;
669 return false;
670 }
671
672 value = it->second;
673 return true;
674}
675
676bool Player::canSee(const Position &pos) const {
677 if (!client) {
678 return false;
679 }
680 return client->canSee(pos);
681}
682
683bool Player::canSeeCreature(const Creature *creature) const {
684 if (creature == this) {
685 return true;
686 }
687
688 if (creature->isInGhostMode() && !group->access) {
689 return false;
690 }
691
692 if (!creature->getPlayer() && !canSeeInvisibility() &&
693 creature->isInvisible()) {
694 return false;
695 }
696 return true;
697}
698
699bool Player::canWalkthrough(const Creature *creature) const {
700 if (group->access || creature->isInGhostMode()) {
701 return true;
702 }
703
704 const Player *player = creature->getPlayer();
705 if (!player) {
706 return false;
707 }
708
709 const Tile *playerTile = player->getTile();
710 if (!playerTile ||
711 (!playerTile->hasFlag(TILESTATE_PROTECTIONZONE) &&
712 player->getLevel() > static_cast<uint32_t>(g_config.getNumber(
713 ConfigManager::PROTECTION_LEVEL)))) {
714 return false;
715 }
716
717 const Item *playerTileGround = playerTile->getGround();
718 if (!playerTileGround || !playerTileGround->hasWalkStack()) {
719 return false;
720 }
721
722 Player *thisPlayer = const_cast<Player *>(this);
723 if ((OTSYS_TIME() - lastWalkthroughAttempt) > 2000) {
724 thisPlayer->setLastWalkthroughAttempt(OTSYS_TIME());
725 return false;
726 }
727
728 if (creature->getPosition() != lastWalkthroughPosition) {
729 thisPlayer->setLastWalkthroughPosition(creature->getPosition());
730 return false;
731 }
732
733 thisPlayer->setLastWalkthroughPosition(creature->getPosition());
734 return true;
735}
736
737bool Player::canWalkthroughEx(const Creature *creature) const {
738 if (group->access) {
739 return true;
740 }
741
742 const Player *player = creature->getPlayer();
743 if (!player) {
744 return false;
745 }
746
747 const Tile *playerTile = player->getTile();
748 return playerTile &&
749 (playerTile->hasFlag(TILESTATE_PROTECTIONZONE) ||
750 player->getLevel() <= static_cast<uint32_t>(g_config.getNumber(
751 ConfigManager::PROTECTION_LEVEL)));
752}
753
754void Player::onReceiveMail() const {
755 if (isNearDepotBox()) {
756 sendTextMessage(MESSAGE_EVENT_ADVANCE, "New mail has arrived.");
757 }
758}
759
760bool Player::isNearDepotBox() const {
761 const Position &pos = getPosition();
762 for (int32_t cx = -1; cx <= 1; ++cx) {
763 for (int32_t cy = -1; cy <= 1; ++cy) {
764 Tile *tile = g_game.map.getTile(pos.x + cx, pos.y + cy, pos.z);
765 if (!tile) {
766 continue;
767 }
768
769 if (tile->hasFlag(TILESTATE_DEPOT)) {
770 return true;
771 }
772 }
773 }
774 return false;
775}
776
777DepotChest *Player::getDepotChest(uint32_t depotId, bool autoCreate) {
778 auto it = depotChests.find(depotId);
779 if (it != depotChests.end()) {
780 return it->second;
781 }
782
783 if (!autoCreate) {
784 return nullptr;
785 }
786
787 DepotChest *depotChest = new DepotChest(ITEM_DEPOT);
788 depotChest->incrementReferenceCounter();
789 depotChest->setMaxDepotItems(getMaxDepotItems());
790 depotChests[depotId] = depotChest;
791 return depotChest;
792}
793
794DepotLocker *Player::getDepotLocker(uint32_t depotId) {
795 auto it = depotLockerMap.find(depotId);
796 if (it != depotLockerMap.end()) {
797 inbox->setParent(it->second);
798 return it->second;
799 }
800
801 DepotLocker *depotLocker = new DepotLocker(ITEM_LOCKER1);
802 depotLocker->setDepotId(depotId);
803 depotLocker->internalAddThing(Item::CreateItem(ITEM_MARKET));
804 depotLocker->internalAddThing(inbox);
805 depotLocker->internalAddThing(getDepotChest(depotId, true));
806 depotLockerMap[depotId] = depotLocker;
807 return depotLocker;
808}
809
810void Player::sendCancelMessage(ReturnValue message) const {
811 sendCancelMessage(getReturnMessage(message));
812}
813
814void Player::sendStats() {
815 if (client) {
816 client->sendStats();
817 lastStatsTrainingTime = getOfflineTrainingTime() / 60 / 1000;
818 }
819}
820
821void Player::sendPing() {
822 int64_t timeNow = OTSYS_TIME();
823
824 bool hasLostConnection = false;
825 if ((timeNow - lastPing) >= 5000) {
826 lastPing = timeNow;
827 if (client) {
828 client->sendPing();
829 } else {
830 hasLostConnection = true;
831 }
832 }
833
834 int64_t noPongTime = timeNow - lastPong;
835 if ((hasLostConnection || noPongTime >= 7000) && attackedCreature &&
836 attackedCreature->getPlayer()) {
837 setAttackedCreature(nullptr);
838 }
839
840 if (noPongTime >= 60000 && canLogout()) {
841 if (g_creatureEvents->playerLogout(this)) {
842 if (client) {
843 client->logout(true, true);
844 } else {
845 g_game.removeCreature(this, true);
846 }
847 }
848 }
849}
850
851Item *Player::getWriteItem(uint32_t &windowTextId, uint16_t &maxWriteLen) {
852 windowTextId = this->windowTextId;
853 maxWriteLen = this->maxWriteLen;
854 return writeItem;
855}
856
857void Player::setWriteItem(Item *item, uint16_t maxWriteLen /*= 0*/) {
858 windowTextId++;
859
860 if (writeItem) {
861 writeItem->decrementReferenceCounter();
862 }
863
864 if (item) {
865 writeItem = item;
866 this->maxWriteLen = maxWriteLen;
867 writeItem->incrementReferenceCounter();
868 } else {
869 writeItem = nullptr;
870 this->maxWriteLen = 0;
871 }
872}
873
874House *Player::getEditHouse(uint32_t &windowTextId, uint32_t &listId) {
875 windowTextId = this->windowTextId;
876 listId = this->editListId;
877 return editHouse;
878}
879
880void Player::setEditHouse(House *house, uint32_t listId /*= 0*/) {
881 windowTextId++;
882 editHouse = house;
883 editListId = listId;
884}
885
886void Player::sendHouseWindow(House *house, uint32_t listId) const {
887 if (!client) {
888 return;
889 }
890
891 std::string text;
892 if (house->getAccessList(listId, text)) {
893 client->sendHouseWindow(windowTextId, text);
894 }
895}
896
897// container
898void Player::sendAddContainerItem(const Container *container,
899 const Item *item) {
900 if (!client) {
901 return;
902 }
903
904 for (const auto &it : openContainers) {
905 const OpenContainer &openContainer = it.second;
906 if (openContainer.container != container) {
907 continue;
908 }
909
910 uint16_t slot = openContainer.index;
911 if (container->getID() == ITEM_BROWSEFIELD) {
912 uint16_t containerSize = container->size() - 1;
913 uint16_t pageEnd = openContainer.index + container->capacity() - 1;
914 if (containerSize > pageEnd) {
915 slot = pageEnd;
916 item = container->getItemByIndex(pageEnd);
917 } else {
918 slot = containerSize;
919 }
920 } else if (openContainer.index >= container->capacity()) {
921 item = container->getItemByIndex(openContainer.index - 1);
922 }
923 client->sendAddContainerItem(it.first, slot, item);
924 }
925}
926
927void Player::sendUpdateContainerItem(const Container *container, uint16_t slot,
928 const Item *newItem) {
929 if (!client) {
930 return;
931 }
932
933 for (const auto &it : openContainers) {
934 const OpenContainer &openContainer = it.second;
935 if (openContainer.container != container) {
936 continue;
937 }
938
939 if (slot < openContainer.index) {
940 continue;
941 }
942
943 uint16_t pageEnd = openContainer.index + container->capacity();
944 if (slot >= pageEnd) {
945 continue;
946 }
947
948 client->sendUpdateContainerItem(it.first, slot, newItem);
949 }
950}
951
952void Player::sendRemoveContainerItem(const Container *container,
953 uint16_t slot) {
954 if (!client) {
955 return;
956 }
957
958 for (auto &it : openContainers) {
959 OpenContainer &openContainer = it.second;
960 if (openContainer.container != container) {
961 continue;
962 }
963
964 uint16_t &firstIndex = openContainer.index;
965 if (firstIndex > 0 && firstIndex >= container->size() - 1) {
966 firstIndex -= container->capacity();
967 sendContainer(it.first, container, false, firstIndex);
968 }
969
970 client->sendRemoveContainerItem(
971 it.first, std::max<uint16_t>(slot, firstIndex),
972 container->getItemByIndex(container->capacity() + firstIndex));
973 }
974}
975
976void Player::onUpdateTileItem(const Tile *tile, const Position &pos,
977 const Item *oldItem, const ItemType &oldType,
978 const Item *newItem, const ItemType &newType) {
979 Creature::onUpdateTileItem(tile, pos, oldItem, oldType, newItem, newType);
980
981 if (oldItem != newItem) {
982 onRemoveTileItem(tile, pos, oldType, oldItem);
983 }
984
985 if (tradeState != TRADE_TRANSFER) {
986 if (tradeItem && oldItem == tradeItem) {
987 g_game.internalCloseTrade(this);
988 }
989 }
990}
991
992void Player::onRemoveTileItem(const Tile *tile, const Position &pos,
993 const ItemType &iType, const Item *item) {
994 Creature::onRemoveTileItem(tile, pos, iType, item);
995
996 if (tradeState != TRADE_TRANSFER) {
997 checkTradeState(item);
998
999 if (tradeItem) {
1000 const Container *container = item->getContainer();
1001 if (container && container->isHoldingItem(tradeItem)) {
1002 g_game.internalCloseTrade(this);
1003 }
1004 }
1005 }
1006}
1007
1008void Player::onCreatureAppear(Creature *creature, bool isLogin) {
1009 Creature::onCreatureAppear(creature, isLogin);
1010
1011 if (isLogin && creature == this) {
1012 sendItems();
1013
1014 for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) {
1015 Item *item = inventory[slot];
1016 if (item) {
1017 item->startDecaying();
1018 g_moveEvents->onPlayerEquip(this, item, static_cast<slots_t>(slot),
1019 false);
1020 }
1021 }
1022
1023 for (Condition *condition : storedConditionList) {
1024 addCondition(condition);
1025 }
1026 storedConditionList.clear();
1027
1028 BedItem *bed = g_game.getBedBySleeper(guid);
1029 if (bed) {
1030 bed->wakeUp(this);
1031 }
1032
1033 Account account = IOLoginData::loadAccount(accountNumber);
1034 Game::updatePremium(account);
1035
1036 std::cout << name << " has logged in." << std::endl;
1037
1038 if (guild) {
1039 guild->addMember(this);
1040 }
1041
1042 int32_t offlineTime;
1043 if (getLastLogout() != 0) {
1044 // Not counting more than 21 days to prevent overflow when multiplying
1045 // with 1000 (for milliseconds).
1046 offlineTime =
1047 std::min<int32_t>(time(nullptr) - getLastLogout(), 86400 * 21);
1048 } else {
1049 offlineTime = 0;
1050 }
1051
1052 for (Condition *condition : getMuteConditions()) {
1053 condition->setTicks(condition->getTicks() - (offlineTime * 1000));
1054 if (condition->getTicks() <= 0) {
1055 removeCondition(condition);
1056 }
1057 }
1058
1059 g_game.checkPlayersRecord();
1060 IOLoginData::updateOnlineStatus(guid, true);
1061 }
1062}
1063
1064void Player::onAttackedCreatureDisappear(bool isLogout) {
1065 sendCancelTarget();
1066
1067 if (!isLogout) {
1068 sendTextMessage(MESSAGE_STATUS_SMALL, "Target lost.");
1069 }
1070}
1071
1072void Player::onFollowCreatureDisappear(bool isLogout) {
1073 sendCancelTarget();
1074
1075 if (!isLogout) {
1076 sendTextMessage(MESSAGE_STATUS_SMALL, "Target lost.");
1077 }
1078}
1079
1080void Player::onChangeZone(ZoneType_t zone) {
1081 if (zone == ZONE_PROTECTION) {
1082 if (attackedCreature && !hasFlag(PlayerFlag_IgnoreProtectionZone)) {
1083 setAttackedCreature(nullptr);
1084 onAttackedCreatureDisappear(false);
1085 }
1086
1087 if (!group->access && isMounted()) {
1088 dismount();
1089 g_game.internalCreatureChangeOutfit(this, defaultOutfit);
1090 wasMounted = true;
1091 }
1092 } else {
1093 if (wasMounted) {
1094 toggleMount(true);
1095 wasMounted = false;
1096 }
1097 }
1098
1099 g_game.updateCreatureWalkthrough(this);
1100 sendIcons();
1101}
1102
1103void Player::onAttackedCreatureChangeZone(ZoneType_t zone) {
1104 if (zone == ZONE_PROTECTION) {
1105 if (!hasFlag(PlayerFlag_IgnoreProtectionZone)) {
1106 setAttackedCreature(nullptr);
1107 onAttackedCreatureDisappear(false);
1108 }
1109 } else if (zone == ZONE_NOPVP) {
1110 if (attackedCreature->getPlayer()) {
1111 if (!hasFlag(PlayerFlag_IgnoreProtectionZone)) {
1112 setAttackedCreature(nullptr);
1113 onAttackedCreatureDisappear(false);
1114 }
1115 }
1116 } else if (zone == ZONE_NORMAL) {
1117 // attackedCreature can leave a pvp zone if not pzlocked
1118 if (g_game.getWorldType() == WORLD_TYPE_NO_PVP) {
1119 if (attackedCreature->getPlayer()) {
1120 setAttackedCreature(nullptr);
1121 onAttackedCreatureDisappear(false);
1122 }
1123 }
1124 }
1125}
1126
1127void Player::onRemoveCreature(Creature *creature, bool isLogout) {
1128 Creature::onRemoveCreature(creature, isLogout);
1129
1130 if (creature == this) {
1131 if (isLogout) {
1132 loginPosition = getPosition();
1133 }
1134
1135 lastLogout = time(nullptr);
1136
1137 if (eventWalk != 0) {
1138 setFollowCreature(nullptr);
1139 }
1140
1141 if (tradePartner) {
1142 g_game.internalCloseTrade(this);
1143 }
1144
1145 closeShopWindow();
1146
1147 clearPartyInvitations();
1148
1149 if (party) {
1150 party->leaveParty(this);
1151 }
1152
1153 g_chat->removeUserFromAllChannels(*this);
1154
1155 std::cout << getName() << " has logged out." << std::endl;
1156
1157 if (guild) {
1158 guild->removeMember(this);
1159 }
1160
1161 IOLoginData::updateOnlineStatus(guid, false);
1162
1163 bool saved = false;
1164 for (uint32_t tries = 0; tries < 3; ++tries) {
1165 if (IOLoginData::savePlayer(this)) {
1166 saved = true;
1167 break;
1168 }
1169 }
1170
1171 if (!saved) {
1172 std::cout << "Error while saving player: " << getName() << std::endl;
1173 }
1174 }
1175}
1176
1177void Player::openShopWindow(Npc *npc, const std::list<ShopInfo> &shop) {
1178 shopItemList = shop;
1179 sendShop(npc);
1180 sendSaleItemList();
1181}
1182
1183bool Player::closeShopWindow(bool sendCloseShopWindow /*= true*/) {
1184 // unreference callbacks
1185 int32_t onBuy;
1186 int32_t onSell;
1187
1188 Npc *npc = getShopOwner(onBuy, onSell);
1189 if (!npc) {
1190 shopItemList.clear();
1191 return false;
1192 }
1193
1194 setShopOwner(nullptr, -1, -1);
1195 npc->onPlayerEndTrade(this, onBuy, onSell);
1196
1197 if (sendCloseShopWindow) {
1198 sendCloseShop();
1199 }
1200
1201 shopItemList.clear();
1202 return true;
1203}
1204
1205void Player::onWalk(Direction &dir) {
1206 Creature::onWalk(dir);
1207 setNextActionTask(nullptr);
1208 setNextAction(OTSYS_TIME() + getStepDuration(dir));
1209}
1210
1211void Player::onCreatureMove(Creature *creature, const Tile *newTile,
1212 const Position &newPos, const Tile *oldTile,
1213 const Position &oldPos, bool teleport) {
1214 Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos,
1215 teleport);
1216
1217 if (hasFollowPath &&
1218 (creature == followCreature || (creature == this && followCreature))) {
1219 isUpdatingPath = false;
1220 g_dispatcher.addTask(
1221 createTask(std::bind(&Game::updateCreatureWalk, &g_game, getID())));
1222 }
1223
1224 if (creature != this) {
1225 return;
1226 }
1227
1228 if (tradeState != TRADE_TRANSFER) {
1229 // check if we should close trade
1230 if (tradeItem && !Position::areInRange<1, 1, 0>(tradeItem->getPosition(),
1231 getPosition())) {
1232 g_game.internalCloseTrade(this);
1233 }
1234
1235 if (tradePartner && !Position::areInRange<2, 2, 0>(
1236 tradePartner->getPosition(), getPosition())) {
1237 g_game.internalCloseTrade(this);
1238 }
1239 }
1240
1241 // close modal windows
1242 if (!modalWindows.empty()) {
1243 // TODO: This shouldn't be hardcoded
1244 for (uint32_t modalWindowId : modalWindows) {
1245 if (modalWindowId == std::numeric_limits<uint32_t>::max()) {
1246 sendTextMessage(MESSAGE_EVENT_ADVANCE, "Offline training aborted.");
1247 break;
1248 }
1249 }
1250 modalWindows.clear();
1251 }
1252
1253 // leave market
1254 if (inMarket) {
1255 inMarket = false;
1256 }
1257
1258 if (party) {
1259 party->updateSharedExperience();
1260 }
1261
1262 if (teleport || oldPos.z != newPos.z) {
1263 int32_t ticks = g_config.getNumber(ConfigManager::STAIRHOP_DELAY);
1264 if (ticks > 0) {
1265 if (Condition *condition = Condition::createCondition(
1266 CONDITIONID_DEFAULT, CONDITION_PACIFIED, ticks, 0)) {
1267 addCondition(condition);
1268 }
1269 }
1270 }
1271}
1272
1273// container
1274void Player::onAddContainerItem(const Item *item) { checkTradeState(item); }
1275
1276void Player::onUpdateContainerItem(const Container *container,
1277 const Item *oldItem, const Item *newItem) {
1278 if (oldItem != newItem) {
1279 onRemoveContainerItem(container, oldItem);
1280 }
1281
1282 if (tradeState != TRADE_TRANSFER) {
1283 checkTradeState(oldItem);
1284 }
1285}
1286
1287void Player::onRemoveContainerItem(const Container *container,
1288 const Item *item) {
1289 if (tradeState != TRADE_TRANSFER) {
1290 checkTradeState(item);
1291
1292 if (tradeItem) {
1293 if (tradeItem->getParent() != container &&
1294 container->isHoldingItem(tradeItem)) {
1295 g_game.internalCloseTrade(this);
1296 }
1297 }
1298 }
1299}
1300
1301void Player::onCloseContainer(const Container *container) {
1302 if (!client) {
1303 return;
1304 }
1305
1306 for (const auto &it : openContainers) {
1307 if (it.second.container == container) {
1308 client->sendCloseContainer(it.first);
1309 }
1310 }
1311}
1312
1313void Player::onSendContainer(const Container *container) {
1314 if (!client) {
1315 return;
1316 }
1317
1318 bool hasParent = container->hasParent();
1319 for (const auto &it : openContainers) {
1320 const OpenContainer &openContainer = it.second;
1321 if (openContainer.container == container) {
1322 client->sendContainer(it.first, container, hasParent,
1323 openContainer.index);
1324 }
1325 }
1326}
1327
1328// inventory
1329void Player::onUpdateInventoryItem(Item *oldItem, Item *newItem) {
1330 if (oldItem != newItem) {
1331 onRemoveInventoryItem(oldItem);
1332 }
1333
1334 if (tradeState != TRADE_TRANSFER) {
1335 checkTradeState(oldItem);
1336 }
1337}
1338
1339void Player::onRemoveInventoryItem(Item *item) {
1340 if (tradeState != TRADE_TRANSFER) {
1341 checkTradeState(item);
1342
1343 if (tradeItem) {
1344 const Container *container = item->getContainer();
1345 if (container && container->isHoldingItem(tradeItem)) {
1346 g_game.internalCloseTrade(this);
1347 }
1348 }
1349 }
1350}
1351
1352void Player::checkTradeState(const Item *item) {
1353 if (!tradeItem || tradeState == TRADE_TRANSFER) {
1354 return;
1355 }
1356
1357 if (tradeItem == item) {
1358 g_game.internalCloseTrade(this);
1359 } else {
1360 const Container *container =
1361 dynamic_cast<const Container *>(item->getParent());
1362 while (container) {
1363 if (container == tradeItem) {
1364 g_game.internalCloseTrade(this);
1365 break;
1366 }
1367
1368 container = dynamic_cast<const Container *>(container->getParent());
1369 }
1370 }
1371}
1372
1373void Player::setNextWalkActionTask(SchedulerTask *task) {
1374 if (walkTaskEvent != 0) {
1375 g_scheduler.stopEvent(walkTaskEvent);
1376 walkTaskEvent = 0;
1377 }
1378
1379 delete walkTask;
1380 walkTask = task;
1381}
1382
1383void Player::setNextWalkTask(SchedulerTask *task) {
1384 if (nextStepEvent != 0) {
1385 g_scheduler.stopEvent(nextStepEvent);
1386 nextStepEvent = 0;
1387 }
1388
1389 if (task) {
1390 nextStepEvent = g_scheduler.addEvent(task);
1391 resetIdleTime();
1392 }
1393}
1394
1395void Player::setNextActionTask(SchedulerTask *task,
1396 bool resetIdleTime /*= true */) {
1397 if (actionTaskEvent != 0) {
1398 g_scheduler.stopEvent(actionTaskEvent);
1399 actionTaskEvent = 0;
1400 }
1401
1402 if (task) {
1403 actionTaskEvent = g_scheduler.addEvent(task);
1404 if (resetIdleTime) {
1405 this->resetIdleTime();
1406 }
1407 }
1408}
1409
1410uint32_t Player::getNextActionTime() const {
1411 return std::max<int64_t>(SCHEDULER_MINTICKS, nextAction - OTSYS_TIME());
1412}
1413
1414void Player::onThink(uint32_t interval) {
1415 Creature::onThink(interval);
1416
1417 sendPing();
1418
1419 MessageBufferTicks += interval;
1420 if (MessageBufferTicks >= 1500) {
1421 MessageBufferTicks = 0;
1422 addMessageBuffer();
1423 }
1424
1425 if (!getTile()->hasFlag(TILESTATE_NOLOGOUT) && !isAccessPlayer()) {
1426 idleTime += interval;
1427 const int32_t kickAfterMinutes =
1428 g_config.getNumber(ConfigManager::KICK_AFTER_MINUTES);
1429 if (idleTime > (kickAfterMinutes * 60000) + 60000) {
1430 kickPlayer(true);
1431 } else if (client && idleTime == 60000 * kickAfterMinutes) {
1432 std::ostringstream ss;
1433 ss << "There was no variation in your behaviour for " << kickAfterMinutes
1434 << " minutes. You will be disconnected in one minute if there is no "
1435 "change in your actions until then.";
1436 client->sendTextMessage(TextMessage(MESSAGE_STATUS_WARNING, ss.str()));
1437 }
1438 }
1439
1440 if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) {
1441 checkSkullTicks(interval / 1000);
1442 }
1443
1444 addOfflineTrainingTime(interval);
1445 if (lastStatsTrainingTime != getOfflineTrainingTime() / 60 / 1000) {
1446 sendStats();
1447 }
1448
1449 uint8_t runeEffectId = getCurrentRuneEffect();
1450 if (runeEffectId > 0) {
1451 RuneEffect *re = g_game.runeEffects.getRuneEffectByID(runeEffectId);
1452 if (re && re->bonusDamage > 0) {
1453 int64_t now = OTSYS_TIME();
1454 if (lastRuneDamageTick == 0) {
1455 lastRuneDamageTick = now;
1456 } else if (now - lastRuneDamageTick >= re->damageInterval) {
1457 lastRuneDamageTick = now;
1458
1459 SpectatorVec spectators;
1460 g_game.map.getSpectators(spectators, getPosition(), false, false, 1, 1, 1, 1);
1461
1462 std::vector<Creature *> targets;
1463 for (Creature *c : spectators) {
1464 if (c == this || c->getHealth() <= 0) continue;
1465 if (c->getPlayer()) continue;
1466 targets.push_back(c);
1467 }
1468
1469 if (!targets.empty()) {
1470 int32_t baseDamage = calculateRuneBaseDamage();
1471 if (baseDamage < 1) baseDamage = 1;
1472 uint32_t attackerId = getID();
1473
1474 for (size_t i = 0; i < targets.size(); ++i) {
1475 Creature *target = targets[i];
1476 int32_t delay = static_cast<int32_t>(i) * re->damageDelay;
1477 uint32_t targetId = target->getID();
1478
1479 g_scheduler.addEvent(createSchedulerTask(delay, [attackerId, targetId, baseDamage]() {
1480 Creature *attacker = g_game.getCreatureByID(attackerId);
1481 Creature *target = g_game.getCreatureByID(targetId);
1482 if (!attacker || !target) return;
1483 if (attacker->isRemoved() || attacker->getHealth() <= 0) return;
1484 if (target->isRemoved() || target->getHealth() <= 0) return;
1485
1486 CombatDamage damage;
1487 damage.primary.value = -baseDamage;
1488 damage.primary.type = COMBAT_PHYSICALDAMAGE;
1489 damage.origin = ORIGIN_SPELL;
1490 g_game.combatChangeHealth(attacker, target, damage);
1491 }));
1492 }
1493 }
1494 }
1495 }
1496 }
1497}
1498
1499uint32_t Player::isMuted() const {
1500 if (hasFlag(PlayerFlag_CannotBeMuted)) {
1501 return 0;
1502 }
1503
1504 int32_t muteTicks = 0;
1505 for (Condition *condition : conditions) {
1506 if (condition->getType() == CONDITION_MUTED &&
1507 condition->getTicks() > muteTicks) {
1508 muteTicks = condition->getTicks();
1509 }
1510 }
1511 return static_cast<uint32_t>(muteTicks) / 1000;
1512}
1513
1514void Player::addMessageBuffer() {
1515 if (MessageBufferCount > 0 &&
1516 g_config.getNumber(ConfigManager::MAX_MESSAGEBUFFER) != 0 &&
1517 !hasFlag(PlayerFlag_CannotBeMuted)) {
1518 --MessageBufferCount;
1519 }
1520}
1521
1522void Player::removeMessageBuffer() {
1523 if (hasFlag(PlayerFlag_CannotBeMuted)) {
1524 return;
1525 }
1526
1527 const int32_t maxMessageBuffer =
1528 g_config.getNumber(ConfigManager::MAX_MESSAGEBUFFER);
1529 if (maxMessageBuffer != 0 && MessageBufferCount <= maxMessageBuffer + 1) {
1530 if (++MessageBufferCount > maxMessageBuffer) {
1531 uint32_t muteCount = 1;
1532 auto it = muteCountMap.find(guid);
1533 if (it != muteCountMap.end()) {
1534 muteCount = it->second;
1535 }
1536
1537 uint32_t muteTime = 5 * muteCount * muteCount;
1538 muteCountMap[guid] = muteCount + 1;
1539 Condition *condition = Condition::createCondition(
1540 CONDITIONID_DEFAULT, CONDITION_MUTED, muteTime * 1000, 0);
1541 addCondition(condition);
1542
1543 std::ostringstream ss;
1544 ss << "You are muted for " << muteTime << " seconds.";
1545 sendTextMessage(MESSAGE_STATUS_SMALL, ss.str());
1546 }
1547 }
1548}
1549
1550void Player::drainHealth(Creature *attacker, int32_t damage) {
1551 Creature::drainHealth(attacker, damage);
1552 sendStats();
1553}
1554
1555void Player::drainMana(Creature *attacker, int32_t manaLoss) {
1556 onAttacked();
1557 changeMana(-manaLoss);
1558
1559 if (attacker) {
1560 addDamagePoints(attacker, manaLoss);
1561 }
1562
1563 sendStats();
1564}
1565
1566void Player::addManaSpent(uint64_t amount) {
1567 if (hasFlag(PlayerFlag_NotGainMana)) {
1568 return;
1569 }
1570
1571 uint64_t currReqMana = vocation->getReqMana(magLevel);
1572 uint64_t nextReqMana = vocation->getReqMana(magLevel + 1);
1573 if (currReqMana >= nextReqMana) {
1574 // player has reached max magic level
1575 return;
1576 }
1577
1578 g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, amount);
1579 if (amount == 0) {
1580 return;
1581 }
1582
1583 bool sendUpdateStats = false;
1584 while ((manaSpent + amount) >= nextReqMana) {
1585 amount -= nextReqMana - manaSpent;
1586
1587 magLevel++;
1588 manaSpent = 0;
1589
1590 std::ostringstream ss;
1591 // ss << "You advanced to magic level " << magLevel << '.';
1592 // sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
1593
1594 g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1,
1595 magLevel);
1596
1597 sendUpdateStats = true;
1598 currReqMana = nextReqMana;
1599 nextReqMana = vocation->getReqMana(magLevel + 1);
1600 if (currReqMana >= nextReqMana) {
1601 return;
1602 }
1603 }
1604
1605 manaSpent += amount;
1606
1607 uint8_t oldPercent = magLevelPercent;
1608 if (nextReqMana > currReqMana) {
1609 magLevelPercent = Player::getPercentLevel(manaSpent, nextReqMana);
1610 } else {
1611 magLevelPercent = 0;
1612 }
1613
1614 if (oldPercent != magLevelPercent) {
1615 sendUpdateStats = true;
1616 }
1617
1618 if (sendUpdateStats) {
1619 sendStats();
1620 }
1621}
1622
1623void Player::addExperience(Creature *source, uint64_t exp,
1624 bool sendText /* = false*/) {
1625 uint64_t currLevelExp = Player::getExpForLevel(level);
1626 uint64_t nextLevelExp = Player::getExpForLevel(level + 1);
1627 uint64_t rawExp = exp;
1628 if (currLevelExp >= nextLevelExp) {
1629 // player has reached max level
1630 levelPercent = 0;
1631 sendStats();
1632 return;
1633 }
1634
1635 g_events->eventPlayerOnGainExperience(this, source, exp, rawExp);
1636 if (exp == 0) {
1637 return;
1638 }
1639
1640 RuneEffect* activeEffect = g_game.runeEffects.getRuneEffectByID(getCurrentRuneEffect());
1641 if (activeEffect && activeEffect->bonusXp > 0) {
1642 exp += (exp * activeEffect->bonusXp) / 100;
1643 }
1644
1645 experience += exp;
1646
1647 if (sendText) {
1648 std::string expString =
1649 std::to_string(exp) +
1650 (exp != 1 ? " experience points." : " experience point.");
1651
1652 TextMessage message(MESSAGE_EXPERIENCE, "You gained " + expString);
1653 message.position = position;
1654 message.primary.value = exp;
1655 message.primary.color = TEXTCOLOR_WHITE_EXP;
1656 sendTextMessage(message);
1657
1658 SpectatorVec spectators;
1659 g_game.map.getSpectators(spectators, position, false, true);
1660 spectators.erase(this);
1661 if (!spectators.empty()) {
1662 message.type = MESSAGE_EXPERIENCE_OTHERS;
1663 message.text = getName() + " gained " + expString;
1664 for (Creature *spectator : spectators) {
1665 spectator->getPlayer()->sendTextMessage(message);
1666 }
1667 }
1668 }
1669
1670 uint32_t prevLevel = level;
1671 while (experience >= nextLevelExp) {
1672 ++level;
1673 healthMax += vocation->getHPGain();
1674 health += vocation->getHPGain();
1675 manaMax += vocation->getManaGain();
1676 mana += vocation->getManaGain();
1677 capacity += vocation->getCapGain();
1678
1679 currLevelExp = nextLevelExp;
1680 nextLevelExp = Player::getExpForLevel(level + 1);
1681 if (currLevelExp >= nextLevelExp) {
1682 // player has reached max level
1683 break;
1684 }
1685 }
1686
1687 if (prevLevel != level) {
1688 health = getMaxHealth();
1689 mana = getMaxMana();
1690
1691 updateBaseSpeed();
1692 setBaseSpeed(getBaseSpeed());
1693
1694 g_game.changeSpeed(this, 0);
1695 g_game.addCreatureHealth(this);
1696
1697 const uint32_t protectionLevel = static_cast<uint32_t>(
1698 g_config.getNumber(ConfigManager::PROTECTION_LEVEL));
1699 if (prevLevel < protectionLevel && level >= protectionLevel) {
1700 g_game.updateCreatureWalkthrough(this);
1701 }
1702
1703 if (party) {
1704 party->updateSharedExperience();
1705 }
1706
1707 g_creatureEvents->playerAdvance(this, SKILL_LEVEL, prevLevel, level);
1708
1709 std::ostringstream ss;
1710 ss << "You advanced from Level " << prevLevel << " to Level " << level
1711 << '.';
1712 sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
1713 }
1714
1715 if (nextLevelExp > currLevelExp) {
1716 levelPercent = Player::getPercentLevel(experience - currLevelExp,
1717 nextLevelExp - currLevelExp);
1718 } else {
1719 levelPercent = 0;
1720 }
1721 sendStats();
1722}
1723
1724void Player::removeExperience(uint64_t exp, bool sendText /* = false*/) {
1725 if (experience == 0 || exp == 0) {
1726 return;
1727 }
1728
1729 g_events->eventPlayerOnLoseExperience(this, exp);
1730 if (exp == 0) {
1731 return;
1732 }
1733
1734 uint64_t lostExp = experience;
1735 experience = std::max<int64_t>(0, experience - exp);
1736
1737 if (sendText) {
1738 lostExp -= experience;
1739
1740 std::string expString =
1741 std::to_string(lostExp) +
1742 (lostExp != 1 ? " experience points." : " experience point.");
1743
1744 TextMessage message(MESSAGE_EXPERIENCE, "You lost " + expString);
1745 message.position = position;
1746 message.primary.value = lostExp;
1747 message.primary.color = TEXTCOLOR_RED;
1748 sendTextMessage(message);
1749
1750 SpectatorVec spectators;
1751 g_game.map.getSpectators(spectators, position, false, true);
1752 spectators.erase(this);
1753 if (!spectators.empty()) {
1754 message.type = MESSAGE_EXPERIENCE_OTHERS;
1755 message.text = getName() + " lost " + expString;
1756 for (Creature *spectator : spectators) {
1757 spectator->getPlayer()->sendTextMessage(message);
1758 }
1759 }
1760 }
1761
1762 uint32_t oldLevel = level;
1763 uint64_t currLevelExp = Player::getExpForLevel(level);
1764
1765 while (level > 1 && experience < currLevelExp) {
1766 --level;
1767 healthMax = std::max<int32_t>(0, healthMax - vocation->getHPGain());
1768 manaMax = std::max<int32_t>(0, manaMax - vocation->getManaGain());
1769 capacity = std::max<int32_t>(0, capacity - vocation->getCapGain());
1770 currLevelExp = Player::getExpForLevel(level);
1771 }
1772
1773 if (oldLevel != level) {
1774 health = getMaxHealth();
1775 mana = getMaxMana();
1776
1777 updateBaseSpeed();
1778 setBaseSpeed(getBaseSpeed());
1779
1780 g_game.changeSpeed(this, 0);
1781 g_game.addCreatureHealth(this);
1782
1783 const uint32_t protectionLevel = static_cast<uint32_t>(
1784 g_config.getNumber(ConfigManager::PROTECTION_LEVEL));
1785 if (oldLevel >= protectionLevel && level < protectionLevel) {
1786 g_game.updateCreatureWalkthrough(this);
1787 }
1788
1789 if (party) {
1790 party->updateSharedExperience();
1791 }
1792
1793 std::ostringstream ss;
1794 ss << "You were downgraded from Level " << oldLevel << " to Level " << level
1795 << '.';
1796 sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
1797 }
1798
1799 uint64_t nextLevelExp = Player::getExpForLevel(level + 1);
1800 if (nextLevelExp > currLevelExp) {
1801 levelPercent = Player::getPercentLevel(experience - currLevelExp,
1802 nextLevelExp - currLevelExp);
1803 } else {
1804 levelPercent = 0;
1805 }
1806 sendStats();
1807}
1808
1809uint8_t Player::getPercentLevel(uint64_t count, uint64_t nextLevelCount) {
1810 if (nextLevelCount == 0) {
1811 return 0;
1812 }
1813
1814 uint8_t result = (count * 100) / nextLevelCount;
1815 if (result > 100) {
1816 return 0;
1817 }
1818 return result;
1819}
1820
1821void Player::onBlockHit() {
1822 if (shieldBlockCount > 0) {
1823 --shieldBlockCount;
1824
1825 if (hasShield()) {
1826 addSkillAdvance(SKILL_SHIELD, 1);
1827 }
1828 }
1829}
1830
1831void Player::onAttackedCreatureBlockHit(BlockType_t blockType) {
1832 lastAttackBlockType = blockType;
1833
1834 switch (blockType) {
1835 case BLOCK_NONE: {
1836 addAttackSkillPoint = true;
1837 bloodHitCount = 30;
1838 shieldBlockCount = 30;
1839 break;
1840 }
1841
1842 case BLOCK_DEFENSE:
1843 case BLOCK_ARMOR: {
1844 // need to draw blood every 30 hits
1845 if (bloodHitCount > 0) {
1846 addAttackSkillPoint = true;
1847 --bloodHitCount;
1848 } else {
1849 addAttackSkillPoint = false;
1850 }
1851 break;
1852 }
1853
1854 default: {
1855 addAttackSkillPoint = false;
1856 break;
1857 }
1858 }
1859}
1860
1861bool Player::hasShield() const {
1862 Item *item = inventory[CONST_SLOT_LEFT];
1863 if (item && item->getWeaponType() == WEAPON_SHIELD) {
1864 return true;
1865 }
1866
1867 item = inventory[CONST_SLOT_RIGHT];
1868 if (item && item->getWeaponType() == WEAPON_SHIELD) {
1869 return true;
1870 }
1871 return false;
1872}
1873
1874BlockType_t Player::blockHit(Creature *attacker, CombatType_t combatType,
1875 int32_t &damage, bool checkDefense /* = false*/,
1876 bool checkArmor /* = false*/,
1877 bool field /* = false*/) {
1878 BlockType_t blockType = Creature::blockHit(attacker, combatType, damage,
1879 checkDefense, checkArmor, field);
1880
1881 if (attacker) {
1882 sendCreatureSquare(attacker, SQ_COLOR_BLACK);
1883 }
1884
1885 if (blockType != BLOCK_NONE) {
1886 return blockType;
1887 }
1888
1889 if (damage <= 0) {
1890 damage = 0;
1891 return BLOCK_ARMOR;
1892 }
1893
1894 for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) {
1895 if (!isItemAbilityEnabled(static_cast<slots_t>(slot))) {
1896 continue;
1897 }
1898
1899 Item *item = inventory[slot];
1900 if (!item) {
1901 continue;
1902 }
1903
1904 const ItemType &it = Item::items[item->getID()];
1905 if (!it.abilities) {
1906 if (damage <= 0) {
1907 damage = 0;
1908 return BLOCK_ARMOR;
1909 }
1910
1911 continue;
1912 }
1913
1914 const int16_t &absorbPercent =
1915 it.abilities->absorbPercent[combatTypeToIndex(combatType)];
1916 if (absorbPercent != 0) {
1917 damage -= std::round(damage * (absorbPercent / 100.));
1918
1919 uint16_t charges = item->getCharges();
1920 if (charges != 0) {
1921 g_game.transformItem(item, item->getID(), charges - 1);
1922 }
1923 }
1924
1925 if (field) {
1926 const int16_t &fieldAbsorbPercent =
1927 it.abilities->fieldAbsorbPercent[combatTypeToIndex(combatType)];
1928 if (fieldAbsorbPercent != 0) {
1929 damage -= std::round(damage * (fieldAbsorbPercent / 100.));
1930
1931 uint16_t charges = item->getCharges();
1932 if (charges != 0) {
1933 g_game.transformItem(item, item->getID(), charges - 1);
1934 }
1935 }
1936 }
1937 }
1938
1939 if (damage <= 0) {
1940 damage = 0;
1941 blockType = BLOCK_ARMOR;
1942 }
1943 return blockType;
1944}
1945
1946uint32_t Player::getIP() const {
1947 if (client) {
1948 return client->getIP();
1949 }
1950
1951 return 0;
1952}
1953
1954void Player::death(Creature *lastHitCreature) {
1955 loginPosition = town->getTemplePosition();
1956
1957 if (skillLoss) {
1958 uint8_t unfairFightReduction = 100;
1959 bool lastHitPlayer = Player::lastHitIsPlayer(lastHitCreature);
1960
1961 if (lastHitPlayer) {
1962 uint32_t sumLevels = 0;
1963 uint32_t inFightTicks = g_config.getNumber(ConfigManager::PZ_LOCKED);
1964 for (const auto &it : damageMap) {
1965 CountBlock_t cb = it.second;
1966 if ((OTSYS_TIME() - cb.ticks) <= inFightTicks) {
1967 Player *damageDealer = g_game.getPlayerByID(it.first);
1968 if (damageDealer) {
1969 sumLevels += damageDealer->getLevel();
1970 }
1971 }
1972 }
1973
1974 if (sumLevels > level) {
1975 double reduce = level / static_cast<double>(sumLevels);
1976 unfairFightReduction =
1977 std::max<uint8_t>(20, std::floor((reduce * 100) + 0.5));
1978 }
1979 }
1980
1981 // Magic level loss
1982 uint64_t sumMana = 0;
1983 uint64_t lostMana = 0;
1984
1985 // sum up all the mana
1986 for (uint32_t i = 1; i <= magLevel; ++i) {
1987 sumMana += vocation->getReqMana(i);
1988 }
1989
1990 sumMana += manaSpent;
1991
1992 double deathLossPercent = getLostPercent() * (unfairFightReduction / 100.);
1993
1994 lostMana = static_cast<uint64_t>(sumMana * deathLossPercent);
1995
1996 while (lostMana > manaSpent && magLevel > 0) {
1997 lostMana -= manaSpent;
1998 manaSpent = vocation->getReqMana(magLevel);
1999 magLevel--;
2000 }
2001
2002 manaSpent -= lostMana;
2003
2004 uint64_t nextReqMana = vocation->getReqMana(magLevel + 1);
2005 if (nextReqMana > vocation->getReqMana(magLevel)) {
2006 magLevelPercent = Player::getPercentLevel(manaSpent, nextReqMana);
2007 } else {
2008 magLevelPercent = 0;
2009 }
2010
2011 // Skill loss
2012 for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { // for each skill
2013 uint64_t sumSkillTries = 0;
2014 for (uint16_t c = 11; c <= skills[i].level;
2015 ++c) { // sum up all required tries for all skill levels
2016 sumSkillTries += vocation->getReqSkillTries(i, c);
2017 }
2018
2019 sumSkillTries += skills[i].tries;
2020
2021 uint32_t lostSkillTries =
2022 static_cast<uint32_t>(sumSkillTries * deathLossPercent);
2023 while (lostSkillTries > skills[i].tries) {
2024 lostSkillTries -= skills[i].tries;
2025
2026 if (skills[i].level <= 10) {
2027 skills[i].level = 10;
2028 skills[i].tries = 0;
2029 lostSkillTries = 0;
2030 break;
2031 }
2032
2033 skills[i].tries = vocation->getReqSkillTries(i, skills[i].level);
2034 skills[i].level--;
2035 }
2036
2037 skills[i].tries = std::max<int32_t>(0, skills[i].tries - lostSkillTries);
2038 skills[i].percent = Player::getPercentLevel(
2039 skills[i].tries, vocation->getReqSkillTries(i, skills[i].level));
2040 }
2041
2042 // Level loss
2043 uint64_t expLoss = static_cast<uint64_t>(experience * deathLossPercent);
2044 g_events->eventPlayerOnLoseExperience(this, expLoss);
2045
2046 if (expLoss != 0) {
2047 uint32_t oldLevel = level;
2048
2049 if (vocation->getId() == VOCATION_NONE || level > 7) {
2050 experience -= expLoss;
2051 }
2052
2053 while (level > 1 && experience < Player::getExpForLevel(level)) {
2054 --level;
2055 healthMax = std::max<int32_t>(0, healthMax - vocation->getHPGain());
2056 manaMax = std::max<int32_t>(0, manaMax - vocation->getManaGain());
2057 capacity = std::max<int32_t>(0, capacity - vocation->getCapGain());
2058 }
2059
2060 if (oldLevel != level) {
2061 std::ostringstream ss;
2062 ss << "You were downgraded from Level " << oldLevel << " to Level "
2063 << level << '.';
2064 sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
2065 }
2066
2067 uint64_t currLevelExp = Player::getExpForLevel(level);
2068 uint64_t nextLevelExp = Player::getExpForLevel(level + 1);
2069 if (nextLevelExp > currLevelExp) {
2070 levelPercent = Player::getPercentLevel(experience - currLevelExp,
2071 nextLevelExp - currLevelExp);
2072 } else {
2073 levelPercent = 0;
2074 }
2075 }
2076
2077 std::bitset<6> bitset(blessings);
2078 if (bitset[5]) {
2079 if (lastHitPlayer) {
2080 bitset.reset(5);
2081 blessings = bitset.to_ulong();
2082 } else {
2083 blessings = 32;
2084 }
2085 } else {
2086 blessings = 0;
2087 }
2088
2089 sendStats();
2090 sendSkills();
2091 sendReLoginWindow(unfairFightReduction);
2092
2093 if (getSkull() == SKULL_BLACK) {
2094 health = 40;
2095 mana = 0;
2096 } else {
2097 health = healthMax;
2098 mana = manaMax;
2099 }
2100
2101 auto it = conditions.begin(), end = conditions.end();
2102 while (it != end) {
2103 Condition *condition = *it;
2104 if (condition->isPersistent()) {
2105 it = conditions.erase(it);
2106
2107 condition->endCondition(this);
2108 onEndCondition(condition->getType());
2109 delete condition;
2110 } else {
2111 ++it;
2112 }
2113 }
2114 } else {
2115 setSkillLoss(true);
2116
2117 auto it = conditions.begin(), end = conditions.end();
2118 while (it != end) {
2119 Condition *condition = *it;
2120 if (condition->isPersistent()) {
2121 it = conditions.erase(it);
2122
2123 condition->endCondition(this);
2124 onEndCondition(condition->getType());
2125 delete condition;
2126 } else {
2127 ++it;
2128 }
2129 }
2130
2131 health = healthMax;
2132 g_game.internalTeleport(this, getTemplePosition(), true);
2133 g_game.addCreatureHealth(this);
2134 onThink(EVENT_CREATURE_THINK_INTERVAL);
2135 onIdleStatus();
2136 sendStats();
2137 }
2138}
2139
2140bool Player::dropCorpse(Creature *lastHitCreature, Creature *mostDamageCreature,
2141 bool lastHitUnjustified, bool mostDamageUnjustified) {
2142 if (getZone() != ZONE_PVP || !Player::lastHitIsPlayer(lastHitCreature)) {
2143 return Creature::dropCorpse(lastHitCreature, mostDamageCreature,
2144 lastHitUnjustified, mostDamageUnjustified);
2145 }
2146
2147 setDropLoot(true);
2148 return false;
2149}
2150
2151Item *Player::getCorpse(Creature *lastHitCreature,
2152 Creature *mostDamageCreature) {
2153 Item *corpse = Creature::getCorpse(lastHitCreature, mostDamageCreature);
2154 if (corpse && corpse->getContainer()) {
2155 std::ostringstream ss;
2156 if (lastHitCreature) {
2157 ss << "You recognize " << getNameDescription() << ". "
2158 << (getSex() == PLAYERSEX_FEMALE ? "She" : "He") << " was killed by "
2159 << lastHitCreature->getNameDescription() << '.';
2160 } else {
2161 ss << "You recognize " << getNameDescription() << '.';
2162 }
2163
2164 corpse->setSpecialDescription(ss.str());
2165 }
2166 return corpse;
2167}
2168
2169void Player::addInFightTicks(bool pzlock /*= false*/) {
2170 if (hasFlag(PlayerFlag_NotGainInFight)) {
2171 return;
2172 }
2173
2174 if (pzlock) {
2175 pzLocked = true;
2176 }
2177
2178 Condition *condition = Condition::createCondition(
2179 CONDITIONID_DEFAULT, CONDITION_INFIGHT,
2180 g_config.getNumber(ConfigManager::PZ_LOCKED), 0);
2181 addCondition(condition);
2182}
2183
2184void Player::removeList() {
2185 g_game.removePlayer(this);
2186
2187 for (const auto &it : g_game.getPlayers()) {
2188 it.second->notifyStatusChange(this, VIPSTATUS_OFFLINE);
2189 }
2190}
2191
2192void Player::addList() {
2193 for (const auto &it : g_game.getPlayers()) {
2194 it.second->notifyStatusChange(this, VIPSTATUS_ONLINE);
2195 }
2196
2197 g_game.addPlayer(this);
2198}
2199
2200void Player::kickPlayer(bool displayEffect) {
2201 g_creatureEvents->playerLogout(this);
2202 if (client) {
2203 client->logout(displayEffect, true);
2204 } else {
2205 g_game.removeCreature(this);
2206 }
2207}
2208
2209void Player::notifyStatusChange(Player *loginPlayer, VipStatus_t status) {
2210 if (!client) {
2211 return;
2212 }
2213
2214 auto it = VIPList.find(loginPlayer->guid);
2215 if (it == VIPList.end()) {
2216 return;
2217 }
2218
2219 client->sendUpdatedVIPStatus(loginPlayer->guid, status);
2220
2221 if (status == VIPSTATUS_ONLINE) {
2222 client->sendTextMessage(TextMessage(
2223 MESSAGE_STATUS_SMALL, loginPlayer->getName() + " has logged in."));
2224 } else if (status == VIPSTATUS_OFFLINE) {
2225 client->sendTextMessage(TextMessage(
2226 MESSAGE_STATUS_SMALL, loginPlayer->getName() + " has logged out."));
2227 }
2228}
2229
2230bool Player::removeVIP(uint32_t vipGuid) {
2231 if (VIPList.erase(vipGuid) == 0) {
2232 return false;
2233 }
2234
2235 IOLoginData::removeVIPEntry(accountNumber, vipGuid);
2236 return true;
2237}
2238
2239bool Player::addVIP(uint32_t vipGuid, const std::string &vipName,
2240 VipStatus_t status) {
2241 if (VIPList.size() >= getMaxVIPEntries() ||
2242 VIPList.size() == 200) { // max number of buddies is 200 in 9.53
2243 sendTextMessage(MESSAGE_STATUS_SMALL, "You cannot add more buddies.");
2244 return false;
2245 }
2246
2247 auto result = VIPList.insert(vipGuid);
2248 if (!result.second) {
2249 sendTextMessage(MESSAGE_STATUS_SMALL,
2250 "This player is already in your list.");
2251 return false;
2252 }
2253
2254 IOLoginData::addVIPEntry(accountNumber, vipGuid, "", 0, false);
2255 if (client) {
2256 client->sendVIP(vipGuid, vipName, "", 0, false, status);
2257 }
2258 return true;
2259}
2260
2261bool Player::addVIPInternal(uint32_t vipGuid) {
2262 if (VIPList.size() >= getMaxVIPEntries() ||
2263 VIPList.size() == 200) { // max number of buddies is 200 in 9.53
2264 return false;
2265 }
2266
2267 return VIPList.insert(vipGuid).second;
2268}
2269
2270bool Player::editVIP(uint32_t vipGuid, const std::string &description,
2271 uint32_t icon, bool notify) {
2272 auto it = VIPList.find(vipGuid);
2273 if (it == VIPList.end()) {
2274 return false; // player is not in VIP
2275 }
2276
2277 IOLoginData::editVIPEntry(accountNumber, vipGuid, description, icon, notify);
2278 return true;
2279}
2280
2281// close container and its child containers
2282void Player::autoCloseContainers(const Container *container) {
2283 std::vector<uint32_t> closeList;
2284 for (const auto &it : openContainers) {
2285 Container *tmpContainer = it.second.container;
2286 while (tmpContainer) {
2287 if (tmpContainer->isRemoved() || tmpContainer == container) {
2288 closeList.push_back(it.first);
2289 break;
2290 }
2291
2292 tmpContainer = dynamic_cast<Container *>(tmpContainer->getParent());
2293 }
2294 }
2295
2296 for (uint32_t containerId : closeList) {
2297 closeContainer(containerId);
2298 if (client) {
2299 client->sendCloseContainer(containerId);
2300 }
2301 }
2302}
2303
2304bool Player::hasCapacity(const Item *item, uint32_t count) const {
2305 if (hasFlag(PlayerFlag_CannotPickupItem)) {
2306 return false;
2307 }
2308
2309 if (hasFlag(PlayerFlag_HasInfiniteCapacity) || item->getTopParent() == this) {
2310 return true;
2311 }
2312
2313 uint32_t itemWeight = item->getContainer() != nullptr ? item->getWeight()
2314 : item->getBaseWeight();
2315 if (item->isStackable()) {
2316 itemWeight *= count;
2317 }
2318 return itemWeight <= getFreeCapacity();
2319}
2320
2321ReturnValue Player::queryAdd(int32_t index, const Thing &thing, uint32_t count,
2322 uint32_t flags, Creature *) const {
2323 const Item *item = thing.getItem();
2324 if (item == nullptr) {
2325 return RETURNVALUE_NOTPOSSIBLE;
2326 }
2327
2328 bool childIsOwner = hasBitSet(FLAG_CHILDISOWNER, flags);
2329 if (childIsOwner) {
2330 // a child container is querying the player, just check if enough capacity
2331 bool skipLimit = hasBitSet(FLAG_NOLIMIT, flags);
2332 if (skipLimit || hasCapacity(item, count)) {
2333 return RETURNVALUE_NOERROR;
2334 }
2335 return RETURNVALUE_NOTENOUGHCAPACITY;
2336 }
2337
2338 if (!item->isPickupable()) {
2339 return RETURNVALUE_CANNOTPICKUP;
2340 }
2341
2342 ReturnValue ret = RETURNVALUE_NOERROR;
2343
2344 const int32_t &slotPosition = item->getSlotPosition();
2345 if ((slotPosition & SLOTP_HEAD) || (slotPosition & SLOTP_NECKLACE) ||
2346 (slotPosition & SLOTP_BACKPACK) || (slotPosition & SLOTP_ARMOR) ||
2347 (slotPosition & SLOTP_LEGS) || (slotPosition & SLOTP_FEET) ||
2348 (slotPosition & SLOTP_RING)) {
2349 ret = RETURNVALUE_CANNOTBEDRESSED;
2350 } else if (slotPosition & SLOTP_TWO_HAND) {
2351 ret = RETURNVALUE_PUTTHISOBJECTINBOTHHANDS;
2352 } else if ((slotPosition & SLOTP_RIGHT) || (slotPosition & SLOTP_LEFT)) {
2353 if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) {
2354 ret = RETURNVALUE_CANNOTBEDRESSED;
2355 } else {
2356 ret = RETURNVALUE_PUTTHISOBJECTINYOURHAND;
2357 }
2358 }
2359
2360 switch (index) {
2361 case CONST_SLOT_HEAD: {
2362 if (slotPosition & SLOTP_HEAD) {
2363 ret = RETURNVALUE_NOERROR;
2364 }
2365 break;
2366 }
2367
2368 case CONST_SLOT_NECKLACE: {
2369 if (slotPosition & SLOTP_NECKLACE) {
2370 ret = RETURNVALUE_NOERROR;
2371 }
2372 break;
2373 }
2374
2375 case CONST_SLOT_BACKPACK: {
2376 if (slotPosition & SLOTP_BACKPACK) {
2377 ret = RETURNVALUE_NOERROR;
2378 }
2379 break;
2380 }
2381
2382 case CONST_SLOT_ARMOR: {
2383 if (slotPosition & SLOTP_ARMOR) {
2384 ret = RETURNVALUE_NOERROR;
2385 }
2386 break;
2387 }
2388
2389 case CONST_SLOT_RIGHT: {
2390 if (slotPosition & SLOTP_RIGHT) {
2391 if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) {
2392 if (item->getWeaponType() != WEAPON_SHIELD) {
2393 ret = RETURNVALUE_CANNOTBEDRESSED;
2394 } else {
2395 const Item *leftItem = inventory[CONST_SLOT_LEFT];
2396 if (leftItem) {
2397 if ((leftItem->getSlotPosition() | slotPosition) & SLOTP_TWO_HAND) {
2398 ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE;
2399 } else {
2400 ret = RETURNVALUE_NOERROR;
2401 }
2402 } else {
2403 ret = RETURNVALUE_NOERROR;
2404 }
2405 }
2406 } else if (slotPosition & SLOTP_TWO_HAND) {
2407 if (inventory[CONST_SLOT_LEFT] && inventory[CONST_SLOT_LEFT] != item) {
2408 ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE;
2409 } else {
2410 ret = RETURNVALUE_NOERROR;
2411 }
2412 } else if (inventory[CONST_SLOT_LEFT]) {
2413 const Item *leftItem = inventory[CONST_SLOT_LEFT];
2414 WeaponType_t type = item->getWeaponType(),
2415 leftType = leftItem->getWeaponType();
2416
2417 if (leftItem->getSlotPosition() & SLOTP_TWO_HAND) {
2418 ret = RETURNVALUE_DROPTWOHANDEDITEM;
2419 } else if (item == leftItem && count == item->getItemCount()) {
2420 ret = RETURNVALUE_NOERROR;
2421 } else if (leftType == WEAPON_SHIELD && type == WEAPON_SHIELD) {
2422 ret = RETURNVALUE_CANONLYUSEONESHIELD;
2423 } else if (leftType == WEAPON_NONE || type == WEAPON_NONE ||
2424 leftType == WEAPON_SHIELD || leftType == WEAPON_AMMO ||
2425 type == WEAPON_SHIELD || type == WEAPON_AMMO) {
2426 ret = RETURNVALUE_NOERROR;
2427 } else {
2428 ret = RETURNVALUE_CANONLYUSEONEWEAPON;
2429 }
2430 } else {
2431 ret = RETURNVALUE_NOERROR;
2432 }
2433 }
2434 break;
2435 }
2436
2437 case CONST_SLOT_LEFT: {
2438 if (slotPosition & SLOTP_LEFT) {
2439 if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) {
2440 WeaponType_t type = item->getWeaponType();
2441 if (type == WEAPON_NONE || type == WEAPON_SHIELD) {
2442 ret = RETURNVALUE_CANNOTBEDRESSED;
2443 } else if (inventory[CONST_SLOT_RIGHT] &&
2444 (slotPosition & SLOTP_TWO_HAND)) {
2445 ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE;
2446 } else {
2447 ret = RETURNVALUE_NOERROR;
2448 }
2449 } else if (slotPosition & SLOTP_TWO_HAND) {
2450 if (inventory[CONST_SLOT_RIGHT] &&
2451 inventory[CONST_SLOT_RIGHT] != item) {
2452 ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE;
2453 } else {
2454 ret = RETURNVALUE_NOERROR;
2455 }
2456 } else if (inventory[CONST_SLOT_RIGHT]) {
2457 const Item *rightItem = inventory[CONST_SLOT_RIGHT];
2458 WeaponType_t type = item->getWeaponType(),
2459 rightType = rightItem->getWeaponType();
2460
2461 if (rightItem->getSlotPosition() & SLOTP_TWO_HAND) {
2462 ret = RETURNVALUE_DROPTWOHANDEDITEM;
2463 } else if (item == rightItem && count == item->getItemCount()) {
2464 ret = RETURNVALUE_NOERROR;
2465 } else if (rightType == WEAPON_SHIELD && type == WEAPON_SHIELD) {
2466 ret = RETURNVALUE_CANONLYUSEONESHIELD;
2467 } else if (rightType == WEAPON_NONE || type == WEAPON_NONE ||
2468 rightType == WEAPON_SHIELD || rightType == WEAPON_AMMO ||
2469 type == WEAPON_SHIELD || type == WEAPON_AMMO) {
2470 ret = RETURNVALUE_NOERROR;
2471 } else {
2472 ret = RETURNVALUE_CANONLYUSEONEWEAPON;
2473 }
2474 } else {
2475 ret = RETURNVALUE_NOERROR;
2476 }
2477 }
2478 break;
2479 }
2480
2481 case CONST_SLOT_LEGS: {
2482 if (slotPosition & SLOTP_LEGS) {
2483 ret = RETURNVALUE_NOERROR;
2484 }
2485 break;
2486 }
2487
2488 case CONST_SLOT_FEET: {
2489 if (slotPosition & SLOTP_FEET) {
2490 ret = RETURNVALUE_NOERROR;
2491 }
2492 break;
2493 }
2494
2495 case CONST_SLOT_RING: {
2496 if (slotPosition & SLOTP_RING) {
2497 ret = RETURNVALUE_NOERROR;
2498 }
2499 break;
2500 }
2501
2502 case CONST_SLOT_AMMO: {
2503 if ((slotPosition & SLOTP_AMMO) ||
2504 g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) {
2505 ret = RETURNVALUE_NOERROR;
2506 }
2507 break;
2508 }
2509
2510 case CONST_SLOT_WHEREEVER:
2511 case -1:
2512 ret = RETURNVALUE_NOTENOUGHROOM;
2513 break;
2514
2515 default:
2516 ret = RETURNVALUE_NOTPOSSIBLE;
2517 break;
2518 }
2519
2520 if (ret != RETURNVALUE_NOERROR && ret != RETURNVALUE_NOTENOUGHROOM) {
2521 return ret;
2522 }
2523
2524 // check if enough capacity
2525 if (!hasCapacity(item, count)) {
2526 return RETURNVALUE_NOTENOUGHCAPACITY;
2527 }
2528
2529 ret = g_moveEvents->onPlayerEquip(const_cast<Player *>(this),
2530 const_cast<Item *>(item),
2531 static_cast<slots_t>(index), true);
2532 if (ret != RETURNVALUE_NOERROR) {
2533 return ret;
2534 }
2535
2536 // need an exchange with source? (destination item is swapped with currently
2537 // moved item)
2538 const Item *inventoryItem = getInventoryItem(static_cast<slots_t>(index));
2539 if (inventoryItem && (!inventoryItem->isStackable() ||
2540 inventoryItem->getID() != item->getID())) {
2541 const Cylinder *cylinder = item->getTopParent();
2542 if (cylinder && (dynamic_cast<const DepotChest *>(cylinder) ||
2543 dynamic_cast<const Player *>(cylinder))) {
2544 return RETURNVALUE_NEEDEXCHANGE;
2545 }
2546
2547 return RETURNVALUE_NOTENOUGHROOM;
2548 }
2549 return ret;
2550}
2551
2552ReturnValue Player::queryMaxCount(int32_t index, const Thing &thing,
2553 uint32_t count, uint32_t &maxQueryCount,
2554 uint32_t flags) const {
2555 const Item *item = thing.getItem();
2556 if (item == nullptr) {
2557 maxQueryCount = 0;
2558 return RETURNVALUE_NOTPOSSIBLE;
2559 }
2560
2561 if (index == INDEX_WHEREEVER) {
2562 uint32_t n = 0;
2563 for (int32_t slotIndex = CONST_SLOT_FIRST; slotIndex <= CONST_SLOT_LAST;
2564 ++slotIndex) {
2565 Item *inventoryItem = inventory[slotIndex];
2566 if (inventoryItem) {
2567 if (Container *subContainer = inventoryItem->getContainer()) {
2568 uint32_t queryCount = 0;
2569 subContainer->queryMaxCount(INDEX_WHEREEVER, *item,
2570 item->getItemCount(), queryCount, flags);
2571 n += queryCount;
2572
2573 // iterate through all items, including sub-containers (deep search)
2574 for (ContainerIterator it = subContainer->iterator(); it.hasNext();
2575 it.advance()) {
2576 if (Container *tmpContainer = (*it)->getContainer()) {
2577 queryCount = 0;
2578 tmpContainer->queryMaxCount(INDEX_WHEREEVER, *item,
2579 item->getItemCount(), queryCount,
2580 flags);
2581 n += queryCount;
2582 }
2583 }
2584 } else if (inventoryItem->isStackable() &&
2585 item->equals(inventoryItem) &&
2586 inventoryItem->getItemCount() < 100) {
2587 uint32_t remainder = (100 - inventoryItem->getItemCount());
2588
2589 if (queryAdd(slotIndex, *item, remainder, flags) ==
2590 RETURNVALUE_NOERROR) {
2591 n += remainder;
2592 }
2593 }
2594 } else if (queryAdd(slotIndex, *item, item->getItemCount(), flags) ==
2595 RETURNVALUE_NOERROR) { // empty slot
2596 if (item->isStackable()) {
2597 n += 100;
2598 } else {
2599 ++n;
2600 }
2601 }
2602 }
2603
2604 maxQueryCount = n;
2605 } else {
2606 const Item *destItem = nullptr;
2607
2608 const Thing *destThing = getThing(index);
2609 if (destThing) {
2610 destItem = destThing->getItem();
2611 }
2612
2613 if (destItem) {
2614 if (destItem->isStackable() && item->equals(destItem) &&
2615 destItem->getItemCount() < 100) {
2616 maxQueryCount = 100 - destItem->getItemCount();
2617 } else {
2618 maxQueryCount = 0;
2619 }
2620 } else if (queryAdd(index, *item, count, flags) ==
2621 RETURNVALUE_NOERROR) { // empty slot
2622 if (item->isStackable()) {
2623 maxQueryCount = 100;
2624 } else {
2625 maxQueryCount = 1;
2626 }
2627
2628 return RETURNVALUE_NOERROR;
2629 }
2630 }
2631
2632 if (maxQueryCount < count) {
2633 return RETURNVALUE_NOTENOUGHROOM;
2634 } else {
2635 return RETURNVALUE_NOERROR;
2636 }
2637}
2638
2639ReturnValue Player::queryRemove(const Thing &thing, uint32_t count,
2640 uint32_t flags) const {
2641 int32_t index = getThingIndex(&thing);
2642 if (index == -1) {
2643 return RETURNVALUE_NOTPOSSIBLE;
2644 }
2645
2646 const Item *item = thing.getItem();
2647 if (item == nullptr) {
2648 return RETURNVALUE_NOTPOSSIBLE;
2649 }
2650
2651 if (count == 0 || (item->isStackable() && count > item->getItemCount())) {
2652 return RETURNVALUE_NOTPOSSIBLE;
2653 }
2654
2655 if (!item->isMoveable() && !hasBitSet(FLAG_IGNORENOTMOVEABLE, flags)) {
2656 return RETURNVALUE_NOTMOVEABLE;
2657 }
2658
2659 return RETURNVALUE_NOERROR;
2660}
2661
2662Cylinder *Player::queryDestination(int32_t &index, const Thing &thing,
2663 Item **destItem, uint32_t &flags) {
2664 if (index == 0 /*drop to capacity window*/ || index == INDEX_WHEREEVER) {
2665 *destItem = nullptr;
2666
2667 const Item *item = thing.getItem();
2668 if (item == nullptr) {
2669 return this;
2670 }
2671
2672 bool autoStack = !((flags & FLAG_IGNOREAUTOSTACK) == FLAG_IGNOREAUTOSTACK);
2673 bool isStackable = item->isStackable();
2674
2675 std::vector<Container *> containers;
2676
2677 for (uint32_t slotIndex = CONST_SLOT_FIRST; slotIndex <= CONST_SLOT_LAST;
2678 ++slotIndex) {
2679 Item *inventoryItem = inventory[slotIndex];
2680 if (inventoryItem) {
2681 if (inventoryItem == tradeItem) {
2682 continue;
2683 }
2684
2685 if (inventoryItem == item) {
2686 continue;
2687 }
2688
2689 if (autoStack && isStackable) {
2690 // try find an already existing item to stack with
2691 if (queryAdd(slotIndex, *item, item->getItemCount(), 0) ==
2692 RETURNVALUE_NOERROR) {
2693 if (inventoryItem->equals(item) &&
2694 inventoryItem->getItemCount() < 100) {
2695 index = slotIndex;
2696 *destItem = inventoryItem;
2697 return this;
2698 }
2699 }
2700
2701 if (Container *subContainer = inventoryItem->getContainer()) {
2702 containers.push_back(subContainer);
2703 }
2704 } else if (Container *subContainer = inventoryItem->getContainer()) {
2705 containers.push_back(subContainer);
2706 }
2707 } else if (queryAdd(slotIndex, *item, item->getItemCount(), flags) ==
2708 RETURNVALUE_NOERROR) { // empty slot
2709 index = slotIndex;
2710 *destItem = nullptr;
2711 return this;
2712 }
2713 }
2714
2715 size_t i = 0;
2716 while (i < containers.size()) {
2717 Container *tmpContainer = containers[i++];
2718 if (!autoStack || !isStackable) {
2719 // we need to find first empty container as fast as we can for
2720 // non-stackable items
2721 uint32_t n = tmpContainer->capacity() - tmpContainer->size();
2722 while (n) {
2723 if (tmpContainer->queryAdd(tmpContainer->capacity() - n, *item,
2724 item->getItemCount(),
2725 flags) == RETURNVALUE_NOERROR) {
2726 index = tmpContainer->capacity() - n;
2727 *destItem = nullptr;
2728 return tmpContainer;
2729 }
2730
2731 n--;
2732 }
2733
2734 for (Item *tmpContainerItem : tmpContainer->getItemList()) {
2735 if (Container *subContainer = tmpContainerItem->getContainer()) {
2736 containers.push_back(subContainer);
2737 }
2738 }
2739
2740 continue;
2741 }
2742
2743 uint32_t n = 0;
2744
2745 for (Item *tmpItem : tmpContainer->getItemList()) {
2746 if (tmpItem == tradeItem) {
2747 continue;
2748 }
2749
2750 if (tmpItem == item) {
2751 continue;
2752 }
2753
2754 // try find an already existing item to stack with
2755 if (tmpItem->equals(item) && tmpItem->getItemCount() < 100) {
2756 index = n;
2757 *destItem = tmpItem;
2758 return tmpContainer;
2759 }
2760
2761 if (Container *subContainer = tmpItem->getContainer()) {
2762 containers.push_back(subContainer);
2763 }
2764
2765 n++;
2766 }
2767
2768 if (n < tmpContainer->capacity() &&
2769 tmpContainer->queryAdd(n, *item, item->getItemCount(), flags) ==
2770 RETURNVALUE_NOERROR) {
2771 index = n;
2772 *destItem = nullptr;
2773 return tmpContainer;
2774 }
2775 }
2776
2777 return this;
2778 }
2779
2780 Thing *destThing = getThing(index);
2781 if (destThing) {
2782 *destItem = destThing->getItem();
2783 }
2784
2785 Cylinder *subCylinder = dynamic_cast<Cylinder *>(destThing);
2786 if (subCylinder) {
2787 index = INDEX_WHEREEVER;
2788 *destItem = nullptr;
2789 return subCylinder;
2790 } else {
2791 return this;
2792 }
2793}
2794
2795void Player::addThing(int32_t index, Thing *thing) {
2796 if (index < CONST_SLOT_FIRST || index > CONST_SLOT_LAST) {
2797 return /*RETURNVALUE_NOTPOSSIBLE*/;
2798 }
2799
2800 Item *item = thing->getItem();
2801 if (!item) {
2802 return /*RETURNVALUE_NOTPOSSIBLE*/;
2803 }
2804
2805 item->setParent(this);
2806 inventory[index] = item;
2807
2808 // send to client
2809 sendInventoryItem(static_cast<slots_t>(index), item);
2810}
2811
2812void Player::updateThing(Thing *thing, uint16_t itemId, uint32_t count) {
2813 int32_t index = getThingIndex(thing);
2814 if (index == -1) {
2815 return /*RETURNVALUE_NOTPOSSIBLE*/;
2816 }
2817
2818 Item *item = thing->getItem();
2819 if (!item) {
2820 return /*RETURNVALUE_NOTPOSSIBLE*/;
2821 }
2822
2823 item->setID(itemId);
2824 item->setSubType(count);
2825
2826 // send to client
2827 sendInventoryItem(static_cast<slots_t>(index), item);
2828
2829 // event methods
2830 onUpdateInventoryItem(item, item);
2831}
2832
2833void Player::replaceThing(uint32_t index, Thing *thing) {
2834 if (index > CONST_SLOT_LAST) {
2835 return /*RETURNVALUE_NOTPOSSIBLE*/;
2836 }
2837
2838 Item *oldItem = getInventoryItem(static_cast<slots_t>(index));
2839 if (!oldItem) {
2840 return /*RETURNVALUE_NOTPOSSIBLE*/;
2841 }
2842
2843 Item *item = thing->getItem();
2844 if (!item) {
2845 return /*RETURNVALUE_NOTPOSSIBLE*/;
2846 }
2847
2848 // send to client
2849 sendInventoryItem(static_cast<slots_t>(index), item);
2850
2851 // event methods
2852 onUpdateInventoryItem(oldItem, item);
2853
2854 item->setParent(this);
2855
2856 inventory[index] = item;
2857}
2858
2859void Player::removeThing(Thing *thing, uint32_t count) {
2860 Item *item = thing->getItem();
2861 if (!item) {
2862 return /*RETURNVALUE_NOTPOSSIBLE*/;
2863 }
2864
2865 int32_t index = getThingIndex(thing);
2866 if (index == -1) {
2867 return /*RETURNVALUE_NOTPOSSIBLE*/;
2868 }
2869
2870 if (item->isStackable()) {
2871 if (count == item->getItemCount()) {
2872 // send change to client
2873 sendInventoryItem(static_cast<slots_t>(index), nullptr);
2874
2875 // event methods
2876 onRemoveInventoryItem(item);
2877
2878 item->setParent(nullptr);
2879 inventory[index] = nullptr;
2880 } else {
2881 uint8_t newCount = static_cast<uint8_t>(
2882 std::max<int32_t>(0, item->getItemCount() - count));
2883 item->setItemCount(newCount);
2884
2885 // send change to client
2886 sendInventoryItem(static_cast<slots_t>(index), item);
2887
2888 // event methods
2889 onUpdateInventoryItem(item, item);
2890 }
2891 } else {
2892 // send change to client
2893 sendInventoryItem(static_cast<slots_t>(index), nullptr);
2894
2895 // event methods
2896 onRemoveInventoryItem(item);
2897
2898 item->setParent(nullptr);
2899 inventory[index] = nullptr;
2900 }
2901}
2902
2903int32_t Player::getThingIndex(const Thing *thing) const {
2904 for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
2905 if (inventory[i] == thing) {
2906 return i;
2907 }
2908 }
2909 return -1;
2910}
2911
2912size_t Player::getFirstIndex() const { return CONST_SLOT_FIRST; }
2913
2914size_t Player::getLastIndex() const { return CONST_SLOT_LAST + 1; }
2915
2916uint32_t Player::getItemTypeCount(uint16_t itemId,
2917 int32_t subType /*= -1*/) const {
2918 uint32_t count = 0;
2919 for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) {
2920 Item *item = inventory[i];
2921 if (!item) {
2922 continue;
2923 }
2924
2925 if (item->getID() == itemId) {
2926 count += Item::countByType(item, subType);
2927 }
2928
2929 if (Container *container = item->getContainer()) {
2930 for (ContainerIterator it = container->iterator(); it.hasNext();
2931 it.advance()) {
2932 if ((*it)->getID() == itemId) {
2933 count += Item::countByType(*it, subType);
2934 }
2935 }
2936 }
2937 }
2938 return count;
2939}
2940
2941bool Player::removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType,
2942 bool ignoreEquipped /* = false*/) const {
2943 if (amount == 0) {
2944 return true;
2945 }
2946
2947 std::vector<Item *> itemList;
2948
2949 uint32_t count = 0;
2950 for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) {
2951 Item *item = inventory[i];
2952 if (!item) {
2953 continue;
2954 }
2955
2956 if (!ignoreEquipped && item->getID() == itemId) {
2957 uint32_t itemCount = Item::countByType(item, subType);
2958 if (itemCount == 0) {
2959 continue;
2960 }
2961
2962 itemList.push_back(item);
2963
2964 count += itemCount;
2965 if (count >= amount) {
2966 g_game.internalRemoveItems(std::move(itemList), amount,
2967 Item::items[itemId].stackable);
2968 return true;
2969 }
2970 } else if (Container *container = item->getContainer()) {
2971 for (ContainerIterator it = container->iterator(); it.hasNext();
2972 it.advance()) {
2973 Item *containerItem = *it;
2974 if (containerItem->getID() == itemId) {
2975 uint32_t itemCount = Item::countByType(containerItem, subType);
2976 if (itemCount == 0) {
2977 continue;
2978 }
2979
2980 itemList.push_back(containerItem);
2981
2982 count += itemCount;
2983 if (count >= amount) {
2984 g_game.internalRemoveItems(std::move(itemList), amount,
2985 Item::items[itemId].stackable);
2986 return true;
2987 }
2988 }
2989 }
2990 }
2991 }
2992 return false;
2993}
2994
2995std::map<uint32_t, uint32_t> &
2996Player::getAllItemTypeCount(std::map<uint32_t, uint32_t> &countMap) const {
2997 for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) {
2998 Item *item = inventory[i];
2999 if (!item) {
3000 continue;
3001 }
3002
3003 countMap[item->getID()] += Item::countByType(item, -1);
3004
3005 if (Container *container = item->getContainer()) {
3006 for (ContainerIterator it = container->iterator(); it.hasNext();
3007 it.advance()) {
3008 countMap[(*it)->getID()] += Item::countByType(*it, -1);
3009 }
3010 }
3011 }
3012 return countMap;
3013}
3014
3015Thing *Player::getThing(size_t index) const {
3016 if (index >= CONST_SLOT_FIRST && index <= CONST_SLOT_LAST) {
3017 return inventory[index];
3018 }
3019 return nullptr;
3020}
3021
3022void Player::postAddNotification(Thing *thing, const Cylinder *oldParent,
3023 int32_t index,
3024 cylinderlink_t link /*= LINK_OWNER*/) {
3025 if (link == LINK_OWNER) {
3026 // calling movement scripts
3027 g_moveEvents->onPlayerEquip(this, thing->getItem(),
3028 static_cast<slots_t>(index), false);
3029 }
3030
3031 bool requireListUpdate = false;
3032
3033 if (link == LINK_OWNER || link == LINK_TOPPARENT) {
3034 const Item *i = (oldParent ? oldParent->getItem() : nullptr);
3035
3036 // Check if we owned the old container too, so we don't need to do anything,
3037 // as the list was updated in postRemoveNotification
3038 assert(i ? i->getContainer() != nullptr : true);
3039
3040 if (i) {
3041 requireListUpdate = i->getContainer()->getHoldingPlayer() != this;
3042 } else {
3043 requireListUpdate = oldParent != this;
3044 }
3045
3046 updateInventoryWeight();
3047 updateItemsLight();
3048 sendStats();
3049 }
3050
3051 if (const Item *item = thing->getItem()) {
3052 if (const Container *container = item->getContainer()) {
3053 onSendContainer(container);
3054 }
3055
3056 if (shopOwner && requireListUpdate) {
3057 updateSaleShopList(item);
3058 }
3059 } else if (const Creature *creature = thing->getCreature()) {
3060 if (creature == this) {
3061 // check containers
3062 std::vector<Container *> containers;
3063
3064 for (const auto &it : openContainers) {
3065 Container *container = it.second.container;
3066 if (!Position::areInRange<1, 1, 0>(container->getPosition(),
3067 getPosition())) {
3068 containers.push_back(container);
3069 }
3070 }
3071
3072 for (const Container *container : containers) {
3073 autoCloseContainers(container);
3074 }
3075 }
3076 }
3077}
3078
3079void Player::postRemoveNotification(Thing *thing, const Cylinder *newParent,
3080 int32_t index,
3081 cylinderlink_t link /*= LINK_OWNER*/) {
3082 if (link == LINK_OWNER) {
3083 // calling movement scripts
3084 g_moveEvents->onPlayerDeEquip(this, thing->getItem(),
3085 static_cast<slots_t>(index));
3086 }
3087
3088 bool requireListUpdate = false;
3089
3090 if (link == LINK_OWNER || link == LINK_TOPPARENT) {
3091 const Item *i = (newParent ? newParent->getItem() : nullptr);
3092
3093 // Check if we owned the old container too, so we don't need to do anything,
3094 // as the list was updated in postRemoveNotification
3095 assert(i ? i->getContainer() != nullptr : true);
3096
3097 if (i) {
3098 requireListUpdate = i->getContainer()->getHoldingPlayer() != this;
3099 } else {
3100 requireListUpdate = newParent != this;
3101 }
3102
3103 updateInventoryWeight();
3104 updateItemsLight();
3105 sendStats();
3106 }
3107
3108 if (const Item *item = thing->getItem()) {
3109 if (const Container *container = item->getContainer()) {
3110 if (container->isRemoved() ||
3111 !Position::areInRange<1, 1, 0>(getPosition(),
3112 container->getPosition())) {
3113 autoCloseContainers(container);
3114 } else if (container->getTopParent() == this) {
3115 onSendContainer(container);
3116 } else if (const Container *topContainer =
3117 dynamic_cast<const Container *>(
3118 container->getTopParent())) {
3119 if (const DepotChest *depotChest =
3120 dynamic_cast<const DepotChest *>(topContainer)) {
3121 bool isOwner = false;
3122
3123 for (const auto &it : depotChests) {
3124 if (it.second == depotChest) {
3125 isOwner = true;
3126 onSendContainer(container);
3127 }
3128 }
3129
3130 if (!isOwner) {
3131 autoCloseContainers(container);
3132 }
3133 } else {
3134 onSendContainer(container);
3135 }
3136 } else {
3137 autoCloseContainers(container);
3138 }
3139 }
3140
3141 if (shopOwner && requireListUpdate) {
3142 updateSaleShopList(item);
3143 }
3144 }
3145}
3146
3147bool Player::updateSaleShopList(const Item *item) {
3148 uint16_t itemId = item->getID();
3149 if (itemId != ITEM_GOLD_COIN && itemId != ITEM_PLATINUM_COIN &&
3150 itemId != ITEM_CRYSTAL_COIN) {
3151 auto it = std::find_if(shopItemList.begin(), shopItemList.end(),
3152 [itemId](const ShopInfo &shopInfo) {
3153 return shopInfo.itemId == itemId &&
3154 shopInfo.sellPrice != 0;
3155 });
3156 if (it == shopItemList.end()) {
3157 const Container *container = item->getContainer();
3158 if (!container) {
3159 return false;
3160 }
3161
3162 const auto &items = container->getItemList();
3163 return std::any_of(items.begin(), items.end(),
3164 [this](const Item *containerItem) {
3165 return updateSaleShopList(containerItem);
3166 });
3167 }
3168 }
3169
3170 if (client) {
3171 client->sendSaleItemList(shopItemList);
3172 }
3173 return true;
3174}
3175
3176bool Player::hasShopItemForSale(uint32_t itemId, uint8_t subType) const {
3177 const ItemType &itemType = Item::items[itemId];
3178 return std::any_of(
3179 shopItemList.begin(), shopItemList.end(), [&](const ShopInfo &shopInfo) {
3180 return shopInfo.itemId == itemId && shopInfo.buyPrice != 0 &&
3181 (!itemType.isFluidContainer() || shopInfo.subType == subType);
3182 });
3183}
3184
3185void Player::internalAddThing(Thing *thing) { internalAddThing(0, thing); }
3186
3187void Player::internalAddThing(uint32_t index, Thing *thing) {
3188 Item *item = thing->getItem();
3189 if (!item) {
3190 return;
3191 }
3192
3193 // index == 0 means we should equip this item at the most appropiate slot (no
3194 // action required here)
3195 if (index > CONST_SLOT_WHEREEVER && index <= CONST_SLOT_LAST) {
3196 if (inventory[index]) {
3197 return;
3198 }
3199
3200 inventory[index] = item;
3201 item->setParent(this);
3202 }
3203}
3204
3205bool Player::setFollowCreature(Creature *creature) {
3206 if (!Creature::setFollowCreature(creature)) {
3207 setFollowCreature(nullptr);
3208 setAttackedCreature(nullptr);
3209
3210 sendCancelMessage(RETURNVALUE_THEREISNOWAY);
3211 sendCancelTarget();
3212 stopWalk();
3213 return false;
3214 }
3215 return true;
3216}
3217
3218bool Player::setAttackedCreature(Creature *creature) {
3219 if (!Creature::setAttackedCreature(creature)) {
3220 sendCancelTarget();
3221 return false;
3222 }
3223
3224 if (chaseMode && creature) {
3225 if (followCreature != creature) {
3226 // chase opponent
3227 setFollowCreature(creature);
3228 }
3229 } else if (followCreature) {
3230 setFollowCreature(nullptr);
3231 }
3232
3233 if (creature) {
3234 g_dispatcher.addTask(
3235 createTask(std::bind(&Game::checkCreatureAttack, &g_game, getID())));
3236 }
3237 return true;
3238}
3239
3240void Player::goToFollowCreature() {
3241 if (!walkTask) {
3242 if ((OTSYS_TIME() - lastFailedFollow) < 2000) {
3243 return;
3244 }
3245
3246 Creature::goToFollowCreature();
3247
3248 if (followCreature && !hasFollowPath) {
3249 lastFailedFollow = OTSYS_TIME();
3250 }
3251 }
3252}
3253
3254void Player::getPathSearchParams(const Creature *creature,
3255 FindPathParams &fpp) const {
3256 Creature::getPathSearchParams(creature, fpp);
3257
3258 const Outfit *outfit = Outfits::getInstance().getOutfitByLookType(
3259 getSex(), currentOutfit.lookType);
3260 if (outfit && outfit->attackRange > 1) {
3261 fpp.maxTargetDist = outfit->attackRange;
3262 }
3263
3264 fpp.fullPathSearch = true;
3265}
3266
3267void Player::doAttacking(uint32_t) {
3268 if (lastAttack == 0) {
3269 lastAttack = OTSYS_TIME() - getAttackSpeed() - 1;
3270 }
3271
3272 if (hasCondition(CONDITION_PACIFIED)) {
3273 return;
3274 }
3275
3276 if ((OTSYS_TIME() - lastAttack) >= getAttackSpeed()) {
3277 if (attackedCreature) {
3278 const Position &myPos = getPosition();
3279 const Position &targetPos = attackedCreature->getPosition();
3280 int32_t diffX = targetPos.x - myPos.x;
3281 int32_t diffY = targetPos.y - myPos.y;
3282
3283 Direction dir = DIRECTION_NONE;
3284 int32_t absX = std::abs(diffX);
3285 int32_t absY = std::abs(diffY);
3286 if (absX > 0 || absY > 0) {
3287 if (absX > absY * 2) {
3288 dir = (diffX > 0) ? DIRECTION_EAST : DIRECTION_WEST;
3289 } else if (absY > absX * 2) {
3290 dir = (diffY > 0) ? DIRECTION_SOUTH : DIRECTION_NORTH;
3291 } else {
3292 // Diagonal or close to diagonal
3293 if (diffX > 0 && diffY < 0)
3294 dir = DIRECTION_NORTHEAST;
3295 else if (diffX > 0 && diffY > 0)
3296 dir = DIRECTION_SOUTHEAST;
3297 else if (diffX < 0 && diffY < 0)
3298 dir = DIRECTION_NORTHWEST;
3299 else if (diffX < 0 && diffY > 0)
3300 dir = DIRECTION_SOUTHWEST;
3301 }
3302 }
3303
3304 if (dir != DIRECTION_NONE) {
3305 if (!g_game.internalCreatureTurn(this, dir)) {
3306 // If already turned on server, force a resync to the client in case
3307 // it was ignored during walking.
3308 sendCreatureTurn(this);
3309 }
3310 }
3311 }
3312
3313 bool result = false;
3314
3315 Item *tool = getWeapon();
3316 const Weapon *weapon = g_weapons->getWeapon(tool);
3317 uint32_t delay = getAttackSpeed();
3318 if (weapon) {
3319 // Optimizamos para fluidez: Permitimos que el arma se use incluso si
3320 // estamos caminando (canDoAction es false al caminar) para que el ataque
3321 // no se "cancele" por el movimiento. El ritmo real de ataque lo sigue
3322 // dictando lastAttack.
3323 result = weapon->useWeapon(this, tool, attackedCreature);
3324 } else {
3325 result = Weapon::useFist(this, attackedCreature);
3326 }
3327
3328 SchedulerTask *task = createSchedulerTask(
3329 std::max<uint32_t>(SCHEDULER_MINTICKS, delay),
3330 std::bind(&Game::checkCreatureAttack, &g_game, getID()));
3331 // Usamos el scheduler directamente para que el ataque sea independiente del
3332 // sistema de acciones globales (pociones, caminar), evitando que caminar
3333 // cancele el ataque.
3334 g_scheduler.addEvent(task);
3335
3336 if (result) {
3337 lastAttack = OTSYS_TIME();
3338
3339 uint16_t attackLookType = getAttackOutfitLookType();
3340 if (attackLookType != 0) {
3341 Outfit_t combatOutfit = currentOutfit;
3342 combatOutfit.lookType = attackLookType;
3343 // Usamos getAttackSpeed() para que el pulso dure lo que dura el
3344 // cooldown
3345 pulseAttackOutfit(combatOutfit, getAttackSpeed());
3346 }
3347 }
3348 }
3349}
3350
3351uint64_t Player::getGainedExperience(Creature *attacker) const {
3352 if (g_config.getBoolean(ConfigManager::EXPERIENCE_FROM_PLAYERS)) {
3353 Player *attackerPlayer = attacker->getPlayer();
3354 if (attackerPlayer && attackerPlayer != this && skillLoss &&
3355 std::abs(static_cast<int32_t>(attackerPlayer->getLevel() - level)) <=
3356 g_config.getNumber(ConfigManager::EXP_FROM_PLAYERS_LEVEL_RANGE)) {
3357 return std::max<uint64_t>(
3358 0, std::floor(getLostExperience() * getDamageRatio(attacker) * 0.75));
3359 }
3360 }
3361 return 0;
3362}
3363
3364void Player::onFollowCreature(const Creature *creature) {
3365 if (!creature) {
3366 stopWalk();
3367 }
3368}
3369
3370void Player::setChaseMode(bool mode) {
3371 bool prevChaseMode = chaseMode;
3372 chaseMode = mode;
3373
3374 if (prevChaseMode != chaseMode) {
3375 if (chaseMode) {
3376 if (!followCreature && attackedCreature) {
3377 // chase opponent
3378 setFollowCreature(attackedCreature);
3379 }
3380 } else if (attackedCreature) {
3381 setFollowCreature(nullptr);
3382 cancelNextWalk = true;
3383 }
3384 }
3385}
3386
3387void Player::onWalkAborted() {
3388 setNextWalkActionTask(nullptr);
3389 sendCancelWalk();
3390 sendNewCancelWalk();
3391}
3392
3393void Player::onWalkComplete() {
3394 if (walkTask) {
3395 walkTaskEvent = g_scheduler.addEvent(walkTask);
3396 walkTask = nullptr;
3397 }
3398}
3399
3400void Player::stopWalk() { cancelNextWalk = true; }
3401
3402LightInfo Player::getCreatureLight() const {
3403 if (internalLight.level > itemsLight.level) {
3404 return internalLight;
3405 }
3406 return itemsLight;
3407}
3408
3409void Player::updateItemsLight(bool internal /*=false*/) {
3410 LightInfo maxLight;
3411
3412 for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
3413 Item *item = inventory[i];
3414 if (item) {
3415 LightInfo curLight = item->getLightInfo();
3416
3417 if (curLight.level > maxLight.level) {
3418 maxLight = std::move(curLight);
3419 }
3420 }
3421 }
3422
3423 if (itemsLight.level != maxLight.level ||
3424 itemsLight.color != maxLight.color) {
3425 itemsLight = maxLight;
3426
3427 if (!internal) {
3428 g_game.changeLight(this);
3429 }
3430 }
3431}
3432
3433void Player::onAddCondition(ConditionType_t type) {
3434 Creature::onAddCondition(type);
3435
3436 if (type == CONDITION_OUTFIT) {
3437 dismount();
3438 }
3439
3440 sendIcons();
3441}
3442
3443void Player::onAddCombatCondition(ConditionType_t type) {
3444 switch (type) {
3445 case CONDITION_POISON:
3446 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are poisoned.");
3447 break;
3448
3449 case CONDITION_DROWN:
3450 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are drowning.");
3451 break;
3452
3453 case CONDITION_PARALYZE:
3454 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are paralyzed.");
3455 break;
3456
3457 case CONDITION_DRUNK:
3458 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are drunk.");
3459 break;
3460
3461 case CONDITION_CURSED:
3462 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are cursed.");
3463 break;
3464
3465 case CONDITION_FREEZING:
3466 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are freezing.");
3467 break;
3468
3469 case CONDITION_DAZZLED:
3470 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are dazzled.");
3471 break;
3472
3473 case CONDITION_BLEEDING:
3474 sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are bleeding.");
3475 break;
3476
3477 default:
3478 break;
3479 }
3480}
3481
3482void Player::onEndCondition(ConditionType_t type) {
3483 Creature::onEndCondition(type);
3484
3485 if (type == CONDITION_INFIGHT) {
3486 onIdleStatus();
3487 pzLocked = false;
3488 clearAttacked();
3489
3490 if (getSkull() != SKULL_RED && getSkull() != SKULL_BLACK) {
3491 setSkull(SKULL_NONE);
3492 }
3493 }
3494
3495 sendIcons();
3496}
3497
3498void Player::onCombatRemoveCondition(Condition *condition) {
3499 // Creature::onCombatRemoveCondition(condition);
3500 if (condition->getId() > 0) {
3501 // Means the condition is from an item, id == slot
3502 if (g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) {
3503 Item *item = getInventoryItem(static_cast<slots_t>(condition->getId()));
3504 if (item) {
3505 // 25% chance to destroy the item
3506 if (25 >= uniform_random(1, 100)) {
3507 g_game.internalRemoveItem(item);
3508 }
3509 }
3510 }
3511 } else {
3512 if (!canDoAction()) {
3513 const uint32_t delay = getNextActionTime();
3514 const int32_t ticks = delay - (delay % EVENT_CREATURE_THINK_INTERVAL);
3515 if (ticks < 0) {
3516 removeCondition(condition);
3517 } else {
3518 condition->setTicks(ticks);
3519 }
3520 } else {
3521 removeCondition(condition);
3522 }
3523 }
3524}
3525
3526void Player::onAttackedCreature(Creature *target,
3527 bool addFightTicks /* = true */) {
3528 Creature::onAttackedCreature(target);
3529
3530 if (target->getZone() == ZONE_PVP) {
3531 return;
3532 }
3533
3534 if (target == this) {
3535 if (addFightTicks) {
3536 addInFightTicks();
3537 }
3538 return;
3539 }
3540
3541 if (hasFlag(PlayerFlag_NotGainInFight)) {
3542 return;
3543 }
3544
3545 Player *targetPlayer = target->getPlayer();
3546 if (targetPlayer && !isPartner(targetPlayer) && !isGuildMate(targetPlayer)) {
3547 if (!pzLocked && g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) {
3548 pzLocked = true;
3549 sendIcons();
3550 }
3551
3552 targetPlayer->addInFightTicks();
3553
3554 if (getSkull() == SKULL_NONE &&
3555 getSkullClient(targetPlayer) == SKULL_YELLOW) {
3556 addAttacked(targetPlayer);
3557 targetPlayer->sendCreatureSkull(this);
3558 } else if (!targetPlayer->hasAttacked(this)) {
3559 if (!pzLocked) {
3560 pzLocked = true;
3561 sendIcons();
3562 }
3563
3564 if (!Combat::isInPvpZone(this, targetPlayer) && !isInWar(targetPlayer)) {
3565 addAttacked(targetPlayer);
3566
3567 if (targetPlayer->getSkull() == SKULL_NONE &&
3568 getSkull() == SKULL_NONE) {
3569 setSkull(SKULL_WHITE);
3570 }
3571
3572 if (getSkull() == SKULL_NONE) {
3573 targetPlayer->sendCreatureSkull(this);
3574 }
3575 }
3576 }
3577 }
3578
3579 if (addFightTicks) {
3580 addInFightTicks();
3581 }
3582}
3583
3584void Player::onAttacked() {
3585 Creature::onAttacked();
3586
3587 addInFightTicks();
3588}
3589
3590void Player::onIdleStatus() {
3591 Creature::onIdleStatus();
3592
3593 if (party) {
3594 party->clearPlayerPoints(this);
3595 }
3596}
3597
3598void Player::onPlacedCreature() {
3599 // scripting event - onLogin
3600 if (!g_creatureEvents->playerLogin(this)) {
3601 kickPlayer(true);
3602 }
3603}
3604
3605void Player::onAttackedCreatureDrainHealth(Creature *target, int32_t points) {
3606 Creature::onAttackedCreatureDrainHealth(target, points);
3607
3608 if (target) {
3609 if (party && !Combat::isPlayerCombat(target)) {
3610 Monster *tmpMonster = target->getMonster();
3611 if (tmpMonster && tmpMonster->isHostile()) {
3612 // We have fulfilled a requirement for shared experience
3613 party->updatePlayerTicks(this, points);
3614 }
3615 }
3616 }
3617}
3618
3619void Player::onTargetCreatureGainHealth(Creature *target, int32_t points) {
3620 if (target && party) {
3621 Player *tmpPlayer = nullptr;
3622
3623 if (target->getPlayer()) {
3624 tmpPlayer = target->getPlayer();
3625 } else if (Creature *targetMaster = target->getMaster()) {
3626 if (Player *targetMasterPlayer = targetMaster->getPlayer()) {
3627 tmpPlayer = targetMasterPlayer;
3628 }
3629 }
3630
3631 if (isPartner(tmpPlayer)) {
3632 party->updatePlayerTicks(this, points);
3633 }
3634 }
3635}
3636
3637bool Player::onKilledCreature(Creature *target, bool lastHit /* = true*/) {
3638 bool unjustified = false;
3639
3640 if (hasFlag(PlayerFlag_NotGenerateLoot)) {
3641 target->setDropLoot(false);
3642 }
3643
3644 Creature::onKilledCreature(target, lastHit);
3645
3646 Player *targetPlayer = target->getPlayer();
3647 if (!targetPlayer) {
3648 return false;
3649 }
3650
3651 if (targetPlayer->getZone() == ZONE_PVP) {
3652 targetPlayer->setDropLoot(false);
3653 targetPlayer->setSkillLoss(false);
3654 } else if (!hasFlag(PlayerFlag_NotGainInFight) && !isPartner(targetPlayer)) {
3655 if (!Combat::isInPvpZone(this, targetPlayer) && hasAttacked(targetPlayer) &&
3656 !targetPlayer->hasAttacked(this) && !isGuildMate(targetPlayer) &&
3657 targetPlayer != this) {
3658 if (targetPlayer->getSkull() == SKULL_NONE && !isInWar(targetPlayer)) {
3659 unjustified = true;
3660 addUnjustifiedDead(targetPlayer);
3661 }
3662
3663 if (lastHit && hasCondition(CONDITION_INFIGHT)) {
3664 pzLocked = true;
3665 Condition *condition = Condition::createCondition(
3666 CONDITIONID_DEFAULT, CONDITION_INFIGHT,
3667 g_config.getNumber(ConfigManager::WHITE_SKULL_TIME), 0);
3668 addCondition(condition);
3669 }
3670 }
3671 }
3672
3673 return unjustified;
3674}
3675
3676void Player::gainExperience(uint64_t gainExp, Creature *source) {
3677 if (hasFlag(PlayerFlag_NotGainExperience) || gainExp == 0 ||
3678 staminaMinutes == 0) {
3679 return;
3680 }
3681
3682 addExperience(source, gainExp, true);
3683}
3684
3685void Player::onGainExperience(uint64_t gainExp, Creature *target) {
3686 if (hasFlag(PlayerFlag_NotGainExperience)) {
3687 return;
3688 }
3689
3690 if (target && !target->getPlayer() && party &&
3691 party->isSharedExperienceActive() && party->isSharedExperienceEnabled()) {
3692 party->shareExperience(gainExp, target);
3693 // We will get a share of the experience through the sharing mechanism
3694 return;
3695 }
3696
3697 Creature::onGainExperience(gainExp, target);
3698 gainExperience(gainExp, target);
3699}
3700
3701void Player::onGainSharedExperience(uint64_t gainExp, Creature *source) {
3702 gainExperience(gainExp, source);
3703}
3704
3705bool Player::isImmune(CombatType_t type) const {
3706 if (hasFlag(PlayerFlag_CannotBeAttacked)) {
3707 return true;
3708 }
3709 return Creature::isImmune(type);
3710}
3711
3712bool Player::isImmune(ConditionType_t type) const {
3713 if (hasFlag(PlayerFlag_CannotBeAttacked)) {
3714 return true;
3715 }
3716 return Creature::isImmune(type);
3717}
3718
3719bool Player::isAttackable() const {
3720 return !hasFlag(PlayerFlag_CannotBeAttacked);
3721}
3722
3723bool Player::lastHitIsPlayer(Creature *lastHitCreature) {
3724 if (!lastHitCreature) {
3725 return false;
3726 }
3727
3728 if (lastHitCreature->getPlayer()) {
3729 return true;
3730 }
3731
3732 Creature *lastHitMaster = lastHitCreature->getMaster();
3733 return lastHitMaster && lastHitMaster->getPlayer();
3734}
3735
3736void Player::changeHealth(int32_t healthChange,
3737 bool sendHealthChange /* = true*/) {
3738 Creature::changeHealth(healthChange, sendHealthChange);
3739 sendStats();
3740}
3741
3742void Player::changeMana(int32_t manaChange) {
3743 if (!hasFlag(PlayerFlag_HasInfiniteMana)) {
3744 if (manaChange > 0) {
3745 mana += std::min<int32_t>(manaChange, getMaxMana() - mana);
3746 } else {
3747 mana = std::max<int32_t>(0, mana + manaChange);
3748 }
3749 }
3750
3751 sendStats();
3752}
3753
3754void Player::changeSoul(int32_t soulChange) {
3755 if (soulChange > 0) {
3756 soul += std::min<int32_t>(soulChange, vocation->getSoulMax() - soul);
3757 } else {
3758 soul = std::max<int32_t>(0, soul + soulChange);
3759 }
3760
3761 sendStats();
3762}
3763
3764bool Player::canWear(uint32_t lookType, uint8_t addons) const {
3765 if (group->access) {
3766 return true;
3767 }
3768
3769 const Outfit *outfit =
3770 Outfits::getInstance().getOutfitByLookType(sex, lookType);
3771 if (!outfit) {
3772 return false;
3773 }
3774
3775 if (outfit->premium && !isPremium()) {
3776 return false;
3777 }
3778
3779 if (outfit->unlocked && addons == 0) {
3780 return true;
3781 }
3782
3783 for (const OutfitEntry &outfitEntry : outfits) {
3784 if (outfitEntry.lookType == lookType) {
3785 if (outfitEntry.addons == addons || outfitEntry.addons == 3 ||
3786 addons == 0) {
3787 return true;
3788 }
3789 return false; // have lookType on list and addons don't match
3790 }
3791 }
3792 return false;
3793}
3794
3795bool Player::hasOutfit(uint32_t lookType, uint8_t addons) {
3796 const Outfit *outfit =
3797 Outfits::getInstance().getOutfitByLookType(sex, lookType);
3798 if (!outfit) {
3799 return false;
3800 }
3801
3802 if (outfit->unlocked && addons == 0) {
3803 return true;
3804 }
3805
3806 for (const OutfitEntry &outfitEntry : outfits) {
3807 if (outfitEntry.lookType == lookType) {
3808 if (outfitEntry.addons == addons || outfitEntry.addons == 3 ||
3809 addons == 0) {
3810 return true;
3811 }
3812 return false; // have lookType on list and addons don't match
3813 }
3814 }
3815 return false;
3816}
3817
3818bool Player::canLogout() {
3819 if (isConnecting) {
3820 return false;
3821 }
3822
3823 if (getTile()->hasFlag(TILESTATE_NOLOGOUT)) {
3824 return false;
3825 }
3826
3827 if (getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) {
3828 return true;
3829 }
3830
3831 return !isPzLocked() && !hasCondition(CONDITION_INFIGHT);
3832}
3833
3834void Player::genReservedStorageRange() {
3835 // generate outfits range
3836 uint32_t base_key = PSTRG_OUTFITS_RANGE_START;
3837 for (const OutfitEntry &entry : outfits) {
3838 storageMap[++base_key] = (entry.lookType << 16) | entry.addons;
3839 }
3840}
3841
3842void Player::addOutfit(uint16_t lookType, uint8_t addons) {
3843 for (OutfitEntry &outfitEntry : outfits) {
3844 if (outfitEntry.lookType == lookType) {
3845 outfitEntry.addons |= addons;
3846 return;
3847 }
3848 }
3849 outfits.emplace_back(lookType, addons);
3850}
3851
3852bool Player::removeOutfit(uint16_t lookType) {
3853 for (auto it = outfits.begin(), end = outfits.end(); it != end; ++it) {
3854 OutfitEntry &entry = *it;
3855 if (entry.lookType == lookType) {
3856 outfits.erase(it);
3857 return true;
3858 }
3859 }
3860 return false;
3861}
3862
3863bool Player::removeOutfitAddon(uint16_t lookType, uint8_t addons) {
3864 for (OutfitEntry &outfitEntry : outfits) {
3865 if (outfitEntry.lookType == lookType) {
3866 outfitEntry.addons &= ~addons;
3867 return true;
3868 }
3869 }
3870 return false;
3871}
3872
3873bool Player::getOutfitAddons(const Outfit &outfit, uint8_t &addons) const {
3874 if (group->access) {
3875 addons = 3;
3876 return true;
3877 }
3878
3879 if (outfit.premium && !isPremium()) {
3880 return false;
3881 }
3882
3883 for (const OutfitEntry &outfitEntry : outfits) {
3884 if (outfitEntry.lookType != outfit.lookType) {
3885 continue;
3886 }
3887
3888 addons = outfitEntry.addons;
3889 return true;
3890 }
3891
3892 if (!outfit.unlocked) {
3893 return false;
3894 }
3895
3896 addons = 0;
3897 return true;
3898}
3899
3900void Player::setSex(PlayerSex_t newSex) { sex = newSex; }
3901
3902Skulls_t Player::getSkull() const {
3903 if (hasFlag(PlayerFlag_NotGainInFight)) {
3904 return SKULL_NONE;
3905 }
3906 return skull;
3907}
3908
3909Skulls_t Player::getSkullClient(const Creature *creature) const {
3910 if (!creature || g_game.getWorldType() != WORLD_TYPE_PVP) {
3911 return SKULL_NONE;
3912 }
3913
3914 const Player *player = creature->getPlayer();
3915 if (!player || player->getSkull() != SKULL_NONE) {
3916 return Creature::getSkullClient(creature);
3917 }
3918
3919 if (player->hasAttacked(this)) {
3920 return SKULL_YELLOW;
3921 }
3922
3923 if (isPartner(player)) {
3924 return SKULL_GREEN;
3925 }
3926 return Creature::getSkullClient(creature);
3927}
3928
3929bool Player::hasAttacked(const Player *attacked) const {
3930 if (hasFlag(PlayerFlag_NotGainInFight) || !attacked) {
3931 return false;
3932 }
3933
3934 return attackedSet.find(attacked->guid) != attackedSet.end();
3935}
3936
3937void Player::addAttacked(const Player *attacked) {
3938 if (hasFlag(PlayerFlag_NotGainInFight) || !attacked || attacked == this) {
3939 return;
3940 }
3941
3942 attackedSet.insert(attacked->guid);
3943}
3944
3945void Player::removeAttacked(const Player *attacked) {
3946 if (!attacked || attacked == this) {
3947 return;
3948 }
3949
3950 auto it = attackedSet.find(attacked->guid);
3951 if (it != attackedSet.end()) {
3952 attackedSet.erase(it);
3953 }
3954}
3955
3956void Player::clearAttacked() { attackedSet.clear(); }
3957
3958void Player::addUnjustifiedDead(const Player *attacked) {
3959 if (hasFlag(PlayerFlag_NotGainInFight) || attacked == this ||
3960 g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) {
3961 return;
3962 }
3963
3964 sendTextMessage(MESSAGE_EVENT_ADVANCE, "Warning! The murder of " +
3965 attacked->getName() +
3966 " was not justified.");
3967
3968 skullTicks += g_config.getNumber(ConfigManager::FRAG_TIME);
3969
3970 if (getSkull() != SKULL_BLACK) {
3971 if (g_config.getNumber(ConfigManager::KILLS_TO_BLACK) != 0 &&
3972 skullTicks > (g_config.getNumber(ConfigManager::KILLS_TO_BLACK) - 1) *
3973 static_cast<int64_t>(
3974 g_config.getNumber(ConfigManager::FRAG_TIME))) {
3975 setSkull(SKULL_BLACK);
3976 } else if (getSkull() != SKULL_RED &&
3977 g_config.getNumber(ConfigManager::KILLS_TO_RED) != 0 &&
3978 skullTicks >
3979 (g_config.getNumber(ConfigManager::KILLS_TO_RED) - 1) *
3980 static_cast<int64_t>(
3981 g_config.getNumber(ConfigManager::FRAG_TIME))) {
3982 setSkull(SKULL_RED);
3983 }
3984 }
3985}
3986
3987void Player::checkSkullTicks(int64_t ticks) {
3988 int64_t newTicks = skullTicks - ticks;
3989 if (newTicks < 0) {
3990 skullTicks = 0;
3991 } else {
3992 skullTicks = newTicks;
3993 }
3994
3995 if ((skull == SKULL_RED || skull == SKULL_BLACK) && skullTicks < 1 &&
3996 !hasCondition(CONDITION_INFIGHT)) {
3997 setSkull(SKULL_NONE);
3998 }
3999}
4000
4001bool Player::isPromoted() const {
4002 uint16_t promotedVocation =
4003 g_vocations.getPromotedVocation(vocation->getId());
4004 return promotedVocation == VOCATION_NONE &&
4005 vocation->getId() != promotedVocation;
4006}
4007
4008double Player::getLostPercent() const {
4009 int32_t blessingCount = std::bitset<5>(blessings).count();
4010
4011 int32_t deathLosePercent =
4012 g_config.getNumber(ConfigManager::DEATH_LOSE_PERCENT);
4013 if (deathLosePercent != -1) {
4014 if (isPromoted()) {
4015 deathLosePercent -= 3;
4016 }
4017
4018 deathLosePercent -= blessingCount;
4019 return std::max<int32_t>(0, deathLosePercent) / 100.;
4020 }
4021
4022 double lossPercent;
4023 if (level >= 25) {
4024 double tmpLevel = level + (levelPercent / 100.);
4025 lossPercent =
4026 static_cast<double>((tmpLevel + 50) * 50 *
4027 ((tmpLevel * tmpLevel) - (5 * tmpLevel) + 8)) /
4028 experience;
4029 } else {
4030 lossPercent = 10;
4031 }
4032
4033 double percentReduction = 0;
4034 if (isPromoted()) {
4035 percentReduction += 30;
4036 }
4037 percentReduction += blessingCount * 8;
4038 return lossPercent * (1 - (percentReduction / 100.)) / 100.;
4039}
4040
4041void Player::learnInstantSpell(const std::string &spellName) {
4042 if (!hasLearnedInstantSpell(spellName)) {
4043 learnedInstantSpellList.push_front(spellName);
4044 }
4045}
4046
4047void Player::forgetInstantSpell(const std::string &spellName) {
4048 learnedInstantSpellList.remove(spellName);
4049}
4050
4051bool Player::hasLearnedInstantSpell(const std::string &spellName) const {
4052 if (hasFlag(PlayerFlag_CannotUseSpells)) {
4053 return false;
4054 }
4055
4056 if (hasFlag(PlayerFlag_IgnoreSpellCheck)) {
4057 return true;
4058 }
4059
4060 for (const auto &learnedSpellName : learnedInstantSpellList) {
4061 if (strcasecmp(learnedSpellName.c_str(), spellName.c_str()) == 0) {
4062 return true;
4063 }
4064 }
4065 return false;
4066}
4067
4068bool Player::isInWar(const Player *player) const {
4069 if (!player || !guild) {
4070 return false;
4071 }
4072
4073 const Guild *playerGuild = player->getGuild();
4074 if (!playerGuild) {
4075 return false;
4076 }
4077
4078 return isInWarList(playerGuild->getId()) &&
4079 player->isInWarList(guild->getId());
4080}
4081
4082bool Player::isInWarList(uint32_t guildId) const {
4083 return std::find(guildWarVector.begin(), guildWarVector.end(), guildId) !=
4084 guildWarVector.end();
4085}
4086
4087bool Player::isPremium() const {
4088 if (g_config.getBoolean(ConfigManager::FREE_PREMIUM) ||
4089 hasFlag(PlayerFlag_IsAlwaysPremium)) {
4090 return true;
4091 }
4092
4093 return premiumDays > 0;
4094}
4095
4096void Player::setPremiumDays(int32_t v) {
4097 premiumDays = v;
4098 sendBasicData();
4099}
4100
4101PartyShields_t Player::getPartyShield(const Player *player) const {
4102 if (!player) {
4103 return SHIELD_NONE;
4104 }
4105
4106 if (party) {
4107 if (party->getLeader() == player) {
4108 if (party->isSharedExperienceActive()) {
4109 if (party->isSharedExperienceEnabled()) {
4110 return SHIELD_YELLOW_SHAREDEXP;
4111 }
4112
4113 if (party->canUseSharedExperience(player)) {
4114 return SHIELD_YELLOW_NOSHAREDEXP;
4115 }
4116
4117 return SHIELD_YELLOW_NOSHAREDEXP_BLINK;
4118 }
4119
4120 return SHIELD_YELLOW;
4121 }
4122
4123 if (player->party == party) {
4124 if (party->isSharedExperienceActive()) {
4125 if (party->isSharedExperienceEnabled()) {
4126 return SHIELD_BLUE_SHAREDEXP;
4127 }
4128
4129 if (party->canUseSharedExperience(player)) {
4130 return SHIELD_BLUE_NOSHAREDEXP;
4131 }
4132
4133 return SHIELD_BLUE_NOSHAREDEXP_BLINK;
4134 }
4135
4136 return SHIELD_BLUE;
4137 }
4138
4139 if (isInviting(player)) {
4140 return SHIELD_WHITEBLUE;
4141 }
4142 }
4143
4144 if (player->isInviting(this)) {
4145 return SHIELD_WHITEYELLOW;
4146 }
4147
4148 if (player->party) {
4149 return SHIELD_GRAY;
4150 }
4151
4152 return SHIELD_NONE;
4153}
4154
4155bool Player::isInviting(const Player *player) const {
4156 if (!player || !party || party->getLeader() != this) {
4157 return false;
4158 }
4159 return party->isPlayerInvited(player);
4160}
4161
4162bool Player::isPartner(const Player *player) const {
4163 if (!player || !party || player == this) {
4164 return false;
4165 }
4166 return party == player->party;
4167}
4168
4169bool Player::isGuildMate(const Player *player) const {
4170 if (!player || !guild) {
4171 return false;
4172 }
4173 return guild == player->guild;
4174}
4175
4176void Player::sendPlayerPartyIcons(Player *player) {
4177 sendCreatureShield(player);
4178 sendCreatureSkull(player);
4179}
4180
4181bool Player::addPartyInvitation(Party *party) {
4182 auto it = std::find(invitePartyList.begin(), invitePartyList.end(), party);
4183 if (it != invitePartyList.end()) {
4184 return false;
4185 }
4186
4187 invitePartyList.push_front(party);
4188 return true;
4189}
4190
4191void Player::removePartyInvitation(Party *party) {
4192 invitePartyList.remove(party);
4193}
4194
4195void Player::clearPartyInvitations() {
4196 for (Party *invitingParty : invitePartyList) {
4197 invitingParty->removeInvite(*this, false);
4198 }
4199 invitePartyList.clear();
4200}
4201
4202GuildEmblems_t Player::getGuildEmblem(const Player *player) const {
4203 if (!player) {
4204 return GUILDEMBLEM_NONE;
4205 }
4206
4207 const Guild *playerGuild = player->getGuild();
4208 if (!playerGuild) {
4209 return GUILDEMBLEM_NONE;
4210 }
4211
4212 if (player->getGuildWarVector().empty()) {
4213 if (guild == playerGuild) {
4214 return GUILDEMBLEM_MEMBER;
4215 } else {
4216 return GUILDEMBLEM_OTHER;
4217 }
4218 } else if (guild == playerGuild) {
4219 return GUILDEMBLEM_ALLY;
4220 } else if (isInWar(player)) {
4221 return GUILDEMBLEM_ENEMY;
4222 }
4223
4224 return GUILDEMBLEM_NEUTRAL;
4225}
4226
4227uint8_t Player::getCurrentMount() const {
4228 int32_t value;
4229 if (getStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, value)) {
4230 return value;
4231 }
4232 return 0;
4233}
4234
4235void Player::setCurrentMount(uint8_t mountId) {
4236 addStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, mountId);
4237}
4238
4239bool Player::toggleMount(bool mount) {
4240 if ((OTSYS_TIME() - lastToggleMount) < 3000 && !wasMounted) {
4241 sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED);
4242 return false;
4243 }
4244
4245 if (mount) {
4246 if (isMounted()) {
4247 return false;
4248 }
4249
4250 if (!group->access && tile->hasFlag(TILESTATE_PROTECTIONZONE)) {
4251 sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE);
4252 return false;
4253 }
4254
4255 const Outfit *playerOutfit = Outfits::getInstance().getOutfitByLookType(
4256 getSex(), defaultOutfit.lookType);
4257 if (!playerOutfit) {
4258 return false;
4259 }
4260
4261 uint8_t currentMountId = getCurrentMount();
4262 if (currentMountId == 0) {
4263 sendOutfitWindow();
4264 return false;
4265 }
4266
4267 Mount *currentMount = g_game.mounts.getMountByID(currentMountId);
4268 if (!currentMount) {
4269 return false;
4270 }
4271
4272 if (!hasMount(currentMount)) {
4273 setCurrentMount(0);
4274 sendOutfitWindow();
4275 return false;
4276 }
4277
4278 if (currentMount->premium && !isPremium()) {
4279 sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT);
4280 return false;
4281 }
4282
4283 if (hasCondition(CONDITION_OUTFIT)) {
4284 sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
4285 return false;
4286 }
4287
4288 defaultOutfit.lookMount = currentMount->clientId;
4289
4290 if (currentMount->speed != 0) {
4291 g_game.changeSpeed(this, currentMount->speed);
4292 }
4293 } else {
4294 if (!isMounted()) {
4295 return false;
4296 }
4297
4298 dismount();
4299 }
4300
4301 g_game.internalCreatureChangeOutfit(this, defaultOutfit);
4302 lastToggleMount = OTSYS_TIME();
4303 return true;
4304}
4305
4306bool Player::tameMount(uint8_t mountId) {
4307 if (!g_game.mounts.getMountByID(mountId)) {
4308 return false;
4309 }
4310
4311 const uint8_t tmpMountId = mountId - 1;
4312 const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31);
4313
4314 int32_t value;
4315 if (getStorageValue(key, value)) {
4316 value |= (1 << (tmpMountId % 31));
4317 } else {
4318 value = (1 << (tmpMountId % 31));
4319 }
4320
4321 addStorageValue(key, value);
4322 return true;
4323}
4324
4325bool Player::untameMount(uint8_t mountId) {
4326 if (!g_game.mounts.getMountByID(mountId)) {
4327 return false;
4328 }
4329
4330 const uint8_t tmpMountId = mountId - 1;
4331 const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31);
4332
4333 int32_t value;
4334 if (!getStorageValue(key, value)) {
4335 return true;
4336 }
4337
4338 value &= ~(1 << (tmpMountId % 31));
4339 addStorageValue(key, value);
4340
4341 if (getCurrentMount() == mountId) {
4342 if (isMounted()) {
4343 dismount();
4344 g_game.internalCreatureChangeOutfit(this, defaultOutfit);
4345 }
4346
4347 setCurrentMount(0);
4348 }
4349
4350 return true;
4351}
4352
4353bool Player::hasMount(const Mount *mount) const {
4354 if (isAccessPlayer()) {
4355 return true;
4356 }
4357
4358 if (mount->premium && !isPremium()) {
4359 return false;
4360 }
4361
4362 const uint8_t tmpMountId = mount->id - 1;
4363
4364 int32_t value;
4365 if (!getStorageValue(PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31), value)) {
4366 return false;
4367 }
4368
4369 return ((1 << (tmpMountId % 31)) & value) != 0;
4370}
4371
4372void Player::dismount() {
4373 Mount *mount = g_game.mounts.getMountByID(getCurrentMount());
4374 if (mount && mount->speed > 0) {
4375 g_game.changeSpeed(this, -mount->speed);
4376 }
4377
4378 defaultOutfit.lookMount = 0;
4379}
4380
4381bool Player::addWing(uint8_t wingId) {
4382 if (!g_game.wings.getWingByID(wingId)) {
4383 return false;
4384 }
4385
4386 const uint8_t tmpWingId = wingId - 1;
4387 const uint32_t key = PSTRG_WINGS_RANGE_START + (tmpWingId / 31);
4388
4389 int32_t value;
4390 if (getStorageValue(key, value)) {
4391 value |= (1 << (tmpWingId % 31));
4392 } else {
4393 value = (1 << (tmpWingId % 31));
4394 }
4395
4396 addStorageValue(key, value);
4397 return true;
4398}
4399
4400bool Player::hasWing(const Wing *wing) const {
4401 if (isAccessPlayer()) {
4402 return true;
4403 }
4404
4405 if (wing->premium && !isPremium()) {
4406 return false;
4407 }
4408
4409 const uint8_t tmpWingId = wing->id - 1;
4410
4411 int32_t value;
4412 if (!getStorageValue(PSTRG_WINGS_RANGE_START + (tmpWingId / 31), value)) {
4413 return false;
4414 }
4415
4416 return ((1 << (tmpWingId % 31)) & value) != 0;
4417}
4418
4419uint8_t Player::getCurrentWing() const {
4420 int32_t value;
4421 if (getStorageValue(PSTRG_WINGS_CURRENTWINGS, value)) {
4422 return value;
4423 }
4424 return 0;
4425}
4426
4427void Player::setCurrentWing(uint8_t wingId) {
4428 addStorageValue(PSTRG_WINGS_CURRENTWINGS, wingId);
4429}
4430
4431bool Player::addAura(uint8_t auraId) {
4432 if (!g_game.auras.getAuraByID(auraId)) {
4433 return false;
4434 }
4435
4436 const uint8_t tmpAuraId = auraId - 1;
4437 const uint32_t key = PSTRG_AURAS_RANGE_START + (tmpAuraId / 31);
4438
4439 int32_t value;
4440 if (getStorageValue(key, value)) {
4441 value |= (1 << (tmpAuraId % 31));
4442 } else {
4443 value = (1 << (tmpAuraId % 31));
4444 }
4445
4446 addStorageValue(key, value);
4447 return true;
4448}
4449
4450bool Player::hasAura(const Aura *aura) const {
4451 if (isAccessPlayer()) {
4452 return true;
4453 }
4454
4455 if (aura->premium && !isPremium()) {
4456 return false;
4457 }
4458
4459 const uint8_t tmpAuraId = aura->id - 1;
4460
4461 int32_t value;
4462 if (!getStorageValue(PSTRG_AURAS_RANGE_START + (tmpAuraId / 31), value)) {
4463 return false;
4464 }
4465
4466 return ((1 << (tmpAuraId % 31)) & value) != 0;
4467}
4468
4469uint8_t Player::getCurrentAura() const {
4470 int32_t value;
4471 if (getStorageValue(PSTRG_AURAS_CURRENTAURA, value)) {
4472 return value;
4473 }
4474 return 0;
4475}
4476
4477void Player::setCurrentAura(uint8_t auraId) {
4478 addStorageValue(PSTRG_AURAS_CURRENTAURA, auraId);
4479}
4480
4481bool Player::addShader(uint8_t shaderId) {
4482 if (!g_game.shaders.getShaderByID(shaderId)) {
4483 return false;
4484 }
4485
4486 const uint8_t tmpShaderId = shaderId - 1;
4487 const uint32_t key = PSTRG_SHADERS_RANGE_START + (tmpShaderId / 31);
4488
4489 int32_t value;
4490 if (getStorageValue(key, value)) {
4491 value |= (1 << (tmpShaderId % 31));
4492 } else {
4493 value = (1 << (tmpShaderId % 31));
4494 }
4495
4496 addStorageValue(key, value);
4497 return true;
4498}
4499
4500bool Player::hasShader(const Shader *shader) const {
4501 if (isAccessPlayer()) {
4502 return true;
4503 }
4504
4505 if (shader->premium && !isPremium()) {
4506 return false;
4507 }
4508
4509 const uint8_t tmpShaderId = shader->id - 1;
4510
4511 int32_t value;
4512 if (!getStorageValue(PSTRG_SHADERS_RANGE_START + (tmpShaderId / 31), value)) {
4513 return false;
4514 }
4515
4516 return ((1 << (tmpShaderId % 31)) & value) != 0;
4517}
4518
4519bool Player::addRuneEffect(uint8_t runeEffectId) {
4520 if (!g_game.runeEffects.getRuneEffectByID(runeEffectId)) {
4521 return false;
4522 }
4523
4524 const uint8_t tmpId = runeEffectId - 1;
4525 const uint32_t key = PSTRG_EFFECTS_RANGE_START + (tmpId / 31);
4526
4527 int32_t value;
4528 if (getStorageValue(key, value)) {
4529 value |= (1 << (tmpId % 31));
4530 } else {
4531 value = (1 << (tmpId % 31));
4532 }
4533
4534 addStorageValue(key, value);
4535 return true;
4536}
4537
4538bool Player::hasRuneEffect(const RuneEffect *runeEffect) const {
4539 if (isAccessPlayer()) {
4540 return true;
4541 }
4542
4543 if (runeEffect->premium && !isPremium()) {
4544 return false;
4545 }
4546
4547 const uint8_t tmpId = runeEffect->id - 1;
4548
4549 int32_t value;
4550 if (!getStorageValue(PSTRG_EFFECTS_RANGE_START + (tmpId / 31), value)) {
4551 return false;
4552 }
4553
4554 return ((1 << (tmpId % 31)) & value) != 0;
4555}
4556
4557uint8_t Player::getCurrentRuneEffect() const {
4558 int32_t value;
4559 if (getStorageValue(PSTRG_EFFECTS_CURRENTEFFECT, value)) {
4560 return value;
4561 }
4562 return 0;
4563}
4564
4565void Player::setCurrentRuneEffect(uint8_t runeEffectId) {
4566 addStorageValue(PSTRG_EFFECTS_CURRENTEFFECT, runeEffectId);
4567}
4568
4569int32_t Player::calculateRuneBaseDamage() const {
4570 int32_t maxSkill = 0;
4571 for (skills_t skill = SKILL_FIRST; skill <= SKILL_DISTANCE; skill = static_cast<skills_t>(skill + 1)) {
4572 int32_t s = getSkillLevel(skill);
4573 if (s > maxSkill) {
4574 maxSkill = s;
4575 }
4576 }
4577
4578 int32_t magicLevel = static_cast<int32_t>(getMagicLevel());
4579 if (magicLevel > maxSkill) {
4580 maxSkill = magicLevel;
4581 }
4582
4583 if (maxSkill < 1) {
4584 maxSkill = 1;
4585 }
4586
4587 return Weapons::getMaxWeaponDamage(getLevel(), maxSkill, 100, 1.0f);
4588}
4589
4590int32_t Player::getStepSpeed() const {
4591 int32_t speed = getSpeed();
4592 RuneEffect* activeEffect = g_game.runeEffects.getRuneEffectByID(getCurrentRuneEffect());
4593 if (activeEffect) {
4594 speed += activeEffect->bonusSpeed;
4595 }
4596 return std::max<int32_t>(PLAYER_MIN_SPEED,
4597 std::min<int32_t>(PLAYER_MAX_SPEED, speed));
4598}
4599
4600bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) {
4601 if (tries == 0 || skill == SKILL_LEVEL) {
4602 return false;
4603 }
4604
4605 bool sendUpdate = false;
4606 uint32_t oldSkillValue, newSkillValue;
4607 long double oldPercentToNextLevel, newPercentToNextLevel;
4608
4609 if (skill == SKILL_MAGLEVEL) {
4610 uint64_t currReqMana = vocation->getReqMana(magLevel);
4611 uint64_t nextReqMana = vocation->getReqMana(magLevel + 1);
4612
4613 if (currReqMana >= nextReqMana) {
4614 return false;
4615 }
4616
4617 oldSkillValue = magLevel;
4618 oldPercentToNextLevel =
4619 static_cast<long double>(manaSpent * 100) / nextReqMana;
4620
4621 g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, tries);
4622 uint32_t currMagLevel = magLevel;
4623
4624 while ((manaSpent + tries) >= nextReqMana) {
4625 tries -= nextReqMana - manaSpent;
4626
4627 magLevel++;
4628 manaSpent = 0;
4629
4630 g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1,
4631 magLevel);
4632
4633 sendUpdate = true;
4634 currReqMana = nextReqMana;
4635 nextReqMana = vocation->getReqMana(magLevel + 1);
4636
4637 if (currReqMana >= nextReqMana) {
4638 tries = 0;
4639 break;
4640 }
4641 }
4642
4643 manaSpent += tries;
4644
4645 if (magLevel != currMagLevel) {
4646 std::ostringstream ss;
4647 // ss << "You advanced to magic level " << magLevel << '.';
4648 // sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
4649 }
4650
4651 uint8_t newPercent;
4652 if (nextReqMana > currReqMana) {
4653 newPercent = Player::getPercentLevel(manaSpent, nextReqMana);
4654 newPercentToNextLevel =
4655 static_cast<long double>(manaSpent * 100) / nextReqMana;
4656 } else {
4657 newPercent = 0;
4658 newPercentToNextLevel = 0;
4659 }
4660
4661 if (newPercent != magLevelPercent) {
4662 magLevelPercent = newPercent;
4663 sendUpdate = true;
4664 }
4665
4666 newSkillValue = magLevel;
4667 } else {
4668 uint64_t currReqTries =
4669 vocation->getReqSkillTries(skill, skills[skill].level);
4670 uint64_t nextReqTries =
4671 vocation->getReqSkillTries(skill, skills[skill].level + 1);
4672 if (currReqTries >= nextReqTries) {
4673 return false;
4674 }
4675
4676 oldSkillValue = skills[skill].level;
4677 oldPercentToNextLevel =
4678 static_cast<long double>(skills[skill].tries * 100) / nextReqTries;
4679
4680 g_events->eventPlayerOnGainSkillTries(this, skill, tries);
4681 uint32_t currSkillLevel = skills[skill].level;
4682
4683 while ((skills[skill].tries + tries) >= nextReqTries) {
4684 tries -= nextReqTries - skills[skill].tries;
4685
4686 skills[skill].level++;
4687 skills[skill].tries = 0;
4688 skills[skill].percent = 0;
4689
4690 g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1),
4691 skills[skill].level);
4692
4693 sendUpdate = true;
4694 currReqTries = nextReqTries;
4695 nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1);
4696
4697 if (currReqTries >= nextReqTries) {
4698 tries = 0;
4699 break;
4700 }
4701 }
4702
4703 skills[skill].tries += tries;
4704
4705 if (currSkillLevel != skills[skill].level) {
4706 std::ostringstream ss;
4707 // ss << "You advanced to " << getSkillName(skill) << " level " <<
4708 // skills[skill].level << '.'; sendTextMessage(MESSAGE_EVENT_ADVANCE,
4709 // ss.str());
4710 }
4711
4712 uint8_t newPercent;
4713 if (nextReqTries > currReqTries) {
4714 newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries);
4715 newPercentToNextLevel =
4716 static_cast<long double>(skills[skill].tries * 100) / nextReqTries;
4717 } else {
4718 newPercent = 0;
4719 newPercentToNextLevel = 0;
4720 }
4721
4722 if (skills[skill].percent != newPercent) {
4723 skills[skill].percent = newPercent;
4724 sendUpdate = true;
4725 }
4726
4727 newSkillValue = skills[skill].level;
4728 }
4729
4730 if (sendUpdate) {
4731 sendSkills();
4732 }
4733
4734 std::ostringstream ss;
4735 ss << std::fixed << std::setprecision(2) << "Your "
4736 << ucwords(getSkillName(skill)) << " skill changed from level "
4737 << oldSkillValue << " (with " << oldPercentToNextLevel
4738 << "% progress towards level " << (oldSkillValue + 1) << ") to level "
4739 << newSkillValue << " (with " << newPercentToNextLevel
4740 << "% progress towards level " << (newSkillValue + 1) << ')';
4741 sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
4742 return sendUpdate;
4743}
4744
4745bool Player::hasModalWindowOpen(uint32_t modalWindowId) const {
4746 return find(modalWindows.begin(), modalWindows.end(), modalWindowId) !=
4747 modalWindows.end();
4748}
4749
4750void Player::onModalWindowHandled(uint32_t modalWindowId) {
4751 modalWindows.remove(modalWindowId);
4752}
4753
4754void Player::sendModalWindow(const ModalWindow &modalWindow) {
4755 if (!client) {
4756 return;
4757 }
4758
4759 modalWindows.push_front(modalWindow.id);
4760 client->sendModalWindow(modalWindow);
4761}
4762
4763void Player::clearModalWindows() { modalWindows.clear(); }
4764
4765uint16_t Player::getHelpers() const {
4766 uint16_t helpers;
4767
4768 if (guild && party) {
4769 std::unordered_set<Player *> helperSet;
4770
4771 const auto &guildMembers = guild->getMembersOnline();
4772 helperSet.insert(guildMembers.begin(), guildMembers.end());
4773
4774 const auto &partyMembers = party->getMembers();
4775 helperSet.insert(partyMembers.begin(), partyMembers.end());
4776
4777 const auto &partyInvitees = party->getInvitees();
4778 helperSet.insert(partyInvitees.begin(), partyInvitees.end());
4779
4780 helperSet.insert(party->getLeader());
4781
4782 helpers = helperSet.size();
4783 } else if (guild) {
4784 helpers = guild->getMembersOnline().size();
4785 } else if (party) {
4786 helpers = party->getMemberCount() + party->getInvitationCount() + 1;
4787 } else {
4788 helpers = 0;
4789 }
4790
4791 return helpers;
4792}
4793
4794void Player::sendClosePrivate(uint16_t channelId) {
4795 if (channelId == CHANNEL_GUILD || channelId == CHANNEL_PARTY) {
4796 g_chat->removeUserFromChannel(*this, channelId);
4797 }
4798
4799 if (client) {
4800 client->sendClosePrivate(channelId);
4801 }
4802}
4803
4804uint64_t Player::getMoney() const {
4805 std::vector<const Container *> containers;
4806 uint64_t moneyCount = 0;
4807
4808 for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
4809 Item *item = inventory[i];
4810 if (!item) {
4811 continue;
4812 }
4813
4814 const Container *container = item->getContainer();
4815 if (container) {
4816 containers.push_back(container);
4817 } else {
4818 moneyCount += item->getWorth();
4819 }
4820 }
4821
4822 size_t i = 0;
4823 while (i < containers.size()) {
4824 const Container *container = containers[i++];
4825 for (const Item *item : container->getItemList()) {
4826 const Container *tmpContainer = item->getContainer();
4827 if (tmpContainer) {
4828 containers.push_back(tmpContainer);
4829 } else {
4830 moneyCount += item->getWorth();
4831 }
4832 }
4833 }
4834 return moneyCount;
4835}
4836
4837size_t Player::getMaxVIPEntries() const {
4838 if (group->maxVipEntries != 0) {
4839 return group->maxVipEntries;
4840 } else if (isPremium()) {
4841 return 100;
4842 }
4843 return 20;
4844}
4845
4846size_t Player::getMaxDepotItems() const {
4847 if (group->maxDepotItems != 0) {
4848 return group->maxDepotItems;
4849 } else if (isPremium()) {
4850 return 2000;
4851 }
4852 return 1000;
4853}
4854
4855std::forward_list<Condition *> Player::getMuteConditions() const {
4856 std::forward_list<Condition *> muteConditions;
4857 for (Condition *condition : conditions) {
4858 if (condition->getTicks() <= 0) {
4859 continue;
4860 }
4861
4862 ConditionType_t type = condition->getType();
4863 if (type != CONDITION_MUTED && type != CONDITION_CHANNELMUTEDTICKS &&
4864 type != CONDITION_YELLTICKS) {
4865 continue;
4866 }
4867
4868 muteConditions.push_front(condition);
4869 }
4870 return muteConditions;
4871}
4872
4873void Player::setGuild(Guild *guild) {
4874 if (guild == this->guild) {
4875 return;
4876 }
4877
4878 Guild *oldGuild = this->guild;
4879
4880 this->guildNick.clear();
4881 this->guild = nullptr;
4882 this->guildRank = nullptr;
4883
4884 if (guild) {
4885 GuildRank_ptr rank = guild->getRankByLevel(1);
4886 if (!rank) {
4887 return;
4888 }
4889
4890 this->guild = guild;
4891 this->guildRank = rank;
4892 guild->addMember(this);
4893 }
4894
4895 if (oldGuild) {
4896 oldGuild->removeMember(this);
4897 }
4898}
4899
4900uint16_t Player::getAttackOutfitLookType() const {
4901 if (attackOutfit.lookType != 0) {
4902 return attackOutfit.lookType;
4903 }
4904
4905 // Obtenemos el sexo y buscamos el outfit
4906 PlayerSex_t sex = getSex();
4907 const Outfit *outfit =
4908 Outfits::getInstance().getOutfitByLookType(sex, currentOutfit.lookType);
4909
4910 // Fallback para GMs o trajes que podrían estar en la otra categoría
4911 if (!outfit) {
4912 outfit = Outfits::getInstance().getOutfitByLookType(
4913 (sex == PLAYERSEX_FEMALE ? PLAYERSEX_MALE : PLAYERSEX_FEMALE),
4914 currentOutfit.lookType);
4915 }
4916
4917 if (outfit) {
4918 if (attackedCreature) {
4919 const Position &myPos = getPosition();
4920 const Position &targetPos = attackedCreature->getPosition();
4921 int32_t dx = std::abs(static_cast<int32_t>(myPos.x) -
4922 static_cast<int32_t>(targetPos.x));
4923 int32_t dy = std::abs(static_cast<int32_t>(myPos.y) -
4924 static_cast<int32_t>(targetPos.y));
4925 uint32_t dist = std::max<uint32_t>(static_cast<uint32_t>(dx),
4926 static_cast<uint32_t>(dy));
4927
4928 if (dist <= 1) {
4929 return outfit->attackMeleeType;
4930 }
4931
4932 if (dist <= outfit->attackRange) {
4933 return outfit->attackRangeType;
4934 }
4935
4936 // LOG DE AYUDA: Esto te dirá exactamente qué está pasando en la consola
4937 // negra
4938 std::cout << "[Attack Debug] Outfit: " << outfit->name
4939 << " | Distancia: " << dist
4940 << " | Rango Max XML: " << (int)outfit->attackRange
4941 << std::endl;
4942 return 0; // Fuera de rango
4943 } else {
4944 return outfit->attackMeleeType;
4945 }
4946 }
4947 return 0;
4948}
4949
Top