mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-06 08:52:25 +00:00
Merge branch 'tob_patch' into tob_patch_login
This commit is contained in:
commit
8d6c5d1b24
@ -734,6 +734,8 @@ set(common_headers
|
||||
util/uuid.h
|
||||
version.h
|
||||
zone_store.h
|
||||
links.h
|
||||
links.cpp
|
||||
)
|
||||
|
||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${common_sources})
|
||||
|
||||
@ -374,6 +374,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),
|
||||
|
||||
@ -6244,6 +6244,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 {
|
||||
|
||||
10
common/links.cpp
Normal file
10
common/links.cpp
Normal file
@ -0,0 +1,10 @@
|
||||
//
|
||||
// Created by dannu on 4/18/2026.
|
||||
//
|
||||
|
||||
#include "links.h"
|
||||
|
||||
std::string Links::FormatSpellLink(uint32_t SpellID, const std::string& SpellName)
|
||||
{
|
||||
return fmt::format("{}63^{}^0^'{}{}", ITEM_TAG_CHAR, SpellID, SpellName.c_str(), ITEM_TAG_CHAR);
|
||||
}
|
||||
11
common/links.h
Normal file
11
common/links.h
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// Created by dannu on 4/18/2026.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Links
|
||||
{
|
||||
constexpr char ITEM_TAG_CHAR = '\x12';
|
||||
std::string FormatSpellLink(uint32_t SpellID, const std::string& SpellName);
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -23,7 +23,9 @@
|
||||
#include <numeric>
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <set>
|
||||
|
||||
#include "common/packet_dump.h"
|
||||
#include "world/sof_char_create_data.h"
|
||||
|
||||
namespace TOB
|
||||
@ -144,6 +146,20 @@ namespace TOB
|
||||
#include "ss_define.h"
|
||||
|
||||
// ENCODE methods
|
||||
ENCODE(OP_AAExpUpdate) {
|
||||
ENCODE_LENGTH_EXACT(AltAdvStats_Struct);
|
||||
SETUP_DIRECT_ENCODE(AltAdvStats_Struct, structs::AltAdvStats_Struct);
|
||||
|
||||
//later we should change the underlying server to use this more accurate value
|
||||
//and encode the 330 in the other patches
|
||||
eq->experience = emu->experience * 100000 / 330;
|
||||
|
||||
OUT(unspent);
|
||||
OUT(percentage);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_Action) {
|
||||
ENCODE_LENGTH_EXACT(Action_Struct);
|
||||
SETUP_DIRECT_ENCODE(Action_Struct, structs::MissileHitInfo);
|
||||
@ -217,19 +233,22 @@ namespace TOB
|
||||
|
||||
ENCODE(OP_BlockedBuffs)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(BlockedBuffs_Struct);
|
||||
SETUP_DIRECT_ENCODE(BlockedBuffs_Struct, structs::BlockedBuffs_Struct);
|
||||
// Blocked buffs are a major change. They are stored in a resizable array in TOB, so this sends size, then
|
||||
// spells, then the final two bools -- see 0x140202750
|
||||
SETUP_VAR_ENCODE(BlockedBuffs_Struct);
|
||||
|
||||
for (uint32 i = 0; i < BLOCKED_BUFF_COUNT; ++i)
|
||||
eq->SpellID[i] = emu->SpellID[i];
|
||||
// size is uint32 + count * int32 + uint8 + uint8
|
||||
uint32 sz = 6 + emu->Count * 4;
|
||||
__packet->size = sz;
|
||||
__packet->pBuffer = new unsigned char[sz];
|
||||
memset(__packet->pBuffer, 0, sz);
|
||||
|
||||
for (uint32 i = BLOCKED_BUFF_COUNT; i < structs::BLOCKED_BUFF_COUNT; ++i)
|
||||
eq->SpellID[i] = -1;
|
||||
__packet->WriteUInt32(emu->Count);
|
||||
for (int i = 0; i < emu->Count; i++)
|
||||
__packet->WriteSInt32(emu->SpellID[i]);
|
||||
|
||||
OUT(Count);
|
||||
OUT(Pet);
|
||||
OUT(Initialise);
|
||||
OUT(Flags);
|
||||
__packet->WriteUInt8(emu->Pet);
|
||||
__packet->WriteUInt8(emu->Initialise);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
@ -333,6 +352,7 @@ namespace TOB
|
||||
|
||||
ENCODE(OP_CastSpell)
|
||||
{
|
||||
// I don't think the client handles this at all, it only sends the cast packet
|
||||
ENCODE_LENGTH_EXACT(CastSpell_Struct);
|
||||
SETUP_DIRECT_ENCODE(CastSpell_Struct, structs::CastSpell_Struct);
|
||||
|
||||
@ -453,7 +473,7 @@ namespace TOB
|
||||
|
||||
int item_count = in->size / sizeof(EQ::InternalSerializedItem_Struct);
|
||||
if (!item_count || (in->size % sizeof(EQ::InternalSerializedItem_Struct)) != 0) {
|
||||
Log(Logs::General, Logs::Netcode, "[STRUCTS] Wrong size on outbound %s: Got %d, expected multiple of %d",
|
||||
LogNetcode("[STRUCTS] Wrong size on outbound {}: Got {}, expected multiple of {}",
|
||||
opcodes->EmuToName(in->GetOpcode()), in->size, sizeof(EQ::InternalSerializedItem_Struct));
|
||||
|
||||
delete in;
|
||||
@ -650,8 +670,8 @@ namespace TOB
|
||||
}
|
||||
|
||||
SerializeBuffer buffer;
|
||||
buffer.WriteUInt32(emu->unknown0);
|
||||
buffer.WriteUInt8(0); // Observed
|
||||
buffer.WriteUInt32(0); // This is a string written like the message arrays
|
||||
buffer.WriteUInt8(emu->unknown0);
|
||||
buffer.WriteUInt32(emu->string_id);
|
||||
buffer.WriteUInt32(emu->type);
|
||||
|
||||
@ -939,6 +959,27 @@ namespace TOB
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_MemorizeSpell) {
|
||||
ENCODE_LENGTH_EXACT(MemorizeSpell_Struct);
|
||||
SETUP_DIRECT_ENCODE(MemorizeSpell_Struct, structs::MemorizeSpell_Struct);
|
||||
|
||||
// in TOB, 2 is "finish memming" so that becomes 1 in emu and 3 is "unmem" which becomes 2
|
||||
if (emu->scribing == 1)
|
||||
eq->scribing = 2;
|
||||
else if (emu->scribing == 2)
|
||||
eq->scribing = 3;
|
||||
else if (emu->scribing == 3)
|
||||
eq->scribing = 4;
|
||||
else
|
||||
OUT(scribing);
|
||||
|
||||
OUT(slot);
|
||||
OUT(spell_id);
|
||||
OUT(reduction);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_MobHealth) {
|
||||
ENCODE_LENGTH_EXACT(SpawnHPUpdate_Struct2);
|
||||
SETUP_DIRECT_ENCODE(SpawnHPUpdate_Struct2, structs::MobHealth_Struct);
|
||||
@ -981,6 +1022,7 @@ namespace TOB
|
||||
ENCODE(OP_NewSpawn) { ENCODE_FORWARD(OP_ZoneSpawns); }
|
||||
|
||||
ENCODE(OP_NewZone) {
|
||||
// zoneHeader
|
||||
EQApplicationPacket* in = *p;
|
||||
*p = nullptr;
|
||||
|
||||
@ -2305,7 +2347,6 @@ namespace TOB
|
||||
eq->container_slot = ServerToTOBSlot(emu->unknown1);
|
||||
structs::InventorySlot_Struct TOBSlot;
|
||||
TOBSlot.Type = 8; // Observed
|
||||
TOBSlot.Padding1 = 0;
|
||||
TOBSlot.Slot = 0xffff;
|
||||
TOBSlot.SubIndex = 0xffff;
|
||||
TOBSlot.AugIndex = 0xffff;
|
||||
@ -2332,12 +2373,11 @@ namespace TOB
|
||||
eq->aapoints_assigned[4] = 0;
|
||||
eq->aapoints_assigned[5] = 0;
|
||||
|
||||
for (uint32 i = 0; i < MAX_PP_AA_ARRAY; ++i)
|
||||
for (uint32 i = 0; i < structs::MAX_PP_AA_ARRAY; ++i)
|
||||
{
|
||||
eq->aa_list[i].AA = emu->aa_list[i].AA;
|
||||
eq->aa_list[i].value = emu->aa_list[i].value;
|
||||
eq->aa_list[i].charges = emu->aa_list[i].charges;
|
||||
eq->aa_list[i].bUnknown0x0c = false;
|
||||
}
|
||||
|
||||
FINISH_ENCODE();
|
||||
@ -2386,7 +2426,7 @@ namespace TOB
|
||||
s32 Desc;
|
||||
*/
|
||||
|
||||
buffer.WriteUInt32(emu->id);
|
||||
buffer.WriteUInt32(emu->id); // Index
|
||||
buffer.WriteUInt8(1);
|
||||
buffer.WriteInt32(emu->upper_hotkey_sid);
|
||||
buffer.WriteInt32(emu->lower_hotkey_sid);
|
||||
@ -2903,7 +2943,7 @@ namespace TOB
|
||||
|
||||
buf.WriteString(new_message);
|
||||
|
||||
auto outapp = new EQApplicationPacket(OP_SpecialMesg, buf);
|
||||
auto outapp = new EQApplicationPacket(OP_SpecialMesg, std::move(buf));
|
||||
|
||||
dest->FastQueuePacket(&outapp, ack_req);
|
||||
delete in;
|
||||
@ -2934,14 +2974,14 @@ namespace TOB
|
||||
return;
|
||||
}
|
||||
|
||||
auto outapp = new EQApplicationPacket(OP_ChangeSize, sizeof(ChangeSize_Struct));
|
||||
auto outapp = new EQApplicationPacket(OP_ChangeSize, sizeof(structs::ChangeSize_Struct));
|
||||
|
||||
ChangeSize_Struct* css = (ChangeSize_Struct*)outapp->pBuffer;
|
||||
structs::ChangeSize_Struct* css = (structs::ChangeSize_Struct*)outapp->pBuffer;
|
||||
|
||||
css->EntityID = sas->spawn_id;
|
||||
css->Size = (float)sas->parameter;
|
||||
css->Unknown08 = 0;
|
||||
css->Unknown12 = 1.0f;
|
||||
css->CameraOffset = 0;
|
||||
css->AnimationSpeedRelated = 1.0f;
|
||||
|
||||
dest->FastQueuePacket(&outapp, ack_req);
|
||||
delete in;
|
||||
@ -3574,18 +3614,28 @@ namespace TOB
|
||||
|
||||
DECODE(OP_BlockedBuffs)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::BlockedBuffs_Struct);
|
||||
SETUP_DIRECT_DECODE(BlockedBuffs_Struct, structs::BlockedBuffs_Struct);
|
||||
uint32 count = __packet->ReadUInt32();
|
||||
std::vector<int32> blocked_spell_ids;
|
||||
blocked_spell_ids.reserve(count);
|
||||
for (int i = 0; i < count; ++i)
|
||||
blocked_spell_ids.push_back(static_cast<int32>(__packet->ReadUInt32()));
|
||||
|
||||
for (uint32 i = 0; i < BLOCKED_BUFF_COUNT; ++i)
|
||||
emu->SpellID[i] = eq->SpellID[i];
|
||||
bool pet = __packet->ReadUInt8() == 1;
|
||||
bool init = __packet->ReadUInt8() == 1;
|
||||
|
||||
IN(Count);
|
||||
IN(Pet);
|
||||
IN(Initialise);
|
||||
IN(Flags);
|
||||
__packet->SetReadPosition(0); // reset the packet read to pass it along
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
__packet->size = sizeof(BlockedBuffs_Struct);
|
||||
__packet->pBuffer = new unsigned char[__packet->size]{};
|
||||
BlockedBuffs_Struct* emu = (BlockedBuffs_Struct*)__packet->pBuffer;
|
||||
|
||||
memset(emu->SpellID, -1, sizeof(emu->SpellID));
|
||||
for (int i = 0; i < count; ++i)
|
||||
emu->SpellID[i] = blocked_spell_ids[i];
|
||||
|
||||
emu->Count = count;
|
||||
emu->Pet = pet;
|
||||
emu->Initialise = init;
|
||||
}
|
||||
|
||||
DECODE(OP_CastSpell)
|
||||
@ -3794,6 +3844,35 @@ namespace TOB
|
||||
DECODE_FORWARD(OP_GroupInvite);
|
||||
}
|
||||
|
||||
DECODE(OP_MemorizeSpell) {
|
||||
DECODE_LENGTH_EXACT(structs::MemorizeSpell_Struct);
|
||||
SETUP_DIRECT_DECODE(MemorizeSpell_Struct, structs::MemorizeSpell_Struct);
|
||||
|
||||
// TOB sends status 1 here to let the server know that it's started memming, but doesn't want a response
|
||||
if (eq->scribing == 1) {
|
||||
// TODO: There should be a timer set here to detect short-mem cheats, and then checked when the 2 packet is sent
|
||||
// The previous detection will still happen on scribing == 2, the new client just handles it better
|
||||
__packet->SetOpcode(OP_Unknown);
|
||||
return;
|
||||
}
|
||||
|
||||
// in TOB, 2 is "finish memming" so that becomes 1 in emu and 3 is "unmem" which becomes 2
|
||||
if (eq->scribing == 2)
|
||||
emu->scribing = 1;
|
||||
else if (eq->scribing == 3)
|
||||
emu->scribing = 2;
|
||||
else if (eq->scribing == 4)
|
||||
emu->scribing = 3;
|
||||
else
|
||||
IN(scribing);
|
||||
|
||||
IN(slot);
|
||||
IN(spell_id);
|
||||
IN(reduction);
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_MoveItem)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::MoveItem_Struct);
|
||||
@ -3817,7 +3896,7 @@ namespace TOB
|
||||
|
||||
int r;
|
||||
for (r = 0; r < 29; r++) {
|
||||
// Size 68 in TOB
|
||||
// Size 69 in TOB
|
||||
IN(filters[r]);
|
||||
}
|
||||
|
||||
@ -4121,7 +4200,7 @@ namespace TOB
|
||||
//s32 Variation;
|
||||
//s32 NewArmorId;
|
||||
//s32 NewArmorType;
|
||||
buffer.WriteUInt32(item->Material);
|
||||
buffer.WriteUInt32(item->Material); // this isn't labeled well, material is material *type*
|
||||
buffer.WriteUInt32(0); //unsupported atm
|
||||
buffer.WriteUInt32(item->EliteMaterial);
|
||||
buffer.WriteUInt32(item->HerosForgeModel);
|
||||
@ -4239,9 +4318,20 @@ namespace TOB
|
||||
|
||||
//u8 SpellDataSkillMask[78];
|
||||
for (int j = 0; j < 78; ++j) {
|
||||
buffer.WriteUInt8(0); //unsure what this is exactly
|
||||
buffer.WriteUInt8(0); // TODO: collection of ints for bitfield for each skill required to use. reads 19 ints byte by byte in the client, leave like this for further investigation
|
||||
}
|
||||
|
||||
|
||||
/* There are a static 7 spell data entries on an item:
|
||||
Clicky
|
||||
Proc
|
||||
Worn
|
||||
Focus
|
||||
Scroll
|
||||
Focus2
|
||||
Blessing
|
||||
*/
|
||||
|
||||
/* SpellData:
|
||||
s32 SpellId;
|
||||
u8 RequiredLevel;
|
||||
@ -4650,55 +4740,42 @@ namespace TOB
|
||||
//ItemDefinition Item;
|
||||
SerializeItemDefinition(buffer, item);
|
||||
|
||||
//u32 RealEstateArrayCount;
|
||||
// buffer.WriteInt32(0);
|
||||
//s32 RealEstateArray[RealEstateArrayCount];
|
||||
|
||||
//bool bRealEstateItemPlaceable;
|
||||
// buffer.WriteInt8(0);
|
||||
|
||||
//u32 SubContentSize;
|
||||
uint32 subitem_count = 0;
|
||||
|
||||
int16 SubSlotNumber = EQ::invbag::SLOT_INVALID;
|
||||
|
||||
if (slot_id_in <= EQ::invslot::GENERAL_END && slot_id_in >= EQ::invslot::GENERAL_BEGIN)
|
||||
SubSlotNumber = EQ::invbag::GENERAL_BAGS_BEGIN + ((slot_id_in - EQ::invslot::GENERAL_BEGIN) * EQ::invbag::SLOT_COUNT);
|
||||
SubSlotNumber = EQ::invbag::GENERAL_BAGS_BEGIN + (slot_id_in - EQ::invslot::GENERAL_BEGIN) * EQ::invbag::SLOT_COUNT;
|
||||
else if (slot_id_in == EQ::invslot::slotCursor)
|
||||
SubSlotNumber = EQ::invbag::CURSOR_BAG_BEGIN;
|
||||
else if (slot_id_in <= EQ::invslot::BANK_END && slot_id_in >= EQ::invslot::BANK_BEGIN)
|
||||
SubSlotNumber = EQ::invbag::BANK_BAGS_BEGIN + ((slot_id_in - EQ::invslot::BANK_BEGIN) * EQ::invbag::SLOT_COUNT);
|
||||
SubSlotNumber = EQ::invbag::BANK_BAGS_BEGIN + (slot_id_in - EQ::invslot::BANK_BEGIN) * EQ::invbag::SLOT_COUNT;
|
||||
else if (slot_id_in <= EQ::invslot::SHARED_BANK_END && slot_id_in >= EQ::invslot::SHARED_BANK_BEGIN)
|
||||
SubSlotNumber = EQ::invbag::SHARED_BANK_BAGS_BEGIN + ((slot_id_in - EQ::invslot::SHARED_BANK_BEGIN) * EQ::invbag::SLOT_COUNT);
|
||||
SubSlotNumber = EQ::invbag::SHARED_BANK_BAGS_BEGIN + (slot_id_in - EQ::invslot::SHARED_BANK_BEGIN) * EQ::invbag::SLOT_COUNT;
|
||||
else
|
||||
SubSlotNumber = slot_id_in; // not sure if this is the best way to handle this..leaving for now
|
||||
|
||||
if (SubSlotNumber != EQ::invbag::SLOT_INVALID) {
|
||||
std::vector<std::pair<int, EQ::ItemInstance*>> subitems;
|
||||
for (uint32 index = EQ::invbag::SLOT_BEGIN; index <= EQ::invbag::SLOT_END; ++index) {
|
||||
EQ::ItemInstance* sub = inst->GetItem(index);
|
||||
if (!sub)
|
||||
continue;
|
||||
|
||||
++subitem_count;
|
||||
if (sub != nullptr)
|
||||
subitems.emplace_back(index, sub);
|
||||
}
|
||||
|
||||
buffer.WriteUInt32(subitem_count);
|
||||
|
||||
for (uint32 index = EQ::invbag::SLOT_BEGIN; index <= EQ::invbag::SLOT_END; ++index) {
|
||||
EQ::ItemInstance* sub = inst->GetItem(index);
|
||||
if (!sub)
|
||||
continue;
|
||||
buffer.WriteUInt32(subitems.size());
|
||||
|
||||
// This must be guaranteed to have subitem_count members, where the index is the correct index. The client doesn't loop through all slots here
|
||||
for (const auto& [index, subitem] : subitems) {
|
||||
buffer.WriteUInt32(index);
|
||||
|
||||
SerializeItem(buffer, sub, SubSlotNumber, (depth + 1), packet_type);
|
||||
SerializeItem(buffer, subitem, SubSlotNumber, depth + 1, packet_type);
|
||||
}
|
||||
}
|
||||
} else
|
||||
buffer.WriteUInt32(0); // no subitems, client needs to know that
|
||||
|
||||
//bool bCollected;
|
||||
buffer.WriteInt8(0); //unsupported atm
|
||||
//u64 DontKnow;
|
||||
buffer.WriteUInt64(0); //unsupported atm
|
||||
buffer.WriteInt64(0); //unsupported atm
|
||||
//s32 Luck;
|
||||
buffer.WriteInt32(0); //unsupported atm
|
||||
}
|
||||
@ -4785,7 +4862,9 @@ namespace TOB
|
||||
}
|
||||
default:
|
||||
//unsupported etag right now; just pass it as is
|
||||
message_out.push_back('\x12');
|
||||
message_out.append(segments[segment_iter]);
|
||||
message_out.push_back('\x12');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -5019,8 +5098,8 @@ namespace TOB
|
||||
TOBSlot.Slot = server_slot - EQ::invslot::WORLD_BEGIN;
|
||||
}
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert Server Slot %i to TOB Slot [%i, %i, %i, %i]",
|
||||
server_slot, TOBSlot.Type, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert Server Slot {} to TOB Slot [{}, {}, {}, {}]",
|
||||
server_slot, TOBSlot.Type, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex).c_str());
|
||||
|
||||
return TOBSlot;
|
||||
}
|
||||
@ -5036,8 +5115,8 @@ namespace TOB
|
||||
if (TOBSlot.Slot != invslot::SLOT_INVALID)
|
||||
TOBSlot.Type = invtype::typeCorpse;
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert Server Corpse Slot %i to TOB Corpse Slot [%i, %i, %i, %i]",
|
||||
server_corpse_slot, TOBSlot.Type, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert Server Corpse Slot {} to TOB Corpse Slot [{}, {}, {}, {}]",
|
||||
server_corpse_slot, TOBSlot.Type, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex).c_str());
|
||||
|
||||
return TOBSlot;
|
||||
}
|
||||
@ -5077,8 +5156,8 @@ namespace TOB
|
||||
}
|
||||
}
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert Server Slot %i to TOB Typeless Slot [%i, %i, %i] (implied type: %i)",
|
||||
server_slot, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex, server_type);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert Server Slot {} to TOB Typeless Slot [{}, {}, {}] (implied type: {})",
|
||||
server_slot, TOBSlot.Slot, TOBSlot.SubIndex, TOBSlot.AugIndex, server_type).c_str());
|
||||
|
||||
return TOBSlot;
|
||||
}
|
||||
@ -5086,8 +5165,8 @@ namespace TOB
|
||||
static inline uint32 TOBToServerSlot(structs::InventorySlot_Struct tob_slot)
|
||||
{
|
||||
if (tob_slot.AugIndex < invaug::SOCKET_INVALID || tob_slot.AugIndex >= invaug::SOCKET_COUNT) {
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert TOB Slot [%i, %i, %i, %i] to Server Slot %i",
|
||||
tob_slot.Type, tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, EQ::invslot::SLOT_INVALID);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert TOB Slot [{}, {}, {}, {}] to Server Slot {}",
|
||||
tob_slot.Type, tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, EQ::invslot::SLOT_INVALID).c_str());
|
||||
|
||||
return EQ::invslot::SLOT_INVALID;
|
||||
}
|
||||
@ -5209,8 +5288,8 @@ namespace TOB
|
||||
}
|
||||
}
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert TOB Slot [%i, %i, %i, %i] to Server Slot %i",
|
||||
tob_slot.Type, tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, server_slot);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert TOB Slot [{}, {}, {}, {}] to Server Slot {}",
|
||||
tob_slot.Type, tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, server_slot).c_str());
|
||||
|
||||
return server_slot;
|
||||
}
|
||||
@ -5227,8 +5306,8 @@ namespace TOB
|
||||
ServerSlot = TOBToServerCorpseMainSlot(tob_corpse_slot.Slot);
|
||||
}
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert TOB Slot [%i, %i, %i, %i] to Server Slot %i",
|
||||
tob_corpse_slot.Type, tob_corpse_slot.Slot, tob_corpse_slot.SubIndex, tob_corpse_slot.AugIndex, ServerSlot);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert TOB Slot [{}, {}, {}, {}] to Server Slot {}",
|
||||
tob_corpse_slot.Type, tob_corpse_slot.Slot, tob_corpse_slot.SubIndex, tob_corpse_slot.AugIndex, ServerSlot).c_str());
|
||||
|
||||
return ServerSlot;
|
||||
}
|
||||
@ -5249,8 +5328,8 @@ namespace TOB
|
||||
static inline uint32 TOBToServerTypelessSlot(structs::TypelessInventorySlot_Struct tob_slot, int16 tob_type)
|
||||
{
|
||||
if (tob_slot.AugIndex < invaug::SOCKET_INVALID || tob_slot.AugIndex >= invaug::SOCKET_COUNT) {
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert TOB Typeless Slot [%i, %i, %i] (implied type: %i) to Server Slot %i",
|
||||
tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, tob_type, EQ::invslot::SLOT_INVALID);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert TOB Typeless Slot [{}, {}, {}] (implied type: {}) to Server Slot {}",
|
||||
tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, tob_type, EQ::invslot::SLOT_INVALID).c_str());
|
||||
|
||||
return EQ::invslot::SLOT_INVALID;
|
||||
}
|
||||
@ -5363,8 +5442,8 @@ namespace TOB
|
||||
}
|
||||
}
|
||||
|
||||
Log(Logs::Detail, Logs::Netcode, "Convert TOB Typeless Slot [%i, %i, %i] (implied type: %i) to Server Slot %i",
|
||||
tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, tob_type, ServerSlot);
|
||||
Log(Logs::Detail, Logs::Netcode, fmt::format("Convert TOB Typeless Slot [{}, {}, {}] (implied type: {}) to Server Slot {}",
|
||||
tob_slot.Slot, tob_slot.SubIndex, tob_slot.AugIndex, tob_type, ServerSlot).c_str());
|
||||
|
||||
return ServerSlot;
|
||||
}
|
||||
|
||||
@ -200,8 +200,8 @@ namespace TOB
|
||||
|
||||
const int16 SLOT_INVALID = IINVALID;
|
||||
const int16 SLOT_BEGIN = INULL;
|
||||
const int16 SLOT_END = 9; //254;
|
||||
const int16 SLOT_COUNT = 10; //255; // server Size will be 255..unsure what actual client is (test)
|
||||
const int16 SLOT_END = 199;
|
||||
const int16 SLOT_COUNT = 200; // server Size will be 200..unsure what actual client is (test)
|
||||
|
||||
const char* GetInvBagIndexName(int16 bag_index);
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
//list of packets we need to encode on the way out:
|
||||
E(OP_AAExpUpdate)
|
||||
E(OP_Action)
|
||||
E(OP_Animation)
|
||||
E(OP_ApplyPoison)
|
||||
@ -32,6 +33,7 @@ E(OP_Illusion)
|
||||
E(OP_ItemPacket)
|
||||
E(OP_LogServer)
|
||||
E(OP_ManaChange)
|
||||
E(OP_MemorizeSpell)
|
||||
E(OP_MobHealth)
|
||||
E(OP_MoneyOnCorpse)
|
||||
E(OP_MoveItem)
|
||||
@ -83,6 +85,7 @@ D(OP_GMTraining)
|
||||
D(OP_GroupDisband)
|
||||
D(OP_GroupInvite)
|
||||
D(OP_GroupInvite2)
|
||||
D(OP_MemorizeSpell)
|
||||
D(OP_MoveItem)
|
||||
D(OP_RemoveBlockedBuffs)
|
||||
D(OP_SetServerFilter)
|
||||
|
||||
@ -12,7 +12,7 @@ namespace TOB {
|
||||
static const uint32 MAX_PP_UNKNOWN_ABILITIES = 25;
|
||||
static const uint32 MAX_RECAST_TYPES = 25;
|
||||
static const uint32 MAX_ITEM_RECAST_TYPES = 100;
|
||||
static const uint32 BLOCKED_BUFF_COUNT = 40;
|
||||
static const uint32 BLOCKED_BUFF_COUNT = 60; // this might not be needed?
|
||||
static const uint32 BUFF_COUNT = 62;
|
||||
static const uint32 MAX_PP_LANGUAGE = 32;
|
||||
#pragma pack(1)
|
||||
@ -274,7 +274,6 @@ namespace TOB {
|
||||
Unknown39,
|
||||
Unknown40,
|
||||
Unknown41,
|
||||
Unknown42,
|
||||
Birthdate,
|
||||
EncounterLock
|
||||
};
|
||||
@ -288,6 +287,15 @@ namespace TOB {
|
||||
/*0024*/
|
||||
};
|
||||
|
||||
struct ChangeSize_Struct
|
||||
{
|
||||
/*00*/ uint32 EntityID;
|
||||
/*04*/ float Size;
|
||||
/*08*/ float CameraOffset;
|
||||
/*12*/ float AnimationSpeedRelated;
|
||||
/*16*/
|
||||
};
|
||||
|
||||
struct Spawn_Struct_Bitfields
|
||||
{
|
||||
union {
|
||||
@ -400,14 +408,14 @@ namespace TOB {
|
||||
/*056*/ float X;
|
||||
/*060*/ float Z;
|
||||
/*064*/ float Heading;
|
||||
/*068*/ float DoorAngle; //not sure if this is actually a float; it might be a uint32 like DefaultDoorAngle
|
||||
/*068*/ float DoorAngle;
|
||||
/*072*/ uint32 ScaleFactor; //rof2's size
|
||||
/*076*/ uint32 Unknown76; //client doesn't seem to read this
|
||||
/*080*/ uint8 Id; //doorid
|
||||
/*081*/ uint8 Type; //opentype
|
||||
/*082*/ uint8 State; //state_at_spawn
|
||||
/*083*/ uint8 DefaultState; //invert_state
|
||||
/*084*/ int32 Param; //door_param
|
||||
/*084*/ int32 Param; //door_param (spell id?)
|
||||
/*088*/ uint32 AdventureDoorId;
|
||||
/*092*/ uint32 DynDoorID;
|
||||
/*096*/ uint32 RealEstateDoorID;
|
||||
@ -495,8 +503,8 @@ namespace TOB {
|
||||
|
||||
struct ExpUpdate_Struct
|
||||
{
|
||||
/*000*/ uint64 exp; //This is exp % / 1000 now; eg 69250 = 69.25%
|
||||
/*008*/ uint64 unknown; //unclear, I didn't see the client actually read this value but i might have missed it
|
||||
/*000*/ uint64 exp; // This is exp % / 1000 now; eg 69250 = 69.25%
|
||||
/*008*/ uint64 unknown; // if this is the value "2", it opens up the tip window
|
||||
};
|
||||
|
||||
struct DeleteSpawn_Struct
|
||||
@ -508,15 +516,14 @@ namespace TOB {
|
||||
|
||||
//OP_SetServerFilter
|
||||
struct SetServerFilter_Struct {
|
||||
uint32 filters[68];
|
||||
uint32 filters[69];
|
||||
};
|
||||
|
||||
// Was new to RoF2, doesn't look changed
|
||||
// The padding is because these structs are padded to the default 4 bytes
|
||||
struct InventorySlot_Struct
|
||||
{
|
||||
/*000*/ int16 Type;
|
||||
/*002*/ int16 Padding1;
|
||||
/*000*/ int32 Type;
|
||||
/*004*/ int16 Slot;
|
||||
/*006*/ int16 SubIndex;
|
||||
/*008*/ int16 AugIndex;
|
||||
@ -549,15 +556,6 @@ namespace TOB {
|
||||
/*024*/
|
||||
};
|
||||
|
||||
struct ChangeSize_Struct
|
||||
{
|
||||
/*00*/ uint32 EntityID;
|
||||
/*04*/ float Size;
|
||||
/*08*/ uint32 Unknown08; // Observed 0
|
||||
/*12*/ float Unknown12; // Observed 1.0f
|
||||
/*16*/
|
||||
};
|
||||
|
||||
struct SpawnHPUpdate_Struct
|
||||
{
|
||||
/*00*/ int16 spawn_id;
|
||||
@ -677,11 +675,18 @@ namespace TOB {
|
||||
/*000*/ uint32 spell_id;
|
||||
/*004*/ uint16 caster_id;
|
||||
/*006*/ uint32 cast_time; // in miliseconds
|
||||
/*010*/ uint32 unknown0a; // I think this is caster effective level but im not sure. live always sends 0
|
||||
/*010*/ uint32 unknown0a; // I think this is caster effective level but im not sure. live always sends 0. The client uses this for the spell link
|
||||
/*014*/ uint8 unknown0e; // 0 will short circuit the cast, seen 1 from live usually, maybe related to interrupts or particles or something
|
||||
/*015*/
|
||||
};
|
||||
|
||||
struct MemorizeSpell_Struct {
|
||||
uint32 slot; // Spot in the spell book/memorized slot
|
||||
uint32 spell_id; // Spell id (200 or c8 is minor healing, etc)
|
||||
uint32 scribing; // -1 refreshes book, 0 scribe to book, 2 end mem, 1 start mem, 3 unmem, 4 set activated item keyring -- client will send back 2 if a 0 operation updated a memorized spell of the same group + subgroup
|
||||
uint32 reduction; // lower reuse (only used if scribing is 4)
|
||||
};
|
||||
|
||||
//I've observed 5 s16 that are all -1.
|
||||
//Clicky items don't even trigger this as far as i can tell so not sure what this is for now.
|
||||
//One of these could have changed to a s32 but im not sure.
|
||||
@ -708,6 +713,13 @@ namespace TOB {
|
||||
/*39*/
|
||||
};
|
||||
|
||||
struct InterruptCast_Struct
|
||||
{
|
||||
uint32 spawnid;
|
||||
uint32 messageid;
|
||||
char message[0];
|
||||
};
|
||||
|
||||
struct EQAffectSlot_Struct {
|
||||
/*00*/ int32 slot;
|
||||
/*04*/ int32 padding;
|
||||
@ -749,10 +761,10 @@ namespace TOB {
|
||||
struct ManaChange_Struct
|
||||
{
|
||||
uint32 new_mana;
|
||||
uint32 stamina;
|
||||
uint32 stamina; // endurance
|
||||
uint32 spell_id;
|
||||
uint32 keepcasting;
|
||||
int32 slot;
|
||||
int32 slot; // gem slot
|
||||
};
|
||||
|
||||
//This is what we call OP_Action
|
||||
@ -819,9 +831,8 @@ namespace TOB {
|
||||
struct AA_Array
|
||||
{
|
||||
uint32 AA;
|
||||
uint32 value;
|
||||
uint32 value; // points spent
|
||||
uint32 charges; // expendable charges
|
||||
bool bUnknown0x0c; // added test winter 2024; removed sometime in summer 2024
|
||||
};
|
||||
|
||||
struct AATable_Struct {
|
||||
@ -834,16 +845,7 @@ namespace TOB {
|
||||
/*000*/ uint32 experience;
|
||||
/*004*/ uint32 unspent;
|
||||
/*008*/ uint8 percentage;
|
||||
/*009*/ uint8 unknown009[3];
|
||||
};
|
||||
|
||||
struct BlockedBuffs_Struct
|
||||
{
|
||||
/*000*/ int32 SpellID[BLOCKED_BUFF_COUNT];
|
||||
/*120*/ uint32 Count;
|
||||
/*124*/ uint8 Pet;
|
||||
/*125*/ uint8 Initialise;
|
||||
/*126*/ uint16 Flags;
|
||||
/*009*/ uint8 padding[3];
|
||||
};
|
||||
|
||||
struct ZonePlayerToBind_Struct {
|
||||
|
||||
@ -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 = MAXMERCS)")
|
||||
RULE_CATEGORY_END()
|
||||
|
||||
RULE_CATEGORY(Guild)
|
||||
@ -349,7 +350,7 @@ RULE_STRING(World, MOTD, "", "Server MOTD sent on login, change from empty to ha
|
||||
RULE_STRING(World, Rules, "", "Server Rules, change from empty to have this be used instead of variables table 'rules' value, lines are pipe (|) separated, example: A|B|C")
|
||||
RULE_BOOL(World, EnableAutoLogin, false, "Enables or disables auto login of characters, allowing people to log characters in directly from loginserver to ingame")
|
||||
RULE_BOOL(World, EnablePVPRegions, true, "Enables or disables PVP Regions automatically setting your PVP flag")
|
||||
RULE_STRING(World, SupportedClients, "RoF2", "Comma-delimited list of clients to restrict to. Supported values are Titanium | SoF | SoD | UF | RoF | RoF2. Example: Titanium,RoF2")
|
||||
RULE_STRING(World, SupportedClients, "RoF2,TOB", "Comma-delimited list of clients to restrict to. Supported values are Titanium | SoF | SoD | UF | RoF | RoF2 | TOB. Example: Titanium,RoF2,TOB")
|
||||
RULE_STRING(World, CustomFilesKey, "", "Enable if the server requires custom files and sends a key to validate. Empty string to disable. Example: eqcustom_v1")
|
||||
RULE_STRING(World, CustomFilesUrl, "github.com/knervous/eqnexus/releases", "URL to display at character select if client is missing custom files")
|
||||
RULE_INT(World, CustomFilesAdminLevel, 20, "Admin level at which custom file key is not required when CustomFilesKey is specified")
|
||||
|
||||
@ -999,7 +999,7 @@ uint8 GetSpellLevel(uint16 spell_id, uint8 class_id)
|
||||
return UINT8_MAX;
|
||||
}
|
||||
|
||||
if (class_id >= Class::PLAYER_CLASS_COUNT) {
|
||||
if (class_id < Class::Warrior || class_id > Class::PLAYER_CLASS_COUNT) {
|
||||
return UINT8_MAX;
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| Opcode | Status | Notes | Working On |
|
||||
|:----------------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|
|
||||
| `OP_AAAction` | 🟡 Unverified | | |
|
||||
| `OP_AAExpUpdate` | 🟡 Unverified | | |
|
||||
| `OP_AAExpUpdate` | 🟢 Verified | | |
|
||||
| `OP_AcceptNewTask` | 🔴 Not-Set | | |
|
||||
| `OP_AckPacket` | 🟢 Verified | | |
|
||||
| `OP_Action` | 🟡 Unverified | | |
|
||||
@ -63,9 +63,9 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_BecomeCorpse` | 🔴 Not-Set | | |
|
||||
| `OP_BecomeTrader` | 🔴 Not-Set | | |
|
||||
| `OP_Begging` | 🟡 Unverified | | |
|
||||
| `OP_BeginCast` | 🟡 Unverified | | |
|
||||
| `OP_BeginCast` | 🟢 Verified | | |
|
||||
| `OP_Bind_Wound` | 🟡 Unverified | | |
|
||||
| `OP_BlockedBuffs` | 🟡 Unverified | | |
|
||||
| `OP_BlockedBuffs` | 🟢 Verified | | |
|
||||
| `OP_BoardBoat` | 🟡 Unverified | | |
|
||||
| `OP_BookButton` | 🟡 Unverified | | |
|
||||
| `OP_Buff` | 🟡 Unverified | | |
|
||||
@ -79,17 +79,17 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_CancelTask` | 🔴 Not-Set | | |
|
||||
| `OP_CancelTrade` | 🟡 Unverified | | |
|
||||
| `OP_CashReward` | 🟡 Unverified | | |
|
||||
| `OP_CastSpell` | 🟡 Unverified | | |
|
||||
| `OP_ChangeSize` | 🟡 Unverified | | |
|
||||
| `OP_CastSpell` | 🟢 Verified | | |
|
||||
| `OP_ChangeSize` | 🟢 Verified | | |
|
||||
| `OP_ChannelMessage` | 🟡 Unverified | | |
|
||||
| `OP_ChangePetName` | 🔴 Not-Set | | |
|
||||
| `OP_CharacterCreate` | 🟢 Verified | Sends heroic type, can be used for something? | |
|
||||
| `OP_CharacterCreateRequest` | 🟢 Verified | | |
|
||||
| `OP_CharInventory` | 🟡 Unverified | | |
|
||||
| `OP_CharInventory` | 🟢 Verified | | |
|
||||
| `OP_Charm` | 🟡 Unverified | | |
|
||||
| `OP_ChatMessage` | 🔴 Not-Set | | |
|
||||
| `OP_ClearAA` | 🟡 Unverified | | |
|
||||
| `OP_ClearBlockedBuffs` | 🟡 Unverified | | |
|
||||
| `OP_ClearAA` | 🟢 Verified | | |
|
||||
| `OP_ClearBlockedBuffs` | 🟢 Verified | | |
|
||||
| `OP_ClearLeadershipAbilities` | 🔴 Not-Set | | |
|
||||
| `OP_ClearNPCMarks` | 🔴 Not-Set | | |
|
||||
| `OP_ClearObject` | 🟡 Unverified | | |
|
||||
@ -98,20 +98,20 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_ClickObject` | 🟡 Unverified | | |
|
||||
| `OP_ClickObjectAction` | 🟡 Unverified | | |
|
||||
| `OP_ClientError` | 🔴 Not-Set | | |
|
||||
| `OP_ClientReady` | 🟡 Unverified | | |
|
||||
| `OP_ClientReady` | 🟢 Verified | | |
|
||||
| `OP_ClientTimeStamp` | 🔴 Not-Set | | |
|
||||
| `OP_ClientUpdate` | 🟡 Unverified | | |
|
||||
| `OP_ClientUpdate` | 🟢 Verified | | |
|
||||
| `OP_CloseContainer` | 🔴 Not-Set | | |
|
||||
| `OP_CloseTributeMaster` | 🔴 Not-Set | | |
|
||||
| `OP_ColoredText` | 🟡 Unverified | | |
|
||||
| `OP_ColoredText` | 🟢 Verified | | |
|
||||
| `OP_CombatAbility` | 🟡 Unverified | | |
|
||||
| `OP_Command` | 🔴 Not-Set | | |
|
||||
| `OP_CompletedTasks` | 🔴 Not-Set | | |
|
||||
| `OP_ConfirmDelete` | 🟡 Unverified | | |
|
||||
| `OP_Consent` | 🟡 Unverified | | |
|
||||
| `OP_ConsentDeny` | 🟡 Unverified | | |
|
||||
| `OP_ConsentResponse` | 🟡 Unverified | | |
|
||||
| `OP_Consider` | 🟡 Unverified | | |
|
||||
| `OP_ConsentResponse` | 🟢 Verified | | |
|
||||
| `OP_Consider` | 🟢 Verified | | |
|
||||
| `OP_ConsiderCorpse` | 🟡 Unverified | | |
|
||||
| `OP_Consume` | 🟡 Unverified | | |
|
||||
| `OP_ControlBoat` | 🟡 Unverified | | |
|
||||
@ -170,7 +170,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_EnvDamage` | 🟡 Unverified | | |
|
||||
| `OP_EvolveItem` | 🔴 Not-Set | | |
|
||||
| `OP_ExpansionInfo` | 🟢 Verified | Updated from u32 to u64 and works now | |
|
||||
| `OP_ExpUpdate` | 🟡 Unverified | | |
|
||||
| `OP_ExpUpdate` | 🟢 Verified | | |
|
||||
| `OP_FaceChange` | 🔴 Not-Set | | |
|
||||
| `OP_Feedback` | 🔴 Not-Set | | |
|
||||
| `OP_FeignDeath` | 🟡 Unverified | | |
|
||||
@ -182,10 +182,10 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_FinishWindow2` | 🟡 Unverified | | |
|
||||
| `OP_Fishing` | 🟡 Unverified | | |
|
||||
| `OP_Fling` | 🟡 Unverified | | |
|
||||
| `OP_FloatListThing` | 🟢 Verified | Movement History. Sent from client, but emu doesn't use it so setting it as varified. Reference is 0x1402FFAD0 | |
|
||||
| `OP_FloatListThing` | 🟢 Verified | Movement History. Sent from client, but emu doesn't use it so setting it as verified. Reference is 0x1402FFAD0 | |
|
||||
| `OP_Forage` | 🟡 Unverified | | |
|
||||
| `OP_ForceFindPerson` | 🔴 Not-Set | | |
|
||||
| `OP_FormattedMessage` | 🟡 Unverified | | |
|
||||
| `OP_FormattedMessage` | 🟢 Verified | Some major work to do here -- the client now expects a spell link in the packet, will need to refactor the client work to decouple the stream from the internal representation | |
|
||||
| `OP_FriendsWho` | 🟡 Unverified | | |
|
||||
| `OP_GetGuildMOTD` | 🔴 Not-Set | | |
|
||||
| `OP_GetGuildMOTDReply` | 🔴 Not-Set | | |
|
||||
@ -213,7 +213,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_GMTrainSkillConfirm` | 🟡 Unverified | | |
|
||||
| `OP_GMZoneRequest` | 🔴 Not-Set | | |
|
||||
| `OP_GMZoneRequest2` | 🔴 Not-Set | | |
|
||||
| `OP_GroundSpawn` | 🟡 Unverified | | |
|
||||
| `OP_GroundSpawn` | 🟢 Verified | | |
|
||||
| `OP_GroupAcknowledge` | 🔴 Not-Set | | |
|
||||
| `OP_GroupCancelInvite` | 🔴 Not-Set | | |
|
||||
| `OP_GroupDelete` | 🔴 Not-Set | | |
|
||||
@ -279,7 +279,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_Heartbeat` | 🔴 Not-Set | | |
|
||||
| `OP_Hide` | 🟡 Unverified | | |
|
||||
| `OP_HideCorpse` | 🟡 Unverified | | |
|
||||
| `OP_HPUpdate` | 🟡 Unverified | | |
|
||||
| `OP_HPUpdate` | 🟢 Verified | | |
|
||||
| `OP_Illusion` | 🟡 Unverified | | |
|
||||
| `OP_IncreaseStats` | 🟡 Unverified | | |
|
||||
| `OP_InitialHPUpdate` | 🔴 Not-Set | | |
|
||||
@ -289,7 +289,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_InspectMessageUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_InspectRequest` | 🔴 Not-Set | | |
|
||||
| `OP_InstillDoubt` | 🟡 Unverified | | |
|
||||
| `OP_InterruptCast` | 🟡 Unverified | | |
|
||||
| `OP_InterruptCast` | 🟢 Verified | Some major work to do here -- the client now expects a spell link in the packet, will need to refactor the client work to decouple the stream from the internal representation | |
|
||||
| `OP_InvokeChangePetName` | 🔴 Not-Set | | |
|
||||
| `OP_InvokeChangePetNameImmediate` | 🔴 Not-Set | | |
|
||||
| `OP_InvokeNameChangeImmediate` | 🔴 Not-Set | | |
|
||||
@ -330,7 +330,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_LFPCommand` | 🔴 Not-Set | | |
|
||||
| `OP_LFPGetMatchesRequest` | 🔴 Not-Set | | |
|
||||
| `OP_LFPGetMatchesResponse` | 🔴 Not-Set | | |
|
||||
| `OP_LinkedReuse` | 🟡 Unverified | | |
|
||||
| `OP_LinkedReuse` | 🟢 Verified | | |
|
||||
| `OP_LoadSpellSet` | 🔴 Not-Set | | |
|
||||
| `OP_LocInfo` | 🔴 Not-Set | | |
|
||||
| `OP_LockoutTimerInfo` | 🔴 Not-Set | | |
|
||||
@ -346,12 +346,12 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_LootComplete` | 🟡 Unverified | | |
|
||||
| `OP_LootItem` | 🟡 Unverified | | |
|
||||
| `OP_LootRequest` | 🟡 Unverified | | |
|
||||
| `OP_ManaChange` | 🟡 Unverified | | |
|
||||
| `OP_ManaChange` | 🟢 Verified | | |
|
||||
| `OP_ManaUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_MarkNPC` | 🔴 Not-Set | | |
|
||||
| `OP_MarkRaidNPC` | 🔴 Not-Set | | |
|
||||
| `OP_Marquee` | 🟡 Unverified | | |
|
||||
| `OP_MemorizeSpell` | 🟡 Unverified | | |
|
||||
| `OP_MemorizeSpell` | 🟢 Verified | | |
|
||||
| `OP_Mend` | 🟡 Unverified | | |
|
||||
| `OP_MendHPUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_MercenaryAssign` | 🔴 Not-Set | | |
|
||||
@ -379,7 +379,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_MOTD` | 🟢 Verified | | |
|
||||
| `OP_MoveCoin` | 🟡 Unverified | | |
|
||||
| `OP_MoveDoor` | 🟡 Unverified | | |
|
||||
| `OP_MoveItem` | 🟡 Unverified | | |
|
||||
| `OP_MoveItem` | 🟢 Verified | | |
|
||||
| `OP_MoveMultipleItems` | 🟡 Unverified | | |
|
||||
| `OP_MoveLogDisregard` | 🔴 Not-Set | | |
|
||||
| `OP_MoveLogRequest` | 🔴 Not-Set | | |
|
||||
@ -435,7 +435,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_PVPLeaderBoardRequest` | 🔴 Not-Set | | |
|
||||
| `OP_PVPStats` | 🔴 Not-Set | | |
|
||||
| `OP_QueryResponseThing` | 🔴 Not-Set | | |
|
||||
| `OP_QueryUCSServerStatus` | 🟡 Unverified | | |
|
||||
| `OP_QueryUCSServerStatus` | 🟢 Verified | | |
|
||||
| `OP_RaidDelegateAbility` | 🔴 Not-Set | | |
|
||||
| `OP_RaidClearNPCMarks` | 🔴 Not-Set | | |
|
||||
| `OP_RaidInvite` | 🔴 Not-Set | | |
|
||||
@ -453,7 +453,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_ReclaimCrystals` | 🔴 Not-Set | | |
|
||||
| `OP_ReloadUI` | 🔴 Not-Set | | |
|
||||
| `OP_RemoveAllDoors` | 🟡 Unverified | | |
|
||||
| `OP_RemoveBlockedBuffs` | 🟡 Unverified | | |
|
||||
| `OP_RemoveBlockedBuffs` | 🟢 Verified | | |
|
||||
| `OP_RemoveNimbusEffect` | 🟡 Unverified | | |
|
||||
| `OP_RemoveTrap` | 🔴 Not-Set | | |
|
||||
| `OP_Report` | 🟡 Unverified | | |
|
||||
@ -465,7 +465,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_RequestKnowledgeBase` | 🔴 Not-Set | | |
|
||||
| `OP_RequestTitles` | 🔴 Not-Set | | |
|
||||
| `OP_RespawnWindow` | 🟡 Unverified | | |
|
||||
| `OP_RespondAA` | 🟡 Unverified | | |
|
||||
| `OP_RespondAA` | 🟢 Verified | | |
|
||||
| `OP_RestState` | 🟡 Unverified | | |
|
||||
| `OP_Rewind` | 🟡 Unverified | | |
|
||||
| `OP_RezzAnswer` | 🔴 Not-Set | | |
|
||||
@ -478,9 +478,9 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_SaveOnZoneReq` | 🟡 Unverified | | |
|
||||
| `OP_SelectTribute` | 🔴 Not-Set | | |
|
||||
| `OP_SendAAStats` | 🟡 Unverified | | |
|
||||
| `OP_SendAATable` | 🟡 Unverified | | |
|
||||
| `OP_SendAATable` | 🟢 Verified | | |
|
||||
| `OP_SendCharInfo` | 🟢 Verified | | |
|
||||
| `OP_SendExpZonein` | 🟡 Unverified | | |
|
||||
| `OP_SendExpZonein` | 🟢 Verified | | |
|
||||
| `OP_SendFindableNPCs` | 🔴 Not-Set | | |
|
||||
| `OP_SendGuildTributes` | 🔴 Not-Set | | |
|
||||
| `OP_SendLoginInfo` | 🟢 Verified | | |
|
||||
@ -490,7 +490,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_SendSystemStats` | 🔴 Not-Set | | |
|
||||
| `OP_SendTitleList` | 🔴 Not-Set | | |
|
||||
| `OP_SendTributes` | 🔴 Not-Set | | |
|
||||
| `OP_SendZonepoints` | 🟡 Unverified | | |
|
||||
| `OP_SendZonepoints` | 🟢 Verified | | |
|
||||
| `OP_SenseHeading` | 🟡 Unverified | | |
|
||||
| `OP_SenseTraps` | 🟡 Unverified | | |
|
||||
| `OP_ServerListRequest` | 🔴 Not-Set | | |
|
||||
@ -503,7 +503,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_SetGuildMOTD` | 🔴 Not-Set | | |
|
||||
| `OP_SetGuildRank` | 🔴 Not-Set | | |
|
||||
| `OP_SetRunMode` | 🟡 Unverified | | |
|
||||
| `OP_SetServerFilter` | 🟡 Unverified | | |
|
||||
| `OP_SetServerFilter` | 🟢 Verified | | |
|
||||
| `OP_SetStartCity` | 🔴 Not-Set | | |
|
||||
| `OP_SetTitle` | 🔴 Not-Set | | |
|
||||
| `OP_SetTitleReply` | 🔴 Not-Set | | |
|
||||
@ -533,23 +533,23 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_ShopRetrieveParcel` | 🟡 Unverified | | |
|
||||
| `OP_ShopParcelIcon` | 🟡 Unverified | | |
|
||||
| `OP_ShopRequest` | 🟡 Unverified | | |
|
||||
| `OP_SimpleMessage` | 🟡 Unverified | | |
|
||||
| `OP_SimpleMessage` | 🟢 Verified | | |
|
||||
| `OP_SkillUpdate` | 🟡 Unverified | | |
|
||||
| `OP_Sneak` | 🟡 Unverified | | |
|
||||
| `OP_Some3ByteHPUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_Some6ByteHPUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_SomeItemPacketMaybe` | 🔴 Not-Set | | |
|
||||
| `OP_Sound` | 🟡 Unverified | | |
|
||||
| `OP_SpawnAppearance` | 🟡 Unverified | | |
|
||||
| `OP_SpawnDoor` | 🟡 Unverified | | |
|
||||
| `OP_SpawnAppearance` | 🟢 Verified | | |
|
||||
| `OP_SpawnDoor` | 🟢 Verified | | |
|
||||
| `OP_SpawnPositionUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_SpecialMesg` | 🟡 Unverified | | |
|
||||
| `OP_SpecialMesg` | 🟢 Verified | | |
|
||||
| `OP_SpellEffect` | 🟡 Unverified | | |
|
||||
| `OP_Split` | 🟡 Unverified | | |
|
||||
| `OP_Stamina` | 🟢 Verified | These values are 0-32k instead of 0-127 | |
|
||||
| `OP_Stun` | 🟡 Unverified | | |
|
||||
| `OP_Surname` | 🔴 Not-Set | | |
|
||||
| `OP_SwapSpell` | 🟡 Unverified | | |
|
||||
| `OP_SwapSpell` | 🟢 Verified | | |
|
||||
| `OP_SystemFingerprint` | 🔴 Not-Set | | |
|
||||
| `OP_TargetBuffs` | 🔴 Not-Set | | |
|
||||
| `OP_TargetCommand` | 🟡 Unverified | | |
|
||||
@ -594,7 +594,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_TributeToggle` | 🔴 Not-Set | | |
|
||||
| `OP_TributeUpdate` | 🔴 Not-Set | | |
|
||||
| `OP_Untargetable` | 🟡 Unverified | | |
|
||||
| `OP_UpdateAA` | 🟡 Unverified | | |
|
||||
| `OP_UpdateAA` | 🟢 Verified | | |
|
||||
| `OP_UpdateAura` | 🔴 Not-Set | | |
|
||||
| `OP_UpdateLeadershipAA` | 🔴 Not-Set | | |
|
||||
| `OP_VetClaimReply` | 🔴 Not-Set | | |
|
||||
@ -603,8 +603,8 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_VoiceMacroIn` | 🟡 Unverified | | |
|
||||
| `OP_VoiceMacroOut` | 🟡 Unverified | | |
|
||||
| `OP_WeaponEquip1` | 🔴 Not-Set | | |
|
||||
| `OP_WearChange` | 🟡 Unverified | | |
|
||||
| `OP_Weather` | 🟡 Unverified | | |
|
||||
| `OP_WearChange` | 🟢 Verified | | |
|
||||
| `OP_Weather` | 🟢 Verified | | |
|
||||
| `OP_Weblink` | 🟡 Unverified | | |
|
||||
| `OP_WhoAllRequest` | 🟡 Unverified | | |
|
||||
| `OP_WhoAllResponse` | 🟡 Unverified | | |
|
||||
@ -614,7 +614,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th
|
||||
| `OP_WorldClientReady` | 🟢 Verified | | |
|
||||
| `OP_WorldComplete` | 🟢 Verified | | |
|
||||
| `OP_WorldLogout` | 🔴 Not-Set | | |
|
||||
| `OP_WorldObjectsSent` | 🟡 Unverified | | |
|
||||
| `OP_WorldObjectsSent` | 🟢 Verified | | |
|
||||
| `OP_WorldUnknown001` | 🟢 Verified | SetServerTime. emu doesn't currently send it so setting it to verified, but the reference is 0x140292550 | |
|
||||
| `OP_XTargetAutoAddHaters` | 🔴 Not-Set | | |
|
||||
| `OP_XTargetOpen` | 🔴 Not-Set | | |
|
||||
|
||||
@ -418,6 +418,7 @@ OP_MercenaryUnknown1=0x5d26
|
||||
OP_MercenaryCommand=0x27f2
|
||||
OP_MercenarySuspendRequest=0x4407
|
||||
OP_MercenarySuspendResponse=0x6f03
|
||||
OP_MercenarySwitch=0x1b37
|
||||
OP_MercenaryUnsuspendResponse=0x27a0
|
||||
|
||||
# Looting
|
||||
|
||||
@ -93,7 +93,7 @@ OP_ClearAA=0x6093
|
||||
OP_ClearLeadershipAbilities=0x0000 #removed; leadership abilities are baked in and always on
|
||||
OP_RespondAA=0x4449
|
||||
OP_UpdateAA=0x1655
|
||||
OP_SendAAStats=0x7416 #i'll be honest i think this was removed at some point but this is the op at the spot in the list
|
||||
OP_SendAAStats=0x7416 # Removed in TOB
|
||||
OP_AAExpUpdate=0x04c3 #need to look into whether this has changed; exp did
|
||||
OP_ExpUpdate=0x0e55
|
||||
OP_HPUpdate=0x2723
|
||||
@ -222,7 +222,7 @@ OP_KeyRing=0x0000
|
||||
OP_WhoAllRequest=0x3328
|
||||
OP_WhoAllResponse=0x4dfd
|
||||
OP_FriendsWho=0x3547
|
||||
OP_ConfirmDelete=0x14a8
|
||||
OP_ConfirmDelete=0x14a8 # This is sent fromt the client after a movement update (with just spawn ID as the content)
|
||||
OP_Logout=0x46f8
|
||||
OP_Rewind=0x898a
|
||||
OP_TargetCommand=0x46bf
|
||||
|
||||
@ -5175,7 +5175,7 @@ int Bot::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
ac_bonus = inst->GetItemArmorClass(true) / 25.0f;
|
||||
if (ac_bonus > skill_bonus)
|
||||
ac_bonus = skill_bonus;
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillKick: {
|
||||
float skill_bonus = skill_level / 10.0f;
|
||||
@ -5185,7 +5185,7 @@ int Bot::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
ac_bonus = inst->GetItemArmorClass(true) / 25.0f;
|
||||
if (ac_bonus > skill_bonus)
|
||||
ac_bonus = skill_bonus;
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillBash: {
|
||||
float skill_bonus = skill_level / 10.0f;
|
||||
@ -5199,7 +5199,7 @@ int Bot::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
ac_bonus = inst->GetItemArmorClass(true) / 25.0f;
|
||||
if (ac_bonus > skill_bonus)
|
||||
ac_bonus = skill_bonus;
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillBackstab: {
|
||||
float skill_bonus = static_cast<float>(skill_level) * 0.02f;
|
||||
|
||||
166
zone/client.cpp
166
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);
|
||||
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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -304,6 +304,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;
|
||||
@ -10423,7 +10424,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());
|
||||
@ -10431,9 +10432,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();
|
||||
@ -10489,14 +10487,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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -10680,6 +10678,22 @@ 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();
|
||||
current_merc->SetOwnerID(0);
|
||||
SetMercID(0);
|
||||
}
|
||||
|
||||
// Select a free slot for the new hire
|
||||
int free_slot = GetFirstFreeMercSlot();
|
||||
if (free_slot < 0) {
|
||||
SendMercResponsePackets(6);
|
||||
return;
|
||||
}
|
||||
SetMercSlot(static_cast<uint8>(free_slot));
|
||||
|
||||
// Set time remaining to max on Hire
|
||||
GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS);
|
||||
|
||||
@ -10699,6 +10713,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
|
||||
{
|
||||
@ -10735,6 +10753,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<uint8>(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.
|
||||
@ -12239,6 +12339,9 @@ void Client::Handle_OP_QueryUCSServerStatus(const EQApplicationPacket *app)
|
||||
case EQ::versions::ClientVersion::RoF2:
|
||||
ConnectionType = EQ::versions::ucsRoF2Combined;
|
||||
break;
|
||||
case EQ::versions::ClientVersion::TOB:
|
||||
ConnectionType = EQ::versions::ucsTOBCombined;
|
||||
break;
|
||||
default:
|
||||
ConnectionType = EQ::versions::ucsUnknown;
|
||||
break;
|
||||
|
||||
@ -237,6 +237,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);
|
||||
|
||||
105
zone/merc.cpp
105
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<uint8>(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<uint8>(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);
|
||||
|
||||
@ -33,7 +33,7 @@ namespace EQ
|
||||
struct ItemData;
|
||||
}
|
||||
|
||||
#define MAXMERCS 1
|
||||
constexpr int MAXMERCS = 11;
|
||||
#define TANK 1
|
||||
#define HEALER 2
|
||||
#define MELEEDPS 9
|
||||
|
||||
@ -102,10 +102,10 @@ int Mob::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
}
|
||||
|
||||
if (RuleB(Character, ItemExtraSkillDamageCalcAsPercent) && GetSkillDmgAmt(skill) > 0) {
|
||||
return static_cast<int>(ac_bonus + skill_bonus) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
return (base + static_cast<int>(ac_bonus + skill_bonus)) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
}
|
||||
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillKick:
|
||||
case EQ::skills::SkillRoundKick: {
|
||||
@ -128,10 +128,10 @@ int Mob::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
}
|
||||
|
||||
if (RuleB(Character, ItemExtraSkillDamageCalcAsPercent) && GetSkillDmgAmt(skill) > 0) {
|
||||
return static_cast<int>(ac_bonus + skill_bonus) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
return (base + static_cast<int>(ac_bonus + skill_bonus)) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
}
|
||||
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillBash: {
|
||||
float skill_bonus = skill_level / 10.0f;
|
||||
@ -160,10 +160,10 @@ int Mob::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target)
|
||||
}
|
||||
|
||||
if (RuleB(Character, ItemExtraSkillDamageCalcAsPercent) && GetSkillDmgAmt(skill) > 0) {
|
||||
return static_cast<int>(ac_bonus + skill_bonus) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
return (base + static_cast<int>(ac_bonus + skill_bonus)) * std::abs(GetSkillDmgAmt(skill) / 100);
|
||||
}
|
||||
|
||||
return static_cast<int>(ac_bonus + skill_bonus);
|
||||
return base + static_cast<int>(ac_bonus + skill_bonus);
|
||||
}
|
||||
case EQ::skills::SkillBackstab: {
|
||||
float skill_bonus = static_cast<float>(skill_level) * 0.02f;
|
||||
|
||||
@ -4394,9 +4394,9 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses)
|
||||
case SpellEffect::Familiar:
|
||||
{
|
||||
Mob *mypet = GetPet();
|
||||
if (mypet){
|
||||
if(mypet->IsNPC())
|
||||
mypet->CastToNPC()->Depop();
|
||||
if (mypet && mypet->IsNPC() &&
|
||||
mypet->CastToNPC()->GetPetSpellID() == buffs[slot].spellid) {
|
||||
mypet->CastToNPC()->Depop();
|
||||
SetPetID(0);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -94,6 +94,9 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
#include "common/links.h"
|
||||
#include "common/packet_dump.h"
|
||||
|
||||
extern Zone *zone;
|
||||
extern volatile bool is_zone_loaded;
|
||||
extern WorldServer worldserver;
|
||||
@ -319,6 +322,10 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot,
|
||||
// note that CheckFizzle itself doesn't let NPCs fizzle,
|
||||
// but this code allows for it.
|
||||
if (slot < CastingSlot::MaxGems && !CheckFizzle(spell_id)) {
|
||||
/*
|
||||
MessageFormat: You miss a note, bringing your song to a close! (TOB: You miss a note, bringing your %1 to a close!)
|
||||
MessageFormat: Your spell fizzles! (TOB: Your %1 spell fizzles!)
|
||||
*/
|
||||
int fizzle_msg = IsBardSong(spell_id) ? MISS_NOTE : SPELL_FIZZLE;
|
||||
|
||||
uint32 use_mana = ((spells[spell_id].mana) / 4);
|
||||
@ -328,10 +335,16 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot,
|
||||
Mob::SetMana(GetMana() - use_mana); // We send StopCasting which will update mana
|
||||
StopCasting();
|
||||
|
||||
MessageString(Chat::SpellFailure, fizzle_msg);
|
||||
// TODO: can handle spell name overrides here
|
||||
std::string spell_name(GetSpellName(spell_id));
|
||||
std::string spell_link = Links::FormatSpellLink(spell_id, spell_name);
|
||||
|
||||
// pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches
|
||||
MessageString(Chat::SpellFailure, fizzle_msg, spell_link.c_str());
|
||||
|
||||
/**
|
||||
* Song Failure message
|
||||
* pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches
|
||||
*/
|
||||
entity_list.FilteredMessageCloseString(
|
||||
this,
|
||||
@ -342,11 +355,11 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot,
|
||||
(fizzle_msg == MISS_NOTE ? MISSED_NOTE_OTHER : SPELL_FIZZLE_OTHER),
|
||||
0,
|
||||
/*
|
||||
MessageFormat: You miss a note, bringing your song to a close! (if missed note)
|
||||
MessageFormat: A missed note brings %1's song to a close!
|
||||
MessageFormat: %1's spell fizzles!
|
||||
MessageFormat: A missed note brings %1's song to a close! (TOB: A missed note brings %1's %2 to a close!)
|
||||
MessageFormat: %1's spell fizzles! (TOB: %1's %2 spell fizzles!)
|
||||
*/
|
||||
GetName()
|
||||
GetName(),
|
||||
spell_link.c_str()
|
||||
);
|
||||
|
||||
TryTriggerOnCastRequirement();
|
||||
@ -1299,14 +1312,20 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid)
|
||||
if(!message)
|
||||
message = IsBardSong(spellid) ? SONG_ENDS_ABRUPTLY : INTERRUPT_SPELL;
|
||||
|
||||
// TODO: can handle spell name overrides here
|
||||
std::string spellname(GetSpellName(spellid));
|
||||
std::string spelllink = Links::FormatSpellLink(spellid, spellname);
|
||||
|
||||
// clients need some packets
|
||||
if (IsClient() && message != SONG_ENDS)
|
||||
{
|
||||
// the interrupt message
|
||||
outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct));
|
||||
outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + spelllink.size() + 1);
|
||||
InterruptCast_Struct* ic = (InterruptCast_Struct*) outapp->pBuffer;
|
||||
ic->messageid = message;
|
||||
ic->spawnid = GetID();
|
||||
// pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches
|
||||
fmt::format_to_n(ic->message, spelllink.size(), "{}", spelllink);
|
||||
outapp->priority = 5;
|
||||
CastToClient()->QueuePacket(outapp);
|
||||
safe_delete(outapp);
|
||||
@ -1336,11 +1355,12 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid)
|
||||
}
|
||||
|
||||
// this is the actual message, it works the same as a formatted message
|
||||
outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(GetCleanName()) + 1);
|
||||
outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(GetCleanName()) + spelllink.size() + 2);
|
||||
InterruptCast_Struct* ic = (InterruptCast_Struct*) outapp->pBuffer;
|
||||
ic->messageid = message_other;
|
||||
ic->spawnid = GetID();
|
||||
strcpy(ic->message, GetCleanName());
|
||||
// pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches
|
||||
fmt::format_to_n(ic->message, sizeof(GetCleanName()) + spelllink.size() + 1, "{}\x00{}", GetCleanName(), spelllink);
|
||||
entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, SongMessages), 0, true, IsClient() ? FilterPCSpells : FilterNPCSpells);
|
||||
safe_delete(outapp);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user