/* EQEmu: EQEmulator Copyright (C) 2001-2026 EQEmu Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "forage.h" #include "common/eqemu_logsys.h" #include "common/events/player_event_logs.h" #include "common/misc_functions.h" #include "common/repositories/criteria/content_filter_criteria.h" #include "common/repositories/fishing_repository.h" #include "common/repositories/forage_repository.h" #include "common/rulesys.h" #include "common/strings.h" #include "zone/entity.h" #include "zone/npc.h" #include "zone/queryserv.h" #include "zone/quest_parser_collection.h" #include "zone/string_ids.h" #include "zone/titles.h" #include "zone/water_map.h" #include "zone/worldserver.h" #include "zone/zonedb.h" #include extern WorldServer worldserver; extern QueryServ *QServ; struct NPCType; //max number of items which can be in the foraging // and fishing tables for a given zone. constexpr uint8 FORAGE_ITEM_LIMIT = 50; constexpr uint8 FISHING_ITEM_LIMIT = 50; uint32 ZoneDatabase::LoadForage(uint32 zone_id, uint8 skill_level) { uint32 forage_items[FORAGE_ITEM_LIMIT] = {}; const auto& l = ForageRepository::GetWhere( *this, fmt::format( "(`zoneid` = {} || `zoneid` = 0) AND `level` <= {} {} LIMIT {}", zone_id, skill_level, ContentFilterCriteria::apply(), FORAGE_ITEM_LIMIT ) ); if (l.empty()) { return 0; } LogInfo( "Loaded [{}] Forage Item{}", Strings::Commify(l.size()), l.size() != 1 ? "s" : "" ); int forage_chances[FORAGE_ITEM_LIMIT] = {}; int current_chance = 0; uint32 item_id = 0; uint8 count = 0; for (const auto& e : l) { if (count >= FORAGE_ITEM_LIMIT) { break; } forage_items[count] = e.Itemid; forage_chances[count] = e.chance + current_chance; current_chance = forage_chances[count]; count++; } if (current_chance == 0 || count < 1) { return 0; } if (count == 1) { return forage_items[0]; } const int roll = zone->random.Int(1, current_chance); for (uint16 slot_id = 0; slot_id < count; slot_id++) { if (roll <= forage_chances[slot_id]) { item_id = forage_items[slot_id]; break; } } return item_id; } uint32 ZoneDatabase::LoadFishing(uint32 zone_id, uint8 skill_level, uint32 &npc_id, uint8 &npc_chance) { uint32 fishing_items[FISHING_ITEM_LIMIT] = {}; int fishing_chances[FISHING_ITEM_LIMIT] = {}; const auto& l = FishingRepository::GetWhere( *this, fmt::format( "(`zoneid` = {} || `zoneid` = 0) AND `skill_level` <= {} {}", zone_id, skill_level, ContentFilterCriteria::apply() ) ); if (l.empty()) { return 0; } LogInfo( "Loaded [{}] Fishing Item{}", Strings::Commify(l.size()), l.size() != 1 ? "s" : "" ); uint32 npc_ids[FISHING_ITEM_LIMIT] = {}; uint32 npc_chances[FISHING_ITEM_LIMIT] = {}; int current_chance = 0; uint32 item_id = 0; uint8 count = 0; for (const auto &e: l) { if (count >= FISHING_ITEM_LIMIT) { break; } fishing_items[count] = e.Itemid; fishing_chances[count] = e.chance + current_chance; npc_ids[count] = e.npc_id; npc_chances[count] = e.npc_chance; current_chance = fishing_chances[count]; count++; } npc_id = 0; npc_chance = 0; if (count <= 0) { return 0; } const int roll = zone->random.Int(1, current_chance); for (uint8 i = 0; i < count; i++) { if (roll > fishing_chances[i]) { continue; } item_id = fishing_items[i]; npc_id = npc_ids[i]; npc_chance = npc_chances[i]; break; } return item_id; } //we need this function to immediately determine, after we receive OP_Fishing, if we can even try to fish, otherwise we have to wait a while to get the failure bool Client::CanFish() { //make sure we still have a fishing pole on: const EQ::ItemInstance* Pole = m_inv[EQ::invslot::slotPrimary]; int32 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeFishingBait, 1, invWhereWorn | invWherePersonal); const EQ::ItemInstance* Bait = nullptr; if (bslot != INVALID_INDEX) Bait = m_inv.GetItem(bslot); if (!Pole || !Pole->IsClassCommon() || Pole->GetItem()->ItemType != EQ::item::ItemTypeFishingPole) { if (m_inv.HasItemByUse(EQ::item::ItemTypeFishingPole, 1, invWhereWorn | invWherePersonal | invWhereBank | invWhereSharedBank | invWhereTrading | invWhereCursor)) //We have a fishing pole somewhere, just not equipped MessageString(Chat::Skills, FISHING_EQUIP_POLE); //You need to put your fishing pole in your primary hand. else //We don't have a fishing pole anywhere MessageString(Chat::Skills, FISHING_NO_POLE); //You can't fish without a fishing pole, go buy one. return false; } if (!Bait || !Bait->IsClassCommon() || Bait->GetItem()->ItemType != EQ::item::ItemTypeFishingBait) { MessageString(Chat::Skills, FISHING_NO_BAIT); //You can't fish without fishing bait, go buy some. return false; } if(zone->zonemap != nullptr && zone->watermap != nullptr && RuleB(Watermap, CheckForWaterWhenFishing)) { glm::vec3 rodPosition; // Tweak Rod and LineLength if required const float RodLength = RuleR(Watermap, FishingRodLength); const float LineLength = RuleR(Watermap, FishingLineLength); int HeadingDegrees; HeadingDegrees = (int) ((GetHeading()*360)/512); HeadingDegrees = HeadingDegrees % 360; rodPosition.x = m_Position.x + RodLength * sin(HeadingDegrees * std::numbers::pi / 180.0f); rodPosition.y = m_Position.y + RodLength * cos(HeadingDegrees * std::numbers::pi / 180.0f); rodPosition.z = m_Position.z; float bestz = zone->zonemap->FindBestZ(rodPosition, nullptr); float len = m_Position.z - bestz; if(len > LineLength || len < 0.0f) { MessageString(Chat::Skills, FISHING_LAND); return false; } float step_size = RuleR(Watermap, FishingLineStepSize); for(float i = 0.0f; i < LineLength; i += step_size) { glm::vec3 dest(rodPosition.x, rodPosition.y, m_Position.z - i); bool in_lava = zone->watermap->InLava(dest); bool in_water = zone->watermap->InWater(dest) || zone->watermap->InVWater(dest); if (in_lava) { MessageString(Chat::Skills, FISHING_LAVA); //Trying to catch a fire elemental or something? return false; } if(in_water) { return true; } } MessageString(Chat::Skills, FISHING_LAND); return false; } return true; } void Client::GoFish(bool guarantee, bool use_bait) { //TODO: generate a message if we're already fishing /*if (!fishing_timer.Check()) { //this isn't the right check, may need to add something to the Client class like 'bool is_fishing' MessageString(Chat::White, ALREADY_FISHING); //You are already fishing! return; }*/ fishing_timer.Disable(); //we're doing this a second time (1st in Client::Handle_OP_Fishing) to make sure that, between when we started fishing & now, we're still able to fish (in case we move, change equip, etc) if (!CanFish()) { //if we can't fish here, we don't need to bother with the rest return; } //multiple entries yeilds higher probability of dropping... uint32 common_fish_ids[MAX_COMMON_FISH_IDS] = { 1038, // Tattered Cloth Sandals 1038, // Tattered Cloth Sandals 1038, // Tattered Cloth Sandals 13019, // Fresh Fish 13076, // Fish Scales 13076, // Fish Scales 7007, // Rusty Dagger 7007, // Rusty Dagger 7007 // Rusty Dagger }; //success formula is not researched at all uint16 fishing_skill = GetSkill(EQ::skills::SkillFishing); //will take into account skill bonuses on pole & bait //make sure we still have a fishing pole on: int16 bslot = m_inv.HasItemByUse(EQ::item::ItemTypeFishingBait, 1, invWhereWorn | invWherePersonal); const EQ::ItemInstance* Bait = nullptr; if (bslot != INVALID_INDEX) { Bait = m_inv.GetItem(bslot); } //if the bait isnt equipped, need to add its skill bonus if (bslot >= EQ::invslot::GENERAL_BEGIN && Bait != nullptr && Bait->GetItem()->SkillModType == EQ::skills::SkillFishing) { fishing_skill += Bait->GetItem()->SkillModValue; } if (fishing_skill > 100) { fishing_skill = 100+((fishing_skill-100)/2); } if (guarantee || zone->random.Int(0,175) < fishing_skill) { uint32 food_id = 0; //chance to fish a zone item. if (zone->random.Int(0, RuleI(Zone, FishingChance)) <= fishing_skill ) { uint32 npc_id = 0; uint8 npc_chance = 0; food_id = content_db.LoadFishing(m_pp.zone_id, fishing_skill, npc_id, npc_chance); //check for add NPC if (npc_chance > 0 && npc_id) { if (zone->random.Roll(npc_chance)) { if (zone->CanDoCombat()) { const NPCType *tmp = content_db.LoadNPCTypesData(npc_id); if (tmp != nullptr) { auto positionNPC = GetPosition(); positionNPC.x = positionNPC.x + 3; auto npc = new NPC(tmp, nullptr, positionNPC, GravityBehavior::Water); npc->AddLootTable(); if (npc->DropsGlobalLoot()) npc->CheckGlobalLootTables(); npc->AddToHateList(this, 1, 0, false); // no help yelling entity_list.AddNPC(npc); Message(Chat::Emote, "You fish up a little more than you bargained for..."); } } else { Message(Chat::Emote, "You notice something lurking just below the water's surface..."); } } } } if (use_bait) { //consume bait, should we always consume bait on success? DeleteItemInInventory(bslot, 1, true); //do we need client update? } if (food_id == 0) { int index = zone->random.Int(0, MAX_COMMON_FISH_IDS-1); food_id = (RuleB(Character, UseNoJunkFishing) ? 13019 : common_fish_ids[index]); } const EQ::ItemData* food_item = database.GetItem(food_id); if (food_item) { if (food_item->ItemType != EQ::item::ItemTypeFood) { MessageString(Chat::Skills, FISHING_SUCCESS); } else { MessageString(Chat::Skills, FISHING_SUCCESS_FISH_NAME, food_item->Name); } EQ::ItemInstance* inst = database.CreateItem(food_item, 1); if (inst != nullptr) { if (CheckLoreConflict(inst->GetItem())) { MessageString(Chat::White, DUP_LORE); safe_delete(inst); } else { PushItemOnCursor(*inst); SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketLimbo); if (RuleB(TaskSystem, EnableTaskSystem)) UpdateTasksForItem(TaskActivityType::Fish, food_id); safe_delete(inst); inst = m_inv.GetItem(EQ::invslot::slotCursor); } if (inst) { if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::FISH_SUCCESS)) { auto e = PlayerEvent::FishSuccessEvent{ .item_id = inst->GetItem()->ID, .augment_1_id = inst->GetAugmentItemID(0), .augment_2_id = inst->GetAugmentItemID(1), .augment_3_id = inst->GetAugmentItemID(2), .augment_4_id = inst->GetAugmentItemID(3), .augment_5_id = inst->GetAugmentItemID(4), .augment_6_id = inst->GetAugmentItemID(5), .item_name = inst->GetItem()->Name, }; RecordPlayerEventLog(PlayerEvent::FISH_SUCCESS, e); } CheckItemDiscoverability(inst->GetID()); if (parse->PlayerHasQuestSub(EVENT_FISH_SUCCESS)) { std::vector args = {inst}; parse->EventPlayer(EVENT_FISH_SUCCESS, this, "", inst->GetID(), &args); } } } } } else { //chance to use bait when you dont catch anything... if (zone->random.Int(0, 4) == 1) { DeleteItemInInventory(bslot, 1, true); //do we need client update? MessageString(Chat::Skills, FISHING_LOST_BAIT); //You lost your bait! } else { if (zone->random.Int(0, 15) == 1) //give about a 1 in 15 chance to spill your beer. we could make this a rule, but it doesn't really seem worth it //TODO: check for & consume an alcoholic beverage from inventory when this triggers, and set it as a rule that's disabled by default MessageString(Chat::Skills, FISHING_SPILL_BEER); //You spill your beer while bringing in your line. else MessageString(Chat::Skills, FISHING_FAILED); //You didn't catch anything. } RecordPlayerEventLog(PlayerEvent::FISH_FAILURE, PlayerEvent::EmptyEvent{}); if (parse->PlayerHasQuestSub(EVENT_FISH_FAILURE)) { parse->EventPlayer(EVENT_FISH_FAILURE, this, "", 0); } } //chance to break fishing pole... //this is potentially exploitable in that they can fish //and then swap out items in primary slot... too lazy to fix right now const EQ::ItemInstance* Pole = m_inv[EQ::invslot::slotPrimary]; if (Pole) { const EQ::ItemData* fishing_item = Pole->GetItem(); if (fishing_item && fishing_item->SubType == 0 && zone->random.Int(0, 49) == 1) { MessageString(Chat::Skills, FISHING_POLE_BROKE); //Your fishing pole broke! DeleteItemInInventory(EQ::invslot::slotPrimary, 0, true); } } if (CheckIncreaseSkill(EQ::skills::SkillFishing, nullptr, 5)) { if (title_manager.IsNewTradeSkillTitleAvailable(EQ::skills::SkillFishing, GetRawSkill(EQ::skills::SkillFishing))) NotifyNewTitlesAvailable(); } } void Client::ForageItem(bool guarantee) { int skill_level = GetSkill(EQ::skills::SkillForage); //be wary of the string ids in switch below when changing this. uint32 common_food_ids[MAX_COMMON_FOOD_IDS] = { 13046, // Fruit 13045, // Berries 13419, // Vegetables 13048, // Rabbit Meat 13047, // Roots 13044, // Pod of Water 14905, // Mushroom 13106 // Fishing Grubs }; // these may need to be fine tuned, I am just guessing here if (guarantee || zone->random.Int(0,199) < skill_level) { uint32 foragedfood = 0; uint32 stringid = FORAGE_NOEAT; if (zone->random.Roll(RuleI(Zone, ForageChance))) { foragedfood = content_db.LoadForage(m_pp.zone_id, skill_level); } //not an else in case theres no DB food if (foragedfood == 0 && RuleB(Character, UseForageCommonFood)) { uint8 index = 0; index = zone->random.Int(0, MAX_COMMON_FOOD_IDS-1); foragedfood = common_food_ids[index]; } const EQ::ItemData* food_item = database.GetItem(foragedfood); if (!food_item) { LogError("nullptr returned from database.GetItem in ClientForageItem"); return; } if (foragedfood == 13106) { stringid = FORAGE_GRUBS; } else { switch(food_item->ItemType) { case EQ::item::ItemTypeFood: stringid = FORAGE_FOOD; break; case EQ::item::ItemTypeDrink: if (strstr(food_item->Name, "ater")) { stringid = FORAGE_WATER; } else { stringid = FORAGE_DRINK; } break; default: break; } } MessageString(Chat::Skills, stringid); EQ::ItemInstance* inst = database.CreateItem(food_item, 1); if (inst != nullptr) { // check to make sure it isn't a foraged lore item if (CheckLoreConflict(inst->GetItem())) { MessageString(Chat::White, DUP_LORE); safe_delete(inst); } else { PushItemOnCursor(*inst); SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketLimbo); if(RuleB(TaskSystem, EnableTaskSystem)) { UpdateTasksForItem(TaskActivityType::Forage, foragedfood); } safe_delete(inst); inst = m_inv.GetItem(EQ::invslot::slotCursor); } if (inst) { if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::FORAGE_SUCCESS)) { auto e = PlayerEvent::ForageSuccessEvent{ .item_id = inst->GetItem()->ID, .augment_1_id = inst->GetAugmentItemID(0), .augment_2_id = inst->GetAugmentItemID(1), .augment_3_id = inst->GetAugmentItemID(2), .augment_4_id = inst->GetAugmentItemID(3), .augment_5_id = inst->GetAugmentItemID(4), .augment_6_id = inst->GetAugmentItemID(5), .item_name = inst->GetItem()->Name, }; RecordPlayerEventLog(PlayerEvent::FORAGE_SUCCESS, e); } CheckItemDiscoverability(inst->GetID()); if (parse->PlayerHasQuestSub(EVENT_FORAGE_SUCCESS)) { std::vector args = { inst }; parse->EventPlayer(EVENT_FORAGE_SUCCESS, this, "", inst->GetID(), &args); } } } int ChanceSecondForage = aabonuses.ForageAdditionalItems + itembonuses.ForageAdditionalItems + spellbonuses.ForageAdditionalItems; if (!guarantee && zone->random.Roll(ChanceSecondForage)) { MessageString(Chat::Skills, FORAGE_MASTERY); ForageItem(true); } } else { MessageString(Chat::Skills, FORAGE_FAILED); RecordPlayerEventLog(PlayerEvent::FORAGE_FAILURE, PlayerEvent::EmptyEvent{}); if (parse->PlayerHasQuestSub(EVENT_FORAGE_FAILURE)) { parse->EventPlayer(EVENT_FORAGE_FAILURE, this, "", 0); } } CheckIncreaseSkill(EQ::skills::SkillForage, nullptr, 5); }