diff --git a/common/database_instances.cpp b/common/database_instances.cpp index 372c6751f..2cf3923fa 100644 --- a/common/database_instances.cpp +++ b/common/database_instances.cpp @@ -493,6 +493,9 @@ void Database::DeleteInstance(uint16 instance_id) query = StringFormat("DELETE FROM spawn_condition_values WHERE instance_id=%u", instance_id); QueryDatabase(query); + query = fmt::format("DELETE FROM dynamic_zones WHERE instance_id={}", instance_id); + QueryDatabase(query); + BuryCorpsesInInstance(instance_id); } @@ -582,8 +585,7 @@ void Database::PurgeExpiredInstances() QueryDatabase(fmt::format("DELETE FROM respawn_times WHERE instance_id IN ({})", imploded_instance_ids)); QueryDatabase(fmt::format("DELETE FROM spawn_condition_values WHERE instance_id IN ({})", imploded_instance_ids)); QueryDatabase(fmt::format("UPDATE character_corpses SET is_buried = 1, instance_id = 0 WHERE instance_id IN ({})", imploded_instance_ids)); - - + QueryDatabase(fmt::format("DELETE FROM dynamic_zones WHERE instance_id IN ({})", imploded_instance_ids)); } void Database::SetInstanceDuration(uint16 instance_id, uint32 new_duration) diff --git a/common/database_schema.h b/common/database_schema.h index 0b6536d96..95a550d69 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -50,6 +50,7 @@ namespace DatabaseSchema { {"character_data", "id"}, {"character_disciplines", "id"}, {"character_enabledtasks", "charid"}, + {"character_expedition_lockouts", "character_id"}, {"character_inspect_messages", "id"}, {"character_item_recast", "id"}, {"character_languages", "id"}, @@ -114,6 +115,7 @@ namespace DatabaseSchema { "character_data", "character_disciplines", "character_enabledtasks", + "character_expedition_lockouts", "character_inspect_messages", "character_item_recast", "character_languages", @@ -305,7 +307,11 @@ namespace DatabaseSchema { "banned_ips", "bug_reports", "bugs", + "dynamic_zones", "eventlog", + "expedition_lockouts", + "expedition_members", + "expeditions", "gm_ips", "group_id", "group_leaders", diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 5078cca68..46fd564e6 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -136,20 +136,22 @@ N(OP_Dye), N(OP_DynamicWall), N(OP_DzAddPlayer), N(OP_DzChooseZone), +N(OP_DzChooseZoneReply), N(OP_DzCompass), N(OP_DzExpeditionEndsWarning), N(OP_DzExpeditionInfo), -N(OP_DzExpeditionList), -N(OP_DzJoinExpeditionConfirm), -N(OP_DzJoinExpeditionReply), -N(OP_DzLeaderStatus), +N(OP_DzExpeditionInvite), +N(OP_DzExpeditionInviteResponse), +N(OP_DzExpeditionLockoutTimers), N(OP_DzListTimers), N(OP_DzMakeLeader), N(OP_DzMemberList), -N(OP_DzMemberStatus), +N(OP_DzMemberListName), +N(OP_DzMemberListStatus), N(OP_DzPlayerList), N(OP_DzQuit), N(OP_DzRemovePlayer), +N(OP_DzSetLeaderName), N(OP_DzSwapPlayer), N(OP_Emote), N(OP_EndLootRequest), @@ -271,6 +273,7 @@ N(OP_ItemVerifyRequest), N(OP_ItemViewUnknown), N(OP_Jump), N(OP_KeyRing), +N(OP_KickPlayers), N(OP_KnowledgeBase), N(OP_LDoNButton), N(OP_LDoNDisarmTraps), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index c22069196..57d4c0795 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -4835,17 +4835,98 @@ struct BuffIcon_Struct BuffIconEntry_Struct entries[0]; }; -struct ExpeditionInfo_Struct +struct ExpeditionInvite_Struct { -/*000*/ uint32 max_players; -/*004*/ char expedition_name[128]; -/*132*/ char leader_name[64]; +/*000*/ uint32 client_id; // unique character id +/*004*/ uint32 unknown004; // added after titanium +/*008*/ char inviter_name[64]; +/*072*/ char expedition_name[128]; +/*200*/ uint8 swapping; // 0: adding 1: swapping +/*201*/ char swap_name[64]; // if swapping, swap name being removed +/*265*/ uint8 padding[3]; +/*268*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*270*/ uint16 dz_instance_id; }; -struct ExpeditionJoinPrompt_Struct +struct ExpeditionInviteResponse_Struct { -/*000*/ char player_name[64]; -/*064*/ char expedition_name[64]; +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; // added after titanium +/*008*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*010*/ uint16 dz_instance_id; +/*012*/ uint8 accepted; // 0: declined 1: accepted +/*013*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*014*/ char swap_name[64]; // swap name sent in invite +/*078*/ uint8 unknown078; // padding garbage? +/*079*/ uint8 unknown079; // padding garbage? +}; + +struct ExpeditionInfo_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; // added after titanium +/*008*/ uint32 assigned; // padded bool, 0: not in expedition (clear data), 1: in expedition +/*012*/ uint32 max_players; +/*016*/ char expedition_name[128]; +/*144*/ char leader_name[64]; +}; + +struct ExpeditionMemberEntry_Struct +{ +/*000*/ char name[64]; // variable length, null terminated, max 0x40 (64) +/*064*/ uint8 expedition_status; // 0: unknown, 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*012*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[128]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[256]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char add_player_name[64]; // swap to (player must confirm) +/*072*/ char rem_player_name[64]; // swap from }; struct ExpeditionExpireWarning @@ -4853,48 +4934,67 @@ struct ExpeditionExpireWarning /*008*/ uint32 minutes_remaining; }; -struct ExpeditionCompassEntry_Struct +struct DynamicZoneCompassEntry_Struct { -/*000*/ uint32 enabled; //guess -/*004*/ float y; -/*008*/ float x; -/*012*/ float z; +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; +/*012*/ float y; +/*016*/ float x; +/*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { +/*000*/ uint32 client_id; /*000*/ uint32 count; -/*004*/ ExpeditionCompassEntry_Struct entries[0]; +/*004*/ DynamicZoneCompassEntry_Struct entries[0]; }; -struct ExpeditionMemberEntry_Struct +struct DynamicZoneChooseZoneEntry_Struct { - char name[64]; - char status; +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // seen 28 00 00 00 (40), sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[128]; // variable length, null terminated +/*144*/ char leader_name[64]; // variable length, null terminated }; -struct ExpeditionMemberList_Struct +struct DynamicZoneChooseZone_Struct { -/*000*/ uint32 count; -/*004*/ ExpeditionMemberEntry_Struct entries[0]; +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; }; -struct ExpeditionLockoutEntry_Struct +struct DynamicZoneChooseZoneReply_Struct { -/*000*/ uint32 time_left; -/*004*/ char expedition[128]; -/*132*/ char expedition_event[128]; +/*000*/ uint32 unknown000; // ff ff ff ff +/*004*/ uint32 unknown004; // seen 69 00 00 00 +/*008*/ uint32 unknown008; // ff ff ff ff +/*012*/ uint32 unknown_id1; // from choose zone entry message +/*016*/ uint16 dz_zone_id; // dz_id pair +/*018*/ uint16 dz_instance_id; +/*020*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*024*/ uint32 unknown_id2; // from choose zone entry message +/*028*/ uint32 unknown028; // 00 00 00 00 +/*032*/ uint32 unknown032; // always same as unknown044 +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; +/*044*/ uint32 unknown044; // always same as unknown032 +/*048*/ uint32 unknown048; // seen 01 00 00 00 and 02 00 00 00 }; -struct ExpeditionLockoutList_Struct +struct KickPlayers_Struct { -/*000*/ uint32 count; -/*004*/ ExpeditionLockoutEntry_Struct entries[0]; -}; - -struct ExpeditionLeaderSet_Struct -{ -/*000*/ char leader_name[64]; +/*000*/ char char_name[64]; +/*064*/ uint32 unknown064; // always 0 +/*068*/ uint8 kick_expedition; // true if /kickplayers exp +/*069*/ uint8 kick_task; // true if /kickplayers task +/*070*/ uint8 padding[2]; }; struct CorpseDrag_Struct diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 80cf9851f..d28478a97 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -118,6 +118,8 @@ namespace Logs { Merchants, ZonePoints, Loot, + Expeditions, + DynamicZones, MaxCategoryID /* Don't Remove this */ }; @@ -194,7 +196,9 @@ namespace Logs { "HotReload", "Merchants", "ZonePoints", - "Loot" + "Loot", + "Expeditions", + "DynamicZones", }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 514f37e21..efe25dabe 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -601,6 +601,31 @@ OutF(LogSys, Logs::Detail, Logs::Loot, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogExpeditions(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogExpeditionsModerate(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::Moderate, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogExpeditionsDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogDynamicZones(message, ...) do {\ + if (LogSys.log_settings[Logs::DynamicZones].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::DynamicZones, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogDynamicZonesDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::DynamicZones].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::DynamicZones, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ @@ -952,6 +977,21 @@ #define LogZonePointsDetail(message, ...) do {\ } while (0) +#define LogExpeditions(message, ...) do {\ +} while (0) + +#define LogExpeditionsModerate(message, ...) do {\ +} while (0) + +#define LogExpeditionsDetail(message, ...) do {\ +} while (0) + +#define LogDynamicZones(message, ...) do {\ +} while (0) + +#define LogDynamicZonesDetail(message, ...) do {\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ } while (0) diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index 8811c8746..cb5400f1f 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -710,15 +710,48 @@ namespace RoF FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -742,81 +775,60 @@ namespace RoF ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->unknown004 = 785316192; - eq->unknown008 = 435601; - strncpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); - strncpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strncpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); - strncpy(eq->player_name, emu->player_name, sizeof(eq->player_name)); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -825,26 +837,43 @@ namespace RoF { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -4388,6 +4417,84 @@ namespace RoF FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + IN(unknown000); + IN(unknown004); + IN(unknown008); + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + IN(unknown028); + IN(unknown032); + IN(unknown036); + IN(unknown040); + IN(unknown044); + IN(unknown048); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 09a2d502d..4a902061d 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -759,15 +759,48 @@ namespace RoF2 FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -791,81 +824,60 @@ namespace RoF2 ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->unknown004 = 785316192; - eq->unknown008 = 435601; - strncpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); - strncpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strncpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); - strncpy(eq->player_name, emu->player_name, sizeof(eq->player_name)); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -874,26 +886,43 @@ namespace RoF2 { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -4585,6 +4614,84 @@ namespace RoF2 FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + IN(unknown000); + IN(unknown004); + IN(unknown008); + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + IN(unknown028); + IN(unknown032); + IN(unknown036); + IN(unknown040); + IN(unknown044); + IN(unknown048); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/rof2_ops.h b/common/patches/rof2_ops.h index 32798fef5..22b6e6662 100644 --- a/common/patches/rof2_ops.h +++ b/common/patches/rof2_ops.h @@ -58,13 +58,16 @@ E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DeleteSpawn) E(OP_DisciplineUpdate) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_ExpansionInfo) E(OP_FormattedMessage) @@ -159,6 +162,12 @@ D(OP_ConsiderCorpse) D(OP_Consume) D(OP_Damage) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_EnvDamage) D(OP_FaceChange) diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 96e74187d..c5111da6d 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -4882,52 +4882,169 @@ struct VeteranClaim /*076*/ uint32 action; }; -struct ExpeditionEntryHeader_Struct +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; // unique character id +/*004*/ uint32 unknown004; +/*008*/ char inviter_name[64]; +/*072*/ char expedition_name[128]; +/*200*/ uint8 swapping; // 0: adding 1: swapping +/*201*/ char swap_name[64]; // if swapping, swap name being removed +/*265*/ uint8 padding[3]; +/*268*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*270*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*000*/ uint32 number_of_entries; -}; - -struct ExpeditionJoinPrompt_Struct -{ -/*000*/ uint32 clientid; /*004*/ uint32 unknown004; -/*008*/ char player_name[64]; -/*072*/ char expedition_name[64]; -}; - -struct ExpeditionExpireWarning -{ -/*000*/ uint32 clientid; -/*004*/ uint32 unknown004; -/*008*/ uint32 minutes_remaining; +/*008*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*010*/ uint16 dz_instance_id; +/*012*/ uint8 accepted; // 0: declined 1: accepted +/*013*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*014*/ char swap_name[64]; // swap name sent in invite +/*078*/ uint8 unknown078; // padding garbage? +/*079*/ uint8 unknown079; // padding garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 unknown004; -/*008*/ uint32 unknown008; +/*008*/ uint32 assigned; // padded bool, 0: not in expedition (clear data), 1: in expedition /*012*/ uint32 max_players; -/*016*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*016*/ char expedition_name[128]; +/*144*/ char leader_name[64]; +//*208*/ uint32 unknown208; // live sends 01 00 00 00 here but client doesn't read it }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; // number of players in window +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*012*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char add_player_name[64]; // swap to (player must confirm) +/*072*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; +}; + +struct DynamicZoneChooseZoneEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // seen 28 00 00 00 (40), sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; // ff ff ff ff +/*004*/ uint32 unknown004; // seen 69 00 00 00 +/*008*/ uint32 unknown008; // ff ff ff ff +/*012*/ uint32 unknown_id1; // from choose zone entry message +/*016*/ uint16 dz_zone_id; // dz_id pair +/*018*/ uint16 dz_instance_id; +/*020*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*024*/ uint32 unknown_id2; // from choose zone entry message +/*028*/ uint32 unknown028; // 00 00 00 00 +/*032*/ uint32 unknown032; // always same as unknown044 +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; +/*044*/ uint32 unknown044; // always same as unknown032 +/*048*/ uint32 unknown048; // seen 01 00 00 00 and 02 00 00 00 +}; + +struct KickPlayers_Struct +{ +/*000*/ char char_name[64]; +/*064*/ uint32 unknown064; // always 0 +/*068*/ uint8 kick_expedition; // true if /kickplayers exp +/*069*/ uint8 kick_task; // true if /kickplayers task +/*070*/ uint8 padding[2]; }; struct MaxCharacters_Struct diff --git a/common/patches/rof_ops.h b/common/patches/rof_ops.h index 11fd83f51..f06834d21 100644 --- a/common/patches/rof_ops.h +++ b/common/patches/rof_ops.h @@ -44,13 +44,16 @@ E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DeleteSpawn) E(OP_DisciplineUpdate) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_ExpansionInfo) E(OP_FormattedMessage) @@ -145,6 +148,12 @@ D(OP_ConsiderCorpse) D(OP_Consume) D(OP_Damage) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_EnvDamage) D(OP_FaceChange) diff --git a/common/patches/rof_structs.h b/common/patches/rof_structs.h index c22278aac..a67e7b972 100644 --- a/common/patches/rof_structs.h +++ b/common/patches/rof_structs.h @@ -4815,52 +4815,159 @@ struct VeteranClaim /*076*/ uint32 action; }; -struct ExpeditionEntryHeader_Struct +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; // unique character id +/*004*/ uint32 unknown004; +/*008*/ char inviter_name[64]; +/*072*/ char expedition_name[128]; +/*200*/ uint8 swapping; // 0: adding 1: swapping +/*201*/ char swap_name[64]; // if swapping, swap name being removed +/*265*/ uint8 padding[3]; +/*268*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*270*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*000*/ uint32 number_of_entries; -}; - -struct ExpeditionJoinPrompt_Struct -{ -/*000*/ uint32 clientid; /*004*/ uint32 unknown004; -/*008*/ char player_name[64]; -/*072*/ char expedition_name[64]; -}; - -struct ExpeditionExpireWarning -{ -/*000*/ uint32 clientid; -/*004*/ uint32 unknown004; -/*008*/ uint32 minutes_remaining; +/*008*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*010*/ uint16 dz_instance_id; +/*012*/ uint8 accepted; // 0: declined 1: accepted +/*013*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*014*/ char swap_name[64]; // swap name sent in invite +/*078*/ uint8 unknown078; // padding garbage? +/*079*/ uint8 unknown079; // padding garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 unknown004; -/*008*/ uint32 unknown008; +/*008*/ uint32 assigned; // padded bool /*012*/ uint32 max_players; -/*016*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*016*/ char expedition_name[128]; +/*144*/ char leader_name[64]; }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; // number of players in window +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*012*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char add_player_name[64]; // swap to (player must confirm) +/*072*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; +}; + +struct DynamicZoneChooseZoneEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; // ff ff ff ff +/*004*/ uint32 unknown004; // seen 69 00 00 00 +/*008*/ uint32 unknown008; // ff ff ff ff +/*012*/ uint32 unknown_id1; // from choose zone entry message +/*016*/ uint16 dz_zone_id; // dz_id pair +/*018*/ uint16 dz_instance_id; +/*020*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*024*/ uint32 unknown_id2; // from choose zone entry message +/*028*/ uint32 unknown028; // 00 00 00 00 +/*032*/ uint32 unknown032; // always same as unknown044 +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; +/*044*/ uint32 unknown044; // always same as unknown032 +/*048*/ uint32 unknown048; // seen 01 00 00 00 and 02 00 00 00 }; struct MaxCharacters_Struct diff --git a/common/patches/sod.cpp b/common/patches/sod.cpp index 45f2fe20e..d6eae5838 100644 --- a/common/patches/sod.cpp +++ b/common/patches/sod.cpp @@ -483,15 +483,48 @@ namespace SoD FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -515,81 +548,60 @@ namespace SoD ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->unknown004 = 785316192; - eq->unknown008 = 435601; - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->leader_name, emu->leader_name); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->player_name, emu->player_name); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -598,26 +610,44 @@ namespace SoD { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -2974,6 +3004,84 @@ namespace SoD FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + IN(unknown000); + IN(unknown004); + IN(unknown008); + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + IN(unknown028); + IN(unknown032); + IN(unknown036); + IN(unknown040); + IN(unknown044); + IN(unknown048); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/sod_ops.h b/common/patches/sod_ops.h index 6909e1f28..5216a77ce 100644 --- a/common/patches/sod_ops.h +++ b/common/patches/sod_ops.h @@ -35,13 +35,16 @@ E(OP_Consider) E(OP_Damage) E(OP_DeleteCharge) E(OP_DeleteItem) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_ExpansionInfo) E(OP_FormattedMessage) @@ -111,6 +114,12 @@ D(OP_Consider) D(OP_ConsiderCorpse) D(OP_Consume) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_FaceChange) D(OP_FindPersonRequest) diff --git a/common/patches/sod_structs.h b/common/patches/sod_structs.h index 7a2ae5710..4e7735bf1 100644 --- a/common/patches/sod_structs.h +++ b/common/patches/sod_structs.h @@ -4169,52 +4169,160 @@ struct VeteranReward /*012*/ VeteranRewardItem items[8]; }; -struct ExpeditionEntryHeader_Struct + +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; // unique character id +/*004*/ uint32 unknown004; +/*008*/ char inviter_name[64]; +/*072*/ char expedition_name[128]; +/*200*/ uint8 swapping; // 0: adding 1: swapping +/*201*/ char swap_name[64]; // if swapping, swap name being removed +/*265*/ uint8 padding[3]; +/*268*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*270*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*000*/ uint32 number_of_entries; -}; - -struct ExpeditionJoinPrompt_Struct -{ -/*000*/ uint32 clientid; /*004*/ uint32 unknown004; -/*008*/ char player_name[64]; -/*072*/ char expedition_name[64]; -}; - -struct ExpeditionExpireWarning -{ -/*000*/ uint32 clientid; -/*004*/ uint32 unknown004; -/*008*/ uint32 minutes_remaining; +/*008*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*010*/ uint16 dz_instance_id; +/*012*/ uint8 accepted; // 0: declined 1: accepted +/*013*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*014*/ char swap_name[64]; // swap name sent in invite +/*078*/ uint8 unknown078; // padding garbage? +/*079*/ uint8 unknown079; // padding garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 unknown004; -/*008*/ uint32 unknown008; +/*008*/ uint32 assigned; // padded bool /*012*/ uint32 max_players; -/*016*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*016*/ char expedition_name[128]; +/*144*/ char leader_name[64]; }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; // number of players in window +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*012*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char add_player_name[64]; // swap to (player must confirm) +/*072*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; +}; + +struct DynamicZoneChooseZoneEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; // ff ff ff ff +/*004*/ uint32 unknown004; // seen 69 00 00 00 +/*008*/ uint32 unknown008; // ff ff ff ff +/*012*/ uint32 unknown_id1; // from choose zone entry message +/*016*/ uint16 dz_zone_id; // dz_id pair +/*018*/ uint16 dz_instance_id; +/*020*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*024*/ uint32 unknown_id2; // from choose zone entry message +/*028*/ uint32 unknown028; // 00 00 00 00 +/*032*/ uint32 unknown032; // always same as unknown044 +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; +/*044*/ uint32 unknown044; // always same as unknown032 +/*048*/ uint32 unknown048; // seen 01 00 00 00 and 02 00 00 00 }; struct AltCurrencySelectItem_Struct { diff --git a/common/patches/sof.cpp b/common/patches/sof.cpp index f8ccf0274..f24f265ff 100644 --- a/common/patches/sof.cpp +++ b/common/patches/sof.cpp @@ -471,14 +471,48 @@ namespace SoF FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -502,80 +536,60 @@ namespace SoF ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->enabled_max = 1; - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->leader_name, emu->leader_name); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (int i = 0; i < emu->count; ++i) + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->player_name, emu->player_name); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - //ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -584,26 +598,43 @@ namespace SoF { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -2435,6 +2466,83 @@ namespace SoF FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + emu->unknown000 = eq->unknown000; + emu->unknown008 = eq->unknown004; + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + emu->unknown028 = eq->unknown024; + emu->unknown032 = eq->unknown028; + emu->unknown036 = eq->unknown032; + emu->unknown040 = eq->unknown036; + emu->unknown044 = eq->unknown040; + emu->unknown048 = eq->unknown044; + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/sof_ops.h b/common/patches/sof_ops.h index 0ff2cfd2f..653bb5979 100644 --- a/common/patches/sof_ops.h +++ b/common/patches/sof_ops.h @@ -36,13 +36,16 @@ E(OP_Damage) E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DeleteSpawn) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_ExpansionInfo) E(OP_FormattedMessage) @@ -103,6 +106,12 @@ D(OP_Consider) D(OP_ConsiderCorpse) D(OP_Consume) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_FaceChange) D(OP_FindPersonRequest) diff --git a/common/patches/sof_structs.h b/common/patches/sof_structs.h index b9716d016..bf88bf5c2 100644 --- a/common/patches/sof_structs.h +++ b/common/patches/sof_structs.h @@ -4088,43 +4088,150 @@ struct VeteranReward /*012*/ VeteranRewardItem items[8]; }; -struct ExpeditionExpireWarning +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; +/*004*/ char inviter_name[64]; +/*068*/ char expedition_name[128]; +/*196*/ uint8 swapping; // 0: adding 1: swapping +/*197*/ char swap_name[64]; // if swapping, swap name being removed +/*261*/ uint8 padding[3]; +/*264*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*268*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*004*/ uint32 minutes_remaining; +/*004*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*006*/ uint16 dz_instance_id; +/*008*/ uint8 accepted; // 0: declined 1: accepted +/*009*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*010*/ char swap_name[64]; // swap name sent in invite +/*074*/ uint8 unknown078; // padding/garbage? +/*075*/ uint8 unknown079; // padding/garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; -/*004*/ uint32 enabled_max; +/*000*/ uint32 client_id; +/*004*/ uint32 assigned; // padded bool /*008*/ uint32 max_players; -/*012*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*012*/ char expedition_name[128]; +/*140*/ char leader_name[64]; }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*008*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ char add_player_name[64]; // swap to (player must confirm) +/*068*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; }; -struct ExpeditionJoinPrompt_Struct +struct DynamicZoneChooseZoneEntry_Struct { -/*000*/ uint32 clientid; -/*004*/ char player_name[64]; -/*068*/ char expedition_name[64]; +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ uint32 unknown_id1; +/*012*/ uint16 dz_zone_id; +/*014*/ uint16 dz_instance_id; +/*016*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*020*/ uint32 unknown_id2; +/*024*/ uint32 unknown024; +/*028*/ uint32 unknown028; // always same as unknown040 +/*032*/ uint32 unknown032; +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; // always same as unknown028 +/*044*/ uint32 unknown044; }; struct AltCurrencySelectItem_Struct { diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index 505bca10c..f7d97b8d9 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -414,15 +414,48 @@ namespace Titanium FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -446,80 +479,60 @@ namespace Titanium ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->enabled_max = 1; - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->leader_name, emu->leader_name); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->player_name, emu->player_name); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - //ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -528,26 +541,43 @@ namespace Titanium { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -1943,6 +1973,83 @@ namespace Titanium FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + emu->unknown000 = eq->unknown000; + emu->unknown008 = eq->unknown004; + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + emu->unknown028 = eq->unknown024; + emu->unknown032 = eq->unknown028; + emu->unknown036 = eq->unknown032; + emu->unknown040 = eq->unknown036; + emu->unknown044 = eq->unknown040; + emu->unknown048 = eq->unknown044; + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/titanium_ops.h b/common/patches/titanium_ops.h index d3c8c1dde..961c850fe 100644 --- a/common/patches/titanium_ops.h +++ b/common/patches/titanium_ops.h @@ -32,13 +32,16 @@ E(OP_Damage) E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DeleteSpawn) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_FormattedMessage) E(OP_GroundSpawn) @@ -86,6 +89,12 @@ D(OP_CharacterCreate) D(OP_ClientUpdate) D(OP_Consume) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_FaceChange) D(OP_InspectAnswer) diff --git a/common/patches/titanium_structs.h b/common/patches/titanium_structs.h index f620089d6..b2beae0cd 100644 --- a/common/patches/titanium_structs.h +++ b/common/patches/titanium_structs.h @@ -3299,43 +3299,150 @@ struct VeteranReward /*004*/ VeteranRewardItem item; }; -struct ExpeditionExpireWarning +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; +/*004*/ char inviter_name[64]; +/*068*/ char expedition_name[128]; +/*196*/ uint8 swapping; // 0: adding 1: swapping +/*197*/ char swap_name[64]; // if swapping, swap name being removed +/*261*/ uint8 padding[3]; +/*264*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*268*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*004*/ uint32 minutes_remaining; +/*004*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*006*/ uint16 dz_instance_id; +/*008*/ uint8 accepted; // 0: declined 1: accepted +/*009*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*010*/ char swap_name[64]; // swap name sent in invite +/*074*/ uint8 unknown078; // padding/garbage? +/*075*/ uint8 unknown079; // padding/garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; -/*004*/ uint32 enabled_max; +/*000*/ uint32 client_id; +/*004*/ uint32 assigned; // padded bool /*008*/ uint32 max_players; -/*012*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*012*/ char expedition_name[128]; +/*140*/ char leader_name[64]; }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*008*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ char add_player_name[64]; // swap to (player must confirm) +/*068*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; }; -struct ExpeditionJoinPrompt_Struct +struct DynamicZoneChooseZoneEntry_Struct { -/*000*/ uint32 clientid; -/*004*/ char player_name[64]; -/*068*/ char expedition_name[64]; +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ uint32 unknown_id1; +/*012*/ uint16 dz_zone_id; +/*014*/ uint16 dz_instance_id; +/*016*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*020*/ uint32 unknown_id2; +/*024*/ uint32 unknown024; +/*028*/ uint32 unknown028; // always same as unknown040 +/*032*/ uint32 unknown032; +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; // always same as unknown028 +/*044*/ uint32 unknown044; }; struct LFGuild_SearchPlayer_Struct diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 9dfc653b4..47dafbbf3 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -613,14 +613,48 @@ namespace UF FINISH_ENCODE(); } + ENCODE(OP_DzChooseZone) + { + SETUP_VAR_ENCODE(DynamicZoneChooseZone_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); + + for (uint32 i = 0; i < emu->count; ++i) + { + buf.WriteUInt16(emu->choices[i].dz_zone_id); + buf.WriteUInt16(emu->choices[i].dz_instance_id); + buf.WriteUInt32(emu->choices[i].unknown_id1); + buf.WriteUInt32(emu->choices[i].dz_type); + buf.WriteUInt32(emu->choices[i].unknown_id2); + buf.WriteString(emu->choices[i].description); + buf.WriteString(emu->choices[i].leader_name); + } + + __packet->size = buf.size(); + __packet->pBuffer = new unsigned char[__packet->size]; + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); + + FINISH_ENCODE(); + } + ENCODE(OP_DzCompass) { - SETUP_VAR_ENCODE(ExpeditionCompass_Struct); - ALLOC_VAR_ENCODE(structs::ExpeditionCompass_Struct, sizeof(structs::ExpeditionInfo_Struct) + sizeof(structs::ExpeditionCompassEntry_Struct) * emu->count); + SETUP_VAR_ENCODE(DynamicZoneCompass_Struct); + ALLOC_VAR_ENCODE(structs::DynamicZoneCompass_Struct, + sizeof(structs::DynamicZoneCompass_Struct) + + sizeof(structs::DynamicZoneCompassEntry_Struct) * emu->count + ); + + OUT(client_id); OUT(count); for (uint32 i = 0; i < emu->count; ++i) { + OUT(entries[i].dz_zone_id); + OUT(entries[i].dz_instance_id); + OUT(entries[i].dz_type); OUT(entries[i].x); OUT(entries[i].y); OUT(entries[i].z); @@ -644,81 +678,60 @@ namespace UF ENCODE_LENGTH_EXACT(ExpeditionInfo_Struct); SETUP_DIRECT_ENCODE(ExpeditionInfo_Struct, structs::ExpeditionInfo_Struct); + OUT(client_id); + OUT(assigned); OUT(max_players); - eq->unknown004 = 785316192; - eq->unknown008 = 435601; - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->leader_name, emu->leader_name); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } - ENCODE(OP_DzExpeditionList) + ENCODE(OP_DzExpeditionInvite) { - SETUP_VAR_ENCODE(ExpeditionLockoutList_Struct); + ENCODE_LENGTH_EXACT(ExpeditionInvite_Struct); + SETUP_DIRECT_ENCODE(ExpeditionInvite_Struct, structs::ExpeditionInvite_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); + OUT(client_id); + strn0cpy(eq->inviter_name, emu->inviter_name, sizeof(eq->inviter_name)); + strn0cpy(eq->expedition_name, emu->expedition_name, sizeof(eq->expedition_name)); + OUT(swapping); + strn0cpy(eq->swap_name, emu->swap_name, sizeof(eq->swap_name)); + OUT(dz_zone_id); + OUT(dz_instance_id); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzExpeditionLockoutTimers) + { + SETUP_VAR_ENCODE(ExpeditionLockoutTimers_Struct); + + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->count); for (uint32 i = 0; i < emu->count; ++i) { - ss.write(emu->entries[i].expedition, strlen(emu->entries[i].expedition)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].time_left, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->entries[i].expedition_event, strlen(emu->entries[i].expedition_event)); - ss.write((const char*)&null_term, sizeof(char)); + buf.WriteString(emu->timers[i].expedition_name); + buf.WriteUInt32(emu->timers[i].seconds_remaining); + buf.WriteInt32(emu->timers[i].event_type); + buf.WriteString(emu->timers[i].event_name); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } - ENCODE(OP_DzJoinExpeditionConfirm) + ENCODE(OP_DzSetLeaderName) { - ENCODE_LENGTH_EXACT(ExpeditionJoinPrompt_Struct); - SETUP_DIRECT_ENCODE(ExpeditionJoinPrompt_Struct, structs::ExpeditionJoinPrompt_Struct); + ENCODE_LENGTH_EXACT(ExpeditionSetLeaderName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionSetLeaderName_Struct, structs::ExpeditionSetLeaderName_Struct); - strcpy(eq->expedition_name, emu->expedition_name); - strcpy(eq->player_name, emu->player_name); - - FINISH_ENCODE(); - } - - ENCODE(OP_DzLeaderStatus) - { - SETUP_VAR_ENCODE(ExpeditionLeaderSet_Struct); - - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - uint32 client_id = 0; - uint8 null_term = 0; - - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write(emu->leader_name, strlen(emu->leader_name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//0xffffffff - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&client_id, sizeof(uint32));//1 - ss.write((const char*)&client_id, sizeof(uint32)); - - __packet->size = ss.str().length(); - __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + OUT(client_id); + strn0cpy(eq->leader_name, emu->leader_name, sizeof(eq->leader_name)); FINISH_ENCODE(); } @@ -727,26 +740,43 @@ namespace UF { SETUP_VAR_ENCODE(ExpeditionMemberList_Struct); - std::stringstream ss(std::stringstream::in | std::stringstream::out | std::stringstream::binary); - - uint32 client_id = 0; - uint8 null_term = 0; - ss.write((const char*)&client_id, sizeof(uint32)); - ss.write((const char*)&emu->count, sizeof(uint32)); - for (uint32 i = 0; i < emu->count; ++i) + SerializeBuffer buf; + buf.WriteUInt32(emu->client_id); + buf.WriteUInt32(emu->member_count); + for (uint32 i = 0; i < emu->member_count; ++i) { - ss.write(emu->entries[i].name, strlen(emu->entries[i].name)); - ss.write((const char*)&null_term, sizeof(char)); - ss.write((const char*)&emu->entries[i].status, sizeof(char)); + buf.WriteString(emu->members[i].name); + buf.WriteUInt8(emu->members[i].expedition_status); } - __packet->size = ss.str().length(); + __packet->size = buf.size(); __packet->pBuffer = new unsigned char[__packet->size]; - memcpy(__packet->pBuffer, ss.str().c_str(), __packet->size); + memcpy(__packet->pBuffer, buf.buffer(), __packet->size); FINISH_ENCODE(); } + ENCODE(OP_DzMemberListName) + { + ENCODE_LENGTH_EXACT(ExpeditionMemberListName_Struct); + SETUP_DIRECT_ENCODE(ExpeditionMemberListName_Struct, structs::ExpeditionMemberListName_Struct); + + OUT(client_id); + OUT(add_name); + strn0cpy(eq->name, emu->name, sizeof(eq->name)); + + FINISH_ENCODE(); + } + + ENCODE(OP_DzMemberListStatus) + { + auto emu = reinterpret_cast((*p)->pBuffer); + if (emu->member_count == 1) + { + ENCODE_FORWARD(OP_DzMemberList); + } + } + ENCODE(OP_Emote) { EQApplicationPacket *in = *p; @@ -3315,6 +3345,84 @@ namespace UF FINISH_DIRECT_DECODE(); } + DECODE(OP_DzAddPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzChooseZoneReply) + { + DECODE_LENGTH_EXACT(structs::DynamicZoneChooseZoneReply_Struct); + SETUP_DIRECT_DECODE(DynamicZoneChooseZoneReply_Struct, structs::DynamicZoneChooseZoneReply_Struct); + + IN(unknown000); + IN(unknown004); + IN(unknown008); + IN(unknown_id1); + IN(dz_zone_id); + IN(dz_instance_id); + IN(dz_type); + IN(unknown_id2); + IN(unknown028); + IN(unknown032); + IN(unknown036); + IN(unknown040); + IN(unknown044); + IN(unknown048); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzExpeditionInviteResponse) + { + DECODE_LENGTH_EXACT(structs::ExpeditionInviteResponse_Struct); + SETUP_DIRECT_DECODE(ExpeditionInviteResponse_Struct, structs::ExpeditionInviteResponse_Struct); + + IN(dz_zone_id); + IN(dz_instance_id); + IN(accepted); + IN(swapping); + strn0cpy(emu->swap_name, eq->swap_name, sizeof(emu->swap_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzMakeLeader) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzRemovePlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommand_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommand_Struct, structs::ExpeditionCommand_Struct); + + strn0cpy(emu->name, eq->name, sizeof(emu->name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_DzSwapPlayer) + { + DECODE_LENGTH_EXACT(structs::ExpeditionCommandSwap_Struct); + SETUP_DIRECT_DECODE(ExpeditionCommandSwap_Struct, structs::ExpeditionCommandSwap_Struct); + + strn0cpy(emu->add_player_name, eq->add_player_name, sizeof(emu->add_player_name)); + strn0cpy(emu->rem_player_name, eq->rem_player_name, sizeof(emu->rem_player_name)); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_Emote) { unsigned char *__eq_buffer = __packet->pBuffer; diff --git a/common/patches/uf_ops.h b/common/patches/uf_ops.h index 9d7742cf0..76b7e014b 100644 --- a/common/patches/uf_ops.h +++ b/common/patches/uf_ops.h @@ -38,13 +38,16 @@ E(OP_Damage) E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DisciplineUpdate) +E(OP_DzChooseZone) E(OP_DzCompass) E(OP_DzExpeditionEndsWarning) E(OP_DzExpeditionInfo) -E(OP_DzExpeditionList) -E(OP_DzJoinExpeditionConfirm) -E(OP_DzLeaderStatus) +E(OP_DzExpeditionInvite) +E(OP_DzExpeditionLockoutTimers) E(OP_DzMemberList) +E(OP_DzMemberListName) +E(OP_DzMemberListStatus) +E(OP_DzSetLeaderName) E(OP_Emote) E(OP_ExpansionInfo) E(OP_FormattedMessage) @@ -120,6 +123,12 @@ D(OP_ConsiderCorpse) D(OP_Consume) D(OP_Damage) D(OP_DeleteItem) +D(OP_DzAddPlayer) +D(OP_DzChooseZoneReply) +D(OP_DzExpeditionInviteResponse) +D(OP_DzMakeLeader) +D(OP_DzRemovePlayer) +D(OP_DzSwapPlayer) D(OP_Emote) D(OP_EnvDamage) D(OP_FaceChange) diff --git a/common/patches/uf_structs.h b/common/patches/uf_structs.h index 895881a7a..4a4ac8a36 100644 --- a/common/patches/uf_structs.h +++ b/common/patches/uf_structs.h @@ -4250,52 +4250,160 @@ struct VeteranReward /*012*/ VeteranRewardItem items[8]; }; -struct ExpeditionEntryHeader_Struct + +struct ExpeditionInvite_Struct +{ +/*000*/ uint32 client_id; // unique character id +/*004*/ uint32 unknown004; +/*008*/ char inviter_name[64]; +/*072*/ char expedition_name[128]; +/*200*/ uint8 swapping; // 0: adding 1: swapping +/*201*/ char swap_name[64]; // if swapping, swap name being removed +/*265*/ uint8 padding[3]; +/*268*/ uint16 dz_zone_id; // dz_id zone/instance pair, sent back in reply +/*270*/ uint16 dz_instance_id; +}; + +struct ExpeditionInviteResponse_Struct { /*000*/ uint32 unknown000; -/*000*/ uint32 number_of_entries; -}; - -struct ExpeditionJoinPrompt_Struct -{ -/*000*/ uint32 clientid; /*004*/ uint32 unknown004; -/*008*/ char player_name[64]; -/*072*/ char expedition_name[64]; -}; - -struct ExpeditionExpireWarning -{ -/*000*/ uint32 clientid; -/*004*/ uint32 unknown004; -/*008*/ uint32 minutes_remaining; +/*008*/ uint16 dz_zone_id; // dz_id pair sent in invite +/*010*/ uint16 dz_instance_id; +/*012*/ uint8 accepted; // 0: declined 1: accepted +/*013*/ uint8 swapping; // 0: adding 1: swapping (sent in invite) +/*014*/ char swap_name[64]; // swap name sent in invite +/*078*/ uint8 unknown078; // padding garbage? +/*079*/ uint8 unknown079; // padding garbage? }; struct ExpeditionInfo_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 unknown004; -/*008*/ uint32 unknown008; +/*008*/ uint32 assigned; // padded bool /*012*/ uint32 max_players; -/*016*/ char expedition_name[128]; -/*142*/ char leader_name[64]; +/*016*/ char expedition_name[128]; +/*144*/ char leader_name[64]; }; -struct ExpeditionCompassEntry_Struct +struct ExpeditionMemberEntry_Struct { -/*000*/ float unknown000; //seen *((uint32*)) = 1584791871 -/*004*/ uint32 enabled; //guess -/*008*/ uint32 unknown008; //seen 1019 +/*000*/ char name[1]; // variable length, null terminated, max 0x40 (64) +/*000*/ uint8 expedition_status; // 0: unknown 1: Online, 2: Offline, 3: In Dynamic Zone, 4: Link Dead +}; + +struct ExpeditionMemberList_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 member_count; // number of players in window +/*008*/ ExpeditionMemberEntry_Struct members[0]; // variable length +}; + +struct ExpeditionMemberListName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 add_name; // padded bool, 0: remove name, 1: add name with unknown status +/*012*/ char name[64]; +}; + +struct ExpeditionLockoutTimerEntry_Struct +{ +/*000*/ char expedition_name[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ uint32 seconds_remaining; +/*000*/ int32 event_type; // seen -1 (0xffffffff) for replay timers and 1 for event timers +/*000*/ char event_name[1]; // variable length, null terminated, max 0x100 (256) +}; + +struct ExpeditionLockoutTimers_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ ExpeditionLockoutTimerEntry_Struct timers[0]; +}; + +struct ExpeditionSetLeaderName_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ char leader_name[64]; +}; + +struct ExpeditionCommand_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char name[64]; +}; + +struct ExpeditionCommandSwap_Struct +{ +/*000*/ uint32 unknown000; +/*004*/ uint32 unknown004; +/*008*/ char add_player_name[64]; // swap to (player must confirm) +/*072*/ char rem_player_name[64]; // swap from +}; + +struct ExpeditionExpireWarning +{ +/*000*/ uint32 client_id; +/*004*/ uint32 unknown004; +/*008*/ uint32 minutes_remaining; +}; + +struct DynamicZoneCompassEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // target dz id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 dz_type; // 1: Expedition, 2: Tutorial (purple), 3: Task, 4: Mission, 5: Quest (green) +/*008*/ uint32 unknown008; /*012*/ float y; /*016*/ float x; /*020*/ float z; }; -struct ExpeditionCompass_Struct +struct DynamicZoneCompass_Struct { -/*000*/ uint32 clientid; +/*000*/ uint32 client_id; /*004*/ uint32 count; -/*008*/ ExpeditionCompassEntry_Struct entries[0]; +/*008*/ DynamicZoneCompassEntry_Struct entries[0]; +}; + +struct DynamicZoneChooseZoneEntry_Struct +{ +/*000*/ uint16 dz_zone_id; // dz_id pair +/*002*/ uint16 dz_instance_id; +/*004*/ uint32 unknown_id1; // sent back in reply +/*008*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest -- sent back in reply +/*012*/ uint32 unknown_id2; // possibly an id based on dz type, for expeditions this was same as dz_id (zone|instance) but task dz was different +/*016*/ char description[1]; // variable length, null terminated, max 0x80 (128) +/*000*/ char leader_name[1]; // variable length, null terminated, max 0x40 (64) +}; + +struct DynamicZoneChooseZone_Struct +{ +/*000*/ uint32 client_id; +/*004*/ uint32 count; +/*008*/ DynamicZoneChooseZoneEntry_Struct choices[0]; +}; + +struct DynamicZoneChooseZoneReply_Struct +{ +/*000*/ uint32 unknown000; // ff ff ff ff +/*004*/ uint32 unknown004; // seen 69 00 00 00 +/*008*/ uint32 unknown008; // ff ff ff ff +/*012*/ uint32 unknown_id1; // from choose zone entry message +/*016*/ uint16 dz_zone_id; // dz_id pair +/*018*/ uint16 dz_instance_id; +/*020*/ uint32 dz_type; // 1: Expedition, 2: Tutorial, 3: Task, 4: Mission, 5: Quest +/*024*/ uint32 unknown_id2; // from choose zone entry message +/*028*/ uint32 unknown028; // 00 00 00 00 +/*032*/ uint32 unknown032; // always same as unknown044 +/*036*/ uint32 unknown036; +/*040*/ uint32 unknown040; +/*044*/ uint32 unknown044; // always same as unknown032 +/*048*/ uint32 unknown048; // seen 01 00 00 00 and 02 00 00 00 }; struct AltCurrencySelectItem_Struct { diff --git a/common/ruletypes.h b/common/ruletypes.h index e46439184..5ff662c50 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -785,6 +785,20 @@ RULE_BOOL(Instances, RecycleInstanceIds, true, "Setting whether free instance ID RULE_INT(Instances, GuildHallExpirationDays, 90, "Amount of days before a Guild Hall instance expires") RULE_CATEGORY_END() +RULE_CATEGORY(Expedition) +RULE_INT(Expedition, MinStatusToBypassPlayerCountRequirements, 80, "Minimum GM status to bypass minimum player requirements for Expedition creation") +RULE_BOOL(Expedition, EmptyDzShutdownEnabled, true, "Enable early instance shutdown after last member of expedition removed") +RULE_INT(Expedition, EmptyDzShutdownDelaySeconds, 1500, "Seconds to set dynamic zone instance expiration if early shutdown enabled") +RULE_INT(Expedition, WorldExpeditionProcessRateMS, 6000, "Timer interval (ms) that world checks expedition states") +RULE_BOOL(Expedition, AlwaysNotifyNewLeaderOnChange, false, "Always notify clients when made expedition leader. If false (live-like) new leaders are only notified when made leader via /dzmakeleader") +RULE_REAL(Expedition, LockoutDurationMultiplier, 1.0, "Multiplies lockout duration by this value when new lockouts are added") +RULE_BOOL(Expedition, EnableInDynamicZoneStatus, false, "Enables the 'In Dynamic Zone' member status in expedition window. If false (live-like) players inside the dz will show as 'Online'") +RULE_CATEGORY_END() + +RULE_CATEGORY(DynamicZone) +RULE_INT(DynamicZone, ClientRemovalDelayMS, 60000, "Delay (ms) until a client is teleported out of dynamic zone after being removed as member") +RULE_CATEGORY_END() + #undef RULE_CATEGORY #undef RULE_INT #undef RULE_REAL diff --git a/common/servertalk.h b/common/servertalk.h index 5ed3b3306..40be635ad 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -140,6 +140,35 @@ #define ServerOP_LFPUpdate 0x0213 #define ServerOP_LFPMatches 0x0214 #define ServerOP_ClientVersionSummary 0x0215 + +#define ServerOP_ExpeditionCreate 0x0400 +#define ServerOP_ExpeditionDeleted 0x0401 +#define ServerOP_ExpeditionLeaderChanged 0x0402 +#define ServerOP_ExpeditionLockout 0x0403 +#define ServerOP_ExpeditionMemberChange 0x0404 +#define ServerOP_ExpeditionMemberSwap 0x0405 +#define ServerOP_ExpeditionMemberStatus 0x0406 +#define ServerOP_ExpeditionGetOnlineMembers 0x0407 +#define ServerOP_ExpeditionDzAddPlayer 0x0408 +#define ServerOP_ExpeditionDzMakeLeader 0x0409 +#define ServerOP_ExpeditionDzCompass 0x040a +#define ServerOP_ExpeditionDzSafeReturn 0x040b +#define ServerOP_ExpeditionDzZoneIn 0x040c +#define ServerOP_ExpeditionCharacterLockout 0x040d +#define ServerOP_ExpeditionSaveInvite 0x040e +#define ServerOP_ExpeditionRequestInvite 0x040f +#define ServerOP_ExpeditionReplayOnJoin 0x0410 +#define ServerOP_ExpeditionLockState 0x0411 +#define ServerOP_ExpeditionMembersRemoved 0x0412 +#define ServerOP_ExpeditionDzDuration 0x0413 +#define ServerOP_ExpeditionLockoutDuration 0x0414 +#define ServerOP_ExpeditionSecondsRemaining 0x0415 +#define ServerOP_ExpeditionExpireWarning 0x0416 +#define ServerOP_ExpeditionChooseNewLeader 0x0417 + +#define ServerOP_DzCharacterChange 0x0450 +#define ServerOP_DzRemoveAllCharacters 0x0451 + #define ServerOP_LSInfo 0x1000 #define ServerOP_LSStatus 0x1001 #define ServerOP_LSClientAuthLeg 0x1002 @@ -257,6 +286,7 @@ #define ServerOP_CZTaskRemoveGroup 0x4560 #define ServerOP_CZTaskRemoveRaid 0x4561 #define ServerOP_CZTaskRemoveGuild 0x4562 +#define ServerOP_CZClientMessageString 0x4563 #define ServerOP_WWAssignTask 0x4750 #define ServerOP_WWCastSpell 0x4751 @@ -1433,6 +1463,14 @@ struct CZNPCSignal_Struct { uint32 signal; }; +struct CZClientMessageString_Struct { + uint32 string_id; + uint16 chat_type; + char character_name[64]; + uint32 args_size; + char args[1]; // null delimited +}; + struct CZClientSignalByName_Struct { char character_name[64]; uint32 signal; @@ -1958,6 +1996,146 @@ struct UCSServerStatus_Struct { }; }; +struct ServerExpeditionID_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint32 sender_instance_id; +}; + +struct ServerExpeditionLeaderID_Struct { + uint32 expedition_id; + uint32 leader_id; +}; + +struct ServerExpeditionMemberChange_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 removed; // 0: added, 1: removed + uint32 char_id; + char char_name[64]; +}; + +struct ServerExpeditionMemberSwap_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint32 add_char_id; + uint32 remove_char_id; + char add_char_name[64]; + char remove_char_name[64]; +}; + +struct ServerExpeditionMemberStatus_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 status; // 0: unknown 1: Online 2: Offline 3: In Dynamic Zone 4: Link Dead + uint32 character_id; +}; + +struct ServerExpeditionCharacterEntry_Struct { + uint32 expedition_id; + uint32 character_id; + uint32 character_zone_id; + uint16 character_instance_id; + uint8 character_online; // 0: offline 1: online +}; + +struct ServerExpeditionCharacters_Struct { + uint32 sender_zone_id; + uint16 sender_instance_id; + uint32 count; + ServerExpeditionCharacterEntry_Struct entries[0]; +}; + +struct ServerExpeditionLockout_Struct { + uint32 expedition_id; + uint64 expire_time; + uint32 duration; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 remove; + uint8 members_only; + int seconds_adjust; + char event_name[256]; +}; + +struct ServerExpeditionLockState_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 enabled; + uint8 lock_msg; // 0: none, 1: closing 2: trial begin +}; + +struct ServerExpeditionSetting_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 enabled; +}; + +struct ServerExpeditionCharacterLockout_Struct { + uint8 remove; + uint32 character_id; + uint64 expire_time; + uint32 duration; + char uuid[37]; + char expedition_name[128]; + char event_name[256]; +}; + +struct ServerExpeditionCharacterID_Struct { + uint32_t character_id; +}; + +struct ServerExpeditionUpdateDuration_Struct { + uint32_t expedition_id; + uint32_t new_duration_seconds; +}; + +struct ServerExpeditionExpireWarning_Struct { + uint32_t expedition_id; + uint32_t minutes_remaining; +}; + +struct ServerDzCommand_Struct { + uint32 expedition_id; + uint8 is_char_online; // 0: target name is offline, 1: online + char requester_name[64]; + char target_name[64]; + char remove_name[64]; // used for swap command +}; + +struct ServerDzCommandMakeLeader_Struct { + uint32 expedition_id; + uint32 requester_id; + uint8 is_online; // set by world, 0: new leader name offline, 1: online + uint8 is_success; // set by world, 0: makeleader failed, 1: success (is online member) + char new_leader_name[64]; +}; + +struct ServerDzLocation_Struct { + uint32 owner_id; // system associated with the dz (expedition, shared task, etc) + uint16 dz_zone_id; + uint16 dz_instance_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint32 zone_id; // compass or safereturn zone id + float y; + float x; + float z; + float heading; +}; + +struct ServerDzCharacter_Struct { + uint16 zone_id; + uint16 instance_id; + uint8 remove; // 0: added 1: removed + uint32 character_id; +}; + #pragma pack() #endif diff --git a/common/string_util.cpp b/common/string_util.cpp index 680c2822b..4b3b59eb1 100644 --- a/common/string_util.cpp +++ b/common/string_util.cpp @@ -592,3 +592,15 @@ std::string numberToWords(unsigned long long int n) return res; } + +// first letter capitalized and rest made lower case +std::string FormatName(const std::string& char_name) +{ + std::string formatted(char_name); + if (!formatted.empty()) + { + std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower); + formatted[0] = ::toupper(formatted[0]); + } + return formatted; +} diff --git a/common/string_util.h b/common/string_util.h index 0d602e395..226e21645 100644 --- a/common/string_util.h +++ b/common/string_util.h @@ -206,6 +206,6 @@ void MakeLowerString(const char *source, char *target); void RemoveApostrophes(std::string &s); std::string convert2digit(int n, std::string suffix); std::string numberToWords(unsigned long long int n); - +std::string FormatName(const std::string& char_name); #endif diff --git a/common/version.h b/common/version.h index 82fe629b6..d6647f2c1 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9158 +#define CURRENT_BINARY_DATABASE_VERSION 9159 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9027 diff --git a/utils/patches/patch_RoF.conf b/utils/patches/patch_RoF.conf index 6e638ba1a..76178bf62 100644 --- a/utils/patches/patch_RoF.conf +++ b/utils/patches/patch_RoF.conf @@ -375,16 +375,18 @@ OP_DzRemovePlayer=0x0dc1 OP_DzSwapPlayer=0x4995 OP_DzMakeLeader=0x17b2 OP_DzPlayerList=0x1aff -OP_DzJoinExpeditionConfirm=0x30df -OP_DzJoinExpeditionReply=0x15d4 +OP_DzExpeditionInvite=0x30df +OP_DzExpeditionInviteResponse=0x15d4 OP_DzExpeditionInfo=0x3861 -OP_DzExpeditionList=0x0b3b -OP_DzMemberStatus=0x26c2 -OP_DzLeaderStatus=0x4021 -OP_DzExpeditionEndsWarning=0x32eb +OP_DzExpeditionLockoutTimers=0x0b3b OP_DzMemberList=0x348f +OP_DzMemberListName=0x26c2 +OP_DzMemberListStatus=0x0000 +OP_DzSetLeaderName=0x4021 +OP_DzExpeditionEndsWarning=0x32eb OP_DzCompass=0x0e01 # Was 0x4f09 OP_DzChooseZone=0x6e5e # Maybe 0x29d6 +OP_DzChooseZoneReply=0x0000 # New Opcodes OP_SpawnPositionUpdate=0x0000 # Actually OP_MobUpdate ? diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index f62b90dd4..96f3258a8 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -368,25 +368,28 @@ OP_AggroMeterLockTarget=0x1643 OP_AggroMeterTargetInfo=0x16bc OP_AggroMeterUpdate=0x1781 OP_UnderWorld=0x2eb3 # clients sends up when they detect an underworld issue, might be useful for cheat detection +OP_KickPlayers=0x6770 # Expeditions +OP_DzQuit=0xb2e3 +OP_DzListTimers=0x7b68 OP_DzAddPlayer=0x4701 OP_DzRemovePlayer=0x1abc OP_DzSwapPlayer=0x405b OP_DzMakeLeader=0x543d OP_DzPlayerList=0x14c6 -OP_DzJoinExpeditionConfirm=0x7f4b -OP_DzJoinExpeditionReply=0x1950 -OP_DzListTimers=0x7b68 +OP_DzExpeditionInvite=0x7f4b +OP_DzExpeditionInviteResponse=0x1950 OP_DzExpeditionInfo=0x9119 -OP_DzExpeditionList=0x205f -OP_DzQuit=0xb2e3 -OP_DzMemberStatus=0x32f0 -OP_DzLeaderStatus=0x3de9 +OP_DzExpeditionLockoutTimers=0x205f OP_DzMemberList=0x5ae4 -OP_DzExpeditionEndsWarning=0x383c +OP_DzMemberListName=0x32f0 +OP_DzMemberListStatus=0x12F5 +OP_DzSetLeaderName=0x3de9 +OP_DzExpeditionEndsWarning=0x5189 OP_DzCompass=0x3e0e OP_DzChooseZone=0x0b7d +OP_DzChooseZoneReply=0x4de1 # New Opcodes OP_SpawnPositionUpdate=0x0000 # Actually OP_MobUpdate ? diff --git a/utils/patches/patch_SoD.conf b/utils/patches/patch_SoD.conf index 05e4a335f..1ef8a138a 100644 --- a/utils/patches/patch_SoD.conf +++ b/utils/patches/patch_SoD.conf @@ -367,17 +367,18 @@ OP_DzRemovePlayer=0xa682 OP_DzSwapPlayer=0x0d8d OP_DzMakeLeader=0x1caa OP_DzPlayerList=0x74ca -OP_DzJoinExpeditionConfirm=0x1772 -OP_DzJoinExpeditionReply=0x3c13 +OP_DzExpeditionInvite=0x1772 +OP_DzExpeditionInviteResponse=0x3c13 OP_DzExpeditionInfo=0x128e -OP_DzMemberStatus=0x4661 -OP_DzLeaderStatus=0x226f -OP_DzExpeditionEndsWarning=0x1879 -OP_DzExpeditionList=0x3657 +OP_DzExpeditionLockoutTimers=0x3657 OP_DzMemberList=0x74e4 +OP_DzMemberListName=0x4661 +OP_DzMemberListStatus=0x1d99 +OP_DzSetLeaderName=0x226f +OP_DzExpeditionEndsWarning=0x1879 OP_DzCompass=0x35d3 OP_DzChooseZone=0x0d8a -#0x1d99 was grouped with these too but I don't really know it's purpose. +OP_DzChooseZoneReply=0x5a67 # New Opcodes OP_SpawnPositionUpdate=0x4656 # diff --git a/utils/patches/patch_SoF.conf b/utils/patches/patch_SoF.conf index c6d754ba3..1c5fd3eda 100644 --- a/utils/patches/patch_SoF.conf +++ b/utils/patches/patch_SoF.conf @@ -348,16 +348,18 @@ OP_DzRemovePlayer=0x2ce8 OP_DzSwapPlayer=0x2c3e OP_DzMakeLeader=0x1a75 OP_DzPlayerList=0x5116 -OP_DzJoinExpeditionConfirm=0x1793 -OP_DzJoinExpeditionReply=0x7a6f +OP_DzExpeditionInvite=0x1793 +OP_DzExpeditionInviteResponse=0x7a6f OP_DzExpeditionInfo=0x60a6 -OP_DzMemberStatus=0x0516 -OP_DzLeaderStatus=0x79d3 -OP_DzExpeditionEndsWarning=0x5153 -OP_DzExpeditionList=0x02ac +OP_DzExpeditionLockoutTimers=0x02ac OP_DzMemberList=0x5e14 +OP_DzMemberListName=0x0516 +OP_DzMemberListStatus=0x0000 +OP_DzSetLeaderName=0x79d3 +OP_DzExpeditionEndsWarning=0x5153 OP_DzCompass=0x531d OP_DzChooseZone=0x3c5b +OP_DzChooseZoneReply=0x0000 #Looting OP_LootRequest=0x36E3 #Trevius 02/16/09 diff --git a/utils/patches/patch_Titanium.conf b/utils/patches/patch_Titanium.conf index ade2576aa..f984f88db 100644 --- a/utils/patches/patch_Titanium.conf +++ b/utils/patches/patch_Titanium.conf @@ -297,17 +297,18 @@ OP_DzRemovePlayer=0x540b OP_DzSwapPlayer=0x794a OP_DzMakeLeader=0x0ce9 OP_DzPlayerList=0xada0 -OP_DzJoinExpeditionConfirm=0x3817 -OP_DzJoinExpeditionReply=0x5da9 +OP_DzExpeditionInvite=0x3817 +OP_DzExpeditionInviteResponse=0x5da9 OP_DzExpeditionInfo=0x98e -OP_DzMemberStatus=0x1826 -OP_DzLeaderStatus=0x7abc -OP_DzExpeditionEndsWarning=0x1c3f -OP_DzExpeditionList=0x7c12 +OP_DzExpeditionLockoutTimers=0x7c12 OP_DzMemberList=0x9b6 +OP_DzMemberListName=0x1826 +OP_DzMemberListStatus=0x330d +OP_DzSetLeaderName=0x7abc +OP_DzExpeditionEndsWarning=0x1c3f OP_DzCompass=0x28aa OP_DzChooseZone=0x1022 -#0x330d is something but I'm not sure what yet. +OP_DzChooseZoneReply=0x20e7 #bazaar trader stuff stuff: #become and buy from diff --git a/utils/patches/patch_UF.conf b/utils/patches/patch_UF.conf index 101fafde1..8efce91f6 100644 --- a/utils/patches/patch_UF.conf +++ b/utils/patches/patch_UF.conf @@ -377,16 +377,18 @@ OP_DzRemovePlayer=0x054e OP_DzSwapPlayer=0x4661 OP_DzMakeLeader=0x226f OP_DzPlayerList=0x74e4 -OP_DzJoinExpeditionConfirm=0x3c5e -OP_DzJoinExpeditionReply=0x1154 +OP_DzExpeditionInvite=0x3c5e +OP_DzExpeditionInviteResponse=0x1154 OP_DzExpeditionInfo=0x1150 -OP_DzMemberStatus=0x2d17 -OP_DzLeaderStatus=0x2caf -OP_DzExpeditionEndsWarning=0x6ac2 -OP_DzExpeditionList=0x70d8 +OP_DzExpeditionLockoutTimers=0x70d8 OP_DzMemberList=0x15c4 +OP_DzMemberListName=0x2d17 +OP_DzMemberListStatus=0x0d98 +OP_DzSetLeaderName=0x2caf +OP_DzExpeditionEndsWarning=0x6ac2 OP_DzCompass=0x01cb OP_DzChooseZone=0x65e1 +OP_DzChooseZoneReply=0xa682 #shroud OP_ShroudSelectionWindow=0x72ad diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 9f005365b..bdc41a337 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -412,6 +412,7 @@ 9156|2020_08_16_virtual_zonepoints.sql|SHOW COLUMNS from `zone_points` LIKE 'is_virtual'|empty| 9157|2020_09_02_pet_taunting.sql|SHOW COLUMNS from `character_pet_info` LIKE 'taunting'|empty| 9158|2020_12_09_underworld.sql|SHOW COLUMNS from `zone` LIKE 'underworld_teleport_index'|empty| +9159|2020_12_22_expedition_system.sql|SELECT * FROM db_version WHERE version >= 9159|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2020_12_22_expedition_system.sql b/utils/sql/git/required/2020_12_22_expedition_system.sql new file mode 100644 index 000000000..820940500 --- /dev/null +++ b/utils/sql/git/required/2020_12_22_expedition_system.sql @@ -0,0 +1,82 @@ +CREATE TABLE `expeditions` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` VARCHAR(36) NOT NULL, + `dynamic_zone_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `expedition_name` VARCHAR(128) NOT NULL, + `leader_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `min_players` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0, + `max_players` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0, + `add_replay_on_join` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1, + `is_locked` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE INDEX `dynamic_zone_id` (`dynamic_zone_id`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +; + +CREATE TABLE `expedition_lockouts` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `expedition_id` INT(10) UNSIGNED NOT NULL, + `event_name` VARCHAR(256) NOT NULL, + `expire_time` DATETIME NOT NULL DEFAULT current_timestamp(), + `duration` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `from_expedition_uuid` VARCHAR(36) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `expedition_id_event_name` (`expedition_id`, `event_name`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +; + +CREATE TABLE `expedition_members` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `expedition_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `character_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `is_current_member` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE INDEX `expedition_id_character_id` (`expedition_id`, `character_id`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +; + +CREATE TABLE `character_expedition_lockouts` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `character_id` INT(10) UNSIGNED NOT NULL, + `expedition_name` VARCHAR(128) NOT NULL, + `event_name` VARCHAR(256) NOT NULL, + `expire_time` DATETIME NOT NULL DEFAULT current_timestamp(), + `duration` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `from_expedition_uuid` VARCHAR(36) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `character_id_expedition_name_event_name` (`character_id`, `expedition_name`, `event_name`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +; + +CREATE TABLE `dynamic_zones` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `instance_id` INT(10) NOT NULL DEFAULT 0, + `type` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0, + `compass_zone_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `compass_x` FLOAT NOT NULL DEFAULT 0, + `compass_y` FLOAT NOT NULL DEFAULT 0, + `compass_z` FLOAT NOT NULL DEFAULT 0, + `safe_return_zone_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `safe_return_x` FLOAT NOT NULL DEFAULT 0, + `safe_return_y` FLOAT NOT NULL DEFAULT 0, + `safe_return_z` FLOAT NOT NULL DEFAULT 0, + `safe_return_heading` FLOAT NOT NULL DEFAULT 0, + `zone_in_x` FLOAT NOT NULL DEFAULT 0, + `zone_in_y` FLOAT NOT NULL DEFAULT 0, + `zone_in_z` FLOAT NOT NULL DEFAULT 0, + `zone_in_heading` FLOAT NOT NULL DEFAULT 0, + `has_zone_in` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE INDEX `instance_id` (`instance_id`) +) +COLLATE='utf8mb4_general_ci' +ENGINE=InnoDB +; diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index ab14ed603..50210f2f3 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -9,6 +9,10 @@ SET(world_sources console.cpp eql_config.cpp eqemu_api_world_data_service.cpp + expedition.cpp + expedition_database.cpp + expedition_message.cpp + expedition_state.cpp launcher_link.cpp launcher_list.cpp lfplist.cpp @@ -39,6 +43,10 @@ SET(world_headers console.h eql_config.h eqemu_api_world_data_service.h + expedition.h + expedition_database.h + expedition_message.h + expedition_state.h launcher_link.h launcher_list.h lfplist.h diff --git a/world/cliententry.h b/world/cliententry.h index b4d449c58..769700403 100644 --- a/world/cliententry.h +++ b/world/cliententry.h @@ -127,6 +127,9 @@ public: inline void PushToTellQueue(ServerChannelMessage_Struct *scm) { tell_queue.push_back(scm); } void ProcessTellQueue(); + void SetPendingExpeditionInvite(ServerPacket* pack) { p_pending_expedition_invite.reset(pack->Copy()); }; + std::unique_ptr GetPendingExpeditionInvite() { return std::move(p_pending_expedition_invite); } + private: void ClearVars(bool iAll = false); @@ -171,6 +174,8 @@ private: // Tell Queue -- really a vector :D std::vector tell_queue; + + std::unique_ptr p_pending_expedition_invite = nullptr; }; #endif /*CLIENTENTRY_H_*/ diff --git a/world/expedition.cpp b/world/expedition.cpp new file mode 100644 index 000000000..2f1a8f0be --- /dev/null +++ b/world/expedition.cpp @@ -0,0 +1,195 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition.h" +#include "expedition_database.h" +#include "cliententry.h" +#include "clientlist.h" +#include "zonelist.h" +#include "zoneserver.h" +#include "../common/eqemu_logsys.h" + +extern ClientList client_list; +extern ZSList zoneserver_list; + +Expedition::Expedition(uint32_t expedition_id, uint32_t dz_id, uint32_t dz_instance_id, + uint32_t dz_zone_id, uint32_t start_time, uint32_t duration, uint32_t leader_id +) : + m_expedition_id(expedition_id), + m_dz_id(dz_id), + m_dz_instance_id(dz_instance_id), + m_dz_zone_id(dz_zone_id), + m_start_time(std::chrono::system_clock::from_time_t(start_time)), + m_duration(duration), + m_leader_id(leader_id) +{ + m_expire_time = m_start_time + m_duration; + m_warning_cooldown_timer.Enable(); +} + +void Expedition::AddMember(uint32_t character_id) +{ + auto it = std::find_if(m_member_ids.begin(), m_member_ids.end(), + [&](uint32_t member_id) { return member_id == character_id; }); + + if (it == m_member_ids.end()) + { + m_member_ids.emplace_back(character_id); + } +} + +bool Expedition::HasMember(uint32_t character_id) +{ + return std::any_of(m_member_ids.begin(), m_member_ids.end(), + [&](uint32_t member_id) { return member_id == character_id; }); +} + +void Expedition::RemoveMember(uint32_t character_id) +{ + m_member_ids.erase(std::remove_if(m_member_ids.begin(), m_member_ids.end(), + [&](uint32_t member_id) { return member_id == character_id; } + ), m_member_ids.end()); + + if (!m_member_ids.empty() && character_id == m_leader_id) + { + ChooseNewLeader(); + } +} + +void Expedition::ChooseNewLeader() +{ + // we don't track expedition member status in world so may choose a linkdead member + // this is fine since it will trigger another change when that member goes offline + auto it = std::find_if(m_member_ids.begin(), m_member_ids.end(), [&](uint32_t member_id) { + auto member_cle = (member_id != m_leader_id) ? client_list.FindCLEByCharacterID(member_id) : nullptr; + return (member_id != m_leader_id && member_cle && member_cle->GetOnline() == CLE_Status::InZone); + }); + + if (it == m_member_ids.end()) + { + // no online members found, fallback to choosing any member + it = std::find_if(m_member_ids.begin(), m_member_ids.end(), + [&](uint32_t member_id) { return (member_id != m_leader_id); }); + } + + if (it != m_member_ids.end()) + { + SetNewLeader(*it); + } +} + +bool Expedition::SetNewLeader(uint32_t character_id) +{ + if (!HasMember(character_id)) + { + return false; + } + + LogExpeditionsModerate("Replacing [{}] leader [{}] with [{}]", m_expedition_id, m_leader_id, character_id); + ExpeditionDatabase::UpdateLeaderID(m_expedition_id, character_id); + m_leader_id = character_id; + SendZonesLeaderChanged(); + return true; +} + +void Expedition::SendZonesExpeditionDeleted() +{ + uint32_t pack_size = sizeof(ServerExpeditionID_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionDeleted, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + zoneserver_list.SendPacket(pack.get()); +} + +void Expedition::SendZonesDurationUpdate() +{ + uint32_t packsize = sizeof(ServerExpeditionUpdateDuration_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionDzDuration, packsize)); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->expedition_id = GetID(); + packbuf->new_duration_seconds = static_cast(m_duration.count()); + zoneserver_list.SendPacket(pack.get()); +} + +void Expedition::SendZonesExpireWarning(uint32_t minutes_remaining) +{ + uint32_t pack_size = sizeof(ServerExpeditionExpireWarning_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionExpireWarning, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->minutes_remaining = minutes_remaining; + zoneserver_list.SendPacket(pack.get()); +} + +void Expedition::SendZonesLeaderChanged() +{ + uint32_t pack_size = sizeof(ServerExpeditionLeaderID_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionLeaderChanged, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->leader_id = m_leader_id; + zoneserver_list.SendPacket(pack.get()); +} + +void Expedition::UpdateDzSecondsRemaining(uint32_t seconds_remaining) +{ + auto now = std::chrono::system_clock::now(); + auto update_time = std::chrono::seconds(seconds_remaining); + + auto current_remaining = m_expire_time - now; + if (current_remaining > update_time) // reduce only + { + LogExpeditionsDetail( + "Updating expedition [{}] dz instance [{}] seconds remaining to [{}]s", + GetID(), GetInstanceID(), seconds_remaining + ); + + // preserve original start time and adjust duration instead + m_expire_time = now + update_time; + m_duration = std::chrono::duration_cast(m_expire_time - m_start_time); + + ExpeditionDatabase::UpdateDzDuration(GetInstanceID(), static_cast(m_duration.count())); + + // update zone level caches and update the actual dz instance's timer + SendZonesDurationUpdate(); + } +} + +std::chrono::system_clock::duration Expedition::GetRemainingDuration() const +{ + return m_expire_time - std::chrono::system_clock::now(); +} + +void Expedition::CheckExpireWarning() +{ + if (m_warning_cooldown_timer.Check(false)) + { + using namespace std::chrono_literals; + auto remaining = GetRemainingDuration(); + if ((remaining > 14min && remaining < 15min) || + (remaining > 4min && remaining < 5min) || + (remaining > 0min && remaining < 1min)) + { + int minutes = std::chrono::duration_cast(remaining).count() + 1; + SendZonesExpireWarning(minutes); + m_warning_cooldown_timer.Start(70000); // 1 minute 10 seconds + } + } +} diff --git a/world/expedition.h b/world/expedition.h new file mode 100644 index 000000000..0b5f04e78 --- /dev/null +++ b/world/expedition.h @@ -0,0 +1,73 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef WORLD_EXPEDITION_H +#define WORLD_EXPEDITION_H + +#include "../common/timer.h" +#include +#include +#include + +class Expedition +{ +public: + Expedition() = default; + Expedition(uint32_t expedition_id, uint32_t dz_id, uint32_t dz_instance_id, + uint32_t dz_zone_id, uint32_t expire_time, uint32_t duration, uint32_t leader_id); + + void AddMember(uint32_t character_id); + void RemoveMember(uint32_t character_id); + void RemoveAllMembers() { m_member_ids.clear(); } + void CheckExpireWarning(); + void ChooseNewLeader(); + uint32_t GetID() const { return m_expedition_id; } + uint16_t GetInstanceID() const { return static_cast(m_dz_instance_id); } + uint16_t GetZoneID() const { return static_cast(m_dz_zone_id); } + bool HasMember(uint32_t character_id); + bool IsEmpty() const { return m_member_ids.empty(); } + bool IsExpired() const { return m_expire_time < std::chrono::system_clock::now(); } + bool IsPendingDelete() const { return m_pending_delete; } + bool IsValid() const { return m_expedition_id != 0; } + void SendZonesDurationUpdate(); + void SendZonesExpeditionDeleted(); + void SendZonesExpireWarning(uint32_t minutes_remaining); + bool SetNewLeader(uint32_t new_leader_id); + void SetPendingDelete(bool pending) { m_pending_delete = pending; } + void UpdateDzSecondsRemaining(uint32_t seconds_remaining); + std::chrono::system_clock::duration GetRemainingDuration() const; + +private: + void SendZonesLeaderChanged(); + + uint32_t m_expedition_id = 0; + uint32_t m_dz_id = 0; + uint32_t m_dz_instance_id = 0; + uint32_t m_dz_zone_id = 0; + uint32_t m_leader_id = 0; + bool m_pending_delete = false; + Timer m_warning_cooldown_timer; + std::vector m_member_ids; + std::chrono::seconds m_duration; + std::chrono::time_point m_start_time; + std::chrono::time_point m_expire_time; +}; + +#endif diff --git a/world/expedition_database.cpp b/world/expedition_database.cpp new file mode 100644 index 000000000..f9df67918 --- /dev/null +++ b/world/expedition_database.cpp @@ -0,0 +1,215 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition_database.h" +#include "expedition.h" +#include "worlddb.h" + +void ExpeditionDatabase::PurgeExpiredExpeditions() +{ + std::string query = SQL( + SELECT + expeditions.id + FROM expeditions + LEFT JOIN dynamic_zones ON expeditions.dynamic_zone_id = dynamic_zones.id + LEFT JOIN instance_list ON dynamic_zones.instance_id = instance_list.id + LEFT JOIN + ( + SELECT expedition_id, COUNT(IF(is_current_member = TRUE, 1, NULL)) member_count + FROM expedition_members + GROUP BY expedition_id + ) expedition_members + ON expedition_members.expedition_id = expeditions.id + WHERE + instance_list.id IS NULL + OR expedition_members.member_count IS NULL + OR expedition_members.member_count = 0 + OR (instance_list.start_time + instance_list.duration) <= UNIX_TIMESTAMP(); + ); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + std::vector expedition_ids; + for (auto row = results.begin(); row != results.end(); ++row) + { + expedition_ids.emplace_back(static_cast(strtoul(row[0], nullptr, 10))); + } + + if (!expedition_ids.empty()) + { + ExpeditionDatabase::MoveMembersToSafeReturn(expedition_ids); + ExpeditionDatabase::DeleteExpeditions(expedition_ids); + } + } +} + +void ExpeditionDatabase::PurgeExpiredCharacterLockouts() +{ + std::string query = SQL( + DELETE FROM character_expedition_lockouts + WHERE expire_time <= NOW(); + ); + + database.QueryDatabase(query); +} + +std::vector ExpeditionDatabase::LoadExpeditions(uint32_t select_expedition_id) +{ + std::vector expeditions; + + std::string query = SQL( + SELECT + expeditions.id, + expeditions.dynamic_zone_id, + instance_list.id, + instance_list.zone, + instance_list.start_time, + instance_list.duration, + expeditions.leader_id, + expedition_members.character_id + FROM expeditions + INNER JOIN dynamic_zones ON expeditions.dynamic_zone_id = dynamic_zones.id + INNER JOIN instance_list ON dynamic_zones.instance_id = instance_list.id + INNER JOIN expedition_members ON expedition_members.expedition_id = expeditions.id + AND expedition_members.is_current_member = TRUE + ); + + if (select_expedition_id != 0) + { + query.append(fmt::format(" WHERE expeditions.id = {};", select_expedition_id)); + } + else + { + query.append(" ORDER BY expeditions.id;"); + } + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + uint32_t last_expedition_id = 0; + + for (auto row = results.begin(); row != results.end(); ++row) + { + uint32_t expedition_id = strtoul(row[0], nullptr, 10); + + if (last_expedition_id != expedition_id) + { + expeditions.emplace_back( + static_cast(strtoul(row[0], nullptr, 10)), // expedition_id + static_cast(strtoul(row[1], nullptr, 10)), // dz_id + static_cast(strtoul(row[2], nullptr, 10)), // dz_instance_id + static_cast(strtoul(row[3], nullptr, 10)), // dz_zone_id + static_cast(strtoul(row[4], nullptr, 10)), // start_time + static_cast(strtoul(row[5], nullptr, 10)), // duration + static_cast(strtoul(row[6], nullptr, 10)) // leader_id + ); + } + + last_expedition_id = expedition_id; + + uint32_t member_id = static_cast(strtoul(row[7], nullptr, 10)); + expeditions.back().AddMember(member_id); + } + } + + return expeditions; +} + +Expedition ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) +{ + LogExpeditions("Loading expedition [{}] for world cache", expedition_id); + + Expedition expedition; + + auto expeditions = LoadExpeditions(expedition_id); + if (!expeditions.empty()) + { + expedition = expeditions.front(); + } + + return expedition; +} + +void ExpeditionDatabase::DeleteExpeditions(const std::vector& expedition_ids) +{ + LogExpeditionsDetail("Deleting [{}] expedition(s)", expedition_ids.size()); + + std::string expedition_ids_query = fmt::format("{}", fmt::join(expedition_ids, ",")); + + if (!expedition_ids_query.empty()) + { + auto query = fmt::format("DELETE FROM expeditions WHERE id IN ({});", expedition_ids_query); + database.QueryDatabase(query); + + query = fmt::format("DELETE FROM expedition_members WHERE expedition_id IN ({});", expedition_ids_query); + database.QueryDatabase(query); + + query = fmt::format("DELETE FROM expedition_lockouts WHERE expedition_id IN ({});", expedition_ids_query); + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::UpdateDzDuration(uint16_t instance_id, uint32_t new_duration) +{ + std::string query = fmt::format( + "UPDATE instance_list SET duration = {} WHERE id = {};", + new_duration, instance_id + ); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::UpdateLeaderID(uint32_t expedition_id, uint32_t leader_id) +{ + LogExpeditionsDetail("Updating leader [{}] for expedition [{}]", leader_id, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expeditions SET leader_id = {} WHERE id = {}; + ), leader_id, expedition_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::MoveMembersToSafeReturn(const std::vector& expedition_ids) +{ + LogExpeditionsDetail("Moving members from [{}] expedition(s) to safereturn", expedition_ids.size()); + + // only offline members still in expired dz zones should be updated here + std::string query = fmt::format(SQL( + UPDATE character_data + INNER JOIN expedition_members ON character_data.id = expedition_members.character_id + INNER JOIN expeditions ON expedition_members.expedition_id = expeditions.id + INNER JOIN dynamic_zones ON expeditions.dynamic_zone_id = dynamic_zones.id + INNER JOIN instance_list ON dynamic_zones.instance_id = instance_list.id + AND character_data.zone_instance = instance_list.id + AND character_data.zone_id = instance_list.zone + SET + zone_id = IF(safe_return_zone_id > 0, safe_return_zone_id, zone_id), + zone_instance = IF(safe_return_zone_id > 0, 0, zone_instance), + x = IF(safe_return_zone_id > 0, safe_return_x, x), + y = IF(safe_return_zone_id > 0, safe_return_y, y), + z = IF(safe_return_zone_id > 0, safe_return_z, z), + heading = IF(safe_return_zone_id > 0, safe_return_heading, heading) + WHERE expeditions.id IN ({}); + ), fmt::join(expedition_ids, ",")); + + database.QueryDatabase(query); +} diff --git a/world/expedition_database.h b/world/expedition_database.h new file mode 100644 index 000000000..16341210c --- /dev/null +++ b/world/expedition_database.h @@ -0,0 +1,41 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef WORLD_EXPEDITION_DATABASE_H +#define WORLD_EXPEDITION_DATABASE_H + +#include +#include + +class Expedition; + +namespace ExpeditionDatabase +{ + void DeleteExpeditions(const std::vector& expedition_ids); + std::vector LoadExpeditions(uint32_t select_expedition_id = 0); + Expedition LoadExpedition(uint32_t expedition_id); + void MoveMembersToSafeReturn(const std::vector& expedition_ids); + void PurgeExpiredExpeditions(); + void PurgeExpiredCharacterLockouts(); + void UpdateDzDuration(uint16_t instance_id, uint32_t new_duration); + void UpdateLeaderID(uint32_t expedition_id, uint32_t leader_id); +}; + +#endif diff --git a/world/expedition_message.cpp b/world/expedition_message.cpp new file mode 100644 index 000000000..646e2272e --- /dev/null +++ b/world/expedition_message.cpp @@ -0,0 +1,229 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition.h" +#include "expedition_message.h" +#include "expedition_state.h" +#include "cliententry.h" +#include "clientlist.h" +#include "zonelist.h" +#include "zoneserver.h" +#include "../common/servertalk.h" +#include + +extern ClientList client_list; +extern ZSList zoneserver_list; + +void ExpeditionMessage::HandleZoneMessage(ServerPacket* pack) +{ + switch (pack->opcode) + { + case ServerOP_ExpeditionChooseNewLeader: + { + ExpeditionMessage::ChooseNewLeader(pack); + break; + } + case ServerOP_ExpeditionCreate: + { + auto buf = reinterpret_cast(pack->pBuffer); + expedition_state.AddExpedition(buf->expedition_id); + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionMemberChange: + { + auto buf = reinterpret_cast(pack->pBuffer); + expedition_state.MemberChange(buf->expedition_id, buf->char_id, buf->removed); + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionMemberSwap: + { + auto buf = reinterpret_cast(pack->pBuffer); + expedition_state.MemberChange(buf->expedition_id, buf->add_char_id, false); + expedition_state.MemberChange(buf->expedition_id, buf->remove_char_id, true); + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionMembersRemoved: + { + auto buf = reinterpret_cast(pack->pBuffer); + expedition_state.RemoveAllMembers(buf->expedition_id); + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionGetOnlineMembers: + { + ExpeditionMessage::GetOnlineMembers(pack); + break; + } + case ServerOP_ExpeditionDzAddPlayer: + { + ExpeditionMessage::AddPlayer(pack); + break; + } + case ServerOP_ExpeditionDzMakeLeader: + { + ExpeditionMessage::MakeLeader(pack); + break; + } + case ServerOP_ExpeditionCharacterLockout: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto cle = client_list.FindCLEByCharacterID(buf->character_id); + if (cle && cle->Server()) + { + cle->Server()->SendPacket(pack); + } + break; + } + case ServerOP_ExpeditionSaveInvite: + { + ExpeditionMessage::SaveInvite(pack); + break; + } + case ServerOP_ExpeditionRequestInvite: + { + ExpeditionMessage::RequestInvite(pack); + break; + } + case ServerOP_ExpeditionSecondsRemaining: + { + auto buf = reinterpret_cast(pack->pBuffer); + expedition_state.SetSecondsRemaining(buf->expedition_id, buf->new_duration_seconds); + break; + } + } +} + +void ExpeditionMessage::AddPlayer(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + ClientListEntry* invited_cle = client_list.FindCharacter(buf->target_name); + if (invited_cle && invited_cle->Server()) + { + // continue in the add target's zone + buf->is_char_online = true; + invited_cle->Server()->SendPacket(pack); + } + else + { + // add target not online, return to inviter + ClientListEntry* inviter_cle = client_list.FindCharacter(buf->requester_name); + if (inviter_cle && inviter_cle->Server()) + { + inviter_cle->Server()->SendPacket(pack); + } + } +} + +void ExpeditionMessage::MakeLeader(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + // notify requester (old leader) and new leader of the result + ZoneServer* new_leader_zs = nullptr; + ClientListEntry* new_leader_cle = client_list.FindCharacter(buf->new_leader_name); + if (new_leader_cle && new_leader_cle->Server()) + { + auto expedition = expedition_state.GetExpedition(buf->expedition_id); + if (expedition) + { + buf->is_success = expedition->SetNewLeader(new_leader_cle->CharID()); + } + + buf->is_online = true; + new_leader_zs = new_leader_cle->Server(); + new_leader_zs->SendPacket(pack); + } + + // if old and new leader are in the same zone only send one message + ClientListEntry* requester_cle = client_list.FindCLEByCharacterID(buf->requester_id); + if (requester_cle && requester_cle->Server() && requester_cle->Server() != new_leader_zs) + { + requester_cle->Server()->SendPacket(pack); + } +} + +void ExpeditionMessage::GetOnlineMembers(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + // not efficient but only requested during caching + char zone_name[64] = {0}; + std::vector all_clients; + all_clients.reserve(client_list.GetClientCount()); + client_list.GetClients(zone_name, all_clients); + + for (uint32_t i = 0; i < buf->count; ++i) + { + auto it = std::find_if(all_clients.begin(), all_clients.end(), [&](const ClientListEntry* cle) { + return (cle && cle->CharID() == buf->entries[i].character_id); + }); + + if (it != all_clients.end()) + { + buf->entries[i].character_zone_id = (*it)->zone(); + buf->entries[i].character_instance_id = (*it)->instance(); + buf->entries[i].character_online = true; + } + } + + zoneserver_list.SendPacket(buf->sender_zone_id, buf->sender_instance_id, pack); +} + +void ExpeditionMessage::SaveInvite(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + ClientListEntry* invited_cle = client_list.FindCharacter(buf->target_name); + if (invited_cle) + { + // store packet on cle and re-send it when client requests it + buf->is_char_online = true; + pack->opcode = ServerOP_ExpeditionDzAddPlayer; + invited_cle->SetPendingExpeditionInvite(pack); + } +} + +void ExpeditionMessage::RequestInvite(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + ClientListEntry* cle = client_list.FindCLEByCharacterID(buf->character_id); + if (cle) + { + auto invite_pack = cle->GetPendingExpeditionInvite(); + if (invite_pack && cle->Server()) + { + cle->Server()->SendPacket(invite_pack.get()); + } + } +} + +void ExpeditionMessage::ChooseNewLeader(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = expedition_state.GetExpedition(buf->expedition_id); + if (expedition) + { + expedition->ChooseNewLeader(); + } +} diff --git a/world/expedition_message.h b/world/expedition_message.h new file mode 100644 index 000000000..8789577da --- /dev/null +++ b/world/expedition_message.h @@ -0,0 +1,37 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef WORLD_EXPEDITION_MESSAGE_H +#define WORLD_EXPEDITION_MESSAGE_H + +class ServerPacket; + +namespace ExpeditionMessage +{ + void AddPlayer(ServerPacket* pack); + void ChooseNewLeader(ServerPacket* pack); + void GetOnlineMembers(ServerPacket* pack); + void HandleZoneMessage(ServerPacket* pack); + void MakeLeader(ServerPacket* pack); + void RequestInvite(ServerPacket* pack); + void SaveInvite(ServerPacket* pack); +}; + +#endif diff --git a/world/expedition_state.cpp b/world/expedition_state.cpp new file mode 100644 index 000000000..332231f1b --- /dev/null +++ b/world/expedition_state.cpp @@ -0,0 +1,158 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition_state.h" +#include "expedition.h" +#include "expedition_database.h" +#include "zonelist.h" +#include "zoneserver.h" +#include "../common/eqemu_logsys.h" +#include + +extern ZSList zoneserver_list; + +ExpeditionState expedition_state; + +Expedition* ExpeditionState::GetExpedition(uint32_t expedition_id) +{ + auto it = std::find_if(m_expeditions.begin(), m_expeditions.end(), + [&](const Expedition& expedition) { return expedition.GetID() == expedition_id; }); + + return (it != m_expeditions.end()) ? &(*it) : nullptr; +} + +void ExpeditionState::LoadActiveExpeditions() +{ + BenchTimer benchmark; + + m_expeditions = ExpeditionDatabase::LoadExpeditions(); + + auto elapsed = benchmark.elapsed(); + LogExpeditions("World caching [{}] expeditions took [{}s]", m_expeditions.size(), elapsed); +} + +void ExpeditionState::AddExpedition(uint32_t expedition_id) +{ + if (expedition_id == 0) + { + return; + } + + auto expedition = ExpeditionDatabase::LoadExpedition(expedition_id); + + if (expedition.IsValid()) + { + auto existing_expedition = GetExpedition(expedition_id); + if (!existing_expedition) + { + m_expeditions.emplace_back(expedition); + } + } +} + +void ExpeditionState::RemoveExpedition(uint32_t expedition_id) +{ + m_expeditions.erase(std::remove_if(m_expeditions.begin(), m_expeditions.end(), + [&](const Expedition& expedition) { + return expedition.GetID() == expedition_id; + } + ), m_expeditions.end()); +} + +void ExpeditionState::MemberChange(uint32_t expedition_id, uint32_t character_id, bool remove) +{ + auto expedition = GetExpedition(expedition_id); + if (expedition) + { + if (remove) { + expedition->RemoveMember(character_id); + } else { + expedition->AddMember(character_id); + } + } +} + +void ExpeditionState::RemoveAllMembers(uint32_t expedition_id) +{ + auto expedition = GetExpedition(expedition_id); + if (expedition) + { + expedition->RemoveAllMembers(); + } +} + +void ExpeditionState::SetSecondsRemaining(uint32_t expedition_id, uint32_t seconds_remaining) +{ + auto expedition = GetExpedition(expedition_id); + if (expedition) + { + expedition->UpdateDzSecondsRemaining(seconds_remaining); + } +} + +void ExpeditionState::Process() +{ + if (!m_process_throttle_timer.Check()) + { + return; + } + + std::vector expedition_ids; + + for (auto it = m_expeditions.begin(); it != m_expeditions.end();) + { + bool is_deleted = false; + + if (it->IsEmpty() || it->IsExpired()) + { + // don't delete expedition until its dz instance is empty. this prevents + // an exploit where all members leave expedition and complete an event + // before being kicked from removal timer. the lockout could never be + // applied because the zone expedition cache was already invalidated. + auto dz_zoneserver = zoneserver_list.FindByInstanceID(it->GetInstanceID()); + if (!dz_zoneserver || dz_zoneserver->NumPlayers() == 0) + { + LogExpeditions("Expedition [{}] expired or empty, notifying zones and deleting", it->GetID()); + expedition_ids.emplace_back(it->GetID()); + it->SendZonesExpeditionDeleted(); + is_deleted = true; + } + + if (it->IsEmpty() && !it->IsPendingDelete() && RuleB(Expedition, EmptyDzShutdownEnabled)) + { + it->UpdateDzSecondsRemaining(RuleI(Expedition, EmptyDzShutdownDelaySeconds)); + } + + it->SetPendingDelete(true); + } + else + { + it->CheckExpireWarning(); + } + + it = is_deleted ? m_expeditions.erase(it) : it + 1; + } + + if (!expedition_ids.empty()) + { + ExpeditionDatabase::MoveMembersToSafeReturn(expedition_ids); + ExpeditionDatabase::DeleteExpeditions(expedition_ids); + } +} diff --git a/world/expedition_state.h b/world/expedition_state.h new file mode 100644 index 000000000..9d54dcec9 --- /dev/null +++ b/world/expedition_state.h @@ -0,0 +1,50 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef WORLD_EXPEDITION_STATE_H +#define WORLD_EXPEDITION_STATE_H + +#include "../common/rulesys.h" +#include "../common/timer.h" +#include +#include + +extern class ExpeditionState expedition_state; + +class Expedition; + +class ExpeditionState +{ +public: + void AddExpedition(uint32_t expedition_id); + Expedition* GetExpedition(uint32_t expedition_id); + void LoadActiveExpeditions(); + void MemberChange(uint32_t expedition_id, uint32_t character_id, bool remove); + void Process(); + void RemoveAllMembers(uint32_t expedition_id); + void RemoveExpedition(uint32_t expedition_id); + void SetSecondsRemaining(uint32_t expedition_id, uint32_t seconds_remaining); + +private: + std::vector m_expeditions; + Timer m_process_throttle_timer{static_cast(RuleI(Expedition, WorldExpeditionProcessRateMS))}; +}; + +#endif diff --git a/world/main.cpp b/world/main.cpp index b94ecea4c..b426d13ff 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -88,6 +88,8 @@ union semun { #include "queryserv.h" #include "web_interface.h" #include "console.h" +#include "expedition_database.h" +#include "expedition_state.h" #include "../common/net/servertalk_server.h" #include "../zone/data_bucket.h" @@ -423,12 +425,19 @@ int main(int argc, char** argv) { adventure_manager.LoadLeaderboardInfo(); + LogInfo("Purging expired expeditions"); + ExpeditionDatabase::PurgeExpiredExpeditions(); + ExpeditionDatabase::PurgeExpiredCharacterLockouts(); + LogInfo("Purging expired instances"); database.PurgeExpiredInstances(); Timer PurgeInstanceTimer(450000); PurgeInstanceTimer.Start(450000); + LogInfo("Loading active expeditions"); + expedition_state.LoadActiveExpeditions(); + LogInfo("Loading char create info"); content_db.LoadCharacterCreateAllocations(); content_db.LoadCharacterCreateCombos(); @@ -599,6 +608,7 @@ int main(int argc, char** argv) { if (PurgeInstanceTimer.Check()) { database.PurgeExpiredInstances(); database.PurgeAllDeletedDataBuckets(); + ExpeditionDatabase::PurgeExpiredCharacterLockouts(); } if (EQTimeTimer.Check()) { @@ -614,6 +624,7 @@ int main(int argc, char** argv) { launcher_list.Process(); LFPGroupList.Process(); adventure_manager.Process(); + expedition_state.Process(); if (InterserverTimer.Check()) { InterserverTimer.Start(); diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 5e2268755..f09d08db1 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -36,6 +36,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "ucs.h" #include "queryserv.h" #include "world_store.h" +#include "expedition_message.h" extern ClientList client_list; extern GroupLFPList LFPGroupList; @@ -1355,6 +1356,52 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { cle->ProcessTellQueue(); break; } + case ServerOP_CZClientMessageString: + { + auto buf = reinterpret_cast(pack->pBuffer); + client_list.SendPacket(buf->character_name, pack); + break; + } + case ServerOP_ExpeditionLockout: + case ServerOP_ExpeditionLockoutDuration: + case ServerOP_ExpeditionLockState: + case ServerOP_ExpeditionMemberStatus: + case ServerOP_ExpeditionReplayOnJoin: + case ServerOP_ExpeditionDzCompass: + case ServerOP_ExpeditionDzSafeReturn: + case ServerOP_ExpeditionDzZoneIn: + case ServerOP_ExpeditionExpireWarning: + { + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionChooseNewLeader: + case ServerOP_ExpeditionCreate: + case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionMemberChange: + case ServerOP_ExpeditionMemberSwap: + case ServerOP_ExpeditionMembersRemoved: + case ServerOP_ExpeditionDzAddPlayer: + case ServerOP_ExpeditionDzMakeLeader: + case ServerOP_ExpeditionCharacterLockout: + case ServerOP_ExpeditionSaveInvite: + case ServerOP_ExpeditionRequestInvite: + case ServerOP_ExpeditionSecondsRemaining: + { + ExpeditionMessage::HandleZoneMessage(pack); + break; + } + case ServerOP_DzCharacterChange: + case ServerOP_DzRemoveAllCharacters: + { + auto buf = reinterpret_cast(pack->pBuffer); + ZoneServer* instance_zs = zoneserver_list.FindByInstanceID(buf->instance_id); + if (instance_zs) + { + instance_zs->SendPacket(pack); + } + break; + } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index beacca565..0e1bb877b 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -22,6 +22,7 @@ SET(zone_sources corpse.cpp data_bucket.cpp doors.cpp + dynamiczone.cpp effects.cpp embparser.cpp embparser_api.cpp @@ -30,6 +31,10 @@ SET(zone_sources encounter.cpp entity.cpp exp.cpp + expedition.cpp + expedition_database.cpp + expedition_lockout_timer.cpp + expedition_request.cpp fastmath.cpp fearpath.cpp forage.cpp @@ -48,6 +53,7 @@ SET(zone_sources lua_encounter.cpp lua_entity.cpp lua_entity_list.cpp + lua_expedition.cpp lua_general.cpp lua_group.cpp lua_hate_list.cpp @@ -100,6 +106,7 @@ SET(zone_sources perl_client.cpp perl_doors.cpp perl_entity.cpp + perl_expedition.cpp perl_groups.cpp perl_hateentry.cpp perl_mob.cpp @@ -165,6 +172,7 @@ SET(zone_headers corpse.h data_bucket.h doors.h + dynamiczone.h embparser.h embperl.h embxs.h @@ -172,6 +180,10 @@ SET(zone_headers entity.h errmsg.h event_codes.h + expedition.h + expedition_database.h + expedition_lockout_timer.h + expedition_request.h fastmath.h forage.h global_loot_manager.h @@ -187,6 +199,7 @@ SET(zone_headers lua_encounter.h lua_entity.h lua_entity_list.h + lua_expedition.h lua_general.h lua_group.h lua_hate_list.h diff --git a/zone/client.cpp b/zone/client.cpp index 6338e410f..06f04ec1b 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -40,6 +40,10 @@ extern volatile bool RunLoops; #include "../common/data_verification.h" #include "../common/profanity_manager.h" #include "data_bucket.h" +#include "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" +#include "expedition_request.h" #include "position.h" #include "worldserver.h" #include "zonedb.h" @@ -264,6 +268,7 @@ Client::Client(EQStreamInterface* ieqs) InitializeMercInfo(); SetMerc(0); if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); + dynamiczone_removal_timer.Disable(); //for good measure: memset(&m_pp, 0, sizeof(m_pp)); @@ -3201,6 +3206,27 @@ void Client::MessageString(uint32 type, uint32 string_id, const char* message1, safe_delete(outapp); } +void Client::MessageString(const CZClientMessageString_Struct* msg) +{ + if (msg) + { + if (msg->args_size == 0) + { + MessageString(msg->chat_type, msg->string_id); + } + else + { + uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->args_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_FormattedMessage, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->string_id = msg->string_id; + outbuf->type = msg->chat_type; + memcpy(outbuf->message, msg->args, msg->args_size); + QueuePacket(outapp.get()); + } + } +} + // helper function, returns true if we should see the message bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) { @@ -3397,6 +3423,13 @@ void Client::LinkDead() if(raid){ raid->MemberZoned(this); } + + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::LinkDead); + } + // save_timer.Start(2500); linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); SendAppearancePacket(AT_Linkdead, 1); @@ -6124,21 +6157,19 @@ void Client::CheckEmoteHail(Mob *target, const char* message) void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) { - - auto outapp = new EQApplicationPacket(OP_DzCompass, sizeof(ExpeditionInfo_Struct) + - sizeof(ExpeditionCompassEntry_Struct) * count); - ExpeditionCompass_Struct *ecs = (ExpeditionCompass_Struct*)outapp->pBuffer; - //ecs->clientid = GetID(); - ecs->count = count; - - if (count) { - ecs->entries[0].x = in_x; - ecs->entries[0].y = in_y; - ecs->entries[0].z = in_z; + if (count == 0) + { + m_has_quest_compass = false; + } + else + { + m_has_quest_compass = true; + m_quest_compass.x = in_x; + m_quest_compass.y = in_y; + m_quest_compass.z = in_z; } - FastQueuePacket(&outapp); - safe_delete(outapp); + SendDzCompassUpdate(); } void Client::SendZonePoints() @@ -9459,3 +9490,501 @@ void Client::ShowDevToolsMenu() void Client::SendChatLineBreak(uint16 color) { Message(color, "------------------------------------------------"); } + +void Client::SendCrossZoneMessage( + Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) +{ + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (client) + { + client->Message(chat_type, message.c_str()); + } + else if (!character_name.empty() && !message.empty()) + { + uint32_t pack_size = sizeof(CZMessagePlayer_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_CZMessagePlayer, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->type = chat_type; + strn0cpy(buf->character_name, character_name.c_str(), sizeof(buf->character_name)); + strn0cpy(buf->message, message.c_str(), sizeof(buf->message)); + + worldserver.SendPacket(pack.get()); + } +} + +void Client::SendCrossZoneMessageString( + Client* client, const std::string& character_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& arguments) +{ + // if client is null, falls back to sending a cross zone message by name + if (!client && !character_name.empty()) // double check client isn't in this zone + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (!client && character_name.empty()) + { + return; + } + + SerializeBuffer argument_buffer; + for (const auto& argument : arguments) + { + argument_buffer.WriteString(argument); + } + + uint32_t args_size = static_cast(argument_buffer.size()); + uint32_t pack_size = sizeof(CZClientMessageString_Struct) + args_size; + auto pack = std::unique_ptr(new ServerPacket(ServerOP_CZClientMessageString, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->string_id = string_id; + buf->chat_type = chat_type; + strn0cpy(buf->character_name, character_name.c_str(), sizeof(buf->character_name)); + buf->args_size = args_size; + memcpy(buf->args, argument_buffer.buffer(), argument_buffer.size()); + + if (client) + { + client->MessageString(buf); + } + else + { + worldserver.SendPacket(pack.get()); + } +} + +void Client::UpdateExpeditionInfoAndLockouts() +{ + // this is processed by client after entering a zone + SendDzCompassUpdate(); + + m_expedition_lockouts = ExpeditionDatabase::LoadCharacterLockouts(CharacterID()); + + auto expedition = GetExpedition(); + if (expedition) + { + expedition->SendClientExpeditionInfo(this); + + // live synchronizes lockouts obtained during the active expedition to + // members once they zone into the expedition's dynamic zone instance + if (expedition->GetDynamicZone().IsCurrentZoneDzInstance()) + { + expedition->SyncCharacterLockouts(CharacterID(), m_expedition_lockouts); + expedition->SetMemberStatus(this, ExpeditionMemberStatus::InDynamicZone); + } + else + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Online); + } + } + + SendExpeditionLockoutTimers(); + + // ask world for any pending invite we saved from a previous zone + RequestPendingExpeditionInvite(); +} + +Expedition* Client::CreateExpedition(DynamicZone& dz_instance, ExpeditionRequest& request) +{ + return Expedition::TryCreate(this, dz_instance, request); +} + +Expedition* Client::CreateExpedition( + const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, + uint32 min_players, uint32 max_players, bool disable_messages) +{ + DynamicZone dz_instance{ zone_name, version, duration, DynamicZoneType::Expedition }; + ExpeditionRequest request{ expedition_name, min_players, max_players, disable_messages }; + return Expedition::TryCreate(this, dz_instance, request); +} + +Expedition* Client::GetExpedition() const +{ + if (zone && m_expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; +} + +void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) +{ + // todo: support for account based lockouts like live AoC expeditions + + // if client already has this lockout, we're replacing it with the new one + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& existing_lockout) { + return existing_lockout.IsSameLockout(lockout); + } + ), m_expedition_lockouts.end()); + + m_expedition_lockouts.emplace_back(lockout); + + if (update_db) // for quest api + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }); + } + + SendExpeditionLockoutTimers(); +} + +void Client::AddNewExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) +{ + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, true); +} + +void Client::AddExpeditionLockoutDuration( + const std::string& expedition_name, const std::string& event_name, int seconds, + const std::string& uuid, bool update_db) +{ + auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); + + if (it != m_expedition_lockouts.end()) + { + it->AddLockoutTime(seconds); + + if (!uuid.empty()) + { + it->SetUUID(uuid); + } + + if (update_db) + { + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { *it }); + } + + SendExpeditionLockoutTimers(); + } + else if (seconds > 0) // missing lockouts inserted for reductions would be instantly expired + { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + AddExpeditionLockout(lockout, update_db); + } +} + +void Client::RemoveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool update_db) +{ + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + } + ), m_expedition_lockouts.end()); + + if (update_db) // for quest api + { + ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); + } + + SendExpeditionLockoutTimers(); +} + +void Client::RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db) +{ + if (expedition_name.empty()) + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID()); + } + m_expedition_lockouts.clear(); + } + else + { + if (update_db) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(CharacterID(), expedition_name); + } + + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.GetExpeditionName() == expedition_name; + } + ), m_expedition_lockouts.end()); + } + + SendExpeditionLockoutTimers(); +} + +const ExpeditionLockoutTimer* Client::GetExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) const +{ + for (const auto& expedition_lockout : m_expedition_lockouts) + { + if ((include_expired || !expedition_lockout.IsExpired()) && + expedition_lockout.IsSameLockout(expedition_name, event_name)) + { + return &expedition_lockout; + } + } + return nullptr; +} + +std::vector Client::GetExpeditionLockouts( + const std::string& expedition_name, bool include_expired) +{ + std::vector lockouts; + for (const auto& lockout : m_expedition_lockouts) + { + if ((include_expired || !lockout.IsExpired()) && + lockout.GetExpeditionName() == expedition_name) + { + lockouts.emplace_back(lockout); + } + } + return lockouts; +} + +bool Client::HasExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) +{ + return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); +} + +void Client::SendExpeditionLockoutTimers() +{ + std::vector lockout_entries; + + // client displays lockouts rounded down to nearest minute, send lockouts + // with 60s offset added to compensate (live does this too) + constexpr uint32_t rounding_seconds = 60; + + // erases expired lockouts while building lockout timer list + for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) + { + uint32_t seconds_remaining = it->GetSecondsRemaining(); + if (seconds_remaining == 0) + { + it = m_expedition_lockouts.erase(it); + } + else + { + ExpeditionLockoutTimerEntry_Struct lockout; + strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); + lockout.seconds_remaining = seconds_remaining + rounding_seconds; + lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; + strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); + + lockout_entries.emplace_back(lockout); + ++it; + } + } + + uint32_t lockout_count = static_cast(lockout_entries.size()); + uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; + uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionLockoutTimers, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = lockout_count; + if (!lockout_entries.empty()) + { + memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); + } + QueuePacket(outapp.get()); +} + +void Client::RequestPendingExpeditionInvite() +{ + uint32_t packsize = sizeof(ServerExpeditionCharacterID_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionRequestInvite, packsize)); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->character_id = CharacterID(); + worldserver.SendPacket(pack.get()); +} + +void Client::DzListTimers() +{ + // only lists player's current replay timer lockouts, not all event lockouts + bool found = false; + for (const auto& lockout : m_expedition_lockouts) + { + if (lockout.IsReplayTimer()) + { + found = true; + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + MessageString( + Chat::Yellow, DZLIST_REPLAY_TIMER, + time_remaining.days.c_str(), + time_remaining.hours.c_str(), + time_remaining.mins.c_str(), + lockout.GetExpeditionName().c_str() + ); + } + } + + if (!found) + { + MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); + } +} + +void Client::SetDzRemovalTimer(bool enable_timer) +{ + uint32_t timer_ms = RuleI(DynamicZone, ClientRemovalDelayMS); + + LogDynamicZones( + "Character [{}] instance [{}] removal timer enabled: [{}] delay (ms): [{}]", + CharacterID(), zone ? zone->GetInstanceID() : 0, enable_timer, timer_ms + ); + + if (enable_timer) + { + dynamiczone_removal_timer.Start(timer_ms); + } + else + { + dynamiczone_removal_timer.Disable(); + } +} + +void Client::SendDzCompassUpdate() +{ + // client may be associated with multiple dynamic zone compasses in this zone + std::vector compass_entries; + + for (const auto& client_dz : GetDynamicZones()) + { + auto compass = client_dz.dynamic_zone.GetCompassLocation(); + if (zone && zone->GetZoneID() == compass.zone_id && zone->GetInstanceID() == 0) + { + DynamicZoneCompassEntry_Struct entry; + entry.dz_zone_id = static_cast(client_dz.dynamic_zone.GetZoneID()); + entry.dz_instance_id = static_cast(client_dz.dynamic_zone.GetInstanceID()); + entry.dz_type = static_cast(client_dz.dynamic_zone.GetType()); + entry.x = compass.x; + entry.y = compass.y; + entry.z = compass.z; + + compass_entries.emplace_back(entry); + } + } + + // compass set via MarkSingleCompassLocation() + if (m_has_quest_compass) + { + DynamicZoneCompassEntry_Struct entry; + entry.dz_zone_id = 0; + entry.dz_instance_id = 0; + entry.dz_type = 0; + entry.x = m_quest_compass.x; + entry.y = m_quest_compass.y; + entry.z = m_quest_compass.z; + + compass_entries.emplace_back(entry); + } + + uint32 count = static_cast(compass_entries.size()); + uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzCompass, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + memcpy(outbuf->entries, compass_entries.data(), entries_size); + + QueuePacket(outapp.get()); +} + +void Client::GoToDzSafeReturnOrBind(const DynamicZone& dynamic_zone) +{ + auto safereturn = dynamic_zone.GetSafeReturnLocation(); + LogDynamicZonesDetail( + "Sending character [{}] to safereturn zone [{}] or bind", CharacterID(), safereturn.zone_id + ); + + if (!dynamic_zone.IsValid() || safereturn.zone_id == 0) + { + GoToBind(); + } + else + { + MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading); + } +} + +std::vector Client::GetDynamicZones(uint32_t zone_id, int zone_version) +{ + std::vector client_dzs; + + // check client systems for any associated dynamic zones optionally filtered by zone + Expedition* expedition = GetExpedition(); + if (expedition && + (zone_id == 0 || expedition->GetDynamicZone().GetZoneID() == zone_id) && + (zone_version < 0 || expedition->GetDynamicZone().GetZoneVersion() == zone_version)) + { + client_dzs.emplace_back(expedition->GetName(), expedition->GetLeaderName(), expedition->GetDynamicZone()); + } + + // todo: tasks, missions (shared tasks), and quests with an associated dz to zone_id + + return client_dzs; +} + +void Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) +{ + if (zone_id == 0) + { + return; + } + + auto client_dzs = GetDynamicZones(zone_id, zone_version); + + if (client_dzs.empty()) + { + if (msg_if_invalid) + { + MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message + } + } + else if (client_dzs.size() == 1) + { + const DynamicZone& dz = client_dzs[0].dynamic_zone; + DynamicZoneLocation zonein = dz.GetZoneInLocation(); + ZoneMode zone_mode = dz.HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; + MovePC(zone_id, dz.GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode); + } + else + { + LogDynamicZonesDetail( + "Sending DzSwitchListWnd to character [{}] associated with [{}] dynamic zone(s)", + CharacterID(), client_dzs.size()); + + // more than one dynamic zone to this zone, send out the switchlist window + // note that this will most likely crash clients if they've reloaded the ui + // this occurs on live as well so it may just be a long lasting client bug + uint32 count = static_cast(client_dzs.size()); + uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count; + uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzChooseZone, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->count = count; + for (int i = 0; i < client_dzs.size(); ++i) + { + outbuf->choices[i].dz_zone_id = client_dzs[i].dynamic_zone.GetZoneID(); + outbuf->choices[i].dz_instance_id = client_dzs[i].dynamic_zone.GetInstanceID(); + outbuf->choices[i].dz_type = static_cast(client_dzs[i].dynamic_zone.GetType()); + strn0cpy(outbuf->choices[i].description, client_dzs[i].description.c_str(), sizeof(outbuf->choices[i].description)); + strn0cpy(outbuf->choices[i].leader_name, client_dzs[i].leader_name.c_str(), sizeof(outbuf->choices[i].leader_name)); + } + QueuePacket(outapp.get()); + } +} + +void Client::MovePCDynamicZone(const std::string& zone_name, int zone_version, bool msg_if_invalid) +{ + auto zone_id = ZoneID(zone_name.c_str()); + MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); +} diff --git a/zone/client.h b/zone/client.h index 13f7b81b6..f2b1892c9 100644 --- a/zone/client.h +++ b/zone/client.h @@ -21,12 +21,18 @@ class Client; class EQApplicationPacket; class EQStream; +class DynamicZone; +class Expedition; +class ExpeditionLockoutTimer; +class ExpeditionRequest; class Group; class NPC; class Object; class Raid; class Seperator; class ServerPacket; +struct DynamicZoneInfo; +struct DynamicZoneLocation; enum WaterRegionType : int; namespace EQ @@ -283,6 +289,7 @@ public: uint8 SlotConvert(uint8 slot,bool bracer=false); void MessageString(uint32 type, uint32 string_id, uint32 distance = 0); void MessageString(uint32 type, uint32 string_id, const char* message,const char* message2=0,const char* message3=0,const char* message4=0,const char* message5=0,const char* message6=0,const char* message7=0,const char* message8=0,const char* message9=0, uint32 distance = 0); + void MessageString(const CZClientMessageString_Struct* msg); bool FilteredMessageCheck(Mob *sender, eqFilterType filter); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, @@ -1104,6 +1111,47 @@ public: void MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count=1); + // cross zone client messaging helpers (null client argument will fallback to messaging by name) + static void SendCrossZoneMessage( + Client* client, const std::string& client_name, uint16_t chat_type, const std::string& message); + static void SendCrossZoneMessageString( + Client* client, const std::string& client_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& arguments = {}); + + void AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db = false); + void AddExpeditionLockoutDuration(const std::string& expedition_name, + const std::string& event_Name, int seconds, const std::string& uuid = {}, bool update_db = false); + void AddNewExpeditionLockout(const std::string& expedition_name, + const std::string& event_name, uint32_t duration, std::string uuid = {}); + Expedition* CreateExpedition(DynamicZone& dz_instance, ExpeditionRequest& request); + Expedition* CreateExpedition( + const std::string& zone_name, uint32 version, uint32 duration, const std::string& expedition_name, + uint32 min_players, uint32 max_players, bool disable_messages = false); + Expedition* GetExpedition() const; + uint32 GetExpeditionID() const { return m_expedition_id; } + const ExpeditionLockoutTimer* GetExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired = false) const; + const std::vector& GetExpeditionLockouts() const { return m_expedition_lockouts; }; + std::vector GetExpeditionLockouts(const std::string& expedition_name, bool include_expired = false); + uint32 GetPendingExpeditionInviteID() const { return m_pending_expedition_invite.expedition_id; } + bool HasExpeditionLockout(const std::string& expedition_name, const std::string& event_name, bool include_expired = false); + bool IsInExpedition() const { return m_expedition_id != 0; } + void RemoveAllExpeditionLockouts(const std::string& expedition_name, bool update_db = false); + void RemoveExpeditionLockout(const std::string& expedition_name, + const std::string& event_name, bool update_db = false); + void RequestPendingExpeditionInvite(); + void SendExpeditionLockoutTimers(); + void SetExpeditionID(uint32 expedition_id) { m_expedition_id = expedition_id; }; + void SetPendingExpeditionInvite(ExpeditionInvite&& invite) { m_pending_expedition_invite = invite; } + void UpdateExpeditionInfoAndLockouts(); + void DzListTimers(); + void SetDzRemovalTimer(bool enable_timer); + void SendDzCompassUpdate(); + void GoToDzSafeReturnOrBind(const DynamicZone& dynamic_zone); + void MovePCDynamicZone(uint32 zone_id, int zone_version = -1, bool msg_if_invalid = true); + void MovePCDynamicZone(const std::string& zone_name, int zone_version = -1, bool msg_if_invalid = true); + std::vector GetDynamicZones(uint32_t zone_id = 0, int zone_version = -1); + void CalcItemScale(); bool CalcItemScale(uint32 slot_x, uint32 slot_y); // behavior change: 'slot_y' is now [RANGE]_END and not [RANGE]_END + 1 void DoItemEnterZone(); @@ -1557,6 +1605,7 @@ private: Timer hp_other_update_throttle_timer; /* This is to keep clients from DOSing the server with macros that change client targets constantly */ Timer position_update_timer; /* Timer used when client hasn't updated within a 10 second window */ Timer consent_throttle_timer; + Timer dynamiczone_removal_timer; glm::vec3 m_Proximity; glm::vec4 last_position_before_bulk_update; @@ -1658,6 +1707,12 @@ private: int client_max_level; + uint32 m_expedition_id = 0; + ExpeditionInvite m_pending_expedition_invite { 0 }; + std::vector m_expedition_lockouts; + glm::vec3 m_quest_compass; + bool m_has_quest_compass = false; + #ifdef BOTS public: diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index ed1ec0bdb..12df5cb0e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -49,6 +49,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/zone_numbers.h" #include "data_bucket.h" #include "event_codes.h" +#include "expedition.h" +#include "expedition_database.h" #include "guild_mgr.h" #include "merc.h" #include "petitions.h" @@ -191,6 +193,15 @@ void MapOpcodes() ConnectedOpcodes[OP_DuelResponse2] = &Client::Handle_OP_DuelResponse2; ConnectedOpcodes[OP_DumpName] = &Client::Handle_OP_DumpName; ConnectedOpcodes[OP_Dye] = &Client::Handle_OP_Dye; + ConnectedOpcodes[OP_DzAddPlayer] = &Client::Handle_OP_DzAddPlayer; + ConnectedOpcodes[OP_DzChooseZoneReply] = &Client::Handle_OP_DzChooseZoneReply; + ConnectedOpcodes[OP_DzExpeditionInviteResponse] = &Client::Handle_OP_DzExpeditionInviteResponse; + ConnectedOpcodes[OP_DzListTimers] = &Client::Handle_OP_DzListTimers; + ConnectedOpcodes[OP_DzMakeLeader] = &Client::Handle_OP_DzMakeLeader; + ConnectedOpcodes[OP_DzPlayerList] = &Client::Handle_OP_DzPlayerList; + ConnectedOpcodes[OP_DzRemovePlayer] = &Client::Handle_OP_DzRemovePlayer; + ConnectedOpcodes[OP_DzSwapPlayer] = &Client::Handle_OP_DzSwapPlayer; + ConnectedOpcodes[OP_DzQuit] = &Client::Handle_OP_DzQuit; ConnectedOpcodes[OP_Emote] = &Client::Handle_OP_Emote; ConnectedOpcodes[OP_EndLootRequest] = &Client::Handle_OP_EndLootRequest; ConnectedOpcodes[OP_EnvDamage] = &Client::Handle_OP_EnvDamage; @@ -266,6 +277,7 @@ void MapOpcodes() ConnectedOpcodes[OP_ItemViewUnknown] = &Client::Handle_OP_Ignore; ConnectedOpcodes[OP_Jump] = &Client::Handle_OP_Jump; ConnectedOpcodes[OP_KeyRing] = &Client::Handle_OP_KeyRing; + ConnectedOpcodes[OP_KickPlayers] = &Client::Handle_OP_KickPlayers; ConnectedOpcodes[OP_LDoNButton] = &Client::Handle_OP_LDoNButton; ConnectedOpcodes[OP_LDoNDisarmTraps] = &Client::Handle_OP_LDoNDisarmTraps; ConnectedOpcodes[OP_LDoNInspect] = &Client::Handle_OP_LDoNInspect; @@ -885,6 +897,8 @@ void Client::CompleteConnect() guild_mgr.RequestOnlineGuildMembers(this->CharacterID(), this->GuildID()); } + UpdateExpeditionInfoAndLockouts(); + /** Request adventure info **/ auto pack = new ServerPacket(ServerOP_AdventureDataRequest, 64); strcpy((char*)pack->pBuffer, GetName()); @@ -1701,6 +1715,8 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) /* Task Packets */ LoadClientTaskState(); + m_expedition_id = ExpeditionDatabase::GetExpeditionIDFromCharacterID(CharacterID()); + /** * DevTools Load Settings */ @@ -5604,6 +5620,116 @@ void Client::Handle_OP_Dye(const EQApplicationPacket *app) return; } +void Client::Handle_OP_DzAddPlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzAddPlayer(this, dzcmd->name); + } + else + { + // the only /dz command that sends an error message if no active expedition + Message(Chat::System, DZ_YOU_NOT_ASSIGNED); + } +} + +void Client::Handle_OP_DzChooseZoneReply(const EQApplicationPacket *app) +{ + auto dzmsg = reinterpret_cast(app->pBuffer); + LogDynamicZones( + "Character [{}] chose DynamicZone [{}]:[{}] type: [{}] with system id: [{}]", + CharacterID(), dzmsg->dz_zone_id, dzmsg->dz_instance_id, dzmsg->dz_type, dzmsg->unknown_id2 + ); + + if (!dzmsg->dz_instance_id || !database.VerifyInstanceAlive(dzmsg->dz_instance_id, CharacterID())) + { + // live just no-ops this without a message + LogDynamicZones( + "Character [{}] chose invalid DynamicZone [{}]:[{}] or is no longer a member", + CharacterID(), dzmsg->dz_zone_id, dzmsg->dz_instance_id + ); + return; + } + + auto client_dzs = GetDynamicZones(); + auto it = std::find_if(client_dzs.begin(), client_dzs.end(), [&](const DynamicZoneInfo dz_info) { + return dz_info.dynamic_zone.IsSameDz(dzmsg->dz_zone_id, dzmsg->dz_instance_id); + }); + + if (it != client_dzs.end()) + { + DynamicZoneLocation loc = it->dynamic_zone.GetZoneInLocation(); + ZoneMode zone_mode = it->dynamic_zone.HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords; + MovePC(dzmsg->dz_zone_id, dzmsg->dz_instance_id, loc.x, loc.y, loc.z, loc.heading, 0, zone_mode); + } +} + +void Client::Handle_OP_DzExpeditionInviteResponse(const EQApplicationPacket *app) +{ + auto expedition = Expedition::FindCachedExpeditionByID(m_pending_expedition_invite.expedition_id); + std::string swap_remove_name = m_pending_expedition_invite.swap_remove_name; + m_pending_expedition_invite = { 0 }; // clear before re-validating + + if (expedition) + { + auto dzmsg = reinterpret_cast(app->pBuffer); + expedition->DzInviteResponse(this, dzmsg->accepted, swap_remove_name); + } +} + +void Client::Handle_OP_DzListTimers(const EQApplicationPacket *app) +{ + DzListTimers(); +} + +void Client::Handle_OP_DzMakeLeader(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzMakeLeader(this, dzcmd->name); + } +} + +void Client::Handle_OP_DzPlayerList(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) { + expedition->DzPlayerList(this); + } +} + +void Client::Handle_OP_DzRemovePlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzRemovePlayer(this, dzcmd->name); + } +} + +void Client::Handle_OP_DzSwapPlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzSwapPlayer(this, dzcmd->rem_player_name, dzcmd->add_player_name); + } +} + +void Client::Handle_OP_DzQuit(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) { + expedition->DzQuit(this); + } +} + void Client::Handle_OP_Emote(const EQApplicationPacket *app) { if (app->size != sizeof(Emote_Struct)) { @@ -8874,6 +9000,23 @@ void Client::Handle_OP_KeyRing(const EQApplicationPacket *app) KeyRingList(); } +void Client::Handle_OP_KickPlayers(const EQApplicationPacket *app) +{ + auto buf = reinterpret_cast(app->pBuffer); + if (buf->kick_expedition) + { + auto expedition = GetExpedition(); + if (expedition) + { + expedition->DzKickPlayers(this); + } + } + else if (buf->kick_task) + { + // todo: shared tasks + } +} + void Client::Handle_OP_LDoNButton(const EQApplicationPacket *app) { if (app->size < sizeof(bool)) diff --git a/zone/client_packet.h b/zone/client_packet.h index 9605dc8cf..3951a1761 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -101,6 +101,15 @@ void Handle_OP_DuelResponse2(const EQApplicationPacket *app); void Handle_OP_DumpName(const EQApplicationPacket *app); void Handle_OP_Dye(const EQApplicationPacket *app); + void Handle_OP_DzAddPlayer(const EQApplicationPacket *app); + void Handle_OP_DzChooseZoneReply(const EQApplicationPacket *app); + void Handle_OP_DzExpeditionInviteResponse(const EQApplicationPacket *app); + void Handle_OP_DzListTimers(const EQApplicationPacket *app); + void Handle_OP_DzMakeLeader(const EQApplicationPacket *app); + void Handle_OP_DzPlayerList(const EQApplicationPacket *app); + void Handle_OP_DzRemovePlayer(const EQApplicationPacket *app); + void Handle_OP_DzSwapPlayer(const EQApplicationPacket *app); + void Handle_OP_DzQuit(const EQApplicationPacket *app); void Handle_OP_Emote(const EQApplicationPacket *app); void Handle_OP_EndLootRequest(const EQApplicationPacket *app); void Handle_OP_EnvDamage(const EQApplicationPacket *app); @@ -174,6 +183,7 @@ void Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app); void Handle_OP_Jump(const EQApplicationPacket *app); void Handle_OP_KeyRing(const EQApplicationPacket *app); + void Handle_OP_KickPlayers(const EQApplicationPacket *app); void Handle_OP_LDoNButton(const EQApplicationPacket *app); void Handle_OP_LDoNDisarmTraps(const EQApplicationPacket *app); void Handle_OP_LDoNInspect(const EQApplicationPacket *app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index ba62d470d..15dfa160f 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -44,6 +44,7 @@ #include "../common/spdat.h" #include "../common/string_util.h" #include "event_codes.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "petitions.h" @@ -160,6 +161,12 @@ bool Client::Process() { if (TaskPeriodic_Timer.Check() && taskstate) taskstate->TaskPeriodicChecks(this); + if (dynamiczone_removal_timer.Check() && zone && zone->GetInstanceID() != 0) + { + dynamiczone_removal_timer.Disable(); + GoToDzSafeReturnOrBind(zone->GetDynamicZone()); + } + if (linkdead_timer.Check()) { LeaveGroup(); Save(); @@ -172,6 +179,13 @@ bool Client::Process() { if (myraid) { myraid->MemberZoned(this); } + + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Offline); + } + return false; //delete client } @@ -560,6 +574,12 @@ bool Client::Process() { client_state = CLIENT_LINKDEAD; AI_Start(CLIENT_LD_TIMEOUT); SendAppearancePacket(AT_Linkdead, 1); + + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::LinkDead); + } } } @@ -691,6 +711,12 @@ void Client::OnDisconnect(bool hard_disconnect) { } } + Expedition* expedition = GetExpedition(); + if (expedition && !bZoning) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Offline); + } + RemoveAllAuras(); Mob *Other = trade->With(); diff --git a/zone/command.cpp b/zone/command.cpp index d56325725..2dc55221c 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -60,6 +60,7 @@ #include "data_bucket.h" #include "command.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "qglobals.h" @@ -198,6 +199,8 @@ int command_init(void) command_add("disarmtrap", "Analog for ldon disarm trap for the newer clients since we still don't have it working.", 80, command_disarmtrap) || command_add("distance", "- Reports the distance between you and your target.", 80, command_distance) || command_add("doanim", "[animnum] [type] - Send an EmoteAnim for you or your target", 50, command_doanim) || + command_add("dz", "Manage expeditions and dynamic zone instances", 80, command_dz) || + command_add("dzkickplayers", "Removes all players from current expedition. (/kickplayers alternative for pre-RoF clients)", 0, command_dzkickplayers) || command_add("editmassrespawn", "[name-search] [second-value] - Mass (Zone wide) NPC respawn timer editing command", 100, command_editmassrespawn) || command_add("emote", "['name'/'world'/'zone'] [type] [message] - Send an emote message", 80, command_emote) || command_add("emotesearch", "Searches NPC Emotes", 80, command_emotesearch) || @@ -6825,6 +6828,210 @@ void command_doanim(Client *c, const Seperator *sep) c->DoAnim(atoi(sep->arg[1]),atoi(sep->arg[2])); } +void command_dz(Client* c, const Seperator* sep) +{ + if (!c || !zone) { + return; + } + + if (strcasecmp(sep->arg[1], "expedition") == 0) + { + if (strcasecmp(sep->arg[2], "list") == 0) + { + std::vector expeditions; + for (const auto& expedition : zone->expedition_cache) + { + expeditions.emplace_back(expedition.second.get()); + } + + std::sort(expeditions.begin(), expeditions.end(), + [](const Expedition* lhs, const Expedition* rhs) { + return lhs->GetID() < rhs->GetID(); + }); + + c->Message(Chat::White, fmt::format("Total Active Expeditions: [{}]", expeditions.size()).c_str()); + for (const auto& expedition : expeditions) + { + auto leader_saylink = EQ::SayLinkEngine::GenerateQuestSaylink(fmt::format( + "#goto {}", expedition->GetLeaderName()), false, expedition->GetLeaderName()); + auto zone_saylink = EQ::SayLinkEngine::GenerateQuestSaylink(fmt::format( + "#zoneinstance {}", expedition->GetInstanceID()), false, "zone"); + + auto seconds = expedition->GetDynamicZone().GetSecondsRemaining(); + + c->Message(Chat::White, fmt::format( + "expedition id: [{}] dz id: [{}] name: [{}] leader: [{}] {}: [{}]:[{}]:[{}]:[{}] members: [{}] remaining: [{:02}:{:02}:{:02}]", + expedition->GetID(), + expedition->GetDynamicZoneID(), + expedition->GetName(), + leader_saylink, + zone_saylink, + ZoneName(expedition->GetDynamicZone().GetZoneID()), + expedition->GetDynamicZone().GetZoneID(), + expedition->GetInstanceID(), + expedition->GetDynamicZone().GetZoneVersion(), + expedition->GetMemberCount(), + seconds / 3600, // hours + (seconds / 60) % 60, // minutes + seconds % 60 // seconds + ).c_str()); + } + } + else if (strcasecmp(sep->arg[2], "reload") == 0) + { + Expedition::CacheAllFromDatabase(); + c->Message(Chat::White, fmt::format( + "Reloaded [{}] expeditions to cache from database.", zone->expedition_cache.size() + ).c_str()); + } + else if (strcasecmp(sep->arg[2], "destroy") == 0 && sep->IsNumber(3)) + { + auto expedition_id = std::strtoul(sep->arg[3], nullptr, 10); + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + c->Message(Chat::White, fmt::format("Destroying expedition [{}] ({})", + expedition_id, expedition->GetName()).c_str()); + expedition->RemoveAllMembers(); + } + else + { + c->Message(Chat::Red, fmt::format("Failed to destroy expedition [{}]", sep->arg[3]).c_str()); + } + } + else if (strcasecmp(sep->arg[2], "unlock") == 0 && sep->IsNumber(3)) + { + auto expedition_id = std::strtoul(sep->arg[3], nullptr, 10); + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + c->Message(Chat::White, fmt::format("Unlocking expedition [{}]", expedition_id).c_str()); + expedition->SetLocked(false, ExpeditionLockMessage::None, true); + } + else + { + c->Message(Chat::Red, fmt::format("Failed to find expedition [{}]", sep->arg[3]).c_str()); + } + } + } + else if (strcasecmp(sep->arg[1], "list") == 0) + { + std::string query = SQL( + SELECT + dynamic_zones.id, + dynamic_zones.type, + instance_list.id, + instance_list.zone, + instance_list.version, + instance_list.start_time, + instance_list.duration, + COUNT(instance_list_player.id) member_count + FROM dynamic_zones + INNER JOIN instance_list ON dynamic_zones.instance_id = instance_list.id + LEFT JOIN instance_list_player ON instance_list.id = instance_list_player.id + GROUP BY instance_list.id + ORDER BY dynamic_zones.id; + ); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + c->Message(Chat::White, fmt::format("Total Dynamic Zones: [{}]", results.RowCount()).c_str()); + for (auto row = results.begin(); row != results.end(); ++row) + { + auto start_time = strtoul(row[5], nullptr, 10); + auto duration = strtoul(row[6], nullptr, 10); + auto expire_time = std::chrono::system_clock::from_time_t(start_time + duration); + + auto now = std::chrono::system_clock::now(); + auto remaining = std::chrono::duration_cast(expire_time - now); + auto seconds = std::max(0, static_cast(remaining.count())); + + bool is_expired = now > expire_time; + if (!is_expired || strcasecmp(sep->arg[2], "all") == 0) + { + uint32_t instance_id = strtoul(row[2], nullptr, 10); + auto zone_saylink = is_expired ? "zone" : EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#zoneinstance {}", instance_id), false, "zone"); + + c->Message(Chat::White, fmt::format( + "dz id: [{}] type: [{}] {}: [{}]:[{}]:[{}] members: [{}] remaining: [{:02}:{:02}:{:02}]", + strtoul(row[0], nullptr, 10), // dynamic_zone_id + strtoul(row[1], nullptr, 10), // dynamic_zone_type + zone_saylink, + strtoul(row[3], nullptr, 10), // instance_zone_id + instance_id, // instance_id + strtoul(row[4], nullptr, 10), // instance_zone_version + strtoul(row[7], nullptr, 10), // instance member_count + seconds / 3600, // hours + (seconds / 60) % 60, // minutes + seconds % 60 // seconds + ).c_str()); + } + } + } + } + else if (strcasecmp(sep->arg[1], "lockouts") == 0) + { + if (strcasecmp(sep->arg[2], "remove") == 0 && sep->arg[3][0] != '\0') + { + if (sep->arg[5][0] == '\0') + { + c->Message(Chat::White, fmt::format( + "Removing [{}] lockouts on [{}].", sep->arg[4][0] ? sep->arg[4] : "all", sep->arg[3] + ).c_str()); + } + else + { + c->Message(Chat::White, fmt::format( + "Removing [{}]:[{}] lockout on [{}].", sep->arg[4], sep->arg[5], sep->arg[3] + ).c_str()); + } + Expedition::RemoveLockoutsByCharacterName(sep->arg[3], sep->arg[4], sep->arg[5]); + } + } + else if (strcasecmp(sep->arg[1], "makeleader") == 0 && sep->IsNumber(2) && sep->arg[3][0] != '\0') + { + auto expedition_id = std::strtoul(sep->arg[2], nullptr, 10); + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + auto char_name = FormatName(sep->arg[3]); + c->Message(Chat::White, fmt::format("Setting expedition [{}] leader to [{}]", expedition_id, char_name).c_str()); + expedition->SendWorldMakeLeaderRequest(c->CharacterID(), char_name); + } + else + { + c->Message(Chat::Red, fmt::format("Failed to find expedition [{}]", expedition_id).c_str()); + } + } + else + { + c->Message(Chat::White, "#dz usage:"); + c->Message(Chat::White, "#dz expedition list - list expeditions in current zone cache"); + c->Message(Chat::White, "#dz expedition reload - reload expedition zone cache from database"); + c->Message(Chat::White, "#dz expedition destroy - destroy expedition globally (must be in cache)"); + c->Message(Chat::White, "#dz expedition unlock - unlock expedition"); + c->Message(Chat::White, "#dz list [all] - list dynamic zone instances from database -- 'all' includes expired"); + c->Message(Chat::White, "#dz lockouts remove - delete all of character's expedition lockouts"); + c->Message(Chat::White, "#dz lockouts remove \"\" - delete lockouts by expedition"); + c->Message(Chat::White, "#dz lockouts remove \"\" \"\" - delete lockout by expedition event"); + c->Message(Chat::White, "#dz makeleader - set new expedition leader"); + } +} + +void command_dzkickplayers(Client* c, const Seperator* sep) +{ + if (c) + { + auto expedition = c->GetExpedition(); + if (expedition) + { + expedition->DzKickPlayers(c); + } + } +} + void command_editmassrespawn(Client* c, const Seperator* sep) { if (strcasecmp(sep->arg[1], "usage") == 0) { diff --git a/zone/command.h b/zone/command.h index 87e01ae49..a6f967d78 100644 --- a/zone/command.h +++ b/zone/command.h @@ -92,6 +92,8 @@ void command_disablerecipe(Client *c, const Seperator *sep); void command_disarmtrap(Client *c, const Seperator *sep); void command_distance(Client *c, const Seperator *sep); void command_doanim(Client *c, const Seperator *sep); +void command_dz(Client *c, const Seperator *sep); +void command_dzkickplayers(Client *c, const Seperator *sep); void command_editmassrespawn(Client* c, const Seperator* sep); void command_emote(Client *c, const Seperator *sep); void command_emotesearch(Client* c, const Seperator *sep); diff --git a/zone/common.h b/zone/common.h index 19df1a576..c05c6a95b 100644 --- a/zone/common.h +++ b/zone/common.h @@ -786,5 +786,12 @@ struct DamageHitInfo { EQ::skills::SkillType skill; }; +struct ExpeditionInvite +{ + uint32_t expedition_id; + std::string inviter_name; + std::string swap_remove_name; +}; + #endif diff --git a/zone/corpse.cpp b/zone/corpse.cpp index e0e1d1992..e04fd6a9d 100644 --- a/zone/corpse.cpp +++ b/zone/corpse.cpp @@ -38,6 +38,7 @@ Child of the Mob class. #include "corpse.h" #include "entity.h" +#include "expedition.h" #include "groups.h" #include "mob.h" #include "raids.h" @@ -1275,6 +1276,20 @@ void Corpse::LootItem(Client *client, const EQApplicationPacket *app) return; } + if (zone && zone->GetInstanceID() != 0) + { + // expeditions may prevent looting based on client's lockouts + auto expedition = Expedition::FindCachedExpeditionByZoneInstance(zone->GetZoneID(), zone->GetInstanceID()); + if (expedition && !expedition->CanClientLootCorpse(client, GetNPCTypeID(), GetID())) + { + client->MessageString(Chat::Red, LOOT_NOT_ALLOWED, inst->GetItem()->Name); + client->QueuePacket(app); + SendEndLootErrorPacket(client); + ResetLooter(); + delete inst; + return; + } + } // do we want this to have a fail option too? parse->EventItem(EVENT_LOOT, client, inst, this, buf, 0); diff --git a/zone/doors.cpp b/zone/doors.cpp index ddaa5709e..ba3d36d21 100644 --- a/zone/doors.cpp +++ b/zone/doors.cpp @@ -207,6 +207,8 @@ void Doors::HandleClick(Client* sender, uint8 trigger) { } } + // todo: if IsDzDoor() call Client::MovePCDynamicZone(target_zone_id) (for systems that use dzs) + uint32 required_key_item = GetKeyItem(); uint8 disable_add_to_key_ring = GetNoKeyring(); uint32 player_has_key = 0; diff --git a/zone/dynamiczone.cpp b/zone/dynamiczone.cpp new file mode 100644 index 000000000..9eeb7596d --- /dev/null +++ b/zone/dynamiczone.cpp @@ -0,0 +1,544 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "dynamiczone.h" +#include "client.h" +#include "worldserver.h" +#include "zonedb.h" +#include "../common/eqemu_logsys.h" + +extern WorldServer worldserver; + +DynamicZone::DynamicZone( + uint32_t zone_id, uint32_t version, uint32_t duration, DynamicZoneType type +) : + m_zone_id(zone_id), + m_version(version), + m_duration(duration), + m_type(type) +{ +} + +DynamicZone::DynamicZone( + std::string zone_shortname, uint32_t version, uint32_t duration, DynamicZoneType type +) : + m_version(version), + m_duration(duration), + m_type(type) +{ + m_zone_id = ZoneID(zone_shortname.c_str()); + + if (!m_zone_id) + { + LogDynamicZones("Failed to get zone id for zone [{}]", zone_shortname); + } +} + +std::unordered_map DynamicZone::LoadMultipleDzFromDatabase( + const std::vector& dynamic_zone_ids) +{ + LogDynamicZonesDetail("Loading dynamic zone data for [{}] instances", dynamic_zone_ids.size()); + + std::string in_dynamic_zone_ids_query = fmt::format("{}", fmt::join(dynamic_zone_ids, ",")); + + std::unordered_map dynamic_zones; + + if (!in_dynamic_zone_ids_query.empty()) + { + std::string query = fmt::format(SQL( + {} WHERE dynamic_zones.id IN ({}); + ), DynamicZoneSelectQuery(), in_dynamic_zone_ids_query); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + DynamicZone dz; + dz.LoadDatabaseResult(row); + dynamic_zones.emplace(dz.GetID(), dz); + } + } + } + + return dynamic_zones; +} + +uint32_t DynamicZone::Create() +{ + if (m_id != 0) + { + return m_id; + } + + if (GetInstanceID() == 0) + { + CreateInstance(); + } + + m_id = SaveToDatabase(); + + return m_id; +} + +uint32_t DynamicZone::CreateInstance() +{ + if (m_instance_id) + { + LogDynamicZones("CreateInstance failed, instance id [{}] already created", m_instance_id); + return 0; + } + + if (!m_zone_id) + { + LogDynamicZones("CreateInstance failed, invalid zone id [{}]", m_zone_id); + return 0; + } + + uint16_t instance_id = 0; + if (!database.GetUnusedInstanceID(instance_id)) // todo: doesn't this race with insert? + { + LogDynamicZones("Failed to find unused instance id"); + return 0; + } + + m_start_time = std::chrono::system_clock::now(); + auto start_time = std::chrono::system_clock::to_time_t(m_start_time); + + std::string query = fmt::format(SQL( + INSERT INTO instance_list + (id, zone, version, start_time, duration) + VALUES + ({}, {}, {}, {}, {}) + ), instance_id, m_zone_id, m_version, start_time, m_duration.count()); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogDynamicZones("Failed to create instance [{}] for Dynamic Zone [{}]", instance_id, m_zone_id); + return 0; + } + + m_instance_id = instance_id; + m_never_expires = false; + m_expire_time = m_start_time + m_duration; + + return m_instance_id; +} + +std::string DynamicZone::DynamicZoneSelectQuery() +{ + return std::string(SQL( + SELECT + instance_list.id, + instance_list.zone, + instance_list.version, + instance_list.start_time, + instance_list.duration, + instance_list.never_expires, + dynamic_zones.id, + dynamic_zones.type, + dynamic_zones.compass_zone_id, + dynamic_zones.compass_x, + dynamic_zones.compass_y, + dynamic_zones.compass_z, + dynamic_zones.safe_return_zone_id, + dynamic_zones.safe_return_x, + dynamic_zones.safe_return_y, + dynamic_zones.safe_return_z, + dynamic_zones.safe_return_heading, + dynamic_zones.zone_in_x, + dynamic_zones.zone_in_y, + dynamic_zones.zone_in_z, + dynamic_zones.zone_in_heading, + dynamic_zones.has_zone_in + FROM dynamic_zones + INNER JOIN instance_list ON dynamic_zones.instance_id = instance_list.id + )); +} + +void DynamicZone::LoadDatabaseResult(MySQLRequestRow& row) +{ + m_instance_id = strtoul(row[0], nullptr, 10); + m_zone_id = strtoul(row[1], nullptr, 10); + m_version = strtoul(row[2], nullptr, 10); + m_start_time = std::chrono::system_clock::from_time_t(strtoul(row[3], nullptr, 10)); + m_duration = std::chrono::seconds(strtoul(row[4], nullptr, 10)); + m_expire_time = m_start_time + m_duration; + m_never_expires = (strtoul(row[5], nullptr, 10) != 0); + m_id = strtoul(row[6], nullptr, 10); + m_type = static_cast(strtoul(row[7], nullptr, 10)); + m_compass.zone_id = strtoul(row[8], nullptr, 10); + m_compass.x = strtof(row[9], nullptr); + m_compass.y = strtof(row[10], nullptr); + m_compass.z = strtof(row[11], nullptr); + m_safereturn.zone_id = strtoul(row[12], nullptr, 10); + m_safereturn.x = strtof(row[13], nullptr); + m_safereturn.y = strtof(row[14], nullptr); + m_safereturn.z = strtof(row[15], nullptr); + m_safereturn.heading = strtof(row[16], nullptr); + m_zonein.x = strtof(row[17], nullptr); + m_zonein.y = strtof(row[18], nullptr); + m_zonein.z = strtof(row[19], nullptr); + m_zonein.heading = strtof(row[20], nullptr); + m_has_zonein = (strtoul(row[21], nullptr, 10) != 0); +} + +uint32_t DynamicZone::SaveToDatabase() +{ + LogDynamicZonesDetail("Saving dz instance [{}] to database", m_instance_id); + + if (m_instance_id != 0) + { + std::string query = fmt::format(SQL( + INSERT INTO dynamic_zones + ( + instance_id, + type, + compass_zone_id, + compass_x, + compass_y, + compass_z, + safe_return_zone_id, + safe_return_x, + safe_return_y, + safe_return_z, + safe_return_heading, + zone_in_x, + zone_in_y, + zone_in_z, + zone_in_heading, + has_zone_in + ) + VALUES + ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}); + ), + m_instance_id, + static_cast(m_type), + m_compass.zone_id, + m_compass.x, + m_compass.y, + m_compass.z, + m_safereturn.zone_id, + m_safereturn.x, + m_safereturn.y, + m_safereturn.z, + m_safereturn.heading, + m_zonein.x, + m_zonein.y, + m_zonein.z, + m_zonein.heading, + m_has_zonein + ); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + return results.LastInsertedID(); + } + } + return 0; +} + +void DynamicZone::SaveCompassToDatabase() +{ + LogDynamicZonesDetail( + "Instance [{}] saving compass zone: [{}] xyz: ([{}], [{}], [{}])", + m_instance_id, m_compass.zone_id, m_compass.x, m_compass.y, m_compass.z + ); + + if (m_instance_id != 0) + { + std::string query = fmt::format(SQL( + UPDATE dynamic_zones SET + compass_zone_id = {}, + compass_x = {}, + compass_y = {}, + compass_z = {} + WHERE instance_id = {}; + ), + m_compass.zone_id, + m_compass.x, + m_compass.y, + m_compass.z, + m_instance_id + ); + + database.QueryDatabase(query); + } +} + +void DynamicZone::SaveSafeReturnToDatabase() +{ + LogDynamicZonesDetail( + "Instance [{}] saving safereturn zone: [{}] xyzh: ([{}], [{}], [{}], [{}])", + m_instance_id, m_safereturn.zone_id, m_safereturn.x, m_safereturn.y, m_safereturn.z, m_safereturn.heading + ); + + if (m_instance_id != 0) + { + std::string query = fmt::format(SQL( + UPDATE dynamic_zones SET + safe_return_zone_id = {}, + safe_return_x = {}, + safe_return_y = {}, + safe_return_z = {}, + safe_return_heading = {} + WHERE instance_id = {}; + ), + m_safereturn.zone_id, + m_safereturn.x, + m_safereturn.y, + m_safereturn.z, + m_safereturn.heading, + m_instance_id + ); + + database.QueryDatabase(query); + } +} + +void DynamicZone::SaveZoneInLocationToDatabase() +{ + LogDynamicZonesDetail( + "Instance [{}] saving zonein zone: [{}] xyzh: ([{}], [{}], [{}], [{}]) has: [{}]", + m_instance_id, m_zone_id, m_zonein.x, m_zonein.y, m_zonein.z, m_zonein.heading, m_has_zonein + ); + + if (m_instance_id != 0) + { + std::string query = fmt::format(SQL( + UPDATE dynamic_zones SET + zone_in_x = {}, + zone_in_y = {}, + zone_in_z = {}, + zone_in_heading = {}, + has_zone_in = {} + WHERE instance_id = {}; + ), + m_zonein.x, + m_zonein.y, + m_zonein.z, + m_zonein.heading, + m_has_zonein, + m_instance_id + ); + + database.QueryDatabase(query); + } +} + +void DynamicZone::AddCharacter(uint32_t character_id) +{ + database.AddClientToInstance(m_instance_id, character_id); + SendInstanceCharacterChange(character_id, false); // stops client kick timer +} + +void DynamicZone::RemoveCharacter(uint32_t character_id) +{ + database.RemoveClientFromInstance(m_instance_id, character_id); + SendInstanceCharacterChange(character_id, true); // start client kick timer +} + +void DynamicZone::RemoveAllCharacters(bool enable_removal_timers) +{ + if (GetInstanceID() == 0) + { + return; + } + + if (enable_removal_timers) + { + // just remove all clients in bulk instead of only characters assigned to the instance + if (IsCurrentZoneDzInstance()) + { + for (const auto& client_iter : entity_list.GetClientList()) + { + if (client_iter.second) + { + client_iter.second->SetDzRemovalTimer(true); + } + } + } + else if (GetInstanceID() != 0) + { + uint32_t packsize = sizeof(ServerDzCharacter_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_DzRemoveAllCharacters, packsize)); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->zone_id = GetZoneID(); + packbuf->instance_id = GetInstanceID(); + packbuf->remove = true; + packbuf->character_id = 0; + worldserver.SendPacket(pack.get()); + } + } + + database.RemoveClientsFromInstance(GetInstanceID()); +} + +void DynamicZone::SaveInstanceMembersToDatabase(const std::vector& character_ids) +{ + LogDynamicZonesDetail("Saving [{}] instance members to database", character_ids.size()); + + std::string insert_values; + for (const auto& character_id : character_ids) + { + fmt::format_to(std::back_inserter(insert_values), "({}, {}),", m_instance_id, character_id); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + std::string query = fmt::format(SQL( + REPLACE INTO instance_list_player (id, charid) VALUES {}; + ), insert_values); + + database.QueryDatabase(query); + } +} + +void DynamicZone::SendInstanceCharacterChange(uint32_t character_id, bool removed) +{ + // if removing, sets removal timer on client inside the instance + if (IsCurrentZoneDzInstance()) + { + Client* client = entity_list.GetClientByCharID(character_id); + if (client) + { + client->SetDzRemovalTimer(removed); + } + } + else if (GetInstanceID() != 0) + { + uint32_t packsize = sizeof(ServerDzCharacter_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_DzCharacterChange, packsize)); + auto packbuf = reinterpret_cast(pack->pBuffer); + packbuf->zone_id = GetZoneID(); + packbuf->instance_id = GetInstanceID(); + packbuf->remove = removed; + packbuf->character_id = character_id; + worldserver.SendPacket(pack.get()); + } +} + +void DynamicZone::SetCompass(const DynamicZoneLocation& location, bool update_db) +{ + m_compass = location; + + if (update_db) + { + SaveCompassToDatabase(); + } +} + +void DynamicZone::SetSafeReturn(const DynamicZoneLocation& location, bool update_db) +{ + m_safereturn = location; + + if (update_db) + { + SaveSafeReturnToDatabase(); + } +} + +void DynamicZone::SetZoneInLocation(const DynamicZoneLocation& location, bool update_db) +{ + m_zonein = location; + m_has_zonein = true; + + if (update_db) + { + SaveZoneInLocationToDatabase(); + } +} + +bool DynamicZone::IsCurrentZoneDzInstance() const +{ + return (zone && zone->GetInstanceID() != 0 && zone->GetInstanceID() == GetInstanceID()); +} + +bool DynamicZone::IsInstanceID(uint32_t instance_id) const +{ + return (GetInstanceID() != 0 && GetInstanceID() == instance_id); +} + +bool DynamicZone::IsSameDz(uint32_t zone_id, uint32_t instance_id) const +{ + return zone_id == m_zone_id && instance_id == m_instance_id; +} + +uint32_t DynamicZone::GetSecondsRemaining() const +{ + auto now = std::chrono::system_clock::now(); + if (m_expire_time > now) + { + auto remaining = m_expire_time - now; + return static_cast(std::chrono::duration_cast(remaining).count()); + } + return 0; +} + +void DynamicZone::SetUpdatedDuration(uint32_t new_duration) +{ + // preserves original start time, just modifies duration and expire time + m_duration = std::chrono::seconds(new_duration); + m_expire_time = m_start_time + m_duration; + + LogDynamicZones("Updated zone [{}]:[{}] seconds remaining: [{}]", + m_zone_id, m_instance_id, GetSecondsRemaining()); + + if (zone && IsCurrentZoneDzInstance()) + { + zone->SetInstanceTimer(GetSecondsRemaining()); + } +} + +void DynamicZone::HandleWorldMessage(ServerPacket* pack) +{ + switch (pack->opcode) + { + case ServerOP_DzCharacterChange: + { + auto buf = reinterpret_cast(pack->pBuffer); + Client* client = entity_list.GetClientByCharID(buf->character_id); + if (client) + { + client->SetDzRemovalTimer(buf->remove); // instance kick timer + } + break; + } + case ServerOP_DzRemoveAllCharacters: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (buf->remove) + { + for (const auto& client_list_iter : entity_list.GetClientList()) + { + if (client_list_iter.second) + { + client_list_iter.second->SetDzRemovalTimer(true); + } + } + } + break; + } + } +} diff --git a/zone/dynamiczone.h b/zone/dynamiczone.h new file mode 100644 index 000000000..d519539e0 --- /dev/null +++ b/zone/dynamiczone.h @@ -0,0 +1,132 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef DYNAMICZONE_H +#define DYNAMICZONE_H + +#include +#include +#include +#include +#include + +class MySQLRequestRow; +class ServerPacket; + +enum class DynamicZoneType : uint8_t +{ + None = 0, + Expedition, + Tutorial, + Task, + Mission, // Shared Task + Quest +}; + +struct DynamicZoneLocation +{ + uint32_t zone_id = 0; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float heading = 0.0f; + + DynamicZoneLocation() = default; + DynamicZoneLocation(uint32_t zone_id_, float x_, float y_, float z_, float heading_) + : zone_id(zone_id_), x(x_), y(y_), z(z_), heading(heading_) {} +}; + +class DynamicZone +{ +public: + DynamicZone() = default; + DynamicZone(uint32_t zone_id, uint32_t version, uint32_t duration, DynamicZoneType type); + DynamicZone(std::string zone_shortname, uint32_t version, uint32_t duration, DynamicZoneType type); + DynamicZone(uint32_t dz_id) : m_id(dz_id) {} + DynamicZone(DynamicZoneType type) : m_type(type) {} + + static std::unordered_map LoadMultipleDzFromDatabase( + const std::vector& dynamic_zone_ids); + static void HandleWorldMessage(ServerPacket* pack); + + uint64_t GetExpireTime() const { return std::chrono::system_clock::to_time_t(m_expire_time); } + uint32_t GetID() const { return m_id; } + uint16_t GetInstanceID() const { return static_cast(m_instance_id); } + uint32_t GetSecondsRemaining() const; + uint16_t GetZoneID() const { return static_cast(m_zone_id); } + uint32_t GetZoneIndex() const { return (m_instance_id << 16) | (m_zone_id & 0xffff); } + uint32_t GetZoneVersion() const { return m_version; } + DynamicZoneType GetType() const { return m_type; } + DynamicZoneLocation GetCompassLocation() const { return m_compass; } + DynamicZoneLocation GetSafeReturnLocation() const { return m_safereturn; } + DynamicZoneLocation GetZoneInLocation() const { return m_zonein; } + + void AddCharacter(uint32_t character_id); + uint32_t Create(); + uint32_t CreateInstance(); + bool HasZoneInLocation() const { return m_has_zonein; } + bool IsCurrentZoneDzInstance() const; + bool IsInstanceID(uint32_t instance_id) const; + bool IsValid() const { return m_instance_id != 0; } + bool IsSameDz(uint32_t zone_id, uint32_t instance_id) const; + void RemoveAllCharacters(bool enable_removal_timers = true); + void RemoveCharacter(uint32_t character_id); + void SaveInstanceMembersToDatabase(const std::vector& character_ids); + void SendInstanceCharacterChange(uint32_t character_id, bool removed); + void SetCompass(const DynamicZoneLocation& location, bool update_db = false); + void SetSafeReturn(const DynamicZoneLocation& location, bool update_db = false); + void SetZoneInLocation(const DynamicZoneLocation& location, bool update_db = false); + void SetUpdatedDuration(uint32_t seconds); + +private: + static std::string DynamicZoneSelectQuery(); + void LoadDatabaseResult(MySQLRequestRow& row); + void SaveCompassToDatabase(); + void SaveSafeReturnToDatabase(); + void SaveZoneInLocationToDatabase(); + uint32_t SaveToDatabase(); + + uint32_t m_id = 0; + uint32_t m_zone_id = 0; + uint32_t m_instance_id = 0; + uint32_t m_version = 0; + bool m_never_expires = false; + bool m_has_zonein = false; + DynamicZoneType m_type = DynamicZoneType::None; + DynamicZoneLocation m_compass; + DynamicZoneLocation m_safereturn; + DynamicZoneLocation m_zonein; + std::chrono::seconds m_duration; + std::chrono::time_point m_start_time; + std::chrono::time_point m_expire_time; +}; + +struct DynamicZoneInfo +{ + std::string description; // from owning system + std::string leader_name; + DynamicZone dynamic_zone; + + DynamicZoneInfo() = default; + DynamicZoneInfo(std::string desc, std::string leader, const DynamicZone& dz) + : description(std::move(desc)), leader_name(std::move(leader)), dynamic_zone(dz) {} +}; + +#endif diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 671a26d9c..087e44663 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -971,6 +971,9 @@ void PerlembParser::MapFunctions() "package Doors;" "&boot_Doors;" // load quest Doors XS + "package Expedition;" + "&boot_Expedition;" + #endif "package main;" "}" diff --git a/zone/embparser_api.cpp b/zone/embparser_api.cpp index 1ae4c22c1..17e20b9ec 100644 --- a/zone/embparser_api.cpp +++ b/zone/embparser_api.cpp @@ -29,6 +29,7 @@ #include "embparser.h" #include "embxs.h" #include "entity.h" +#include "expedition.h" #include "queryserv.h" #include "questmgr.h" #include "zone.h" @@ -6059,6 +6060,265 @@ XS(XS__SetContentFlag) XSRETURN_EMPTY; } +XS(XS__get_expedition); +XS(XS__get_expedition) { + dXSARGS; + if (items != 0) { + Perl_croak(aTHX_ "Usage: quest::get_expedition()"); + } + + Expedition* RETVAL = nullptr; + if (zone && zone->GetInstanceID() != 0) + { + RETVAL = Expedition::FindCachedExpeditionByZoneInstance(zone->GetZoneID(), zone->GetInstanceID()); + } + + EXTEND(sp, 1); // grow stack, function had 0 arguments + ST(0) = sv_newmortal(); // PUSHs(sv_newmortal()); + if (RETVAL) { + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + } + + XSRETURN(1); +} + +XS(XS__get_expedition_by_char_id); +XS(XS__get_expedition_by_char_id) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: quest::get_expedition_by_char_id(uint32 character_id)"); + } + + uint32 character_id = (int)SvUV(ST(0)); + + Expedition* RETVAL = Expedition::FindCachedExpeditionByCharacterID(character_id); + + ST(0) = sv_newmortal(); + if (RETVAL) { + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + } + + XSRETURN(1); +} + +XS(XS__get_expedition_by_dz_id); +XS(XS__get_expedition_by_dz_id) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: quest::get_expedition_by_dz_id(uint32 dynamic_zone_id)"); + } + + uint32 dz_id = (int)SvUV(ST(0)); + + Expedition* RETVAL = Expedition::FindCachedExpeditionByDynamicZoneID(dz_id); + + ST(0) = sv_newmortal(); + if (RETVAL) { + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + } + + XSRETURN(1); +} + +XS(XS__get_expedition_by_zone_instance); +XS(XS__get_expedition_by_zone_instance) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: quest::GetExpeditionByZoneInstance(uint16 zone_id, uint16 instance_id)"); + } + + uint16 zone_id = (uint16)SvUV(ST(0)); + uint16 instance_id = (uint16)SvUV(ST(1)); + + Expedition* RETVAL = Expedition::FindCachedExpeditionByZoneInstance(zone_id, instance_id); + + ST(0) = sv_newmortal(); + if (RETVAL) { + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + } + + XSRETURN(1); +} + +XS(XS__get_expedition_lockout_by_char_id); +XS(XS__get_expedition_lockout_by_char_id) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: quest::get_expedition_lockout_by_char_id(uint32 character_id, string expedition_name, string event_name)"); + } + + uint32_t character_id = static_cast(SvUV(ST(0))); + std::string expedition_name = SvPV_nolen(ST(1)); + std::string event_name = SvPV_nolen(ST(2)); + + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(character_id); + auto it = std::find_if(lockouts.begin(), lockouts.end(), [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); + + // mortalize so its refcnt is auto decremented on function exit to avoid leak + HV* hash = (HV*)sv_2mortal((SV*)newHV()); // hash refcnt +1 (mortal -1) + + if (it != lockouts.end()) + { + hv_store(hash, "remaining", strlen("remaining"), newSVuv(it->GetSecondsRemaining()), 0); + hv_store(hash, "uuid", strlen("uuid"), newSVpv(it->GetExpeditionUUID().c_str(), 0), 0); + } + + ST(0) = sv_2mortal(newRV((SV*)hash)); // hash refcnt: 2 (-1 mortal), reference: 1 (-1 mortal) + XSRETURN(1); +} + +XS(XS__get_expedition_lockouts_by_char_id); +XS(XS__get_expedition_lockouts_by_char_id) { + dXSARGS; + if (items != 1 && items != 2) { + Perl_croak(aTHX_ "Usage: quest::get_expedition_lockouts_by_char_id(uint32 character_id, [string expedition_name])"); + } + + HV* hash = newHV(); // hash refcnt +1 (non-mortal, newRV_noinc to not inc) + SV* hash_ref = nullptr; // for expedition event hash if filtering on expedition + + uint32_t character_id = static_cast(SvUV(ST(0))); + std::string expedition_name; + if (items == 2) + { + expedition_name = SvPV_nolen(ST(1)); + } + + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(character_id); + + for (const auto& lockout : lockouts) + { + uint32_t name_len = static_cast(lockout.GetExpeditionName().size()); + uint32_t event_len = static_cast(lockout.GetEventName().size()); + + // hashes are stored through references inside other hashes/arrays. we need + // to wrap newHV in newRV references when inserting nested hash values. + // we use newRV_noinc to not increment the hash's ref count; rv will own it + + SV** entry = hv_fetch(hash, lockout.GetExpeditionName().c_str(), name_len, false); + if (!entry) + { + // create expedition entry in hash with its value as ref to event hash + SV* event_hash_ref = newRV_noinc((SV*)newHV()); // ref takes ownership + if (!expedition_name.empty() && lockout.GetExpeditionName() == expedition_name) + { + hash_ref = event_hash_ref; // save ref for filtered expedition return + } + entry = hv_store(hash, lockout.GetExpeditionName().c_str(), name_len, event_hash_ref, 0); + } + + // *entry is a reference to expedition's event hash (which it owns). the + // event entry in the hash will contain ref to a lockout detail hash + if (entry && SvROK(*entry) && SvTYPE(SvRV(*entry)) == SVt_PVHV) // is ref to hash type + { + HV* details_hash = newHV(); // refcnt +1, reference will take ownership + hv_store(details_hash, "remaining", strlen("remaining"), newSVuv(lockout.GetSecondsRemaining()), 0); + hv_store(details_hash, "uuid", strlen("uuid"), newSVpv(lockout.GetExpeditionUUID().c_str(), 0), 0); + + HV* event_hash = (HV*)SvRV(*entry); + hv_store(event_hash, lockout.GetEventName().c_str(), event_len, + (SV*)newRV_noinc((SV*)details_hash), 0); + } + } + + SV* rv = &PL_sv_undef; + + if (!expedition_name.empty()) + { + rv = hash_ref ? sv_2mortal(hash_ref) : &PL_sv_undef; // ref that owns event hash for expedition + } + else + { + rv = sv_2mortal(newRV_noinc((SV*)hash)); // takes ownership of expedition hash + } + + ST(0) = rv; + XSRETURN(1); +} + +XS(XS__add_expedition_lockout_all_clients); +XS(XS__add_expedition_lockout_all_clients) { + dXSARGS; + if (items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: quest::add_expedition_lockout_all_clients(string expedition_name, string event_name, uint32 seconds, [string uuid])"); + } + + std::string expedition_name = SvPV_nolen(ST(0)); + std::string event_name = SvPV_nolen(ST(1)); + uint32_t seconds = static_cast(SvUV(ST(2))); + std::string uuid; + + if (items == 4) + { + uuid = SvPV_nolen(ST(3)); + } + + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + Expedition::AddLockoutClients(lockout); + + XSRETURN_EMPTY; +} + +XS(XS__add_expedition_lockout_by_char_id); +XS(XS__add_expedition_lockout_by_char_id) { + dXSARGS; + if (items != 4 && items != 5) { + Perl_croak(aTHX_ "Usage: quest::add_expedition_lockout_by_char_id(uint32 character_id, string expedition_name, string event_name, uint32 seconds, [string uuid])"); + } + + std::string uuid; + if (items == 5) + { + uuid = SvPV_nolen(ST(4)); + } + + uint32_t character_id = static_cast(SvUV(ST(0))); + std::string expedition_name = SvPV_nolen(ST(1)); + std::string event_name = SvPV_nolen(ST(2)); + uint32_t seconds = static_cast(SvUV(ST(3))); + + Expedition::AddLockoutByCharacterID(character_id, expedition_name, event_name, seconds, uuid); + + XSRETURN_EMPTY; +} + +XS(XS__remove_expedition_lockout_by_char_id); +XS(XS__remove_expedition_lockout_by_char_id) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: quest::remove_expedition_lockout_by_char_id(uint32 character_id, string expedition_name, string event_name)"); + } + + uint32_t character_id = static_cast(SvUV(ST(0))); + std::string expedition_name = SvPV_nolen(ST(1)); + std::string event_name = SvPV_nolen(ST(2)); + + Expedition::RemoveLockoutsByCharacterID(character_id, expedition_name, event_name); + + XSRETURN_EMPTY; +} + +XS(XS__remove_all_expedition_lockouts_by_char_id); +XS(XS__remove_all_expedition_lockouts_by_char_id) { + dXSARGS; + if (items != 1 && items != 2) { + Perl_croak(aTHX_ "Usage: quest::remove_expedition_lockout_by_char_id(uint32 character_id, [string expedition_name])"); + } + + std::string expedition_name; + if (items == 2) + { + expedition_name = SvPV_nolen(ST(1)); + } + + uint32_t character_id = static_cast(SvUV(ST(0))); + Expedition::RemoveLockoutsByCharacterID(character_id, expedition_name); + + XSRETURN_EMPTY; +} + /* This is the callback perl will look for to setup the quest package's XSUBs @@ -6129,6 +6389,8 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "activespeakactivity"), XS__activespeakactivity, file); newXS(strcpy(buf, "activespeaktask"), XS__activespeaktask, file); newXS(strcpy(buf, "activetasksinset"), XS__activetasksinset, file); + newXS(strcpy(buf, "add_expedition_lockout_all_clients"), XS__add_expedition_lockout_all_clients, file); + newXS(strcpy(buf, "add_expedition_lockout_by_char_id"), XS__add_expedition_lockout_by_char_id, file); newXS(strcpy(buf, "addldonloss"), XS__addldonpoints, file); newXS(strcpy(buf, "addldonpoints"), XS__addldonpoints, file); newXS(strcpy(buf, "addldonwin"), XS__addldonpoints, file); @@ -6263,6 +6525,12 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "getcharidbyname"), XS__getcharidbyname, file); newXS(strcpy(buf, "getclassname"), XS__getclassname, file); newXS(strcpy(buf, "getcurrencyid"), XS__getcurrencyid, file); + newXS(strcpy(buf, "get_expedition"), XS__get_expedition, file); + newXS(strcpy(buf, "get_expedition_by_char_id"), XS__get_expedition_by_char_id, file); + newXS(strcpy(buf, "get_expedition_by_dz_id"), XS__get_expedition_by_dz_id, file); + newXS(strcpy(buf, "get_expedition_by_zone_instance"), XS__get_expedition_by_zone_instance, file); + newXS(strcpy(buf, "get_expedition_lockout_by_char_id"), XS__get_expedition_lockout_by_char_id, file); + newXS(strcpy(buf, "get_expedition_lockouts_by_char_id"), XS__get_expedition_lockouts_by_char_id, file); newXS(strcpy(buf, "getinventoryslotid"), XS__getinventoryslotid, file); newXS(strcpy(buf, "getitemname"), XS__getitemname, file); newXS(strcpy(buf, "getItemName"), XS_qc_getItemName, file); @@ -6328,6 +6596,8 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "rain"), XS__rain, file); newXS(strcpy(buf, "rebind"), XS__rebind, file); newXS(strcpy(buf, "reloadzonestaticdata"), XS__reloadzonestaticdata, file); + newXS(strcpy(buf, "remove_all_expedition_lockouts_by_char_id"), XS__remove_all_expedition_lockouts_by_char_id, file); + newXS(strcpy(buf, "remove_expedition_lockout_by_char_id"), XS__remove_expedition_lockout_by_char_id, file); newXS(strcpy(buf, "removeitem"), XS__removeitem, file); newXS(strcpy(buf, "removetitle"), XS__removetitle, file); newXS(strcpy(buf, "repopzone"), XS__repopzone, file); diff --git a/zone/embperl.cpp b/zone/embperl.cpp index c7ebb8060..ce584c799 100644 --- a/zone/embperl.cpp +++ b/zone/embperl.cpp @@ -37,6 +37,7 @@ EXTERN_C XS(boot_HateEntry); EXTERN_C XS(boot_Object); EXTERN_C XS(boot_Doors); EXTERN_C XS(boot_PerlPacket); +EXTERN_C XS(boot_Expedition); #endif #endif @@ -87,6 +88,7 @@ EXTERN_C void xs_init(pTHX) newXS(strcpy(buf, "HateEntry::boot_HateEntry"), boot_HateEntry, file); newXS(strcpy(buf, "Object::boot_Object"), boot_Object, file); newXS(strcpy(buf, "Doors::boot_Doors"), boot_Doors, file); + newXS(strcpy(buf, "Expedition::boot_Expedition"), boot_Expedition, file); ; #endif #endif diff --git a/zone/entity.cpp b/zone/entity.cpp index 417bac493..1ad03eea0 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -32,6 +32,7 @@ #include "../common/features.h" #include "../common/guilds.h" +#include "dynamiczone.h" #include "guild_mgr.h" #include "petitions.h" #include "quest_parser_collection.h" @@ -5202,3 +5203,25 @@ std::unordered_map &EntityList::GetCloseMobList(Mob *mob, float d return mob_list; } +void EntityList::GateAllClientsToSafeReturn() +{ + DynamicZone dz; + if (zone) + { + dz = zone->GetDynamicZone(); + + LogDynamicZones( + "Sending all clients in zone: [{}] instance: [{}] to dz safereturn or bind", + zone->GetZoneID(), zone->GetInstanceID() + ); + } + + for (const auto& client_list_iter : client_list) + { + if (client_list_iter.second) + { + // falls back to gating clients to bind if dz invalid + client_list_iter.second->GoToDzSafeReturnOrBind(dz); + } + } +} diff --git a/zone/entity.h b/zone/entity.h index 3815f3c50..b7f90b6f9 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -49,6 +49,7 @@ class Raid; class Spawn2; class Trap; +struct DynamicZoneSafeReturn; struct GuildBankItemUpdate_Struct; struct NewSpawn_Struct; struct QGlobal; @@ -496,6 +497,8 @@ public: void UpdateFindableNPCState(NPC *n, bool Remove); void HideCorpses(Client *c, uint8 CurrentMode, uint8 NewMode); + void GateAllClientsToSafeReturn(); + uint16 GetClientCount(); void GetMobList(std::list &m_list); void GetNPCList(std::list &n_list); diff --git a/zone/expedition.cpp b/zone/expedition.cpp new file mode 100644 index 000000000..cc88b8c70 --- /dev/null +++ b/zone/expedition.cpp @@ -0,0 +1,2281 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" +#include "expedition_request.h" +#include "client.h" +#include "string_ids.h" +#include "worldserver.h" +#include "zonedb.h" +#include "../common/eqemu_logsys.h" +#include "../common/util/uuid.h" + +extern WorldServer worldserver; +extern Zone* zone; + +// message string 8271 (not in emu clients) +const char* const DZ_YOU_NOT_ASSIGNED = "You could not use this command because you are not currently assigned to a dynamic zone."; +// message string 9265 (not in emu clients) +const char* const EXPEDITION_OTHER_BELONGS = "{} attempted to create an expedition but {} already belongs to one."; +// lockout warnings were added to live in March 11 2020 patch +const char* const DZADD_INVITE_WARNING = "Warning! You will be given replay timers for the following events if you enter %s:"; +const char* const DZADD_INVITE_WARNING_TIMER = "%s - %sD:%sH:%sM"; +const char* const KICKPLAYERS_EVERYONE = "Everyone"; +// message string 8312 added in September 08 2020 Test patch (used by both dz and shared tasks) +const char* const CREATE_NOT_ALL_ADDED = "Not all players in your {} were added to the {}. The {} can take a maximum of {} players, and your {} has {}."; +// various expeditions re-use these strings when locking +constexpr char LOCK_CLOSE[] = "Your expedition is nearing its close. You cannot bring any additional people into your expedition at this time."; +constexpr char LOCK_BEGIN[] = "The trial has begun. You cannot bring any additional people into your expedition at this time."; + +const int32_t Expedition::REPLAY_TIMER_ID = -1; +const int32_t Expedition::EVENT_TIMER_ID = 1; + +Expedition::Expedition( + uint32_t id, const std::string& uuid, const DynamicZone& dynamic_zone, const std::string& expedition_name, + const ExpeditionMember& leader, uint32_t min_players, uint32_t max_players +) : + m_id(id), + m_uuid(uuid), + m_dynamiczone(dynamic_zone), + m_expedition_name(expedition_name), + m_leader(leader), + m_min_players(min_players), + m_max_players(max_players) +{ +} + +Expedition* Expedition::TryCreate( + Client* requester, DynamicZone& dynamiczone, ExpeditionRequest& request) +{ + if (!requester || !zone) + { + return nullptr; + } + + // request parses leader, members list, and lockouts while validating + if (!request.Validate(requester)) + { + LogExpeditionsModerate("[{}] request by [{}] denied", request.GetExpeditionName(), requester->GetName()); + return nullptr; + } + + auto dynamic_zone_id = dynamiczone.Create(); + if (dynamic_zone_id == 0) + { + // live uses this message when trying to enter an instance that isn't ready + // we can use it as the client error message if instance creation fails + requester->MessageString(Chat::Red, DZ_PREVENT_ENTERING); + LogExpeditions("Failed to create a dynamic zone instance for expedition"); + return nullptr; + } + + std::string expedition_uuid = EQ::Util::UUID::Generate().ToString(); + + // unique expedition ids are created from database via auto-increment column + auto expedition_id = ExpeditionDatabase::InsertExpedition( + expedition_uuid, + dynamiczone.GetID(), + request.GetExpeditionName(), + request.GetLeaderID(), + request.GetMinPlayers(), + request.GetMaxPlayers() + ); + + if (expedition_id) + { + auto expedition = std::unique_ptr(new Expedition( + expedition_id, + expedition_uuid, + dynamiczone, + request.GetExpeditionName(), + ExpeditionMember{ request.GetLeaderID(), request.GetLeaderName() }, + request.GetMinPlayers(), + request.GetMaxPlayers() + )); + + LogExpeditions( + "Created [{}] [{}] instance id: [{}] leader: [{}] minplayers: [{}] maxplayers: [{}]", + expedition->GetID(), + expedition->GetName(), + expedition->GetInstanceID(), + expedition->GetLeaderName(), + expedition->GetMinPlayers(), + expedition->GetMaxPlayers() + ); + + expedition->SaveMembers(request); + expedition->SaveLockouts(request); + + auto inserted = zone->expedition_cache.emplace(expedition_id, std::move(expedition)); + + inserted.first->second->SendUpdatesToZoneMembers(); + inserted.first->second->SendWorldExpeditionUpdate(ServerOP_ExpeditionCreate); // cache in other zones + inserted.first->second->SendLeaderMessage(request.GetLeaderClient(), + Chat::System, EXPEDITION_AVAILABLE, { request.GetExpeditionName() }); + + if (!request.GetNotAllAddedMessage().empty()) + { + Client::SendCrossZoneMessage(request.GetLeaderClient(), request.GetLeaderName(), + Chat::System, request.GetNotAllAddedMessage()); + } + + return inserted.first->second.get(); + } + + return nullptr; +} + +void Expedition::CacheExpeditions(MySQLRequestResult& results) +{ + if (!results.Success() || !zone) + { + return; + } + + std::vector expedition_ids; + std::vector dynamic_zone_ids;; + std::vector> expedition_character_ids; + + using col = LoadExpeditionColumns::eLoadExpeditionColumns; + + uint32_t last_expedition_id = 0; + + for (auto row = results.begin(); row != results.end(); ++row) + { + auto expedition_id = strtoul(row[col::id], nullptr, 10); + + if (expedition_id != last_expedition_id) + { + expedition_ids.emplace_back(expedition_id); + + uint32_t leader_id = strtoul(row[col::leader_id], nullptr, 10); + uint32_t dynamic_zone_id = strtoul(row[col::dz_id], nullptr, 10); + + dynamic_zone_ids.emplace_back(dynamic_zone_id); + + std::unique_ptr expedition = std::unique_ptr(new Expedition( + expedition_id, + row[col::uuid], // expedition uuid + DynamicZone{ dynamic_zone_id }, + row[col::expedition_name], // expedition name + ExpeditionMember{ leader_id, row[col::leader_name] }, // expedition leader id, name + strtoul(row[col::min_players], nullptr, 10), // min_players + strtoul(row[col::max_players], nullptr, 10) // max_players + )); + + bool add_replay_on_join = (strtoul(row[col::add_replay_on_join], nullptr, 10) != 0); + bool is_locked = (strtoul(row[col::is_locked], nullptr, 10) != 0); + + expedition->SetReplayLockoutOnMemberJoin(add_replay_on_join); + expedition->SetLocked(is_locked, ExpeditionLockMessage::None); + + zone->expedition_cache.emplace(expedition_id, std::move(expedition)); + } + + last_expedition_id = expedition_id; + + // looping expedition members + auto current_expedition = Expedition::FindCachedExpeditionByID(last_expedition_id); + if (current_expedition) + { + auto member_id = strtoul(row[col::member_id], nullptr, 10); + current_expedition->AddInternalMember( + row[col::member_name], member_id, ExpeditionMemberStatus::Offline); + expedition_character_ids.emplace_back(expedition_id, member_id); + } + } + + // ask world for online members from all cached expeditions at once + Expedition::SendWorldGetOnlineMembers(expedition_character_ids); + + // bulk load dynamic zone data and expedition lockouts for cached expeditions + auto dynamic_zones = DynamicZone::LoadMultipleDzFromDatabase(dynamic_zone_ids); + auto expedition_lockouts = ExpeditionDatabase::LoadMultipleExpeditionLockouts(expedition_ids); + + for (const auto& expedition_id : expedition_ids) + { + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + auto dz_iter = dynamic_zones.find(expedition->GetDynamicZoneID()); + if (dz_iter != dynamic_zones.end()) + { + expedition->m_dynamiczone = dz_iter->second; + } + + auto lockout_iter = expedition_lockouts.find(expedition->GetID()); + if (lockout_iter != expedition_lockouts.end()) + { + expedition->m_lockouts = lockout_iter->second; + } + + // send member updates now that all data is loaded for the cached expedition + expedition->SendUpdatesToZoneMembers(); + } + } +} + +void Expedition::CacheFromDatabase(uint32_t expedition_id) +{ + if (zone) + { + BenchTimer benchmark; + + auto results = ExpeditionDatabase::LoadExpedition(expedition_id); + if (!results.Success()) + { + LogExpeditions("Failed to load Expedition [{}] for zone cache", expedition_id); + return; + } + + CacheExpeditions(results); + + auto elapsed = benchmark.elapsed(); + LogExpeditions("Caching new expedition [{}] took [{}s]", expedition_id, elapsed); + } +} + +bool Expedition::CacheAllFromDatabase() +{ + if (!zone) + { + return false; + } + + BenchTimer benchmark; + + zone->expedition_cache.clear(); + + // load all active expeditions and members to current zone cache + auto results = ExpeditionDatabase::LoadAllExpeditions(); + if (!results.Success()) + { + LogExpeditions("Failed to load Expeditions for zone cache"); + return false; + } + + CacheExpeditions(results); + + auto elapsed = benchmark.elapsed(); + LogExpeditions("Caching [{}] expedition(s) took [{}s]", zone->expedition_cache.size(), elapsed); + + return true; +} + +void Expedition::SaveLockouts(ExpeditionRequest& request) +{ + m_lockouts = request.GetLockouts(); + ExpeditionDatabase::InsertLockouts(m_id, m_lockouts); +} + +void Expedition::SaveMembers(ExpeditionRequest& request) +{ + m_members = request.GetMembers(); + + std::vector member_ids; + for (const auto& member : m_members) + { + member_ids.emplace_back(member.char_id); + } + + ExpeditionDatabase::InsertMembers(m_id, m_members); + m_dynamiczone.SaveInstanceMembersToDatabase(member_ids); +} + +Expedition* Expedition::FindCachedExpeditionByCharacterID(uint32_t character_id) +{ + if (zone) + { + for (const auto& expedition : zone->expedition_cache) + { + if (expedition.second->HasMember(character_id)) + { + return expedition.second.get(); + } + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByCharacterName(const std::string& char_name) +{ + if (zone) + { + for (const auto& expedition : zone->expedition_cache) + { + if (expedition.second->HasMember(char_name)) + { + return expedition.second.get(); + } + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByDynamicZoneID(uint32_t dz_id) +{ + if (zone && dz_id != 0) + { + for (const auto& cached_expedition : zone->expedition_cache) + { + if (cached_expedition.second->GetDynamicZone().GetID() == dz_id) + { + return cached_expedition.second.get(); + } + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByID(uint32_t expedition_id) +{ + if (zone && expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByZoneInstance(uint32_t zone_id, uint32_t instance_id) +{ + if (zone && zone_id != 0 && instance_id != 0) + { + for (const auto& cached_expedition : zone->expedition_cache) + { + if (cached_expedition.second->GetDynamicZone().GetZoneID() == zone_id && + cached_expedition.second->GetDynamicZone().GetInstanceID() == instance_id) + { + return cached_expedition.second.get(); + } + } + } + return nullptr; +} + +bool Expedition::HasLockout(const std::string& event_name) +{ + return (m_lockouts.find(event_name) != m_lockouts.end()); +} + +bool Expedition::HasReplayLockout() +{ + return HasLockout(DZ_REPLAY_TIMER_NAME); +} + +bool Expedition::HasMember(uint32_t character_id) +{ + return std::any_of(m_members.begin(), m_members.end(), [&](const ExpeditionMember& member) { + return member.char_id == character_id; + }); +} + +bool Expedition::HasMember(const std::string& character_name) +{ + return std::any_of(m_members.begin(), m_members.end(), [&](const ExpeditionMember& member) { + return (strcasecmp(member.name.c_str(), character_name.c_str()) == 0); + }); +} + +ExpeditionMember Expedition::GetMemberData(uint32_t character_id) +{ + auto it = std::find_if(m_members.begin(), m_members.end(), [&](const ExpeditionMember& member) { + return member.char_id == character_id; + }); + + ExpeditionMember member_data; + if (it != m_members.end()) + { + member_data = *it; + } + return member_data; +} + +ExpeditionMember Expedition::GetMemberData(const std::string& character_name) +{ + auto it = std::find_if(m_members.begin(), m_members.end(), [&](const ExpeditionMember& member) { + return (strcasecmp(member.name.c_str(), character_name.c_str()) == 0); + }); + + ExpeditionMember member_data; + if (it != m_members.end()) + { + member_data = *it; + } + return member_data; +} + +void Expedition::SetReplayLockoutOnMemberJoin(bool add_on_join, bool update_db) +{ + m_add_replay_on_join = add_on_join; + + if (update_db) + { + ExpeditionDatabase::UpdateReplayLockoutOnJoin(m_id, add_on_join); + SendWorldSettingChanged(ServerOP_ExpeditionReplayOnJoin, m_add_replay_on_join); + } +} + +void Expedition::AddReplayLockout(uint32_t seconds) +{ + AddLockout(DZ_REPLAY_TIMER_NAME, seconds); +} + +void Expedition::AddLockout(const std::string& event_name, uint32_t seconds) +{ + auto lockout = ExpeditionLockoutTimer::CreateLockout(m_expedition_name, event_name, seconds, m_uuid); + AddLockout(lockout); +} + +void Expedition::AddLockout(const ExpeditionLockoutTimer& lockout, bool members_only) +{ + if (!members_only) + { + ExpeditionDatabase::InsertLockout(m_id, lockout); + } + ExpeditionDatabase::InsertMembersLockout(m_members, lockout); + + ProcessLockoutUpdate(lockout, false, members_only); + SendWorldLockoutUpdate(lockout, false, members_only); +} + +void Expedition::AddLockoutDuration(const std::string& event_name, int seconds, bool members_only) +{ + // lockout timers use unsigned durations to define intent but we may need + // to insert a new lockout while still supporting timer reductions + auto lockout = ExpeditionLockoutTimer::CreateLockout( + m_expedition_name, event_name, std::max(0, seconds), m_uuid); + + if (!members_only) + { + auto it = m_lockouts.find(event_name); + if (it != m_lockouts.end()) + { + it->second.AddLockoutTime(seconds); + ExpeditionDatabase::InsertLockout(m_id, it->second); // replaces current one + } + else + { + ExpeditionDatabase::InsertLockout(m_id, lockout); + } + } + + // processing lockout duration applies multiplier again in client methods, + // update database with modified value now but pass original on + int modified_seconds = static_cast(seconds * RuleR(Expedition, LockoutDurationMultiplier)); + ExpeditionDatabase::AddLockoutDuration(m_members, lockout, modified_seconds); + + ProcessLockoutDuration(lockout, seconds, members_only); + SendWorldLockoutDuration(lockout, seconds, members_only); +} + +void Expedition::AddReplayLockoutDuration(int seconds, bool members_only) +{ + AddLockoutDuration(DZ_REPLAY_TIMER_NAME, seconds, members_only); +} + +void Expedition::UpdateLockoutDuration( + const std::string& event_name, uint32_t seconds, bool members_only) +{ + // some live expeditions update existing lockout timers during progression + auto it = m_lockouts.find(event_name); + if (it != m_lockouts.end()) + { + seconds = static_cast(seconds * RuleR(Expedition, LockoutDurationMultiplier)); + + uint64_t expire_time = it->second.GetStartTime() + seconds; + AddLockout({ m_uuid, m_expedition_name, event_name, expire_time, seconds }, members_only); + } +} + +void Expedition::RemoveLockout(const std::string& event_name) +{ + ExpeditionDatabase::DeleteLockout(m_id, event_name); + ExpeditionDatabase::DeleteMembersLockout(m_members, m_expedition_name, event_name); + + ExpeditionLockoutTimer lockout{m_uuid, m_expedition_name, event_name, 0, 0}; + ProcessLockoutUpdate(lockout, true); + SendWorldLockoutUpdate(lockout, true); +} + +void Expedition::AddInternalMember( + const std::string& char_name, uint32_t character_id, ExpeditionMemberStatus status) +{ + auto it = std::find_if(m_members.begin(), m_members.end(), + [character_id](const ExpeditionMember& member) { + return member.char_id == character_id; + }); + + if (it == m_members.end()) + { + m_members.emplace_back(character_id, char_name, status); + } +} + +bool Expedition::AddMember(const std::string& add_char_name, uint32_t add_char_id) +{ + if (HasMember(add_char_id)) + { + return false; + } + + ExpeditionDatabase::InsertMember(m_id, add_char_id); + m_dynamiczone.AddCharacter(add_char_id); + + ProcessMemberAdded(add_char_name, add_char_id); + SendWorldMemberChanged(add_char_name, add_char_id, false); + + return true; +} + +void Expedition::RemoveAllMembers(bool enable_removal_timers) +{ + m_dynamiczone.RemoveAllCharacters(enable_removal_timers); + + ExpeditionDatabase::DeleteAllMembers(m_id); + + SendUpdatesToZoneMembers(true); + SendWorldExpeditionUpdate(ServerOP_ExpeditionMembersRemoved); + + m_members.clear(); +} + +bool Expedition::RemoveMember(const std::string& remove_char_name) +{ + auto member = GetMemberData(remove_char_name); + if (!member.IsValid()) + { + return false; + } + + ExpeditionDatabase::DeleteMember(m_id, member.char_id); + m_dynamiczone.RemoveCharacter(member.char_id); + + ProcessMemberRemoved(member.name, member.char_id); + SendWorldMemberChanged(member.name, member.char_id, true); + + return true; +} + +void Expedition::SwapMember(Client* add_client, const std::string& remove_char_name) +{ + if (!add_client || remove_char_name.empty()) + { + return; + } + + auto member = GetMemberData(remove_char_name); + if (!member.IsValid()) + { + return; + } + + // make remove and add atomic to avoid racing with separate world messages + ExpeditionDatabase::DeleteMember(m_id, member.char_id); + ExpeditionDatabase::InsertMember(m_id, add_client->CharacterID()); + m_dynamiczone.RemoveCharacter(member.char_id); + m_dynamiczone.AddCharacter(add_client->CharacterID()); + + ProcessMemberRemoved(member.name, member.char_id); + ProcessMemberAdded(add_client->GetName(), add_client->CharacterID()); + SendWorldMemberSwapped(member.name, member.char_id, add_client->GetName(), add_client->CharacterID()); +} + +void Expedition::SetMemberStatus(Client* client, ExpeditionMemberStatus status) +{ + if (client) + { + UpdateMemberStatus(client->CharacterID(), status); + SendWorldMemberStatus(client->CharacterID(), status); + + // world could detect this itself but it'd have to process member status updates + // a member coming online will trigger a leader change if all members were offline + if (m_leader.status == ExpeditionMemberStatus::Offline) + { + SendWorldExpeditionUpdate(ServerOP_ExpeditionChooseNewLeader); + } + } +} + +void Expedition::UpdateMemberStatus(uint32_t update_member_id, ExpeditionMemberStatus status) +{ + auto member_data = GetMemberData(update_member_id); + if (!member_data.IsValid()) + { + return; + } + + if (status == ExpeditionMemberStatus::InDynamicZone && !RuleB(Expedition, EnableInDynamicZoneStatus)) + { + status = ExpeditionMemberStatus::Online; + } + + if (update_member_id == m_leader.char_id) + { + m_leader.status = status; + } + + // if zone already had this member status cached avoid packet update to clients + auto it = std::find_if(m_members.begin(), m_members.end(), + [&](const ExpeditionMember& member) { return member.char_id == update_member_id; }); + + if (it != m_members.end() && it->status == status) + { + return; + } + + auto outapp_member_status = CreateMemberListStatusPacket(member_data.name, status); + + for (auto& member : m_members) + { + if (member.char_id == update_member_id) + { + member.status = status; + } + + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp_member_status.get()); + } + } +} + +void Expedition::SendClientExpeditionInvite( + Client* client, const std::string& inviter_name, const std::string& swap_remove_name) +{ + if (!client) + { + return; + } + + LogExpeditionsModerate( + "Sending expedition [{}] invite to player [{}] inviter [{}] swap name [{}]", + m_id, client->GetName(), inviter_name, swap_remove_name + ); + + client->SetPendingExpeditionInvite({ m_id, inviter_name, swap_remove_name }); + + client->MessageString(Chat::System, EXPEDITION_ASKED_TO_JOIN, + m_leader.name.c_str(), m_expedition_name.c_str()); + + // live (as of March 11 2020 patch) sends warnings for lockouts added + // during current expedition that client would receive on entering dz + bool warned = false; + for (const auto& lockout_iter : m_lockouts) + { + // live doesn't issue a warning for the dz's replay timer + const ExpeditionLockoutTimer& lockout = lockout_iter.second; + if (!lockout.IsReplayTimer() && !lockout.IsExpired() && lockout.IsFromExpedition(m_uuid) && + !client->HasExpeditionLockout(m_expedition_name, lockout.GetEventName())) + { + if (!warned) + { + client->Message(Chat::System, DZADD_INVITE_WARNING, m_expedition_name.c_str()); + warned = true; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + client->Message( + Chat::System, DZADD_INVITE_WARNING_TIMER, + lockout.GetEventName().c_str(), + time_remaining.days.c_str(), + time_remaining.hours.c_str(), + time_remaining.mins.c_str() + ); + } + } + + auto outapp = CreateInvitePacket(inviter_name, swap_remove_name); + client->QueuePacket(outapp.get()); +} + +void Expedition::SendLeaderMessage( + Client* leader_client, uint16_t chat_type, uint32_t string_id, const std::initializer_list& args) +{ + Client::SendCrossZoneMessageString(leader_client, m_leader.name, chat_type, string_id, args); +} + +bool Expedition::ProcessAddConflicts(Client* leader_client, Client* add_client, bool swapping) +{ + if (!add_client) // a null leader_client is handled by SendLeaderMessage fallback + { + return true; + } + + bool has_conflict = false; + + if (m_dynamiczone.IsCurrentZoneDzInstance()) + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_LEAVE_ZONE_FIRST, { add_client->GetName() }); + has_conflict = true; + } + + auto expedition_id = add_client->GetExpeditionID(); + if (expedition_id) + { + auto string_id = (expedition_id == GetID()) ? DZADD_ALREADY_PART : DZADD_ALREADY_ASSIGNED; + SendLeaderMessage(leader_client, Chat::Red, string_id, { add_client->GetName() }); + has_conflict = true; + } + + // check any extra event lockouts for this expedition that the client has and expedition doesn't + auto client_lockouts = add_client->GetExpeditionLockouts(m_expedition_name); + for (const auto& client_lockout : client_lockouts) + { + if (client_lockout.IsReplayTimer()) + { + // client with a replay lockout is allowed only if the replay timer was from this expedition + if (client_lockout.GetExpeditionUUID() != GetUUID()) + { + has_conflict = true; + + auto time_remaining = client_lockout.GetDaysHoursMinutesRemaining(); + SendLeaderMessage(leader_client, Chat::Red, DZADD_REPLAY_TIMER, { + add_client->GetName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins + }); + } + } + else + { + bool is_missing_lockout = (m_lockouts.find(client_lockout.GetEventName()) == m_lockouts.end()); + if (is_missing_lockout) + { + has_conflict = true; + + auto time_remaining = client_lockout.GetDaysHoursMinutesRemaining(); + SendLeaderMessage(leader_client, Chat::Red, DZADD_EVENT_TIMER, { + add_client->GetName(), + client_lockout.GetEventName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins, + client_lockout.GetEventName() + }); + } + } + } + + // swapping ignores the max player count check since it's a 1:1 change + if (!swapping && GetMemberCount() >= m_max_players) + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_EXCEED_MAX, { fmt::format_int(m_max_players).str() }); + has_conflict = true; + } + + auto invite_id = add_client->GetPendingExpeditionInviteID(); + if (invite_id) + { + auto string_id = (invite_id == GetID()) ? DZADD_PENDING : DZADD_PENDING_OTHER; + SendLeaderMessage(leader_client, Chat::Red, string_id, { add_client->GetName() }); + has_conflict = true; + } + + return has_conflict; +} + +void Expedition::DzInviteResponse(Client* add_client, bool accepted, const std::string& swap_remove_name) +{ + if (!add_client) + { + return; + } + + LogExpeditionsModerate( + "Invite response by [{}] accepted [{}] swap_name [{}]", + add_client->GetName(), accepted, swap_remove_name + ); + + // a null leader_client is handled by SendLeaderMessage fallbacks + // note current leader receives invite reply messages (if leader changed) + Client* leader_client = entity_list.GetClientByCharID(m_leader.char_id); + + if (!accepted) + { + SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_DECLINED, { add_client->GetName() }); + return; + } + + bool was_swap_invite = !swap_remove_name.empty(); + bool has_conflicts = m_is_locked; + + if (m_is_locked) + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_NOT_ALLOWING); + } + else + { + has_conflicts = ProcessAddConflicts(leader_client, add_client, was_swap_invite); + } + + // error if swapping and character was already removed before the accept + if (was_swap_invite && !HasMember(swap_remove_name)) + { + has_conflicts = true; + } + + if (has_conflicts) + { + SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_ERROR, { add_client->GetName() }); + } + else + { + SendLeaderMessage(leader_client, Chat::Yellow, EXPEDITION_INVITE_ACCEPTED, { add_client->GetName() }); + + // replay timers are optionally added to new members on join with fresh expire time + if (m_add_replay_on_join) + { + auto replay_lockout = m_lockouts.find(DZ_REPLAY_TIMER_NAME); + if (replay_lockout != m_lockouts.end() && + replay_lockout->second.IsFromExpedition(m_uuid) && + !add_client->HasExpeditionLockout(m_expedition_name, DZ_REPLAY_TIMER_NAME)) + { + ExpeditionLockoutTimer replay_timer = replay_lockout->second; // copy + replay_timer.Reset(); + add_client->AddExpeditionLockout(replay_timer, true); + } + } + + if (was_swap_invite) + { + SwapMember(add_client, swap_remove_name); + } + else + { + AddMember(add_client->GetName(), add_client->CharacterID()); + } + } +} + +bool Expedition::ConfirmLeaderCommand(Client* requester) +{ + if (!requester) + { + return false; + } + + if (!m_leader.IsValid()) + { + requester->MessageString(Chat::Red, UNABLE_RETRIEVE_LEADER); // unconfirmed message + return false; + } + + if (m_leader.char_id != requester->CharacterID()) + { + requester->MessageString(Chat::System, EXPEDITION_NOT_LEADER, m_leader.name.c_str()); + return false; + } + + return true; +} + +void Expedition::TryAddClient( + Client* add_client, const std::string& inviter_name, const std::string& swap_remove_name, + Client* leader_client) +{ + if (!add_client) + { + return; + } + + LogExpeditionsModerate( + "Add player request for expedition [{}] by inviter [{}] add name [{}] swap name [{}]", + m_id, inviter_name, add_client->GetName(), swap_remove_name + ); + + // null leader client handled by ProcessAddConflicts/SendLeaderMessage fallbacks + if (!leader_client) + { + leader_client = entity_list.GetClientByName(inviter_name.c_str()); + } + + bool has_conflicts = ProcessAddConflicts(leader_client, add_client, !swap_remove_name.empty()); + if (!has_conflicts) + { + // live uses the original unsanitized input string in invite messages + uint32_t string_id = swap_remove_name.empty() ? DZADD_INVITE : DZSWAP_INVITE; + SendLeaderMessage(leader_client, Chat::Yellow, string_id, { add_client->GetName() }); + SendClientExpeditionInvite(add_client, inviter_name.c_str(), swap_remove_name); + } + else if (swap_remove_name.empty()) // swap command doesn't result in this message + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_INVITE_FAIL, { add_client->GetName() }); + } +} + +void Expedition::DzAddPlayer( + Client* requester, const std::string& add_char_name, const std::string& swap_remove_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + bool invite_failed = false; + + if (m_is_locked) + { + requester->MessageString(Chat::Red, DZADD_NOT_ALLOWING); + invite_failed = true; + } + else if (add_char_name.empty()) + { + requester->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_char_name.c_str()); + invite_failed = true; + } + else + { + auto member_data = GetMemberData(add_char_name); + if (member_data.IsValid()) + { + // live prioritizes offline message before already a member message + if (member_data.status == ExpeditionMemberStatus::Offline) + { + requester->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_char_name.c_str()); + } + else + { + requester->MessageString(Chat::Red, DZADD_ALREADY_PART, add_char_name.c_str()); + } + invite_failed = true; + } + } + + if (invite_failed) + { + requester->MessageString(Chat::Red, DZADD_INVITE_FAIL, FormatName(add_char_name).c_str()); + return; + } + + Client* add_client = entity_list.GetClientByName(add_char_name.c_str()); + if (add_client) + { + // client is online in this zone + TryAddClient(add_client, requester->GetName(), swap_remove_name, requester); + } + else + { + // forward to world to check if client is online and perform cross-zone invite + SendWorldAddPlayerInvite(requester->GetName(), swap_remove_name, FormatName(add_char_name)); + } +} + +void Expedition::DzAddPlayerContinue( + std::string inviter_name, std::string add_name, std::string swap_remove_name) +{ + // continuing expedition invite from leader in another zone + Client* add_client = entity_list.GetClientByName(add_name.c_str()); + if (add_client) + { + TryAddClient(add_client, inviter_name, swap_remove_name); + } +} + +void Expedition::DzMakeLeader(Client* requester, std::string new_leader_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + if (new_leader_name.empty()) + { + requester->MessageString(Chat::Red, DZMAKELEADER_NOT_ONLINE, new_leader_name.c_str()); + return; + } + + // leader can only be changed by world + SendWorldMakeLeaderRequest(requester->CharacterID(), FormatName(new_leader_name)); +} + +void Expedition::DzRemovePlayer(Client* requester, std::string char_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + // live only seems to enforce min_players for requesting expeditions, no need to check here + bool removed = RemoveMember(char_name); + if (!removed) + { + requester->MessageString(Chat::Red, EXPEDITION_NOT_MEMBER, FormatName(char_name).c_str()); + } + else + { + requester->MessageString(Chat::Yellow, EXPEDITION_REMOVED, FormatName(char_name).c_str(), m_expedition_name.c_str()); + } +} + +void Expedition::DzQuit(Client* requester) +{ + if (requester) + { + RemoveMember(requester->GetName()); + } +} + +void Expedition::DzSwapPlayer( + Client* requester, std::string remove_char_name, std::string add_char_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + if (remove_char_name.empty() || !HasMember(remove_char_name)) + { + requester->MessageString(Chat::Red, DZSWAP_CANNOT_REMOVE, FormatName(remove_char_name).c_str()); + return; + } + + DzAddPlayer(requester, add_char_name, remove_char_name); +} + +void Expedition::DzPlayerList(Client* requester) +{ + if (requester) + { + requester->MessageString(Chat::Yellow, EXPEDITION_LEADER, m_leader.name.c_str()); + + std::string member_names; + for (const auto& member : m_members) + { + fmt::format_to(std::back_inserter(member_names), "{}, ", member.name); + } + + if (member_names.size() > 1) + { + member_names.erase(member_names.length() - 2); // trailing comma and space + } + + requester->MessageString(Chat::Yellow, EXPEDITION_MEMBERS, member_names.c_str()); + } +} + +void Expedition::DzKickPlayers(Client* requester) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + RemoveAllMembers(); + requester->MessageString(Chat::Red, EXPEDITION_REMOVED, KICKPLAYERS_EVERYONE, m_expedition_name.c_str()); +} + +void Expedition::SetLocked( + bool lock_expedition, ExpeditionLockMessage lock_msg, bool update_db, uint32_t msg_color) +{ + m_is_locked = lock_expedition; + + if (m_is_locked && lock_msg != ExpeditionLockMessage::None && m_dynamiczone.IsCurrentZoneDzInstance()) + { + auto msg = (lock_msg == ExpeditionLockMessage::Close) ? LOCK_CLOSE : LOCK_BEGIN; + for (const auto& client_iter : entity_list.GetClientList()) + { + if (client_iter.second) + { + client_iter.second->Message(msg_color, msg); + } + } + } + + if (update_db) + { + ExpeditionDatabase::UpdateLockState(m_id, lock_expedition); + SendWorldSettingChanged(ServerOP_ExpeditionLockState, m_is_locked); + } +} + +void Expedition::ProcessLeaderChanged(uint32_t new_leader_id) +{ + auto new_leader = GetMemberData(new_leader_id); + if (!new_leader.IsValid()) + { + LogExpeditions("Processed invalid new leader id [{}] for expedition [{}]", new_leader_id, m_id); + return; + } + + LogExpeditionsModerate("Replaced [{}] leader [{}] with [{}]", m_id, m_leader.name, new_leader.name); + + m_leader = new_leader; + + // update each client's expedition window in this zone + auto outapp_leader = CreateLeaderNamePacket(); + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp_leader.get()); + + if (member.char_id == new_leader_id && RuleB(Expedition, AlwaysNotifyNewLeaderOnChange)) + { + member_client->MessageString(Chat::Yellow, DZMAKELEADER_YOU); + } + } + } +} + +void Expedition::ProcessMakeLeader(Client* old_leader_client, Client* new_leader_client, + const std::string& new_leader_name, bool is_success, bool is_online) +{ + if (old_leader_client) + { + // success flag is set by world to indicate new leader set to an online member + if (is_success) + { + old_leader_client->MessageString(Chat::Yellow, DZMAKELEADER_NAME, new_leader_name.c_str()); + } + else if (!is_online) + { + old_leader_client->MessageString(Chat::Red, DZMAKELEADER_NOT_ONLINE, new_leader_name.c_str()); + } + else + { + old_leader_client->MessageString(Chat::Red, EXPEDITION_NOT_MEMBER, new_leader_name.c_str()); + } + } + + if (is_success && new_leader_client && !RuleB(Expedition, AlwaysNotifyNewLeaderOnChange)) + { + new_leader_client->MessageString(Chat::Yellow, DZMAKELEADER_YOU); + } +} + +void Expedition::ProcessMemberAdded(const std::string& char_name, uint32_t added_char_id) +{ + // adds the member to this expedition and notifies both leader and new member + Client* leader_client = entity_list.GetClientByCharID(m_leader.char_id); + if (leader_client) + { + leader_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); + } + + AddInternalMember(char_name, added_char_id, ExpeditionMemberStatus::Online); + + Client* member_client = entity_list.GetClientByCharID(added_char_id); + if (member_client) + { + member_client->SetExpeditionID(GetID()); + member_client->SendDzCompassUpdate(); + SendClientExpeditionInfo(member_client); + member_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); + } + + SendNewMemberAddedToZoneMembers(char_name); +} + +void Expedition::ProcessMemberRemoved(const std::string& removed_char_name, uint32_t removed_char_id) +{ + if (m_members.empty()) + { + return; + } + + auto outapp_member_name = CreateMemberListNamePacket(removed_char_name, true); + + for (auto it = m_members.begin(); it != m_members.end();) + { + bool is_removed = (it->name == removed_char_name); + + Client* member_client = entity_list.GetClientByCharID(it->char_id); + if (member_client) + { + // all members receive the removed player name packet + member_client->QueuePacket(outapp_member_name.get()); + + if (is_removed) + { + // live doesn't clear expedition info on clients removed while inside dz. + // it instead let's the dz kick timer do it even if character zones out + // before it triggers. for simplicity we'll always clear immediately + member_client->SetExpeditionID(0); + member_client->SendDzCompassUpdate(); + member_client->QueuePacket(CreateInfoPacket(true).get()); + member_client->MessageString(Chat::Yellow, EXPEDITION_REMOVED, + it->name.c_str(), m_expedition_name.c_str()); + } + } + + it = is_removed ? m_members.erase(it) : it + 1; + } + + LogExpeditionsDetail( + "Processed member [{}] ({}) removal from [{}], cache member count: [{}]", + removed_char_name, removed_char_id, m_id, m_members.size() + ); +} + +void Expedition::ProcessLockoutDuration( + const ExpeditionLockoutTimer& lockout, int seconds, bool members_only) +{ + if (!members_only) + { + auto it = m_lockouts.find(lockout.GetEventName()); + if (it != m_lockouts.end()) + { + it->second.AddLockoutTime(seconds); + } + else + { + m_lockouts[lockout.GetEventName()] = lockout; + } + } + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->AddExpeditionLockoutDuration(m_expedition_name, + lockout.GetEventName(), seconds, m_uuid); + } + } + + if (m_dynamiczone.IsCurrentZoneDzInstance()) + { + AddLockoutDurationClients(lockout, seconds, GetID()); + } +} + +void Expedition::AddLockoutDurationClients( + const ExpeditionLockoutTimer& lockout, int seconds, uint32_t exclude_id) +{ + std::vector lockout_clients; + for (const auto& client_iter : entity_list.GetClientList()) + { + Client* client = client_iter.second; + if (client && (exclude_id == 0 || client->GetExpeditionID() != exclude_id)) + { + lockout_clients.emplace_back(client->CharacterID(), client->GetName()); + client->AddExpeditionLockoutDuration(m_expedition_name, + lockout.GetEventName(), seconds, m_uuid); + } + } + + if (!lockout_clients.empty()) + { + int modified_seconds = static_cast(seconds * RuleR(Expedition, LockoutDurationMultiplier)); + ExpeditionDatabase::AddLockoutDuration(lockout_clients, lockout, modified_seconds); + } +} + +void Expedition::ProcessLockoutUpdate( + const ExpeditionLockoutTimer& lockout, bool remove, bool members_only) +{ + if (!members_only) + { + if (!remove) + { + m_lockouts[lockout.GetEventName()] = lockout; + } + else + { + m_lockouts.erase(lockout.GetEventName()); + } + } + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + if (!remove) + { + member_client->AddExpeditionLockout(lockout); + } + else + { + member_client->RemoveExpeditionLockout(m_expedition_name, lockout.GetEventName()); + } + } + } + + // if this is the expedition's dz instance, all clients inside the zone need + // to receive added lockouts. this is done on live to avoid exploits where + // members leave the expedition but haven't been kicked from zone yet + if (!remove && m_dynamiczone.IsCurrentZoneDzInstance()) + { + AddLockoutClients(lockout, GetID()); + } +} + +void Expedition::AddLockoutClients( + const ExpeditionLockoutTimer& lockout, uint32_t exclude_expedition_id) +{ + std::vector lockout_clients; + for (const auto& client_iter : entity_list.GetClientList()) + { + Client* client = client_iter.second; + if (client && (exclude_expedition_id == 0 || client->GetExpeditionID() != exclude_expedition_id)) + { + lockout_clients.emplace_back(client->CharacterID(), client->GetName()); + client->AddExpeditionLockout(lockout); + } + } + + if (!lockout_clients.empty()) + { + ExpeditionDatabase::InsertMembersLockout(lockout_clients, lockout); + } +} + +void Expedition::SendNewMemberAddedToZoneMembers(const std::string& added_name) +{ + // live only sends MemberListName when members are added from a swap, otherwise + // it sends expedition info (unnecessary) and the full member list + // we send a full member list update for both cases since MemberListName adds as + // "unknown" status (either due to unknown packet fields or future client change) + auto outapp_members = CreateMemberListPacket(false); + + for (const auto& member : m_members) + { + if (member.name != added_name) // new member already updated + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp_members.get()); + } + } + } +} + +void Expedition::SendUpdatesToZoneMembers(bool clear, bool message_on_clear) +{ + if (!m_members.empty()) + { + auto outapp_info = CreateInfoPacket(clear); + auto outapp_members = CreateMemberListPacket(clear); + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->SetExpeditionID(clear ? 0 : GetID()); + member_client->SendDzCompassUpdate(); + member_client->QueuePacket(outapp_info.get()); + member_client->QueuePacket(outapp_members.get()); + member_client->SendExpeditionLockoutTimers(); + if (clear && message_on_clear) + { + member_client->MessageString(Chat::Yellow, EXPEDITION_REMOVED, + member_client->GetName(), m_expedition_name.c_str()); + } + } + } + } +} + +void Expedition::SendClientExpeditionInfo(Client* client) +{ + if (client) + { + client->QueuePacket(CreateInfoPacket().get()); + client->QueuePacket(CreateMemberListPacket().get()); + } +} + +void Expedition::SendWorldPendingInvite(const ExpeditionInvite& invite, const std::string& add_name) +{ + LogExpeditions( + "Character [{}] saving pending invite from [{}] to expedition [{}] in world", + add_name, invite.inviter_name, invite.expedition_id + ); + + SendWorldAddPlayerInvite(invite.inviter_name, invite.swap_remove_name, add_name, true); +} + +std::unique_ptr Expedition::CreateExpireWarningPacket(uint32_t minutes_remaining) +{ + uint32_t outsize = sizeof(ExpeditionExpireWarning); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionEndsWarning, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->minutes_remaining = minutes_remaining; + return outapp; +} + +std::unique_ptr Expedition::CreateInfoPacket(bool clear) +{ + uint32_t outsize = sizeof(ExpeditionInfo_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionInfo, outsize)); + auto info = reinterpret_cast(outapp->pBuffer); + if (!clear) + { + info->assigned = true; + strn0cpy(info->expedition_name, m_expedition_name.c_str(), sizeof(info->expedition_name)); + strn0cpy(info->leader_name, m_leader.name.c_str(), sizeof(info->leader_name)); + info->max_players = m_max_players; + } + return outapp; +} + +std::unique_ptr Expedition::CreateInvitePacket( + const std::string& inviter_name, const std::string& swap_remove_name) +{ + uint32_t outsize = sizeof(ExpeditionInvite_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionInvite, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + strn0cpy(outbuf->inviter_name, inviter_name.c_str(), sizeof(outbuf->inviter_name)); + strn0cpy(outbuf->expedition_name, m_expedition_name.c_str(), sizeof(outbuf->expedition_name)); + strn0cpy(outbuf->swap_name, swap_remove_name.c_str(), sizeof(outbuf->swap_name)); + outbuf->swapping = !swap_remove_name.empty(); + outbuf->dz_zone_id = m_dynamiczone.GetZoneID(); + outbuf->dz_instance_id = m_dynamiczone.GetInstanceID(); + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListPacket(bool clear) +{ + uint32_t member_count = clear ? 0 : static_cast(m_members.size()); + uint32_t member_entries_size = sizeof(ExpeditionMemberEntry_Struct) * member_count; + uint32_t outsize = sizeof(ExpeditionMemberList_Struct) + member_entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberList, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + + buf->member_count = member_count; + + if (!clear) + { + for (auto i = 0; i < m_members.size(); ++i) + { + strn0cpy(buf->members[i].name, m_members[i].name.c_str(), sizeof(buf->members[i].name)); + buf->members[i].expedition_status = static_cast(m_members[i].status); + } + } + + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListNamePacket( + const std::string& name, bool remove_name) +{ + uint32_t outsize = sizeof(ExpeditionMemberListName_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberListName, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->add_name = !remove_name; + strn0cpy(buf->name, name.c_str(), sizeof(buf->name)); + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListStatusPacket( + const std::string& name, ExpeditionMemberStatus status) +{ + // member list status uses member list struct with a single entry + uint32_t outsize = sizeof(ExpeditionMemberList_Struct) + sizeof(ExpeditionMemberEntry_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberListStatus, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->member_count = 1; + + auto entry = reinterpret_cast(buf->members); + strn0cpy(entry->name, name.c_str(), sizeof(entry->name)); + entry->expedition_status = static_cast(status); + + return outapp; +} + +std::unique_ptr Expedition::CreateLeaderNamePacket() +{ + uint32_t outsize = sizeof(ExpeditionSetLeaderName_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzSetLeaderName, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + strn0cpy(buf->leader_name, m_leader.name.c_str(), sizeof(buf->leader_name)); + return outapp; +} + +void Expedition::SendWorldExpeditionUpdate(uint16_t server_opcode) +{ + uint32_t pack_size = sizeof(ServerExpeditionID_Struct); + auto pack = std::unique_ptr(new ServerPacket(server_opcode, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldAddPlayerInvite( + const std::string& inviter_name, const std::string& swap_remove_name, const std::string& add_name, bool pending) +{ + auto server_opcode = pending ? ServerOP_ExpeditionSaveInvite : ServerOP_ExpeditionDzAddPlayer; + uint32_t pack_size = sizeof(ServerDzCommand_Struct); + auto pack = std::unique_ptr(new ServerPacket(server_opcode, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->is_char_online = false; + strn0cpy(buf->requester_name, inviter_name.c_str(), sizeof(buf->requester_name)); + strn0cpy(buf->target_name, add_name.c_str(), sizeof(buf->target_name)); + strn0cpy(buf->remove_name, swap_remove_name.c_str(), sizeof(buf->remove_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldLockoutDuration( + const ExpeditionLockoutTimer& lockout, int seconds, bool members_only) +{ + uint32_t pack_size = sizeof(ServerExpeditionLockout_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionLockoutDuration, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->expire_time = lockout.GetExpireTime(); + buf->duration = lockout.GetDuration(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->members_only = members_only; + buf->seconds_adjust = seconds; + strn0cpy(buf->event_name, lockout.GetEventName().c_str(), sizeof(buf->event_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldLockoutUpdate( + const ExpeditionLockoutTimer& lockout, bool remove, bool members_only) +{ + uint32_t pack_size = sizeof(ServerExpeditionLockout_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionLockout, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->expire_time = lockout.GetExpireTime(); + buf->duration = lockout.GetDuration(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->remove = remove; + buf->members_only = members_only; + strn0cpy(buf->event_name, lockout.GetEventName().c_str(), sizeof(buf->event_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMakeLeaderRequest(uint32_t requester_id, const std::string& new_leader_name) +{ + uint32_t pack_size = sizeof(ServerDzCommandMakeLeader_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionDzMakeLeader, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->requester_id = requester_id; + strn0cpy(buf->new_leader_name, new_leader_name.c_str(), sizeof(buf->new_leader_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberChanged(const std::string& char_name, uint32_t char_id, bool remove) +{ + // notify other zones of added or removed member + uint32_t pack_size = sizeof(ServerExpeditionMemberChange_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberChange, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->removed = remove; + buf->char_id = char_id; + strn0cpy(buf->char_name, char_name.c_str(), sizeof(buf->char_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberStatus(uint32_t character_id, ExpeditionMemberStatus status) +{ + uint32_t pack_size = sizeof(ServerExpeditionMemberStatus_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberStatus, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->status = static_cast(status); + buf->character_id = character_id; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldDzLocationUpdate(uint16_t server_opcode, const DynamicZoneLocation& location) +{ + uint32_t pack_size = sizeof(ServerDzLocation_Struct); + auto pack = std::unique_ptr(new ServerPacket(server_opcode, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->owner_id = GetID(); + buf->dz_zone_id = m_dynamiczone.GetZoneID(); + buf->dz_instance_id = m_dynamiczone.GetInstanceID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->zone_id = location.zone_id; + buf->x = location.x; + buf->y = location.y; + buf->z = location.z; + buf->heading = location.heading; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberSwapped( + const std::string& remove_char_name, uint32_t remove_char_id, const std::string& add_char_name, uint32_t add_char_id) +{ + uint32_t pack_size = sizeof(ServerExpeditionMemberSwap_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberSwap, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->add_char_id = add_char_id; + buf->remove_char_id = remove_char_id; + strn0cpy(buf->add_char_name, add_char_name.c_str(), sizeof(buf->add_char_name)); + strn0cpy(buf->remove_char_name, remove_char_name.c_str(), sizeof(buf->remove_char_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldSettingChanged(uint16_t server_opcode, bool setting_value) +{ + uint32_t pack_size = sizeof(ServerExpeditionSetting_Struct); + auto pack = std::unique_ptr(new ServerPacket(server_opcode, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->enabled = setting_value; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldGetOnlineMembers( + const std::vector>& expedition_character_ids) +{ + // request online status of characters + uint32_t count = static_cast(expedition_character_ids.size()); + uint32_t entries_size = sizeof(ServerExpeditionCharacterEntry_Struct) * count; + uint32_t pack_size = sizeof(ServerExpeditionCharacters_Struct) + entries_size; + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionGetOnlineMembers, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->count = count; + for (uint32_t i = 0; i < buf->count; ++i) + { + buf->entries[i].expedition_id = expedition_character_ids[i].first; + buf->entries[i].character_id = expedition_character_ids[i].second; + buf->entries[i].character_zone_id = 0; + buf->entries[i].character_instance_id = 0; + buf->entries[i].character_online = false; + } + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldCharacterLockout( + uint32_t character_id, const ExpeditionLockoutTimer& lockout, bool remove) +{ + uint32_t pack_size = sizeof(ServerExpeditionCharacterLockout_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionCharacterLockout, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->remove = remove; + buf->character_id = character_id; + buf->expire_time = lockout.GetExpireTime(); + buf->duration = lockout.GetDuration(); + strn0cpy(buf->uuid, lockout.GetExpeditionUUID().c_str(), sizeof(buf->uuid)); + strn0cpy(buf->expedition_name, lockout.GetExpeditionName().c_str(), sizeof(buf->expedition_name)); + strn0cpy(buf->event_name, lockout.GetEventName().c_str(), sizeof(buf->event_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldSetSecondsRemaining(uint32_t seconds_remaining) +{ + uint32_t pack_size = sizeof(ServerExpeditionUpdateDuration_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionSecondsRemaining, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->new_duration_seconds = seconds_remaining; + worldserver.SendPacket(pack.get()); +} + +void Expedition::AddLockoutByCharacterID( + uint32_t character_id, const std::string& expedition_name, const std::string& event_name, + uint32_t seconds, const std::string& uuid) +{ + if (character_id) + { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + ExpeditionDatabase::InsertCharacterLockouts(character_id, { lockout }); + SendWorldCharacterLockout(character_id, lockout, false); + } +} + +void Expedition::AddLockoutByCharacterName( + const std::string& character_name, const std::string& expedition_name, const std::string& event_name, + uint32_t seconds, const std::string& uuid) +{ + if (!character_name.empty()) + { + uint32_t character_id = database.GetCharacterID(character_name.c_str()); + AddLockoutByCharacterID(character_id, expedition_name, event_name, seconds, uuid); + } +} + +bool Expedition::HasLockoutByCharacterID( + uint32_t character_id, const std::string& expedition_name, const std::string& event_name) +{ + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(character_id); + return std::any_of(lockouts.begin(), lockouts.end(), [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); +} + +bool Expedition::HasLockoutByCharacterName( + const std::string& character_name, const std::string& expedition_name, const std::string& event_name) +{ + if (!character_name.empty()) + { + uint32_t character_id = database.GetCharacterID(character_name.c_str()); + return HasLockoutByCharacterID(character_id, expedition_name, event_name); + } + return false; +} + +void Expedition::RemoveLockoutsByCharacterID( + uint32_t character_id, const std::string& expedition_name, const std::string& event_name) +{ + if (character_id) + { + if (!event_name.empty()) + { + ExpeditionDatabase::DeleteCharacterLockout(character_id, expedition_name, event_name); + } + else if (!expedition_name.empty()) + { + ExpeditionDatabase::DeleteAllCharacterLockouts(character_id, expedition_name); + } + else + { + ExpeditionDatabase::DeleteAllCharacterLockouts(character_id); + } + + ExpeditionLockoutTimer lockout{{}, expedition_name, event_name, 0, 0}; + SendWorldCharacterLockout(character_id, lockout, true); + } +} + +void Expedition::RemoveLockoutsByCharacterName( + const std::string& character_name, const std::string& expedition_name, const std::string& event_name) +{ + if (!character_name.empty()) + { + uint32_t character_id = database.GetCharacterID(character_name.c_str()); + RemoveLockoutsByCharacterID(character_id, expedition_name, event_name); + } +} + +void Expedition::HandleWorldMessage(ServerPacket* pack) +{ + switch (pack->opcode) + { + case ServerOP_ExpeditionCreate: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + Expedition::CacheFromDatabase(buf->expedition_id); + } + break; + } + case ServerOP_ExpeditionDeleted: + { + // sent by world when it deletes expired or empty expeditions + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (zone && expedition) + { + expedition->SendUpdatesToZoneMembers(true, false); // any members silently removed + + LogExpeditionsModerate("Deleting expedition [{}] from zone cache", buf->expedition_id); + zone->expedition_cache.erase(buf->expedition_id); + } + break; + } + case ServerOP_ExpeditionMembersRemoved: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->SendUpdatesToZoneMembers(true); + expedition->m_members.clear(); + } + } + break; + } + case ServerOP_ExpeditionLeaderChanged: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->ProcessLeaderChanged(buf->leader_id); + } + break; + } + case ServerOP_ExpeditionLockout: + case ServerOP_ExpeditionLockoutDuration: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + ExpeditionLockoutTimer lockout{ expedition->GetUUID(), expedition->GetName(), + buf->event_name, buf->expire_time, buf->duration }; + + if (pack->opcode == ServerOP_ExpeditionLockout) + { + expedition->ProcessLockoutUpdate(lockout, buf->remove, buf->members_only); + } + else if (pack->opcode == ServerOP_ExpeditionLockoutDuration) + { + expedition->ProcessLockoutDuration(lockout, buf->seconds_adjust, buf->members_only); + } + } + } + break; + } + case ServerOP_ExpeditionMemberChange: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + if (buf->removed) + { + expedition->ProcessMemberRemoved(buf->char_name, buf->char_id); + } + else + { + expedition->ProcessMemberAdded(buf->char_name, buf->char_id); + } + } + } + break; + } + case ServerOP_ExpeditionMemberSwap: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->ProcessMemberRemoved(buf->remove_char_name, buf->remove_char_id); + expedition->ProcessMemberAdded(buf->add_char_name, buf->add_char_id); + } + } + break; + } + case ServerOP_ExpeditionMemberStatus: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->UpdateMemberStatus(buf->character_id, static_cast(buf->status)); + } + } + break; + } + case ServerOP_ExpeditionLockState: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->SetLocked(buf->enabled, static_cast(buf->lock_msg)); + } + } + break; + } + case ServerOP_ExpeditionReplayOnJoin: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->SetReplayLockoutOnMemberJoin(buf->enabled); + } + } + break; + } + case ServerOP_ExpeditionGetOnlineMembers: + { + // reply from world for online member statuses request (for multiple expeditions) + auto buf = reinterpret_cast(pack->pBuffer); + for (uint32_t i = 0; i < buf->count; ++i) + { + auto member = reinterpret_cast(&buf->entries[i]); + auto expedition = Expedition::FindCachedExpeditionByID(member->expedition_id); + if (expedition) + { + auto is_online = member->character_online; + auto status = is_online ? ExpeditionMemberStatus::Online : ExpeditionMemberStatus::Offline; + if (is_online && expedition->GetDynamicZone().IsInstanceID(member->character_instance_id)) + { + status = ExpeditionMemberStatus::InDynamicZone; + } + expedition->UpdateMemberStatus(member->character_id, status); + } + } + break; + } + case ServerOP_ExpeditionDzAddPlayer: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (buf->is_char_online) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->DzAddPlayerContinue(buf->requester_name, buf->target_name, buf->remove_name); + } + } + else + { + Client* leader = entity_list.GetClientByName(buf->requester_name); + if (leader) + { + std::string target_name = FormatName(buf->target_name); + leader->MessageString(Chat::Red, DZADD_NOT_ONLINE, target_name.c_str()); + leader->MessageString(Chat::Red, DZADD_INVITE_FAIL, target_name.c_str()); + } + } + break; + } + case ServerOP_ExpeditionDzMakeLeader: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + auto old_leader_client = entity_list.GetClientByCharID(buf->requester_id); + auto new_leader_client = entity_list.GetClientByName(buf->new_leader_name); + expedition->ProcessMakeLeader(old_leader_client, new_leader_client, + buf->new_leader_name, buf->is_success, buf->is_online); + } + break; + } + case ServerOP_ExpeditionDzCompass: + case ServerOP_ExpeditionDzSafeReturn: + case ServerOP_ExpeditionDzZoneIn: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->owner_id); + if (expedition) + { + if (pack->opcode == ServerOP_ExpeditionDzCompass) + { + expedition->SetDzCompass(buf->zone_id, buf->x, buf->y, buf->z, false); + } + else if (pack->opcode == ServerOP_ExpeditionDzSafeReturn) + { + expedition->SetDzSafeReturn(buf->zone_id, buf->x, buf->y, buf->z, buf->heading, false); + } + else if (pack->opcode == ServerOP_ExpeditionDzZoneIn) + { + expedition->SetDzZoneInLocation(buf->x, buf->y, buf->z, buf->heading, false); + } + } + } + break; + } + case ServerOP_ExpeditionCharacterLockout: + { + auto buf = reinterpret_cast(pack->pBuffer); + Client* client = entity_list.GetClientByCharID(buf->character_id); + if (client) + { + if (!buf->remove) + { + client->AddExpeditionLockout(ExpeditionLockoutTimer{ + buf->uuid, buf->expedition_name, buf->event_name, buf->expire_time, buf->duration + }); + } + else if (buf->event_name[0] != '\0') + { + client->RemoveExpeditionLockout(buf->expedition_name, buf->event_name); + } + else + { + client->RemoveAllExpeditionLockouts(buf->expedition_name); + } + } + break; + } + case ServerOP_ExpeditionDzDuration: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->UpdateDzDuration(buf->new_duration_seconds); + } + break; + } + case ServerOP_ExpeditionExpireWarning: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->SendMembersExpireWarning(buf->minutes_remaining); + } + break; + } + } +} + +void Expedition::SetDzCompass(uint32_t zone_id, float x, float y, float z, bool update_db) +{ + DynamicZoneLocation location{ zone_id, x, y, z, 0.0f }; + m_dynamiczone.SetCompass(location, update_db); + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->SendDzCompassUpdate(); + } + } + + if (update_db) + { + SendWorldDzLocationUpdate(ServerOP_ExpeditionDzCompass, location); + } +} + +void Expedition::SetDzCompass(const std::string& zone_name, float x, float y, float z, bool update_db) +{ + auto zone_id = ZoneID(zone_name.c_str()); + SetDzCompass(zone_id, x, y, z, update_db); +} + +void Expedition::SetDzSafeReturn(uint32_t zone_id, float x, float y, float z, float heading, bool update_db) +{ + DynamicZoneLocation location{ zone_id, x, y, z, heading }; + + m_dynamiczone.SetSafeReturn(location, update_db); + + if (update_db) + { + SendWorldDzLocationUpdate(ServerOP_ExpeditionDzSafeReturn, location); + } +} + +void Expedition::SetDzSafeReturn(const std::string& zone_name, float x, float y, float z, float heading, bool update_db) +{ + auto zone_id = ZoneID(zone_name.c_str()); + SetDzSafeReturn(zone_id, x, y, z, heading, update_db); +} + +void Expedition::SetDzSecondsRemaining(uint32_t seconds_remaining) +{ + SendWorldSetSecondsRemaining(seconds_remaining); // async +} + +void Expedition::SetDzZoneInLocation(float x, float y, float z, float heading, bool update_db) +{ + DynamicZoneLocation location{ 0, x, y, z, heading }; + + m_dynamiczone.SetZoneInLocation(location, update_db); + + if (update_db) + { + SendWorldDzLocationUpdate(ServerOP_ExpeditionDzZoneIn, location); + } +} + +bool Expedition::CanClientLootCorpse(Client* client, uint32_t npc_type_id, uint32_t spawn_id) +{ + if (client && m_dynamiczone.IsCurrentZoneDzInstance()) + { + // entity id takes priority, falls back to checking by npc type if not set + std::string event_name = GetLootEventBySpawnID(spawn_id); + if (event_name.empty()) + { + event_name = GetLootEventByNPCTypeID(npc_type_id); + } + + if (!event_name.empty()) + { + auto client_lockout = client->GetExpeditionLockout(GetName(), event_name); + if (!client_lockout || client_lockout->GetExpeditionUUID() != GetUUID()) + { + // client lockout not received in this expedition, prevent looting + LogExpeditions( + "Character [{}] denied looting npc [{}] spawn [{}] for lockout event [{}]", + client->CharacterID(), npc_type_id, spawn_id, event_name + ); + return false; + } + } + } + + return true; +} + +void Expedition::SetLootEventByNPCTypeID(uint32_t npc_type_id, const std::string& event_name) +{ + if (npc_type_id && m_dynamiczone.IsCurrentZoneDzInstance()) + { + LogExpeditions("Setting loot event [{}] for npc type id [{}]", event_name, npc_type_id); + m_npc_loot_events[npc_type_id] = event_name; + } +} + +void Expedition::SetLootEventBySpawnID(uint32_t spawn_id, const std::string& event_name) +{ + if (spawn_id && m_dynamiczone.IsCurrentZoneDzInstance()) + { + LogExpeditions("Setting loot event [{}] for entity id [{}]", event_name, spawn_id); + m_spawn_loot_events[spawn_id] = event_name; + } +} + +std::string Expedition::GetLootEventByNPCTypeID(uint32_t npc_type_id) +{ + std::string event_name; + + if (npc_type_id && m_dynamiczone.IsCurrentZoneDzInstance()) + { + auto it = m_npc_loot_events.find(npc_type_id); + if (it != m_npc_loot_events.end()) + { + event_name = it->second; + } + } + + return event_name; +} + +std::string Expedition::GetLootEventBySpawnID(uint32_t spawn_id) +{ + std::string event_name; + + if (spawn_id && m_dynamiczone.IsCurrentZoneDzInstance()) + { + auto it = m_spawn_loot_events.find(spawn_id); + if (it != m_spawn_loot_events.end()) + { + event_name = it->second; + } + } + + return event_name; +} + +std::vector Expedition::GetExpeditionLockoutsByCharacterID(uint32_t character_id) +{ + std::vector lockouts; + if (character_id == 0) + { + return lockouts; + } + + auto client = entity_list.GetClientByCharID(character_id); + if (client) + { + lockouts = client->GetExpeditionLockouts(); + } + else + { + lockouts = ExpeditionDatabase::LoadCharacterLockouts(character_id); + } + + return lockouts; +} + +void Expedition::SendMembersExpireWarning(uint32_t minutes_remaining) +{ + // expeditions warn members in all zones not just the dz + auto outapp = CreateExpireWarningPacket(minutes_remaining); + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp.get()); + member_client->MessageString(Chat::Yellow, EXPEDITION_MIN_REMAIN, + fmt::format_int(minutes_remaining).c_str()); + } + } +} + +void Expedition::SyncCharacterLockouts( + uint32_t character_id, std::vector& client_lockouts) +{ + // adds missing event lockouts to client for this expedition and updates + // client timers that are both shorter and from another expedition + BenchTimer benchmark; + + bool modified = false; + + for (const auto& lockout_iter : m_lockouts) + { + const ExpeditionLockoutTimer& lockout = lockout_iter.second; + if (lockout.IsReplayTimer() || lockout.IsExpired() || lockout.GetExpeditionUUID() != m_uuid) + { + continue; + } + + auto client_lockout_iter = std::find_if(client_lockouts.begin(), client_lockouts.end(), + [&](const ExpeditionLockoutTimer& client_lockout) { + return client_lockout.IsSameLockout(lockout); + }); + + if (client_lockout_iter == client_lockouts.end()) + { + modified = true; + client_lockouts.emplace_back(lockout); // insert missing + } + else if (client_lockout_iter->GetSecondsRemaining() < lockout.GetSecondsRemaining() && + client_lockout_iter->GetExpeditionUUID() != m_uuid) + { + // only update lockout timer not uuid so loot event apis still work + modified = true; + client_lockout_iter->SetDuration(lockout.GetDuration()); + client_lockout_iter->SetExpireTime(lockout.GetExpireTime()); + } + } + + if (modified) + { + ExpeditionDatabase::InsertCharacterLockouts(character_id, client_lockouts); + } + + LogExpeditionsDetail("Syncing character lockouts with expedition took [{}] s", benchmark.elapsed()); +} diff --git a/zone/expedition.h b/zone/expedition.h new file mode 100644 index 000000000..7e6c4a048 --- /dev/null +++ b/zone/expedition.h @@ -0,0 +1,242 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EXPEDITION_H +#define EXPEDITION_H + +#include "dynamiczone.h" +#include "expedition_lockout_timer.h" +#include "../common/eq_constants.h" +#include +#include +#include +#include +#include + +class Client; +class EQApplicationPacket; +struct ExpeditionInvite; +class ExpeditionRequest; +class MySQLRequestResult; +class ServerPacket; + +extern const char* const DZ_YOU_NOT_ASSIGNED; +extern const char* const EXPEDITION_OTHER_BELONGS; +extern const char* const CREATE_NOT_ALL_ADDED; + +enum class ExpeditionMemberStatus : uint8_t +{ + Unknown = 0, + Online, + Offline, + InDynamicZone, + LinkDead +}; + +enum class ExpeditionLockMessage : uint8_t +{ + None = 0, + Close, + Begin +}; + +struct ExpeditionMember +{ + uint32_t char_id = 0; + std::string name; + ExpeditionMemberStatus status = ExpeditionMemberStatus::Online; + + ExpeditionMember() = default; + ExpeditionMember(uint32_t char_id_, const std::string& name_) + : char_id(char_id_), name(name_) {} + ExpeditionMember(uint32_t char_id_, const std::string& name_, ExpeditionMemberStatus status_) + : char_id(char_id_), name(name_), status(status_) {} + + bool IsValid() const { return char_id != 0 && !name.empty(); } +}; + +class Expedition +{ +public: + Expedition() = delete; + Expedition(uint32_t id, const std::string& uuid, const DynamicZone& dz, const std::string& expedition_name, + const ExpeditionMember& leader, uint32_t min_players, uint32_t max_players); + + static Expedition* TryCreate(Client* requester, DynamicZone& dynamiczone, ExpeditionRequest& request); + + static void CacheFromDatabase(uint32_t expedition_id); + static bool CacheAllFromDatabase(); + static Expedition* FindCachedExpeditionByCharacterID(uint32_t character_id); + static Expedition* FindCachedExpeditionByCharacterName(const std::string& char_name); + static Expedition* FindCachedExpeditionByDynamicZoneID(uint32_t dz_id); + static Expedition* FindCachedExpeditionByID(uint32_t expedition_id); + static Expedition* FindCachedExpeditionByZoneInstance(uint32_t zone_id, uint32_t instance_id); + static std::vector GetExpeditionLockoutsByCharacterID(uint32_t character_id); + static void HandleWorldMessage(ServerPacket* pack); + static void AddLockoutByCharacterID(uint32_t character_id, const std::string& expedition_name, + const std::string& event_name, uint32_t seconds, const std::string& uuid = {}); + static void AddLockoutByCharacterName(const std::string& character_name, const std::string& expedition_name, + const std::string& event_name, uint32_t seconds, const std::string& uuid = {}); + static bool HasLockoutByCharacterID(uint32_t character_id, + const std::string& expedition_name, const std::string& event_name); + static bool HasLockoutByCharacterName(const std::string& character_name, + const std::string& expedition_name, const std::string& event_name); + static void RemoveLockoutsByCharacterID(uint32_t character_id, + const std::string& expedition_name = {}, const std::string& event_name = {}); + static void RemoveLockoutsByCharacterName(const std::string& character_name, + const std::string& expedition_name = {}, const std::string& event_name = {}); + static void AddLockoutClients(const ExpeditionLockoutTimer& lockout, uint32_t exclude_id = 0); + + uint32_t GetDynamicZoneID() const { return m_dynamiczone.GetID(); } + uint32_t GetID() const { return m_id; } + uint16_t GetInstanceID() const { return m_dynamiczone.GetInstanceID(); } + uint32_t GetLeaderID() const { return m_leader.char_id; } + uint32_t GetMinPlayers() const { return m_min_players; } + uint32_t GetMaxPlayers() const { return m_max_players; } + uint32_t GetMemberCount() const { return static_cast(m_members.size()); } + const DynamicZone& GetDynamicZone() const { return m_dynamiczone; } + const std::string& GetName() const { return m_expedition_name; } + const std::string& GetLeaderName() const { return m_leader.name; } + const std::string& GetUUID() const { return m_uuid; } + const std::unordered_map& GetLockouts() const { return m_lockouts; } + const std::vector& GetMembers() const { return m_members; } + + bool AddMember(const std::string& add_char_name, uint32_t add_char_id); + bool HasMember(const std::string& character_name); + bool HasMember(uint32_t character_id); + void RemoveAllMembers(bool enable_removal_timers = true); + bool RemoveMember(const std::string& remove_char_name); + void SetMemberStatus(Client* client, ExpeditionMemberStatus status); + void SwapMember(Client* add_client, const std::string& remove_char_name); + void SetLocked(bool lock_expedition, ExpeditionLockMessage lock_msg, + bool update_db = false, uint32_t msg_color = Chat::Yellow); + + void AddLockout(const std::string& event_name, uint32_t seconds); + void AddLockoutDuration(const std::string& event_name, int seconds, bool members_only = true); + void AddReplayLockout(uint32_t seconds); + void AddReplayLockoutDuration(int seconds, bool members_only = true); + bool HasLockout(const std::string& event_name); + bool HasReplayLockout(); + void RemoveLockout(const std::string& event_name); + void SetReplayLockoutOnMemberJoin(bool add_on_join, bool update_db = false); + void SyncCharacterLockouts(uint32_t character_id, std::vector& client_lockouts); + void UpdateLockoutDuration(const std::string& event_name, uint32_t seconds, bool members_only = true); + + bool CanClientLootCorpse(Client* client, uint32_t npc_type_id, uint32_t spawn_id); + std::string GetLootEventByNPCTypeID(uint32_t npc_id); + std::string GetLootEventBySpawnID(uint32_t spawn_id); + void SetLootEventByNPCTypeID(uint32_t npc_type_id, const std::string& event_name); + void SetLootEventBySpawnID(uint32_t spawn_id, const std::string& event_name); + + void SendClientExpeditionInfo(Client* client); + void SendWorldMakeLeaderRequest(uint32_t requester_id, const std::string& new_leader_name); + void SendWorldPendingInvite(const ExpeditionInvite& invite, const std::string& add_name); + + void DzAddPlayer(Client* requester, const std::string& add_char_name, const std::string& swap_remove_name = {}); + void DzAddPlayerContinue(std::string leader_name, std::string add_char_name, std::string swap_remove_name = {}); + void DzInviteResponse(Client* add_client, bool accepted, const std::string& swap_remove_name); + void DzMakeLeader(Client* requester, std::string new_leader_name); + void DzPlayerList(Client* requester); + void DzRemovePlayer(Client* requester, std::string remove_char_name); + void DzSwapPlayer(Client* requester, std::string remove_char_name, std::string add_char_name); + void DzQuit(Client* requester); + void DzKickPlayers(Client* requester); + + void SetDzCompass(uint32_t zone_id, float x, float y, float z, bool update_db = false); + void SetDzCompass(const std::string& zone_name, float x, float y, float z, bool update_db = false); + void SetDzSafeReturn(uint32_t zone_id, float x, float y, float z, float heading, bool update_db = false); + void SetDzSafeReturn(const std::string& zone_name, float x, float y, float z, float heading, bool update_db = false); + void SetDzSecondsRemaining(uint32_t seconds_remaining); + void SetDzZoneInLocation(float x, float y, float z, float heading, bool update_db = false); + + static const int32_t REPLAY_TIMER_ID; + static const int32_t EVENT_TIMER_ID; + +private: + static void CacheExpeditions(MySQLRequestResult& results); + static void SendWorldGetOnlineMembers(const std::vector>& expedition_character_ids); + static void SendWorldCharacterLockout(uint32_t character_id, const ExpeditionLockoutTimer& lockout, bool remove); + + void AddLockout(const ExpeditionLockoutTimer& lockout, bool members_only = false); + void AddLockoutDurationClients(const ExpeditionLockoutTimer& lockout, int seconds, uint32_t exclude_id = 0); + void AddInternalMember(const std::string& char_name, uint32_t char_id, ExpeditionMemberStatus status); + bool ConfirmLeaderCommand(Client* requester); + bool ProcessAddConflicts(Client* leader_client, Client* add_client, bool swapping); + void ProcessLeaderChanged(uint32_t new_leader_id); + void ProcessLockoutDuration(const ExpeditionLockoutTimer& lockout, int seconds, bool members_only = false); + void ProcessLockoutUpdate(const ExpeditionLockoutTimer& lockout, bool remove, bool members_only = false); + void ProcessMakeLeader(Client* old_leader, Client* new_leader, + const std::string& new_leader_name, bool is_success, bool is_online); + void ProcessMemberAdded(const std::string& added_char_name, uint32_t added_char_id); + void ProcessMemberRemoved(const std::string& removed_char_name, uint32_t removed_char_id); + void SaveLockouts(ExpeditionRequest& request); + void SaveMembers(ExpeditionRequest& request); + void SendClientExpeditionInvite( + Client* client, const std::string& inviter_name, const std::string& swap_remove_name); + void SendLeaderMessage(Client* leader_client, uint16_t chat_type, uint32_t string_id, + const std::initializer_list& args = {}); + void SendMembersExpireWarning(uint32_t minutes); + void SendNewMemberAddedToZoneMembers(const std::string& added_name); + void SendUpdatesToZoneMembers(bool clear = false, bool message_on_clear = true); + void SendWorldDzLocationUpdate(uint16_t server_opcode, const DynamicZoneLocation& location); + void SendWorldExpeditionUpdate(uint16_t server_opcode); + void SendWorldAddPlayerInvite(const std::string& inviter_name, const std::string& swap_remove_name, + const std::string& add_name, bool pending = false); + void SendWorldLockoutDuration( + const ExpeditionLockoutTimer& lockout, int seconds, bool members_only = false); + void SendWorldLockoutUpdate( + const ExpeditionLockoutTimer& lockout, bool remove, bool members_only = false); + void SendWorldMemberChanged(const std::string& char_name, uint32_t char_id, bool remove); + void SendWorldMemberStatus(uint32_t character_id, ExpeditionMemberStatus status); + void SendWorldMemberSwapped(const std::string& remove_char_name, uint32_t remove_char_id, + const std::string& add_char_name, uint32_t add_char_id); + void SendWorldSetSecondsRemaining(uint32_t seconds_remaining); + void SendWorldSettingChanged(uint16_t server_opcode, bool setting_value); + void TryAddClient(Client* add_client, const std::string& inviter_name, + const std::string& swap_remove_name, Client* leader_client = nullptr); + void UpdateDzDuration(uint32_t new_duration) { m_dynamiczone.SetUpdatedDuration(new_duration); } + void UpdateMemberStatus(uint32_t update_character_id, ExpeditionMemberStatus status); + + ExpeditionMember GetMemberData(uint32_t character_id); + ExpeditionMember GetMemberData(const std::string& character_name); + std::unique_ptr CreateExpireWarningPacket(uint32_t minutes_remaining); + std::unique_ptr CreateInfoPacket(bool clear = false); + std::unique_ptr CreateInvitePacket(const std::string& inviter_name, const std::string& swap_remove_name); + std::unique_ptr CreateMemberListPacket(bool clear = false); + std::unique_ptr CreateMemberListNamePacket(const std::string& name, bool remove_name); + std::unique_ptr CreateMemberListStatusPacket(const std::string& name, ExpeditionMemberStatus status); + std::unique_ptr CreateLeaderNamePacket(); + + uint32_t m_id = 0; + uint32_t m_min_players = 0; + uint32_t m_max_players = 0; + bool m_is_locked = false; + bool m_add_replay_on_join = true; + std::string m_uuid; + std::string m_expedition_name; + DynamicZone m_dynamiczone { DynamicZoneType::Expedition }; + ExpeditionMember m_leader; + std::vector m_members; + std::unordered_map m_lockouts; + std::unordered_map m_npc_loot_events; // only valid inside dz zone + std::unordered_map m_spawn_loot_events; // only valid inside dz zone +}; + +#endif diff --git a/zone/expedition_database.cpp b/zone/expedition_database.cpp new file mode 100644 index 000000000..7d1fd7a3c --- /dev/null +++ b/zone/expedition_database.cpp @@ -0,0 +1,643 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition_database.h" +#include "expedition.h" +#include "expedition_lockout_timer.h" +#include "zonedb.h" +#include "../common/database.h" +#include "../common/string_util.h" +#include + +uint32_t ExpeditionDatabase::InsertExpedition( + const std::string& uuid, uint32_t dz_id, const std::string& expedition_name, + uint32_t leader_id, uint32_t min_players, uint32_t max_players) +{ + LogExpeditionsDetail( + "Inserting new expedition [{}] leader [{}] uuid [{}]", expedition_name, leader_id, uuid + ); + + std::string query = fmt::format(SQL( + INSERT INTO expeditions + (uuid, dynamic_zone_id, expedition_name, leader_id, min_players, max_players) + VALUES + ('{}', {}, '{}', {}, {}, {}); + ), uuid, dz_id, EscapeString(expedition_name), leader_id, min_players, max_players); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to obtain an expedition id for [{}]", expedition_name); + return 0; + } + + return results.LastInsertedID(); +} + +std::string ExpeditionDatabase::LoadExpeditionsSelectQuery() +{ + return std::string(SQL( + SELECT + expeditions.id, + expeditions.uuid, + expeditions.dynamic_zone_id, + expeditions.expedition_name, + expeditions.leader_id, + expeditions.min_players, + expeditions.max_players, + expeditions.add_replay_on_join, + expeditions.is_locked, + character_data.name leader_name, + expedition_members.character_id, + member_data.name + FROM expeditions + INNER JOIN character_data ON expeditions.leader_id = character_data.id + INNER JOIN expedition_members ON expeditions.id = expedition_members.expedition_id + AND expedition_members.is_current_member = TRUE + INNER JOIN character_data member_data ON expedition_members.character_id = member_data.id + )); +} + +MySQLRequestResult ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) +{ + LogExpeditionsDetail("Loading expedition [{}]", expedition_id); + + std::string query = fmt::format(SQL( + {} WHERE expeditions.id = {}; + ), LoadExpeditionsSelectQuery(), expedition_id); + + return database.QueryDatabase(query); +} + +MySQLRequestResult ExpeditionDatabase::LoadAllExpeditions() +{ + LogExpeditionsDetail("Loading all expeditions from database"); + + std::string query = fmt::format(SQL( + {} ORDER BY expeditions.id; + ), LoadExpeditionsSelectQuery()); + + return database.QueryDatabase(query); +} + +std::vector ExpeditionDatabase::LoadCharacterLockouts(uint32_t character_id) +{ + LogExpeditionsDetail("Loading character [{}] lockouts", character_id); + + std::vector lockouts; + + auto query = fmt::format(SQL( + SELECT + from_expedition_uuid, + expedition_name, + event_name, + UNIX_TIMESTAMP(expire_time), + duration + FROM character_expedition_lockouts + WHERE character_id = {} AND expire_time > NOW(); + ), character_id); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + lockouts.emplace_back( + row[0], // expedition_uuid + row[1], // expedition_name + row[2], // event_name + strtoull(row[3], nullptr, 10), // expire_time + static_cast(strtoul(row[4], nullptr, 10)) // duration + ); + } + } + + return lockouts; +} + +std::vector ExpeditionDatabase::LoadCharacterLockouts( + uint32_t character_id, const std::string& expedition_name) +{ + LogExpeditionsDetail("Loading character [{}] lockouts for [{}]", character_id, expedition_name); + + std::vector lockouts; + + auto query = fmt::format(SQL( + SELECT + from_expedition_uuid, + event_name, + UNIX_TIMESTAMP(expire_time), + duration + FROM character_expedition_lockouts + WHERE + character_id = {} + AND expire_time > NOW() + AND expedition_name = '{}'; + ), character_id, EscapeString(expedition_name)); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + lockouts.emplace_back( + row[0], // expedition_uuid + expedition_name, + row[1], // event_name + strtoull(row[2], nullptr, 10), // expire_time + static_cast(strtoul(row[3], nullptr, 10)) // duration + ); + } + } + + return lockouts; +} + +std::unordered_map> +ExpeditionDatabase::LoadMultipleExpeditionLockouts( + const std::vector& expedition_ids) +{ + LogExpeditionsDetail("Loading internal lockouts for [{}] expeditions", expedition_ids.size()); + + std::string in_expedition_ids_query = fmt::format("{}", fmt::join(expedition_ids, ",")); + + // these are loaded into the same container type expeditions use to store lockouts + std::unordered_map> lockouts; + + if (!in_expedition_ids_query.empty()) + { + std::string query = fmt::format(SQL( + SELECT + expedition_lockouts.expedition_id, + expedition_lockouts.from_expedition_uuid, + expeditions.expedition_name, + expedition_lockouts.event_name, + UNIX_TIMESTAMP(expedition_lockouts.expire_time), + expedition_lockouts.duration + FROM expedition_lockouts + INNER JOIN expeditions ON expedition_lockouts.expedition_id = expeditions.id + WHERE expedition_id IN ({}) + ORDER BY expedition_id; + ), in_expedition_ids_query); + + auto results = database.QueryDatabase(query); + + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + auto expedition_id = strtoul(row[0], nullptr, 10); + lockouts[expedition_id].emplace(row[3], ExpeditionLockoutTimer{ + row[1], // expedition_uuid + row[2], // expedition_name + row[3], // event_name + strtoull(row[4], nullptr, 10), // expire_time + static_cast(strtoul(row[5], nullptr, 10)) // original duration + }); + } + } + } + + return lockouts; +} + +MySQLRequestResult ExpeditionDatabase::LoadMembersForCreateRequest( + const std::vector& character_names, const std::string& expedition_name) +{ + LogExpeditionsDetail( + "Loading data of [{}] characters for [{}] request", character_names.size(), expedition_name + ); + + std::string in_character_names_query; + for (const auto& character_name : character_names) + { + fmt::format_to(std::back_inserter(in_character_names_query), "'{}',", character_name); + } + + MySQLRequestResult results; + + if (!in_character_names_query.empty()) + { + in_character_names_query.pop_back(); // trailing comma + + // for create validation, loads each character's lockouts and possible current expedition + auto query = fmt::format(SQL( + SELECT + character_data.id, + character_data.name, + member.expedition_id, + lockout.from_expedition_uuid, + UNIX_TIMESTAMP(lockout.expire_time), + lockout.duration, + lockout.event_name + FROM character_data + LEFT JOIN character_expedition_lockouts lockout + ON character_data.id = lockout.character_id + AND lockout.expire_time > NOW() + AND lockout.expedition_name = '{}' + LEFT JOIN expedition_members member + ON character_data.id = member.character_id + AND member.is_current_member = TRUE + WHERE character_data.name IN ({}) + ORDER BY FIELD(character_data.name, {}) + ), EscapeString(expedition_name), in_character_names_query, in_character_names_query); + + results = database.QueryDatabase(query); + } + + return results; +} + +void ExpeditionDatabase::DeleteAllCharacterLockouts(uint32_t character_id) +{ + LogExpeditionsDetail("Deleting all character [{}] lockouts", character_id); + + if (character_id != 0) + { + std::string query = fmt::format(SQL( + DELETE FROM character_expedition_lockouts + WHERE character_id = {}; + ), character_id); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::DeleteAllCharacterLockouts( + uint32_t character_id, const std::string& expedition_name) +{ + LogExpeditionsDetail("Deleting all character [{}] lockouts for [{}]", character_id, expedition_name); + + if (character_id != 0 && !expedition_name.empty()) + { + std::string query = fmt::format(SQL( + DELETE FROM character_expedition_lockouts + WHERE character_id = {} AND expedition_name = '{}'; + ), character_id, EscapeString(expedition_name)); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::DeleteCharacterLockout( + uint32_t character_id, const std::string& expedition_name, const std::string& event_name) +{ + LogExpeditionsDetail( + "Deleting character [{}] lockout: [{}]:[{}]", character_id, expedition_name, event_name + ); + + auto query = fmt::format(SQL( + DELETE FROM character_expedition_lockouts + WHERE + character_id = {} + AND expedition_name = '{}' + AND event_name = '{}'; + ), character_id, EscapeString(expedition_name), EscapeString(event_name)); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::DeleteMembersLockout( + const std::vector& members, + const std::string& expedition_name, const std::string& event_name) +{ + LogExpeditionsDetail("Deleting members lockout: [{}]:[{}]", expedition_name, event_name); + + std::string query_character_ids; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(query_character_ids), "{},", member.char_id); + } + + if (!query_character_ids.empty()) + { + query_character_ids.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + DELETE FROM character_expedition_lockouts + WHERE character_id + IN ({}) + AND expedition_name = '{}' + AND event_name = '{}'; + ), query_character_ids, EscapeString(expedition_name), EscapeString(event_name)); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::DeleteLockout(uint32_t expedition_id, const std::string& event_name) +{ + LogExpeditionsDetail("Deleting expedition [{}] lockout event [{}]", expedition_id, event_name); + + auto query = fmt::format(SQL( + DELETE FROM expedition_lockouts + WHERE expedition_id = {} AND event_name = '{}'; + ), expedition_id, EscapeString(event_name)); + + database.QueryDatabase(query); +} + +uint32_t ExpeditionDatabase::GetExpeditionIDFromCharacterID(uint32_t character_id) +{ + LogExpeditionsDetail("Getting expedition id for character [{}]", character_id); + + uint32_t expedition_id = 0; + auto query = fmt::format(SQL( + SELECT expedition_id FROM expedition_members + WHERE character_id = {} AND is_current_member = TRUE; + ), character_id); + + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) + { + auto row = results.begin(); + expedition_id = strtoul(row[0], nullptr, 10); + } + return expedition_id; +} + +void ExpeditionDatabase::InsertCharacterLockouts(uint32_t character_id, + const std::vector& lockouts) +{ + LogExpeditionsDetail("Inserting [{}] lockouts for character [{}]", lockouts.size(), character_id); + + std::string insert_values; + for (const auto& lockout : lockouts) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, FROM_UNIXTIME({}), {}, '{}', '{}', '{}'),", + character_id, + lockout.GetExpireTime(), + lockout.GetDuration(), + lockout.GetExpeditionUUID(), + EscapeString(lockout.GetExpeditionName()), + EscapeString(lockout.GetEventName()) + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO character_expedition_lockouts + (character_id, expire_time, duration, from_expedition_uuid, expedition_name, event_name) + VALUES {} + ON DUPLICATE KEY UPDATE + from_expedition_uuid = VALUES(from_expedition_uuid), + expire_time = VALUES(expire_time), + duration = VALUES(duration); + ), insert_values); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::InsertMembersLockout( + const std::vector& members, const ExpeditionLockoutTimer& lockout) +{ + LogExpeditionsDetail( + "Inserting members lockout [{}]:[{}] with expire time [{}]", + lockout.GetExpeditionName(), lockout.GetEventName(), lockout.GetExpireTime() + ); + + std::string insert_values; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, FROM_UNIXTIME({}), {}, '{}', '{}', '{}'),", + member.char_id, + lockout.GetExpireTime(), + lockout.GetDuration(), + lockout.GetExpeditionUUID(), + EscapeString(lockout.GetExpeditionName()), + EscapeString(lockout.GetEventName()) + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO character_expedition_lockouts + (character_id, expire_time, duration, from_expedition_uuid, expedition_name, event_name) + VALUES {} + ON DUPLICATE KEY UPDATE + from_expedition_uuid = VALUES(from_expedition_uuid), + expire_time = VALUES(expire_time), + duration = VALUES(duration); + ), insert_values); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::InsertLockout( + uint32_t expedition_id, const ExpeditionLockoutTimer& lockout) +{ + LogExpeditionsDetail( + "Inserting expedition [{}] lockout: [{}]:[{}] expire time: [{}]", + expedition_id, lockout.GetExpeditionName(), lockout.GetEventName(), lockout.GetExpireTime() + ); + + auto query = fmt::format(SQL( + INSERT INTO expedition_lockouts + (expedition_id, from_expedition_uuid, event_name, expire_time, duration) + VALUES + ({}, '{}', '{}', FROM_UNIXTIME({}), {}) + ON DUPLICATE KEY UPDATE + from_expedition_uuid = VALUES(from_expedition_uuid), + expire_time = VALUES(expire_time), + duration = VALUES(duration); + ), + expedition_id, + lockout.GetExpeditionUUID(), + EscapeString(lockout.GetEventName()), + lockout.GetExpireTime(), + lockout.GetDuration() + ); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::InsertLockouts( + uint32_t expedition_id, const std::unordered_map& lockouts) +{ + LogExpeditionsDetail("Inserting expedition [{}] lockouts", expedition_id); + + std::string insert_values; + for (const auto& lockout : lockouts) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, '{}', '{}', FROM_UNIXTIME({}), {}),", + expedition_id, + lockout.second.GetExpeditionUUID(), + EscapeString(lockout.second.GetEventName()), + lockout.second.GetExpireTime(), + lockout.second.GetDuration() + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO expedition_lockouts + (expedition_id, from_expedition_uuid, event_name, expire_time, duration) + VALUES {} + ON DUPLICATE KEY UPDATE + from_expedition_uuid = VALUES(from_expedition_uuid), + expire_time = VALUES(expire_time), + duration = VALUES(duration); + ), insert_values); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::InsertMember(uint32_t expedition_id, uint32_t character_id) +{ + LogExpeditionsDetail("Inserting character [{}] into expedition [{}]", character_id, expedition_id); + + auto query = fmt::format(SQL( + INSERT INTO expedition_members + (expedition_id, character_id) + VALUES + ({}, {}) + ON DUPLICATE KEY UPDATE is_current_member = TRUE; + ), expedition_id, character_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::InsertMembers( + uint32_t expedition_id, const std::vector& members) +{ + LogExpeditionsDetail("Inserting characters into expedition [{}]", expedition_id); + + std::string insert_values; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, {}),", + expedition_id, member.char_id + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO expedition_members + (expedition_id, character_id) + VALUES {} + ON DUPLICATE KEY UPDATE is_current_member = TRUE; + ), insert_values); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::UpdateLockState(uint32_t expedition_id, bool is_locked) +{ + LogExpeditionsDetail("Updating lock state [{}] for expedition [{}]", is_locked, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expeditions SET is_locked = {} WHERE id = {}; + ), is_locked, expedition_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::DeleteMember(uint32_t expedition_id, uint32_t character_id) +{ + LogExpeditionsDetail("Removing member [{}] from expedition [{}]", character_id, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expedition_members SET is_current_member = FALSE + WHERE expedition_id = {} AND character_id = {}; + ), expedition_id, character_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::DeleteAllMembers(uint32_t expedition_id) +{ + LogExpeditionsDetail("Removing all members of expedition [{}]", expedition_id); + + auto query = fmt::format(SQL( + UPDATE expedition_members SET is_current_member = FALSE WHERE expedition_id = {}; + ), expedition_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::UpdateReplayLockoutOnJoin(uint32_t expedition_id, bool add_on_join) +{ + LogExpeditionsDetail("Updating replay lockout on join [{}] for expedition [{}]", add_on_join, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expeditions SET add_replay_on_join = {} WHERE id = {}; + ), add_on_join, expedition_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::AddLockoutDuration(const std::vector& members, + const ExpeditionLockoutTimer& lockout, int seconds) +{ + LogExpeditionsDetail( + "Adding duration [{}] seconds to members lockouts [{}]:[{}]", + seconds, lockout.GetExpeditionName(), lockout.GetEventName()); + + std::string insert_values; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, FROM_UNIXTIME({}), {}, '{}', '{}', '{}'),", + member.char_id, + lockout.GetExpireTime(), + lockout.GetDuration(), + lockout.GetExpeditionUUID(), + EscapeString(lockout.GetExpeditionName()), + EscapeString(lockout.GetEventName()) + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO character_expedition_lockouts + (character_id, expire_time, duration, from_expedition_uuid, expedition_name, event_name) + VALUES {} + ON DUPLICATE KEY UPDATE + from_expedition_uuid = VALUES(from_expedition_uuid), + expire_time = DATE_ADD(expire_time, INTERVAL {} SECOND), + duration = GREATEST(0, CAST(duration AS SIGNED) + {}); + ), insert_values, seconds, seconds); + + database.QueryDatabase(query); + } +} diff --git a/zone/expedition_database.h b/zone/expedition_database.h new file mode 100644 index 000000000..2efeba2f6 --- /dev/null +++ b/zone/expedition_database.h @@ -0,0 +1,112 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EXPEDITION_DATABASE_H +#define EXPEDITION_DATABASE_H + +#include +#include +#include +#include +#include +#include + +class Expedition; +class ExpeditionLockoutTimer; +struct ExpeditionMember; +class MySQLRequestResult; + +namespace ExpeditionDatabase +{ + uint32_t InsertExpedition( + const std::string& uuid, uint32_t instance_id, const std::string& expedition_name, + uint32_t leader_id, uint32_t min_players, uint32_t max_players); + std::string LoadExpeditionsSelectQuery(); + MySQLRequestResult LoadExpedition(uint32_t expedition_id); + MySQLRequestResult LoadAllExpeditions(); + MySQLRequestResult LoadMembersForCreateRequest( + const std::vector& character_names, const std::string& expedition_name); + std::vector LoadCharacterLockouts(uint32_t character_id); + std::vector LoadCharacterLockouts(uint32_t character_id, + const std::string& expedition_name); + std::unordered_map> + LoadMultipleExpeditionLockouts(const std::vector& expedition_ids); + void DeleteAllMembers(uint32_t expedition_id); + void DeleteMember(uint32_t expedition_id, uint32_t character_id); + void DeleteAllCharacterLockouts(uint32_t character_id); + void DeleteAllCharacterLockouts(uint32_t character_id, const std::string& expedition_name); + void DeleteCharacterLockout(uint32_t character_id, const std::string& expedition_name, + const std::string& event_name); + void DeleteLockout(uint32_t expedition_id, const std::string& event_name); + void DeleteMembersLockout(const std::vector& members, + const std::string& expedition_name, const std::string& event_name); + uint32_t GetExpeditionIDFromCharacterID(uint32_t character_id); + std::pair, std::vector> GetMembersLockout( + const std::vector& members, const std::string& expedition_name, + const std::string& event_name); + void InsertCharacterLockouts(uint32_t character_id, + const std::vector& lockouts); + void InsertMembersLockout(const std::vector& members, + const ExpeditionLockoutTimer& lockout); + void InsertLockout(uint32_t expedition_id, const ExpeditionLockoutTimer& lockout); + void InsertLockouts(uint32_t expedition_id, + const std::unordered_map& lockouts); + void InsertMember(uint32_t expedition_id, uint32_t character_id); + void InsertMembers(uint32_t expedition_id, const std::vector& members); + void UpdateLockState(uint32_t expedition_id, bool is_locked); + void UpdateReplayLockoutOnJoin(uint32_t expedition_id, bool add_on_join); + void AddLockoutDuration(const std::vector& members, + const ExpeditionLockoutTimer& lockout, int seconds); +}; + +namespace LoadExpeditionColumns +{ + enum eLoadExpeditionColumns + { + id = 0, + uuid, + dz_id, + expedition_name, + leader_id, + min_players, + max_players, + add_replay_on_join, + is_locked, + leader_name, + member_id, + member_name + }; +}; + +namespace LoadMembersForCreateRequestColumns +{ + enum eLoadMembersForCreateRequestColumns + { + character_id = 0, + character_name, + character_expedition_id, + lockout_uuid, + lockout_expire_time, + lockout_duration, + lockout_event_name + }; +}; + +#endif diff --git a/zone/expedition_lockout_timer.cpp b/zone/expedition_lockout_timer.cpp new file mode 100644 index 000000000..42a69c6bc --- /dev/null +++ b/zone/expedition_lockout_timer.cpp @@ -0,0 +1,101 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition_lockout_timer.h" +#include "../common/string_util.h" +#include "../common/rulesys.h" +#include "../common/util/uuid.h" +#include + +const char* const DZ_REPLAY_TIMER_NAME = "Replay Timer"; // see December 14, 2016 patch notes + +ExpeditionLockoutTimer::ExpeditionLockoutTimer( + const std::string& expedition_uuid, const std::string& expedition_name, + const std::string& event_name, uint64_t expire_time, uint32_t duration +) : + m_expedition_uuid(expedition_uuid), + m_expedition_name(expedition_name), + m_event_name(event_name), + m_expire_time(std::chrono::system_clock::from_time_t(expire_time)), + m_duration(duration) +{ + if (event_name == DZ_REPLAY_TIMER_NAME) + { + m_is_replay_timer = true; + } +} + +ExpeditionLockoutTimer ExpeditionLockoutTimer::CreateLockout( + const std::string& expedition_name, const std::string& event_name, uint32_t seconds, std::string uuid) +{ + seconds = static_cast(seconds * RuleR(Expedition, LockoutDurationMultiplier)); + + if (uuid.empty()) + { + uuid = EQ::Util::UUID::Generate().ToString(); + } + + ExpeditionLockoutTimer lockout{uuid, expedition_name, event_name, 0, seconds}; + lockout.Reset(); // sets expire time + return lockout; +} + +uint32_t ExpeditionLockoutTimer::GetSecondsRemaining() const +{ + auto now = std::chrono::system_clock::now(); + if (m_expire_time > now) + { + auto remaining = m_expire_time - now; + return static_cast(std::chrono::duration_cast(remaining).count()); + } + return 0; +} + +ExpeditionLockoutTimer::DaysHoursMinutes ExpeditionLockoutTimer::GetDaysHoursMinutesRemaining() const +{ + auto seconds = GetSecondsRemaining(); + return ExpeditionLockoutTimer::DaysHoursMinutes{ + fmt::format_int(seconds / 86400).str(), // days + fmt::format_int((seconds / 3600) % 24).str(), // hours + fmt::format_int((seconds / 60) % 60).str() // minutes + }; +} + +bool ExpeditionLockoutTimer::IsSameLockout(const ExpeditionLockoutTimer& compare_lockout) const +{ + return compare_lockout.IsSameLockout(GetExpeditionName(), GetEventName()); +} + +bool ExpeditionLockoutTimer::IsSameLockout( + const std::string& expedition_name, const std::string& event_name) const +{ + return GetExpeditionName() == expedition_name && GetEventName() == event_name; +} + +void ExpeditionLockoutTimer::AddLockoutTime(int seconds) +{ + seconds = static_cast(seconds * RuleR(Expedition, LockoutDurationMultiplier)); + + auto new_duration = std::max(0, static_cast(m_duration.count()) + seconds); + + auto start_time = m_expire_time - m_duration; + m_duration = std::chrono::seconds(new_duration); + m_expire_time = start_time + m_duration; +} diff --git a/zone/expedition_lockout_timer.h b/zone/expedition_lockout_timer.h new file mode 100644 index 000000000..62c5e4705 --- /dev/null +++ b/zone/expedition_lockout_timer.h @@ -0,0 +1,76 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EXPEDITION_LOCKOUT_TIMER_H +#define EXPEDITION_LOCKOUT_TIMER_H + +#include +#include + +extern const char* const DZ_REPLAY_TIMER_NAME; + +class ExpeditionLockoutTimer +{ +public: + ExpeditionLockoutTimer() = default; + ExpeditionLockoutTimer( + const std::string& expedition_uuid, const std::string& expedition_name, + const std::string& event_name, uint64_t expire_time, uint32_t duration); + + static ExpeditionLockoutTimer CreateLockout( + const std::string& expedition_name, const std::string& event_name, + uint32_t seconds, std::string uuid = {}); + + struct DaysHoursMinutes + { + std::string days; + std::string hours; + std::string mins; + }; + + void AddLockoutTime(int seconds); + uint32_t GetDuration() const { return static_cast(m_duration.count()); } + uint64_t GetExpireTime() const { return std::chrono::system_clock::to_time_t(m_expire_time); } + uint64_t GetStartTime() const { return std::chrono::system_clock::to_time_t(m_expire_time - m_duration); } + uint32_t GetSecondsRemaining() const; + DaysHoursMinutes GetDaysHoursMinutesRemaining() const; + const std::string& GetExpeditionName() const { return m_expedition_name; } + const std::string& GetExpeditionUUID() const { return m_expedition_uuid; } + const std::string& GetEventName() const { return m_event_name; } + bool IsExpired() const { return GetSecondsRemaining() == 0; } + bool IsFromExpedition(const std::string& uuid) const { return uuid == m_expedition_uuid; } + bool IsReplayTimer() const { return m_is_replay_timer; } + bool IsSameLockout(const ExpeditionLockoutTimer& compare_lockout) const; + bool IsSameLockout(const std::string& expedition_name, const std::string& event_name) const; + void Reset() { m_expire_time = std::chrono::system_clock::now() + m_duration; } + void SetDuration(uint32_t seconds) { m_duration = std::chrono::seconds(seconds); } + void SetExpireTime(uint64_t expire_time) { m_expire_time = std::chrono::system_clock::from_time_t(expire_time); } + void SetUUID(const std::string& uuid) { m_expedition_uuid = uuid; } + +private: + bool m_is_replay_timer = false; + std::string m_expedition_uuid; // expedition received in + std::string m_expedition_name; + std::string m_event_name; + std::chrono::seconds m_duration; + std::chrono::time_point m_expire_time; +}; + +#endif diff --git a/zone/expedition_request.cpp b/zone/expedition_request.cpp new file mode 100644 index 000000000..dedff6dcc --- /dev/null +++ b/zone/expedition_request.cpp @@ -0,0 +1,397 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "expedition_request.h" +#include "client.h" +#include "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" +#include "groups.h" +#include "raids.h" +#include "string_ids.h" +#include "worldserver.h" + +extern WorldServer worldserver; + +constexpr char SystemName[] = "expedition"; + +struct ExpeditionRequestConflict +{ + std::string character_name; + ExpeditionLockoutTimer lockout; +}; + +ExpeditionRequest::ExpeditionRequest( + std::string expedition_name, uint32_t min_players, uint32_t max_players, bool disable_messages +) : + m_expedition_name(expedition_name), + m_min_players(min_players), + m_max_players(max_players), + m_disable_messages(disable_messages) +{ +} + +bool ExpeditionRequest::Validate(Client* requester) +{ + m_requester = requester; + if (!m_requester) + { + return false; + } + + // a message is sent to leader for every member that fails a requirement + + BenchTimer benchmark; + + bool requirements_met = false; + + Raid* raid = m_requester->GetRaid(); + Group* group = m_requester->GetGroup(); + if (raid) + { + requirements_met = CanRaidRequest(raid); + } + else if (group) + { + requirements_met = CanGroupRequest(group); + } + else // solo request + { + m_leader = m_requester; + m_leader_id = m_requester->CharacterID(); + m_leader_name = m_requester->GetName(); + requirements_met = CanMembersJoin({m_leader_name}); + } + + auto elapsed = benchmark.elapsed(); + LogExpeditions("Create validation for [{}] members took [{}s]", m_members.size(), elapsed); + + return requirements_met; +} + +bool ExpeditionRequest::CanRaidRequest(Raid* raid) +{ + m_leader = raid->GetLeader(); + m_leader_name = raid->leadername; + m_leader_id = m_leader ? m_leader->CharacterID() : database.GetCharacterID(raid->leadername); + + // live (as of September 16, 2020) supports creation even if raid count exceeds + // expedition max. members are added up to the max ordered by group number. + auto raid_members = raid->GetMembers(); + + if (raid_members.size() > m_max_players) + { + // stable_sort not needed, order within a raid group may not be what is displayed + std::sort(raid_members.begin(), raid_members.end(), + [&](const RaidMember& lhs, const RaidMember& rhs) { + if (m_leader_name == lhs.membername) { // leader always added first + return true; + } else if (m_leader_name == rhs.membername) { + return false; + } + return lhs.GroupNumber < rhs.GroupNumber; + }); + + m_not_all_added_msg = fmt::format(CREATE_NOT_ALL_ADDED, "raid", SystemName, + SystemName, m_max_players, "raid", raid_members.size()); + } + + // live still performs conflict checks for all members even those beyond max + std::vector member_names; + for (int i = 0; i < raid_members.size(); ++i) + { + member_names.emplace_back(raid_members[i].membername); + } + + return CanMembersJoin(member_names); +} + +bool ExpeditionRequest::CanGroupRequest(Group* group) +{ + m_leader = nullptr; + if (group->GetLeader() && group->GetLeader()->IsClient()) + { + m_leader = group->GetLeader()->CastToClient(); + } + + // Group::GetLeaderName() is broken if group formed across zones, ask database instead + m_leader_name = m_leader ? m_leader->GetName() : GetGroupLeaderName(group->GetID()); // group->GetLeaderName(); + m_leader_id = m_leader ? m_leader->CharacterID() : database.GetCharacterID(m_leader_name.c_str()); + + std::vector member_names; + member_names.emplace_back(m_leader_name); // leader always added first + + for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) + { + if (group->membername[i][0] && m_leader_name != group->membername[i]) + { + member_names.emplace_back(group->membername[i]); + } + } + + if (member_names.size() > m_max_players) + { + m_not_all_added_msg = fmt::format(CREATE_NOT_ALL_ADDED, "group", SystemName, + SystemName, m_max_players, "group", member_names.size()); + } + + return CanMembersJoin(member_names); +} + +std::string ExpeditionRequest::GetGroupLeaderName(uint32_t group_id) +{ + char leader_name_buffer[64] = { 0 }; + database.GetGroupLeadershipInfo(group_id, leader_name_buffer); + return std::string(leader_name_buffer); +} + +bool ExpeditionRequest::CanMembersJoin(const std::vector& member_names) +{ + if (member_names.empty()) + { + return false; + } + + bool requirements_met = true; + + if (CheckMembersForConflicts(member_names)) + { + requirements_met = false; + } + + // live only checks player count requirement after other expensive checks pass (?) + // maybe it's done intentionally as a way to preview lockout conflicts + if (requirements_met) + { + requirements_met = IsPlayerCountValidated(); + } + + return requirements_met; +} + +bool ExpeditionRequest::LoadLeaderLockouts() +{ + // leader's lockouts are used to check member conflicts and later stored in expedition + auto lockouts = ExpeditionDatabase::LoadCharacterLockouts(m_leader_id, m_expedition_name); + + for (auto& lockout : lockouts) + { + if (!lockout.IsExpired()) + { + m_lockouts.emplace(lockout.GetEventName(), lockout); + + // on live if leader has a replay lockout it never bothers checking for event conflicts + if (m_check_event_lockouts && lockout.IsReplayTimer()) + { + m_check_event_lockouts = false; + } + } + } + + return true; +} + +bool ExpeditionRequest::CheckMembersForConflicts(const std::vector& member_names) +{ + // load data for each member and compare with leader lockouts + auto results = ExpeditionDatabase::LoadMembersForCreateRequest(member_names, m_expedition_name); + if (!results.Success() || !LoadLeaderLockouts()) + { + LogExpeditions("Failed to load data to verify members for expedition request"); + return true; + } + + bool is_solo = (member_names.size() == 1); + bool has_conflicts = false; + + using col = LoadMembersForCreateRequestColumns::eLoadMembersForCreateRequestColumns; + + std::vector member_lockout_conflicts; + + uint32_t last_character_id = 0; + for (auto row = results.begin(); row != results.end(); ++row) + { + uint32_t character_id = std::strtoul(row[col::character_id], nullptr, 10); + std::string character_name = row[col::character_name]; + bool has_expedition = (row[col::character_expedition_id] != nullptr); + + if (character_id != last_character_id) + { + // defaults to online status, if offline group members implemented this needs to change + m_members.emplace_back(character_id, character_name); + + // process event lockout conflict messages from the previous character + for (const auto& member_lockout : member_lockout_conflicts) + { + SendLeaderMemberEventLockout(member_lockout.character_name, member_lockout.lockout); + } + member_lockout_conflicts.clear(); + + if (has_expedition) + { + has_conflicts = true; + SendLeaderMemberInExpedition(character_name, is_solo); + + // solo requests break out early if requester in an expedition + if (is_solo) + { + return has_conflicts; + } + } + } + + last_character_id = character_id; + + // compare member lockouts with leader lockouts + if (row[col::lockout_uuid]) // lockout results may be null + { + auto expire_time = strtoull(row[col::lockout_expire_time], nullptr, 10); + uint32_t duration = strtoul(row[col::lockout_duration], nullptr, 10); + + ExpeditionLockoutTimer lockout{ + row[col::lockout_uuid], m_expedition_name, row[col::lockout_event_name], expire_time, duration + }; + + if (!lockout.IsExpired()) + { + if (lockout.IsReplayTimer()) + { + // replay timer conflict messages always show up before event conflicts + has_conflicts = true; + SendLeaderMemberReplayLockout(character_name, lockout, is_solo); + } + else if (m_check_event_lockouts && character_id != m_leader_id) + { + if (m_lockouts.find(lockout.GetEventName()) == m_lockouts.end()) + { + // leader doesn't have this lockout. queue instead of messaging + // now so message comes after any replay lockout messages + has_conflicts = true; + member_lockout_conflicts.push_back({character_name, lockout}); + } + } + } + } + } + + // event lockout messages for last processed character + for (const auto& member_lockout : member_lockout_conflicts) + { + SendLeaderMemberEventLockout(member_lockout.character_name, member_lockout.lockout); + } + + return has_conflicts; +} + +void ExpeditionRequest::SendLeaderMessage( + uint16_t chat_type, uint32_t string_id, const std::initializer_list& args) +{ + if (!m_disable_messages) + { + Client::SendCrossZoneMessageString(m_leader, m_leader_name, chat_type, string_id, args); + } +} + +void ExpeditionRequest::SendLeaderMemberInExpedition(const std::string& member_name, bool is_solo) +{ + if (m_disable_messages) + { + return; + } + + if (is_solo) + { + SendLeaderMessage(Chat::Red, EXPEDITION_YOU_BELONG); + } + else if (m_requester) + { + std::string message = fmt::format(EXPEDITION_OTHER_BELONGS, m_requester->GetName(), member_name); + Client::SendCrossZoneMessage(m_leader, m_leader_name, Chat::Red, message); + } +} + +void ExpeditionRequest::SendLeaderMemberReplayLockout( + const std::string& member_name, const ExpeditionLockoutTimer& lockout, bool is_solo) +{ + if (m_disable_messages) + { + return; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + if (is_solo) + { + SendLeaderMessage(Chat::Red, EXPEDITION_YOU_PLAYED_HERE, { + time_remaining.days, time_remaining.hours, time_remaining.mins + }); + } + else + { + SendLeaderMessage(Chat::Red, EXPEDITION_REPLAY_TIMER, { + member_name, time_remaining.days, time_remaining.hours, time_remaining.mins + }); + } +} + +void ExpeditionRequest::SendLeaderMemberEventLockout( + const std::string& member_name, const ExpeditionLockoutTimer& lockout) +{ + if (m_disable_messages) + { + return; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + SendLeaderMessage(Chat::Red, EXPEDITION_EVENT_TIMER, { + member_name, + lockout.GetEventName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins, + lockout.GetEventName() + }); +} + +bool ExpeditionRequest::IsPlayerCountValidated() +{ + // note: offline group members count towards requirement but not added to expedition + bool requirements_met = true; + + auto bypass_status = RuleI(Expedition, MinStatusToBypassPlayerCountRequirements); + auto gm_bypass = (m_requester && m_requester->GetGM() && m_requester->Admin() >= bypass_status); + + if (m_members.size() > m_max_players) + { + // members were sorted at start, truncate after conflict checks to act like live + m_members.resize(m_max_players); + } + else if (!gm_bypass && m_members.size() < m_min_players) + { + requirements_met = false; + + SendLeaderMessage(Chat::System, REQUIRED_PLAYER_COUNT, { + fmt::format_int(m_members.size()).str(), + fmt::format_int(m_min_players).str(), + fmt::format_int(m_max_players).str() + }); + } + + return requirements_met; +} diff --git a/zone/expedition_request.h b/zone/expedition_request.h new file mode 100644 index 000000000..edfdd6122 --- /dev/null +++ b/zone/expedition_request.h @@ -0,0 +1,83 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EXPEDITION_REQUEST_H +#define EXPEDITION_REQUEST_H + +#include "expedition.h" +#include "expedition_lockout_timer.h" +#include +#include +#include +#include + +class Client; +class Group; +class MySQLRequestResult; +class Raid; +class ServerPacket; + +class ExpeditionRequest +{ +public: + ExpeditionRequest( + std::string expedition_name, uint32_t min_players, uint32_t max_players, + bool disable_messages = false); + + bool Validate(Client* requester); + + const std::string& GetExpeditionName() const { return m_expedition_name; } + Client* GetLeaderClient() const { return m_leader; } + uint32_t GetLeaderID() const { return m_leader_id; } + const std::string& GetLeaderName() const { return m_leader_name; } + const std::string& GetNotAllAddedMessage() const { return m_not_all_added_msg; } + uint32_t GetMinPlayers() const { return m_min_players; } + uint32_t GetMaxPlayers() const { return m_max_players; } + std::vector GetMembers() const { return m_members; } + std::unordered_map GetLockouts() const { return m_lockouts; } + +private: + bool CanMembersJoin(const std::vector& member_names); + bool CanRaidRequest(Raid* raid); + bool CanGroupRequest(Group* group); + bool CheckMembersForConflicts(const std::vector& member_names); + std::string GetGroupLeaderName(uint32_t group_id); + bool IsPlayerCountValidated(); + bool LoadLeaderLockouts(); + void SendLeaderMemberInExpedition(const std::string& member_name, bool is_solo); + void SendLeaderMemberReplayLockout(const std::string& member_name, const ExpeditionLockoutTimer& lockout, bool is_solo); + void SendLeaderMemberEventLockout(const std::string& member_name, const ExpeditionLockoutTimer& lockout); + void SendLeaderMessage(uint16_t chat_type, uint32_t string_id, const std::initializer_list& args = {}); + + Client* m_requester = nullptr; + Client* m_leader = nullptr; + uint32_t m_leader_id = 0; + uint32_t m_min_players = 0; + uint32_t m_max_players = 0; + bool m_check_event_lockouts = true; + bool m_disable_messages = false; + std::string m_expedition_name; + std::string m_leader_name; + std::string m_not_all_added_msg; + std::vector m_members; + std::unordered_map m_lockouts; +}; + +#endif diff --git a/zone/groups.cpp b/zone/groups.cpp index 60a58b99c..4b8e25d09 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -18,6 +18,7 @@ #include "../common/global_define.h" #include "../common/eqemu_logsys.h" +#include "expedition.h" #include "masterentity.h" #include "npc_ai.h" #include "../common/packet_functions.h" @@ -2503,3 +2504,24 @@ void Group::QueueClients(Mob *sender, const EQApplicationPacket *app, bool ack_r } } } + +bool Group::DoesAnyMemberHaveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, int max_check_count) +{ + if (max_check_count <= 0) + { + max_check_count = MAX_GROUP_MEMBERS; + } + + for (int i = 0; i < MAX_GROUP_MEMBERS && i < max_check_count; ++i) + { + if (membername[i][0]) + { + if (Expedition::HasLockoutByCharacterName(membername[i], expedition_name, event_name)) + { + return true; + } + } + } + return false; +} diff --git a/zone/groups.h b/zone/groups.h index 0d39409e0..57dfdbfb6 100644 --- a/zone/groups.h +++ b/zone/groups.h @@ -153,6 +153,8 @@ public: inline int GetMentorPercent() { return mentor_percent; } inline Client *GetMentoree() { return mentoree; } + bool DoesAnyMemberHaveExpeditionLockout(const std::string& expedition_name, const std::string& event_name, int max_check_count = 0); + Mob* members[MAX_GROUP_MEMBERS]; char membername[MAX_GROUP_MEMBERS][64]; uint8 MemberRoles[MAX_GROUP_MEMBERS]; diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index 9309c7cbb..30518e520 100644 --- a/zone/lua_client.cpp +++ b/zone/lua_client.cpp @@ -4,7 +4,11 @@ #include #include "client.h" +#include "dynamiczone.h" +#include "expedition_lockout_timer.h" +#include "expedition_request.h" #include "lua_client.h" +#include "lua_expedition.h" #include "lua_npc.h" #include "lua_item.h" #include "lua_iteminst.h" @@ -1644,7 +1648,234 @@ int Lua_Client::GetClientMaxLevel() { return self->GetClientMaxLevel(); } +DynamicZoneLocation GetDynamicZoneLocationFromTable(const luabind::object& lua_table) +{ + DynamicZoneLocation zone_location; + if (luabind::type(lua_table) == LUA_TTABLE) + { + luabind::object lua_zone = lua_table["zone"]; + + // default invalid/missing args to 0 + uint32_t zone_id = 0; + if (luabind::type(lua_zone) == LUA_TSTRING) + { + zone_id = ZoneID(luabind::object_cast(lua_zone)); + } + else if (luabind::type(lua_zone) == LUA_TNUMBER) + { + zone_id = luabind::object_cast(lua_zone); + } + + float x = (luabind::type(lua_table["x"]) != LUA_TNIL) ? luabind::object_cast(lua_table["x"]) : 0.0f; + float y = (luabind::type(lua_table["y"]) != LUA_TNIL) ? luabind::object_cast(lua_table["y"]) : 0.0f; + float z = (luabind::type(lua_table["z"]) != LUA_TNIL) ? luabind::object_cast(lua_table["z"]) : 0.0f; + float h = (luabind::type(lua_table["h"]) != LUA_TNIL) ? luabind::object_cast(lua_table["h"]) : 0.0f; + + zone_location = { zone_id, x, y, z, h }; + } + + return zone_location; +} + +Lua_Expedition Lua_Client::CreateExpedition(luabind::object expedition_table) { + Lua_Safe_Call_Class(Lua_Expedition); + + if (luabind::type(expedition_table) != LUA_TTABLE) + { + return nullptr; + } + + // luabind will catch thrown cast_failed exceptions for invalid/missing args + luabind::object instance_info = expedition_table["instance"]; + luabind::object zone = instance_info["zone"]; + + uint32_t zone_id = 0; + if (luabind::type(zone) == LUA_TSTRING) + { + zone_id = ZoneID(luabind::object_cast(zone)); + } + else if (luabind::type(zone) == LUA_TNUMBER) + { + zone_id = luabind::object_cast(zone); + } + + uint32_t zone_version = luabind::object_cast(instance_info["version"]); + uint32_t zone_duration = luabind::object_cast(instance_info["duration"]); + + DynamicZone dz{ zone_id, zone_version, zone_duration, DynamicZoneType::Expedition }; + + // the dz_info table supports optional hash entries for 'compass', 'safereturn', and 'zonein' data + if (luabind::type(expedition_table["compass"]) == LUA_TTABLE) + { + auto compass_loc = GetDynamicZoneLocationFromTable(expedition_table["compass"]); + dz.SetCompass(compass_loc); + } + + if (luabind::type(expedition_table["safereturn"]) == LUA_TTABLE) + { + auto safereturn_loc = GetDynamicZoneLocationFromTable(expedition_table["safereturn"]); + dz.SetSafeReturn(safereturn_loc); + } + + if (luabind::type(expedition_table["zonein"]) == LUA_TTABLE) + { + auto zonein_loc = GetDynamicZoneLocationFromTable(expedition_table["zonein"]); + dz.SetZoneInLocation(zonein_loc); + } + + luabind::object expedition_info = expedition_table["expedition"]; + + std::string expedition_name = luabind::object_cast(expedition_info["name"]); + uint32_t min_players = luabind::object_cast(expedition_info["min_players"]); + uint32_t max_players = luabind::object_cast(expedition_info["max_players"]); + bool disable_messages = false; + + if (luabind::type(expedition_info["disable_messages"]) == LUA_TBOOLEAN) + { + disable_messages = luabind::object_cast(expedition_info["disable_messages"]); + } + + ExpeditionRequest request{ expedition_name, min_players, max_players, disable_messages }; + + return self->CreateExpedition(dz, request); +} + +Lua_Expedition Lua_Client::CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players) { + Lua_Safe_Call_Class(Lua_Expedition); + return self->CreateExpedition(zone_name, version, duration, expedition_name, min_players, max_players); +} + +Lua_Expedition Lua_Client::CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players, bool disable_messages) { + Lua_Safe_Call_Class(Lua_Expedition); + return self->CreateExpedition(zone_name, version, duration, expedition_name, min_players, max_players, disable_messages); +} + +Lua_Expedition Lua_Client::GetExpedition() { + Lua_Safe_Call_Class(Lua_Expedition); + return self->GetExpedition(); +} + +luabind::object Lua_Client::GetExpeditionLockouts(lua_State* L) +{ + auto lua_table = luabind::newtable(L); + if (d_) + { + auto self = reinterpret_cast(d_); + auto lockouts = self->GetExpeditionLockouts(); + + for (const auto& lockout : lockouts) + { + auto lockout_table = lua_table[lockout.GetExpeditionName()]; + if (luabind::type(lockout_table) != LUA_TTABLE) + { + lockout_table = luabind::newtable(L); + } + lockout_table[lockout.GetEventName()] = lockout.GetSecondsRemaining(); + } + } + return lua_table; +} + +luabind::object Lua_Client::GetExpeditionLockouts(lua_State* L, std::string expedition_name) +{ + auto lua_table = luabind::newtable(L); + if (d_) + { + auto self = reinterpret_cast(d_); + auto lockouts = self->GetExpeditionLockouts(); + + for (const auto& lockout : lockouts) + { + if (lockout.GetExpeditionName() == expedition_name) + { + lua_table[lockout.GetEventName()] = lockout.GetSecondsRemaining(); + } + } + } + return lua_table; +} + +std::string Lua_Client::GetLockoutExpeditionUUID(std::string expedition_name, std::string event_name) { + Lua_Safe_Call_String(); + std::string uuid; + auto lockout = self->GetExpeditionLockout(expedition_name, event_name); + if (lockout) + { + uuid = lockout->GetExpeditionUUID(); + } + return uuid; +} + +void Lua_Client::AddExpeditionLockout(std::string expedition_name, std::string event_name, uint32 seconds) { + Lua_Safe_Call_Void(); + self->AddNewExpeditionLockout(expedition_name, event_name, seconds); +} + +void Lua_Client::AddExpeditionLockout(std::string expedition_name, std::string event_name, uint32 seconds, std::string uuid) { + Lua_Safe_Call_Void(); + self->AddNewExpeditionLockout(expedition_name, event_name, seconds, uuid); +} + +void Lua_Client::AddExpeditionLockoutDuration(std::string expedition_name, std::string event_name, int seconds) { + Lua_Safe_Call_Void(); + self->AddExpeditionLockoutDuration(expedition_name, event_name, seconds, {}, true); +} + +void Lua_Client::AddExpeditionLockoutDuration(std::string expedition_name, std::string event_name, int seconds, std::string uuid) { + Lua_Safe_Call_Void(); + self->AddExpeditionLockoutDuration(expedition_name, event_name, seconds, uuid, true); +} + +void Lua_Client::RemoveAllExpeditionLockouts() { + Lua_Safe_Call_Void(); + self->RemoveAllExpeditionLockouts({}, true); +} + +void Lua_Client::RemoveAllExpeditionLockouts(std::string expedition_name) { + Lua_Safe_Call_Void(); + self->RemoveAllExpeditionLockouts(expedition_name, true); +} + +void Lua_Client::RemoveExpeditionLockout(std::string expedition_name, std::string event_name) { + Lua_Safe_Call_Void(); + self->RemoveExpeditionLockout(expedition_name, event_name, true); +} + +bool Lua_Client::HasExpeditionLockout(std::string expedition_name, std::string event_name) { + Lua_Safe_Call_Bool(); + return self->HasExpeditionLockout(expedition_name, event_name); +} + +void Lua_Client::MovePCDynamicZone(uint32 zone_id) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_id); +} + +void Lua_Client::MovePCDynamicZone(uint32 zone_id, int zone_version) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_id, zone_version); +} + +void Lua_Client::MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_id, zone_version, msg_if_invalid); +} + +void Lua_Client::MovePCDynamicZone(std::string zone_name) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_name); +} + +void Lua_Client::MovePCDynamicZone(std::string zone_name, int zone_version) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_name, zone_version); +} + +void Lua_Client::MovePCDynamicZone(std::string zone_name, int zone_version, bool msg_if_invalid) { + Lua_Safe_Call_Void(); + return self->MovePCDynamicZone(zone_name, zone_version, msg_if_invalid); +} luabind::scope lua_register_client() { return luabind::class_("Client") @@ -1952,7 +2183,28 @@ luabind::scope lua_register_client() { .def("EnableAreaRegens", &Lua_Client::EnableAreaRegens) .def("DisableAreaRegens", &Lua_Client::DisableAreaRegens) .def("SetClientMaxLevel", (void(Lua_Client::*)(int))&Lua_Client::SetClientMaxLevel) - .def("GetClientMaxLevel", (int(Lua_Client::*)(void))&Lua_Client::GetClientMaxLevel); + .def("GetClientMaxLevel", (int(Lua_Client::*)(void))&Lua_Client::GetClientMaxLevel) + .def("CreateExpedition", (Lua_Expedition(Lua_Client::*)(luabind::object))&Lua_Client::CreateExpedition) + .def("CreateExpedition", (Lua_Expedition(Lua_Client::*)(std::string, uint32, uint32, std::string, uint32, uint32))&Lua_Client::CreateExpedition) + .def("CreateExpedition", (Lua_Expedition(Lua_Client::*)(std::string, uint32, uint32, std::string, uint32, uint32, bool))&Lua_Client::CreateExpedition) + .def("GetExpedition", (Lua_Expedition(Lua_Client::*)(void))&Lua_Client::GetExpedition) + .def("GetExpeditionLockouts", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetExpeditionLockouts) + .def("GetExpeditionLockouts", (luabind::object(Lua_Client::*)(lua_State* L, std::string))&Lua_Client::GetExpeditionLockouts) + .def("GetLockoutExpeditionUUID", (std::string(Lua_Client::*)(std::string, std::string))&Lua_Client::GetLockoutExpeditionUUID) + .def("AddExpeditionLockout", (void(Lua_Client::*)(std::string, std::string, uint32))&Lua_Client::AddExpeditionLockout) + .def("AddExpeditionLockout", (void(Lua_Client::*)(std::string, std::string, uint32, std::string))&Lua_Client::AddExpeditionLockout) + .def("AddExpeditionLockoutDuration", (void(Lua_Client::*)(std::string, std::string, int))&Lua_Client::AddExpeditionLockoutDuration) + .def("AddExpeditionLockoutDuration", (void(Lua_Client::*)(std::string, std::string, int, std::string))&Lua_Client::AddExpeditionLockoutDuration) + .def("RemoveAllExpeditionLockouts", (void(Lua_Client::*)(void))&Lua_Client::RemoveAllExpeditionLockouts) + .def("RemoveAllExpeditionLockouts", (void(Lua_Client::*)(std::string))&Lua_Client::RemoveAllExpeditionLockouts) + .def("RemoveExpeditionLockout", (void(Lua_Client::*)(std::string, std::string))&Lua_Client::RemoveExpeditionLockout) + .def("HasExpeditionLockout", (bool(Lua_Client::*)(std::string, std::string))&Lua_Client::HasExpeditionLockout) + .def("MovePCDynamicZone", (void(Lua_Client::*)(uint32))&Lua_Client::MovePCDynamicZone) + .def("MovePCDynamicZone", (void(Lua_Client::*)(uint32, int))&Lua_Client::MovePCDynamicZone) + .def("MovePCDynamicZone", (void(Lua_Client::*)(uint32, int, bool))&Lua_Client::MovePCDynamicZone) + .def("MovePCDynamicZone", (void(Lua_Client::*)(std::string))&Lua_Client::MovePCDynamicZone) + .def("MovePCDynamicZone", (void(Lua_Client::*)(std::string, int))&Lua_Client::MovePCDynamicZone) + .def("MovePCDynamicZone", (void(Lua_Client::*)(std::string, int, bool))&Lua_Client::MovePCDynamicZone); } luabind::scope lua_register_inventory_where() { diff --git a/zone/lua_client.h b/zone/lua_client.h index 2f5de4a59..8bfa5bda0 100644 --- a/zone/lua_client.h +++ b/zone/lua_client.h @@ -5,6 +5,7 @@ #include "lua_mob.h" class Client; +class Lua_Expedition; class Lua_Group; class Lua_Raid; class Lua_Inventory; @@ -332,12 +333,33 @@ public: void EnableAreaRegens(int value); void DisableAreaRegens(); - void SetPrimaryWeaponOrnamentation(uint32 model_id); void SetSecondaryWeaponOrnamentation(uint32 model_id); void SetClientMaxLevel(int value); int GetClientMaxLevel(); + + Lua_Expedition CreateExpedition(luabind::object expedition_info); + Lua_Expedition CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players); + Lua_Expedition CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players, bool disable_messages); + Lua_Expedition GetExpedition(); + luabind::object GetExpeditionLockouts(lua_State* L); + luabind::object GetExpeditionLockouts(lua_State* L, std::string expedition_name); + std::string GetLockoutExpeditionUUID(std::string expedition_name, std::string event_name); + void AddExpeditionLockout(std::string expedition_name, std::string event_name, uint32 seconds); + void AddExpeditionLockout(std::string expedition_name, std::string event_name, uint32 seconds, std::string uuid); + void AddExpeditionLockoutDuration(std::string expedition_name, std::string event_name, int seconds); + void AddExpeditionLockoutDuration(std::string expedition_name, std::string event_name, int seconds, std::string uuid); + void RemoveAllExpeditionLockouts(); + void RemoveAllExpeditionLockouts(std::string expedition_name); + void RemoveExpeditionLockout(std::string expedition_name, std::string event_name); + bool HasExpeditionLockout(std::string expedition_name, std::string event_name); + void MovePCDynamicZone(uint32 zone_id); + void MovePCDynamicZone(uint32 zone_id, int zone_version); + void MovePCDynamicZone(uint32 zone_id, int zone_version, bool msg_if_invalid); + void MovePCDynamicZone(std::string zone_name); + void MovePCDynamicZone(std::string zone_name, int zone_version); + void MovePCDynamicZone(std::string zone_name, int zone_version, bool msg_if_invalid); }; #endif diff --git a/zone/lua_expedition.cpp b/zone/lua_expedition.cpp new file mode 100644 index 000000000..28f783dd1 --- /dev/null +++ b/zone/lua_expedition.cpp @@ -0,0 +1,301 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifdef LUA_EQEMU + +#include "lua_expedition.h" +#include "expedition.h" +#include "zone_store.h" +#include "lua.hpp" +#include +#include + +void Lua_Expedition::AddLockout(std::string event_name, uint32_t seconds) { + Lua_Safe_Call_Void(); + self->AddLockout(event_name, seconds); +} + +void Lua_Expedition::AddLockoutDuration(std::string event_name, int seconds) { + Lua_Safe_Call_Void(); + self->AddLockoutDuration(event_name, seconds); +} + +void Lua_Expedition::AddLockoutDuration(std::string event_name, int seconds, bool members_only) { + Lua_Safe_Call_Void(); + self->AddLockoutDuration(event_name, seconds, members_only); +} + +void Lua_Expedition::AddReplayLockout(uint32_t seconds) { + Lua_Safe_Call_Void(); + self->AddReplayLockout(seconds); +} + +void Lua_Expedition::AddReplayLockoutDuration(int seconds) { + Lua_Safe_Call_Void(); + self->AddReplayLockoutDuration(seconds); +} + +void Lua_Expedition::AddReplayLockoutDuration(int seconds, bool members_only) { + Lua_Safe_Call_Void(); + self->AddReplayLockoutDuration(seconds, members_only); +} + +uint32_t Lua_Expedition::GetDynamicZoneID() { + Lua_Safe_Call_Int(); + return self->GetDynamicZoneID(); +} + +uint32_t Lua_Expedition::GetID() { + Lua_Safe_Call_Int(); + return self->GetID(); +} + +int Lua_Expedition::GetInstanceID() { + Lua_Safe_Call_Int(); + return self->GetDynamicZone().GetInstanceID(); +} + +std::string Lua_Expedition::GetLeaderName() { + Lua_Safe_Call_String(); + return self->GetLeaderName(); +} + +luabind::object Lua_Expedition::GetLockouts(lua_State* L) { + luabind::object lua_table = luabind::newtable(L); + + if (d_) + { + auto self = reinterpret_cast(d_); + auto lockouts = self->GetLockouts(); + for (const auto& lockout : lockouts) + { + lua_table[lockout.first] = lockout.second.GetSecondsRemaining(); + } + } + return lua_table; +} + +std::string Lua_Expedition::GetLootEventByNPCTypeID(uint32_t npc_type_id) { + Lua_Safe_Call_String(); + return self->GetLootEventByNPCTypeID(npc_type_id); +} + +std::string Lua_Expedition::GetLootEventBySpawnID(uint32_t spawn_id) { + Lua_Safe_Call_String(); + return self->GetLootEventBySpawnID(spawn_id); +} + +uint32_t Lua_Expedition::GetMemberCount() { + Lua_Safe_Call_Int(); + return self->GetMemberCount(); +} + +luabind::object Lua_Expedition::GetMembers(lua_State* L) { + luabind::object lua_table = luabind::newtable(L); + + if (d_) + { + auto self = reinterpret_cast(d_); + for (const auto& member : self->GetMembers()) + { + lua_table[member.name] = member.char_id; + } + } + return lua_table; +} + +std::string Lua_Expedition::GetName() { + Lua_Safe_Call_String(); + return self->GetName(); +} + +int Lua_Expedition::GetSecondsRemaining() { + Lua_Safe_Call_Int(); + return self->GetDynamicZone().GetSecondsRemaining(); +} + +std::string Lua_Expedition::GetUUID() { + Lua_Safe_Call_String(); + return self->GetUUID(); +} + +int Lua_Expedition::GetZoneID() { + Lua_Safe_Call_Int(); + return self->GetDynamicZone().GetZoneID(); +} + +std::string Lua_Expedition::GetZoneName() { + Lua_Safe_Call_String(); + return ZoneName(self->GetDynamicZone().GetZoneID()); +} + +int Lua_Expedition::GetZoneVersion() { + Lua_Safe_Call_Int(); + return self->GetDynamicZone().GetZoneVersion(); +} + +bool Lua_Expedition::HasLockout(std::string event_name) { + Lua_Safe_Call_Bool(); + return self->HasLockout(event_name); +} + +bool Lua_Expedition::HasReplayLockout() { + Lua_Safe_Call_Bool(); + return self->HasReplayLockout(); +} + +void Lua_Expedition::RemoveCompass() { + Lua_Safe_Call_Void(); + self->SetDzCompass(0, 0, 0, 0, true); +} + +void Lua_Expedition::RemoveLockout(std::string event_name) { + Lua_Safe_Call_Void(); + self->RemoveLockout(event_name); +} + +void Lua_Expedition::SetCompass(uint32_t zone_id, float x, float y, float z) { + Lua_Safe_Call_Void(); + self->SetDzCompass(zone_id, x, y, z, true); +} + +void Lua_Expedition::SetCompass(std::string zone_name, float x, float y, float z) { + Lua_Safe_Call_Void(); + self->SetDzCompass(zone_name, x, y, z, true); +} + +void Lua_Expedition::SetLocked(bool lock_expedition) { + Lua_Safe_Call_Void(); + self->SetLocked(lock_expedition, ExpeditionLockMessage::None, true); +} + +void Lua_Expedition::SetLocked(bool lock_expedition, int lock_msg) { + Lua_Safe_Call_Void(); + self->SetLocked(lock_expedition, static_cast(lock_msg), true); +} + +void Lua_Expedition::SetLocked(bool lock_expedition, int lock_msg, uint32_t msg_color) { + Lua_Safe_Call_Void(); + self->SetLocked(lock_expedition, static_cast(lock_msg), true, msg_color); +} + +void Lua_Expedition::SetLootEventByNPCTypeID(uint32_t npc_type_id, std::string event_name) { + Lua_Safe_Call_Void(); + self->SetLootEventByNPCTypeID(npc_type_id, event_name); +} + +void Lua_Expedition::SetLootEventBySpawnID(uint32_t spawn_id, std::string event_name) { + Lua_Safe_Call_Void(); + self->SetLootEventBySpawnID(spawn_id, event_name); +} + +void Lua_Expedition::SetReplayLockoutOnMemberJoin(bool enable) { + Lua_Safe_Call_Void(); + self->SetReplayLockoutOnMemberJoin(enable, true); +} + +void Lua_Expedition::SetSafeReturn(uint32_t zone_id, float x, float y, float z, float heading) { + Lua_Safe_Call_Void(); + self->SetDzSafeReturn(zone_id, x, y, z, heading, true); +} + +void Lua_Expedition::SetSafeReturn(std::string zone_name, float x, float y, float z, float heading) { + Lua_Safe_Call_Void(); + self->SetDzSafeReturn(zone_name, x, y, z, heading, true); +} + +void Lua_Expedition::SetSecondsRemaining(uint32_t seconds_remaining) +{ + Lua_Safe_Call_Void(); + self->SetDzSecondsRemaining(seconds_remaining); +} + +void Lua_Expedition::SetZoneInLocation(float x, float y, float z, float heading) { + Lua_Safe_Call_Void(); + self->SetDzZoneInLocation(x, y, z, heading, true); +} + +void Lua_Expedition::UpdateLockoutDuration(std::string event_name, uint32_t duration) { + Lua_Safe_Call_Void(); + self->UpdateLockoutDuration(event_name, duration); +} + +void Lua_Expedition::UpdateLockoutDuration(std::string event_name, uint32_t duration, bool members_only) { + Lua_Safe_Call_Void(); + self->UpdateLockoutDuration(event_name, duration, members_only); +} + +luabind::scope lua_register_expedition() { + return luabind::class_("Expedition") + .def(luabind::constructor<>()) + .property("null", &Lua_Expedition::Null) + .property("valid", &Lua_Expedition::Valid) + .def("AddLockout", (void(Lua_Expedition::*)(std::string, uint32_t))&Lua_Expedition::AddLockout) + .def("AddLockoutDuration", (void(Lua_Expedition::*)(std::string, int))&Lua_Expedition::AddLockoutDuration) + .def("AddLockoutDuration", (void(Lua_Expedition::*)(std::string, int, bool))&Lua_Expedition::AddLockoutDuration) + .def("AddReplayLockout", (void(Lua_Expedition::*)(uint32_t))&Lua_Expedition::AddReplayLockout) + .def("AddReplayLockoutDuration", (void(Lua_Expedition::*)(int))&Lua_Expedition::AddReplayLockoutDuration) + .def("AddReplayLockoutDuration", (void(Lua_Expedition::*)(int, bool))&Lua_Expedition::AddReplayLockoutDuration) + .def("GetDynamicZoneID", &Lua_Expedition::GetDynamicZoneID) + .def("GetID", (uint32_t(Lua_Expedition::*)(void))&Lua_Expedition::GetID) + .def("GetInstanceID", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetInstanceID) + .def("GetLeaderName", (std::string(Lua_Expedition::*)(void))&Lua_Expedition::GetLeaderName) + .def("GetLockouts", &Lua_Expedition::GetLockouts) + .def("GetLootEventByNPCTypeID", (std::string(Lua_Expedition::*)(uint32_t))&Lua_Expedition::GetLootEventByNPCTypeID) + .def("GetLootEventBySpawnID", (std::string(Lua_Expedition::*)(uint32_t))&Lua_Expedition::GetLootEventBySpawnID) + .def("GetMemberCount", (uint32_t(Lua_Expedition::*)(void))&Lua_Expedition::GetMemberCount) + .def("GetMembers", &Lua_Expedition::GetMembers) + .def("GetName", (std::string(Lua_Expedition::*)(void))&Lua_Expedition::GetName) + .def("GetSecondsRemaining", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetSecondsRemaining) + .def("GetUUID", (std::string(Lua_Expedition::*)(void))&Lua_Expedition::GetUUID) + .def("GetZoneID", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetZoneID) + .def("GetZoneName", &Lua_Expedition::GetZoneName) + .def("GetZoneVersion", &Lua_Expedition::GetZoneVersion) + .def("HasLockout", (bool(Lua_Expedition::*)(std::string))&Lua_Expedition::HasLockout) + .def("HasReplayLockout", (bool(Lua_Expedition::*)(void))&Lua_Expedition::HasReplayLockout) + .def("RemoveCompass", (void(Lua_Expedition::*)(void))&Lua_Expedition::RemoveCompass) + .def("RemoveLockout", (void(Lua_Expedition::*)(std::string))&Lua_Expedition::RemoveLockout) + .def("SetCompass", (void(Lua_Expedition::*)(uint32_t, float, float, float))&Lua_Expedition::SetCompass) + .def("SetCompass", (void(Lua_Expedition::*)(std::string, float, float, float))&Lua_Expedition::SetCompass) + .def("SetLocked", (void(Lua_Expedition::*)(bool))&Lua_Expedition::SetLocked) + .def("SetLocked", (void(Lua_Expedition::*)(bool, int))&Lua_Expedition::SetLocked) + .def("SetLocked", (void(Lua_Expedition::*)(bool, int, uint32_t))&Lua_Expedition::SetLocked) + .def("SetLootEventByNPCTypeID", (void(Lua_Expedition::*)(uint32_t, std::string))&Lua_Expedition::SetLootEventByNPCTypeID) + .def("SetLootEventBySpawnID", (void(Lua_Expedition::*)(uint32_t, std::string))&Lua_Expedition::SetLootEventBySpawnID) + .def("SetReplayLockoutOnMemberJoin", (void(Lua_Expedition::*)(bool))&Lua_Expedition::SetReplayLockoutOnMemberJoin) + .def("SetSafeReturn", (void(Lua_Expedition::*)(uint32_t, float, float, float, float))&Lua_Expedition::SetSafeReturn) + .def("SetSafeReturn", (void(Lua_Expedition::*)(std::string, float, float, float, float))&Lua_Expedition::SetSafeReturn) + .def("SetSecondsRemaining", &Lua_Expedition::SetSecondsRemaining) + .def("SetZoneInLocation", (void(Lua_Expedition::*)(float, float, float, float))&Lua_Expedition::SetZoneInLocation) + .def("UpdateLockoutDuration", (void(Lua_Expedition::*)(std::string, uint32_t))&Lua_Expedition::UpdateLockoutDuration) + .def("UpdateLockoutDuration", (void(Lua_Expedition::*)(std::string, uint32_t, bool))&Lua_Expedition::UpdateLockoutDuration); +} + +luabind::scope lua_register_expedition_lock_messages() { + return luabind::class_("ExpeditionLockMessage") + .enum_("constants") + [ + luabind::value("None", static_cast(ExpeditionLockMessage::None)), + luabind::value("Close", static_cast(ExpeditionLockMessage::Close)), + luabind::value("Begin", static_cast(ExpeditionLockMessage::Begin)) + ]; +} + +#endif // LUA_EQEMU diff --git a/zone/lua_expedition.h b/zone/lua_expedition.h new file mode 100644 index 000000000..6409d574a --- /dev/null +++ b/zone/lua_expedition.h @@ -0,0 +1,98 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_LUA_EXPEDITION_H +#define EQEMU_LUA_EXPEDITION_H +#ifdef LUA_EQEMU + +#include "lua_ptr.h" +#include "../common/types.h" +#include + +class Expedition; +class Lua_Client; +struct lua_State; + +namespace luabind { + struct scope; + namespace adl { + class object; + } + using adl::object; +} + +luabind::scope lua_register_expedition(); +luabind::scope lua_register_expedition_lock_messages(); + +class Lua_Expedition : public Lua_Ptr +{ + typedef Expedition NativeType; +public: + Lua_Expedition() : Lua_Ptr(nullptr) { } + Lua_Expedition(Expedition *d) : Lua_Ptr(d) { } + virtual ~Lua_Expedition() { } + + operator Expedition*() { + return reinterpret_cast(GetLuaPtrData()); + } + + void AddLockout(std::string event_name, uint32_t seconds); + void AddLockoutDuration(std::string event_name, int seconds); + void AddLockoutDuration(std::string event_name, int seconds, bool members_only); + void AddReplayLockout(uint32_t seconds); + void AddReplayLockoutDuration(int seconds); + void AddReplayLockoutDuration(int seconds, bool members_only); + uint32_t GetDynamicZoneID(); + uint32_t GetID(); + int GetInstanceID(); + std::string GetLeaderName(); + luabind::object GetLockouts(lua_State* L); + std::string GetLootEventByNPCTypeID(uint32_t npc_type_id); + std::string GetLootEventBySpawnID(uint32_t spawn_id); + uint32_t GetMemberCount(); + luabind::object GetMembers(lua_State* L); + std::string GetName(); + int GetSecondsRemaining(); + std::string GetUUID(); + int GetZoneID(); + std::string GetZoneName(); + int GetZoneVersion(); + bool HasLockout(std::string event_name); + bool HasReplayLockout(); + void RemoveCompass(); + void RemoveLockout(std::string event_name); + void SetCompass(uint32_t zone_id, float x, float y, float z); + void SetCompass(std::string zone_name, float x, float y, float z); + void SetLocked(bool lock_expedition); + void SetLocked(bool lock_expedition, int lock_msg); + void SetLocked(bool lock_expedition, int lock_msg, uint32_t color); + void SetLootEventByNPCTypeID(uint32_t npc_type_id, std::string event_name); + void SetLootEventBySpawnID(uint32_t spawn_id, std::string event_name); + void SetReplayLockoutOnMemberJoin(bool enable); + void SetSafeReturn(uint32_t zone_id, float x, float y, float z, float heading); + void SetSafeReturn(std::string zone_name, float x, float y, float z, float heading); + void SetSecondsRemaining(uint32_t seconds_remaining); + void SetZoneInLocation(float x, float y, float z, float heading); + void UpdateLockoutDuration(std::string event_name, uint32_t duration); + void UpdateLockoutDuration(std::string event_name, uint32_t duration, bool members_only); +}; + +#endif // LUA_EQEMU +#endif // EQEMU_LUA_EXPEDITION_H diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index ecb072369..34cc30b29 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -18,12 +18,14 @@ #include "lua_client.h" #include "lua_npc.h" #include "lua_entity_list.h" +#include "lua_expedition.h" #include "quest_parser_collection.h" #include "questmgr.h" #include "qglobals.h" #include "encounter.h" #include "lua_encounter.h" #include "data_bucket.h" +#include "expedition.h" struct Events { }; struct Factions { }; @@ -2178,6 +2180,118 @@ void lua_set_content_flag(std::string flag_name, bool enabled){ ZoneStore::SetContentFlag(flag_name, enabled); } +Lua_Expedition lua_get_expedition() { + if (zone && zone->GetInstanceID() != 0) + { + return Expedition::FindCachedExpeditionByZoneInstance(zone->GetZoneID(), zone->GetInstanceID()); + } + return nullptr; +} + +Lua_Expedition lua_get_expedition_by_char_id(uint32 char_id) { + return Expedition::FindCachedExpeditionByCharacterID(char_id); +} + +Lua_Expedition lua_get_expedition_by_dz_id(uint32 dz_id) { + return Expedition::FindCachedExpeditionByDynamicZoneID(dz_id); +} + +Lua_Expedition lua_get_expedition_by_zone_instance(uint32 zone_id, uint32 instance_id) { + return Expedition::FindCachedExpeditionByZoneInstance(zone_id, instance_id); +} + +luabind::object lua_get_expedition_lockout_by_char_id(lua_State* L, uint32 char_id, std::string expedition_name, std::string event_name) { + luabind::adl::object lua_table = luabind::newtable(L); + + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(char_id); + + auto it = std::find_if(lockouts.begin(), lockouts.end(), [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + }); + + if (it != lockouts.end()) + { + lua_table["remaining"] = it->GetSecondsRemaining(); + lua_table["uuid"] = it->GetExpeditionUUID(); + } + + return lua_table; +} + +luabind::object lua_get_expedition_lockouts_by_char_id(lua_State* L, uint32 char_id) { + luabind::adl::object lua_table = luabind::newtable(L); + + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(char_id); + for (const auto& lockout : lockouts) + { + auto lockout_table = lua_table[lockout.GetExpeditionName()]; + if (luabind::type(lockout_table) != LUA_TTABLE) + { + lockout_table = luabind::newtable(L); + } + + auto event_table = lockout_table[lockout.GetEventName()]; + if (luabind::type(event_table) != LUA_TTABLE) + { + event_table = luabind::newtable(L); + } + + event_table["remaining"] = lockout.GetSecondsRemaining(); + event_table["uuid"] = lockout.GetExpeditionUUID(); + } + return lua_table; +} + +luabind::object lua_get_expedition_lockouts_by_char_id(lua_State* L, uint32 char_id, std::string expedition_name) { + luabind::adl::object lua_table = luabind::newtable(L); + + auto lockouts = Expedition::GetExpeditionLockoutsByCharacterID(char_id); + for (const auto& lockout : lockouts) + { + if (lockout.GetExpeditionName() == expedition_name) + { + auto event_table = lua_table[lockout.GetEventName()]; + if (luabind::type(event_table) != LUA_TTABLE) + { + event_table = luabind::newtable(L); + } + event_table["remaining"] = lockout.GetSecondsRemaining(); + event_table["uuid"] = lockout.GetExpeditionUUID(); + } + } + return lua_table; +} + +void lua_add_expedition_lockout_all_clients(std::string expedition_name, std::string event_name, uint32 seconds) { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds); + Expedition::AddLockoutClients(lockout); +} + +void lua_add_expedition_lockout_all_clients(std::string expedition_name, std::string event_name, uint32 seconds, std::string uuid) { + auto lockout = ExpeditionLockoutTimer::CreateLockout(expedition_name, event_name, seconds, uuid); + Expedition::AddLockoutClients(lockout); +} + +void lua_add_expedition_lockout_by_char_id(uint32 char_id, std::string expedition_name, std::string event_name, uint32 seconds) { + Expedition::AddLockoutByCharacterID(char_id, expedition_name, event_name, seconds); +} + +void lua_add_expedition_lockout_by_char_id(uint32 char_id, std::string expedition_name, std::string event_name, uint32 seconds, std::string uuid) { + Expedition::AddLockoutByCharacterID(char_id, expedition_name, event_name, seconds, uuid); +} + +void lua_remove_expedition_lockout_by_char_id(uint32 char_id, std::string expedition_name, std::string event_name) { + Expedition::RemoveLockoutsByCharacterID(char_id, expedition_name, event_name); +} + +void lua_remove_all_expedition_lockouts_by_char_id(uint32 char_id) { + Expedition::RemoveLockoutsByCharacterID(char_id); +} + +void lua_remove_all_expedition_lockouts_by_char_id(uint32 char_id, std::string expedition_name) { + Expedition::RemoveLockoutsByCharacterID(char_id, expedition_name); +} + #define LuaCreateNPCParse(name, c_type, default_value) do { \ cur = table[#name]; \ if(luabind::type(cur) != LUA_TNIL) { \ @@ -2775,7 +2889,22 @@ luabind::scope lua_register_general() { * Content flags */ luabind::def("is_content_flag_enabled", (bool(*)(std::string))&lua_is_content_flag_enabled), - luabind::def("set_content_flag", (void(*)(std::string, bool))&lua_set_content_flag) + luabind::def("set_content_flag", (void(*)(std::string, bool))&lua_set_content_flag), + + luabind::def("get_expedition", &lua_get_expedition), + luabind::def("get_expedition_by_char_id", &lua_get_expedition_by_char_id), + luabind::def("get_expedition_by_dz_id", &lua_get_expedition_by_dz_id), + luabind::def("get_expedition_by_zone_instance", &lua_get_expedition_by_zone_instance), + luabind::def("get_expedition_lockout_by_char_id", &lua_get_expedition_lockout_by_char_id), + luabind::def("get_expedition_lockouts_by_char_id", (luabind::object(*)(lua_State*, uint32))&lua_get_expedition_lockouts_by_char_id), + luabind::def("get_expedition_lockouts_by_char_id", (luabind::object(*)(lua_State*, uint32, std::string))&lua_get_expedition_lockouts_by_char_id), + luabind::def("add_expedition_lockout_all_clients", (void(*)(std::string, std::string, uint32))&lua_add_expedition_lockout_all_clients), + luabind::def("add_expedition_lockout_all_clients", (void(*)(std::string, std::string, uint32, std::string))&lua_add_expedition_lockout_all_clients), + luabind::def("add_expedition_lockout_by_char_id", (void(*)(uint32, std::string, std::string, uint32))&lua_add_expedition_lockout_by_char_id), + luabind::def("add_expedition_lockout_by_char_id", (void(*)(uint32, std::string, std::string, uint32, std::string))&lua_add_expedition_lockout_by_char_id), + luabind::def("remove_expedition_lockout_by_char_id", &lua_remove_expedition_lockout_by_char_id), + luabind::def("remove_all_expedition_lockouts_by_char_id", (void(*)(uint32))&lua_remove_all_expedition_lockouts_by_char_id), + luabind::def("remove_all_expedition_lockouts_by_char_id", (void(*)(uint32, std::string))&lua_remove_all_expedition_lockouts_by_char_id) ]; } diff --git a/zone/lua_group.cpp b/zone/lua_group.cpp index f297d72f1..2cac41418 100644 --- a/zone/lua_group.cpp +++ b/zone/lua_group.cpp @@ -107,6 +107,18 @@ Lua_Mob Lua_Group::GetMember(int index) { return self->members[index]; } +bool Lua_Group::DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name) +{ + Lua_Safe_Call_Bool(); + return self->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name); +} + +bool Lua_Group::DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name, int max_check_count) +{ + Lua_Safe_Call_Bool(); + return self->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name, max_check_count); +} + luabind::scope lua_register_group() { return luabind::class_("Group") .def(luabind::constructor<>()) @@ -129,7 +141,9 @@ luabind::scope lua_register_group() { .def("GetLowestLevel", (int(Lua_Group::*)(void))&Lua_Group::GetLowestLevel) .def("TeleportGroup", (void(Lua_Group::*)(Lua_Mob,uint32,uint32,float,float,float,float))&Lua_Group::TeleportGroup) .def("GetID", (int(Lua_Group::*)(void))&Lua_Group::GetID) - .def("GetMember", (Lua_Mob(Lua_Group::*)(int))&Lua_Group::GetMember); + .def("GetMember", (Lua_Mob(Lua_Group::*)(int))&Lua_Group::GetMember) + .def("DoesAnyMemberHaveExpeditionLockout", (bool(Lua_Group::*)(std::string, std::string))&Lua_Group::DoesAnyMemberHaveExpeditionLockout) + .def("DoesAnyMemberHaveExpeditionLockout", (bool(Lua_Group::*)(std::string, std::string, int))&Lua_Group::DoesAnyMemberHaveExpeditionLockout); } #endif diff --git a/zone/lua_group.h b/zone/lua_group.h index e1aa2a11d..46bc48f50 100644 --- a/zone/lua_group.h +++ b/zone/lua_group.h @@ -44,6 +44,8 @@ public: void TeleportGroup(Lua_Mob sender, uint32 zone_id, uint32 instance_id, float x, float y, float z, float h); int GetID(); Lua_Mob GetMember(int index); + bool DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name); + bool DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name, int max_check_count); }; #endif diff --git a/zone/lua_packet.cpp b/zone/lua_packet.cpp index 648d6fa87..4936259e6 100644 --- a/zone/lua_packet.cpp +++ b/zone/lua_packet.cpp @@ -788,16 +788,18 @@ luabind::scope lua_register_packet_opcodes() { luabind::value("DzRemovePlayer", static_cast(OP_DzRemovePlayer)), luabind::value("DzSwapPlayer", static_cast(OP_DzSwapPlayer)), luabind::value("DzMakeLeader", static_cast(OP_DzMakeLeader)), - luabind::value("DzJoinExpeditionConfirm", static_cast(OP_DzJoinExpeditionConfirm)), - luabind::value("DzJoinExpeditionReply", static_cast(OP_DzJoinExpeditionReply)), + luabind::value("DzExpeditionInvite", static_cast(OP_DzExpeditionInvite)), + luabind::value("DzExpeditionInviteResponse", static_cast(OP_DzExpeditionInviteResponse)), luabind::value("DzExpeditionInfo", static_cast(OP_DzExpeditionInfo)), - luabind::value("DzMemberStatus", static_cast(OP_DzMemberStatus)), - luabind::value("DzLeaderStatus", static_cast(OP_DzLeaderStatus)), + luabind::value("DzMemberListName", static_cast(OP_DzMemberListName)), + luabind::value("DzMemberListStatus", static_cast(OP_DzMemberListStatus)), + luabind::value("DzSetLeaderName", static_cast(OP_DzSetLeaderName)), luabind::value("DzExpeditionEndsWarning", static_cast(OP_DzExpeditionEndsWarning)), - luabind::value("DzExpeditionList", static_cast(OP_DzExpeditionList)), + luabind::value("DzExpeditionLockoutTimers", static_cast(OP_DzExpeditionLockoutTimers)), luabind::value("DzMemberList", static_cast(OP_DzMemberList)), luabind::value("DzCompass", static_cast(OP_DzCompass)), luabind::value("DzChooseZone", static_cast(OP_DzChooseZone)), + luabind::value("DzChooseZoneReply", static_cast(OP_DzChooseZoneReply)), luabind::value("BuffCreate", static_cast(OP_BuffCreate)), luabind::value("GuildStatus", static_cast(OP_GuildStatus)), luabind::value("BuffRemoveRequest", static_cast(OP_BuffRemoveRequest)), diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index ebe88f32a..494e3e0e2 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -19,6 +19,7 @@ #include "lua_parser.h" #include "lua_bit.h" #include "lua_entity.h" +#include "lua_expedition.h" #include "lua_item.h" #include "lua_iteminst.h" #include "lua_mob.h" @@ -1108,7 +1109,9 @@ void LuaParser::MapFunctions(lua_State *L) { lua_register_ruler(), lua_register_ruleb(), lua_register_journal_speakmode(), - lua_register_journal_mode() + lua_register_journal_mode(), + lua_register_expedition(), + lua_register_expedition_lock_messages() ]; } catch(std::exception &ex) { diff --git a/zone/lua_raid.cpp b/zone/lua_raid.cpp index c181b8bd2..ae01012d5 100644 --- a/zone/lua_raid.cpp +++ b/zone/lua_raid.cpp @@ -132,6 +132,17 @@ int Lua_Raid::GetGroupNumber(int index) { return self->members[index].GroupNumber; } +bool Lua_Raid::DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name) +{ + Lua_Safe_Call_Bool(); + return self->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name); +} + +bool Lua_Raid::DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name, int max_check_count) +{ + Lua_Safe_Call_Bool(); + return self->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name, max_check_count); +} luabind::scope lua_register_raid() { return luabind::class_("Raid") @@ -158,7 +169,9 @@ luabind::scope lua_register_raid() { .def("TeleportRaid", (int(Lua_Raid::*)(Lua_Mob,uint32,uint32,float,float,float,float))&Lua_Raid::TeleportRaid) .def("GetID", (int(Lua_Raid::*)(void))&Lua_Raid::GetID) .def("GetMember", (Lua_Client(Lua_Raid::*)(int))&Lua_Raid::GetMember) - .def("GetGroupNumber", (int(Lua_Raid::*)(int))&Lua_Raid::GetGroupNumber); + .def("GetGroupNumber", (int(Lua_Raid::*)(int))&Lua_Raid::GetGroupNumber) + .def("DoesAnyMemberHaveExpeditionLockout", (bool(Lua_Raid::*)(std::string, std::string))&Lua_Raid::DoesAnyMemberHaveExpeditionLockout) + .def("DoesAnyMemberHaveExpeditionLockout", (bool(Lua_Raid::*)(std::string, std::string, int))&Lua_Raid::DoesAnyMemberHaveExpeditionLockout); } #endif diff --git a/zone/lua_raid.h b/zone/lua_raid.h index bc10a729c..9eb100f59 100644 --- a/zone/lua_raid.h +++ b/zone/lua_raid.h @@ -48,6 +48,8 @@ public: int GetID(); Lua_Client GetMember(int index); int GetGroupNumber(int index); + bool DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name); + bool DoesAnyMemberHaveExpeditionLockout(std::string expedition_name, std::string event_name, int max_check_count); }; #endif diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index 82cf3b730..5b8ed1a05 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -37,12 +37,26 @@ #endif #include "client.h" +#include "expedition.h" #include "titles.h" #ifdef THIS /* this macro seems to leak out on some systems */ #undef THIS #endif +#define VALIDATE_THIS_IS_CLIENT \ + do { \ + if (sv_derived_from(ST(0), "Client")) { \ + IV tmp = SvIV((SV*)SvRV(ST(0))); \ + THIS = INT2PTR(Client*, tmp); \ + } else { \ + Perl_croak(aTHX_ "THIS is not of type Client"); \ + } \ + if (THIS == nullptr) { \ + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); \ + } \ + } while (0); + XS(XS_Client_SendSound); /* prototype to pass -Wmissing-prototypes */ XS(XS_Client_SendSound) { dXSARGS; @@ -6759,6 +6773,283 @@ XS(XS_Client_GetClientMaxLevel) { XSRETURN(1); } +XS(XS_Client_CreateExpedition); +XS(XS_Client_CreateExpedition) { + dXSARGS; + if (items != 7 && items != 8) { + Perl_croak(aTHX_ "Usage: Client::CreateExpedition(THIS, string zone_name, uint32 zone_version, uint32 duration, string expedition_name, uint32 min_players, uint32 max_players, [bool disable_messages = false])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string zone_name(SvPV_nolen(ST(1))); + uint32 zone_version = (uint32)SvUV(ST(2)); + uint32 duration = (uint32)SvUV(ST(3)); + std::string expedition_name(SvPV_nolen(ST(4))); + uint32 min_players = (uint32)SvUV(ST(5)); + uint32 max_players = (uint32)SvUV(ST(6)); + bool disable_messages = (items > 7) ? (bool)SvTRUE(ST(7)) : false; + + Expedition* RETVAL = THIS->CreateExpedition(zone_name, zone_version, duration, + expedition_name, min_players, max_players, disable_messages); + + ST(0) = sv_newmortal(); + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + + XSRETURN(1); +} + +XS(XS_Client_GetExpedition); +XS(XS_Client_GetExpedition) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Client::GetExpedition(THIS)"); + } + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + Expedition* RETVAL = THIS->GetExpedition(); + + ST(0) = sv_newmortal(); + sv_setref_pv(ST(0), "Expedition", (void*)RETVAL); + + XSRETURN(1); +} + +XS(XS_Client_GetExpeditionLockouts); +XS(XS_Client_GetExpeditionLockouts) { + dXSARGS; + if (items != 1 && items != 2) { + Perl_croak(aTHX_ "Usage: Client::GetExpeditionLockouts(THIS, [string expedition_name])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + HV* hash = newHV(); + SV* hash_ref = nullptr; // for expedition event hash if filtering on expedition + + std::string expedition_name; + if (items == 2) + { + expedition_name = SvPV_nolen(ST(1)); + } + + auto lockouts = THIS->GetExpeditionLockouts(); + + for (const auto& lockout : lockouts) + { + uint32_t name_len = static_cast(lockout.GetExpeditionName().size()); + uint32_t event_len = static_cast(lockout.GetEventName().size()); + + SV** entry = hv_fetch(hash, lockout.GetExpeditionName().c_str(), name_len, false); + if (!entry) + { + SV* event_hash_ref = newRV_noinc((SV*)newHV()); // takes ownership of hash + if (!expedition_name.empty() && lockout.GetExpeditionName() == expedition_name) + { + hash_ref = event_hash_ref; // save ref to event hash for return + } + entry = hv_store(hash, lockout.GetExpeditionName().c_str(), name_len, event_hash_ref, 0); + } + + if (entry && SvROK(*entry) && SvTYPE(SvRV(*entry)) == SVt_PVHV) + { + HV* event_hash = (HV*)SvRV(*entry); + hv_store(event_hash, lockout.GetEventName().c_str(), event_len, + newSVuv(lockout.GetSecondsRemaining()), 0); + } + } + + SV* rv = &PL_sv_undef; + + if (!expedition_name.empty()) + { + rv = hash_ref ? sv_2mortal(hash_ref) : &PL_sv_undef; // ref that owns event hash for expedition + } + else + { + rv = sv_2mortal(newRV_noinc((SV*)hash)); // owns expedition hash + } + + ST(0) = rv; + XSRETURN(1); +} + +XS(XS_Client_GetLockoutExpeditionUUID); +XS(XS_Client_GetLockoutExpeditionUUID) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Client::GetLockoutExpeditionUUID(THIS, string expedition_name, string event_name)"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name = SvPV_nolen(ST(1)); + std::string event_name = SvPV_nolen(ST(2)); + + auto lockout = THIS->GetExpeditionLockout(expedition_name, event_name); + if (lockout) + { + XSRETURN_PV(lockout->GetExpeditionUUID().c_str()); + } + + XSRETURN_UNDEF; +} + +XS(XS_Client_AddExpeditionLockout); +XS(XS_Client_AddExpeditionLockout) { + dXSARGS; + if (items != 4 && items != 5) { + Perl_croak(aTHX_ "Usage: Client::AddExpeditionLockout(THIS, string expedition_name, string event_name, uint32 seconds, [string uuid])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + uint32 seconds = (uint32)SvUV(ST(3)); + std::string uuid; + + if (items == 5) + { + uuid = SvPV_nolen(ST(4)); + } + + THIS->AddNewExpeditionLockout(expedition_name, event_name, seconds, uuid); + + XSRETURN_EMPTY; +} + +XS(XS_Client_AddExpeditionLockoutDuration); +XS(XS_Client_AddExpeditionLockoutDuration) { + dXSARGS; + if (items != 4 && items != 5) { + Perl_croak(aTHX_ "Usage: Client::AddExpeditionLockoutDuration(THIS, string expedition_name, string event_name, int seconds, [string uuid])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + int seconds = static_cast(SvUV(ST(3))); + std::string uuid; + + if (items == 5) + { + uuid = SvPV_nolen(ST(4)); + } + + THIS->AddExpeditionLockoutDuration(expedition_name, event_name, seconds, uuid, true); + + XSRETURN_EMPTY; +} + +XS(XS_Client_RemoveAllExpeditionLockouts); +XS(XS_Client_RemoveAllExpeditionLockouts) { + dXSARGS; + if (items != 1 && items != 2) { + Perl_croak(aTHX_ "Usage: Client::RemoveAllExpeditionLockouts(THIS, [string expedition_name])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name; + if (items == 2) + { + expedition_name = SvPV_nolen(ST(1)); + } + + THIS->RemoveAllExpeditionLockouts(expedition_name, true); + + XSRETURN_EMPTY; +} + +XS(XS_Client_RemoveExpeditionLockout); +XS(XS_Client_RemoveExpeditionLockout) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Client::RemoveExpeditionLockout(THIS, string expedition_name, string event_name)"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + + THIS->RemoveExpeditionLockout(expedition_name, event_name, true); + + XSRETURN_EMPTY; +} + +XS(XS_Client_HasExpeditionLockout); +XS(XS_Client_HasExpeditionLockout) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Client::HasExpeditionLockout(THIS, string expedition_name, string event_name)"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + + bool result = THIS->HasExpeditionLockout(expedition_name, event_name); + ST(0) = boolSV(result); + + XSRETURN(1); +} + +XS(XS_Client_MovePCDynamicZone); +XS(XS_Client_MovePCDynamicZone) { + dXSARGS; + if (items != 2 && items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Client::MovePCDynamicZone(THIS, uint32 zone_id | string zone_name, [int zone_version = -1], [bool message_if_invalid = true])"); + } + + Client* THIS = nullptr; + VALIDATE_THIS_IS_CLIENT; + + if (SvTYPE(ST(1)) == SVt_PV) + { + std::string zone_name(SvPV_nolen(ST(1))); + int zone_version = (items >= 3) ? static_cast(SvIV(ST(2))) : -1; + if (items == 4) + { + THIS->MovePCDynamicZone(zone_name, zone_version, (bool)SvTRUE(ST(3))); + } + else + { + THIS->MovePCDynamicZone(zone_name, zone_version); + } + } + else if (SvTYPE(ST(1)) == SVt_IV) + { + uint32 zone_id = (uint32)SvUV(ST(1)); + int zone_version = (items >= 3) ? static_cast(SvIV(ST(2))) : -1; + if (items == 3) + { + THIS->MovePCDynamicZone(zone_id, zone_version, (bool)SvTRUE(ST(2))); + } + else + { + THIS->MovePCDynamicZone(zone_id, zone_version); + } + } + else + { + Perl_croak(aTHX_ "Client::MovePCDynamicZone expected an integer or string"); + } + + XSRETURN_EMPTY; +} #ifdef __cplusplus extern "C" @@ -6786,6 +7077,8 @@ XS(boot_Client) { newXSproto(strcpy(buf, "AddAlternateCurrencyValue"), XS_Client_AddAlternateCurrencyValue, file, "$$$"); newXSproto(strcpy(buf, "AddCrystals"), XS_Client_AddCrystals, file, "$$"); newXSproto(strcpy(buf, "AddEXP"), XS_Client_AddEXP, file, "$$;$$"); + newXSproto(strcpy(buf, "AddExpeditionLockout"), XS_Client_AddExpeditionLockout, file, "$$$$;$"); + newXSproto(strcpy(buf, "AddExpeditionLockoutDuration"), XS_Client_AddExpeditionLockoutDuration, file, "$$$$;$"); newXSproto(strcpy(buf, "AddLevelBasedExp"), XS_Client_AddLevelBasedExp, file, "$$;$$"); newXSproto(strcpy(buf, "AddMoneyToPP"), XS_Client_AddMoneyToPP, file, "$$$$$$"); newXSproto(strcpy(buf, "AddPVPPoints"), XS_Client_AddPVPPoints, file, "$$"); @@ -6804,6 +7097,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "CheckSpecializeIncrease"), XS_Client_CheckSpecializeIncrease, file, "$$"); newXSproto(strcpy(buf, "ClearCompassMark"), XS_Client_ClearCompassMark, file, "$"); newXSproto(strcpy(buf, "ClearZoneFlag"), XS_Client_ClearZoneFlag, file, "$$"); + newXSproto(strcpy(buf, "CreateExpedition"), XS_Client_CreateExpedition, file, "$$$$$$$;$"); newXSproto(strcpy(buf, "Connected"), XS_Client_Connected, file, "$"); newXSproto(strcpy(buf, "DecreaseByID"), XS_Client_DecreaseByID, file, "$$$"); newXSproto(strcpy(buf, "DeleteItemInInventory"), XS_Client_DeleteItemInInventory, file, "$$;$$"); @@ -6858,6 +7152,8 @@ XS(boot_Client) { newXSproto(strcpy(buf, "GetEndurance"), XS_Client_GetEndurance, file, "$"); newXSproto(strcpy(buf, "GetEnduranceRatio"), XS_Client_GetEnduranceRatio, file, "$"); newXSproto(strcpy(buf, "GetEXP"), XS_Client_GetEXP, file, "$"); + newXSproto(strcpy(buf, "GetExpedition"), XS_Client_GetExpedition, file, "$"); + newXSproto(strcpy(buf, "GetExpeditionLockouts"), XS_Client_GetExpeditionLockouts, file, "$;$"); newXSproto(strcpy(buf, "GetFace"), XS_Client_GetFace, file, "$"); newXSproto(strcpy(buf, "GetFactionLevel"), XS_Client_GetFactionLevel, file, "$$$$$$$$"); newXSproto(strcpy(buf, "GetFeigned"), XS_Client_GetFeigned, file, "$"); @@ -6880,6 +7176,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "GetLDoNPointsTheme"), XS_Client_GetLDoNPointsTheme, file, "$"); newXSproto(strcpy(buf, "GetLDoNWins"), XS_Client_GetLDoNWins, file, "$"); newXSproto(strcpy(buf, "GetLDoNWinsTheme"), XS_Client_GetLDoNWinsTheme, file, "$$"); + newXSproto(strcpy(buf, "GetLockoutExpeditionUUID"), XS_Client_GetLockoutExpeditionUUID, file, "$$$"); newXSproto(strcpy(buf, "GetMaxEndurance"), XS_Client_GetMaxEndurance, file, "$"); newXSproto(strcpy(buf, "GetModCharacterFactionLevel"), XS_Client_GetModCharacterFactionLevel, file, "$$"); newXSproto(strcpy(buf, "GetMoney"), XS_Client_GetMoney, file, "$$$"); @@ -6907,6 +7204,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "GrantAlternateAdvancementAbility"), XS_Client_GrantAlternateAdvancementAbility, file, "$$$;$"); newXSproto(strcpy(buf, "GuildID"), XS_Client_GuildID, file, "$"); newXSproto(strcpy(buf, "GuildRank"), XS_Client_GuildRank, file, "$"); + newXSproto(strcpy(buf, "HasExpeditionLockout"), XS_Client_HasExpeditionLockout, file, "$$$"); newXSproto(strcpy(buf, "HasSkill"), XS_Client_HasSkill, file, "$$"); newXSproto(strcpy(buf, "HasSpellScribed"), XS_Client_HasSkill, file, "$$"); newXSproto(strcpy(buf, "HasZoneFlag"), XS_Client_HasZoneFlag, file, "$$"); @@ -6938,6 +7236,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "MaxSkill"), XS_Client_MaxSkill, file, "$$;$$"); newXSproto(strcpy(buf, "MemSpell"), XS_Client_MemSpell, file, "$$$;$"); newXSproto(strcpy(buf, "MovePC"), XS_Client_MovePC, file, "$$$$$$"); + newXSproto(strcpy(buf, "MovePCDynamicZone"), XS_Client_MovePCDynamicZone, file, "$$;$$"); newXSproto(strcpy(buf, "MovePCInstance"), XS_Client_MovePCInstance, file, "$$$$$$$"); newXSproto(strcpy(buf, "MoveZone"), XS_Client_MoveZone, file, "$$"); newXSproto(strcpy(buf, "MoveZoneGroup"), XS_Client_MoveZoneGroup, file, "$$"); @@ -6954,6 +7253,8 @@ XS(boot_Client) { newXSproto(strcpy(buf, "QuestReward"), XS_Client_QuestReward, file, "$$;$$$$$$$"); newXSproto(strcpy(buf, "ReadBook"), XS_Client_ReadBook, file, "$$$"); newXSproto(strcpy(buf, "RefundAA"), XS_Client_RefundAA, file, "$$"); + newXSproto(strcpy(buf, "RemoveAllExpeditionLockouts"), XS_Client_RemoveAllExpeditionLockouts, file, "$;$"); + newXSproto(strcpy(buf, "RemoveExpeditionLockout"), XS_Client_RemoveExpeditionLockout, file, "$$$"); newXSproto(strcpy(buf, "RemoveNoRent"), XS_Client_RemoveNoRent, file, "$"); newXSproto(strcpy(buf, "ResetAA"), XS_Client_ResetAA, file, "$"); newXSproto(strcpy(buf, "ResetDisciplineTimer"), XS_Client_ResetDisciplineTimer, file, "$$"); diff --git a/zone/perl_expedition.cpp b/zone/perl_expedition.cpp new file mode 100644 index 000000000..40fe1bad1 --- /dev/null +++ b/zone/perl_expedition.cpp @@ -0,0 +1,676 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "../common/features.h" + +#ifdef EMBPERL_XS_CLASSES + +#include "expedition.h" +#include "zone_store.h" +#include "embperl.h" +#include "../common/global_define.h" + +#ifdef seed +#undef seed +#endif + +#ifdef THIS /* this macro seems to leak out on some systems */ +#undef THIS +#endif + +#define VALIDATE_THIS_IS_EXPEDITION \ + do { \ + if (sv_derived_from(ST(0), "Expedition")) { \ + IV tmp = SvIV((SV*)SvRV(ST(0))); \ + THIS = INT2PTR(Expedition*, tmp); \ + } else { \ + Perl_croak(aTHX_ "THIS is not of type Expedition"); \ + } \ + if (THIS == nullptr) { \ + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); \ + } \ + } while (0); + +XS(XS_Expedition_AddLockout); +XS(XS_Expedition_AddLockout) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Expedition::AddLockout(THIS, string event_name, uint32 seconds)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + std::string event_name(SvPV_nolen(ST(1))); + uint32_t seconds = static_cast(SvUV(ST(2))); + + THIS->AddLockout(event_name, seconds); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_AddLockoutDuration); +XS(XS_Expedition_AddLockoutDuration) { + dXSARGS; + if (items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Expedition::AddLockout(THIS, string event_name, int seconds, [bool members_only = true])"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + std::string event_name(SvPV_nolen(ST(1))); + int seconds = static_cast(SvUV(ST(2))); + if (items == 4) + { + bool members_only = (bool)SvTRUE(ST(3)); + THIS->AddLockoutDuration(event_name, seconds, members_only); + } + else + { + THIS->AddLockoutDuration(event_name, seconds); + } + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_AddReplayLockout); +XS(XS_Expedition_AddReplayLockout) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::AddReplayLockout(THIS, uint32 seconds)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t seconds = static_cast(SvUV(ST(1))); + + THIS->AddReplayLockout(seconds); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_AddReplayLockoutDuration); +XS(XS_Expedition_AddReplayLockoutDuration) { + dXSARGS; + if (items != 2 && items != 3) { + Perl_croak(aTHX_ "Usage: Expedition::AddReplayLockoutDuration(THIS, int seconds, [bool members_only = true])"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + int seconds = static_cast(SvUV(ST(1))); + if (items == 3) + { + bool members_only = (bool)SvTRUE(ST(2)); + THIS->AddReplayLockoutDuration(seconds, members_only); + } + else + { + THIS->AddReplayLockoutDuration(seconds); + } + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_GetDynamicZoneID); +XS(XS_Expedition_GetDynamicZoneID) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetDynamicZoneID(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetDynamicZoneID()); +} + +XS(XS_Expedition_GetID); +XS(XS_Expedition_GetID) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetID(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetID()); +} + +XS(XS_Expedition_GetInstanceID); +XS(XS_Expedition_GetInstanceID) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetInstanceID(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetInstanceID()); +} + +XS(XS_Expedition_GetLeaderName); +XS(XS_Expedition_GetLeaderName) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetLeaderName(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_PV(THIS->GetLeaderName().c_str()); +} + +XS(XS_Expedition_GetLockouts); +XS(XS_Expedition_GetLockouts) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetLockouts(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + HV* hash = newHV(); + + auto lockouts = THIS->GetLockouts(); + for (const auto& lockout : lockouts) + { + hv_store(hash, lockout.first.c_str(), static_cast(lockout.first.size()), + newSVuv(lockout.second.GetSecondsRemaining()), 0); + } + + ST(0) = sv_2mortal(newRV_noinc((SV*)hash)); // take ownership of hash (refcnt remains 1) + XSRETURN(1); +} + +XS(XS_Expedition_GetLootEventByNPCTypeID); +XS(XS_Expedition_GetLootEventByNPCTypeID) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::GetLootEventByNPCTypeID(THIS, uint32 npc_type_id)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t npc_type_id = static_cast(SvUV(ST(1))); + + XSRETURN_PV(THIS->GetLootEventByNPCTypeID(npc_type_id).c_str()); +} + +XS(XS_Expedition_GetLootEventBySpawnID); +XS(XS_Expedition_GetLootEventBySpawnID) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::GetLootEventBySpawnID(THIS, uint32 spawn_id)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t spawn_id = static_cast(SvUV(ST(1))); + + XSRETURN_PV(THIS->GetLootEventBySpawnID(spawn_id).c_str()); +} + +XS(XS_Expedition_GetMemberCount); +XS(XS_Expedition_GetMemberCount) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetMemberCount(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetMemberCount()); +} + +XS(XS_Expedition_GetMembers); +XS(XS_Expedition_GetMembers) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetMembers(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + HV* hash = newHV(); + + auto members = THIS->GetMembers(); + for (const auto& member : members) + { + hv_store(hash, member.name.c_str(), static_cast(member.name.size()), + newSVuv(member.char_id), 0); + } + + ST(0) = sv_2mortal(newRV_noinc((SV*)hash)); + XSRETURN(1); +} + +XS(XS_Expedition_GetName); +XS(XS_Expedition_GetName) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetName(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_PV(THIS->GetName().c_str()); +} + +XS(XS_Expedition_GetSecondsRemaining); +XS(XS_Expedition_GetSecondsRemaining) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetSecondsRemaining(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetDynamicZone().GetSecondsRemaining()); +} + +XS(XS_Expedition_GetUUID); +XS(XS_Expedition_GetUUID) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetUUID(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_PV(THIS->GetUUID().c_str()); +} + +XS(XS_Expedition_GetZoneID); +XS(XS_Expedition_GetZoneID) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetZoneID(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetDynamicZone().GetZoneID()); +} + +XS(XS_Expedition_GetZoneName); +XS(XS_Expedition_GetZoneName) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetZoneName(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_PV(ZoneName(THIS->GetDynamicZone().GetZoneID())); +} + +XS(XS_Expedition_GetZoneVersion); +XS(XS_Expedition_GetZoneVersion) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::GetZoneVersion(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + XSRETURN_UV(THIS->GetDynamicZone().GetZoneVersion()); +} + +XS(XS_Expedition_HasLockout); +XS(XS_Expedition_HasLockout) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::HasLockout(THIS, string event_name)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + std::string event_name(SvPV_nolen(ST(1))); + + bool result = THIS->HasLockout(event_name); + ST(0) = boolSV(result); + XSRETURN(1); +} + +XS(XS_Expedition_HasReplayLockout); +XS(XS_Expedition_HasReplayLockout) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::HasReplayLockout(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + bool result = THIS->HasReplayLockout(); + ST(0) = boolSV(result); + XSRETURN(1); +} + +XS(XS_Expedition_RemoveCompass); +XS(XS_Expedition_RemoveCompass) { + dXSARGS; + if (items != 1) { + Perl_croak(aTHX_ "Usage: Expedition::RemoveCompass(THIS)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + THIS->SetDzCompass(0, 0, 0, 0, true); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_RemoveLockout); +XS(XS_Expedition_RemoveLockout) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::RemoveLockout(THIS, string event_name)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + std::string event_name(SvPV_nolen(ST(1))); + + THIS->RemoveLockout(event_name); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetCompass); +XS(XS_Expedition_SetCompass) { + dXSARGS; + if (items != 5) { + Perl_croak(aTHX_ "Usage: Expedition::SetCompass(THIS, uint32 zone_id | string zone_name, float x, float y, float z)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + float x = static_cast(SvNV(ST(2))); + float y = static_cast(SvNV(ST(3))); + float z = static_cast(SvNV(ST(4))); + + if (SvTYPE(ST(1)) == SVt_PV) + { + std::string zone_name(SvPV_nolen(ST(1))); + THIS->SetDzCompass(zone_name, x, y, z, true); + } + else if (SvTYPE(ST(1)) == SVt_IV) + { + uint32_t zone_id = static_cast(SvUV(ST(1))); + THIS->SetDzCompass(zone_id, x, y, z, true); + } + else + { + Perl_croak(aTHX_ "Expedition::SetCompass expected an integer or string"); + } + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetLocked); +XS(XS_Expedition_SetLocked) { + dXSARGS; + if (items != 2 && items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Expedition::SetLocked(THIS, bool locked, [int lock_msg = 0], [uint32 color = 15])"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + bool locked = (bool)SvTRUE(ST(1)); + int lock_msg = (items == 3) ? static_cast(SvIV(ST(2))) : 0; + if (items == 4) + { + THIS->SetLocked(locked, static_cast(lock_msg), true, (uint32)SvUV(ST(3))); + } + else + { + THIS->SetLocked(locked, static_cast(lock_msg), true); + } + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetLootEventByNPCTypeID); +XS(XS_Expedition_SetLootEventByNPCTypeID) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Expedition::SetLootEventByNPCTypeID(THIS, uint32 npc_type_id, string event_name)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t npc_type_id = static_cast(SvUV(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + + THIS->SetLootEventByNPCTypeID(npc_type_id, event_name); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetLootEventBySpawnID); +XS(XS_Expedition_SetLootEventBySpawnID) { + dXSARGS; + if (items != 3) { + Perl_croak(aTHX_ "Usage: Expedition::SetLootEventBySpawnID(THIS, uint32 spawn_id, string event_name)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t spawn_id = static_cast(SvUV(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + + THIS->SetLootEventBySpawnID(spawn_id, event_name); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetReplayLockoutOnMemberJoin); +XS(XS_Expedition_SetReplayLockoutOnMemberJoin) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::SetReplayLockoutOnMemberJoin(THIS, bool enable)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + bool enable = (bool)SvTRUE(ST(1)); + + THIS->SetReplayLockoutOnMemberJoin(enable, true); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetSafeReturn); +XS(XS_Expedition_SetSafeReturn) { + dXSARGS; + if (items != 6) { + Perl_croak(aTHX_ "Usage: Expedition::SetSafeReturn(THIS, uint32 zone_id | string zone_name, float x, float y, float z, float heading)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + float x = static_cast(SvNV(ST(2))); + float y = static_cast(SvNV(ST(3))); + float z = static_cast(SvNV(ST(4))); + float heading = static_cast(SvNV(ST(5))); + + if (SvTYPE(ST(1)) == SVt_PV) + { + std::string zone_name(SvPV_nolen(ST(1))); + THIS->SetDzSafeReturn(zone_name, x, y, z, heading, true); + } + else if (SvTYPE(ST(1)) == SVt_IV) + { + uint32_t zone_id = static_cast(SvUV(ST(1))); + THIS->SetDzSafeReturn(zone_id, x, y, z, heading, true); + } + else + { + Perl_croak(aTHX_ "Expedition::SetSafeReturn expected an integer or string"); + } + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetSecondsRemaining); +XS(XS_Expedition_SetSecondsRemaining) { + dXSARGS; + if (items != 2) { + Perl_croak(aTHX_ "Usage: Expedition::SetSecondsRemaining(THIS, uint32 seconds_remaining)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + uint32_t seconds_remaining = static_cast(SvUV(ST(1))); + THIS->SetDzSecondsRemaining(seconds_remaining); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_SetZoneInLocation); +XS(XS_Expedition_SetZoneInLocation) { + dXSARGS; + if (items != 5) { + Perl_croak(aTHX_ "Usage: Expedition::SetZoneInLocation(THIS, float x, float y, float z, float heading)"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + float x = static_cast(SvNV(ST(1))); + float y = static_cast(SvNV(ST(2))); + float z = static_cast(SvNV(ST(3))); + float heading = static_cast(SvNV(ST(4))); + + THIS->SetDzZoneInLocation(x, y, z, heading, true); + + XSRETURN_EMPTY; +} + +XS(XS_Expedition_UpdateLockoutDuration); +XS(XS_Expedition_UpdateLockoutDuration) { + dXSARGS; + if (items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Expedition::UpdateLockoutDuration(THIS, string event_name, uint32 seconds, [bool members_only = true])"); + } + + Expedition* THIS = nullptr; + VALIDATE_THIS_IS_EXPEDITION; + + std::string event_name(SvPV_nolen(ST(1))); + uint32_t seconds = static_cast(SvUV(ST(2))); + + if (items == 4) + { + bool members_only = (bool)SvTRUE(ST(3)); + THIS->UpdateLockoutDuration(event_name, seconds, members_only); + } + else + { + THIS->UpdateLockoutDuration(event_name, seconds); + } + + XSRETURN_EMPTY; +} + +XS(boot_Expedition); +XS(boot_Expedition) { + dXSARGS; + char file[256]; + strncpy(file, __FILE__, 256); + file[255] = 0; + + if (items != 1) { + fprintf(stderr, "boot_Expedition does not take any arguments."); + } + char buf[128]; + + XS_VERSION_BOOTCHECK; + newXSproto(strcpy(buf, "AddLockout"), XS_Expedition_AddLockout, file, "$$$"); + newXSproto(strcpy(buf, "AddLockoutDuration"), XS_Expedition_AddLockoutDuration, file, "$$$;$"); + newXSproto(strcpy(buf, "AddReplayLockout"), XS_Expedition_AddReplayLockout, file, "$$"); + newXSproto(strcpy(buf, "AddReplayLockoutDuration"), XS_Expedition_AddReplayLockoutDuration, file, "$$;$"); + newXSproto(strcpy(buf, "GetDynamicZoneID"), XS_Expedition_GetDynamicZoneID, file, "$"); + newXSproto(strcpy(buf, "GetID"), XS_Expedition_GetID, file, "$"); + newXSproto(strcpy(buf, "GetInstanceID"), XS_Expedition_GetInstanceID, file, "$"); + newXSproto(strcpy(buf, "GetLeaderName"), XS_Expedition_GetLeaderName, file, "$"); + newXSproto(strcpy(buf, "GetLockouts"), XS_Expedition_GetLockouts, file, "$"); + newXSproto(strcpy(buf, "GetLootEventByNPCTypeID"), XS_Expedition_GetLootEventByNPCTypeID, file, "$$"); + newXSproto(strcpy(buf, "GetLootEventBySpawnID"), XS_Expedition_GetLootEventBySpawnID, file, "$$"); + newXSproto(strcpy(buf, "GetMemberCount"), XS_Expedition_GetMemberCount, file, "$"); + newXSproto(strcpy(buf, "GetMembers"), XS_Expedition_GetMembers, file, "$"); + newXSproto(strcpy(buf, "GetName"), XS_Expedition_GetName, file, "$"); + newXSproto(strcpy(buf, "GetSecondsRemaining"), XS_Expedition_GetSecondsRemaining, file, "$"); + newXSproto(strcpy(buf, "GetUUID"), XS_Expedition_GetUUID, file, "$"); + newXSproto(strcpy(buf, "GetZoneID"), XS_Expedition_GetZoneID, file, "$"); + newXSproto(strcpy(buf, "GetZoneName"), XS_Expedition_GetZoneName, file, "$"); + newXSproto(strcpy(buf, "GetZoneVersion"), XS_Expedition_GetZoneVersion, file, "$"); + newXSproto(strcpy(buf, "HasLockout"), XS_Expedition_HasLockout, file, "$$"); + newXSproto(strcpy(buf, "HasReplayLockout"), XS_Expedition_HasReplayLockout, file, "$"); + newXSproto(strcpy(buf, "RemoveCompass"), XS_Expedition_RemoveCompass, file, "$"); + newXSproto(strcpy(buf, "RemoveLockout"), XS_Expedition_RemoveLockout, file, "$$"); + newXSproto(strcpy(buf, "SetCompass"), XS_Expedition_SetCompass, file, "$$$$$"); + newXSproto(strcpy(buf, "SetLocked"), XS_Expedition_SetLocked, file, "$$;$$"); + newXSproto(strcpy(buf, "SetLootEventByNPCTypeID"), XS_Expedition_SetLootEventByNPCTypeID, file, "$$$"); + newXSproto(strcpy(buf, "SetLootEventBySpawnID"), XS_Expedition_SetLootEventBySpawnID, file, "$$$"); + newXSproto(strcpy(buf, "SetReplayLockoutOnMemberJoin"), XS_Expedition_SetReplayLockoutOnMemberJoin, file, "$$"); + newXSproto(strcpy(buf, "SetSafeReturn"), XS_Expedition_SetSafeReturn, file, "$$$$$$"); + newXSproto(strcpy(buf, "SetSecondsRemaining"), XS_Expedition_SetSecondsRemaining, file, "$$"); + newXSproto(strcpy(buf, "SetZoneInLocation"), XS_Expedition_SetZoneInLocation, file, "$$$$$"); + newXSproto(strcpy(buf, "UpdateLockoutDuration"), XS_Expedition_UpdateLockoutDuration, file, "$$$;$"); + + HV* stash = gv_stashpvs("ExpeditionLockMessage", GV_ADD); + newCONSTSUB(stash, "None", newSViv(static_cast(ExpeditionLockMessage::None))); + newCONSTSUB(stash, "Close", newSViv(static_cast(ExpeditionLockMessage::Close))); + newCONSTSUB(stash, "Begin", newSViv(static_cast(ExpeditionLockMessage::Begin))); + + XSRETURN_YES; +} + +#endif //EMBPERL_XS_CLASSES diff --git a/zone/perl_groups.cpp b/zone/perl_groups.cpp index c23246b4a..c663ce0ea 100644 --- a/zone/perl_groups.cpp +++ b/zone/perl_groups.cpp @@ -576,6 +576,31 @@ XS(XS_Group_GetMember) { XSRETURN(1); } +XS(XS_Group_DoesAnyMemberHaveExpeditionLockout); +XS(XS_Group_DoesAnyMemberHaveExpeditionLockout) { + dXSARGS; + if (items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Group::DoesAnyMemberHaveExpeditionLockout(THIS, string expedition_name, string event_name, [int max_check_count = 0])"); + } + + Group* THIS = nullptr; + if (sv_derived_from(ST(0), "Group")) { + IV tmp = SvIV((SV *) SvRV(ST(0))); + THIS = INT2PTR(Group *, tmp); + } else + Perl_croak(aTHX_ "THIS is not of type Group"); + if (THIS == nullptr) + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + int max_check_count = (items == 4) ? static_cast(SvIV(ST(3))) : 0; + + bool result = THIS->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name, max_check_count); + ST(0) = boolSV(result); + XSRETURN(1); +} + #ifdef __cplusplus extern "C" #endif @@ -612,6 +637,7 @@ XS(boot_Group) { newXSproto(strcpy(buf, "TeleportGroup"), XS_Group_TeleportGroup, file, "$$$$$$$"); newXSproto(strcpy(buf, "GetID"), XS_Group_GetID, file, "$"); newXSproto(strcpy(buf, "GetMember"), XS_Group_GetMember, file, "$$"); + newXSproto(strcpy(buf, "DoesAnyMemberHaveExpeditionLockout"), XS_Group_DoesAnyMemberHaveExpeditionLockout, file, "$$$;$"); XSRETURN_YES; } diff --git a/zone/perl_raids.cpp b/zone/perl_raids.cpp index 670d445e5..5a7c80c1c 100644 --- a/zone/perl_raids.cpp +++ b/zone/perl_raids.cpp @@ -545,6 +545,31 @@ XS(XS_Raid_GetMember) { XSRETURN(1); } +XS(XS_Raid_DoesAnyMemberHaveExpeditionLockout); +XS(XS_Raid_DoesAnyMemberHaveExpeditionLockout) { + dXSARGS; + if (items != 3 && items != 4) { + Perl_croak(aTHX_ "Usage: Raid::DoesAnyMemberHaveExpeditionLockout(THIS, string expedition_name, string event_name, [int max_check_count = 0])"); + } + + Raid* THIS = nullptr; + if (sv_derived_from(ST(0), "Raid")) { + IV tmp = SvIV((SV *) SvRV(ST(0))); + THIS = INT2PTR(Raid *, tmp); + } else + Perl_croak(aTHX_ "THIS is not of type Raid"); + if (THIS == nullptr) + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); + + std::string expedition_name(SvPV_nolen(ST(1))); + std::string event_name(SvPV_nolen(ST(2))); + int max_check_count = (items == 4) ? static_cast(SvIV(ST(3))) : 0; + + bool result = THIS->DoesAnyMemberHaveExpeditionLockout(expedition_name, event_name, max_check_count); + ST(0) = boolSV(result); + XSRETURN(1); +} + #ifdef __cplusplus extern "C" #endif @@ -581,6 +606,7 @@ XS(boot_Raid) { newXSproto(strcpy(buf, "TeleportRaid"), XS_Raid_TeleportRaid, file, "$$$$$$$"); newXSproto(strcpy(buf, "GetID"), XS_Raid_GetID, file, "$"); newXSproto(strcpy(buf, "GetMember"), XS_Raid_GetMember, file, "$$"); + newXSproto(strcpy(buf, "DoesAnyMemberHaveExpeditionLockout"), XS_Raid_DoesAnyMemberHaveExpeditionLockout, file, "$$$;$"); XSRETURN_YES; } diff --git a/zone/raids.cpp b/zone/raids.cpp index 51c52d8d4..55e6d9e55 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -20,6 +20,7 @@ #include "client.h" #include "entity.h" +#include "expedition.h" #include "groups.h" #include "mob.h" #include "raids.h" @@ -1849,3 +1850,42 @@ void Raid::QueueClients(Mob *sender, const EQApplicationPacket *app, bool ack_re } } } + +std::vector Raid::GetMembers() const +{ + std::vector raid_members; + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + { + if (members[i].membername[0]) + { + raid_members.emplace_back(members[i]); + } + } + return raid_members; +} + +bool Raid::DoesAnyMemberHaveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, int max_check_count) +{ + auto raid_members = GetMembers(); + + if (max_check_count > 0) + { + // priority is leader, group number, then ungrouped members + std::sort(raid_members.begin(), raid_members.end(), + [&](const RaidMember& lhs, const RaidMember& rhs) { + if (lhs.IsRaidLeader) { + return true; + } else if (rhs.IsRaidLeader) { + return false; + } + return lhs.GroupNumber < rhs.GroupNumber; + }); + + raid_members.resize(max_check_count); + } + + return std::any_of(raid_members.begin(), raid_members.end(), [&](const RaidMember& raid_member) { + return Expedition::HasLockoutByCharacterName(raid_member.membername, expedition_name, event_name); + }); +} diff --git a/zone/raids.h b/zone/raids.h index 26ba90a02..a04d1eb54 100644 --- a/zone/raids.h +++ b/zone/raids.h @@ -239,6 +239,10 @@ public: void QueueClients(Mob *sender, const EQApplicationPacket *app, bool ack_required = true, bool ignore_sender = true, float distance = 0, bool group_only = true); + bool DoesAnyMemberHaveExpeditionLockout(const std::string& expedition_name, const std::string& event_name, int max_check_count = 0); + + std::vector GetMembers() const; + RaidMember members[MAX_RAID_MEMBERS]; char leadername[64]; protected: diff --git a/zone/string_ids.h b/zone/string_ids.h index b7ee5ddd4..88a3f9f3e 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -293,8 +293,47 @@ #define TRADESKILL_MISSING_ITEM 3455 //You are missing a %1. #define TRADESKILL_MISSING_COMPONENTS 3456 //Sorry, but you don't have everything you need for this recipe in your general inventory. #define TRADESKILL_LEARN_RECIPE 3457 //You have learned the recipe %1! -#define EXPEDITION_MIN_REMAIN 3551 //You only have %1 minutes remaining before this expedition comes to an end. +#define EXPEDITION_YOU_BELONG 3500 //You cannot create this expedition since you already belong to another. +#define EXPEDITION_YOU_PLAYED_HERE 3501 //You cannot create this expedition for another %1d:%2h:%3m since you have recently played here. +#define REQUIRED_PLAYER_COUNT 3503 //You do not meet the player count requirement. You have %1 players. You must have at least %2 and no more than %3. +#define EXPEDITION_REPLAY_TIMER 3504 //%1 cannot be added to this expedition for another %2D:%3H:%4M since they have recently played in this area. +#define EXPEDITION_AVAILABLE 3507 //%1 is now available to you. +#define DZADD_INVITE 3508 //Sending an invitation to: %1. +#define DZ_PREVENT_ENTERING 3510 //A strange magical presence prevents you from entering. It's too dangerous to enter at the moment. +#define DZADD_INVITE_FAIL 3511 //%1 could not be invited to join you. +#define UNABLE_RETRIEVE_LEADER 3512 //Unable to retrieve information on the leader to check permissions. +#define EXPEDITION_NOT_LEADER 3513 //You are not the expedition leader, only %1 can issue this command. +#define EXPEDITION_NOT_MEMBER 3514 //%1 is not a member of this expedition. +#define EXPEDITION_REMOVED 3516 //%1 has been removed from %2. +#define DZSWAP_INVITE 3517 //Sending an invitation to: %1. They must accept in order to swap party members. +#define DZMAKELEADER_NOT_ONLINE 3518 //%1 is not currently online. You can only transfer leadership to an online member of the expedition you are in. +#define DZLIST_REPLAY_TIMER 3519 //You have %1d:%2h:%3m remaining until you may enter %4. +#define DZMAKELEADER_NAME 3520 //%1 has been made the leader for this expedition. +#define DZMAKELEADER_YOU 3521 //You have been made the leader of this expedition. +#define EXPEDITION_INVITE_ACCEPTED 3522 //%1 has accepted your offer to join your expedition. +#define EXPEDITION_MEMBER_ADDED 3523 //%1 has been added to %2. +#define EXPEDITION_INVITE_ERROR 3524 //%1 accepted your offer to join your expedition but could not due to error(s). +#define EXPEDITION_INVITE_DECLINED 3525 //%1 has declined your offer to join your expedition. +#define EXPEDITION_ASKED_TO_JOIN 3527 //%1 has asked you to join the expedition: %2. Would you like to join? +#define DYNAMICZONE_WAY_IS_BLOCKED 3528 //The way is blocked to you. Perhaps you would be able to enter if there was a reason to come here. +#define EXPEDITION_NO_TIMERS 3529 //You have no outstanding timers. +#define EXPEDITION_MIN_REMAIN 3551 //You only have %1 minutes remaining before this expedition comes to an end. +#define EXPEDITION_LEADER 3552 //Expedition Leader: %1 +#define EXPEDITION_MEMBERS 3553 //Expedition Members: %1 +#define EXPEDITION_EVENT_TIMER 3561 //%1 cannot be added to this expedition since they have recently experienced %2. They must wait another %3D:%4H:%5M until they can experience it again. They may be added to the expedition later, once %2 has been completed. #define LOOT_NOT_ALLOWED 3562 //You are not allowed to loot the item: %1. +#define DZ_UNABLE_RETRIEVE_LEADER 3583 //Unable to retrieve dynamic zone leader to check permissions. +#define DZADD_NOT_ALLOWING 3585 //The expedition is not allowing players to be added. +#define DZADD_NOT_ONLINE 3586 //%1 is not currently online. A player needs to be online to be added to a Dynamic Zone +#define DZADD_EXCEED_MAX 3587 //You can not add another player since you currently have the maximum number of players allowed (%1) in this zone. +#define DZADD_ALREADY_PART 3588 //You can not add %1 since they are already part of this zone. +#define DZADD_LEAVE_ZONE_FIRST 3589 //You can not add %1 since they first need to leave the zone before being allowed back in. +#define DZADD_ALREADY_ASSIGNED 3590 //%1 can not be added to this dynamic zone since they are already assigned to another dynamic zone. +#define DZADD_REPLAY_TIMER 3591 //%1 can not be added to this dynamic zone for another %2D:%3H:%4M since they have recently played this zone. +#define DZADD_EVENT_TIMER 3592 //%1 can not be added to this dynamic zone since they have recently experienced %2. They must wait for another %3D:%4H:%5M, or until event %2 has occurred. +#define DZADD_PENDING 3593 //%1 currently has an outstanding invitation to join this Dynamic Zone. +#define DZADD_PENDING_OTHER 3594 //%1 currently has an outstanding invitation to join another Dynamic Zone. Players may only have one invitation outstanding. +#define DZSWAP_CANNOT_REMOVE 3595 //%1 can not be removed from this dynamic zone since they are not assigned to it. #define NOT_YOUR_TRAP 3671 //You cannot remove this, you are only allowed to remove traps you have set. #define NO_CAST_ON_PET 4045 //You cannot cast this spell on your pet. #define REWIND_WAIT 4059 //You must wait a bit longer before using the rewind command again. diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index d4fa5e572..920a3d958 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -42,6 +42,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "client.h" #include "corpse.h" #include "entity.h" +#include "expedition.h" #include "quest_parser_collection.h" #include "guild_mgr.h" #include "mob.h" @@ -2846,7 +2847,6 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } - case ServerOP_ChangeSharedMem: { std::string hotfix_name = std::string((char*)pack->pBuffer); @@ -2881,6 +2881,45 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } break; } + case ServerOP_CZClientMessageString: + { + auto buf = reinterpret_cast(pack->pBuffer); + Client* client = entity_list.GetClientByName(buf->character_name); + if (client) { + client->MessageString(buf); + } + break; + } + case ServerOP_ExpeditionCreate: + case ServerOP_ExpeditionDeleted: + case ServerOP_ExpeditionLeaderChanged: + case ServerOP_ExpeditionLockout: + case ServerOP_ExpeditionLockoutDuration: + case ServerOP_ExpeditionLockState: + case ServerOP_ExpeditionMemberChange: + case ServerOP_ExpeditionMemberSwap: + case ServerOP_ExpeditionMemberStatus: + case ServerOP_ExpeditionMembersRemoved: + case ServerOP_ExpeditionReplayOnJoin: + case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionDzAddPlayer: + case ServerOP_ExpeditionDzMakeLeader: + case ServerOP_ExpeditionDzCompass: + case ServerOP_ExpeditionDzSafeReturn: + case ServerOP_ExpeditionDzZoneIn: + case ServerOP_ExpeditionDzDuration: + case ServerOP_ExpeditionCharacterLockout: + case ServerOP_ExpeditionExpireWarning: + { + Expedition::HandleWorldMessage(pack); + break; + } + case ServerOP_DzCharacterChange: + case ServerOP_DzRemoveAllCharacters: + { + DynamicZone::HandleWorldMessage(pack); + break; + } default: { std::cout << " Unknown ZSopcode:" << (int)pack->opcode; std::cout << " size:" << pack->size << std::endl; @@ -3265,4 +3304,4 @@ void WorldServer::OnKeepAlive(EQ::Timer *t) { ServerPacket pack(ServerOP_KeepAlive, 0); SendPacket(&pack); -} \ No newline at end of file +} diff --git a/zone/zone.cpp b/zone/zone.cpp index 66644edf1..23e25c280 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -37,6 +37,7 @@ #include "../common/string_util.h" #include "../common/eqemu_logsys.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "npc.h" @@ -1183,6 +1184,9 @@ bool Zone::Init(bool iStaticZone) { petition_list.ClearPetitions(); petition_list.ReadDatabase(); + LogInfo("Loading active Expeditions"); + Expedition::CacheAllFromDatabase(); + LogInfo("Loading timezone data"); zone->zone_time.setEQTimeZone(content_db.GetZoneTZ(zoneid, GetInstanceVersion())); @@ -1487,7 +1491,14 @@ bool Zone::Process() { { if(Instance_Timer->Check()) { - entity_list.GateAllClients(); + // if this is a dynamic zone instance notify system associated with it + auto expedition = Expedition::FindCachedExpeditionByZoneInstance(GetZoneID(), GetInstanceID()); + if (expedition) + { + expedition->RemoveAllMembers(false); // entity list will teleport clients out immediately + } + // todo: move corpses to non-instanced version of dz at same coords (if no graveyard) + entity_list.GateAllClientsToSafeReturn(); database.DeleteInstance(GetInstanceID()); Instance_Shutdown_Timer = new Timer(20000); //20 seconds } @@ -1497,20 +1508,29 @@ bool Zone::Process() { if(Instance_Warning_timer == nullptr) { uint32 rem_time = Instance_Timer->GetRemainingTime(); + uint32_t minutes_warning = 0; if(rem_time < 60000 && rem_time > 55000) { - entity_list.ExpeditionWarning(1); - Instance_Warning_timer = new Timer(10000); + minutes_warning = 1; } else if(rem_time < 300000 && rem_time > 295000) { - entity_list.ExpeditionWarning(5); - Instance_Warning_timer = new Timer(10000); + minutes_warning = 5; } else if(rem_time < 900000 && rem_time > 895000) { - entity_list.ExpeditionWarning(15); - Instance_Warning_timer = new Timer(10000); + minutes_warning = 15; + } + + if (minutes_warning > 0) + { + // expedition expire warnings are handled by world + auto expedition = Expedition::FindCachedExpeditionByZoneInstance(GetZoneID(), GetInstanceID()); + if (!expedition) + { + entity_list.ExpeditionWarning(minutes_warning); + Instance_Warning_timer = new Timer(10000); + } } } else if(Instance_Warning_timer->Check()) @@ -2699,3 +2719,25 @@ void Zone::SetInstanceTimeRemaining(uint32 instance_time_remaining) Zone::instance_time_remaining = instance_time_remaining; } +bool Zone::IsZone(uint32 zone_id, uint16 instance_id) const +{ + return (zoneid == zone_id && instanceid == instance_id); +} + +DynamicZone Zone::GetDynamicZone() +{ + if (GetInstanceID() == 0) + { + return {}; // invalid + } + + auto expedition = Expedition::FindCachedExpeditionByZoneInstance(GetZoneID(), GetInstanceID()); + if (expedition) + { + return expedition->GetDynamicZone(); + } + + // todo: tasks, missions, and quests with an associated dz for this instance id + + return {}; // invalid +} diff --git a/zone/zone.h b/zone/zone.h index 824140700..31c0f16ee 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -33,6 +33,7 @@ #include "spawn2.h" #include "spawngroup.h" #include "aa_ability.h" +#include "dynamiczone.h" #include "pathfinder_interface.h" #include "global_loot_manager.h" @@ -81,6 +82,7 @@ struct item_tick_struct { }; class Client; +class Expedition; class Map; class Mob; class WaterMap; @@ -129,6 +131,7 @@ public: bool IsPVPZone() { return pvpzone; } bool IsSpellBlocked(uint32 spell_id, const glm::vec3 &location); bool IsUCSServerAvailable() { return m_ucss_available; } + bool IsZone(uint32 zone_id, uint16 instance_id) const; bool LoadGroundSpawns(); bool LoadZoneCFG(const char *filename, uint16 instance_id); bool LoadZoneObjects(); @@ -175,6 +178,7 @@ public: void DumpMerchantList(uint32 npcid); int SaveTempItem(uint32 merchantid, uint32 npcid, uint32 item, int32 charges, bool sold = false); int32 MobsAggroCount() { return aggroedmobs; } + DynamicZone GetDynamicZone(); IPathfinder *pathing; LinkedList NPCEmoteList; @@ -217,6 +221,8 @@ public: std::vector zone_grids; std::vector zone_grid_entries; + std::unordered_map> expedition_cache; + time_t weather_timer; Timer spawn2_timer; Timer hot_reload_timer; @@ -402,4 +408,3 @@ private: }; #endif - diff --git a/zone/zoning.cpp b/zone/zoning.cpp index ac8732e82..e294caa4e 100644 --- a/zone/zoning.cpp +++ b/zone/zoning.cpp @@ -21,6 +21,7 @@ #include "../common/rulesys.h" #include "../common/string_util.h" +#include "expedition.h" #include "queryserv.h" #include "quest_parser_collection.h" #include "string_ids.h" @@ -404,6 +405,16 @@ void Client::DoZoneSuccess(ZoneChange_Struct *zc, uint16 zone_id, uint32 instanc if(this->GetPet()) entity_list.RemoveFromHateLists(this->GetPet()); + if (GetPendingExpeditionInviteID() != 0) + { + // live re-invites if client zoned with a pending invite, save pending invite info in world + auto expedition = Expedition::FindCachedExpeditionByID(GetPendingExpeditionInviteID()); + if (expedition) + { + expedition->SendWorldPendingInvite(m_pending_expedition_invite, GetName()); + } + } + LogInfo("Zoning [{}] to: [{}] ([{}]) - ([{}]) x [{}] y [{}] z [{}]", m_pp.name, ZoneName(zone_id), zone_id, instance_id, dest_x, dest_y, dest_z); //set the player's coordinates in the new zone so they have them