diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 1f8366662..e85c83e9e 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -376,6 +376,7 @@ N(OP_MercenaryDismiss), N(OP_MercenaryHire), N(OP_MercenarySuspendRequest), N(OP_MercenarySuspendResponse), +N(OP_MercenarySwitch), N(OP_MercenaryTimer), N(OP_MercenaryTimerRequest), N(OP_MercenaryUnknown1), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index c11945cc4..d6736c071 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -6236,6 +6236,12 @@ struct SuspendMercenary_Struct { /*0001*/ }; +// [OPCode: 0x1b37 (RoF2)] [Client->Server] [Size: 4] +struct SwitchMercenary_Struct { +/*0000*/ uint32 MercIndex; // 0-based UI index into owned merc list +/*0004*/ +}; + // [OPCode: 0x2528] On Live as of April 2 2012 [Server->Client] [Size: 4] // Response to suspend merc with timestamp struct SuspendMercenaryResponse_Struct { diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 992f0c538..a2b3ba30e 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 - trailing field (unlocked slot count) } else { diff --git a/common/ruletypes.h b/common/ruletypes.h index d9df94afd..6e3c9dd91 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 6)") RULE_CATEGORY_END() RULE_CATEGORY(Guild) diff --git a/submodules/websocketpp b/submodules/websocketpp index b9aeec6ea..4dfe1be74 160000 --- a/submodules/websocketpp +++ b/submodules/websocketpp @@ -1 +1 @@ -Subproject commit b9aeec6eaf3d5610503439b4fae3581d9aff08e8 +Subproject commit 4dfe1be74e684acca19ac1cf96cce0df9eac2a2d diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 3533aaa87..e5d9173cf 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -418,6 +418,7 @@ OP_MercenaryUnknown1=0x5d26 OP_MercenaryCommand=0x27f2 OP_MercenarySuspendRequest=0x4407 OP_MercenarySuspendResponse=0x6f03 +OP_MercenarySwitch=0x1b37 OP_MercenaryUnsuspendResponse=0x27a0 # Looting diff --git a/zone/client.cpp b/zone/client.cpp index 56d59c4b3..f70f1e808 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,125 @@ 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; + + // Lambda to populate a single merc entry in the packet + int max_slots = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + uint32 merc_index = 0; + + auto fillMercEntry = [&](int slot) { + auto& info = GetMercInfo(slot); + if (info.mercid == 0 || merc_index >= MAX_MERC) { + return; + } + + auto tmpl_it = zone->merc_templates.find(info.MercTemplateID); + if (tmpl_it == zone->merc_templates.end()) { + return; + } + + 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); + return; + } + + 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 = (slot == GetMercSlot()) ? 1 : 0; + 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++; + }; + + // Emit the active merc slot first — the client marks the first entry + // in the list with the X (active marker), so order matters. + if (GetMercSlot() < max_slots && GetMercInfo().mercid != 0) { + fillMercEntry(GetMercSlot()); + } + + // Then emit remaining owned mercs in slot order + for (int slot = 0; slot < max_slots; slot++) { + if (slot == GetMercSlot()) { + continue; // already emitted + } + fillMercEntry(slot); + } + + // 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..5ad6b46cb 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -305,6 +305,7 @@ void MapOpcodes() ConnectedOpcodes[OP_MercenaryDismiss] = &Client::Handle_OP_MercenaryDismiss; ConnectedOpcodes[OP_MercenaryHire] = &Client::Handle_OP_MercenaryHire; ConnectedOpcodes[OP_MercenarySuspendRequest] = &Client::Handle_OP_MercenarySuspendRequest; + ConnectedOpcodes[OP_MercenarySwitch] = &Client::Handle_OP_MercenarySwitch; ConnectedOpcodes[OP_MercenaryTimerRequest] = &Client::Handle_OP_MercenaryTimerRequest; ConnectedOpcodes[OP_MoveCoin] = &Client::Handle_OP_MoveCoin; ConnectedOpcodes[OP_MoveItem] = &Client::Handle_OP_MoveItem; @@ -10430,7 +10431,7 @@ void Client::Handle_OP_MercenaryCommand(const EQApplicationPacket *app) } MercenaryCommand_Struct* mc = (MercenaryCommand_Struct*)app->pBuffer; - uint32 merc_command = mc->MercCommand; // Seen 0 (zone in with no merc or suspended), 1 (dismiss merc), 5 (normal state), 20 (unknown), 36 (zone in with merc) + uint32 merc_command = mc->MercCommand; // Seen 0 (zone in with no merc or suspended), 1 (stance/state update), 5 (normal state), 20 (unknown), 36 (zone in with merc) int32 option = mc->Option; // Seen -1 (zone in with no merc), 0 (setting to passive stance), 1 (normal or setting to balanced stance) Log(Logs::General, Logs::Mercenaries, "Command %i, Option %i received from %s.", merc_command, option, GetName()); @@ -10438,9 +10439,6 @@ void Client::Handle_OP_MercenaryCommand(const EQApplicationPacket *app) if (!RuleB(Mercs, AllowMercs)) return; - // Handle the Command here... - // Will need a list of what every type of command is supposed to do - // Unsure if there is a server response to this packet if (option >= 0) { Merc* merc = GetMerc(); @@ -10496,14 +10494,14 @@ void Client::Handle_OP_MercenaryDataRequest(const EQApplicationPacket *app) if (merchant_id == 0) { //send info about your current merc(s) - if (GetMercInfo().mercid) + if (GetNumberOfMercenaries() > 0) { Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo Request for %s.", GetName()); SendMercPersonalInfo(); } else { - Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo Not Sent - MercID (%i) for %s.", GetMercInfo().mercid, GetName()); + Log(Logs::General, Logs::Mercenaries, "SendMercPersonalInfo Not Sent - no mercs owned for %s.", GetName()); } } @@ -10687,6 +10685,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 +10719,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 { @@ -10742,6 +10759,88 @@ void Client::Handle_OP_MercenarySuspendRequest(const EQApplicationPacket *app) SuspendMercCommand(); } +void Client::Handle_OP_MercenarySwitch(const EQApplicationPacket *app) +{ + if (app->size != sizeof(SwitchMercenary_Struct)) { + LogDebug("Size mismatch in OP_MercenarySwitch expected [{}] got [{}]", sizeof(SwitchMercenary_Struct), app->size); + DumpPacket(app); + return; + } + + if (!RuleB(Mercs, AllowMercs)) + return; + + SwitchMercenary_Struct* sm = (SwitchMercenary_Struct*)app->pBuffer; + uint32 merc_ui_index = sm->MercIndex; + + Log(Logs::General, Logs::Mercenaries, "Switch request to UI index %u received from %s.", merc_ui_index, GetName()); + + // The client sends a dense UI index (0, 1, 2...) that corresponds to the Nth + // owned merc in the list, matching the order sent by SendMercPersonalInfo(). + // SendMercPersonalInfo emits the active slot first, then remaining slots in order. + // We must replicate that same ordering to map UI index -> internal slot. + int target_slot = -1; + int max_slots = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + uint32 ui_pos = 0; + + // First: the active merc slot (emitted first in the packet) + if (GetMercSlot() < max_slots && m_mercinfo[GetMercSlot()].mercid != 0) { + if (ui_pos == merc_ui_index) { + target_slot = GetMercSlot(); + } + ui_pos++; + } + + // Then: remaining slots in order (skipping active slot) + if (target_slot < 0) { + for (int slot = 0; slot < max_slots; slot++) { + if (slot == GetMercSlot()) { + continue; + } + if (m_mercinfo[slot].mercid != 0) { + if (ui_pos == merc_ui_index) { + target_slot = slot; + break; + } + ui_pos++; + } + } + } + + if (target_slot < 0) { + Log(Logs::General, Logs::Mercenaries, "Switch request denied — UI index %u has no corresponding merc for %s.", merc_ui_index, GetName()); + SendMercResponsePackets(0); + return; + } + + if (target_slot == GetMercSlot()) { + Log(Logs::General, Logs::Mercenaries, "Switch request ignored — already on slot %i for %s.", target_slot, GetName()); + return; + } + + // Suspend the currently active merc if one is spawned + Merc* current_merc = GetMerc(); + if (current_merc) { + current_merc->Suspend(); + // Clear merc pointer without wiping slot data (SetMerc(nullptr) would zero the slot) + current_merc->SetOwnerID(0); + SetMercID(0); + } + + // Clear the suspend timer so the target merc can be unsuspended immediately. + // The cooldown is meant for rapid suspend/unsuspend of the same merc, not for switching. + if (!GetPTimers().Expired(&database, pTimerMercSuspend, false)) { + GetPTimers().Clear(&database, pTimerMercSuspend); + } + + SetMercSlot(static_cast(target_slot)); + + Log(Logs::General, Logs::Mercenaries, "Switched active merc slot to %i (UI index %u) for %s.", target_slot, merc_ui_index, GetName()); + + // Unsuspend the target merc + SuspendMercCommand(); +} + void Client::Handle_OP_MercenaryTimerRequest(const EQApplicationPacket *app) { // The payload is 0 bytes. diff --git a/zone/client_packet.h b/zone/client_packet.h index 009dd75d6..b2a455b97 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -239,6 +239,7 @@ void Handle_OP_MercenaryDismiss(const EQApplicationPacket *app); void Handle_OP_MercenaryHire(const EQApplicationPacket *app); void Handle_OP_MercenarySuspendRequest(const EQApplicationPacket *app); + void Handle_OP_MercenarySwitch(const EQApplicationPacket *app); void Handle_OP_MercenaryTimerRequest(const EQApplicationPacket *app); void Handle_OP_MoveCoin(const EQApplicationPacket *app); void Handle_OP_MoveItem(const EQApplicationPacket *app); diff --git a/zone/merc.cpp b/zone/merc.cpp index 283132a2d..d9212db8d 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; } @@ -5579,6 +5611,17 @@ uint8 Client::GetNumberOfMercenaries() return count; } +int Client::GetFirstFreeMercSlot() +{ + int max_slots = std::min(RuleI(Mercs, MaxMercSlots), MAXMERCS); + for (int slot_id = 0; slot_id < max_slots; slot_id++) { + if (m_mercinfo[slot_id].mercid == 0) { + return slot_id; + } + } + return -1; +} + void Merc::SetMercData( uint32 template_id ) { MercTemplate* merc_template = zone->GetMercTemplate(template_id); 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;