diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 992f0c538..177fedfb2 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -2278,15 +2278,19 @@ namespace RoF2 // There are 2 different sized versions of this packet depending if a merc is hired or not if (emu->MercStatus >= 0) { - PacketSize += sizeof(structs::MercenaryDataUpdate_Struct) + (sizeof(structs::MercenaryData_Struct) - sizeof(structs::MercenaryStance_Struct)) * emu->MercCount; - + // Per-merc size: base struct minus Stances[1] and MercUnk05, + // then add back actual stances and name length per merc. + // MercUnk05 is a single trailing field after all mercs. + PacketSize += sizeof(structs::MercenaryDataUpdate_Struct); uint32 r; uint32 k; for (r = 0; r < emu->MercCount; r++) { + PacketSize += sizeof(structs::MercenaryData_Struct) - sizeof(structs::MercenaryStance_Struct) - sizeof(uint32); // subtract Stances[1] and MercUnk05 PacketSize += sizeof(structs::MercenaryStance_Struct) * emu->MercData[r].StanceCount; PacketSize += strlen(emu->MercData[r].MercName); // Null Terminator size already accounted for in the struct } + PacketSize += sizeof(uint32); // MercUnk05 - trailing field after all mercs outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, PacketSize); Buffer = (char *)outapp->pBuffer; @@ -2312,15 +2316,14 @@ namespace RoF2 VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->MercData[r].StanceCount); VARSTRUCT_ENCODE_TYPE(int32, Buffer, emu->MercData[r].MercUnk03); VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->MercData[r].MercUnk04); - //VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // MercName VARSTRUCT_ENCODE_STRING(Buffer, emu->MercData[r].MercName); for (k = 0; k < emu->MercData[r].StanceCount; k++) { VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->MercData[r].Stances[k].StanceIndex); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->MercData[r].Stances[k].Stance); } - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 1); // MercUnk05 } + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->MercData[0].MercUnk05); // MercUnk05 } else { diff --git a/common/ruletypes.h b/common/ruletypes.h index d9df94afd..02ff8d39b 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -255,6 +255,7 @@ RULE_BOOL(Mercs, AllowMercSuspendInCombat, true, "Allow merc suspend in combat") RULE_BOOL(Mercs, MercsIgnoreLevelBasedHasteCaps, false, "Ignores hard coded level based haste caps.") RULE_INT(Mercs, MercsHasteCap, 100, "Haste cap for non-v3(over haste) haste") RULE_INT(Mercs, MercsHastev3Cap, 25, "Haste cap for v3(over haste) haste") +RULE_INT(Mercs, MaxMercSlots, 6, "Maximum number of mercenary slots per character (max 11)") RULE_CATEGORY_END() RULE_CATEGORY(Guild) diff --git a/zone/client.cpp b/zone/client.cpp index 56d59c4b3..3387b08d0 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -1037,7 +1037,7 @@ bool Client::Save(uint8 iCommitNow) { } if (dead || (!GetMerc() && !GetMercInfo().IsSuspended)) { - memset(&m_mercinfo, 0, sizeof(struct MercInfo)); + memset(&m_mercinfo, 0, sizeof(m_mercinfo)); } m_pp.lastlogin = time(nullptr); @@ -7940,75 +7940,113 @@ void Client::SendWebLink(const char *website) void Client::SendMercPersonalInfo() { - uint32 mercTypeCount = 1; - uint32 mercCount = 1; //TODO: Un-hardcode this and support multiple mercs like in later clients than SoD. - uint32 i = 0; uint32 altCurrentType = 19; //TODO: Implement alternate currency purchases involving mercs! - MercTemplate *mercData = &zone->merc_templates[GetMercInfo().MercTemplateID]; - - int stancecount = 0; - stancecount += zone->merc_stance_list[GetMercInfo().MercTemplateID].size(); - if(stancecount > MAX_MERC_STANCES || mercCount > MAX_MERC || mercTypeCount > MAX_MERC_GRADES) - { - Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo canceled: (%i) (%i) (%i) for %s", stancecount, mercCount, mercTypeCount, GetName()); - SendMercMerchantResponsePacket(0); - return; - } - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { - auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, sizeof(MercenaryDataUpdate_Struct)); - auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; - - mdus->MercStatus = 0; - mdus->MercCount = mercCount; - mdus->MercData[i].MercID = mercData->MercTemplateID; - mdus->MercData[i].MercType = mercData->MercType; - mdus->MercData[i].MercSubType = mercData->MercSubType; - mdus->MercData[i].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); - mdus->MercData[i].Status = 0; - mdus->MercData[i].AltCurrencyCost = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyUpkeep = Merc::CalcPurchaseCost( - mercData->MercTemplateID, - GetLevel(), - altCurrentType - ); - mdus->MercData[i].AltCurrencyType = altCurrentType; - mdus->MercData[i].MercUnk01 = 0; - mdus->MercData[i].TimeLeft = GetMercInfo().MercTimerRemaining; //GetMercTimer().GetRemainingTime(); - mdus->MercData[i].MerchantSlot = i + 1; - mdus->MercData[i].MercUnk02 = 1; - mdus->MercData[i].StanceCount = zone->merc_stance_list[mercData->MercTemplateID].size(); - mdus->MercData[i].MercUnk03 = 0; - mdus->MercData[i].MercUnk04 = 1; - - strn0cpy(mdus->MercData[i].MercName, GetMercInfo().merc_name, sizeof(mdus->MercData[i].MercName)); - - uint32 stanceindex = 0; - if (mdus->MercData[i].StanceCount != 0) { - auto iter = zone->merc_stance_list[mercData->MercTemplateID].begin(); - while (iter != zone->merc_stance_list[mercData->MercTemplateID].end()) { - mdus->MercData[i].Stances[stanceindex].StanceIndex = stanceindex; - mdus->MercData[i].Stances[stanceindex].Stance = (iter->StanceID); - stanceindex++; - ++iter; - } + // Count owned mercs across all slots + uint32 mercCount = GetNumberOfMercenaries(); + if (mercCount == 0) { + SendClearMercInfo(); + return; } - mdus->MercData[i].MercUnk05 = 1; + uint32 packetSize = sizeof(MercenaryDataUpdate_Struct) + (mercCount * sizeof(MercenaryData_Struct)); + auto outapp = new EQApplicationPacket(OP_MercenaryDataUpdate, packetSize); + memset(outapp->pBuffer, 0, packetSize); + auto mdus = (MercenaryDataUpdate_Struct *) outapp->pBuffer; + + mdus->MercStatus = 0; + mdus->MercCount = mercCount; + + int max_slots = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + uint32 merc_index = 0; + for (int slot = 0; slot < max_slots && merc_index < MAX_MERC; slot++) { + auto& info = GetMercInfo(slot); + if (info.mercid == 0) { + continue; + } + + Message(Chat::Yellow, "SendMercPersonalInfo: slot %i, mercid %u, templateid %u, name '%s', suspended %i", + slot, info.mercid, info.MercTemplateID, info.merc_name, info.IsSuspended); + + auto tmpl_it = zone->merc_templates.find(info.MercTemplateID); + if (tmpl_it == zone->merc_templates.end()) { + Message(Chat::Red, "SendMercPersonalInfo: slot %i template %u NOT FOUND, skipping", slot, info.MercTemplateID); + continue; + } + + MercTemplate *mercData = &tmpl_it->second; + uint32 stancecount = 0; + auto stance_it = zone->merc_stance_list.find(mercData->MercTemplateID); + if (stance_it != zone->merc_stance_list.end()) { + stancecount = stance_it->second.size(); + } + + if (stancecount > MAX_MERC_STANCES) { + Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo: stance count %u exceeds max for slot %i, skipping", stancecount, slot); + continue; + } + + mdus->MercData[merc_index].MercID = mercData->MercTemplateID; + mdus->MercData[merc_index].MercType = mercData->MercType; + mdus->MercData[merc_index].MercSubType = mercData->MercSubType; + mdus->MercData[merc_index].PurchaseCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[merc_index].UpkeepCost = Merc::CalcUpkeepCost(mercData->MercTemplateID, GetLevel(), 0); + mdus->MercData[merc_index].Status = info.IsSuspended ? 0 : 1; + mdus->MercData[merc_index].AltCurrencyCost = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType); + mdus->MercData[merc_index].AltCurrencyUpkeep = Merc::CalcPurchaseCost(mercData->MercTemplateID, GetLevel(), altCurrentType); + mdus->MercData[merc_index].AltCurrencyType = altCurrentType; + mdus->MercData[merc_index].MercUnk01 = 0; + mdus->MercData[merc_index].TimeLeft = info.MercTimerRemaining; + mdus->MercData[merc_index].MerchantSlot = merc_index + 1; + mdus->MercData[merc_index].MercUnk02 = 1; + mdus->MercData[merc_index].StanceCount = stancecount; + mdus->MercData[merc_index].MercUnk03 = 0; + mdus->MercData[merc_index].MercUnk04 = 1; + + strn0cpy(mdus->MercData[merc_index].MercName, info.merc_name, sizeof(mdus->MercData[merc_index].MercName)); + + uint32 stanceindex = 0; + if (stance_it != zone->merc_stance_list.end()) { + for (const auto& stance : stance_it->second) { + mdus->MercData[merc_index].Stances[stanceindex].StanceIndex = stanceindex; + mdus->MercData[merc_index].Stances[stanceindex].Stance = stance.StanceID; + stanceindex++; + } + } + + mdus->MercData[merc_index].MercUnk05 = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + merc_index++; + } + + // Update count in case we skipped any invalid entries + mdus->MercCount = merc_index; + FastQueuePacket(&outapp); safe_delete(outapp); return; } else { + // Pre-RoF path (SoD and earlier) — single merc only + if (GetMercInfo().MercTemplateID == 0) { + SendClearMercInfo(); + return; + } + + auto tmpl_it = zone->merc_templates.find(GetMercInfo().MercTemplateID); + if (tmpl_it == zone->merc_templates.end()) { + SendClearMercInfo(); + return; + } + + MercTemplate *mercData = &tmpl_it->second; + uint32 mercTypeCount = 1; + uint32 mercCount = 1; + uint32 i = 0; + auto outapp = new EQApplicationPacket(OP_MercenaryDataResponse, sizeof(MercenaryMerchantList_Struct)); auto mml = (MercenaryMerchantList_Struct *) outapp->pBuffer; - mml->MercTypeCount = mercTypeCount; //We should only have one merc entry. + mml->MercTypeCount = mercTypeCount; mml->MercGrades[i] = 1; mml->MercCount = mercCount; diff --git a/zone/client.h b/zone/client.h index 4e9b5e891..62709bbd0 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1773,6 +1773,7 @@ public: MercInfo& GetMercInfo(uint8 slot) { return m_mercinfo[slot]; } MercInfo& GetMercInfo() { return m_mercinfo[mercSlot]; } uint8 GetNumberOfMercenaries(); + int GetFirstFreeMercSlot(); void SetMerc(Merc* newmerc); void SendMercResponsePackets(uint32 ResponseType); void SendMercMerchantResponsePacket(int32 response_type); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 7fc5174f6..d8ec9a16f 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -10687,6 +10687,21 @@ void Client::Handle_OP_MercenaryHire(const EQApplicationPacket *app) return; } + // Suspend active merc if one exists before hiring into a new slot + Merc* current_merc = GetMerc(); + if (current_merc) { + current_merc->Suspend(); + SetMerc(nullptr); + } + + // Select a free slot for the new hire + int free_slot = GetFirstFreeMercSlot(); + if (free_slot < 0) { + SendMercResponsePackets(6); + return; + } + SetMercSlot(static_cast(free_slot)); + // Set time remaining to max on Hire GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS); @@ -10706,6 +10721,10 @@ void Client::Handle_OP_MercenaryHire(const EQApplicationPacket *app) // approved hire request SendMercMerchantResponsePacket(0); + + // Update the client's Manage tab with the newly hired merc info + SendMercPersonalInfo(); + SendMercTimer(merc); } else { diff --git a/zone/merc.cpp b/zone/merc.cpp index 283132a2d..eacfe83d4 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -4885,14 +4885,8 @@ bool Client::CheckCanHireMerc(Mob* merchant, uint32 template_id) { MercTemplate* mercTemplate = zone->GetMercTemplate(template_id); - //check for suspended merc - if(GetMercInfo().mercid != 0 && GetMercInfo().IsSuspended) { - SendMercResponsePackets(6); - return false; - } - - // Check if max number of mercs is already reached - if(GetNumberOfMercenaries() >= MAXMERCS) { + // Check if all merc slots are full (counts both active and suspended mercs) + if (GetFirstFreeMercSlot() < 0) { SendMercResponsePackets(6); return false; } @@ -5046,8 +5040,21 @@ void Client::SuspendMercCommand() { return; } + // Suspend any currently active merc before unsuspending this one + Merc* active_merc = GetMerc(); + if (active_merc) { + active_merc->Suspend(); + SetMerc(nullptr); + } + // Get merc, assign it to client & spawn - Merc* merc = Merc::LoadMercenary(this, &zone->merc_templates[GetMercInfo().MercTemplateID], 0, true); + auto tmpl_it = zone->merc_templates.find(GetMercInfo().MercTemplateID); + if (tmpl_it == zone->merc_templates.end()) { + SendMercResponsePackets(3); + Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Invalid template for %s.", GetName()); + return; + } + Merc* merc = Merc::LoadMercenary(this, &tmpl_it->second, 0, true); if(merc) { SpawnMerc(merc, false); @@ -5119,40 +5126,56 @@ void Client::SpawnMercOnZone() { if(database.LoadMercenaryInfo(this)) { - if(!GetMercInfo().IsSuspended) - { + // Find the active (non-suspended) merc slot, or fall back to the first owned slot + int active_slot = -1; + int first_owned_slot = -1; + int max_slots = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + for (int slot = 0; slot < max_slots; slot++) { + if (GetMercInfo(slot).mercid != 0) { + if (first_owned_slot < 0) { + first_owned_slot = slot; + } + if (!GetMercInfo(slot).IsSuspended) { + active_slot = slot; + break; + } + } + } + + if (active_slot >= 0) { + SetMercSlot(static_cast(active_slot)); GetMercInfo().SuspendedTime = 0; // Get merc, assign it to client & spawn - Merc* merc = Merc::LoadMercenary(this, &zone->merc_templates[GetMercInfo().MercTemplateID], 0, true); - if(merc) - { - SpawnMerc(merc, false); + auto tmpl_it = zone->merc_templates.find(GetMercInfo().MercTemplateID); + if (tmpl_it != zone->merc_templates.end()) { + Merc* merc = Merc::LoadMercenary(this, &tmpl_it->second, 0, true); + if (merc) { + SpawnMerc(merc, false); + } } - Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Normal Merc for %s.", GetName()); - } - else - { + Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Normal Merc (slot %i) for %s.", active_slot, GetName()); + } else if (first_owned_slot >= 0) { + SetMercSlot(static_cast(first_owned_slot)); int32 TimeDiff = GetMercInfo().SuspendedTime - time(nullptr); - if (TimeDiff > 0) - { - if (!GetPTimers().Enabled(pTimerMercSuspend)) - { - // Start the timer to send the packet that refreshes the Unsuspend Button + if (TimeDiff > 0) { + if (!GetPTimers().Enabled(pTimerMercSuspend)) { GetPTimers().Start(pTimerMercSuspend, TimeDiff); } } - // Send Mercenary Status/Timer packet SendMercTimer(GetMerc()); + Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Suspended Merc (slot %i) for %s.", first_owned_slot, GetName()); + } else { + Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone No valid merc slots found for %s.", GetName()); + } - Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Suspended Merc for %s.", GetName()); + // Send merc personal info for all owned mercs (populates the Manage tab) + if (GetNumberOfMercenaries() > 0) { + SendMercPersonalInfo(); } } else { - // No Merc Hired - // RoF+ displays a message from the following packet, which seems useless - //SendClearMercInfo(); - Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Failed to load Merc Info from the Database for %s.", GetName()); + Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone No merc info in database for %s.", GetName()); } } @@ -5323,9 +5346,18 @@ bool Client::DismissMerc(uint32 MercID) { GetMerc()->Depop(); } - SendClearMercInfo(); + // Clear the dismissed merc's slot data so it becomes available + memset(&GetMercInfo(), 0, sizeof(MercInfo)); + SetMerc(nullptr); + // Update the client with remaining mercs or clear if none left + if (GetNumberOfMercenaries() > 0) { + SendMercPersonalInfo(); + } else { + SendClearMercInfo(); + } + return Dismissed; } diff --git a/zone/merc.h b/zone/merc.h index fb27c9f87..0eba77f17 100644 --- a/zone/merc.h +++ b/zone/merc.h @@ -33,7 +33,7 @@ namespace EQ struct ItemData; } -#define MAXMERCS 1 +constexpr int MAXMERCS = 11; #define TANK 1 #define HEALER 2 #define MELEEDPS 9 diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 703f012ad..24414a2d0 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -2225,7 +2225,7 @@ bool ZoneDatabase::LoadCurrentMercenary(Client* c) { const uint8 mercenary_slot = c->GetMercSlot(); - if (mercenary_slot > MAXMERCS) { + if (mercenary_slot >= MAXMERCS) { return false; } @@ -2277,7 +2277,7 @@ bool ZoneDatabase::SaveMercenary(Merc* m) auto e = MercsRepository::NewEntity(); e.OwnerCharacterID = m->GetMercenaryCharacterID(); - e.Slot = (c->GetNumberOfMercenaries() - 1); + e.Slot = c->GetMercSlot(); e.Name = m->GetCleanName(); e.TemplateID = m->GetMercenaryTemplateID(); e.SuspendedTime = c->GetMercInfo().SuspendedTime; @@ -2318,7 +2318,7 @@ bool ZoneDatabase::SaveMercenary(Merc* m) auto e = MercsRepository::FindOne(*this, m->GetMercenaryID()); e.OwnerCharacterID = m->GetMercenaryCharacterID(); - e.Slot = (c->GetNumberOfMercenaries() - 1); + e.Slot = c->GetMercSlot(); e.Name = m->GetCleanName(); e.TemplateID = m->GetMercenaryTemplateID(); e.SuspendedTime = c->GetMercInfo().SuspendedTime;