diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 000000000..749ab62c5 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,16 @@ +--- +kind: pipeline +type: docker +name: EQEmulator Server Linux CI + +# Limits how many of these builds can run on the drone runner at a time, this isn't about cores +concurrency: + limit: 1 + +steps: + - name: server-build + # Source build script https://github.com/Akkadius/akk-stack/blob/master/containers/eqemu-server/Dockerfile#L20 + image: akkadius/eqemu-server:latest + commands: + - sudo chown eqemu:eqemu /drone/src/ * -R + - git submodule init && git submodule update && mkdir -p build && cd build && cmake -DEQEMU_BUILD_LOGIN=ON -DEQEMU_BUILD_BOTS=ON -DEQEMU_BUILD_LUA=ON -G 'Unix Makefiles' .. && make -j$((`nproc`-4)) \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 24816a323..0efa7f511 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ IF(NOT CMAKE_BUILD_TYPE) SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) ENDIF(NOT CMAKE_BUILD_TYPE) -SET(CMAKE_CXX_STANDARD 11) +SET(CMAKE_CXX_STANDARD 14) SET(CMAKE_CXX_STANDARD_REQUIRED ON) SET(CMAKE_CXX_EXTENSIONS OFF) diff --git a/README.md b/README.md index e999b7050..fea2c23a4 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,4 @@ forum, although pull requests will be much quicker and easier on all parties. + diff --git a/common/classes.cpp b/common/classes.cpp index 3aaeeded3..b9445443f 100644 --- a/common/classes.cpp +++ b/common/classes.cpp @@ -378,6 +378,8 @@ const char *GetClassIDName(uint8 class_id, uint8 level) return "Berserker Guildmaster"; case MERCHANT: return "Merchant"; + case DISCORD_MERCHANT: + return "Discord Merchant"; case ADVENTURERECRUITER: return "Adventure Recruiter"; case ADVENTUREMERCHANT: @@ -388,6 +390,18 @@ const char *GetClassIDName(uint8 class_id, uint8 level) return "Tribute Master"; case GUILD_TRIBUTE_MASTER: return "Guild Tribute Master"; + case GUILD_BANKER: + return "Guild Banker"; + case NORRATHS_KEEPERS_MERCHANT: + return "Radiant Crystal Merchant"; + case DARK_REIGN_MERCHANT: + return "Ebon Crystal Merchant"; + case FELLOWSHIP_MASTER: + return "Fellowship Master"; + case ALT_CURRENCY_MERCHANT: + return "Alternate Currency Merchant"; + case MERCERNARY_MASTER: + return "Mercenary Liaison"; default: return "Unknown"; } diff --git a/common/classes.h b/common/classes.h index f63758937..a35014d94 100644 --- a/common/classes.h +++ b/common/classes.h @@ -61,6 +61,7 @@ #define CORPSE_CLASS 62 // only seen on Danvi's Corpse in Akheva so far.. #define TRIBUTE_MASTER 63 #define GUILD_TRIBUTE_MASTER 64 // not sure +#define GUILD_BANKER 66 #define NORRATHS_KEEPERS_MERCHANT 67 #define DARK_REIGN_MERCHANT 68 #define FELLOWSHIP_MASTER 69 diff --git a/common/database/database_dump_service.cpp b/common/database/database_dump_service.cpp index df083901d..7efd80f83 100644 --- a/common/database/database_dump_service.cpp +++ b/common/database/database_dump_service.cpp @@ -300,8 +300,7 @@ void DatabaseDumpService::Dump() config->DatabaseUsername ); - std::string options = "--allow-keywords --extended-insert"; - + std::string options = "--allow-keywords --extended-insert --max-allowed-packet=1G --net-buffer-length=32704"; if (IsDumpWithNoData()) { options += " --no-data"; } 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 d6e8b0c97..57d4c0795 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -375,13 +375,16 @@ struct NewZone_Struct { /*0686*/ uint16 zone_instance; /*0688*/ uint32 unknown688; /*0692*/ uint8 unknown692[8]; +// Titanium doesn't have a translator, but we can still safely add stuff under here without issues since client memcpy's only what it knows +// Just wastes some bandwidth sending to tit clients /shrug /*0700*/ float fog_density; /*0704*/ uint32 SuspendBuffs; /*0708*/ uint32 FastRegenHP; /*0712*/ uint32 FastRegenMana; /*0716*/ uint32 FastRegenEndurance; /*0720*/ uint32 NPCAggroMaxDist; -/*0724*/ +/*0724*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, if this value is 0, it prevents you from running off edges that would end up underworld +/*0728*/ }; /* @@ -4832,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 @@ -4850,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..c8f724a17 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -118,6 +118,9 @@ namespace Logs { Merchants, ZonePoints, Loot, + Expeditions, + DynamicZones, + Group, MaxCategoryID /* Don't Remove this */ }; @@ -194,7 +197,10 @@ namespace Logs { "HotReload", "Merchants", "ZonePoints", - "Loot" + "Loot", + "Expeditions", + "DynamicZones", + "Group", }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 514f37e21..e32b43b4f 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -161,6 +161,16 @@ OutF(LogSys, Logs::Detail, Logs::Doors, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogGroup(message, ...) do {\ + if (LogSys.log_settings[Logs::Group].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Group, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogGroupDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Group].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Group, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define LogGuilds(message, ...) do {\ if (LogSys.log_settings[Logs::Guilds].is_category_enabled == 1)\ OutF(LogSys, Logs::General, Logs::Guilds, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ @@ -601,6 +611,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__);\ @@ -694,6 +729,12 @@ #define LogDoorsDetail(message, ...) do {\ } while (0) +#define LogGroup(message, ...) do {\ +} while (0) + +#define LogGroupDetail(message, ...) do {\ +} while (0) + #define LogGuilds(message, ...) do {\ } while (0) @@ -952,6 +993,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 c9e80a0f1..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; @@ -1827,6 +1856,7 @@ namespace RoF OUT(FastRegenHP); OUT(FastRegenMana); OUT(FastRegenEndurance); + OUT(underworld_teleport_index); eq->FogDensity = emu->fog_density; @@ -4387,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 1b3827984..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; @@ -1876,6 +1905,7 @@ namespace RoF2 OUT(FastRegenHP); OUT(FastRegenMana); OUT(FastRegenEndurance); + OUT(underworld_teleport_index); eq->FogDensity = emu->fog_density; @@ -4584,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 67947d57f..c5111da6d 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -628,7 +628,7 @@ struct NewZone_Struct { /*0856*/ uint32 scriptNPCReceivedanItem; /*0860*/ uint32 bCheck; // padded bool /*0864*/ uint32 scriptIDSomething; - /*0868*/ uint32 scriptIDSomething2; + /*0868*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, -1 affects some collisions /*0872*/ uint32 scriptIDSomething3; /*0876*/ uint32 SuspendBuffs; // padded bool /*0880*/ uint32 LavaDamage; // LavaDamage value @@ -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 76324d59b..a67e7b972 100644 --- a/common/patches/rof_structs.h +++ b/common/patches/rof_structs.h @@ -575,7 +575,11 @@ struct NewZone_Struct { /*0848*/ int32 unknown848; /*0852*/ uint16 zone_id; /*0854*/ uint16 zone_instance; -/*0856*/ char unknown856[20]; +/*0856*/ uint32 scriptNPCReceivedanItem; +/*0860*/ uint32 bCheck; // padded bool +/*0864*/ uint32 scriptIDSomething; +/*0868*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, -1 affects some collisions +/*0872*/ uint32 scriptIDSomething3; /*0876*/ uint32 SuspendBuffs; /*0880*/ uint32 unknown880; // Seen 50 /*0884*/ uint32 unknown884; // Seen 10 @@ -4811,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 6e3f274e7..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; @@ -1353,6 +1383,7 @@ namespace SoD OUT(FastRegenHP); OUT(FastRegenMana); OUT(FastRegenEndurance); + OUT(underworld_teleport_index); /*fill in some unknowns with observed values, hopefully it will help */ eq->unknown800 = -1; @@ -2973,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 2a8b0cec8..4e7735bf1 100644 --- a/common/patches/sod_structs.h +++ b/common/patches/sod_structs.h @@ -444,7 +444,11 @@ struct NewZone_Struct { /*0848*/ int32 unknown848; /*0852*/ uint16 zone_id; /*0854*/ uint16 zone_instance; -/*0856*/ char unknown856[20]; +/*0856*/ uint32 scriptNPCReceivedanItem; +/*0860*/ uint32 bCheck; // padded bool +/*0864*/ uint32 scriptIDSomething; +/*0868*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, -1 affects some collisions +/*0872*/ uint32 scriptIDSomething3; /*0876*/ uint32 SuspendBuffs; /*0880*/ uint32 unknown880; //seen 50 /*0884*/ uint32 unknown884; //seen 10 @@ -4165,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 94e94f946..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; @@ -1030,6 +1061,7 @@ namespace SoF OUT(FastRegenHP); OUT(FastRegenMana); OUT(FastRegenEndurance); + OUT(underworld_teleport_index); /*fill in some unknowns with observed values, hopefully it will help */ eq->unknown796 = -1; @@ -2434,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 c76ef0b96..bf88bf5c2 100644 --- a/common/patches/sof_structs.h +++ b/common/patches/sof_structs.h @@ -448,7 +448,11 @@ struct NewZone_Struct { /*0844*/ int32 unknown844; /*0848*/ uint16 zone_id; /*0850*/ uint16 zone_instance; -/*0852*/ char unknown852[20]; +/*0852*/ uint32 scriptNPCReceivedanItem; +/*0856*/ uint32 bCheck; // padded bool +/*0860*/ uint32 scriptIDSomething; +/*0864*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, -1 affects some collisions +/*0868*/ uint32 scriptIDSomething3; /*0872*/ uint32 SuspendBuffs; /*0876*/ uint32 unknown876; //seen 50 /*0880*/ uint32 unknown880; //seen 10 @@ -4084,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 49c13833d..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; @@ -1577,6 +1607,7 @@ namespace UF OUT(FastRegenHP); OUT(FastRegenMana); OUT(FastRegenEndurance); + OUT(underworld_teleport_index); eq->FogDensity = emu->fog_density; @@ -3314,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 a87ab7a07..4a4ac8a36 100644 --- a/common/patches/uf_structs.h +++ b/common/patches/uf_structs.h @@ -444,7 +444,11 @@ struct NewZone_Struct { /*0848*/ int32 unknown848; /*0852*/ uint16 zone_id; /*0854*/ uint16 zone_instance; -/*0856*/ char unknown856[20]; +/*0856*/ uint32 scriptNPCReceivedanItem; +/*0860*/ uint32 bCheck; // padded bool +/*0864*/ uint32 scriptIDSomething; +/*0868*/ uint32 underworld_teleport_index; // > 0 teleports w/ zone point index, invalid succors, -1 affects some collisions +/*0872*/ uint32 scriptIDSomething3; /*0876*/ uint32 SuspendBuffs; /*0880*/ uint32 unknown880; //seen 50 /*0884*/ uint32 unknown884; //seen 10 @@ -4246,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 016fefc7d..5ff662c50 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -36,7 +36,7 @@ RULE_CATEGORY(Character) -RULE_INT(Character, MaxLevel, 65, "Sets the highest level attainable by experience for players") +RULE_INT(Character, MaxLevel, 65, "Sets the highest level for players that can be reached through experience") RULE_BOOL(Character, PerCharacterQglobalMaxLevel, false, "Check for qglobal 'CharMaxLevel' character qglobal (Type 5, \"\"), if player tries to level beyond that point, it will not go beyond that level") RULE_BOOL(Character, PerCharacterBucketMaxLevel, false, "Check for data bucket 'CharMaxLevel', if player tries to level beyond that point, it will not go beyond that level") RULE_INT(Character, MaxExpLevel, 0, "Defines the maximum level that can be reached through experience") @@ -57,8 +57,8 @@ RULE_REAL(Character, ExpMultiplier, 0.5, "If greater than 0, the experience gain RULE_REAL(Character, AAExpMultiplier, 0.5, "If greater than 0, the AA experience gained is multiplied by this value. ") RULE_REAL(Character, GroupExpMultiplier, 0.5, "The experience in a group is multiplied by this value in addition to the group multiplier. The group multiplier is: 2 members=x 1.2, 3=x1.4, 4=x1.6, 5=x1.8, 6=x2.16") RULE_REAL(Character, RaidExpMultiplier, 0.2, "The experience gained in raids is multiplied by (1-RaidExpMultiplier) ") -RULE_BOOL(Character, UseXPConScaling, true, "When activated, the experience is modified depending on the difference between player level and NPC level. The settings Green/LightBlue/Blue//White/Yellow and RedModifier are used") -RULE_INT(Character, ShowExpValues, 0, "Show expirience values. 0=normal, 1=show raw experience values, 2=show raw experience values and percent") +RULE_BOOL(Character, UseXPConScaling, true, "When activated, the experience is modified depending on the difference between player level and NPC level. The values from the rules GreenModifier to RedModifier are used") +RULE_INT(Character, ShowExpValues, 0, "Show experience values. 0=normal, 1=show raw experience values, 2=show raw experience values and percent") RULE_INT(Character, GreenModifier, 20, "The experience obtained for green con mobs is multiplied by value/100") RULE_INT(Character, LightBlueModifier, 40, "The experience obtained for light-blue con mobs is multiplied by value/100") RULE_INT(Character, BlueModifier, 90, "The experience obtained for blue con mobs is multiplied by value/100") @@ -80,7 +80,7 @@ RULE_INT(Character, ItemAccuracyCap, 150, "Limit on accuracy granted by items") RULE_INT(Character, ItemAvoidanceCap, 100, "Limit on avoidance granted by items") RULE_INT(Character, ItemCombatEffectsCap, 100, "Limit on combat effects granted by items") RULE_INT(Character, ItemShieldingCap, 35, "Limit on shielding granted by items") -RULE_INT(Character, ItemSpellShieldingCap, 35, "Limit on spell shieldung granted by items") +RULE_INT(Character, ItemSpellShieldingCap, 35, "Limit on spell shielding granted by items") RULE_INT(Character, ItemDoTShieldingCap, 35, "Limit on DoT shielding granted by items") RULE_INT(Character, ItemStunResistCap, 35, "Limit on resistance granted by items") RULE_INT(Character, ItemStrikethroughCap, 35, "Limit on strikethrough granted by items") @@ -91,7 +91,7 @@ RULE_INT(Character, ItemClairvoyanceCap, 250, "Limit on clairvoyance granted by RULE_INT(Character, ItemDSMitigationCap, 50, "Limit on damageshield mitigation granted by items") RULE_INT(Character, ItemEnduranceRegenCap, 15, "Limit on endurance regeneration granted by items") RULE_INT(Character, ItemExtraDmgCap, 150, "Cap for bonuses to melee skills like Bash, Frenzy, etc.") -RULE_INT(Character, HasteCap, 100, "Haste cap for non-v3(overhaste) haste") +RULE_INT(Character, HasteCap, 100, "Haste cap for non-v3(over haste) haste") RULE_INT(Character, SkillUpModifier, 100, "The probability for a skill-up is multiplied by value/100") RULE_BOOL(Character, SharedBankPlat, false, "Shared bank platinum. Off by default to prevent duplication") RULE_BOOL(Character, BindAnywhere, false, "Allows players to bind their soul anywhere in the world") @@ -112,14 +112,14 @@ RULE_INT(Character, RespawnFromHoverTimer, 300, "Respawn Window countdown timer, RULE_BOOL(Character, UseNewStatsWindow, true, "Setting whether the new Stats window, which displays all information, should be used") RULE_BOOL(Character, ItemCastsUseFocus, false, "If true, this allows item clickies to use focuses that have limited maximum levels on them") RULE_INT(Character, MinStatusForNoDropExemptions, 80, "This allows status x and higher to trade no drop items") -RULE_INT(Character, SkillCapMaxLevel, 75, "Sets the Maximum Level used for Skill Caps (from skill_caps table). -1 makes it use MaxLevel rule value. It is set to 75 because PEQ only has skillcaps up to that level, and grabbing the players' skill past 75 will return 0, breaking all skills past that level. This helps servers with obsurd level caps (75+ level cap) function without any modifications") +RULE_INT(Character, SkillCapMaxLevel, 75, "Sets the Maximum Level used for Skill Caps (from skill_caps table). -1 makes it use MaxLevel rule value. It is set to 75 because PEQ only has skill caps up to that level, and grabbing the players' skill past 75 will return 0, breaking all skills past that level. This helps servers with obsurd level caps (75+ level cap) function without any modifications") RULE_INT(Character, StatCap, 0, "If StatCap > 0 then this value is used. If it is 0, the value of the following code is used: If Level < 61: 255. If Level >= 61 and the client SoF or newer: 255 + 5 x (level -60). If the client is older than SoF and the level < 71 then: 255 + x (level-60). In all other cases: 330.") RULE_BOOL(Character, CheckCursorEmptyWhenLooting, true, "If true, a player cannot loot a corpse (player or NPC) with an item on their cursor") RULE_BOOL(Character, MaintainIntoxicationAcrossZones, true, "If true, alcohol effects are maintained across zoning and logging out/in") RULE_BOOL(Character, EnableDiscoveredItems, true, "If enabled, it enables EVENT_DISCOVER_ITEM and also saves character names and timestamps for the first time an item is discovered") RULE_BOOL(Character, EnableXTargetting, true, "Enable Extended Targeting Window, for users with UF and later clients") RULE_BOOL(Character, EnableAggroMeter, true, "Enable Aggro Meter, for users with RoF and later clients") -RULE_BOOL(Character, KeepLevelOverMax, false, "Don't delevel a character that has somehow gone over the level cap") +RULE_BOOL(Character, KeepLevelOverMax, false, "Don't de-level a character that has somehow gone over the level cap") RULE_INT(Character, FoodLossPerUpdate, 32, "How much food/water you lose per stamina update") RULE_BOOL(Character, EnableHungerPenalties, false, "Being hungry/thirsty has negative effects -- it does appear normal live servers do not have penalties") RULE_BOOL(Character, EnableFoodRequirement, true, "If disabled, food is no longer required") @@ -129,16 +129,16 @@ RULE_INT(Character, BaseRunSpeedCap, 158, "Base Run Speed Cap, on live it's 158% RULE_INT(Character, OrnamentationAugmentType, 20, "Ornamentation Augment Type") RULE_REAL(Character, EnvironmentDamageMulipliter, 1, "Multiplier for environmental damage like fall damage.") RULE_BOOL(Character, UnmemSpellsOnDeath, true, "Setting whether at death all memorized Spells are forgotten") -RULE_INT(Character, TradeskillUpAlchemy, 2, "Alchemy skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpBaking, 2, "Baking skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpBlacksmithing, 2, "Blacksmithing skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpBrewing, 3, "Brewing skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpFletching, 2, "Fletching skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpJewelcrafting, 2, "Jewelcrafting skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpMakePoison, 2, "Make Poison skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpPottery, 4, "Pottery skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpResearch, 1, "Research skillup rate adjust. Lower is faster") -RULE_INT(Character, TradeskillUpTinkering, 2, "Tinkering skillup rate adjust. Lower is faster") +RULE_INT(Character, TradeskillUpAlchemy, 2, "Alchemy skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpBaking, 2, "Baking skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpBlacksmithing, 2, "Blacksmithing skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpBrewing, 3, "Brewing skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpFletching, 2, "Fletching skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpJewelcrafting, 2, "Jewelcrafting skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpMakePoison, 2, "Make Poison skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpPottery, 4, "Pottery skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpResearch, 1, "Research skillup rate adjustment. Lower is faster") +RULE_INT(Character, TradeskillUpTinkering, 2, "Tinkering skillup rate adjustment. Lower is faster") RULE_BOOL(Character, MarqueeHPUpdates, false, "Will show health percentage in center of screen if health lesser than 100%") RULE_INT(Character, IksarCommonTongue, 95, "Starting value for Common Tongue for Iksars") RULE_INT(Character, OgreCommonTongue, 95, "Starting value for Common Tongue for Ogres") @@ -147,7 +147,7 @@ RULE_BOOL(Character, ActiveInvSnapshots, false, "Takes a periodic snapshot of in RULE_INT(Character, InvSnapshotMinIntervalM, 180, "Minimum time between inventory snapshots (minutes)") RULE_INT(Character, InvSnapshotMinRetryM, 30, "Time to re-attempt an inventory snapshot after a failure (minutes)") RULE_INT(Character, InvSnapshotHistoryD, 30, "Time to keep snapshot entries (days)") -RULE_BOOL(Character, RestrictSpellScribing, false, "Setting whether to testricts spell scribing to allowable races/classes of spell scroll") +RULE_BOOL(Character, RestrictSpellScribing, false, "Setting whether to restrict spell scribing to allowable races/classes of spell scroll") RULE_BOOL(Character, UseStackablePickPocketing, true, "Allows stackable pickpocketed items to stack instead of only being allowed in empty inventory slots") RULE_BOOL(Character, EnableAvoidanceCap, false, "Setting whether the avoidance cap should be activated") RULE_INT(Character, AvoidanceCap, 750, "750 Is a pretty good value, seen people dodge all attacks beyond 1,000 Avoidance") @@ -197,7 +197,7 @@ RULE_INT(Skills, MaxTrainSpecializations, 50, "Maximum level a GM trainer will t RULE_INT(Skills, SwimmingStartValue, 100, "Start value of swimming skill") RULE_BOOL(Skills, TrainSenseHeading, false, "Switch whether SenseHeading is trained by use") RULE_INT(Skills, SenseHeadingStartValue, 200, "Start value of sense heading skill") -RULE_BOOL(Skills, SelfLanguageLearning, true, "Enabling self learning of languages") +RULE_BOOL(Skills, SelfLanguageLearning, true, "Enabling self-learning of languages") RULE_BOOL(Skills, RequireTomeHandin, false, "Disable click-to-learn and force hand in to Guild Master") RULE_CATEGORY_END() @@ -246,6 +246,7 @@ RULE_BOOL(World, MaxClientsSimplifiedLogic, false, "New logic that only uses Exe RULE_INT (World, TellQueueSize, 20, "Maximum tell queue size") RULE_BOOL(World, StartZoneSameAsBindOnCreation, true, "Should the start zone always be the same location as your bind?") RULE_BOOL(World, EnforceCharacterLimitAtLogin, false, "Enforce the limit for characters that are online at login") +RULE_BOOL(World, EnableDevTools, true, "Enable or Disable the Developer Tools globally (Most of the time you want this enabled)") RULE_CATEGORY_END() RULE_CATEGORY(Zone) @@ -339,7 +340,7 @@ RULE_BOOL(Spells, ReflectMessagesClose, true, "True (Live functionality) is for RULE_INT(Spells, VirusSpreadDistance, 30, "The distance a viral spell will jump to its next victim") RULE_BOOL(Spells, LiveLikeFocusEffects, true, "Determines whether specific healing, dmg and mana reduction focuses are randomized") RULE_INT(Spells, BaseImmunityLevel, 55, "The level that targets start to be immune to stun, fear and mez spells with a maximum level of 0") -RULE_BOOL(Spells, NPCIgnoreBaseImmunity, true, "Whether or not NPCget to ignore the BaseImmunityLevel for their spells") +RULE_BOOL(Spells, NPCIgnoreBaseImmunity, true, "Whether or not NPC get to ignore the BaseImmunityLevel for their spells") RULE_REAL(Spells, AvgSpellProcsPerMinute, 6.0, "Adjust rate for sympathetic spell procs") RULE_INT(Spells, ResistFalloff, 67, "Maximum that level that will adjust our resist chance based on level modifiers") RULE_INT(Spells, CharismaEffectiveness, 10, "Determines how much resist modification charisma applies to charm/pacify checks. Default 10 CHA = -1 resist mod") @@ -397,7 +398,7 @@ RULE_INT(Combat, PetBaseCritChance, 0, "Pet base crit chance") RULE_INT(Combat, NPCBashKickLevel, 6, "The level that NPCcan KICK/BASH") RULE_INT(Combat, NPCBashKickStunChance, 15, "Percent chance that a bash/kick will stun") RULE_INT(Combat, MeleeCritDifficulty, 8900, "Value against which is rolled to check if a melee crit is triggered. Lower is easier") -RULE_INT(Combat, ArcheryCritDifficulty, 3400, "Value against which is rolled to check if a archery crit is triggered. Lower is easier") +RULE_INT(Combat, ArcheryCritDifficulty, 3400, "Value against which is rolled to check if an archery crit is triggered. Lower is easier") RULE_INT(Combat, ThrowingCritDifficulty, 1100, "Value against which is rolled to check if a throwing crit is triggered. Lower is easier") RULE_BOOL(Combat, NPCCanCrit, false, "Setting whether an NPC can land critical hits") RULE_BOOL(Combat, UseIntervalAC, true, "Switch whether bonuses, armour class, multipliers, classes and caps should be considered in the calculation of damage values") @@ -416,7 +417,7 @@ RULE_REAL(Combat, BaseProcChance, 0.035, "Base chance for procs") RULE_REAL(Combat, ProcDexDivideBy, 11000, "Divisor for the probability of a proc increased by dexterity") RULE_BOOL(Combat, AdjustSpecialProcPerMinute, true, "Set PPM for special abilities like HeadShot (Live does this as of 4-14)") RULE_REAL(Combat, AvgSpecialProcsPerMinute, 2.0, "Unclear what best value is atm") -RULE_REAL(Combat, BaseHitChance, 69.0, "Base chance to hiz") +RULE_REAL(Combat, BaseHitChance, 69.0, "Base chance to hit") RULE_REAL(Combat, NPCBonusHitChance, 26.0, "Bonus chance to hit for NPC") RULE_REAL(Combat, HitFalloffMinor, 5.0, "Hit will fall off up to value over the initial level range (percent)") RULE_REAL(Combat, HitFalloffModerate, 7.0, "Hit will fall off up to value over the three levels after the initial level range (percent)") @@ -490,14 +491,14 @@ RULE_BOOL(Combat, ProjectileDmgOnImpact, true, "If enabled, projectiles (ie arro RULE_BOOL(Combat, MeleePush, true, "Eenable melee push") RULE_INT(Combat, MeleePushChance, 50, "NPC chance the target will be pushed. Made up, 100 actually isn't that bad") RULE_BOOL(Combat, UseLiveCombatRounds, true, "Turn this false if you don't want to worry about fixing up combat rounds for NPCs") -RULE_INT(Combat, NPCAssistCap, 5, "Maxiumium number of NPCthat will assist another NPC at once") +RULE_INT(Combat, NPCAssistCap, 5, "Maxiumium number of NPC that will assist another NPC at once") RULE_INT(Combat, NPCAssistCapTimer, 6000, "Time a NPC will take to clear assist aggro cap space (milliseconds)") RULE_BOOL(Combat, UseRevampHandToHand, false, "Use h2h revamped dmg/delays I believe this was implemented during SoF") -RULE_BOOL(Combat, ClassicMasterWu, false, "Classic master wu uses a random special, modern doesn't") +RULE_BOOL(Combat, ClassicMasterWu, false, "Classic Master Wu uses a random special, modern doesn't") RULE_REAL(Combat, HitBoxMod, 1.00, "Added to test hit boxes.") RULE_INT(Combat, LevelToStopDamageCaps, 0, "Level to stop damage caps. 1 will effectively disable them, 20 should give basically same results as old incorrect system") RULE_INT(Combat, LevelToStopACTwinkControl, 50, "Level to stop armorclass twink control. 1 will effectively disable it, 50 should give basically same results as current system") -RULE_BOOL(Combat, ClassicNPCBackstab, false, "True disables npc facestab - NPCget normal attack if not behind") +RULE_BOOL(Combat, ClassicNPCBackstab, false, "True disables NPC facestab - NPC get normal attack if not behind") RULE_BOOL(Combat, UseNPCDamageClassLevelMods, true, "Uses GetClassLevelDamageMod calc in npc_scale_manager") RULE_BOOL(Combat, UseExtendedPoisonProcs, false, "Allow old school poisons to last until characrer zones, at a lower proc rate") RULE_CATEGORY_END() @@ -508,7 +509,7 @@ RULE_INT(NPC, MajorNPCCorpseDecayTimeMS, 1500000, "NPC corpse decay time, if NPC RULE_INT(NPC, CorpseUnlockTimer, 150000, "Time after which corpses are unlocked for everyone to loot (milliseconds)") RULE_INT(NPC, EmptyNPCCorpseDecayTimeMS, 0, "NPC corpse decay time, if no items are left on the corpse (milliseconds)") RULE_BOOL(NPC, UseItemBonusesForNonPets, true, "Switch whether item bonuses should be used for NPCs who are not pets") -RULE_BOOL(NPC, UseBaneDamage, false, "If NPCs can't inheriently hit the target we don't add bane/magic dmg which isn't exactly the same as PCs") +RULE_BOOL(NPC, UseBaneDamage, false, "If NPCs can't inherently hit the target we don't add bane/magic dmg which isn't exactly the same as PCs") RULE_INT(NPC, SayPauseTimeInSec, 5, "Time span in which an NPC pauses his movement after a Say event without aggro (seconds)") RULE_INT(NPC, OOCRegen, 0, "Enable out-of-combat regeneration for NPC") RULE_BOOL(NPC, BuffFriends, false, "Setting whether NPC should buff other NPC") @@ -516,27 +517,27 @@ RULE_BOOL(NPC, EnableNPCQuestJournal, false, "Setting whether the NPC Quest Jour RULE_INT(NPC, LastFightingDelayMovingMin, 10000, "Minimum time before mob goes home after all aggro loss (milliseconds)") RULE_INT(NPC, LastFightingDelayMovingMax, 20000, "Maximum time before mob goes home after all aggro loss (milliseconds)") RULE_BOOL(NPC, SmartLastFightingDelayMoving, true, "When true, mobs that started going home previously will do so again immediately if still on FD hate list") -RULE_BOOL(NPC, ReturnNonQuestNoDropItems, false, "Returns NO DROP items on NPCthat don't have an EVENT_TRADE sub in their script") +RULE_BOOL(NPC, ReturnNonQuestNoDropItems, false, "Returns NO DROP items on NPC that don't have an EVENT_TRADE sub in their script") RULE_INT(NPC, StartEnrageValue, 9, " Percentage HP that an NPC will begin to enrage") RULE_BOOL(NPC, LiveLikeEnrage, false, "If set to true then only player controlled pets will enrage") RULE_BOOL(NPC, EnableMeritBasedFaction, false, "If set to true, faction will given in the same way as experience (solo/group/raid)") RULE_INT(NPC, NPCToNPCAggroTimerMin, 500, "Minimum time span after which one NPC aggro another NPC (milliseconds)") RULE_INT(NPC, NPCToNPCAggroTimerMax, 6000, "Maximum time span after which one NPC aggro another NPC (milliseconds)") -RULE_BOOL(NPC, UseClassAsLastName, true, "Uses class archetype as LastName for NPCwith none") +RULE_BOOL(NPC, UseClassAsLastName, true, "Uses class archetype as LastName for NPC with none") RULE_BOOL(NPC, NewLevelScaling, true, "Better level scaling, use old if new formulas would break your server") RULE_INT(NPC, NPCGatePercent, 20, " Percentage at which the NPC Will attempt to gate at") RULE_BOOL(NPC, NPCGateNearBind, false, "Will NPC attempt to gate when near bind location?") RULE_INT(NPC, NPCGateDistanceBind, 75, "Distance from bind before NPC will attempt to gate") RULE_BOOL(NPC, NPCHealOnGate, true, "Will the NPC Heal on Gate") RULE_BOOL(NPC, UseMeditateBasedManaRegen, false, "Based NPC ooc regen on Meditate skill") -RULE_REAL(NPC, NPCHealOnGateAmount, 25, "How much the npc will heal on gate if enabled") +RULE_REAL(NPC, NPCHealOnGateAmount, 25, "How much the NPC will heal on gate if enabled") RULE_CATEGORY_END() RULE_CATEGORY(Aggro) RULE_BOOL(Aggro, SmartAggroList, true, "Smart aggro list attempts to choose targets in a much smarter fashion, prefering players to pets, sitting and critically injured players to normal players, and players in melee range to players not") RULE_INT(Aggro, SittingAggroMod, 35, "Aggro increase against sitting targets. 35=35%") RULE_INT(Aggro, MeleeRangeAggroMod, 10, "Aggro increase against targets in melee range. 10=10%") -RULE_INT(Aggro, CurrentTargetAggroMod, 0, "Aggro increase against current target. 0% = prefer the current target to any other. Makes it harder for our NPCto switch targets") +RULE_INT(Aggro, CurrentTargetAggroMod, 0, "Aggro increase against current target. 0% = prefer the current target to any other. Makes it harder for our NPC to switch targets") RULE_INT(Aggro, CriticallyWoundedAggroMod, 100, "Aggro increase against critical wounded targets") RULE_INT(Aggro, SpellAggroMod, 100, "Aggro increase for spells") RULE_INT(Aggro, SongAggroMod, 33, "Aggro increase for songs") @@ -547,7 +548,8 @@ RULE_INT(Aggro, IntAggroThreshold, 75, "Int lesser or equal the value will aggro RULE_BOOL(Aggro, AllowTickPulling, false, "tick pulling is an exploit in an NPC's call for help fixed sometime in 2006 on live") RULE_INT(Aggro, MinAggroLevel, 18, "Minimum level for use with UseLevelAggro") RULE_BOOL(Aggro, UseLevelAggro, true, "MinAggroLevel rule value+ and Undead will aggro regardless of level difference. This will disabled Rule:IntAggroThreshold if set to true") -RULE_INT(Aggro, ClientAggroCheckInterval, 6, "Interval in which clients actually check for aggro - in seconds") +RULE_INT(Aggro, ClientAggroCheckMovingInterval, 1000, "Interval in which clients actually check for aggro while moving - in milliseconds - this should be lower than ClientAggroCheckIdleInterval") +RULE_INT(Aggro, ClientAggroCheckIdleInterval, 6000, "Interval in which clients actually check for aggro while idle - in milliseconds - this should be higher than ClientAggroCheckMovingInterval") RULE_REAL(Aggro, PetAttackRange, 40000.0, "Maximum squared range /pet attack works at default is 200") RULE_BOOL(Aggro, NPCAggroMaxDistanceEnabled, true, "If enabled, NPC's will drop aggro beyond 600 units or what is defined at the zone level") RULE_CATEGORY_END() @@ -619,7 +621,7 @@ RULE_BOOL(Chat, EnableVoiceMacros, true, "Enable voice macros") RULE_BOOL(Chat, EnableMailKeyIPVerification, true, "Setting whether the authenticity of the client should be verified via its IP address when accessing the InGame mailbox") RULE_BOOL(Chat, EnableAntiSpam, true, "Enable anti-spam system for chat") RULE_BOOL(Chat, SuppressCommandErrors, false, "Do not suppress command errors by default") -RULE_INT(Chat, MinStatusToBypassAntiSpam, 100, "Minimum status to bypass the anti spam system") +RULE_INT(Chat, MinStatusToBypassAntiSpam, 100, "Minimum status to bypass the anti-spam system") RULE_INT(Chat, MinimumMessagesPerInterval, 4, "Minimum number of chat messages allowed per interval. The karma value is added to this value") RULE_INT(Chat, MaximumMessagesPerInterval, 12, "Maximum value of chat messages allowed per interval") RULE_INT(Chat, MaxMessagesBeforeKick, 20, "If an attempt is made to send more than the maximum allowed number of chat messages per interval, the client will be disconnected after this absolute number of messages") @@ -783,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/say_link.cpp b/common/say_link.cpp index 5fb847294..b47897112 100644 --- a/common/say_link.cpp +++ b/common/say_link.cpp @@ -101,7 +101,7 @@ const std::string &EQ::SayLinkEngine::GenerateLink() m_Error = true; m_Link = ""; LogError("SayLinkEngine::GenerateLink() failed to generate a useable say link"); - LogError(">> LinkType: {}, Lengths: {link: {}({}), body: {}({}), text: {}({})}", + LogError(">> LinkType: {}, Lengths: [link: {}({}), body: {}({}), text: {}({})]", m_LinkType, m_Link.length(), EQ::constants::SAY_LINK_MAXIMUM_SIZE, diff --git a/common/servertalk.h b/common/servertalk.h index 5ed3b3306..816807b56 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -80,6 +80,7 @@ #define ServerOP_UpdateSpawn 0x003f #define ServerOP_SpawnStatusChange 0x0040 #define ServerOP_DropClient 0x0041 // DropClient +#define ServerOP_ChangeGroupLeader 0x0042 #define ServerOP_ReloadTasks 0x0060 #define ServerOP_DepopAllPlayersCorpses 0x0061 #define ServerOP_ReloadTitles 0x0062 @@ -140,6 +141,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 +287,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 @@ -831,6 +862,7 @@ struct ServerGroupLeave_Struct { uint16 instance_id; uint32 gid; char member_name[64]; //kick this member from the group + bool checkleader; }; struct ServerGroupJoin_Struct { @@ -840,10 +872,20 @@ struct ServerGroupJoin_Struct { char member_name[64]; //this person is joining the group }; +struct ServerGroupLeader_Struct { + uint32 zoneid; + uint16 instance_id; + uint32 gid; + char leader_name[64]; + char oldleader_name[64]; +}; + struct ServerForceGroupUpdate_Struct { uint32 origZoneID; uint16 instance_id; uint32 gid; + char leader_name[64]; + char oldleader_name[64]; }; struct ServerGroupChannelMessage_Struct { @@ -1433,6 +1475,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 +2008,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/spdat.cpp b/common/spdat.cpp index 3f0ae6e41..fe6e4db9c 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -1271,7 +1271,7 @@ bool IsSpellUsableThisZoneType(uint16 spell_id, uint8 zone_type) return false; } -const char* GetSpellName(int16 spell_id) +const char* GetSpellName(uint16 spell_id) { return spells[spell_id].name; } diff --git a/common/spdat.h b/common/spdat.h index 811825757..d6d6ef144 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -981,6 +981,6 @@ uint32 GetNimbusEffect(uint16 spell_id); int32 GetFuriousBash(uint16 spell_id); bool IsShortDurationBuff(uint16 spell_id); bool IsSpellUsableThisZoneType(uint16 spell_id, uint8 zone_type); -const char *GetSpellName(int16 spell_id); +const char *GetSpellName(uint16 spell_id); #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 e524afb01..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 9157 +#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 31b091ff8..76178bf62 100644 --- a/utils/patches/patch_RoF.conf +++ b/utils/patches/patch_RoF.conf @@ -366,6 +366,7 @@ OP_CancelSneakHide=0x265f OP_AggroMeterLockTarget=0x70b7 OP_AggroMeterTargetInfo=0x18fe OP_AggroMeterUpdate=0x75aa +OP_UnderWorld=0x44f9 # clients sends up when they detect an underworld issue, might be useful for cheat detection OP_DzQuit=0x5fc8 OP_DzListTimers=0x67b9 @@ -374,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 a39fde999..96f3258a8 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -367,25 +367,29 @@ OP_CancelSneakHide=0x0927 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 619f986c1..1ef8a138a 100644 --- a/utils/patches/patch_SoD.conf +++ b/utils/patches/patch_SoD.conf @@ -357,6 +357,7 @@ OP_OpenContainer=0x3278 OP_Marquee=0x7dc9 OP_Fling=0x2b88 OP_CancelSneakHide=0x7705 +OP_UnderWorld=0x51ae # clients sends up when they detect an underworld issue, might be useful for cheat detection # Expedition OP_DzQuit=0x054e @@ -366,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 6a5d41fb4..1c5fd3eda 100644 --- a/utils/patches/patch_SoF.conf +++ b/utils/patches/patch_SoF.conf @@ -338,6 +338,7 @@ OP_OpenContainer=0x10e3 OP_Marquee=0x2f75 OP_Untargetable=0x3e36 OP_CancelSneakHide=0x5335 +OP_UnderWorld=0x7580 # clients sends up when they detect an underworld issue, might be useful for cheat detection #expedition OP_DzQuit=0x20d6 @@ -347,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 9a7e1e800..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 @@ -543,6 +544,7 @@ OP_PlayerStateRemove=0x381d OP_VoiceMacroIn=0x2866 # Client to Server OP_VoiceMacroOut=0x2ec6 # Server to Client OP_CameraEffect=0x0937 # Correct +OP_UnderWorld=0x7186 # clients sends up when they detect an underworld issue, might be useful for cheat detection #named unknowns, to make looking for real unknown easier OP_AnnoyingZoneUnknown=0x729c diff --git a/utils/patches/patch_UF.conf b/utils/patches/patch_UF.conf index 69d6bb1e5..8efce91f6 100644 --- a/utils/patches/patch_UF.conf +++ b/utils/patches/patch_UF.conf @@ -368,6 +368,7 @@ OP_OpenContainer=0x041a OP_Marquee=0x3675 OP_Fling=0x51b1 OP_CancelSneakHide=0x7686 +OP_UnderWorld=0x2d9d # clients sends up when they detect an underworld issue, might be useful for cheat detection OP_DzQuit=0x1539 OP_DzListTimers=0x21e9 @@ -376,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 63af98bbd..bdc41a337 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -411,6 +411,8 @@ 9155|2020_08_15_lootdrop_level_filtering.sql|SHOW COLUMNS from `lootdrop_entries` LIKE 'trivial_min_level'|empty| 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_09_underworld.sql b/utils/sql/git/required/2020_12_09_underworld.sql new file mode 100644 index 000000000..f2f2ba032 --- /dev/null +++ b/utils/sql/git/required/2020_12_09_underworld.sql @@ -0,0 +1,6 @@ +ALTER TABLE `zone` ADD COLUMN `underworld_teleport_index` INT(4) NOT NULL DEFAULT '0'; +UPDATE `zone` SET `underworld` = '-2030' WHERE `zoneidnumber` = '71'; +UPDATE `zone` SET `underworld_teleport_index` = '11' WHERE `zoneidnumber` = '71'; +UPDATE `zone` SET `underworld_teleport_index` = '-1' WHERE `zoneidnumber` = '75'; +UPDATE `zone` SET `underworld_teleport_index` = '-1' WHERE `zoneidnumber` = '150'; + 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..58bd9f177 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; @@ -276,6 +277,14 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { break; } + case ServerOP_ChangeGroupLeader: { + if (pack->size != sizeof(ServerGroupLeader_Struct)) { + break; + } + zoneserver_list.SendPacket(pack); //bounce it to all zones + break; + } + case ServerOP_RaidAdd: { if (pack->size != sizeof(ServerRaidGeneralAction_Struct)) break; @@ -1355,6 +1364,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/aggro.cpp b/zone/aggro.cpp index d23732df4..33221b63a 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -468,6 +468,12 @@ bool Mob::IsAttackAllowed(Mob *target, bool isSpellAttack) return false; } + if (target->GetSpecialAbility(IMMUNE_DAMAGE_CLIENT) && IsClient()) + return false; + + if (target->GetSpecialAbility(IMMUNE_DAMAGE_NPC) && IsNPC()) + return false; + // can't damage own pet (applies to everthing) Mob *target_owner = target->GetOwner(); Mob *our_owner = GetOwner(); diff --git a/zone/attack.cpp b/zone/attack.cpp index 1d780a27e..3f614598a 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -2247,7 +2247,7 @@ bool NPC::Death(Mob* killer_mob, int32 damage, uint16 spell, EQ::skills::SkillTy d->spawn_id = GetID(); d->killer_id = killer_mob ? killer_mob->GetID() : 0; d->bindzoneid = 0; - d->spell_id = spell == SPELL_UNKNOWN ? 0xffffffff : spell; + d->spell_id = 0xffffffff; // Sending spell was causing extra DoT land msg d->attack_skill = SkillDamageTypes[attack_skill]; d->damage = damage; app->priority = 6; @@ -2667,6 +2667,12 @@ void Mob::AddToHateList(Mob* other, uint32 hate /*= 0*/, int32 damage /*= 0*/, b if (IsFamiliar() || GetSpecialAbility(IMMUNE_AGGRO)) return; + if (GetSpecialAbility(IMMUNE_AGGRO_NPC) && other->IsNPC()) + return; + + if (GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && other->IsClient()) + return; + if (spell_id != SPELL_UNKNOWN && NoDetrimentalSpellAggro(spell_id)) return; @@ -2758,8 +2764,11 @@ void Mob::AddToHateList(Mob* other, uint32 hate /*= 0*/, int32 damage /*= 0*/, b else { // cb:2007-08-17 // owner must get on list, but he's not actually gained any hate yet - if (!owner->GetSpecialAbility(IMMUNE_AGGRO)) - { + if ( + !owner->GetSpecialAbility(IMMUNE_AGGRO) && + !(this->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && owner->IsClient()) && + !(this->GetSpecialAbility(IMMUNE_AGGRO_NPC) && owner->IsNPC()) + ) { if (owner->IsClient() && !CheckAggro(owner)) owner->CastToClient()->AddAutoXTarget(this); hate_list.AddEntToHateList(owner, 0, 0, false, !iBuffTic); @@ -2768,12 +2777,24 @@ void Mob::AddToHateList(Mob* other, uint32 hate /*= 0*/, int32 damage /*= 0*/, b } if (mypet && !mypet->IsHeld() && !mypet->IsPetStop()) { // I have a pet, add other to it - if (!mypet->IsFamiliar() && !mypet->GetSpecialAbility(IMMUNE_AGGRO)) + if ( + !mypet->IsFamiliar() && + !mypet->GetSpecialAbility(IMMUNE_AGGRO) && + !(mypet->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && this->IsClient()) && + !(mypet->GetSpecialAbility(IMMUNE_AGGRO_NPC) && this->IsNPC()) + ) { mypet->hate_list.AddEntToHateList(other, 0, 0, bFrenzy); + } } else if (myowner) { // I am a pet, add other to owner if it's NPC/LD - if (myowner->IsAIControlled() && !myowner->GetSpecialAbility(IMMUNE_AGGRO)) + if ( + myowner->IsAIControlled() && + !myowner->GetSpecialAbility(IMMUNE_AGGRO) && + !(this->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && myowner->IsClient()) && + !(this->GetSpecialAbility(IMMUNE_AGGRO_NPC) && myowner->IsNPC()) + ) { myowner->hate_list.AddEntToHateList(other, 0, 0, bFrenzy); + } } if (other->GetTempPetCount()) @@ -3467,8 +3488,19 @@ void Mob::CommonDamage(Mob* attacker, int &damage, const uint16 spell_id, const // pets that have GHold will never automatically add NPCs // pets that have Hold and no Focus will add NPCs if they're engaged // pets that have Hold and Focus will not add NPCs - if (pet && !pet->IsFamiliar() && !pet->GetSpecialAbility(IMMUNE_AGGRO) && !pet->IsEngaged() && attacker && attacker != this && !attacker->IsCorpse() && !pet->IsGHeld() && !attacker->IsTrap()) - { + if ( + pet && + !pet->IsFamiliar() && + !pet->GetSpecialAbility(IMMUNE_AGGRO) && + !pet->IsEngaged() && + attacker && + !(pet->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && attacker->IsClient()) && + !(pet->GetSpecialAbility(IMMUNE_AGGRO_NPC) && attacker->IsNPC()) && + attacker != this && + !attacker->IsCorpse() && + !pet->IsGHeld() && + !attacker->IsTrap() + ) { if (!pet->IsHeld()) { LogAggro("Sending pet [{}] into battle due to attack", pet->GetName()); if (IsClient()) { diff --git a/zone/bot.cpp b/zone/bot.cpp index 48e468a05..a45e575d9 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -2211,12 +2211,12 @@ bool Bot::Process() return false; } - if (mob_scan_close.Check()) { + if (mob_close_scan_timer.Check()) { LogAIScanClose( "is_moving [{}] bot [{}] timer [{}]", moving ? "true" : "false", GetCleanName(), - mob_scan_close.GetDuration() + mob_close_scan_timer.GetDuration() ); entity_list.ScanCloseClientMobs(close_mobs, this); diff --git a/zone/client.cpp b/zone/client.cpp index fdc75cfe6..1f1ab10b9 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" @@ -139,7 +143,7 @@ Client::Client(EQStreamInterface* ieqs) endupkeep_timer(1000), forget_timer(0), autosave_timer(RuleI(Character, AutosaveIntervalS) * 1000), - client_scan_npc_aggro_timer(RuleI(Aggro, ClientAggroCheckInterval) * 1000), + client_scan_npc_aggro_timer(RuleI(Aggro, ClientAggroCheckIdleInterval)), client_zone_wide_full_position_update_timer(5 * 60 * 1000), tribute_timer(Tribute_duration), proximity_timer(ClientProximity_interval), @@ -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)); @@ -350,8 +355,8 @@ Client::Client(EQStreamInterface* ieqs) /** * GM */ - display_mob_info_window = true; - dev_tools_window_enabled = true; + SetDisplayMobInfoWindow(true); + SetDevToolsEnabled(true); #ifdef BOTS bot_owner_options[booDeathMarquee] = false; @@ -2428,13 +2433,17 @@ bool Client::CheckIncreaseSkill(EQ::skills::SkillType skillid, Mob *against_who, char buffer[24] = { 0 }; snprintf(buffer, 23, "%d %d", skillid, skillval); parse->EventPlayer(EVENT_USE_SKILL, this, buffer, 0); - if(against_who) - { - if(against_who->GetSpecialAbility(IMMUNE_AGGRO) || against_who->IsClient() || - GetLevelCon(against_who->GetLevel()) == CON_GRAY) - { + if (against_who) { + if ( + against_who->GetSpecialAbility(IMMUNE_AGGRO) || + against_who->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) || + against_who->IsClient() || + GetLevelCon(against_who->GetLevel()) == CON_GRAY + ) { //false by default - if( !mod_can_increase_skill(skillid, against_who) ) { return(false); } + if (!mod_can_increase_skill(skillid, against_who)) { + return false; + } } } @@ -3201,6 +3210,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 +3427,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); @@ -4725,6 +4762,12 @@ void Client::IncrementAggroCount(bool raid_target) uint32 newtimer = raid_target ? RuleI(Character, RestRegenRaidTimeToActivate) : RuleI(Character, RestRegenTimeToActivate); + // When our aggro count is 1 here, we are exiting rest state. We need to pause our current timer, if we have time remaining + // We should not actually have to do anything to the Timer object since the AggroCount counter blocks it from being checked + // and will have it's timer changed when we exit combat so let's not do any extra work + if (AggroCount == 1 && rest_timer.GetRemainingTime()) // the Client::rest_timer is never disabled, so don't need to check + m_pp.RestTimer = std::max(1u, rest_timer.GetRemainingTime() / 1000); // I guess round up? + // save the new timer if it's higher m_pp.RestTimer = std::max(m_pp.RestTimer, newtimer); @@ -4734,10 +4777,6 @@ void Client::IncrementAggroCount(bool raid_target) if(AggroCount > 1) return; - // Pause the rest timer, it's possible the new timer is a non-raid timer we're currently ticking down on a raid timer - if (AggroCount == 1) - m_pp.RestTimer = std::max(m_pp.RestTimer, rest_timer.GetRemainingTime() / 1000); - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { auto outapp = new EQApplicationPacket(OP_RestState, 1); char *Buffer = (char *)outapp->pBuffer; @@ -5743,6 +5782,18 @@ void Client::AddCrystals(uint32 Radiant, uint32 Ebon) SendCrystalCounts(); } +void Client::SetEbonCrystals(uint32 value) { + m_pp.currentEbonCrystals = value; + SaveCurrency(); + SendCrystalCounts(); +} + +void Client::SetRadiantCrystals(uint32 value) { + m_pp.currentRadCrystals = value; + SaveCurrency(); + SendCrystalCounts(); +} + // Processes a client request to inspect a SoF+ client's equipment. void Client::ProcessInspectRequest(Client* requestee, Client* requester) { if(requestee && requester) { @@ -6110,21 +6161,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() @@ -8617,7 +8666,7 @@ void Client::QuestReward(Mob* target, uint32 copper, uint32 silver, uint32 gold, AddMoneyToPP(copper, silver, gold, platinum, false); if (itemid > 0) - SummonItem(itemid, 0, 0, 0, 0, 0, 0, false, EQ::invslot::slotCursor); + SummonItem(itemid, -1, 0, 0, 0, 0, 0, false, EQ::invslot::slotCursor); if (faction) { @@ -9169,17 +9218,14 @@ void Client::SetDisplayMobInfoWindow(bool display_mob_info_window) Client::display_mob_info_window = display_mob_info_window; } -bool Client::IsDevToolsWindowEnabled() const +bool Client::IsDevToolsEnabled() const { - return dev_tools_window_enabled; + return dev_tools_enabled && RuleB(World, EnableDevTools); } -/** - * @param in_dev_tools_window_enabled - */ -void Client::SetDevToolsWindowEnabled(bool in_dev_tools_window_enabled) +void Client::SetDevToolsEnabled(bool in_dev_tools_enabled) { - Client::dev_tools_window_enabled = in_dev_tools_window_enabled; + Client::dev_tools_enabled = in_dev_tools_enabled; } /** @@ -9388,7 +9434,7 @@ void Client::ShowDevToolsMenu() std::string menu_commands_search; std::string menu_commands_show; std::string reload_commands_show; - std::string window_toggle_command; + std::string devtools_toggle; /** * Search entity commands @@ -9423,9 +9469,9 @@ void Client::ShowDevToolsMenu() /** * Show window status */ - window_toggle_command = "Disabled [" + EQ::SayLinkEngine::GenerateQuestSaylink("#devtools enable_window", false, "Enable") + "] "; - if (IsDevToolsWindowEnabled()) { - window_toggle_command = "Enabled [" + EQ::SayLinkEngine::GenerateQuestSaylink("#devtools disable_window", false, "Disable") + "] "; + devtools_toggle = "Disabled [" + EQ::SayLinkEngine::GenerateQuestSaylink("#devtools enable", false, "Enable") + "] "; + if (IsDevToolsEnabled()) { + devtools_toggle = "Enabled [" + EQ::SayLinkEngine::GenerateQuestSaylink("#devtools disable", false, "Disable") + "] "; } /** @@ -9433,8 +9479,8 @@ void Client::ShowDevToolsMenu() */ SendChatLineBreak(); Message( - Chat::White, "| [Devtools] Window %s Show this menu with %s | Current expansion [%s]", - window_toggle_command.c_str(), + Chat::White, "| [Devtools] %s Show this menu with %s | Current expansion [%s]", + devtools_toggle.c_str(), EQ::SayLinkEngine::GenerateQuestSaylink("#dev", false, "#dev").c_str(), content_service.GetCurrentExpansionName().c_str() ); @@ -9448,3 +9494,643 @@ 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); +} + +void Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clipping) { + BuffFadeByEffect(SE_Levitate); + if (CheckLosFN(target_x, target_y, target_z, 6.0f) || ignore_los) { + auto outapp_fling = new EQApplicationPacket(OP_Fling, sizeof(fling_struct)); + fling_struct* flingTo = (fling_struct*)outapp_fling->pBuffer; + if(clipping) + flingTo->collision = 0; + else + flingTo->collision = -1; + + flingTo->travel_time = -1; + flingTo->unk3 = 1; + flingTo->disable_fall_damage = 1; + flingTo->speed_z = value; + flingTo->new_y = target_y; + flingTo->new_x = target_x; + flingTo->new_z = target_z; + outapp_fling->priority = 6; + FastQueuePacket(&outapp_fling); + } +} + +std::vector Client::GetLearnableDisciplines(uint8 min_level, uint8 max_level) { + bool SpellGlobalRule = RuleB(Spells, EnableSpellGlobals); + bool SpellBucketRule = RuleB(Spells, EnableSpellBuckets); + bool SpellGlobalCheckResult = false; + bool SpellBucketCheckResult = false; + std::vector learnable_disciplines; + for (int spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool learnable = false; + if (!IsValidSpell(spell_id)) + continue; + if (!IsDiscipline(spell_id)) + continue; + if (spells[spell_id].classes[WARRIOR] == 0) + continue; + if (max_level > 0 && spells[spell_id].classes[m_pp.class_ - 1] > max_level) + continue; + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) + continue; + if (spells[spell_id].skill == 52) + continue; + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effectid[EFFECT_COUNT - 1] == 10) + continue; + if (HasDisciplineLearned(spell_id)) + continue; + + if (SpellGlobalRule) { + SpellGlobalCheckResult = SpellGlobalCheck(spell_id, CharacterID()); + if (SpellGlobalCheckResult) { + learnable = true; + } + } else if (SpellBucketRule) { + SpellBucketCheckResult = SpellBucketCheck(spell_id, CharacterID()); + if (SpellBucketCheckResult) { + learnable = true; + } + } else { + learnable = true; + } + + if (learnable) { + learnable_disciplines.push_back(spell_id); + } + } + return learnable_disciplines; +} + +std::vector Client::GetLearnedDisciplines() { + std::vector learned_disciplines; + for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + learned_disciplines.push_back(m_pp.disciplines.values[index]); + } + } + return learned_disciplines; +} + +std::vector Client::GetMemmedSpells() { + std::vector memmed_spells; + for (int index = 0; index < EQ::spells::SPELL_GEM_COUNT; index++) { + if (IsValidSpell(m_pp.mem_spells[index])) { + memmed_spells.push_back(m_pp.mem_spells[index]); + } + } + return memmed_spells; +} + +std::vector Client::GetScribeableSpells(uint8 min_level, uint8 max_level) { + bool SpellGlobalRule = RuleB(Spells, EnableSpellGlobals); + bool SpellBucketRule = RuleB(Spells, EnableSpellBuckets); + bool SpellGlobalCheckResult = false; + bool SpellBucketCheckResult = false; + std::vector scribeable_spells; + for (int spell_id = 0; spell_id < SPDAT_RECORDS; ++spell_id) { + bool scribeable = false; + if (!IsValidSpell(spell_id)) + continue; + if (spells[spell_id].classes[WARRIOR] == 0) + continue; + if (max_level > 0 && spells[spell_id].classes[m_pp.class_ - 1] > max_level) + continue; + if (min_level > 1 && spells[spell_id].classes[m_pp.class_ - 1] < min_level) + continue; + if (spells[spell_id].skill == 52) + continue; + if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effectid[EFFECT_COUNT - 1] == 10) + continue; + if (HasSpellScribed(spell_id)) + continue; + + if (SpellGlobalRule) { + SpellGlobalCheckResult = SpellGlobalCheck(spell_id, CharacterID()); + if (SpellGlobalCheckResult) { + scribeable = true; + } + } else if (SpellBucketRule) { + SpellBucketCheckResult = SpellBucketCheck(spell_id, CharacterID()); + if (SpellBucketCheckResult) { + scribeable = true; + } + } else { + scribeable = true; + } + + if (scribeable) { + scribeable_spells.push_back(spell_id); + } + } + return scribeable_spells; +} + +std::vector Client::GetScribedSpells() { + std::vector scribed_spells; + for(int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + scribed_spells.push_back(m_pp.spell_book[index]); + } + } + return scribed_spells; +} diff --git a/zone/client.h b/zone/client.h index d2778d55a..cc461bc22 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 @@ -232,8 +238,8 @@ public: void SetDisplayMobInfoWindow(bool display_mob_info_window); bool GetDisplayMobInfoWindow() const; - bool IsDevToolsWindowEnabled() const; - void SetDevToolsWindowEnabled(bool dev_tools_window_enabled); + bool IsDevToolsEnabled() const; + void SetDevToolsEnabled(bool in_dev_tools_enabled); void SetPrimaryWeaponOrnamentation(uint32 model_id); void SetSecondaryWeaponOrnamentation(uint32 model_id); @@ -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, @@ -594,9 +601,9 @@ public: uint32 GetPVPPoints() { return m_pp.PVPCurrentPoints; } void AddPVPPoints(uint32 Points); uint32 GetRadiantCrystals() { return m_pp.currentRadCrystals; } - void SetRadiantCrystals(uint32 Crystals) { m_pp.currentRadCrystals = Crystals; } + void SetRadiantCrystals(uint32 value); uint32 GetEbonCrystals() { return m_pp.currentEbonCrystals; } - void SetEbonCrystals(uint32 Crystals) { m_pp.currentEbonCrystals = Crystals; } + void SetEbonCrystals(uint32 value); void AddCrystals(uint32 Radiant, uint32 Ebon); void SendCrystalCounts(); @@ -777,6 +784,11 @@ public: void UnmemSpellAll(bool update_client = true); uint16 FindMemmedSpellBySlot(int slot); int MemmedCount(); + std::vector GetLearnableDisciplines(uint8 min_level = 1, uint8 max_level = 0); + std::vector GetLearnedDisciplines(); + std::vector GetMemmedSpells(); + std::vector GetScribeableSpells(uint8 min_level = 1, uint8 max_level = 0); + std::vector GetScribedSpells(); void ScribeSpell(uint16 spell_id, int slot, bool update_client = true); void UnscribeSpell(int slot, bool update_client = true); void UnscribeSpellAll(bool update_client = true); @@ -787,6 +799,8 @@ public: uint32 GetCharMaxLevelFromQGlobal(); uint32 GetCharMaxLevelFromBucket(); + void Fling(float value, float target_x, float target_y, float target_z, bool ignore_los = false, bool clipping = false); + inline bool IsStanding() const {return (playeraction == 0);} inline bool IsSitting() const {return (playeraction == 1);} inline bool IsCrouching() const {return (playeraction == 2);} @@ -966,6 +980,7 @@ public: void SendDisciplineUpdate(); void SendDisciplineTimer(uint32 timer_id, uint32 duration); bool UseDiscipline(uint32 spell_id, uint32 target); + bool HasDisciplineLearned(uint16 spell_id); void SetLinkedSpellReuseTimer(uint32 timer_id, uint32 duration); bool IsLinkedSpellReuseTimerReady(uint32 timer_id); @@ -988,6 +1003,7 @@ public: void ProcessInspectRequest(Client* requestee, Client* requester); bool ClientFinishedLoading() { return (conn_state == ClientConnectFinished); } int FindSpellBookSlotBySpellID(uint16 spellid); + uint32 GetSpellIDByBookSlot(int book_slot); int GetNextAvailableSpellBookSlot(int starting_slot = 0); inline uint32 GetSpellByBookSlot(int book_slot) { return m_pp.spell_book[book_slot]; } inline bool HasSpellScribed(int spellid) { return (FindSpellBookSlotBySpellID(spellid) != -1 ? true : false); } @@ -1103,6 +1119,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(); @@ -1476,7 +1533,7 @@ private: uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004 bool display_mob_info_window; - bool dev_tools_window_enabled; + bool dev_tools_enabled; int32 max_end; int32 current_endurance; @@ -1556,6 +1613,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; @@ -1657,6 +1715,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 316c08cd9..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()); @@ -920,7 +934,7 @@ void Client::CompleteConnect() entity_list.ScanCloseMobs(close_mobs, this, true); - if (GetGM()) { + if (GetGM() && IsDevToolsEnabled()) { ShowDevToolsMenu(); } @@ -1701,13 +1715,15 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) /* Task Packets */ LoadClientTaskState(); + m_expedition_id = ExpeditionDatabase::GetExpeditionIDFromCharacterID(CharacterID()); + /** * DevTools Load Settings */ if (Admin() >= EQ::DevTools::GM_ACCOUNT_STATUS_LEVEL) { - std::string dev_tools_window_key = StringFormat("%i-dev-tools-window-disabled", AccountID()); + std::string dev_tools_window_key = StringFormat("%i-dev-tools-disabled", AccountID()); if (DataBucket::GetData(dev_tools_window_key) == "true") { - dev_tools_window_enabled = false; + dev_tools_enabled = false; } } @@ -2025,12 +2041,10 @@ void Client::Handle_OP_AdventureMerchantPurchase(const EQApplicationPacket *app) else if (aps->Type == NorrathsKeepersMerchant) { SetRadiantCrystals(GetRadiantCrystals() - (int32)item->LDoNPrice); - SendCrystalCounts(); } else if (aps->Type == DarkReignMerchant) { SetEbonCrystals(GetEbonCrystals() - (int32)item->LDoNPrice); - SendCrystalCounts(); } int16 charges = 1; if (item->MaxCharges != 0) @@ -4533,23 +4547,63 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { rewind_timer.Start(30000, true); } - /* Handle client aggro scanning timers NPCs */ - is_client_moving = (cy == m_Position.y && cx == m_Position.x) ? false : true; + + is_client_moving = !(cy == m_Position.y && cx == m_Position.x); + + + /** + * Client aggro scanning + */ + const uint16 client_scan_npc_aggro_timer_idle = RuleI(Aggro, ClientAggroCheckIdleInterval); + const uint16 client_scan_npc_aggro_timer_moving = RuleI(Aggro, ClientAggroCheckMovingInterval); + + LogAggroDetail( + "ClientUpdate [{}] {}moving, scan timer [{}]", + GetCleanName(), + is_client_moving ? "" : "NOT ", + client_scan_npc_aggro_timer.GetRemainingTime() + ); if (is_client_moving) { - LogDebug("ClientUpdate: Client is moving - scan timer is: [{}]", client_scan_npc_aggro_timer.GetDuration()); - if (client_scan_npc_aggro_timer.GetDuration() > 1000) { + if (client_scan_npc_aggro_timer.GetRemainingTime() > client_scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName()); client_scan_npc_aggro_timer.Disable(); - client_scan_npc_aggro_timer.Start(500); + client_scan_npc_aggro_timer.Start(client_scan_npc_aggro_timer_moving); + client_scan_npc_aggro_timer.Trigger(); } } - else { - LogDebug("ClientUpdate: Client is NOT moving - scan timer is: [{}]", client_scan_npc_aggro_timer.GetDuration()); - if (client_scan_npc_aggro_timer.GetDuration() < 1000) { - client_scan_npc_aggro_timer.Disable(); - client_scan_npc_aggro_timer.Start(3000); + else if (client_scan_npc_aggro_timer.GetDuration() == client_scan_npc_aggro_timer_moving) { + LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName()); + client_scan_npc_aggro_timer.Disable(); + client_scan_npc_aggro_timer.Start(client_scan_npc_aggro_timer_idle); + } + + /** + * Client mob close list cache scan timer + */ + const uint16 client_mob_close_scan_timer_moving = 6000; + const uint16 client_mob_close_scan_timer_idle = 60000; + + LogAIScanCloseDetail( + "Client [{}] {}moving, scan timer [{}]", + GetCleanName(), + is_client_moving ? "" : "NOT ", + mob_close_scan_timer.GetRemainingTime() + ); + + if (is_client_moving) { + if (mob_close_scan_timer.GetRemainingTime() > client_mob_close_scan_timer_moving) { + LogAIScanCloseDetail("Client [{}] Restarting with moving timer", GetCleanName()); + mob_close_scan_timer.Disable(); + mob_close_scan_timer.Start(client_mob_close_scan_timer_moving); + mob_close_scan_timer.Trigger(); } } + else if (mob_close_scan_timer.GetDuration() == client_mob_close_scan_timer_moving) { + LogAIScanCloseDetail("Client [{}] Restarting with idle timer", GetCleanName()); + mob_close_scan_timer.Disable(); + mob_close_scan_timer.Start(client_mob_close_scan_timer_idle); + } /** * On a normal basis we limit mob movement updates based on distance @@ -5566,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)) { @@ -8836,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)) @@ -10872,8 +11053,8 @@ void Client::Handle_OP_PopupResponse(const EQApplicationPacket *app) break; case EQ::popupresponse::MOB_INFO_DISMISS: - this->SetDisplayMobInfoWindow(false); - this->Message(Chat::Yellow, "[DevTools] Window snoozed in this zone..."); + SetDisplayMobInfoWindow(false); + Message(Chat::Yellow, "[DevTools] Window snoozed in this zone..."); break; default: break; 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 18ee9979f..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 } @@ -257,7 +271,7 @@ bool Client::Process() { * Used in aggro checks */ if (mob_close_scan_timer.Check()) { - entity_list.ScanCloseMobs(close_mobs, this, true); + entity_list.ScanCloseMobs(close_mobs, this, is_client_moving); } bool may_use_attacks = false; @@ -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 9b42adacd..72ee3fab8 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,11 +199,14 @@ 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) || command_add("emoteview", "Lists all NPC Emotes", 80, command_emoteview) || command_add("enablerecipe", "[recipe_id] - Enables a recipe using the recipe id.", 80, command_enablerecipe) || + command_add("endurance", "Restores you or your target's endurance.", 50, command_endurance) || command_add("equipitem", "[slotid(0-21)] - Equip the item on your cursor into the specified slot", 50, command_equipitem) || command_add("face", "- Change the face of your target", 80, command_face) || command_add("faction", "[Find (criteria | all ) | Review (criteria | all) | Reset (id)] - Resets Player's Faction", 80, command_faction) || @@ -435,6 +439,7 @@ int command_init(void) command_add("wp", "[add/delete] [grid_num] [pause] [wp_num] [-h] - Add/delete a waypoint to/from a wandering grid", 170, command_wp) || command_add("wpadd", "[pause] [-h] - Add your current location as a waypoint to your NPC target's AI path", 170, command_wpadd) || command_add("wpinfo", "- Show waypoint info about your NPC target", 170, command_wpinfo) || + command_add("worldwide", "Performs world-wide GM functions such as cast (can be extended for other commands). Use caution", 250, command_worldwide) || command_add("xtargets", "Show your targets Extended Targets and optionally set how many xtargets they can have.", 250, command_xtargets) || command_add("zclip", "[min] [max] - modifies and resends zhdr packet", 80, command_zclip) || command_add("zcolor", "[red] [green] [blue] - Change sky color", 80, command_zcolor) || @@ -734,6 +739,42 @@ void command_logcommand(Client *c, const char *message) /* * commands go below here */ +void command_worldwide(Client *c, const Seperator *sep) +{ + std::string sub_command; + if (sep->arg[1]) { + sub_command = sep->arg[1]; + } + + if (sub_command == "cast") { + if (sep->arg[2][0] && Seperator::IsNumber(sep->arg[2])) { + int spell_id = atoi(sep->arg[2]); + quest_manager.WorldWideCastSpell(spell_id, 0, 0); + worldserver.SendEmoteMessage(0, 0, 15, fmt::format(" A GM has cast [{}] world-wide!", GetSpellName(spell_id)).c_str()); + } + else { + c->Message(Chat::Yellow, "Usage: #worldwide cast [spellid]"); + } + } + + if (!sep->arg[1]) { + c->Message(Chat::White, "This command is used to perform world-wide tasks"); + c->Message(Chat::White, "Usage: #worldwide cast [spellid]"); + } +} +void command_endurance(Client *c, const Seperator *sep) +{ + Mob *t; + + t = c->GetTarget() ? c->GetTarget() : c; + + if (t->IsClient()) + t->CastToClient()->SetEndurance(t->CastToClient()->GetMaxEndurance()); + else + t->SetEndurance(t->GetMaxEndurance()); + + t->Message(Chat::White, "Your endurance has been refilled."); +} void command_setstat(Client* c, const Seperator* sep){ if(sep->arg[1][0] && sep->arg[2][0] && c->GetTarget()!=0 && c->GetTarget()->IsClient()){ c->GetTarget()->CastToClient()->SetStats(atoi(sep->arg[1]),atoi(sep->arg[2])); @@ -4837,6 +4878,10 @@ void command_corpse(Client *c, const Seperator *sep) c->Message(Chat::White, "Insufficient status to depop player corpse."); } + else if (strcasecmp(sep->arg[1], "moveallgraveyard") == 0) { + int count = entity_list.MovePlayerCorpsesToGraveyard(true); + c->Message(Chat::White, "Moved [%d] player corpse(s) to zone graveyard", count); + } else if (sep->arg[1][0] == 0 || strcasecmp(sep->arg[1], "help") == 0) { c->Message(Chat::White, "#Corpse Sub-Commands:"); c->Message(Chat::White, " DeleteNPCCorpses"); @@ -4844,6 +4889,7 @@ void command_corpse(Client *c, const Seperator *sep) c->Message(Chat::White, " ListNPC"); c->Message(Chat::White, " ListPlayer"); c->Message(Chat::White, " Lock - GM locks the corpse - cannot be looted by non-GM"); + c->Message(Chat::White, " MoveAllGraveyard - move all player corpses to zone's graveyard or non-instance"); c->Message(Chat::White, " UnLock"); c->Message(Chat::White, " RemoveCash"); c->Message(Chat::White, " InspectLoot"); @@ -5592,18 +5638,18 @@ void command_depopzone(Client *c, const Seperator *sep) void command_devtools(Client *c, const Seperator *sep) { - std::string dev_tools_window_key = StringFormat("%i-dev-tools-window-disabled", c->AccountID()); + std::string dev_tools_key = StringFormat("%i-dev-tools-disabled", c->AccountID()); /** * Handle window toggle */ - if (strcasecmp(sep->arg[1], "disable_window") == 0) { - DataBucket::SetData(dev_tools_window_key, "true"); - c->SetDevToolsWindowEnabled(false); + if (strcasecmp(sep->arg[1], "disable") == 0) { + DataBucket::SetData(dev_tools_key, "true"); + c->SetDevToolsEnabled(false); } - if (strcasecmp(sep->arg[1], "enable_window") == 0) { - DataBucket::DeleteData(dev_tools_window_key); - c->SetDevToolsWindowEnabled(true); + if (strcasecmp(sep->arg[1], "enable") == 0) { + DataBucket::DeleteData(dev_tools_key); + c->SetDevToolsEnabled(true); } c->ShowDevToolsMenu(); @@ -6825,6 +6871,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) { @@ -6879,7 +7129,7 @@ void command_editmassrespawn(Client* c, const Seperator* sep) int results_count = 0; - auto results = database.QueryDatabase(query); + auto results = content_db.QueryDatabase(query); if (results.Success() && results.RowCount()) { results_count = results.RowCount(); @@ -6906,7 +7156,7 @@ void command_editmassrespawn(Client* c, const Seperator* sep) if (change_apply) { - results = database.QueryDatabase( + results = content_db.QueryDatabase( fmt::format( SQL( UPDATE spawn2 @@ -7741,13 +7991,6 @@ void command_itemsearch(Client *c, const Seperator *sep) return; } - std::vector amounts = { - "1", - "10", - "100", - "1000" - }; - int count = 0; char sName[64]; char sCriteria[255]; @@ -7761,14 +8004,25 @@ void command_itemsearch(Client *c, const Seperator *sep) pdest = strstr(sName, sCriteria); if (pdest != nullptr) { linker.SetItemData(item); - - std::string saylink_commands; - for (auto &amount : amounts) { - saylink_commands += EQ::SayLinkEngine::GenerateQuestSaylink( - "#si " + std::to_string(item->ID) + " " + amount, + std::string item_id = std::to_string(item->ID); + std::string saylink_commands = + "[" + + EQ::SayLinkEngine::GenerateQuestSaylink( + "#si " + item_id, false, - "[" + amount + "] " - ); + "X" + ) + + "] "; + if (item->Stackable && item->StackSize > 1) { + std::string stack_size = std::to_string(item->StackSize); + saylink_commands += + "[" + + EQ::SayLinkEngine::GenerateQuestSaylink( + "#si " + item_id + " " + stack_size, + false, + stack_size + ) + + "]"; } c->Message( @@ -7776,8 +8030,8 @@ void command_itemsearch(Client *c, const Seperator *sep) fmt::format( " Summon {} [{}] [{}]", saylink_commands, - item->ID, - linker.GenerateLink() + linker.GenerateLink(), + item->ID ).c_str() ); @@ -7855,14 +8109,10 @@ void command_setcrystals(Client *c, const Seperator *sep) else if(!strcasecmp(sep->arg[1], "radiant")) { t->SetRadiantCrystals(atoi(sep->arg[2])); - t->SendCrystalCounts(); - t->SaveCurrency(); } else if(!strcasecmp(sep->arg[1], "ebon")) { t->SetEbonCrystals(atoi(sep->arg[2])); - t->SendCrystalCounts(); - t->SaveCurrency(); } else { diff --git a/zone/command.h b/zone/command.h index 87e01ae49..303e0f132 100644 --- a/zone/command.h +++ b/zone/command.h @@ -92,11 +92,14 @@ 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); void command_emoteview(Client* c, const Seperator *sep); void command_enablerecipe(Client *c, const Seperator *sep); +void command_endurance(Client *c, const Seperator *sep); void command_equipitem(Client *c, const Seperator *sep); void command_face(Client *c, const Seperator *sep); void command_faction(Client *c, const Seperator *sep); @@ -342,6 +345,7 @@ void command_worldshutdown(Client *c, const Seperator *sep); void command_wp(Client *c, const Seperator *sep); void command_wpadd(Client *c, const Seperator *sep); void command_wpinfo(Client *c, const Seperator *sep); +void command_worldwide(Client *c, const Seperator *sep); void command_xtargets(Client *c, const Seperator *sep); void command_zclip(Client *c, const Seperator *sep); void command_zcolor(Client *c, const Seperator *sep); diff --git a/zone/common.h b/zone/common.h index 19df1a576..fd513bdde 100644 --- a/zone/common.h +++ b/zone/common.h @@ -195,7 +195,11 @@ enum { COUNTER_AVOID_DAMAGE = 44, PROX_AGGRO = 45, IMMUNE_RANGED_ATTACKS = 46, - MAX_SPECIAL_ATTACK = 47 + IMMUNE_DAMAGE_CLIENT = 47, + IMMUNE_DAMAGE_NPC = 48, + IMMUNE_AGGRO_CLIENT = 49, + IMMUNE_AGGRO_NPC = 50, + MAX_SPECIAL_ATTACK = 51 }; typedef enum { //fear states @@ -786,5 +790,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..ad75521a7 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" @@ -825,22 +826,7 @@ bool Corpse::Process() { } if (corpse_graveyard_timer.Check()) { - if (zone->HasGraveyard()) { - Save(); - player_corpse_depop = true; - database.SendCharacterCorpseToGraveyard(corpse_db_id, zone->graveyard_zoneid(), - (zone->GetZoneID() == zone->graveyard_zoneid()) ? zone->GetInstanceID() : 0, zone->GetGraveyardPoint()); - corpse_graveyard_timer.Disable(); - auto pack = new ServerPacket(ServerOP_SpawnPlayerCorpse, sizeof(SpawnPlayerCorpse_Struct)); - SpawnPlayerCorpse_Struct* spc = (SpawnPlayerCorpse_Struct*)pack->pBuffer; - spc->player_corpse_id = corpse_db_id; - spc->zone_id = zone->graveyard_zoneid(); - worldserver.SendPacket(pack); - safe_delete(pack); - LogDebug("Moved [{}] player corpse to the designated graveyard in zone [{}]", this->GetName(), ZoneName(zone->graveyard_zoneid())); - corpse_db_id = 0; - } - + MovePlayerCorpseToGraveyard(); corpse_graveyard_timer.Disable(); return false; } @@ -1275,6 +1261,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); @@ -1628,3 +1628,53 @@ void Corpse::LoadPlayerCorpseDecayTime(uint32 corpse_db_id){ corpse_graveyard_timer.SetTimer(3000); } } + +void Corpse::SendWorldSpawnPlayerCorpseInZone(uint32_t zone_id) +{ + auto pack = std::unique_ptr(new ServerPacket(ServerOP_SpawnPlayerCorpse, sizeof(SpawnPlayerCorpse_Struct))); + SpawnPlayerCorpse_Struct* spc = reinterpret_cast(pack->pBuffer); + spc->player_corpse_id = corpse_db_id; + spc->zone_id = zone_id; + worldserver.SendPacket(pack.get()); +} + +bool Corpse::MovePlayerCorpseToGraveyard() +{ + if (IsPlayerCorpse() && zone && zone->HasGraveyard()) + { + Save(); + + uint16_t instance_id = (zone->GetZoneID() == zone->graveyard_zoneid()) ? zone->GetInstanceID() : 0; + database.SendCharacterCorpseToGraveyard(corpse_db_id, zone->graveyard_zoneid(), instance_id, zone->GetGraveyardPoint()); + SendWorldSpawnPlayerCorpseInZone(zone->graveyard_zoneid()); + + corpse_db_id = 0; + player_corpse_depop = true; + corpse_graveyard_timer.Disable(); + + LogDebug("Moved [{}] player corpse to the designated graveyard in zone [{}]", GetName(), ZoneName(zone->graveyard_zoneid())); + return true; + } + + return false; +} + +bool Corpse::MovePlayerCorpseToNonInstance() +{ + if (IsPlayerCorpse() && zone && zone->GetInstanceID() != 0) + { + Save(); + + database.SendCharacterCorpseToNonInstance(corpse_db_id); + SendWorldSpawnPlayerCorpseInZone(zone->GetZoneID()); + + corpse_db_id = 0; + player_corpse_depop = true; + corpse_graveyard_timer.Disable(); + + LogDebug("Moved [{}] player corpse to non-instance version of zone [{}]", GetName(), ZoneName(zone->GetZoneID())); + return true; + } + + return false; +} diff --git a/zone/corpse.h b/zone/corpse.h index 300569f89..cd564e80d 100644 --- a/zone/corpse.h +++ b/zone/corpse.h @@ -79,6 +79,9 @@ class Corpse : public Mob { void SetConsentGuildID(uint32 guild_id) { if (IsPlayerCorpse()) { consented_guild_id = guild_id; } } void AddConsentName(std::string consent_player_name); void RemoveConsentName(std::string consent_player_name); + void SendWorldSpawnPlayerCorpseInZone(uint32_t zone_id); + bool MovePlayerCorpseToGraveyard(); + bool MovePlayerCorpseToNonInstance(); void Delete(); void Bury(); 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/effects.cpp b/zone/effects.cpp index c2a20cf71..b62a2b383 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -678,6 +678,17 @@ void Client::ResetDisciplineTimer(uint32 timer_id) { SendDisciplineTimer(timer_id, 0); } +bool Client::HasDisciplineLearned(uint16 spell_id) { + bool has_learned = false; + for (auto index = 0; index < MAX_PP_DISCIPLINES; ++index) { + if (GetPP().disciplines.values[index] == spell_id) { + has_learned = true; + break; + } + } + return has_learned; +} + void Client::SendDisciplineTimer(uint32 timer_id, uint32 duration) { if (timer_id < MAX_DISCIPLINE_TIMERS) 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 9977f9ca3..fc541a088 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" @@ -2792,6 +2793,29 @@ XS(XS__we) { XSRETURN_EMPTY; } +XS(XS__message); +XS(XS__message) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: quest::message(int color, string message)"); + + int color = (int) SvIV(ST(0)); + char *message = (char *) SvPV_nolen(ST(1)); + quest_manager.message(color, message); + XSRETURN_EMPTY; +} + +XS(XS__whisper); +XS(XS__whisper) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: quest::whisper(string message)"); + + char *message = (char *) SvPV_nolen(ST(0)); + quest_manager.whisper(message); + XSRETURN_EMPTY; +} + XS(XS__getlevel); XS(XS__getlevel) { dXSARGS; @@ -2926,6 +2950,22 @@ XS(XS__countitem) { XSRETURN_IV(quantity); } +XS(XS__removeitem); +XS(XS__removeitem) { + dXSARGS; + if (items < 1 || items > 2) + Perl_croak(aTHX_ "Usage: quest::removeitem(int item_id, [int quantity = 1])"); + + uint32 item_id = (int) SvIV(ST(0)); + uint32 quantity = 1; + if (items > 1) + quantity = (int) SvIV(ST(1)); + + quest_manager.removeitem(item_id, quantity); + + XSRETURN_EMPTY; +} + XS(XS__getitemname); XS(XS__getitemname) { dXSARGS; @@ -3733,10 +3773,11 @@ XS(XS__GetZoneLongName) { if (items != 1) Perl_croak(aTHX_ "Usage: quest::GetZoneLongName(string zone)"); dXSTARG; - char *zone = (char *) SvPV_nolen(ST(0)); - Const_char *RETVAL = quest_manager.GetZoneLongName(zone); - sv_setpv(TARG, RETVAL); + std::string zone = (std::string) SvPV_nolen(ST(0)); + std::string RETVAL = quest_manager.GetZoneLongName(zone); + + sv_setpv(TARG, RETVAL.c_str()); XSprePUSH; PUSHTARG; XSRETURN(1); @@ -3767,7 +3808,7 @@ XS(XS__crosszoneassigntaskbycharid) { if (items == 3) { enforce_level_requirement = (bool) SvTRUE(ST(2)); - } + } quest_manager.CrossZoneAssignTaskByCharID(character_id, task_id, enforce_level_requirement); } @@ -3801,13 +3842,13 @@ XS(XS__crosszoneassigntaskbyraidid) { int raid_id = (int) SvIV(ST(0)); uint32 task_id = (uint32) SvIV(ST(1)); bool enforce_level_requirement = false; - + if (items == 3) { enforce_level_requirement = (bool) SvTRUE(ST(2)); } quest_manager.CrossZoneAssignTaskByRaidID(raid_id, task_id, enforce_level_requirement); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneassigntaskbyguildid); @@ -3825,7 +3866,7 @@ XS(XS__crosszoneassigntaskbyguildid) { } quest_manager.CrossZoneAssignTaskByGuildID(guild_id, task_id, enforce_level_requirement); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonecastspellbycharid); @@ -3838,7 +3879,7 @@ XS(XS__crosszonecastspellbycharid) { uint32 spell_id = (uint32) SvIV(ST(1)); quest_manager.CrossZoneCastSpellByCharID(character_id, spell_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonecastspellbygroupid); @@ -3851,7 +3892,7 @@ XS(XS__crosszonecastspellbygroupid) { uint32 spell_id = (uint32) SvIV(ST(1)); quest_manager.CrossZoneCastSpellByGroupID(group_id, spell_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonecastspellbyraidid); @@ -3864,7 +3905,7 @@ XS(XS__crosszonecastspellbyraidid) { uint32 spell_id = (uint32) SvIV(ST(1)); quest_manager.CrossZoneCastSpellByRaidID(raid_id, spell_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonecastspellbyguildid); @@ -3877,7 +3918,7 @@ XS(XS__crosszonecastspellbyguildid) { uint32 spell_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneCastSpellByGuildID(guild_id, spell_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonedisabletaskbycharid); @@ -3890,7 +3931,7 @@ XS(XS__crosszonedisabletaskbycharid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneDisableTaskByCharID(char_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonedisabletaskbygroupid); @@ -3903,7 +3944,7 @@ XS(XS__crosszonedisabletaskbygroupid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneDisableTaskByGroupID(group_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonedisabletaskbyraidid); @@ -3916,7 +3957,7 @@ XS(XS__crosszonedisabletaskbyraidid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneDisableTaskByRaidID(raid_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonedisabletaskbyguildid); @@ -3929,7 +3970,7 @@ XS(XS__crosszonedisabletaskbyguildid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneDisableTaskByGuildID(guild_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneenabletaskbycharid); @@ -3942,7 +3983,7 @@ XS(XS__crosszoneenabletaskbycharid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneEnableTaskByCharID(char_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneenabletaskbygroupid); @@ -3955,7 +3996,7 @@ XS(XS__crosszoneenabletaskbygroupid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneEnableTaskByGroupID(group_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneenabletaskbyraidid); @@ -3968,7 +4009,7 @@ XS(XS__crosszoneenabletaskbyraidid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneEnableTaskByRaidID(raid_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneenabletaskbyguildid); @@ -3981,7 +4022,7 @@ XS(XS__crosszoneenabletaskbyguildid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneEnableTaskByGuildID(guild_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonefailtaskbycharid); @@ -3994,7 +4035,7 @@ XS(XS__crosszonefailtaskbycharid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneFailTaskByCharID(char_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonefailtaskbygroupid); @@ -4007,7 +4048,7 @@ XS(XS__crosszonefailtaskbygroupid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneFailTaskByGroupID(group_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonefailtaskbyraidid); @@ -4020,7 +4061,7 @@ XS(XS__crosszonefailtaskbyraidid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneFailTaskByRaidID(raid_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonefailtaskbyguildid); @@ -4033,7 +4074,7 @@ XS(XS__crosszonefailtaskbyguildid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneFailTaskByGuildID(guild_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonemarqueebycharid); @@ -4390,7 +4431,7 @@ XS(XS__crosszoneremovetaskbycharid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneRemoveTaskByCharID(char_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneremovetaskbygroupid); @@ -4403,7 +4444,7 @@ XS(XS__crosszoneremovetaskbygroupid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneRemoveTaskByGroupID(group_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneremovetaskbyraidid); @@ -4416,7 +4457,7 @@ XS(XS__crosszoneremovetaskbyraidid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneRemoveTaskByRaidID(raid_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneremovetaskbyguildid); @@ -4429,7 +4470,7 @@ XS(XS__crosszoneremovetaskbyguildid) { uint32 task_id = (uint32) SvUV(ST(1)); quest_manager.CrossZoneRemoveTaskByGuildID(guild_id, task_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneresetactivitybycharid); @@ -4443,7 +4484,7 @@ XS(XS__crosszoneresetactivitybycharid) { int activity_id = (int) SvIV(ST(2)); quest_manager.CrossZoneResetActivityByCharID(char_id, task_id, activity_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneresetactivitybygroupid); @@ -4457,7 +4498,7 @@ XS(XS__crosszoneresetactivitybygroupid) { int activity_id = (int) SvIV(ST(2)); quest_manager.CrossZoneResetActivityByGroupID(group_id, task_id, activity_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneresetactivitybyraidid); @@ -4471,7 +4512,7 @@ XS(XS__crosszoneresetactivitybyraidid) { int activity_id = (int) SvIV(ST(2)); quest_manager.CrossZoneResetActivityByRaidID(raid_id, task_id, activity_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneresetactivitybyguildid); @@ -4485,7 +4526,7 @@ XS(XS__crosszoneresetactivitybyguildid) { int activity_id = (int) SvIV(ST(2)); quest_manager.CrossZoneResetActivityByGuildID(guild_id, task_id, activity_id); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszonesetentityvariablebynpctypeid); @@ -4684,7 +4725,7 @@ XS(XS__crosszoneupdateactivitybycharid) { } quest_manager.CrossZoneUpdateActivityByCharID(char_id, task_id, activity_id, activity_count); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneupdateactivitybygroupid); @@ -4702,7 +4743,7 @@ XS(XS__crosszoneupdateactivitybygroupid) { } quest_manager.CrossZoneUpdateActivityByGroupID(group_id, task_id, activity_id, activity_count); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneupdateactivitybyraidid); @@ -4720,7 +4761,7 @@ XS(XS__crosszoneupdateactivitybyraidid) { } quest_manager.CrossZoneUpdateActivityByRaidID(raid_id, task_id, activity_id, activity_count); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__crosszoneupdateactivitybyguildid); @@ -4738,7 +4779,7 @@ XS(XS__crosszoneupdateactivitybyguildid) { } quest_manager.CrossZoneUpdateActivityByGuildID(guild_id, task_id, activity_id, activity_count); } - XSRETURN_EMPTY; + XSRETURN_EMPTY; } XS(XS__worldwideassigntask); @@ -4753,7 +4794,7 @@ XS(XS__worldwideassigntask) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4775,7 +4816,7 @@ XS(XS__worldwidecastspell) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4797,7 +4838,7 @@ XS(XS__worldwidedisabletask) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4819,7 +4860,7 @@ XS(XS__worldwideenabletask) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4841,7 +4882,7 @@ XS(XS__worldwidefailtask) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4868,7 +4909,7 @@ XS(XS__worldwidemarquee) { if (items == 7) { min_status = (uint8) SvUV(ST(6)); } - + if (items == 8) { max_status = (uint8) SvUV(ST(7)); } @@ -4891,7 +4932,7 @@ XS(XS__worldwidemessage) { if (items == 3) { min_status = (uint8) SvUV(ST(2)); } - + if (items == 4) { max_status = (uint8) SvUV(ST(3)); } @@ -4914,7 +4955,7 @@ XS(XS__worldwidemove) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4936,7 +4977,7 @@ XS(XS__worldwidemoveinstance) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4958,7 +4999,7 @@ XS(XS__worldwideremovespell) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -4980,7 +5021,7 @@ XS(XS__worldwideremovetask) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(2)); } @@ -5003,7 +5044,7 @@ XS(XS__worldwideresetactivity) { if (items == 3) { min_status = (uint8) SvUV(ST(2)); } - + if (items == 4) { max_status = (uint8) SvUV(ST(3)); } @@ -5026,7 +5067,7 @@ XS(XS__worldwidesetentityvariableclient) { if (items == 3) { min_status = (uint8) SvUV(ST(2)); } - + if (items == 4) { max_status = (uint8) SvUV(ST(3)); } @@ -5075,7 +5116,7 @@ XS(XS__worldwidesignalclient) { if (items == 2) { min_status = (uint8) SvUV(ST(1)); } - + if (items == 3) { max_status = (uint8) SvUV(ST(1)); } @@ -5102,7 +5143,7 @@ XS(XS__worldwideupdateactivity) { if (items == 4) { min_status = (uint8) SvUV(ST(3)); } - + if (items == 5) { max_status = (uint8) SvUV(ST(4)); } @@ -6042,6 +6083,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 @@ -6112,6 +6412,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); @@ -6171,7 +6473,7 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "crosszonemoveinstancebycharid"), XS__crosszonemoveinstancebycharid, file); newXS(strcpy(buf, "crosszonemoveinstancebygroupid"), XS__crosszonemoveinstancebygroupid, file); newXS(strcpy(buf, "crosszonemoveinstancebyraidid"), XS__crosszonemoveinstancebyraidid, file); - newXS(strcpy(buf, "crosszonemoveinstancebyguildid"), XS__crosszonemoveinstancebyguildid, file); + newXS(strcpy(buf, "crosszonemoveinstancebyguildid"), XS__crosszonemoveinstancebyguildid, file); newXS(strcpy(buf, "crosszoneremovespellbycharid"), XS__crosszoneremovespellbycharid, file); newXS(strcpy(buf, "crosszoneremovespellbygroupid"), XS__crosszoneremovespellbygroupid, file); newXS(strcpy(buf, "crosszoneremovespellbyraidid"), XS__crosszoneremovespellbyraidid, file); @@ -6246,6 +6548,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); @@ -6284,6 +6592,7 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "log"), XS__log, file); newXS(strcpy(buf, "log_combat"), XS__log_combat, file); newXS(strcpy(buf, "me"), XS__me, file); + newXS(strcpy(buf, "message"), XS__message, file); newXS(strcpy(buf, "modifynpcstat"), XS__ModifyNPCStat, file); newXS(strcpy(buf, "movegrp"), XS__movegrp, file); newXS(strcpy(buf, "movepc"), XS__movepc, file); @@ -6311,6 +6620,9 @@ 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); newXS(strcpy(buf, "resettaskactivity"), XS__resettaskactivity, file); @@ -6377,6 +6689,7 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "voicetell"), XS__voicetell, file); newXS(strcpy(buf, "we"), XS__we, file); newXS(strcpy(buf, "wearchange"), XS__wearchange, file); + newXS(strcpy(buf, "whisper"), XS__whisper, file); newXS(strcpy(buf, "write"), XS__write, file); newXS(strcpy(buf, "ze"), XS__ze, file); newXS(strcpy(buf, "zone"), XS__zone, 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 e02495e86..f48b371ce 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" @@ -712,6 +713,8 @@ void EntityList::AddNPC(NPC *npc, bool SendSpawnPacket, bool dontqueue) npc_list.insert(std::pair(npc->GetID(), npc)); mob_list.insert(std::pair(npc->GetID(), npc)); + entity_list.ScanCloseMobs(npc->close_mobs, npc, true); + /* Zone controller process EVENT_SPAWN_ZONE */ if (RuleB(Zone, UseZoneController)) { if (entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID) && npc->GetNPCTypeID() != ZONE_CONTROLLER_NPC_ID){ @@ -1322,8 +1325,8 @@ void EntityList::SendZoneSpawnsBulk(Client *client) const glm::vec4 &client_position = client->GetPosition(); const float distance_max = (600.0 * 600.0); - for (auto it = mob_list.begin(); it != mob_list.end(); ++it) { - spawn = it->second; + for (auto & it : mob_list) { + spawn = it.second; if (spawn && spawn->GetID() > 0 && spawn->Spawned()) { if (!spawn->ShouldISpawnFor(client)) { continue; @@ -2508,7 +2511,7 @@ void EntityList::DespawnAllDoors() auto outapp = new EQApplicationPacket(OP_RemoveAllDoors, 0); for (auto it = client_list.begin(); it != client_list.end(); ++it) { if (it->second) { - it->second->QueuePacket(outapp); + it->second->QueuePacket(outapp, true, Client::CLIENT_CONNECTED); } } safe_delete(outapp); @@ -2521,7 +2524,7 @@ void EntityList::RespawnAllDoors() if (it->second) { auto outapp = new EQApplicationPacket(); MakeDoorSpawnPacket(outapp, it->second); - it->second->FastQueuePacket(&outapp); + it->second->FastQueuePacket(&outapp, true, Client::CLIENT_CONNECTED); } ++it; } @@ -2690,6 +2693,36 @@ void EntityList::RemoveAuraFromMobs(Mob *aura) } /** + * The purpose of this system is so that we cache relevant entities that are "close" + * + * In general; it becomes incredibly expensive to run zone-wide checks against every single mob in the zone when in reality + * we only care about entities closest to us + * + * A very simple example of where this is relevant is Aggro, the below example is skewed because the overall implementation + * of Aggro was also tweaked in conjunction with close lists. We also scan more aggressively when entities are moving (1-6 seconds) + * versus 60 seconds when idle. We also have entities that are moving add themselves to those closest to them so that their close + * lists remain always up to date + * + * Before: Aggro checks for NPC to Client aggro | (40 clients in zone) x (525 npcs) x 2 (times a second) = 2,520,000 checks a minute + * After: Aggro checks for NPC to Client aggro | (40 clients in zone) x (20-30 npcs) x 2 (times a second) = 144,000 checks a minute (This is actually far less today) + * + * Places in the code where this logic makes a huge impact + * + * Aggro checks (zone wide -> close) + * Aura processing (zone wide -> close) + * AE Taunt (zone wide -> close) + * AOE Spells (zone wide -> close) + * Bard Pulse AOE (zone wide -> close) + * Mass Group Buff (zone wide -> close) + * AE Attack (zone wide -> close) + * Packet QueueCloseClients (zone wide -> close) + * Check Close Beneficial Spells (Buffs; should I heal other npcs) (zone wide -> close) + * AI Yell for Help (NPC Assist other NPCs) (zone wide -> close) + * + * All of the above makes a tremendous impact on the bottom line of cpu cycle performance because we run an order of magnitude + * less checks by focusing our hot path logic down to a very small subset of relevant entities instead of looping an entire + * entity list (zone wide) + * * @param close_mobs * @param scanning_mob */ @@ -4102,8 +4135,13 @@ void EntityList::AddTempPetsToHateList(Mob *owner, Mob* other, bool bFrenzy) NPC* n = it->second; if (n->GetSwarmInfo()) { if (n->GetSwarmInfo()->owner_id == owner->GetID()) { - if (!n->GetSpecialAbility(IMMUNE_AGGRO)) + if ( + !n->GetSpecialAbility(IMMUNE_AGGRO) && + !(n->GetSpecialAbility(IMMUNE_AGGRO_CLIENT) && other->IsClient()) && + !(n->GetSpecialAbility(IMMUNE_AGGRO_NPC) && other->IsNPC()) + ) { n->hate_list.AddEntToHateList(other, 0, 0, bFrenzy); + } } } ++it; @@ -4206,7 +4244,7 @@ void EntityList::ForceGroupUpdate(uint32 gid) } } -void EntityList::SendGroupLeave(uint32 gid, const char *name) +void EntityList::SendGroupLeave(uint32 gid, const char *name, bool checkleader) { auto it = client_list.begin(); while (it != client_list.end()) { @@ -4222,13 +4260,39 @@ void EntityList::SendGroupLeave(uint32 gid, const char *name) gj->action = groupActLeave; strcpy(gj->yourname, c->GetName()); Mob *Leader = g->GetLeader(); - if (Leader) + if (Leader) { Leader->CastToClient()->GetGroupAAs(&gj->leader_aas); + } c->QueuePacket(outapp); safe_delete(outapp); - g->DelMemberOOZ(name); - if (g->IsLeader(c) && c->IsLFP()) + g->DelMemberOOZ(name, checkleader); + if (g->IsLeader(c) && c->IsLFP()) { c->UpdateLFP(); + } + } + } + } + ++it; + } +} + +void EntityList::SendGroupLeader(uint32 gid, const char *lname, const char *oldlname) +{ + auto it = client_list.begin(); + while (it != client_list.end()) { + if (it->second){ + Group *g = nullptr; + g = it->second->GetGroup(); + if (g) { + if (g->GetID() == gid) { + EQApplicationPacket* outapp = new EQApplicationPacket(OP_GroupUpdate,sizeof(GroupJoin_Struct)); + GroupJoin_Struct* gj = (GroupJoin_Struct*) outapp->pBuffer; + gj->action = groupActMakeLeader; + strcpy(gj->membername, lname); + strcpy(gj->yourname, oldlname); + it->second->QueuePacket(outapp); + Log(Logs::Detail, Logs::Group, "SendGroupLeader(): Entity loop leader update packet sent to: %s .", it->second->GetName()); + safe_delete(outapp); } } } @@ -4251,9 +4315,9 @@ void EntityList::SendGroupJoin(uint32 gid, const char *name) gj->action = groupActJoin; strcpy(gj->yourname, it->second->GetName()); Mob *Leader = g->GetLeader(); - if (Leader) + if (Leader) { Leader->CastToClient()->GetGroupAAs(&gj->leader_aas); - + } it->second->QueuePacket(outapp); safe_delete(outapp); } @@ -5187,6 +5251,8 @@ void EntityList::ReloadMerchants() { * If we have a distance requested that is greater than our scanning distance * then we return the full list * + * See comments @EntityList::ScanCloseMobs for system explanation + * * @param mob * @param distance * @return @@ -5200,3 +5266,65 @@ 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); + } + } +} + +int EntityList::MovePlayerCorpsesToGraveyard(bool force_move_from_instance) +{ + if (!zone) + { + return 0; + } + + int moved_count = 0; + + for (auto it = corpse_list.begin(); it != corpse_list.end();) + { + bool moved = false; + if (it->second && it->second->IsPlayerCorpse()) + { + if (zone->HasGraveyard()) + { + moved = it->second->MovePlayerCorpseToGraveyard(); + } + else if (force_move_from_instance && zone->GetInstanceID() != 0) + { + moved = it->second->MovePlayerCorpseToNonInstance(); + } + } + + if (moved) + { + safe_delete(it->second); + free_ids.push(it->first); + it = corpse_list.erase(it); + ++moved_count; + } + else + { + ++it; + } + } + + return moved_count; +} diff --git a/zone/entity.h b/zone/entity.h index 3815f3c50..5bb5981fa 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; @@ -477,8 +478,9 @@ public: void CameraEffect(uint32 duration, uint32 intensity); Mob* GetClosestMobByBodyType(Mob* sender, bodyType BodyType); void ForceGroupUpdate(uint32 gid); - void SendGroupLeave(uint32 gid, const char *name); + void SendGroupLeave(uint32 gid, const char *name, bool checkleader); void SendGroupJoin(uint32 gid, const char *name); + void SendGroupLeader(uint32 gid, const char *lname, const char *oldlname); void SaveAllClientsTaskState(); void ReloadAllClientsTaskState(int TaskID=0); @@ -496,6 +498,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); @@ -534,6 +538,8 @@ public: void UpdateAllTraps(bool respawn, bool repopnow = false); void ClearTrapPointers(); + int MovePlayerCorpsesToGraveyard(bool force_move_from_instance = false); + protected: friend class Zone; void Depop(bool StartSpawnTimer = false); diff --git a/zone/exp.cpp b/zone/exp.cpp index 8f722b1f3..f6c682b21 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -664,11 +664,18 @@ void Client::SetEXP(uint32 set_exp, uint32 set_aaxp, bool isrezzexp) { m_pp.aapoints += last_unspentAA; //figure out how many points were actually gained - /*uint32 gained = m_pp.aapoints - last_unspentAA;*/ //unused + uint32 gained = (m_pp.aapoints - last_unspentAA); //Message(Chat::Yellow, "You have gained %d skill points!!", m_pp.aapoints - last_unspentAA); - char val1[20]={0}; - MessageString(Chat::Experience, GAIN_ABILITY_POINT, ConvertArray(m_pp.aapoints, val1),m_pp.aapoints == 1 ? "" : "(s)"); //You have gained an ability point! You now have %1 ability point%2. + char val1[20] = { 0 }; + char val2[20] = { 0 }; + if (gained == 1 && m_pp.aapoints == 1) + MessageString(Chat::Experience, GAIN_SINGLE_AA_SINGLE_AA, ConvertArray(m_pp.aapoints, val1)); //You have gained an ability point! You now have %1 ability point. + else if (gained == 1 && m_pp.aapoints > 1) + MessageString(Chat::Experience, GAIN_SINGLE_AA_MULTI_AA, ConvertArray(m_pp.aapoints, val1)); //You have gained an ability point! You now have %1 ability points. + else + MessageString(Chat::Experience, GAIN_MULTI_AA_MULTI_AA, ConvertArray(gained, val1), ConvertArray(m_pp.aapoints, val2)); //You have gained %1 ability point(s)! You now have %2 ability point(s). + if (RuleB(AA, SoundForAAEarned)) { SendSound(); } @@ -718,7 +725,8 @@ void Client::SetEXP(uint32 set_exp, uint32 set_aaxp, bool isrezzexp) { else Message(Chat::Yellow, "Welcome to level %i!", check_level); - if (check_level == RuleI(Character, DeathItemLossLevel)) + if (check_level == RuleI(Character, DeathItemLossLevel) && + m_ClientVersionBit & EQ::versions::maskUFAndEarlier) MessageString(Chat::Yellow, CORPSE_ITEM_LOST); if (check_level == RuleI(Character, DeathExpLossLevel)) diff --git a/zone/expedition.cpp b/zone/expedition.cpp new file mode 100644 index 000000000..cf9ebc0c1 --- /dev/null +++ b/zone/expedition.cpp @@ -0,0 +1,2293 @@ +/** + * 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() + }); + } + } + } + + // member swapping integrity is handled by invite response + if (!swapping) + { + auto member_count = ExpeditionDatabase::GetMemberCount(m_id); + if (member_count == 0) + { + has_conflict = true; + } + else if (member_count >= 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) + { + auto swap_member = GetMemberData(swap_remove_name); + if (!swap_member.IsValid() || !ExpeditionDatabase::HasMember(m_id, swap_member.char_id)) + { + 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..d5bb4925d --- /dev/null +++ b/zone/expedition_database.cpp @@ -0,0 +1,685 @@ +/** + * 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; +} + +uint32_t ExpeditionDatabase::GetMemberCount(uint32_t expedition_id) +{ + LogExpeditionsDetail("Getting expedition [{}] member count from db", expedition_id); + + uint32_t member_count = 0; + if (expedition_id != 0) + { + auto query = fmt::format(SQL( + SELECT COUNT(*) + FROM expedition_members + WHERE expedition_id = {} AND is_current_member = TRUE; + ), expedition_id); + + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) + { + auto row = results.begin(); + member_count = strtoul(row[0], nullptr, 10); + } + } + return member_count; +} + +bool ExpeditionDatabase::HasMember(uint32_t expedition_id, uint32_t character_id) +{ + LogExpeditionsDetail("Checking db expedition [{}] for character [{}]", expedition_id, character_id); + + if (expedition_id == 0 || character_id == 0) + { + return false; + } + + auto query = fmt::format(SQL( + SELECT id + FROM expedition_members + WHERE expedition_id = {} AND character_id = {} AND is_current_member = TRUE; + ), expedition_id, character_id); + + auto results = database.QueryDatabase(query); + return (results.Success() && results.RowCount() > 0); +} + +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..5484ea25a --- /dev/null +++ b/zone/expedition_database.h @@ -0,0 +1,111 @@ +/** + * 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); + uint32_t GetMemberCount(uint32_t expedition_id); + bool HasMember(uint32_t expedition_id, uint32_t character_id); + 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..abc868460 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" @@ -58,8 +59,12 @@ Group::Group(uint32 gid) } if(gid != 0) { - if(!LearnMembers()) + if(!LearnMembers()) { SetID(0); + } + if(GetLeader() != nullptr) { + SetOldLeaderName(GetLeaderName()); + } } for(int i = 0; i < MAX_MARKED_NPCS; ++i) MarkedNPCs[i] = 0; @@ -77,6 +82,8 @@ Group::Group(Mob* leader) members[0] = leader; leader->SetGrouped(true); SetLeader(leader); + SetOldLeaderName(leader->GetName()); + Log(Logs::Detail, Logs::Group, "Group:Group() Setting OldLeader to: %s and Leader to: %s", GetOldLeaderName(), leader->GetName()); AssistTargetID = 0; TankTargetID = 0; PullerTargetID = 0; @@ -603,39 +610,61 @@ void Group::SendGroupJoinOOZ(Mob* NewMember) { } -bool Group::DelMemberOOZ(const char *Name) { +bool Group::DelMemberOOZ(const char *Name, bool checkleader) { - if(!Name) return false; + if (!Name) return false; + bool removed = false; // If a member out of zone has disbanded, clear out their name. - // - for(unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if(!strcasecmp(Name, membername[i])) + for (unsigned int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!strcasecmp(Name, membername[i])) { // This shouldn't be called if the member is in this zone. - if(!members[i]) { - if(!strncmp(GetLeaderName(), Name, 64)) - { + if (!members[i]) { + if (!strncmp(GetLeaderName(), Name, 64)) { //TODO: Transfer leadership if leader disbands OOZ. UpdateGroupAAs(); } - memset(membername[i], 0, 64); - MemberRoles[i] = 0; - if(GroupCount() < 3) - { + if (GroupCount() < 3) { UnDelegateMarkNPC(NPCMarkerName.c_str()); - if (GetLeader() && GetLeader()->IsClient() && GetLeader()->CastToClient()->ClientVersion() < EQ::versions::ClientVersion::SoD) { - UnDelegateMainAssist(MainAssistName.c_str()); + if (GetLeader() && GetLeader()->IsClient() && + GetLeader()->CastToClient()->ClientVersion() < EQ::versions::ClientVersion::SoD) { + UnDelegateMainAssist(MainAssistName.c_str()); } ClearAllNPCMarks(); } - if (Name == mentoree_name) + if (Name == mentoree_name) { ClearGroupMentor(); - return true; + } + + memset(membername[i], 0, 64); + MemberRoles[i] = 0; + removed = true; + Log(Logs::Detail, Logs::Group, "DelMemberOOZ: Removed Member: %s", Name); + break; } + } } - return false; + if (GroupCount() < 2) { + DisbandGroup(); + return true; + } + + if (checkleader) { + Log(Logs::Detail, Logs::Group, "DelMemberOOZ: Checking leader..."); + if (strcmp(GetOldLeaderName(), Name) == 0 && GroupCount() >= 2) { + for (uint32 nl = 0; nl < MAX_GROUP_MEMBERS; nl++) { + if (members[nl]) { + if (members[nl]->IsClient()) { + ChangeLeader(members[nl]); + break; + } + } + } + } + } + return removed; } bool Group::DelMember(Mob* oldmember, bool ignoresender) @@ -645,16 +674,6 @@ bool Group::DelMember(Mob* oldmember, bool ignoresender) return false; } - // TODO: fix this shit - // okay, so there is code below that tries to handle this. It does not. - // So instead of figuring it out now, lets just disband the group so the client doesn't - // sit there with a broken group and there isn't any group leader shuffling going on - // since the code below doesn't work. - if (oldmember == GetLeader()) { - DisbandGroup(); - return true; - } - for (uint32 i = 0; i < MAX_GROUP_MEMBERS; i++) { if (members[i] == oldmember) @@ -663,44 +682,36 @@ bool Group::DelMember(Mob* oldmember, bool ignoresender) membername[i][0] = '\0'; memset(membername[i],0,64); MemberRoles[i] = 0; + Log(Logs::Detail, Logs::Group, "DelMember: Removed Member: %s", oldmember->GetCleanName()); break; } } - /* This may seem pointless but the case above does not cover the following situation: - * Group has Leader a, member b, member c - * b and c are out of zone - * a disconnects/quits - * b or c zone back in and disconnects/quits - * a is still "leader" from GetLeader()'s perspective and will crash the zone when we DelMember(b) - * Ultimately we should think up a better solution to this. - */ - if(oldmember == GetLeader()) + if(GroupCount() < 2) { - SetLeader(nullptr); + DisbandGroup(); + return true; } - //handle leader quitting group gracefully - if (oldmember == GetLeader() && GroupCount() >= 2) + // If the leader has quit and we have 2 or more players left in group, we want to first check the zone the old leader was in for a new leader. + // If a suitable replacement cannot be found, we need to go out of zone. If checkleader remains true after this method completes, another + // loop will be run in DelMemberOOZ. + bool checkleader = true; + if (strcmp(GetOldLeaderName(),oldmember->GetCleanName()) == 0 && GroupCount() >= 2) { for(uint32 nl = 0; nl < MAX_GROUP_MEMBERS; nl++) { - if(members[nl]) + if(members[nl]) { if (members[nl]->IsClient()) { ChangeLeader(members[nl]); + checkleader = false; break; } } } } - - if (!GetLeaderName()) - { - DisbandGroup(); - return true; - } auto pack = new ServerPacket(ServerOP_GroupLeave, sizeof(ServerGroupLeave_Struct)); ServerGroupLeave_Struct* gl = (ServerGroupLeave_Struct*)pack->pBuffer; @@ -708,6 +719,7 @@ bool Group::DelMember(Mob* oldmember, bool ignoresender) gl->zoneid = zone->GetZoneID(); gl->instance_id = zone->GetInstanceID(); strcpy(gl->member_name, oldmember->GetCleanName()); + gl->checkleader = checkleader; worldserver.SendPacket(pack); safe_delete(pack); @@ -799,6 +811,7 @@ bool Group::DelMember(Mob* oldmember, bool ignoresender) Bot::UpdateGroupCastingRoles(this); #endif + safe_delete(outapp); return true; } @@ -2329,17 +2342,16 @@ void Group::ChangeLeader(Mob* newleader) // this changes the current group leader, notifies other members, and updates leadship AA // if the new leader is invalid, do nothing - if (!newleader || !newleader->IsClient()) + if (!newleader) { return; - - Mob* oldleader = GetLeader(); + } auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); GroupJoin_Struct* gu = (GroupJoin_Struct*) outapp->pBuffer; gu->action = groupActMakeLeader; strcpy(gu->membername, newleader->GetName()); - strcpy(gu->yourname, oldleader->GetName()); + strcpy(gu->yourname, GetOldLeaderName()); SetLeader(newleader); database.SetGroupLeaderName(GetID(), newleader->GetName()); UpdateGroupAAs(); @@ -2351,9 +2363,22 @@ void Group::ChangeLeader(Mob* newleader) members[i]->CastToClient()->SendGroupLeaderChangePacket(newleader->GetName()); members[i]->CastToClient()->QueuePacket(outapp); + Log(Logs::Detail, Logs::Group, "ChangeLeader(): Local leader update packet sent to: %s .", members[i]->GetName()); } } safe_delete(outapp); + + Log(Logs::Detail, Logs::Group, "ChangeLeader(): Old Leader is: %s New leader is: %s", GetOldLeaderName(), newleader->GetName()); + + ServerPacket* pack = new ServerPacket(ServerOP_ChangeGroupLeader, sizeof(ServerGroupLeader_Struct)); + ServerGroupLeader_Struct* fgu = (ServerGroupLeader_Struct*)pack->pBuffer; + fgu->zoneid = zone->GetZoneID(); + fgu->gid = GetID(); + strcpy(fgu->leader_name, newleader->GetName()); + strcpy(fgu->oldleader_name, GetOldLeaderName()); + worldserver.SendPacket(pack); + + SetOldLeaderName(newleader->GetName()); } const char *Group::GetClientNameByIndex(uint8 index) @@ -2503,3 +2528,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..c27f9d59a 100644 --- a/zone/groups.h +++ b/zone/groups.h @@ -37,6 +37,8 @@ public: GroupIDConsumer() { id = 0; } GroupIDConsumer(uint32 gid) { id = gid; } inline const uint32 GetID() const { return id; } + void SetOldLeaderName(const char* oldleader) { strcpy(oldleadername, oldleader); } + char* GetOldLeaderName() { return oldleadername; } protected: friend class EntityList; @@ -44,6 +46,7 @@ protected: inline void SetID(uint32 set_id) { id = set_id; } private: uint32 id; + char oldleadername[64]; // Keeps the previous leader name, so when the entity is destroyed we can still transfer leadership. }; class Group : public GroupIDConsumer { @@ -58,6 +61,7 @@ public: void SendLeadershipAAUpdate(); void SendWorldGroup(uint32 zone_id,Mob* zoningmember); bool DelMemberOOZ(const char *Name); + bool DelMemberOOZ(const char *Name, bool checkleader); bool DelMember(Mob* oldmember,bool ignoresender = false); void DisbandGroup(bool joinraid = false); void GetMemberList(std::list& member_list, bool clear_list = true); @@ -153,6 +157,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/inventory.cpp b/zone/inventory.cpp index b047197d2..0a052824a 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -1154,9 +1154,34 @@ bool Client::AutoPutLootInInventory(EQ::ItemInstance& inst, bool try_worn, bool } } } + if( i == EQ::invslot::slotPrimary && m_inv[EQ::invslot::slotSecondary] ) { + uint8 instrument = m_inv[EQ::invslot::slotSecondary]->GetItem()->ItemType; + if( + instrument == EQ::item::ItemTypeWindInstrument || + instrument == EQ::item::ItemTypeStringedInstrument || + instrument == EQ::item::ItemTypeBrassInstrument || + instrument == EQ::item::ItemTypePercussionInstrument + ) { + LogInventory("Cannot equip a primary item with [{}] already in the secondary.", m_inv[EQ::invslot::slotSecondary]->GetItem()->Name); + continue; // Do not auto-equip Primary when instrument is in Secondary + } + } if (i == EQ::invslot::slotSecondary && m_inv[EQ::invslot::slotPrimary]) { // check to see if primary slot is a two hander - if (m_inv[EQ::invslot::slotPrimary]->GetItem()->IsType2HWeapon()) + uint8 instrument = inst.GetItem()->ItemType; + if( + instrument == EQ::item::ItemTypeWindInstrument || + instrument == EQ::item::ItemTypeStringedInstrument || + instrument == EQ::item::ItemTypeBrassInstrument || + instrument == EQ::item::ItemTypePercussionInstrument + ) { + LogInventory("Cannot equip a secondary instrument with [{}] already in the primary.", m_inv[EQ::invslot::slotPrimary]->GetItem()->Name); + continue; // Do not auto-equip instrument in Secondary when Primary is equipped. + } + + uint8 use = m_inv[EQ::invslot::slotPrimary]->GetItem()->ItemType; + if(use == EQ::item::ItemType2HSlash || use == EQ::item::ItemType2HBlunt || use == EQ::item::ItemType2HPiercing) { continue; + } } if (i == EQ::invslot::slotSecondary && inst.IsWeapon() && !CanThisClassDualWield()) { continue; @@ -1169,7 +1194,6 @@ bool Client::AutoPutLootInInventory(EQ::ItemInstance& inst, bool try_worn, bool if (worn_slot_material != EQ::textures::materialInvalid) { SendWearChange(worn_slot_material); } - parse->EventItem(EVENT_EQUIP_ITEM, this, &inst, nullptr, "", i); return true; } diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index a6145000d..41d082cf3 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" @@ -130,6 +134,16 @@ void Lua_Client::SetBaseGender(int v) { self->SetBaseGender(v); } +int Lua_Client::GetClassBitmask() { + Lua_Safe_Call_Int(); + return GetPlayerClassBit(self->GetClass()); +} + +int Lua_Client::GetRaceBitmask() { + Lua_Safe_Call_Int(); + return GetPlayerRaceBit(self->GetBaseRace()); +} + int Lua_Client::GetBaseFace() { Lua_Safe_Call_Int(); return self->GetBaseFace(); @@ -325,6 +339,21 @@ uint32 Lua_Client::GetBindZoneID(int index) { return self->GetBindZoneID(index); } +float Lua_Client::GetTargetRingX() { + Lua_Safe_Call_Real(); + return self->GetTargetRingX(); +} + +float Lua_Client::GetTargetRingY() { + Lua_Safe_Call_Real(); + return self->GetTargetRingY(); +} + +float Lua_Client::GetTargetRingZ() { + Lua_Safe_Call_Real(); + return self->GetTargetRingZ(); +} + void Lua_Client::MovePC(int zone, float x, float y, float z, float heading) { Lua_Safe_Call_Void(); self->MovePC(zone, x, y, z, heading); @@ -605,6 +634,132 @@ int Lua_Client::MemmedCount() { return self->MemmedCount(); } +luabind::object Lua_Client::GetLearnableDisciplines(lua_State* L) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto learnable_disciplines = self->GetLearnableDisciplines(); + int index = 0; + for (auto spell_id : learnable_disciplines) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetLearnableDisciplines(lua_State* L, uint8 min_level) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto learnable_disciplines = self->GetLearnableDisciplines(min_level); + int index = 0; + for (auto spell_id : learnable_disciplines) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetLearnableDisciplines(lua_State* L, uint8 min_level, uint8 max_level) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto learnable_disciplines = self->GetLearnableDisciplines(min_level, max_level); + int index = 0; + for (auto spell_id : learnable_disciplines) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetLearnedDisciplines(lua_State* L) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto learned_disciplines = self->GetLearnedDisciplines(); + int index = 0; + for (auto spell_id : learned_disciplines) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetMemmedSpells(lua_State* L) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto memmed_spells = self->GetMemmedSpells(); + int index = 0; + for (auto spell_id : memmed_spells) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetScribeableSpells(lua_State* L) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto scribeable_spells = self->GetScribeableSpells(); + int index = 0; + for (auto spell_id : scribeable_spells) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetScribeableSpells(lua_State* L, uint8 min_level) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto scribeable_spells = self->GetScribeableSpells(min_level); + int index = 0; + for (auto spell_id : scribeable_spells) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetScribeableSpells(lua_State* L, uint8 min_level, uint8 max_level) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto scribeable_spells = self->GetScribeableSpells(min_level, max_level); + int index = 0; + for (auto spell_id : scribeable_spells) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + +luabind::object Lua_Client::GetScribedSpells(lua_State* L) { + auto lua_table = luabind::newtable(L); + if (d_) { + auto self = reinterpret_cast(d_); + auto scribed_spells = self->GetScribedSpells(); + int index = 0; + for (auto spell_id : scribed_spells) { + lua_table[index] = spell_id; + index++; + } + } + return lua_table; +} + void Lua_Client::ScribeSpell(int spell_id, int slot) { Lua_Safe_Call_Void(); self->ScribeSpell(spell_id, slot); @@ -900,6 +1055,11 @@ bool Lua_Client::UseDiscipline(int spell_id, int target_id) { return self->UseDiscipline(spell_id, target_id); } +bool Lua_Client::HasDisciplineLearned(uint16 spell_id) { + Lua_Safe_Call_Bool(); + return self->HasDisciplineLearned(spell_id); +} + int Lua_Client::GetCharacterFactionLevel(int faction_id) { Lua_Safe_Call_Int(); return self->GetCharacterFactionLevel(faction_id); @@ -1040,6 +1200,16 @@ void Lua_Client::AddCrystals(uint32 radiant, uint32 ebon) { self->AddCrystals(radiant, ebon); } +void Lua_Client::SetEbonCrystals(uint32 value) { + Lua_Safe_Call_Void(); + self->SetEbonCrystals(value); +} + +void Lua_Client::SetRadiantCrystals(uint32 value) { + Lua_Safe_Call_Void(); + self->SetRadiantCrystals(value); +} + uint32 Lua_Client::GetPVPPoints() { Lua_Safe_Call_Int(); return self->GetPVPPoints(); @@ -1160,6 +1330,11 @@ int Lua_Client::GetNextAvailableSpellBookSlot() { return self->GetNextAvailableSpellBookSlot(); } +uint32 Lua_Client::GetSpellIDByBookSlot(int slot_id) { + Lua_Safe_Call_Int(); + return self->GetSpellIDByBookSlot(slot_id); +} + int Lua_Client::GetNextAvailableSpellBookSlot(int start) { Lua_Safe_Call_Int(); return self->GetNextAvailableSpellBookSlot(start); @@ -1629,7 +1804,249 @@ 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); +} + +void Lua_Client::Fling(float value, float target_x, float target_y, float target_z) { + Lua_Safe_Call_Void(); + self->Fling(value, target_x, target_y, target_z); +} + +void Lua_Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los) { + Lua_Safe_Call_Void(); + self->Fling(value, target_x, target_y, target_z, ignore_los); +} + +void Lua_Client::Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clipping) { + Lua_Safe_Call_Void(); + self->Fling(value, target_x, target_y, target_z, ignore_los, clipping); +} luabind::scope lua_register_client() { return luabind::class_("Client") @@ -1657,6 +2074,8 @@ luabind::scope lua_register_client() { .def("SetBaseClass", (void(Lua_Client::*)(int))&Lua_Client::SetBaseClass) .def("SetBaseRace", (void(Lua_Client::*)(int))&Lua_Client::SetBaseRace) .def("SetBaseGender", (void(Lua_Client::*)(int))&Lua_Client::SetBaseGender) + .def("GetClassBitmask", (int(Lua_Client::*)(void))&Lua_Client::GetClassBitmask) + .def("GetRaceBitmask", (int(Lua_Client::*)(void))&Lua_Client::GetRaceBitmask) .def("GetBaseFace", (int(Lua_Client::*)(void))&Lua_Client::GetBaseFace) .def("GetLanguageSkill", (int(Lua_Client::*)(int))&Lua_Client::GetLanguageSkill) .def("GetLastName", (const char *(Lua_Client::*)(void))&Lua_Client::GetLastName) @@ -1696,6 +2115,9 @@ luabind::scope lua_register_client() { .def("GetBindHeading", (float(Lua_Client::*)(int))&Lua_Client::GetBindHeading) .def("GetBindZoneID", (uint32(Lua_Client::*)(void))&Lua_Client::GetBindZoneID) .def("GetBindZoneID", (uint32(Lua_Client::*)(int))&Lua_Client::GetBindZoneID) + .def("GetTargetRingX", (float(Lua_Client::*)(void))&Lua_Client::GetTargetRingX) + .def("GetTargetRingY", (float(Lua_Client::*)(void))&Lua_Client::GetTargetRingY) + .def("GetTargetRingZ", (float(Lua_Client::*)(void))&Lua_Client::GetTargetRingZ) .def("SetPrimaryWeaponOrnamentation", (void(Lua_Client::*)(uint32))&Lua_Client::SetPrimaryWeaponOrnamentation) .def("SetSecondaryWeaponOrnamentation", (void(Lua_Client::*)(uint32))&Lua_Client::SetSecondaryWeaponOrnamentation) .def("MovePC", (void(Lua_Client::*)(int,float,float,float,float))&Lua_Client::MovePC) @@ -1754,6 +2176,15 @@ luabind::scope lua_register_client() { .def("UnmemSpellAll", (void(Lua_Client::*)(bool))&Lua_Client::UnmemSpellAll) .def("FindMemmedSpellBySlot", (uint16(Lua_Client::*)(int))&Lua_Client::FindMemmedSpellBySlot) .def("MemmedCount", (int(Lua_Client::*)(void))&Lua_Client::MemmedCount) + .def("GetLearnableDisciplines", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetLearnableDisciplines) + .def("GetLearnableDisciplines", (luabind::object(Lua_Client::*)(lua_State* L,uint8))&Lua_Client::GetLearnableDisciplines) + .def("GetLearnableDisciplines", (luabind::object(Lua_Client::*)(lua_State* L,uint8,uint8))&Lua_Client::GetLearnableDisciplines) + .def("GetLearnedDisciplines", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetLearnedDisciplines) + .def("GetMemmedSpells", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetMemmedSpells) + .def("GetScribedSpells", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetScribedSpells) + .def("GetScribeableSpells", (luabind::object(Lua_Client::*)(lua_State* L))&Lua_Client::GetScribeableSpells) + .def("GetScribeableSpells", (luabind::object(Lua_Client::*)(lua_State* L,uint8))&Lua_Client::GetScribeableSpells) + .def("GetScribeableSpells", (luabind::object(Lua_Client::*)(lua_State* L,uint8,uint8))&Lua_Client::GetScribeableSpells) .def("ScribeSpell", (void(Lua_Client::*)(int,int))&Lua_Client::ScribeSpell) .def("ScribeSpell", (void(Lua_Client::*)(int,int,bool))&Lua_Client::ScribeSpell) .def("UnscribeSpell", (void(Lua_Client::*)(int))&Lua_Client::UnscribeSpell) @@ -1813,6 +2244,7 @@ luabind::scope lua_register_client() { .def("GetDisciplineTimer", (uint32(Lua_Client::*)(uint32))&Lua_Client::GetDisciplineTimer) .def("ResetDisciplineTimer", (void(Lua_Client::*)(uint32))&Lua_Client::ResetDisciplineTimer) .def("UseDiscipline", (bool(Lua_Client::*)(int,int))&Lua_Client::UseDiscipline) + .def("HasDisciplineLearned", (bool(Lua_Client::*)(uint16))&Lua_Client::HasDisciplineLearned) .def("GetCharacterFactionLevel", (int(Lua_Client::*)(int))&Lua_Client::GetCharacterFactionLevel) .def("SetZoneFlag", (void(Lua_Client::*)(int))&Lua_Client::SetZoneFlag) .def("ClearZoneFlag", (void(Lua_Client::*)(int))&Lua_Client::ClearZoneFlag) @@ -1841,6 +2273,8 @@ luabind::scope lua_register_client() { .def("KeyRingCheck", (bool(Lua_Client::*)(uint32))&Lua_Client::KeyRingCheck) .def("AddPVPPoints", (void(Lua_Client::*)(uint32))&Lua_Client::AddPVPPoints) .def("AddCrystals", (void(Lua_Client::*)(uint32,uint32))&Lua_Client::AddCrystals) + .def("SetEbonCrystals", (void(Lua_Client::*)(uint32))&Lua_Client::SetEbonCrystals) + .def("SetRadiantCrystals", (void(Lua_Client::*)(uint32))&Lua_Client::SetRadiantCrystals) .def("GetPVPPoints", (uint32(Lua_Client::*)(void))&Lua_Client::GetPVPPoints) .def("GetRadiantCrystals", (uint32(Lua_Client::*)(void))&Lua_Client::GetRadiantCrystals) .def("GetEbonCrystals", (uint32(Lua_Client::*)(void))&Lua_Client::GetEbonCrystals) @@ -1866,6 +2300,7 @@ luabind::scope lua_register_client() { .def("ClearCompassMark",(void(Lua_Client::*)(void))&Lua_Client::ClearCompassMark) .def("GetNextAvailableSpellBookSlot", (int(Lua_Client::*)(void))&Lua_Client::GetNextAvailableSpellBookSlot) .def("GetNextAvailableSpellBookSlot", (int(Lua_Client::*)(int))&Lua_Client::GetNextAvailableSpellBookSlot) + .def("GetSpellIDByBookSlot", (uint32(Lua_Client::*)(int))& Lua_Client::GetSpellIDByBookSlot) .def("FindSpellBookSlotBySpellID", (int(Lua_Client::*)(int))&Lua_Client::FindSpellBookSlotBySpellID) .def("UpdateTaskActivity", (void(Lua_Client::*)(int,int,int))&Lua_Client::UpdateTaskActivity) .def("AssignTask", (void(Lua_Client::*)(int,int))&Lua_Client::AssignTask) @@ -1934,7 +2369,31 @@ 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) + .def("Fling", (void(Lua_Client::*)(float,float,float,float))&Lua_Client::Fling) + .def("Fling", (void(Lua_Client::*)(float,float,float,float,bool))&Lua_Client::Fling) + .def("Fling", (void(Lua_Client::*)(float,float,float,float,bool,bool))&Lua_Client::Fling); } luabind::scope lua_register_inventory_where() { diff --git a/zone/lua_client.h b/zone/lua_client.h index 5900930f0..90c15c065 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; @@ -51,7 +52,9 @@ public: bool GetGM(); void SetBaseClass(int v); void SetBaseRace(int v); - void SetBaseGender(int v); + void SetBaseGender(int v); + int GetClassBitmask(); + int GetRaceBitmask(); int GetBaseFace(); int GetLanguageSkill(int skill_id); const char *GetLastName(); @@ -91,6 +94,9 @@ public: float GetBindHeading(int index); uint32 GetBindZoneID(); uint32 GetBindZoneID(int index); + float GetTargetRingX(); + float GetTargetRingY(); + float GetTargetRingZ(); void MovePC(int zone, float x, float y, float z, float heading); void MovePCInstance(int zone, int instance, float x, float y, float z, float heading); void MoveZone(const char *zone_short_name); @@ -146,6 +152,15 @@ public: void UnmemSpellAll(bool update_client); uint16 FindMemmedSpellBySlot(int slot); int MemmedCount(); + luabind::object GetLearnableDisciplines(lua_State* L); + luabind::object GetLearnableDisciplines(lua_State* L, uint8 min_level); + luabind::object GetLearnableDisciplines(lua_State* L, uint8 min_level, uint8 max_level); + luabind::object GetLearnedDisciplines(lua_State* L); + luabind::object GetMemmedSpells(lua_State* L); + luabind::object GetScribedSpells(lua_State* L); + luabind::object GetScribeableSpells(lua_State* L); + luabind::object GetScribeableSpells(lua_State* L, uint8 min_level); + luabind::object GetScribeableSpells(lua_State* L, uint8 min_level, uint8 max_level); void ScribeSpell(int spell_id, int slot); void ScribeSpell(int spell_id, int slot, bool update_client); void UnscribeSpell(int slot); @@ -207,6 +222,7 @@ public: uint32 GetDisciplineTimer(uint32 timer_id); void ResetDisciplineTimer(uint32 timer_id); bool UseDiscipline(int spell_id, int target_id); + bool HasDisciplineLearned(uint16 spell_id); int GetCharacterFactionLevel(int faction_id); void SetZoneFlag(int zone_id); void ClearZoneFlag(int zone_id); @@ -235,6 +251,8 @@ public: bool KeyRingCheck(uint32 item); void AddPVPPoints(uint32 points); void AddCrystals(uint32 radiant, uint32 ebon); + void SetEbonCrystals(uint32 value); + void SetRadiantCrystals(uint32 value); uint32 GetPVPPoints(); uint32 GetRadiantCrystals(); uint32 GetEbonCrystals(); @@ -260,6 +278,7 @@ public: void ClearCompassMark(); int GetNextAvailableSpellBookSlot(); int GetNextAvailableSpellBookSlot(int start); + uint32 GetSpellIDByBookSlot(int book_slot); int FindSpellBookSlotBySpellID(int spell_id); void UpdateTaskActivity(int task, int activity, int count); void AssignTask(int task, int npc_id); @@ -329,12 +348,36 @@ 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); + void Fling(float value, float target_x, float target_y, float target_z); + void Fling(float value, float target_x, float target_y, float target_z, bool ignore_los); + void Fling(float value, float target_x, float target_y, float target_z, bool ignore_los, bool clipping); }; #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 1fb257150..b903ca774 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 { }; @@ -766,6 +768,14 @@ void lua_world_emote(int type, const char *str) { quest_manager.we(type, str); } +void lua_message(int color, const char *message) { + quest_manager.message(color, message); +} + +void lua_whisper(const char *message) { + quest_manager.whisper(message); +} + int lua_get_level(int type) { return quest_manager.getlevel(type); } @@ -806,6 +816,14 @@ int lua_count_item(uint32 item_id) { return quest_manager.countitem(item_id); } +void lua_remove_item(uint32 item_id) { + quest_manager.removeitem(item_id); +} + +void lua_remove_item(uint32 item_id, uint32 quantity) { + quest_manager.removeitem(item_id, quantity); +} + void lua_update_spawn_timer(uint32 id, uint32 new_time) { quest_manager.UpdateSpawnTimer(id, new_time); } @@ -2170,6 +2188,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) { \ @@ -2477,6 +2607,8 @@ luabind::scope lua_register_general() { luabind::def("clear_spawn_timers", &lua_clear_spawn_timers), luabind::def("zone_emote", &lua_zone_emote), luabind::def("world_emote", &lua_world_emote), + luabind::def("message", &lua_message), + luabind::def("whisper", &lua_whisper), luabind::def("get_level", &lua_get_level), luabind::def("create_ground_object", (void(*)(uint32,float,float,float,float))&lua_create_ground_object), luabind::def("create_ground_object", (void(*)(uint32,float,float,float,float,uint32))&lua_create_ground_object), @@ -2487,6 +2619,8 @@ luabind::scope lua_register_general() { luabind::def("modify_npc_stat", &lua_modify_npc_stat), luabind::def("collect_items", &lua_collect_items), luabind::def("count_item", &lua_count_item), + luabind::def("remove_item", (void(*)(uint32))&lua_remove_item), + luabind::def("remove_item", (void(*)(uint32,uint32))&lua_remove_item), luabind::def("update_spawn_timer", &lua_update_spawn_timer), luabind::def("merchant_set_item", (void(*)(uint32,uint32))&lua_merchant_set_item), luabind::def("merchant_set_item", (void(*)(uint32,uint32,uint32))&lua_merchant_set_item), @@ -2765,7 +2899,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) ]; } @@ -3254,7 +3403,24 @@ luabind::scope lua_register_message_types() { return luabind::class_("MT") .enum_("constants") [ + luabind::value("White", Chat::White), + luabind::value("DimGray", Chat::DimGray), + luabind::value("Default", Chat::Default), + luabind::value("Green", Chat::Green), + luabind::value("BrightBlue", Chat::BrightBlue), + luabind::value("LightBlue", Chat::LightBlue), + luabind::value("Magenta", Chat::Magenta), + luabind::value("Gray", Chat::Gray), + luabind::value("LightGray", Chat::LightGray), luabind::value("NPCQuestSay", Chat::NPCQuestSay), + luabind::value("DarkGray", Chat::DarkGray), + luabind::value("Red", Chat::Red), + luabind::value("Lime", Chat::Lime), + luabind::value("Yellow", Chat::Yellow), + luabind::value("Blue", Chat::Blue), + luabind::value("LightNavy", Chat::LightNavy), + luabind::value("Cyan", Chat::Cyan), + luabind::value("Black", Chat::Black), luabind::value("Say", Chat::Say), luabind::value("Tell", Chat::Tell), luabind::value("Group", Chat::Group), 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_mob.cpp b/zone/lua_mob.cpp index 53658e63d..53d277b12 100644 --- a/zone/lua_mob.cpp +++ b/zone/lua_mob.cpp @@ -372,6 +372,11 @@ int Lua_Mob::GetRace() { return self->GetRace(); } +const char *Lua_Mob::GetRaceName() { + Lua_Safe_Call_String(); + return GetRaceIDName(self->GetRace()); +} + int Lua_Mob::GetGender() { Lua_Safe_Call_Int(); return self->GetGender(); @@ -442,6 +447,11 @@ int Lua_Mob::GetClass() { return self->GetClass(); } +const char *Lua_Mob::GetClassName() { + Lua_Safe_Call_String(); + return GetClassIDName(self->GetClass()); +} + int Lua_Mob::GetLevel() { Lua_Safe_Call_Int(); return self->GetLevel(); @@ -2316,6 +2326,7 @@ luabind::scope lua_register_mob() { .def("GetBaseGender", &Lua_Mob::GetBaseGender) .def("GetDeity", &Lua_Mob::GetDeity) .def("GetRace", &Lua_Mob::GetRace) + .def("GetRaceName", &Lua_Mob::GetRaceName) .def("GetGender", &Lua_Mob::GetGender) .def("GetTexture", &Lua_Mob::GetTexture) .def("GetHelmTexture", &Lua_Mob::GetHelmTexture) @@ -2330,6 +2341,7 @@ luabind::scope lua_register_mob() { .def("GetDrakkinTattoo", &Lua_Mob::GetDrakkinTattoo) .def("GetDrakkinDetails", &Lua_Mob::GetDrakkinDetails) .def("GetClass", &Lua_Mob::GetClass) + .def("GetClassName", &Lua_Mob::GetClassName) .def("GetLevel", &Lua_Mob::GetLevel) .def("GetCleanName", &Lua_Mob::GetCleanName) .def("GetTarget", &Lua_Mob::GetTarget) @@ -2696,7 +2708,11 @@ luabind::scope lua_register_special_abilities() { luabind::value("ignore_root_aggro_rules", static_cast(IGNORE_ROOT_AGGRO_RULES)), luabind::value("casting_resist_diff", static_cast(CASTING_RESIST_DIFF)), luabind::value("counter_avoid_damage", static_cast(COUNTER_AVOID_DAMAGE)), - luabind::value("immune_ranged_attacks", static_cast(IMMUNE_RANGED_ATTACKS)) + luabind::value("immune_ranged_attacks", static_cast(IMMUNE_RANGED_ATTACKS)), + luabind::value("immune_damage_client", static_cast(IMMUNE_DAMAGE_CLIENT)), + luabind::value("immune_damage_npc", static_cast(IMMUNE_DAMAGE_NPC)), + luabind::value("immune_aggro_client", static_cast(IMMUNE_AGGRO_CLIENT)), + luabind::value("immune_aggro_npc", static_cast(IMMUNE_AGGRO_NPC)) ]; } diff --git a/zone/lua_mob.h b/zone/lua_mob.h index e2dbf9e24..426ac01af 100644 --- a/zone/lua_mob.h +++ b/zone/lua_mob.h @@ -92,6 +92,8 @@ public: int GetBaseGender(); int GetDeity(); int GetRace(); + const char *GetClassName(); + const char *GetRaceName(); int GetGender(); int GetTexture(); int GetHelmTexture(); 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/mob.cpp b/zone/mob.cpp index 63eb351cb..d2ed1e983 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -118,7 +118,7 @@ Mob::Mob( attack_anim_timer(500), position_update_melee_push_timer(500), hate_list_cleanup_timer(6000), - mob_scan_close(6000), + mob_close_scan_timer(6000), mob_check_moving_timer(1000) { mMovementManager = &MobMovementManager::Get(); @@ -463,7 +463,7 @@ Mob::Mob( m_manual_follow = false; #endif - mob_scan_close.Trigger(); + mob_close_scan_timer.Trigger(); } Mob::~Mob() @@ -1197,24 +1197,27 @@ void Mob::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) else ns->spawn.flymode = flymode; - if(IsBoat()) { - ns->spawn.flymode = GravityBehavior::Floating; - } - ns->spawn.lastName[0] = '\0'; strn0cpy(ns->spawn.lastName, lastname, sizeof(ns->spawn.lastName)); //for (i = 0; i < _MaterialCount; i++) - for (i = 0; i < 9; i++) - { + for (i = 0; i < 9; i++) { // Only Player Races Wear Armor - if (Mob::IsPlayerRace(race) || i > 6) - { - ns->spawn.equipment.Slot[i].Material = GetEquipmentMaterial(i); - ns->spawn.equipment.Slot[i].EliteModel = IsEliteMaterialItem(i); + if (Mob::IsPlayerRace(race) || i > 6) { + ns->spawn.equipment.Slot[i].Material = GetEquipmentMaterial(i); + ns->spawn.equipment.Slot[i].EliteModel = IsEliteMaterialItem(i); ns->spawn.equipment.Slot[i].HerosForgeModel = GetHerosForgeModel(i); - ns->spawn.equipment_tint.Slot[i].Color = GetEquipmentColor(i); + ns->spawn.equipment_tint.Slot[i].Color = GetEquipmentColor(i); + } + } + + if (texture > 0) { + for (i = 0; i < 9; i++) { + if (i == EQ::textures::weaponPrimary || i == EQ::textures::weaponSecondary || texture == 255) { + continue; + } + ns->spawn.equipment.Slot[i].Material = texture; } } @@ -1731,6 +1734,7 @@ void Mob::GMMove(float x, float y, float z, float heading, bool SendUpdate) { m_Position.x = x; m_Position.y = y; m_Position.z = z; + SetHeading(heading); mMovementManager->SendCommandToClients(this, 0.0, 0.0, 0.0, 0.0, 0, ClientRangeAny); if (IsNPC()) { @@ -1757,78 +1761,69 @@ void Mob::SendIllusionPacket( float in_size ) { - uint8 new_texture = in_texture; - uint8 new_helmtexture = in_helmtexture; - uint8 new_haircolor; - uint8 new_beardcolor; - uint8 new_eyecolor1; - uint8 new_eyecolor2; - uint8 new_hairstyle; - uint8 new_luclinface; - uint8 new_beard; - uint8 new_aa_title; + uint8 new_texture = in_texture; + uint8 new_helmtexture = in_helmtexture; + uint8 new_haircolor; + uint8 new_beardcolor; + uint8 new_eyecolor1; + uint8 new_eyecolor2; + uint8 new_hairstyle; + uint8 new_luclinface; + uint8 new_beard; + uint8 new_aa_title; uint32 new_drakkin_heritage; uint32 new_drakkin_tattoo; uint32 new_drakkin_details; race = in_race; - if (race == 0) - { + if (race == 0) { race = (use_model) ? use_model : GetBaseRace(); - } + } - if (in_gender != 0xFF) - { + if (in_gender != 0xFF) { gender = in_gender; - } - else - { + } + else { gender = (in_race) ? GetDefaultGender(race, gender) : GetBaseGender(); - } + } - if (in_texture == 0xFF && !IsPlayerRace(in_race)) - { + if (in_texture == 0xFF && !IsPlayerRace(in_race)) { new_texture = GetTexture(); - } + } - if (in_helmtexture == 0xFF && !IsPlayerRace(in_race)) - { + if (in_helmtexture == 0xFF && !IsPlayerRace(in_race)) { new_helmtexture = GetHelmTexture(); - } + } - new_haircolor = (in_haircolor == 0xFF) ? GetHairColor() : in_haircolor; - new_beardcolor = (in_beardcolor == 0xFF) ? GetBeardColor() : in_beardcolor; - new_eyecolor1 = (in_eyecolor1 == 0xFF) ? GetEyeColor1() : in_eyecolor1; - new_eyecolor2 = (in_eyecolor2 == 0xFF) ? GetEyeColor2() : in_eyecolor2; - new_hairstyle = (in_hairstyle == 0xFF) ? GetHairStyle() : in_hairstyle; - new_luclinface = (in_luclinface == 0xFF) ? GetLuclinFace() : in_luclinface; - new_beard = (in_beard == 0xFF) ? GetBeard() : in_beard; - new_drakkin_heritage = - (in_drakkin_heritage == 0xFFFFFFFF) ? GetDrakkinHeritage() : in_drakkin_heritage; - new_drakkin_tattoo = - (in_drakkin_tattoo == 0xFFFFFFFF) ? GetDrakkinTattoo() : in_drakkin_tattoo; - new_drakkin_details = - (in_drakkin_details == 0xFFFFFFFF) ? GetDrakkinDetails() : in_drakkin_details; - new_aa_title = in_aa_title; - size = (in_size <= 0.0f) ? GetSize() : in_size; + new_haircolor = (in_haircolor == 0xFF) ? GetHairColor() : in_haircolor; + new_beardcolor = (in_beardcolor == 0xFF) ? GetBeardColor() : in_beardcolor; + new_eyecolor1 = (in_eyecolor1 == 0xFF) ? GetEyeColor1() : in_eyecolor1; + new_eyecolor2 = (in_eyecolor2 == 0xFF) ? GetEyeColor2() : in_eyecolor2; + new_hairstyle = (in_hairstyle == 0xFF) ? GetHairStyle() : in_hairstyle; + new_luclinface = (in_luclinface == 0xFF) ? GetLuclinFace() : in_luclinface; + new_beard = (in_beard == 0xFF) ? GetBeard() : in_beard; + new_drakkin_heritage = (in_drakkin_heritage == 0xFFFFFFFF) ? GetDrakkinHeritage() : in_drakkin_heritage; + new_drakkin_tattoo = (in_drakkin_tattoo == 0xFFFFFFFF) ? GetDrakkinTattoo() : in_drakkin_tattoo; + new_drakkin_details = (in_drakkin_details == 0xFFFFFFFF) ? GetDrakkinDetails() : in_drakkin_details; + new_aa_title = in_aa_title; // Reset features to Base from the Player Profile if (IsClient() && in_race == 0) { - race = CastToClient()->GetBaseRace(); - gender = CastToClient()->GetBaseGender(); - new_texture = texture = 0xFF; - new_helmtexture = helmtexture = 0xFF; - new_haircolor = haircolor = CastToClient()->GetBaseHairColor(); - new_beardcolor = beardcolor = CastToClient()->GetBaseBeardColor(); - new_eyecolor1 = eyecolor1 = CastToClient()->GetBaseEyeColor(); - new_eyecolor2 = eyecolor2 = CastToClient()->GetBaseEyeColor(); - new_hairstyle = hairstyle = CastToClient()->GetBaseHairStyle(); - new_luclinface = luclinface = CastToClient()->GetBaseFace(); - new_beard = beard = CastToClient()->GetBaseBeard(); - new_aa_title = aa_title = 0xFF; - new_drakkin_heritage = drakkin_heritage = CastToClient()->GetBaseHeritage(); - new_drakkin_tattoo = drakkin_tattoo = CastToClient()->GetBaseTattoo(); - new_drakkin_details = drakkin_details = CastToClient()->GetBaseDetails(); + race = CastToClient()->GetBaseRace(); + gender = CastToClient()->GetBaseGender(); + new_texture = texture = 0xFF; + new_helmtexture = helmtexture = 0xFF; + new_haircolor = haircolor = CastToClient()->GetBaseHairColor(); + new_beardcolor = beardcolor = CastToClient()->GetBaseBeardColor(); + new_eyecolor1 = eyecolor1 = CastToClient()->GetBaseEyeColor(); + new_eyecolor2 = eyecolor2 = CastToClient()->GetBaseEyeColor(); + new_hairstyle = hairstyle = CastToClient()->GetBaseHairStyle(); + new_luclinface = luclinface = CastToClient()->GetBaseFace(); + new_beard = beard = CastToClient()->GetBaseBeard(); + new_aa_title = aa_title = 0xFF; + new_drakkin_heritage = drakkin_heritage = CastToClient()->GetBaseHeritage(); + new_drakkin_tattoo = drakkin_tattoo = CastToClient()->GetBaseTattoo(); + new_drakkin_details = drakkin_details = CastToClient()->GetBaseDetails(); switch (race) { case OGRE: size = 9; @@ -1859,6 +1854,21 @@ void Mob::SendIllusionPacket( } } + // update internal values for mob + size = (in_size <= 0.0f) ? GetSize() : in_size; + texture = new_texture; + helmtexture = new_helmtexture; + haircolor = new_haircolor; + beardcolor = new_beardcolor; + eyecolor1 = new_eyecolor1; + eyecolor2 = new_eyecolor2; + hairstyle = new_hairstyle; + luclinface = new_luclinface; + beard = new_beard; + drakkin_heritage = new_drakkin_heritage; + drakkin_tattoo = new_drakkin_tattoo; + drakkin_details = new_drakkin_details; + auto outapp = new EQApplicationPacket(OP_Illusion, sizeof(Illusion_Struct)); Illusion_Struct *is = (Illusion_Struct *) outapp->pBuffer; is->spawnid = GetID(); @@ -1883,9 +1893,10 @@ void Mob::SendIllusionPacket( safe_delete(outapp); /* Refresh armor and tints after send illusion packet */ - this->SendArmorAppearance(); + SendArmorAppearance(); - LogSpells("Illusion: Race = [{}], Gender = [{}], Texture = [{}], HelmTexture = [{}], HairColor = [{}], BeardColor = [{}], EyeColor1 = [{}], EyeColor2 = [{}], HairStyle = [{}], Face = [{}], DrakkinHeritage = [{}], DrakkinTattoo = [{}], DrakkinDetails = [{}], Size = [{}]", + LogSpells( + "Illusion: Race [{}] Gender [{}] Texture [{}] HelmTexture [{}] HairColor [{}] BeardColor [{}] EyeColor1 [{}] EyeColor2 [{}] HairStyle [{}] Face [{}] DrakkinHeritage [{}] DrakkinTattoo [{}] DrakkinDetails [{}] Size [{}]", race, gender, new_texture, @@ -1899,7 +1910,8 @@ void Mob::SendIllusionPacket( new_drakkin_heritage, new_drakkin_tattoo, new_drakkin_details, - size); + size + ); } bool Mob::RandomizeFeatures(bool send_illusion, bool set_variables) @@ -2405,7 +2417,7 @@ void Mob::ChangeSize(float in_size = 0, bool bNoRestriction) { if (in_size > 255.0) in_size = 255.0; //End of Size Code - this->size = in_size; + size = in_size; SendAppearancePacket(AT_Size, (uint32) in_size); } @@ -2809,6 +2821,11 @@ bool Mob::HateSummon() { } void Mob::FaceTarget(Mob* mob_to_face /*= 0*/) { + + if (IsBoat()) { + return; + } + Mob* faced_mob = mob_to_face; if(!faced_mob) { if(!GetTarget()) { @@ -3121,6 +3138,12 @@ void Mob::ExecWeaponProc(const EQ::ItemInstance *inst, uint16 spell_id, Mob *on, return; } + if (on->GetSpecialAbility(IMMUNE_DAMAGE_CLIENT) && IsClient()) + return; + + if (on->GetSpecialAbility(IMMUNE_DAMAGE_NPC) && IsNPC()) + return; + if (IsNoCast()) return; @@ -4872,7 +4895,8 @@ bool Mob::IsBoat() const { race == RACE_SHIP_404 || race == RACE_MERCHANT_SHIP_550 || race == RACE_PIRATE_SHIP_551 || - race == RACE_GHOST_SHIP_552 + race == RACE_GHOST_SHIP_552 || + race == RACE_BOAT_533 ); } diff --git a/zone/mob.h b/zone/mob.h index 6fd040175..1f8705a30 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -172,8 +172,8 @@ public: void DisplayInfo(Mob *mob); std::unordered_map close_mobs; - Timer mob_scan_close; - Timer mob_check_moving_timer; + Timer mob_close_scan_timer; + Timer mob_check_moving_timer; //Somewhat sorted: needs documenting! diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index 994724f0d..9d0ea02be 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -1605,11 +1605,12 @@ void NPC::AI_DoMovement() { * if the roam box was sloppily configured */ if (!this->GetWasSpawnedInWater()) { + roambox_destination_z = GetGroundZ(roambox_destination_x, roambox_destination_y); if (zone->HasMap() && zone->HasWaterMap()) { auto position = glm::vec3( roambox_destination_x, roambox_destination_y, - (m_Position.z - 15) + roambox_destination_z ); /** @@ -1629,6 +1630,19 @@ void NPC::AI_DoMovement() { } } } + else { // Mob was in water, make sure new spot is in water also + roambox_destination_z = m_Position.z; + auto position = glm::vec3( + roambox_destination_x, + roambox_destination_y, + m_Position.z + 15 + ); + if (zone->HasWaterMap() && !zone->watermap->InLiquid(position)) { + roambox_destination_x = m_SpawnPoint.x; + roambox_destination_y = m_SpawnPoint.y; + roambox_destination_z = m_SpawnPoint.z; + } + } PathfinderOptions opts; opts.smooth_path = true; @@ -1643,7 +1657,7 @@ void NPC::AI_DoMovement() { glm::vec3( roambox_destination_x, roambox_destination_y, - GetGroundZ(roambox_destination_x, roambox_destination_y) + roambox_destination_z ), partial, stuck, @@ -1659,8 +1673,6 @@ void NPC::AI_DoMovement() { return; } - roambox_destination_z = 0; - Log( Logs::General, Logs::NPCRoamBox, diff --git a/zone/mob_info.cpp b/zone/mob_info.cpp index 95f04edec..44d6a8c88 100644 --- a/zone/mob_info.cpp +++ b/zone/mob_info.cpp @@ -640,7 +640,7 @@ void Mob::DisplayInfo(Mob *mob) Client *client = this->CastToClient(); - if (!client->IsDevToolsWindowEnabled()) { + if (!client->IsDevToolsEnabled()) { return; } diff --git a/zone/mob_movement_manager.cpp b/zone/mob_movement_manager.cpp index 904bc5246..90bc075d4 100644 --- a/zone/mob_movement_manager.cpp +++ b/zone/mob_movement_manager.cpp @@ -1088,6 +1088,17 @@ void MobMovementManager::UpdatePath(Mob *who, float x, float y, float z, MobMove PushFlyTo(ent.second, x, y, z, mob_movement_mode); PushStopMoving(ent.second); } + // Below for npcs that can traverse land or water so they don't sink + else if (who->GetFlyMode() == GravityBehavior::Water && + zone->watermap->InLiquid(who->GetPosition()) && + zone->watermap->InLiquid(glm::vec3(x, y, z)) && + zone->zonemap->CheckLoS(who->GetPosition(), glm::vec3(x, y, z))) { + auto iter = _impl->Entries.find(who); + auto &ent = (*iter); + + PushSwimTo(ent.second, x, y, z, mob_movement_mode); + PushStopMoving(ent.second); + } else { UpdatePathGround(who, x, y, z, mob_movement_mode); } @@ -1214,7 +1225,7 @@ void MobMovementManager::UpdatePathGround(Mob *who, float x, float y, float z, M ) ); } - else { + else if(!next_node.teleport) { if (zone->watermap->InLiquid(previous_pos)) { PushSwimTo(ent.second, next_node.pos.x, next_node.pos.y, next_node.pos.z, mode); } @@ -1334,7 +1345,7 @@ void MobMovementManager::UpdatePathUnderwater(Mob *who, float x, float y, float next_node.pos.y )); } - else { + else if(!next_node.teleport) { PushSwimTo(ent.second, next_node.pos.x, next_node.pos.y, next_node.pos.z, movement_mode); } } diff --git a/zone/npc.cpp b/zone/npc.cpp index 2cd5cd095..13e49d4ea 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -230,9 +230,14 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi adventure_template_id = npc_type_data->adventure_template; flymode = iflymode; + // If server has set a flymode in db honor it over all else. + // If server has not set a flymde in db, and this is a boat - force floating. if (npc_type_data->flymode >= 0) { flymode = static_cast(npc_type_data->flymode); } + else if (IsBoat()) { + flymode = GravityBehavior::Floating; + } guard_anim = eaStanding; roambox_distance = 0; @@ -715,7 +720,7 @@ bool NPC::Process() } return false; } - + if (IsStunned() && stunned_timer.Check()) { Mob::UnStun(); this->spun_timer.Disable(); @@ -723,22 +728,27 @@ bool NPC::Process() SpellProcess(); - if (mob_scan_close.Check()) { - - entity_list.ScanCloseMobs(close_mobs, this); - - if (moving) { - mob_scan_close.Disable(); - mob_scan_close.Start(RandomTimer(3000, 6000)); - } - else { - mob_scan_close.Disable(); - mob_scan_close.Start(RandomTimer(6000, 60000)); - } + if (mob_close_scan_timer.Check()) { + entity_list.ScanCloseMobs(close_mobs, this, IsMoving()); } - if (mob_check_moving_timer.Check() && moving) { - mob_scan_close.Trigger(); + const uint16 npc_mob_close_scan_timer_moving = 6000; + const uint16 npc_mob_close_scan_timer_idle = 60000; + + if (mob_check_moving_timer.Check()) { + if (moving) { + if (mob_close_scan_timer.GetRemainingTime() > npc_mob_close_scan_timer_moving) { + LogAIScanCloseDetail("NPC [{}] Restarting with moving timer", GetCleanName()); + mob_close_scan_timer.Disable(); + mob_close_scan_timer.Start(npc_mob_close_scan_timer_moving); + mob_close_scan_timer.Trigger(); + } + } + else if (mob_close_scan_timer.GetDuration() == npc_mob_close_scan_timer_moving) { + LogAIScanCloseDetail("NPC [{}] Restarting with idle timer", GetCleanName()); + mob_close_scan_timer.Disable(); + mob_close_scan_timer.Start(npc_mob_close_scan_timer_idle); + } } if (tic_timer.Check()) { @@ -963,9 +973,12 @@ void NPC::Depop(bool StartSpawnTimer) { if(emoteid != 0) this->DoNPCEmote(ONDESPAWN,emoteid); p_depop = true; - if (StartSpawnTimer) { - if (respawn2 != 0) { + if (respawn2) + { + if (StartSpawnTimer) { respawn2->DeathReset(); + } else { + respawn2->Depop(); } } } @@ -1747,7 +1760,7 @@ void NPC::PickPocket(Client* thief) steal_item = false; break; } - + auto item_inst = database.CreateItem(loot_selection[random].first, loot_selection[random].second); if (item_inst == nullptr) { steal_item = false; @@ -1773,7 +1786,7 @@ void NPC::PickPocket(Client* thief) while (!steal_item && has_coin) { uint32 coin_amount = zone->random.Int(1, (steal_skill / 25) + 1); - + int coin_type = PickPocketPlatinum; while (coin_type <= PickPocketCopper) { if (money[coin_type]) { @@ -2509,10 +2522,10 @@ void NPC::LevelScale() { max_hp += (random_level - level) * 100; base_hp += (random_level - level) * 100; } - + current_hp = max_hp; } - + // Don't add max_dmg to dynamically scaled NPCs since this will be calculated later if (max_dmg > 0 || skip_auto_scale) { @@ -2812,7 +2825,7 @@ FACTION_VALUE NPC::CheckNPCFactionAlly(int32 other_faction) { // I believe that the assumption is, barring no entry in npc_faction_entries // that two npcs on like faction con ally to each other. This catches cases - // where an npc is on a faction but has no hits (hence no entry in + // where an npc is on a faction but has no hits (hence no entry in // npc_faction_entries). if (GetPrimaryFaction() == other_faction) diff --git a/zone/object.cpp b/zone/object.cpp index 97fdcd69b..88038bbe8 100644 --- a/zone/object.cpp +++ b/zone/object.cpp @@ -463,7 +463,7 @@ void Object::RandomSpawn(bool send_packet) { m_data.x = zone->random.Real(m_min_x, m_max_x); m_data.y = zone->random.Real(m_min_y, m_max_y); - if(m_data.z == BEST_Z_INVALID) { + if (m_data.z == BEST_Z_INVALID && zone->HasMap()) { glm::vec3 me; me.x = m_data.x; me.y = m_data.y; diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index aabeaa77d..a1113601e 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; @@ -4707,6 +4721,50 @@ XS(XS_Client_AddCrystals) { XSRETURN_EMPTY; } +XS(XS_Client_SetEbonCrystals); +XS(XS_Client_SetEbonCrystals) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: Client::SetEbonCrystals(THIS, uint32 value)"); + { + Client *THIS; + uint32 value = (uint32) SvUV(ST(1)); + + 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."); + + THIS->SetEbonCrystals(value); + } + XSRETURN_EMPTY; +} + +XS(XS_Client_SetRadiantCrystals); +XS(XS_Client_SetRadiantCrystals) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: Client::SetRadiantCrystals(THIS, uint32 value)"); + { + Client *THIS; + uint32 value = (uint32) SvUV(ST(1)); + + 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."); + + THIS->SetRadiantCrystals(value); + } + XSRETURN_EMPTY; +} + XS(XS_Client_GetPVPPoints); /* prototype to pass -Wmissing-prototypes */ XS(XS_Client_GetPVPPoints) { dXSARGS; @@ -5297,6 +5355,33 @@ XS(XS_Client_GetSpellBookSlotBySpellID) { XSRETURN(1); } +XS(XS_Client_GetSpellIDByBookSlot); /* prototype to pass -Wmissing-prototypes */ +XS(XS_Client_GetSpellIDByBookSlot) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: Client::GetSpellIDByBookSlot(THIS, int slot_id)"); + { + Client* THIS; + int RETVAL; + int slot_id = SvUV(ST(1)); + dXSTARG; + + 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."); + + RETVAL = THIS->GetSpellIDByBookSlot(slot_id); + XSprePUSH; + PUSHi((IV)RETVAL); + } + XSRETURN(1); +} + XS(XS_Client_UpdateTaskActivity); /* prototype to pass -Wmissing-prototypes */ XS(XS_Client_UpdateTaskActivity) { dXSARGS; @@ -6549,7 +6634,7 @@ XS(XS_Client_Popup2); /* prototype to pass -Wmissing-prototypes */ XS(XS_Client_Popup2) { dXSARGS; if (items < 3 || items > 10) - Perl_croak(aTHX_ "Usage: Client::SendFullPopup(THIS, string title, string text, uint32 popup_id, uint32 negative_id, uint32 buttons, uint32 duration, string button_name_0, string button_name_1, uint32 sound_controls)"); + Perl_croak(aTHX_ "Usage: Client::Popup2(THIS, string title, string text, uint32 popup_id, uint32 negative_id, uint32 buttons, uint32 duration, string button_name_0, string button_name_1, uint32 sound_controls)"); { Client *THIS; char *Title = (char *) SvPV_nolen(ST(1)); @@ -6688,6 +6773,520 @@ 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; +} + +XS(XS_Client_Fling); /* prototype to pass -Wmissing-prototypes */ +XS(XS_Client_Fling) { + dXSARGS; + if (items < 5 || items > 7) + Perl_croak(aTHX_ "Usage: Client::Fling(THIS, value, target_x, target_y, target_z, ignore_los, clipping)"); + { + Client* THIS; + float value = (float) SvNV(ST(1)); + float target_x = (float) SvNV(ST(2)); + float target_y = (float) SvNV(ST(3)); + float target_z = (float) SvNV(ST(4)); + bool ignore_los = false; + bool clipping = false; + + 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."); + + if (items > 5) + ignore_los = (bool) SvTRUE(ST(5)); + + if (items > 6) + clipping = (bool) SvTRUE(ST(6)); + + THIS->Fling(value, target_x, target_y, target_z, ignore_los, clipping); + } + XSRETURN_EMPTY; +} + +XS(XS_Client_HasDisciplineLearned); /* prototype to pass -Wmissing-prototypes */ +XS(XS_Client_HasDisciplineLearned) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: Client::HasDisciplineLearned(THIS, uint16 spell_id)"); + { + Client *THIS; + bool has_learned; + uint16 spell_id = (uint16) SvUV(ST(1)); + + 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."); + + has_learned = THIS->HasDisciplineLearned(spell_id); + ST(0) = boolSV(has_learned); + sv_2mortal(ST(0)); + } + XSRETURN(1); +} + +XS(XS_Client_GetClassBitmask); +XS(XS_Client_GetClassBitmask) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Client::GetClassBitmask(THIS)"); + { + Client* THIS; + int client_bitmask = 0; + dXSTARG; + + 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."); + + client_bitmask = GetPlayerClassBit(THIS->GetClass()); + XSprePUSH; + PUSHu((UV) client_bitmask); + } + XSRETURN(1); +} + +XS(XS_Client_GetRaceBitmask); +XS(XS_Client_GetRaceBitmask) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Client::GetRaceBitmask(THIS)"); + { + Client* THIS; + int client_bitmask = 0; + dXSTARG; + + 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."); + + client_bitmask = GetPlayerRaceBit(THIS->GetBaseRace()); + XSprePUSH; + PUSHu((UV) client_bitmask); + } + XSRETURN(1); +} + +XS(XS_Client_GetLearnableDisciplines); +XS(XS_Client_GetLearnableDisciplines) { + dXSARGS; + if (items < 1 || items > 3) + Perl_croak(aTHX_ "Usage: Client::GetLearnableDisciplines(THIS, [uint8 min_level, uint8 max_level])"); + + uint8 min_level = 1; + uint8 max_level = 0; + if (items > 1) + min_level = (uint8)SvUV(ST(1)); + if (items > 2) + max_level = (uint8)SvUV(ST(2)); + + Client* THIS; + VALIDATE_THIS_IS_CLIENT; + auto learnable_disciplines = THIS->GetLearnableDisciplines(min_level, max_level); + auto learnable_size = learnable_disciplines.size(); + if (learnable_size > 0) { + EXTEND(sp, learnable_size); + for (int index = 0; index < learnable_size; ++index) { + ST(index) = sv_2mortal(newSVuv(learnable_disciplines[index])); + } + XSRETURN(learnable_size); + } + SV* return_value = &PL_sv_undef; + ST(0) = return_value; + XSRETURN(1); +} + +XS(XS_Client_GetLearnedDisciplines); +XS(XS_Client_GetLearnedDisciplines) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Client::GetLearnedDisciplines(THIS)"); + + Client* THIS; + VALIDATE_THIS_IS_CLIENT; + auto learned_disciplines = THIS->GetLearnedDisciplines(); + auto learned_size = learned_disciplines.size(); + if (learned_size > 0) { + EXTEND(sp, learned_size); + for (int index = 0; index < learned_size; ++index) { + ST(index) = sv_2mortal(newSVuv(learned_disciplines[index])); + } + XSRETURN(learned_size); + } + SV* return_value = &PL_sv_undef; + ST(0) = return_value; + XSRETURN(1); +} + +XS(XS_Client_GetMemmedSpells); +XS(XS_Client_GetMemmedSpells) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Client::GetMemmedSpells(THIS)"); + + Client* THIS; + VALIDATE_THIS_IS_CLIENT; + auto memmed_spells = THIS->GetMemmedSpells(); + auto memmed_size = memmed_spells.size(); + if (memmed_size > 0) { + EXTEND(sp, memmed_size); + for (int index = 0; index < memmed_size; ++index) { + ST(index) = sv_2mortal(newSVuv(memmed_spells[index])); + } + XSRETURN(memmed_size); + } + SV* return_value = &PL_sv_undef; + ST(0) = return_value; + XSRETURN(1); +} + +XS(XS_Client_GetScribeableSpells); +XS(XS_Client_GetScribeableSpells) { + dXSARGS; + if (items < 1 || items > 3) + Perl_croak(aTHX_ "Usage: Client::GetScribeableSpells(THIS, [uint8 min_level, uint8 max_level])"); + + uint8 min_level = 1; + uint8 max_level = 0; + if (items > 1) + min_level = (uint8)SvUV(ST(1)); + if (items > 2) + max_level = (uint8)SvUV(ST(2)); + + Client* THIS; + VALIDATE_THIS_IS_CLIENT; + auto scribeable_spells = THIS->GetScribeableSpells(min_level, max_level); + auto scribeable_size = scribeable_spells.size(); + if (scribeable_size > 0) { + EXTEND(sp, scribeable_size); + for (int index = 0; index < scribeable_size; ++index) { + ST(index) = sv_2mortal(newSVuv(scribeable_spells[index])); + } + XSRETURN(scribeable_size); + } + SV* return_value = &PL_sv_undef; + ST(0) = return_value; + XSRETURN(1); +} + +XS(XS_Client_GetScribedSpells); +XS(XS_Client_GetScribedSpells) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Client::GetScribedSpells(THIS)"); + + Client* THIS; + VALIDATE_THIS_IS_CLIENT; + auto scribed_spells = THIS->GetScribedSpells(); + auto scribed_size = scribed_spells.size(); + if (scribed_size > 0) { + EXTEND(sp, scribed_size); + for (int index = 0; index < scribed_size; ++index) { + ST(index) = sv_2mortal(newSVuv(scribed_spells[index])); + } + XSRETURN(scribed_size); + } + SV* return_value = &PL_sv_undef; + ST(0) = return_value; + XSRETURN(1); +} #ifdef __cplusplus extern "C" @@ -6715,6 +7314,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, "$$"); @@ -6733,6 +7334,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, "$$;$$"); @@ -6743,6 +7345,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "Escape"), XS_Client_Escape, file, "$"); newXSproto(strcpy(buf, "ExpeditionMessage"), XS_Client_ExpeditionMessage, file, "$$$"); newXSproto(strcpy(buf, "FailTask"), XS_Client_FailTask, file, "$$"); + newXSproto(strcpy(buf, "Fling"), XS_Client_Fling, file, "$$$$$;$$"); newXSproto(strcpy(buf, "ForageItem"), XS_Client_ForageItem, file, "$"); newXSproto(strcpy(buf, "Freeze"), XS_Client_Freeze, file, "$"); newXSproto(strcpy(buf, "GetAAExp"), XS_Client_GetAAExp, file, "$"); @@ -6773,6 +7376,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "GetBindZoneID"), XS_Client_GetBindZoneID, file, "$$"); newXSproto(strcpy(buf, "GetCarriedMoney"), XS_Client_GetCarriedMoney, file, "$"); newXSproto(strcpy(buf, "GetCharacterFactionLevel"), XS_Client_GetCharacterFactionLevel, file, "$$"); + newXSproto(strcpy(buf, "GetClassBitmask"), XS_Client_GetClassBitmask, file, "$"); newXSproto(strcpy(buf, "GetClientMaxLevel"), XS_Client_GetClientMaxLevel, file, "$"); newXSproto(strcpy(buf, "GetClientVersion"), XS_Client_GetClientVersion, file, "$"); newXSproto(strcpy(buf, "GetClientVersionBit"), XS_Client_GetClientVersionBit, file, "$"); @@ -6787,6 +7391,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, "$"); @@ -6809,23 +7415,31 @@ 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, "GetLearnableDisciplines"), XS_Client_GetLearnableDisciplines, file, "$;$$"); + newXSproto(strcpy(buf, "GetLearnedDisciplines"), XS_Client_GetLearnedDisciplines, file, "$"); + newXSproto(strcpy(buf, "GetLockoutExpeditionUUID"), XS_Client_GetLockoutExpeditionUUID, file, "$$$"); newXSproto(strcpy(buf, "GetMaxEndurance"), XS_Client_GetMaxEndurance, file, "$"); + newXSproto(strcpy(buf, "GetMemmedSpells"), XS_Client_GetMemmedSpells, file, "$"); newXSproto(strcpy(buf, "GetModCharacterFactionLevel"), XS_Client_GetModCharacterFactionLevel, file, "$$"); newXSproto(strcpy(buf, "GetMoney"), XS_Client_GetMoney, file, "$$$"); newXSproto(strcpy(buf, "GetPVP"), XS_Client_GetPVP, file, "$"); newXSproto(strcpy(buf, "GetPVPPoints"), XS_Client_GetPVPPoints, file, "$"); + newXSproto(strcpy(buf, "GetRaceBitmask"), XS_Client_GetRaceBitmask, file, "$"); newXSproto(strcpy(buf, "GetRadiantCrystals"), XS_Client_GetRadiantCrystals, file, "$"); newXSproto(strcpy(buf, "GetRaid"), XS_Client_GetRaid, file, "$"); newXSproto(strcpy(buf, "GetRaidPoints"), XS_Client_GetRaidPoints, file, "$"); newXSproto(strcpy(buf, "GetRawItemAC"), XS_Client_GetRawItemAC, file, "$"); newXSproto(strcpy(buf, "GetRawSkill"), XS_Client_GetRawSkill, file, "$$"); + newXSproto(strcpy(buf, "GetScribeableSpells"), XS_Client_GetScribeableSpells, file, "$;$$"); + newXSproto(strcpy(buf, "GetScribedSpells"), XS_Client_GetScribedSpells, file, "$"); newXSproto(strcpy(buf, "GetSkillPoints"), XS_Client_GetSkillPoints, file, "$"); newXSproto(strcpy(buf, "GetSpellBookSlotBySpellID"), XS_Client_GetSpellBookSlotBySpellID, file, "$$"); + newXSproto(strcpy(buf, "GetSpellIDByBookSlot"), XS_Client_GetSpellIDByBookSlot, file, "$$"); newXSproto(strcpy(buf, "GetSpentAA"), XS_Client_GetSpentAA, file, "$$"); newXSproto(strcpy(buf, "GetStartZone"), XS_Client_GetStartZone, file, "$"); - newXSproto(strcpy(buf, "GetTargetRingX"), XS_Client_GetTargetRingX, file, "$$"); - newXSproto(strcpy(buf, "GetTargetRingY"), XS_Client_GetTargetRingY, file, "$$"); - newXSproto(strcpy(buf, "GetTargetRingZ"), XS_Client_GetTargetRingZ, file, "$$"); + newXSproto(strcpy(buf, "GetTargetRingX"), XS_Client_GetTargetRingX, file, "$"); + newXSproto(strcpy(buf, "GetTargetRingY"), XS_Client_GetTargetRingY, file, "$"); + newXSproto(strcpy(buf, "GetTargetRingZ"), XS_Client_GetTargetRingZ, file, "$"); newXSproto(strcpy(buf, "GetTaskActivityDoneCount"), XS_Client_GetTaskActivityDoneCount, file, "$$$"); newXSproto(strcpy(buf, "GetThirst"), XS_Client_GetThirst, file, "$$"); newXSproto(strcpy(buf, "GetTotalSecondsPlayed"), XS_Client_GetTotalSecondsPlayed, file, "$"); @@ -6835,6 +7449,8 @@ 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, "HasDisciplineLearned"), XS_Client_HasDisciplineLearned, 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, "$$"); @@ -6866,6 +7482,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, "$$"); @@ -6882,6 +7499,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, "$$"); @@ -6914,6 +7533,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "SetDeity"), XS_Client_SetDeity, file, "$$"); newXSproto(strcpy(buf, "SetDueling"), XS_Client_SetDueling, file, "$$"); newXSproto(strcpy(buf, "SetDuelTarget"), XS_Client_SetDuelTarget, file, "$$"); + newXSproto(strcpy(buf, "SetEbonCrystals"), XS_Client_SetEbonCrystals, file, "$$"); newXSproto(strcpy(buf, "SetEndurance"), XS_Client_SetEndurance, file, "$$"); newXSproto(strcpy(buf, "SetEXP"), XS_Client_SetEXP, file, "$$$;$"); newXSproto(strcpy(buf, "SetFactionLevel"), XS_Client_SetFactionLevel, file, "$$$$$$"); @@ -6926,6 +7546,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "SetMaterial"), XS_Client_SetMaterial, file, "$$$"); newXSproto(strcpy(buf, "SetPrimaryWeaponOrnamentation"), XS_Client_SetPrimaryWeaponOrnamentation, file, "$$"); newXSproto(strcpy(buf, "SetPVP"), XS_Client_SetPVP, file, "$$"); + newXSproto(strcpy(buf, "SetRadiantCrystals"), XS_Client_SetRadiantCrystals, file, "$$"); newXSproto(strcpy(buf, "SetSecondaryWeaponOrnamentation"), XS_Client_SetSecondaryWeaponOrnamentation, file, "$$"); newXSproto(strcpy(buf, "SetSkill"), XS_Client_SetSkill, file, "$$$"); newXSproto(strcpy(buf, "SetSkillPoints"), XS_Client_SetSkillPoints, 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_mob.cpp b/zone/perl_mob.cpp index f90435887..132719195 100644 --- a/zone/perl_mob.cpp +++ b/zone/perl_mob.cpp @@ -8594,6 +8594,60 @@ XS(XS_Mob_TryMoveAlong) { XSRETURN_EMPTY; } +XS(XS_Mob_GetClassName); +XS(XS_Mob_GetClassName) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Mob::GetClassName(THIS)"); + { + Mob* THIS; + Const_char *class_name; + dXSTARG; + + if (sv_derived_from(ST(0), "Mob")) { + IV tmp = SvIV((SV*)SvRV(ST(0))); + THIS = INT2PTR(Mob*, tmp); + } else + Perl_croak(aTHX_ "THIS is not of type Mob"); + + if (THIS == nullptr) + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); + + class_name = GetClassIDName(THIS->GetClass()); + sv_setpv(TARG, class_name); + XSprePUSH; + PUSHTARG; + } + XSRETURN(1); +} + +XS(XS_Mob_GetRaceName); +XS(XS_Mob_GetRaceName) { + dXSARGS; + if (items != 1) + Perl_croak(aTHX_ "Usage: Mob::GetRaceName(THIS)"); + { + Mob* THIS; + Const_char *race_name; + dXSTARG; + + if (sv_derived_from(ST(0), "Mob")) { + IV tmp = SvIV((SV*)SvRV(ST(0))); + THIS = INT2PTR(Mob*, tmp); + } else + Perl_croak(aTHX_ "THIS is not of type Mob"); + + if (THIS == nullptr) + Perl_croak(aTHX_ "THIS is nullptr, avoiding crash."); + + race_name = GetRaceIDName(THIS->GetRace()); + sv_setpv(TARG, race_name); + XSprePUSH; + PUSHTARG; + } + XSRETURN(1); +} + #ifdef __cplusplus extern "C" #endif @@ -8668,6 +8722,7 @@ XS(boot_Mob) { newXSproto(strcpy(buf, "GetBaseGender"), XS_Mob_GetBaseGender, file, "$"); newXSproto(strcpy(buf, "GetDeity"), XS_Mob_GetDeity, file, "$"); newXSproto(strcpy(buf, "GetRace"), XS_Mob_GetRace, file, "$"); + newXSproto(strcpy(buf, "GetRaceName"), XS_Mob_GetRaceName, file, "$"); newXSproto(strcpy(buf, "GetGender"), XS_Mob_GetGender, file, "$"); newXSproto(strcpy(buf, "GetTexture"), XS_Mob_GetTexture, file, "$"); newXSproto(strcpy(buf, "GetHelmTexture"), XS_Mob_GetHelmTexture, file, "$"); @@ -8682,6 +8737,7 @@ XS(boot_Mob) { newXSproto(strcpy(buf, "GetDrakkinTattoo"), XS_Mob_GetDrakkinTattoo, file, "$"); newXSproto(strcpy(buf, "GetDrakkinDetails"), XS_Mob_GetDrakkinDetails, file, "$"); newXSproto(strcpy(buf, "GetClass"), XS_Mob_GetClass, file, "$"); + newXSproto(strcpy(buf, "GetClassName"), XS_Mob_GetClassName, file, "$"); newXSproto(strcpy(buf, "GetLevel"), XS_Mob_GetLevel, file, "$"); newXSproto(strcpy(buf, "GetCleanName"), XS_Mob_GetCleanName, file, "$"); newXSproto(strcpy(buf, "GetTarget"), XS_Mob_GetTarget, file, "$"); 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/questmgr.cpp b/zone/questmgr.cpp index 82fae08ee..3c4522630 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -1088,174 +1088,47 @@ void QuestManager::permagender(int gender_id) { uint16 QuestManager::scribespells(uint8 max_level, uint8 min_level) { QuestManagerCurrentQuestVars(); int book_slot = initiator->GetNextAvailableSpellBookSlot(); - int spell_id = 0; - int count = 0; - - uint32 char_id = initiator->CharacterID(); - bool SpellGlobalRule = RuleB(Spells, EnableSpellGlobals); - bool SpellBucketRule = RuleB(Spells, EnableSpellBuckets); - bool SpellGlobalCheckResult = false; - bool SpellBucketCheckResult = false; - - for ( ; spell_id < SPDAT_RECORDS && book_slot < EQ::spells::SPELLBOOK_SIZE; ++spell_id) { - if (book_slot == -1) { - initiator->Message( - 13, - "Unable to scribe spell %s (%i) to spellbook: no more spell book slots available.", - ((spell_id >= 0 && spell_id < SPDAT_RECORDS) ? spells[spell_id].name : "Out-of-range"), - spell_id - ); - - break; - } - if (spell_id < 0 || spell_id >= SPDAT_RECORDS) { - initiator->Message(Chat::Red, "FATAL ERROR: Spell id out-of-range (id: %i, min: 0, max: %i)", spell_id, SPDAT_RECORDS); - return count; - } - if (book_slot < 0 || book_slot >= EQ::spells::SPELLBOOK_SIZE) { - initiator->Message(Chat::Red, "FATAL ERROR: Book slot out-of-range (slot: %i, min: 0, max: %i)", book_slot, EQ::spells::SPELLBOOK_SIZE); - return count; - } - - while (true) { - if (spells[spell_id].classes[WARRIOR] == 0) // check if spell exists + std::vector spell_ids = initiator->GetScribeableSpells(min_level, max_level); + int spell_count = spell_ids.size(); + if (spell_count > 0) { + for (auto spell_id : spell_ids) { + if (book_slot == -1) { + initiator->Message( + Chat::Red, + "Unable to scribe spell %s (%i) to Spell Book: Spell Book is Full.", spells[spell_id].name, spell_id + ); break; - if (spells[spell_id].classes[initiator->GetPP().class_ - 1] > max_level) // maximum level - break; - if (spells[spell_id].classes[initiator->GetPP().class_ - 1] < min_level) // minimum level - break; - if (spells[spell_id].skill == 52) - break; - if (spells[spell_id].effectid[EFFECT_COUNT - 1] == 10) - break; - - uint16 spell_id_ = (uint16)spell_id; - if ((spell_id_ != spell_id) || (spell_id != spell_id_)) { - initiator->Message(Chat::Red, "FATAL ERROR: Type conversion data loss with spell_id (%i != %u)", spell_id, spell_id_); - return count; } - - if (!IsDiscipline(spell_id_) && !initiator->HasSpellScribed(spell_id)) { // isn't a discipline & we don't already have it scribed - if (SpellGlobalRule) { - // bool to see if the character has the required QGlobal to scribe it if one exists in the Spell_Globals table - SpellGlobalCheckResult = initiator->SpellGlobalCheck(spell_id_, char_id); - if (SpellGlobalCheckResult) { - initiator->ScribeSpell(spell_id_, book_slot); - ++count; - } - } - else if (SpellBucketRule) { - // bool to see if the character has the required bucket to train it if one exists in the spell_buckets table - SpellBucketCheckResult = initiator->SpellBucketCheck(spell_id_, char_id); - if (SpellBucketCheckResult) { - initiator->ScribeSpell(spell_id_, book_slot); - ++count; - } - } - else { - initiator->ScribeSpell(spell_id_, book_slot); - ++count; - } - } - - break; + initiator->ScribeSpell(spell_id, book_slot); + book_slot = initiator->GetNextAvailableSpellBookSlot(book_slot); } - - book_slot = initiator->GetNextAvailableSpellBookSlot(book_slot); } - - return count; // how many spells were scribed successfully + return spell_count; } uint16 QuestManager::traindiscs(uint8 max_level, uint8 min_level) { QuestManagerCurrentQuestVars(); - int spell_id = 0; - int count = 0; - - uint32 char_id = initiator->CharacterID(); - bool SpellGlobalRule = RuleB(Spells, EnableSpellGlobals); - bool SpellBucketRule = RuleB(Spells, EnableSpellBuckets); - bool SpellGlobalCheckResult = false; - bool SpellBucketCheckResult = false; - - bool change = false; - - for( ; spell_id < SPDAT_RECORDS; ++spell_id) { - if (spell_id < 0 || spell_id >= SPDAT_RECORDS) { - initiator->Message(Chat::Red, "FATAL ERROR: Spell id out-of-range (id: %i, min: 0, max: %i)", spell_id, SPDAT_RECORDS); - return count; - } - - while (true) { - if (spells[spell_id].classes[WARRIOR] == 0) // check if spell exists - break; - if (spells[spell_id].classes[initiator->GetPP().class_ - 1] > max_level) // maximum level - break; - if (spells[spell_id].classes[initiator->GetPP().class_ - 1] < min_level) // minimum level - break; - if (spells[spell_id].skill == 52) - break; - if (RuleB(Spells, UseCHAScribeHack) && spells[spell_id].effectid[EFFECT_COUNT - 1] == 10) - break; - - uint16 spell_id_ = (uint16)spell_id; - if ((spell_id_ != spell_id) || (spell_id != spell_id_)) { - initiator->Message(Chat::Red, "FATAL ERROR: Type conversion data loss with spell_id (%i != %u)", spell_id, spell_id_); - return count; - } - - if (!IsDiscipline(spell_id_)) - break; - - for (uint32 r = 0; r < MAX_PP_DISCIPLINES; r++) { - if (initiator->GetPP().disciplines.values[r] == spell_id_) { - initiator->Message(Chat::Red, "You already know this discipline."); - break; // continue the 1st loop - } - else if (initiator->GetPP().disciplines.values[r] == 0) { - if (SpellGlobalRule) { - // bool to see if the character has the required QGlobal to train it if one exists in the Spell_Globals table - SpellGlobalCheckResult = initiator->SpellGlobalCheck(spell_id_, char_id); - if (SpellGlobalCheckResult) { - initiator->GetPP().disciplines.values[r] = spell_id_; - database.SaveCharacterDisc(char_id, r, spell_id_); - change = true; - initiator->Message(Chat::White, "You have learned a new discipline!"); - ++count; // success counter - } - break; // continue the 1st loop - } - else if (SpellBucketRule) { - // bool to see if the character has the required bucket to train it if one exists in the spell_buckets table - SpellBucketCheckResult = initiator->SpellBucketCheck(spell_id_, char_id); - if (SpellBucketCheckResult) { - initiator->GetPP().disciplines.values[r] = spell_id_; - database.SaveCharacterDisc(char_id, r, spell_id_); - change = true; - initiator->Message(Chat::White, "You have learned a new discipline!"); - ++count; - } - break; - } - else { - initiator->GetPP().disciplines.values[r] = spell_id_; - database.SaveCharacterDisc(char_id, r, spell_id_); - change = true;; - initiator->Message(Chat::White, "You have learned a new discipline!"); - ++count; // success counter - break; // continue the 1st loop - } + int character_id = initiator->CharacterID(); + std::vector spell_ids = initiator->GetLearnableDisciplines(min_level, max_level); + int discipline_count = spell_ids.size(); + bool discipline_learned = false; + if (discipline_count > 0) { + for (auto spell_id : spell_ids) { + for (uint32 index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (initiator->GetPP().disciplines.values[index] == 0) { + initiator->GetPP().disciplines.values[index] = spell_id; + database.SaveCharacterDisc(character_id, index, spell_id); + initiator->Message(Chat::White, "You have learned a new discipline!"); + discipline_learned = true; } } - - break; } } - if (change) + if (discipline_learned) initiator->SendDisciplineUpdate(); - return count; // how many disciplines were learned successfully + return discipline_count; } void QuestManager::unscribespells() { @@ -2580,6 +2453,24 @@ void QuestManager::we(int type, const char *str) { worldserver.SendEmoteMessage(0, 0, type, str); } +void QuestManager::message(int color, const char *message) { + QuestManagerCurrentQuestVars(); + if (!initiator) + return; + + initiator->Message(color, message); +} + +void QuestManager::whisper(const char *message) { + QuestManagerCurrentQuestVars(); + if (!initiator || !owner) + return; + + std::string mob_name = owner->GetCleanName(); + std::string new_message = fmt::format("{} whispers, '{}'", mob_name, message); + initiator->Message(315, new_message.c_str()); +} + int QuestManager::getlevel(uint8 type) { QuestManagerCurrentQuestVars(); @@ -2729,6 +2620,43 @@ int QuestManager::countitem(uint32 item_id) { return quantity; } +void QuestManager::removeitem(uint32 item_id, uint32 quantity) { + QuestManagerCurrentQuestVars(); + EQ::ItemInstance *item = nullptr; + static const int16 slots[][2] = { + { EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END }, + { EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END }, + { EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END}, + { EQ::invslot::BANK_BEGIN, EQ::invslot::BANK_END }, + { EQ::invbag::BANK_BAGS_BEGIN, EQ::invbag::BANK_BAGS_END }, + { EQ::invslot::SHARED_BANK_BEGIN, EQ::invslot::SHARED_BANK_END }, + { EQ::invbag::SHARED_BANK_BAGS_BEGIN, EQ::invbag::SHARED_BANK_BAGS_END }, + }; + int removed_count = 0; + const size_t size = sizeof(slots) / sizeof(slots[0]); + for (int slot_index = 0; slot_index < size; ++slot_index) { + for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) { + if (removed_count == quantity) + break; + + item = initiator->GetInv().GetItem(slot_id); + if (item && item->GetID() == item_id) { + int stack_size = item->IsStackable() ? item->GetCharges() : 1; + if ((removed_count + stack_size) <= quantity) { + removed_count += stack_size; + initiator->DeleteItemInInventory(slot_id, stack_size, true); + } else { + int amount_left = (quantity - removed_count); + if (amount_left > 0 && stack_size >= amount_left) { + removed_count += amount_left; + initiator->DeleteItemInInventory(slot_id, amount_left, true); + } + } + } + } + } +} + void QuestManager::UpdateSpawnTimer(uint32 id, uint32 newTime) { bool found = false; @@ -3272,13 +3200,11 @@ int32 QuestManager::GetZoneID(const char *zone) { return static_cast(ZoneID(zone)); } -const char* QuestManager::GetZoneLongName(const char *zone) { - char *long_name; - content_db.GetZoneLongName(zone, &long_name); - std::string ln = long_name; - safe_delete_array(long_name); - - return ln.c_str(); +std::string QuestManager::GetZoneLongName(std::string zone_short_name) +{ + return zone_store.GetZoneLongName( + zone_store.GetZoneID(zone_short_name) + ); } void QuestManager::CrossZoneAssignTaskByCharID(int character_id, uint32 task_id, bool enforce_level_requirement) { @@ -3958,7 +3884,7 @@ void QuestManager::CrossZoneUpdateActivityByGuildID(int guild_id, uint32 task_id } } -void QuestManager::WorldWideAssignTask(uint32 task_id, bool enforce_level_requirement, uint8 min_status, uint8 max_status) { +void QuestManager::WorldWideAssignTask(uint32 task_id, bool enforce_level_requirement, uint8 min_status, uint8 max_status) { QuestManagerCurrentQuestVars(); if (initiator && owner) { auto pack = new ServerPacket(ServerOP_WWAssignTask, sizeof(WWAssignTask_Struct)); diff --git a/zone/questmgr.h b/zone/questmgr.h index 9ea34e1e1..a5620941d 100644 --- a/zone/questmgr.h +++ b/zone/questmgr.h @@ -223,10 +223,13 @@ public: void clearspawntimers(); void ze(int type, const char *str); void we(int type, const char *str); + void message(int color, const char *message); + void whisper(const char *message); int getlevel(uint8 type); int collectitems(uint32 item_id, bool remove); int collectitems_processSlot(int16 slot_id, uint32 item_id, bool remove); int countitem(uint32 item_id); + void removeitem(uint32 item_id, uint32 quantity = 1); std::string getitemname(uint32 item_id); void enabletitle(int titleset); bool checktitle(int titlecheck); @@ -280,7 +283,7 @@ public: void SendMail(const char *to, const char *from, const char *subject, const char *message); uint16 CreateDoor( const char* model, float x, float y, float z, float heading, uint8 opentype, uint16 size); int32 GetZoneID(const char *zone); - const char *GetZoneLongName(const char *zone); + static std::string GetZoneLongName(std::string zone_short_name); void CrossZoneAssignTaskByCharID(int character_id, uint32 task_id, bool enforce_level_requirement = false); void CrossZoneAssignTaskByGroupID(int group_id, uint32 task_id, bool enforce_level_requirement = false); void CrossZoneAssignTaskByRaidID(int raid_id, uint32 task_id, bool enforce_level_requirement = false); 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/spell_effects.cpp b/zone/spell_effects.cpp index 8c3599aca..b6c2f9cf8 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -774,6 +774,13 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove caster->SetPet(this); SetOwnerID(caster->GetID()); SetPetOrder(SPO_Follow); + SetAppearance(eaStanding); + // Client has saved previous pet sit/stand - make all new pets + // stand on charm. + if (caster->IsClient()) { + caster->CastToClient()->SetPetCommandState(PET_BUTTON_SIT,0); + } + SetPetType(petCharmed); if(caster->IsClient()){ diff --git a/zone/spells.cpp b/zone/spells.cpp index ea925ecda..742878bbc 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -5090,6 +5090,13 @@ void Client::UnmemSpellAll(bool update_client) UnmemSpell(i, update_client); } +uint32 Client::GetSpellIDByBookSlot(int book_slot) { + if (book_slot <= EQ::spells::SPELLBOOK_SIZE) { + return GetSpellByBookSlot(book_slot); + } + return -1; //default +} + uint16 Client::FindMemmedSpellBySlot(int slot) { if (m_pp.mem_spells[slot] != 0xFFFFFFFF) return m_pp.mem_spells[slot]; diff --git a/zone/string_ids.h b/zone/string_ids.h index b7ee5ddd4..25c62d6b2 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. @@ -364,6 +403,9 @@ #define LDON_NO_LOCKPICK 7564 //You must have a lock pick in your inventory to do this. #define LDON_WAS_NOT_LOCKED 7565 //%1 was not locked. #define LDON_WAS_NOT_TRAPPED 7566 //%1 was not trapped +#define GAIN_SINGLE_AA_SINGLE_AA 8019 //You have gained an ability point! You now have %1 ability point. +#define GAIN_SINGLE_AA_MULTI_AA 8020 //You have gained an ability point! You now have %1 ability points. +#define GAIN_MULTI_AA_MULTI_AA 8021 //You have gained %1 ability point(s)! You now have %2 ability point(s). #define GAIN_GROUP_LEADERSHIP_POINT 8585 // #define GAIN_RAID_LEADERSHIP_POINT 8589 // #define MAX_GROUP_LEADERSHIP_POINTS 8584 // diff --git a/zone/trading.cpp b/zone/trading.cpp index 632fd4fb7..1d75bd941 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -2648,6 +2648,11 @@ void Client::SellToBuyer(const EQApplicationPacket *app) { return; } + if(item->IsClassBag()) { + Message(Chat::Red, "That item is a Bag."); + return; + } + if(!item->Stackable) { for(uint32 i = 0; i < Quantity; i++) { @@ -3001,29 +3006,27 @@ void Client::UpdateBuyLine(const EQApplicationPacket *app) { LogTrading("UpdateBuyLine: Char: [{}] BuySlot: [{}] ItemID [{}] [{}] Quantity [{}] Toggle: [{}] Price [{}] ItemCount [{}] LoreConflict [{}]", GetName(), BuySlot, ItemID, item->Name, Quantity, ToggleOnOff, Price, ItemCount, LoreConflict); - if((item->NoDrop != 0) && !LoreConflict && (Quantity > 0) && HasMoney(Quantity * Price) && ToggleOnOff && (ItemCount == 0)) { + if((item->NoDrop != 0) && (!item->IsClassBag())&& !LoreConflict && (Quantity > 0) && HasMoney(Quantity * Price) && ToggleOnOff && (ItemCount == 0)) { LogTrading("Adding to database"); database.AddBuyLine(CharacterID(), BuySlot, ItemID, ItemName, Quantity, Price); QueuePacket(app); } else { - if(ItemCount > 0) + if(ItemCount > 0) { Message(Chat::Red, "Buy line %s disabled as Item Compensation is not currently supported.", ItemName); - - else if(Quantity <= 0) + } else if(Quantity <= 0) { Message(Chat::Red, "Buy line %s disabled as the quantity is invalid.", ItemName); - - else if(LoreConflict) + } else if(LoreConflict) { Message(Chat::Red, "Buy line %s disabled as the item is LORE and you have one already.", ItemName); - - else if(item->NoDrop == 0) + } else if(item->NoDrop == 0) { Message(Chat::Red, "Buy line %s disabled as the item is NODROP.", ItemName); - - else if(ToggleOnOff) + } else if(item->IsClassBag()) { + Message(Chat::Red, "Buy line %s disabled as the item is a Bag.", ItemName); + } else if(ToggleOnOff) { Message(Chat::Red, "Buy line %s disabled due to insufficient funds.", ItemName); - - else + } else { database.RemoveBuyLine(CharacterID(), BuySlot); + } auto outapp = new EQApplicationPacket(OP_Barter, 936); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index d4fa5e572..eea45d057 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" @@ -876,7 +877,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) if (gl->zoneid == zone->GetZoneID() && gl->instance_id == zone->GetInstanceID()) break; - entity_list.SendGroupLeave(gl->gid, gl->member_name); + entity_list.SendGroupLeave(gl->gid, gl->member_name, gl->checkleader); } break; } @@ -1113,6 +1114,17 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } + case ServerOP_ChangeGroupLeader: { + ServerGroupLeader_Struct *fgu = (ServerGroupLeader_Struct *) pack->pBuffer; + if (zone) { + if (fgu->zoneid == zone->GetZoneID()) { + break; + } + entity_list.SendGroupLeader(fgu->gid, fgu->leader_name, fgu->oldleader_name); + } + break; + } + case ServerOP_OOZGroupMessage: { ServerGroupChannelMessage_Struct* gcm = (ServerGroupChannelMessage_Struct*)pack->pBuffer; if (zone) { @@ -2846,7 +2858,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 +2892,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 +3315,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..9efea0a84 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" @@ -274,7 +275,9 @@ bool Zone::LoadZoneObjects() position.y = data.y; position.z = data.z; - data.z = zone->zonemap->FindBestZ(position, nullptr); + if (zone->HasMap()) { + data.z = zone->zonemap->FindBestZ(position, nullptr); + } EQ::ItemInstance *inst = nullptr; // FatherNitwit: this dosent seem to work... @@ -1183,6 +1186,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 +1493,17 @@ 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 + } + + // instance shutting down, move corpses to graveyard or non-instanced zone at same coords + entity_list.MovePlayerCorpsesToGraveyard(true); + + entity_list.GateAllClientsToSafeReturn(); database.DeleteInstance(GetInstanceID()); Instance_Shutdown_Timer = new Timer(20000); //20 seconds } @@ -1497,20 +1513,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 +2724,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/zonedb.cpp b/zone/zonedb.cpp index bc1b542dd..bbd929450 100755 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -105,68 +105,69 @@ bool ZoneDatabase::GetZoneCFG( std::string query = StringFormat( "SELECT " - "ztype, " // 0 - "fog_red, " // 1 - "fog_green, " // 2 - "fog_blue, " // 3 - "fog_minclip, " // 4 - "fog_maxclip, " // 5 - "fog_red2, " // 6 - "fog_green2, " // 7 - "fog_blue2, " // 8 - "fog_minclip2, " // 9 - "fog_maxclip2, " // 10 - "fog_red3, " // 11 - "fog_green3, " // 12 - "fog_blue3, " // 13 - "fog_minclip3, " // 14 - "fog_maxclip3, " // 15 - "fog_red4, " // 16 - "fog_green4, " // 17 - "fog_blue4, " // 18 - "fog_minclip4, " // 19 - "fog_maxclip4, " // 20 - "fog_density, " // 21 - "sky, " // 22 - "zone_exp_multiplier, " // 23 - "safe_x, " // 24 - "safe_y, " // 25 - "safe_z, " // 26 - "underworld, " // 27 - "minclip, " // 28 - "maxclip, " // 29 - "time_type, " // 30 - "canbind, " // 31 - "cancombat, " // 32 - "canlevitate, " // 33 - "castoutdoor, " // 34 - "hotzone, " // 35 - "ruleset, " // 36 - "suspendbuffs, " // 37 - "map_file_name, " // 38 - "short_name, " // 39 - "rain_chance1, " // 40 - "rain_chance2, " // 41 - "rain_chance3, " // 42 - "rain_chance4, " // 43 - "rain_duration1, " // 44 - "rain_duration2, " // 45 - "rain_duration3, " // 46 - "rain_duration4, " // 47 - "snow_chance1, " // 48 - "snow_chance2, " // 49 - "snow_chance3, " // 50 - "snow_chance4, " // 51 - "snow_duration1, " // 52 - "snow_duration2, " // 53 - "snow_duration3, " // 54 - "snow_duration4, " // 55 - "gravity, " // 56 - "fast_regen_hp, " // 57 - "fast_regen_mana, " // 58 - "fast_regen_endurance, " // 59 - "npc_max_aggro_dist, " // 60 - "max_movement_update_range " // 61 + "ztype, " // 0 + "fog_red, " // 1 + "fog_green, " // 2 + "fog_blue, " // 3 + "fog_minclip, " // 4 + "fog_maxclip, " // 5 + "fog_red2, " // 6 + "fog_green2, " // 7 + "fog_blue2, " // 8 + "fog_minclip2, " // 9 + "fog_maxclip2, " // 10 + "fog_red3, " // 11 + "fog_green3, " // 12 + "fog_blue3, " // 13 + "fog_minclip3, " // 14 + "fog_maxclip3, " // 15 + "fog_red4, " // 16 + "fog_green4, " // 17 + "fog_blue4, " // 18 + "fog_minclip4, " // 19 + "fog_maxclip4, " // 20 + "fog_density, " // 21 + "sky, " // 22 + "zone_exp_multiplier, " // 23 + "safe_x, " // 24 + "safe_y, " // 25 + "safe_z, " // 26 + "underworld, " // 27 + "minclip, " // 28 + "maxclip, " // 29 + "time_type, " // 30 + "canbind, " // 31 + "cancombat, " // 32 + "canlevitate, " // 33 + "castoutdoor, " // 34 + "hotzone, " // 35 + "ruleset, " // 36 + "suspendbuffs, " // 37 + "map_file_name, " // 38 + "short_name, " // 39 + "rain_chance1, " // 40 + "rain_chance2, " // 41 + "rain_chance3, " // 42 + "rain_chance4, " // 43 + "rain_duration1, " // 44 + "rain_duration2, " // 45 + "rain_duration3, " // 46 + "rain_duration4, " // 47 + "snow_chance1, " // 48 + "snow_chance2, " // 49 + "snow_chance3, " // 50 + "snow_chance4, " // 51 + "snow_duration1, " // 52 + "snow_duration2, " // 53 + "snow_duration3, " // 54 + "snow_duration4, " // 55 + "gravity, " // 56 + "fast_regen_hp, " // 57 + "fast_regen_mana, " // 58 + "fast_regen_endurance, " // 59 + "npc_max_aggro_dist, " // 60 + "max_movement_update_range, " // 61 + "underworld_teleport_index " // 62 "FROM zone WHERE zoneidnumber = %i AND version = %i %s", zoneid, instance_id, @@ -218,6 +219,7 @@ bool ZoneDatabase::GetZoneCFG( zone_data->FastRegenMana = atoi(row[58]); zone_data->FastRegenEndurance = atoi(row[59]); zone_data->NPCAggroMaxDist = atoi(row[60]); + zone_data->underworld_teleport_index = atoi(row[62]); int bindable = 0; bindable = atoi(row[31]); @@ -4298,6 +4300,18 @@ uint32 ZoneDatabase::SendCharacterCorpseToGraveyard(uint32 dbid, uint32 zone_id, return dbid; } +void ZoneDatabase::SendCharacterCorpseToNonInstance(uint32 corpse_db_id) +{ + if (corpse_db_id != 0) + { + auto query = fmt::format(SQL( + UPDATE character_corpses SET instance_id = 0 WHERE id = {}; + ), corpse_db_id); + + QueryDatabase(query); + } +} + uint32 ZoneDatabase::GetCharacterCorpseDecayTimer(uint32 corpse_db_id){ std::string query = StringFormat("SELECT(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(time_of_death)) FROM `character_corpses` WHERE `id` = %d AND NOT `time_of_death` = 0", corpse_db_id); auto results = QueryDatabase(query); diff --git a/zone/zonedb.h b/zone/zonedb.h index bb6311eb0..569819100 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -376,6 +376,7 @@ public: uint32 GetCharacterCorpseID(uint32 char_id, uint8 corpse); uint32 GetCharacterCorpseItemAt(uint32 corpse_id, uint16 slotid); uint32 GetPlayerCorpseTimeLeft(uint8 corpse, uint8 type); + void SendCharacterCorpseToNonInstance(uint32 corpse_db_id); /* Faction */ bool GetNPCFactionList(uint32 npcfaction_id, int32* faction_id, int32* value, uint8* temp, int32* primary_faction = 0); 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