From c82f1b9afc9bf6be74afcff052d9516d0d2aa80b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Wed, 8 Jan 2025 17:41:16 -0600 Subject: [PATCH] [Zone] Implement zone player count sharding (#4536) * [Zone] Implement zone player count sharding * Update client.cpp * Update database_instances.cpp * You must request a shard change from the zone you are currently in. * // zone sharding * You cannot request a shard change while in combat. * Query adjustment * Use safe coords * Changes * Fixes to instance query * Push * Push * Final push * Update client.cpp * Update eq_packet_structs.h * Remove pick menu * Comment * Update character_data_repository.h * Update zoning.cpp --------- Co-authored-by: Kinglykrab --- common/database/database_update_manifest.cpp | 12 ++ common/database_instances.cpp | 10 +- common/emu_oplist.h | 6 +- common/eq_packet_structs.h | 20 +++ common/opcode_dispatch.h | 1 + .../repositories/base/base_zone_repository.h | 12 ++ .../repositories/character_data_repository.h | 87 +++++++++++ common/ruletypes.h | 1 + common/version.h | 2 +- utils/patches/patch_RoF2.conf | 5 +- zone/client.cpp | 64 +++++++- zone/client.h | 1 + zone/client_packet.cpp | 17 +++ zone/client_packet.h | 1 + zone/command.cpp | 2 + zone/command.h | 1 + zone/gm_commands/zone_shard.cpp | 137 ++++++++++++++++++ zone/lua_client.cpp | 16 +- zone/lua_client.h | 1 + zone/lua_packet.cpp | 3 +- zone/perl_client.cpp | 6 + zone/zoning.cpp | 60 ++++++++ 22 files changed, 443 insertions(+), 22 deletions(-) create mode 100644 zone/gm_commands/zone_shard.cpp diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index fde8820f0..cd484122b 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5791,6 +5791,18 @@ ALTER TABLE `trader` .match = "float", .sql = R"( ALTER TABLE `npc_types` MODIFY COLUMN `walkspeed` float NOT NULL DEFAULT 0; +)", + .content_schema_update = true + }, + ManifestEntry{ + .version = 9288, + .description = "2024_11_10_zone_player_partitioning.sql", + .check = "SHOW CREATE TABLE `zone`", + .condition = "missing", + .match = "shard_at_player_count", + .sql = R"( +ALTER TABLE `zone` +ADD COLUMN `shard_at_player_count` int(11) NULL DEFAULT 0 AFTER `seconds_before_idle`; )", .content_schema_update = true } diff --git a/common/database_instances.cpp b/common/database_instances.cpp index e9068a534..2e3b4fd0a 100644 --- a/common/database_instances.cpp +++ b/common/database_instances.cpp @@ -114,7 +114,9 @@ bool Database::CheckInstanceExpired(uint16 instance_id) timeval tv{}; gettimeofday(&tv, nullptr); - return (i.start_time + i.duration) <= tv.tv_sec; + // Use uint64_t for the addition to prevent overflow + uint64_t expiration_time = static_cast(i.start_time) + static_cast(i.duration); + return expiration_time <= tv.tv_sec; } bool Database::CreateInstance(uint16 instance_id, uint32 zone_id, uint32 version, uint32 duration) @@ -469,15 +471,13 @@ void Database::AssignRaidToInstance(uint32 raid_id, uint32 instance_id) void Database::DeleteInstance(uint16 instance_id) { + // I'm not sure why this isn't in here but we should add it in a later change and make sure it's tested + // InstanceListRepository::DeleteWhere(*this, fmt::format("id = {}", instance_id)); InstanceListPlayerRepository::DeleteWhere(*this, fmt::format("id = {}", instance_id)); - RespawnTimesRepository::DeleteWhere(*this, fmt::format("instance_id = {}", instance_id)); - SpawnConditionValuesRepository::DeleteWhere(*this, fmt::format("instance_id = {}", instance_id)); - DynamicZoneMembersRepository::DeleteByInstance(*this, instance_id); DynamicZonesRepository::DeleteWhere(*this, fmt::format("instance_id = {}", instance_id)); - CharacterCorpsesRepository::BuryInstance(*this, instance_id); } diff --git a/common/emu_oplist.h b/common/emu_oplist.h index aa8190720..e41e4994b 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -35,7 +35,7 @@ N(OP_AltCurrencyMerchantRequest), N(OP_AltCurrencyPurchase), N(OP_AltCurrencyReclaim), N(OP_AltCurrencySell), -N(OP_AltCurrencySellSelection), // Used by eqstr_us.txt 8066, 8068, 8069 +N(OP_AltCurrencySellSelection), // Used by eqstr_us.txt 8066, 8068, 8069 N(OP_Animation), N(OP_AnnoyingZoneUnknown), N(OP_ApplyPoison), @@ -400,6 +400,8 @@ N(OP_PetitionSearchText), N(OP_PetitionUnCheckout), N(OP_PetitionUpdate), N(OP_PickPocket), +N(OP_PickZone), +N(OP_PickZoneWindow), N(OP_PlayerProfile), N(OP_PlayerStateAdd), N(OP_PlayerStateRemove), @@ -514,7 +516,7 @@ N(OP_ShopPlayerSell), N(OP_ShopSendParcel), N(OP_ShopDeleteParcel), N(OP_ShopRespondParcel), -N(OP_ShopRetrieveParcel), +N(OP_ShopRetrieveParcel), N(OP_ShopParcelIcon), N(OP_ShopRequest), N(OP_SimpleMessage), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 33e860654..e4e3c28f8 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -6440,6 +6440,26 @@ struct BuylineItemDetails_Struct { uint32 item_quantity; }; +struct PickZoneEntry_Struct { + int16 zone_id; + int16 unknown; + int32 player_count; + int32 instance_id; +}; + +struct PickZoneWindow_Struct { + char padding000[64]; + int64 session_id; + int8 option_count; + char padding073[23]; + PickZoneEntry_Struct entries[10]; +}; + +struct PickZone_Struct { + int64 session_id; + int32 selection_id; +}; + // Restore structure packing to default #pragma pack() diff --git a/common/opcode_dispatch.h b/common/opcode_dispatch.h index a19cb79a3..38212acb9 100644 --- a/common/opcode_dispatch.h +++ b/common/opcode_dispatch.h @@ -250,6 +250,7 @@ IN(OP_TraderBuy, TraderBuy_Struct); IN(OP_Trader, Trader_ShowItems_Struct); IN(OP_GMFind, GMSummon_Struct); IN(OP_PickPocket, PickPocket_Struct); +IN(OP_PickZone, PickZone_Struct); IN(OP_Bind_Wound, BindWound_Struct); INr(OP_TrackTarget); INr(OP_Track); diff --git a/common/repositories/base/base_zone_repository.h b/common/repositories/base/base_zone_repository.h index 0e23c0b85..3ba11466d 100644 --- a/common/repositories/base/base_zone_repository.h +++ b/common/repositories/base/base_zone_repository.h @@ -117,6 +117,7 @@ public: int32_t min_lava_damage; uint8_t idle_when_empty; uint32_t seconds_before_idle; + int32_t shard_at_player_count; }; static std::string PrimaryKey() @@ -225,6 +226,7 @@ public: "min_lava_damage", "idle_when_empty", "seconds_before_idle", + "shard_at_player_count", }; } @@ -329,6 +331,7 @@ public: "min_lava_damage", "idle_when_empty", "seconds_before_idle", + "shard_at_player_count", }; } @@ -467,6 +470,7 @@ public: e.min_lava_damage = 10; e.idle_when_empty = 1; e.seconds_before_idle = 60; + e.shard_at_player_count = 0; return e; } @@ -601,6 +605,7 @@ public: e.min_lava_damage = row[95] ? static_cast(atoi(row[95])) : 10; e.idle_when_empty = row[96] ? static_cast(strtoul(row[96], nullptr, 10)) : 1; e.seconds_before_idle = row[97] ? static_cast(strtoul(row[97], nullptr, 10)) : 60; + e.shard_at_player_count = row[98] ? static_cast(atoi(row[98])) : 0; return e; } @@ -731,6 +736,7 @@ public: v.push_back(columns[95] + " = " + std::to_string(e.min_lava_damage)); v.push_back(columns[96] + " = " + std::to_string(e.idle_when_empty)); v.push_back(columns[97] + " = " + std::to_string(e.seconds_before_idle)); + v.push_back(columns[98] + " = " + std::to_string(e.shard_at_player_count)); auto results = db.QueryDatabase( fmt::format( @@ -850,6 +856,7 @@ public: v.push_back(std::to_string(e.min_lava_damage)); v.push_back(std::to_string(e.idle_when_empty)); v.push_back(std::to_string(e.seconds_before_idle)); + v.push_back(std::to_string(e.shard_at_player_count)); auto results = db.QueryDatabase( fmt::format( @@ -977,6 +984,7 @@ public: v.push_back(std::to_string(e.min_lava_damage)); v.push_back(std::to_string(e.idle_when_empty)); v.push_back(std::to_string(e.seconds_before_idle)); + v.push_back(std::to_string(e.shard_at_player_count)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -1108,6 +1116,7 @@ public: e.min_lava_damage = row[95] ? static_cast(atoi(row[95])) : 10; e.idle_when_empty = row[96] ? static_cast(strtoul(row[96], nullptr, 10)) : 1; e.seconds_before_idle = row[97] ? static_cast(strtoul(row[97], nullptr, 10)) : 60; + e.shard_at_player_count = row[98] ? static_cast(atoi(row[98])) : 0; all_entries.push_back(e); } @@ -1230,6 +1239,7 @@ public: e.min_lava_damage = row[95] ? static_cast(atoi(row[95])) : 10; e.idle_when_empty = row[96] ? static_cast(strtoul(row[96], nullptr, 10)) : 1; e.seconds_before_idle = row[97] ? static_cast(strtoul(row[97], nullptr, 10)) : 60; + e.shard_at_player_count = row[98] ? static_cast(atoi(row[98])) : 0; all_entries.push_back(e); } @@ -1402,6 +1412,7 @@ public: v.push_back(std::to_string(e.min_lava_damage)); v.push_back(std::to_string(e.idle_when_empty)); v.push_back(std::to_string(e.seconds_before_idle)); + v.push_back(std::to_string(e.shard_at_player_count)); auto results = db.QueryDatabase( fmt::format( @@ -1522,6 +1533,7 @@ public: v.push_back(std::to_string(e.min_lava_damage)); v.push_back(std::to_string(e.idle_when_empty)); v.push_back(std::to_string(e.seconds_before_idle)); + v.push_back(std::to_string(e.shard_at_player_count)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/character_data_repository.h b/common/repositories/character_data_repository.h index 249218279..74f884cf3 100644 --- a/common/repositories/character_data_repository.h +++ b/common/repositories/character_data_repository.h @@ -80,6 +80,93 @@ public: return l.empty() ? CharacterDataRepository::NewEntity() : l.front(); } + + struct InstancePlayerCount { + int32_t instance_id; + uint32_t zone_id; + uint32_t player_count; + }; + + static std::vector GetInstanceZonePlayerCounts(Database& db, int zone_id) { + std::vector zone_player_counts; + + uint64_t shard_instance_duration = 3155760000; + + auto query = fmt::format(SQL( + SELECT + zone_id, + 0 AS instance_id, + COUNT(id) AS player_count + FROM + character_data + WHERE + zone_instance = 0 + AND zone_id = {} + AND last_login >= UNIX_TIMESTAMP(NOW()) - 600 + GROUP BY + zone_id + ORDER BY + zone_id, player_count DESC + ), zone_id); + + auto results = db.QueryDatabase(query); + for (auto row = results.begin(); row != results.end(); ++row) { + InstancePlayerCount e{}; + e.zone_id = std::stoi(row[0]); + e.instance_id = 0; + e.player_count = std::stoi(row[2]); + zone_player_counts.push_back(e); + } + + if (zone_player_counts.empty()) { + InstancePlayerCount e{}; + e.zone_id = zone_id; + e.instance_id = 0; + e.player_count = 0; + zone_player_counts.push_back(e); + } + + // duration 3155760000 is for shards explicitly + query = fmt::format( + SQL( + SELECT + i.id AS instance_id, + i.zone AS zone_id, + COUNT(c.id) AS player_count + FROM + instance_list i + LEFT JOIN + character_data c + ON + i.zone = c.zone_id + AND i.id = c.zone_instance + AND c.last_login >= UNIX_TIMESTAMP(NOW()) - 600 + AND (i.start_time + i.duration >= UNIX_TIMESTAMP(NOW()) OR i.never_expires = 0) + AND i.duration = {} + WHERE + i.zone IS NOT NULL AND i.zone = {} + GROUP BY + i.id, i.zone, i.version + ORDER BY + i.id ASC; + ), shard_instance_duration, zone_id + ); + + results = db.QueryDatabase(query); + if (!results.Success() || results.RowCount() == 0) { + return zone_player_counts; + } + + for (auto row = results.begin(); row != results.end(); ++row) { + InstancePlayerCount e{}; + e.instance_id = std::stoi(row[0]); + e.zone_id = std::stoi(row[1]); + e.player_count = std::stoi(row[2]); + zone_player_counts.push_back(e); + } + + return zone_player_counts; + } }; #endif //EQEMU_CHARACTER_DATA_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 60560345e..c0682979b 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -372,6 +372,7 @@ RULE_INT(Zone, FishingChance, 399, "Chance of fishing from zone table vs global RULE_BOOL(Zone, AllowCrossZoneSpellsOnBots, false, "Set to true to allow cross zone spells (cast/remove) to affect bots") RULE_BOOL(Zone, AllowCrossZoneSpellsOnMercs, false, "Set to true to allow cross zone spells (cast/remove) to affect mercenaries") RULE_BOOL(Zone, AllowCrossZoneSpellsOnPets, false, "Set to true to allow cross zone spells (cast/remove) to affect pets") +RULE_BOOL(Zone, ZoneShardQuestMenuOnly, false, "Set to true if you only want quests to show the zone shard menu") RULE_CATEGORY_END() RULE_CATEGORY(Map) diff --git a/common/version.h b/common/version.h index 192a20614..9ca78821e 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9287 +#define CURRENT_BINARY_DATABASE_VERSION 9288 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9045 #endif diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 0995b83ba..a63916911 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -566,7 +566,7 @@ OP_RaidUpdate=0x3973 OP_RaidJoin=0x0000 OP_RaidDelegateAbility=0x2b33 OP_MarkRaidNPC=0x5a58 -OP_RaidClearNPCMarks=0x20d3 +OP_RaidClearNPCMarks=0x20d3 # Button-push commands OP_Taunt=0x2703 @@ -728,3 +728,6 @@ OP_InitialHPUpdate=0x0000 #aura related OP_UpdateAura=0x1456 OP_RemoveTrap=0x71da + +OP_PickZoneWindow=0x72d8 +OP_PickZone=0xaaba diff --git a/zone/client.cpp b/zone/client.cpp index c98946846..be693e453 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -13037,19 +13037,19 @@ void Client::ClientToNpcAggroProcess() const std::vector& Client::GetInventorySlots() { static const std::vector> slots = { - { 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 }, + {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}, }; static std::vector slot_ids; if (slot_ids.empty()) { - for (const auto& [begin, end] : slots) { + for (const auto &[begin, end]: slots) { for (int16 slot_id = begin; slot_id <= end; ++slot_id) { slot_ids.emplace_back(slot_id); } @@ -13058,3 +13058,51 @@ const std::vector& Client::GetInventorySlots() return slot_ids; } + +void Client::ShowZoneShardMenu() +{ + auto z = GetZone(GetZoneID()); + if (z && !z->shard_at_player_count) { + return; + } + + auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, GetZoneID()); + LogZoning("Zone sharding results count [{}]", results.size()); + + if (results.empty()) { + Message(Chat::White, "No zone shards found."); + return; + } + + if (!results.empty()) { + Message(Chat::White, "Available Zone Shards:"); + } + + int number = 1; + for (auto &e: results) { + std::string teleport_link = Saylink::Silent( + fmt::format("#zoneshard {} {}", e.zone_id, (e.instance_id == 0 ? -1 : e.instance_id)), + "Teleport" + ); + + std::string yours; + if (e.zone_id == GetZoneID() && e.instance_id == GetInstanceID()) { + teleport_link = "Teleport"; + yours = " (Yours)"; + } + + Message( + Chat::White, fmt::format( + " --> [{}] #{} {} ({}) [{}/{}] players {}", + teleport_link, + number, + z->long_name, + e.instance_id, + e.player_count, + z->shard_at_player_count, + yours + ).c_str() + ); + number++; + } +} diff --git a/zone/client.h b/zone/client.h index 03202af2f..a60b01b43 100644 --- a/zone/client.h +++ b/zone/client.h @@ -2236,6 +2236,7 @@ private: public: const std::string &GetMailKeyFull() const; const std::string &GetMailKey() const; + void ShowZoneShardMenu(); }; #endif diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 4175d56ff..55085620f 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -339,6 +339,7 @@ void MapOpcodes() ConnectedOpcodes[OP_PlayerStateAdd] = &Client::Handle_OP_PlayerStateAdd; ConnectedOpcodes[OP_PlayerStateRemove] = &Client::Handle_OP_PlayerStateRemove; ConnectedOpcodes[OP_PickPocket] = &Client::Handle_OP_PickPocket; + ConnectedOpcodes[OP_PickZone] = &Client::Handle_OP_PickZone; ConnectedOpcodes[OP_PopupResponse] = &Client::Handle_OP_PopupResponse; ConnectedOpcodes[OP_PotionBelt] = &Client::Handle_OP_PotionBelt; ConnectedOpcodes[OP_PurchaseLeadershipAA] = &Client::Handle_OP_PurchaseLeadershipAA; @@ -941,6 +942,11 @@ void Client::CompleteConnect() ShowDevToolsMenu(); } + auto z = GetZone(GetZoneID(), GetInstanceVersion()); + if (z && z->shard_at_player_count > 0 && !RuleB(Zone, ZoneShardQuestMenuOnly)) { + ShowZoneShardMenu(); + } + // shared tasks memberlist if (RuleB(TaskSystem, EnableTaskSystem) && GetTaskState()->HasActiveSharedTask()) { @@ -11839,6 +11845,17 @@ void Client::Handle_OP_PickPocket(const EQApplicationPacket *app) SendPickPocketResponse(victim, 0, PickPocketFailed); } +void Client::Handle_OP_PickZone(const EQApplicationPacket *app) +{ + if (app->size != sizeof(PickZone_Struct)) { + LogDebug("Size mismatch in OP_PickZone expected [{}] got [{}]", sizeof(PickZone_Struct), app->size); + DumpPacket(app); + return; + } + + // handle +} + void Client::Handle_OP_PopupResponse(const EQApplicationPacket *app) { diff --git a/zone/client_packet.h b/zone/client_packet.h index 57a4bfcf4..4ef62bdf9 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -242,6 +242,7 @@ void Handle_OP_PlayerStateAdd(const EQApplicationPacket *app); void Handle_OP_PlayerStateRemove(const EQApplicationPacket *app); void Handle_OP_PickPocket(const EQApplicationPacket *app); + void Handle_OP_PickZone(const EQApplicationPacket *app); void Handle_OP_PopupResponse(const EQApplicationPacket *app); void Handle_OP_PotionBelt(const EQApplicationPacket *app); void Handle_OP_PurchaseLeadershipAA(const EQApplicationPacket *app); diff --git a/zone/command.cpp b/zone/command.cpp index 513cd0218..1007e09d3 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -243,6 +243,7 @@ int command_init(void) command_add("zone", "[Zone ID|Zone Short Name] [X] [Y] [Z] - Teleport to specified Zone by ID or Short Name (coordinates are optional)", AccountStatus::Guide, command_zone) || command_add("zonebootup", "[ZoneServerID] [shortname] - Make a zone server boot a specific zone", AccountStatus::GMLeadAdmin, command_zonebootup) || command_add("zoneinstance", "[Instance ID] [X] [Y] [Z] - Teleport to specified Instance by ID (coordinates are optional)", AccountStatus::Guide, command_zone_instance) || + command_add("zoneshard", "[zone] [instance_id] - Teleport explicitly to a zone shard", AccountStatus::Player, command_zone_shard) || command_add("zoneshutdown", "[shortname] - Shut down a zone server", AccountStatus::GMLeadAdmin, command_zoneshutdown) || command_add("zsave", " Saves zheader to the database", AccountStatus::QuestTroupe, command_zsave) ) { @@ -936,4 +937,5 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/zonebootup.cpp" #include "gm_commands/zoneshutdown.cpp" #include "gm_commands/zone_instance.cpp" +#include "gm_commands/zone_shard.cpp" #include "gm_commands/zsave.cpp" diff --git a/zone/command.h b/zone/command.h index d59fdf9a5..189909516 100644 --- a/zone/command.h +++ b/zone/command.h @@ -195,6 +195,7 @@ void command_wpadd(Client *c, const Seperator *sep); void command_worldwide(Client *c, const Seperator *sep); void command_zone(Client *c, const Seperator *sep); void command_zone_instance(Client *c, const Seperator *sep); +void command_zone_shard(Client *c, const Seperator *sep); void command_zonebootup(Client *c, const Seperator *sep); void command_zoneshutdown(Client *c, const Seperator *sep); void command_zopp(Client *c, const Seperator *sep); diff --git a/zone/gm_commands/zone_shard.cpp b/zone/gm_commands/zone_shard.cpp new file mode 100644 index 000000000..07ef0541e --- /dev/null +++ b/zone/gm_commands/zone_shard.cpp @@ -0,0 +1,137 @@ +#include "../client.h" + +void command_zone_shard(Client *c, const Seperator *sep) +{ + int arguments = sep->argnum; + if (!arguments || !sep->IsNumber(1)) { + if (!RuleB(Zone, ZoneShardQuestMenuOnly)) { + c->ShowZoneShardMenu(); + } + + return; + } + + if (c->GetAggroCount() > 0) { + c->Message(Chat::White, "You cannot request a shard change while in combat."); + return; + } + + std::string zone_input = sep->arg[1]; + uint32 zone_id = 0; + + // if input is id + if (Strings::IsNumber(zone_input)) { + zone_id = Strings::ToInt(zone_input); + + // validate + if (zone_id != 0 && !GetZone(zone_id)) { + c->Message(Chat::White, fmt::format("Could not find zone by id [{}]", zone_id).c_str()); + return; + } + } + else { + // validate + if (!zone_store.GetZone(zone_input)) { + c->Message(Chat::White, fmt::format("Could not find zone by short_name [{}]", zone_input).c_str()); + return; + } + + // validate we got id + zone_id = ZoneID(zone_input); + if (zone_id == 0) { + c->Message(Chat::White, fmt::format("Could not find zone id by short_name [{}]", zone_input).c_str()); + return; + } + } + + auto z = GetZone(zone_id); + if (z && z->shard_at_player_count == 0) { + c->Message(Chat::White, "Zone does not have sharding enabled."); + return; + } + + auto instance_id = sep->arg[2] ? Strings::ToBigInt(sep->arg[2]) : 0; + if (instance_id < -1) { + c->Message(Chat::White, "You must enter a valid Instance ID."); + return; + } + + if (zone_id == c->GetZoneID() && c->GetInstanceID() == instance_id) { + c->Message(Chat::White, "You are already in this shard."); + return; + } + + if (zone_id != c->GetZoneID()) { + c->Message(Chat::White, "You must request a shard change from the zone you are currently in."); + return; + } + + auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, c->GetZoneID()); + if (results.size() <= 1) { + c->Message(Chat::White, "No shards found."); + return; + } + + if (instance_id > 0) { + if (!database.CheckInstanceExists(instance_id)) { + c->Message( + Chat::White, + fmt::format( + "Instance ID {} does not exist.", + instance_id + ).c_str() + ); + return; + } + + auto instance_zone_id = database.GetInstanceZoneID(instance_id); + if (!instance_zone_id) { + c->Message( + Chat::White, + fmt::format( + "Instance ID {} not found or zone is set to null.", + instance_id + ).c_str() + ); + return; + } + + if (instance_zone_id != zone_id) { + c->Message( + Chat::White, + fmt::format( + "Instance Zone ID {} does not match zone ID {}.", + instance_id, + zone_id + ).c_str() + ); + return; + } + + if (!database.CheckInstanceByCharID(instance_id, c->CharacterID())) { + database.AddClientToInstance(instance_id, c->CharacterID()); + } + + if (!database.VerifyInstanceAlive(instance_id, c->CharacterID())) { + c->Message( + Chat::White, + fmt::format( + "Instance ID {} expired.", + instance_id + ).c_str() + ); + return; + } + } + + c->MovePC( + zone_id, + instance_id, + c->GetX(), + c->GetY(), + c->GetZ(), + c->GetHeading(), + 0, + ZoneSolicited + ); +} diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index 13982fcc0..97747d642 100644 --- a/zone/lua_client.cpp +++ b/zone/lua_client.cpp @@ -3436,13 +3436,14 @@ void Lua_Client::AreaTaunt(float range, int bonus_hate) entity_list.AETaunt(self, range, bonus_hate); } -luabind::object Lua_Client::GetInventorySlots(lua_State* L) { +luabind::object Lua_Client::GetInventorySlots(lua_State* L) +{ auto lua_table = luabind::newtable(L); if (d_) { - auto self = reinterpret_cast(d_); - int index = 1; - for (const int16& slot_id : self->GetInventorySlots()) { + auto self = reinterpret_cast(d_); + int index = 1; + for (const int16 &slot_id: self->GetInventorySlots()) { lua_table[index] = slot_id; index++; } @@ -3451,6 +3452,12 @@ luabind::object Lua_Client::GetInventorySlots(lua_State* L) { return lua_table; } +void Lua_Client::ShowZoneShardMenu() +{ + Lua_Safe_Call_Void(); + self->ShowZoneShardMenu(); +} + luabind::scope lua_register_client() { return luabind::class_("Client") .def(luabind::constructor<>()) @@ -3970,6 +3977,7 @@ luabind::scope lua_register_client() { .def("SetTint", (void(Lua_Client::*)(int,uint32))&Lua_Client::SetTint) .def("SetTitleSuffix", (void(Lua_Client::*)(const char *))&Lua_Client::SetTitleSuffix) .def("SetZoneFlag", (void(Lua_Client::*)(uint32))&Lua_Client::SetZoneFlag) + .def("ShowZoneShardMenu", (void(Lua_Client::*)(void))&Lua_Client::ShowZoneShardMenu) .def("Signal", (void(Lua_Client::*)(int))&Lua_Client::Signal) .def("Sit", (void(Lua_Client::*)(void))&Lua_Client::Sit) .def("Stand", (void(Lua_Client::*)(void))&Lua_Client::Stand) diff --git a/zone/lua_client.h b/zone/lua_client.h index fb012b3bc..4374fe199 100644 --- a/zone/lua_client.h +++ b/zone/lua_client.h @@ -587,6 +587,7 @@ public: void DialogueWindow(std::string markdown); bool ReloadDataBuckets(); + void ShowZoneShardMenu(); 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); diff --git a/zone/lua_packet.cpp b/zone/lua_packet.cpp index c6a1ab6e2..3e8325e4f 100644 --- a/zone/lua_packet.cpp +++ b/zone/lua_packet.cpp @@ -915,7 +915,8 @@ luabind::scope lua_register_packet_opcodes() { luabind::value("Marquee", static_cast(OP_Marquee)), luabind::value("ClientTimeStamp", static_cast(OP_ClientTimeStamp)), luabind::value("GuildPromote", static_cast(OP_GuildPromote)), - luabind::value("Fling", static_cast(OP_Fling)) + luabind::value("Fling", static_cast(OP_Fling)), + luabind::value("PickZoneWindow", static_cast(OP_PickZoneWindow)) )]; } diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index c41b39ba5..fdeece8f8 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -1756,6 +1756,11 @@ int Perl_Client_GetClientMaxLevel(Client* self) return self->GetClientMaxLevel(); } +void Perl_Client_ShowZoneShardMenu(Client* self) // @categories Script Utility +{ + self->ShowZoneShardMenu(); +} + DynamicZoneLocation GetDynamicZoneLocationFromHash(perl::hash table) { // dynamic zone helper method, defaults invalid/missing keys to 0 @@ -3742,6 +3747,7 @@ void perl_register_client() package.add("SetTitleSuffix", (void(*)(Client*, std::string))&Perl_Client_SetTitleSuffix); package.add("SetTitleSuffix", (void(*)(Client*, std::string, bool))&Perl_Client_SetTitleSuffix); package.add("SetZoneFlag", &Perl_Client_SetZoneFlag); + package.add("ShowZoneShardMenu", &Perl_Client_ShowZoneShardMenu); package.add("Signal", &Perl_Client_Signal); package.add("SignalClient", &Perl_Client_SignalClient); package.add("SilentMessage", &Perl_Client_SilentMessage); diff --git a/zone/zoning.cpp b/zone/zoning.cpp index 225d21a91..a02ab943a 100644 --- a/zone/zoning.cpp +++ b/zone/zoning.cpp @@ -778,6 +778,66 @@ void Client::ZonePC(uint32 zoneID, uint32 instance_id, float x, float y, float z } } + // zone sharding + if (zoneID == zd->zoneidnumber && + instance_id == 0 && + zd->shard_at_player_count > 0) { + + bool found_shard = false; + auto results = CharacterDataRepository::GetInstanceZonePlayerCounts(database, zoneID); + + LogZoning("Zone sharding results count [{}]", results.size()); + + uint64_t shard_instance_duration = 3155760000; + + for (auto &e: results) { + LogZoning( + "Zone sharding results [{}] ({}) instance_id [{}] player_count [{}]", + ZoneName(e.zone_id) ? ZoneName(e.zone_id) : "Unknown", + e.zone_id, + e.instance_id, + e.player_count + ); + + if (e.player_count < zd->shard_at_player_count) { + instance_id = e.instance_id; + + database.AddClientToInstance(instance_id, CharacterID()); + + LogZoning( + "Client [{}] attempting zone to sharded zone > instance_id [{}] zone [{}] ({})", + GetCleanName(), + instance_id, + ZoneName(zoneID) ? ZoneName(zoneID) : "Unknown", + zoneID + ); + + found_shard = true; + break; + } + } + + if (!found_shard) { + uint16 new_instance_id = 0; + database.GetUnusedInstanceID(new_instance_id); + database.CreateInstance(new_instance_id, zoneID, zd->version, shard_instance_duration); + database.AddClientToInstance(new_instance_id, CharacterID()); + instance_id = new_instance_id; + LogZoning( + "Client [{}] creating new sharded zone > instance_id [{}] zone [{}] ({})", + GetCleanName(), + new_instance_id, + ZoneName(zoneID) ? ZoneName(zoneID) : "Unknown", + zoneID + ); + } + } + + // passed from zone shard request to normal zone + if (instance_id == -1) { + instance_id = 0; + } + LogInfo( "Client [{}] zone_id [{}] instance_id [{}] x [{}] y [{}] z [{}] heading [{}] ignorerestrictions [{}] zone_mode [{}]", GetCleanName(),