eqemu-server/zone/zoning.cpp
Knightly 7ab909ee47 Standardize Licensing
- License was intended to be GPLv3 per earlier commit of GPLv3 LICENSE FILE
- This is confirmed by the inclusion of libraries that are incompatible with GPLv2
- This is also confirmed by KLS and the agreement of KLS's predecessors
- Added GPLv3 license headers to the compilable source files
- Removed Folly licensing in strings.h since the string functions do not match the Folly functions and are standard functions - this must have been left over from previous implementations
- Removed individual contributor license headers since the project has been under the "developer" mantle for many years
- Removed comments on files that were previously automatically generated since they've been manually modified multiple times and there are no automatic scripts referencing them (removed in 2023)
2026-04-01 17:09:57 -07:00

1479 lines
43 KiB
C++

/* EQEmu: EQEmulator
Copyright (C) 2001-2026 EQEmu Development Team
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; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
*/
#include "client.h"
#include "common/eqemu_logsys.h"
#include "common/events/player_event_logs.h"
#include "common/repositories/character_peqzone_flags_repository.h"
#include "common/repositories/zone_flags_repository.h"
#include "common/rulesys.h"
#include "common/strings.h"
#include "zone/bot.h"
#include "zone/dynamic_zone.h"
#include "zone/queryserv.h"
#include "zone/quest_parser_collection.h"
#include "zone/string_ids.h"
#include "zone/worldserver.h"
#include "zone/zone.h"
extern QueryServ* QServ;
extern WorldServer worldserver;
extern Zone* zone;
void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) {
if (RuleB(Bots, Enabled)) {
Bot::ProcessClientZoneChange(this);
}
bZoning = true;
if (app->size != sizeof(ZoneChange_Struct)) {
LogDebug("Wrong size: OP_ZoneChange, size=[{}], expected [{}]", app->size, sizeof(ZoneChange_Struct));
return;
}
#if EQDEBUG >= 5
LogDebug("Zone request from [{}]", GetName());
DumpPacket(app);
#endif
auto* zc = (ZoneChange_Struct*)app->pBuffer;
LogZoning(
"Client [{}] char_name [{}] zoning to [{}] ({}) instance_id [{}] x [{}] y [{}] z [{}] zone_reason [{}] success [{}] zone_mode [{}] ({})",
GetCleanName(),
zc->char_name,
ZoneName(zc->zoneID) ? ZoneName(zc->zoneID) : "Unknown",
zc->zoneID,
zc->instanceID,
zc->x,
zc->y,
zc->z,
zc->zone_reason,
zc->success,
GetZoneModeString(zone_mode),
int(zone_mode)
);
WorldContentService::Instance()->HandleZoneRoutingMiddleware(zc);
uint16 target_zone_id = 0;
auto target_instance_id = zc->instanceID;
ZonePoint* zone_point = nullptr;
//figure out where they are going.
if (zc->zoneID == 0) {
//client dosent know where they are going...
//try to figure it out for them.
switch(zone_mode) {
case EvacToSafeCoords:
case ZoneToSafeCoords:
//going to safe coords, but client dosent know where?
//assume it is this zone for now.
target_zone_id = zone->GetZoneID();
break;
case GMSummon:
case GMHiddenSummon:
case ZoneSolicited: //we told the client to zone somewhere, so we know where they are going.
target_zone_id = zonesummon_id;
break;
case GateToBindPoint:
case ZoneToBindPoint:
target_zone_id = m_pp.binds[0].zone_id;
target_instance_id = m_pp.binds[0].instance_id;
break;
case ZoneUnsolicited: //client came up with this on its own.
zone_point = zone->GetClosestZonePointWithoutZone(GetX(), GetY(), GetZ(), this, ZONEPOINT_NOZONE_RANGE);
if (zone_point) {
//we found a zone point, which is a reasonable distance away
//assume that is the one were going with.
target_zone_id = zone_point->target_zone_id;
target_instance_id = zone_point->target_zone_instance;
} else {
//unable to find a zone point... is there anything else
//that can be a valid un-zolicited zone request?
Message(Chat::Red, "Invalid unsolicited zone request.");
LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]", GetName(), target_zone_id);
cheat_manager.CheatDetected(GetBindZoneID() == target_zone_id ? MQGate : MQZone, glm::vec3(zc->x, zc->y, zc->z));
SendZoneCancel(zc);
return;
}
break;
default:
break;
};
} else {
// This is to allow both 6.2 and Titanium clients to perform a proper zoning of the client when evac/succor
// WildcardX 27 January 2008
if (zone_mode == EvacToSafeCoords && zonesummon_id) {
target_zone_id = zonesummon_id;
} else {
target_zone_id = zc->zoneID;
}
//if we are zoning to a specific zone unsolicied,
//then until otherwise determined, they must be zoning
//on a zone line.
if (zone_mode == ZoneUnsolicited) {
if (target_zone_id == zone->GetZoneID()) {
SendZoneCancel(zc);
return;
}
zone_point = zone->GetClosestZonePoint(glm::vec3(GetPosition()), target_zone_id, this, ZONEPOINT_ZONE_RANGE);
//if we didnt get a zone point, or its to a different zone,
//then we assume this is invalid.
if (!zone_point || zone_point->target_zone_id != target_zone_id) {
LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]", GetName(), target_zone_id);
cheat_manager.CheatDetected(GetBindZoneID() == target_zone_id ? MQGate : MQZone, glm::vec3(zc->x, zc->y, zc->z));
SendZoneCancel(zc);
return;
}
}
}
if (target_instance_id) {
LogZoning(
"Client [{}] attempting zone to instance_id [{}] zone [{}] ({})",
GetCleanName(),
target_instance_id,
ZoneName(target_zone_id) ? ZoneName(target_zone_id) : "Unknown",
target_zone_id
);
//make sure we are in it and it's unexpired.
if (!database.VerifyInstanceAlive(target_instance_id, CharacterID())) {
Message(
Chat::Red,
fmt::format(
"Instance ID {} was expired or you were not a member of it.",
target_instance_id
).c_str()
);
LogZoning("Client [{}] Invalid zone instance request to zone id [{}]", GetCleanName(), target_zone_id);
SendZoneCancel(zc);
return;
}
if (!database.VerifyZoneInstance(target_zone_id, target_instance_id)) {
Message(
Chat::Red,
fmt::format(
"Instance ID was {}, this does not match Zone ID {}.",
target_instance_id,
target_zone_id
).c_str()
);
LogZoning("Client [{}] Invalid zone instance request to zone id [{}]", GetCleanName(), target_zone_id);
SendZoneCancel(zc);
return;
}
}
/* Check for Valid Zone */
auto* target_zone_name = ZoneName(target_zone_id);
if (!target_zone_name) {
//invalid zone...
Message(Chat::Red, "Invalid target zone ID.");
LogError("Zoning [{}]: Unable to get zone name for zone id [{}]", GetName(), target_zone_id);
SendZoneCancel(zc);
return;
}
auto target_instance_version = database.GetInstanceVersion(target_instance_id);
auto zone_data = GetZoneVersionWithFallback(
ZoneID(target_zone_name),
target_instance_version
);
if (!zone_data) {
Message(Chat::Red, "Invalid target zone while getting safe points.");
LogError("Zoning [{}]: Unable to get safe coordinates for zone [{}]", GetName(), target_zone_name);
SendZoneCancel(zc);
return;
}
float safe_x, safe_y, safe_z, safe_heading;
int16 min_status = AccountStatus::Player;
uint8 min_level = 0;
LogZoning("Loaded zone flag [{}]", zone_data->flag_needed);
safe_x = zone_data->safe_x;
safe_y = zone_data->safe_y;
safe_z = zone_data->safe_z;
safe_heading = zone_data->safe_heading;
min_status = zone_data->min_status;
min_level = zone_data->min_level;
if (parse->PlayerHasQuestSub(EVENT_ZONE)) {
const auto& export_string = fmt::format(
"{} {} {} {} {} {}",
zone->GetZoneID(),
zone->GetInstanceID(),
zone->GetInstanceVersion(),
target_zone_id,
target_instance_id,
target_instance_version
);
if (parse->EventPlayer(EVENT_ZONE, this, export_string, 0) != 0) {
SendZoneCancel(zc);
return;
}
}
if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::ZONING)) {
auto e = PlayerEvent::ZoningEvent{};
e.from_zone_long_name = zone->GetLongName();
e.from_zone_short_name = zone->GetShortName();
e.from_zone_id = zone->GetZoneID();
e.from_instance_id = zone->GetInstanceID();
e.from_instance_version = zone->GetInstanceVersion();
e.to_zone_long_name = ZoneLongName(target_zone_id);
e.to_zone_short_name = ZoneName(target_zone_id);
e.to_zone_id = target_zone_id;
e.to_instance_id = target_instance_id;
e.to_instance_version = target_instance_version;
RecordPlayerEventLog(PlayerEvent::ZONING, e);
}
//handle circumvention of zone restrictions
//we need the value when creating the outgoing packet as well.
uint8 ignore_restrictions = zonesummon_ignorerestrictions;
zonesummon_ignorerestrictions = 0;
auto target_x = 0.0f, target_y = 0.0f, target_z = 0.0f, target_heading = 0.0f;
switch (zone_mode) {
case EvacToSafeCoords:
case ZoneToSafeCoords:
LogDebug(
"Zoning [{}] to safe coords ([{}], [{}], [{}], [{}]) in [{}] ([{}])",
GetName(),
safe_x,
safe_y,
safe_z,
safe_heading,
target_zone_name,
target_zone_id
);
target_x = safe_x;
target_y = safe_y;
target_z = safe_z;
target_heading = safe_heading;
break;
case GMSummon:
case GMHiddenSummon:
target_x = m_ZoneSummonLocation.x;
target_y = m_ZoneSummonLocation.y;
target_z = m_ZoneSummonLocation.z;
target_heading = m_ZoneSummonLocation.w;
ignore_restrictions = 1;
break;
case GateToBindPoint:
target_x = m_pp.binds[0].x;
target_y = m_pp.binds[0].y;
target_z = m_pp.binds[0].z;
target_heading = m_pp.binds[0].heading;
break;
case ZoneToBindPoint:
target_x = m_pp.binds[0].x;
target_y = m_pp.binds[0].y;
target_z = m_pp.binds[0].z;
target_heading = m_pp.binds[0].heading;
ignore_restrictions = 1; //can always get to our bind point? seems exploitable
break;
case ZoneSolicited: //we told the client to zone somewhere, so we know where they are going.
//recycle zonesummon variables
target_x = m_ZoneSummonLocation.x;
target_y = m_ZoneSummonLocation.y;
target_z = m_ZoneSummonLocation.z;
target_heading = m_ZoneSummonLocation.w;
break;
case ZoneUnsolicited: //client came up with this on its own.
//client requested a zoning... what are the cases when this could happen?
//Handle zone point case:
if (zone_point) {
//they are zoning using a valid zone point, figure out coords
//999999 is a placeholder for 'same as where they were from'
target_x = zone_point->target_x == 999999 ? GetX() : zone_point->target_x;
target_y = zone_point->target_y == 999999 ? GetY() : zone_point->target_y;
target_z = zone_point->target_z == 999999 ? GetZ() : zone_point->target_z;
target_heading = zone_point->target_heading == 999 ? GetHeading() : zone_point->target_heading;
break;
}
//for now, there are no other cases...
//could not find a valid reason for them to be zoning, stop it.
cheat_manager.CheatDetected(GetBindZoneID() == target_zone_id ? MQGate : MQZone, glm::vec3(zc->x, zc->y, zc->z));
LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]. Not near a zone point", GetName(), target_zone_name);
SendZoneCancel(zc);
return;
default:
break;
};
auto zoning_message = ZoningMessage::ZoneSuccess;
// Check Minimum Status, Minimum Level, Maximum Level, and Zone Flag
if (
!ignore_restrictions &&
!CanEnterZone(ZoneName(target_zone_id), target_instance_version)
) {
zoning_message = ZoningMessage::ZoneNoExperience;
}
//TODO: ADVENTURE ENTRANCE CHECK
// Expansion checks and routing
if (WorldContentService::Instance()->GetCurrentExpansion() >= Expansion::Classic && !GetGM()) {
bool meets_zone_expansion_check = false;
auto z = ZoneStore::Instance()->GetZoneWithFallback(ZoneID(target_zone_name), 0);
if (z->expansion <= WorldContentService::Instance()->GetCurrentExpansion() || z->bypass_expansion_check) {
meets_zone_expansion_check = true;
}
LogZoning(
"Checking zone request [{}] for expansion [{}] ({}) success [{}]",
target_zone_name,
(WorldContentService::Instance()->GetCurrentExpansion()),
WorldContentService::Instance()->GetCurrentExpansionName(),
meets_zone_expansion_check ? "true" : "false"
);
if (!meets_zone_expansion_check) {
zoning_message = ZoningMessage::ZoneNoExpansion;
}
}
if (WorldContentService::Instance()->GetCurrentExpansion() >= Expansion::Classic && GetGM()) {
LogInfo("[{}] Bypassing zone expansion checks because GM flag is set", GetCleanName());
Message(Chat::White, "Your GM flag allows you to bypass zone expansion checks.");
}
if (zoning_message == ZoningMessage::ZoneSuccess) {
DoZoneSuccess(zc, target_zone_id, target_instance_id, target_x, target_y, target_z, target_heading, ignore_restrictions);
} else {
LogError("Zoning [{}]: Rules prevent this char from zoning into [{}]", GetName(), target_zone_name);
SendZoneError(zc, zoning_message);
}
}
void Client::SendZoneCancel(ZoneChange_Struct *zc) {
//effectively zone them right back to where they were
//unless we find a better way to stop the zoning process.
cheat_manager.SetExemptStatus(Port, true);
EQApplicationPacket *outapp = nullptr;
outapp = new EQApplicationPacket(OP_ZoneChange, sizeof(ZoneChange_Struct));
ZoneChange_Struct *zc2 = (ZoneChange_Struct*)outapp->pBuffer;
LogZoning(
"(zs) Client [{}] char_name [{}] zoning to [{}] ({}) cancelled instance_id [{}] x [{}] y [{}] z [{}] zone_reason [{}] success [{}]",
GetCleanName(),
zc->char_name,
ZoneName(zc2->zoneID) ? ZoneName(zc2->zoneID) : "Unknown",
zc->zoneID,
zc->instanceID,
zc->x,
zc->y,
zc->z,
zc->zone_reason,
zc->success
);
strn0cpy(zc2->char_name, zc->char_name, 64);
zc2->zoneID = zone->GetZoneID();
zc2->success = 1;
zc2->instanceID = zone->GetInstanceID();
// this fixes an issue where when we do a zone cancel what often ends up happening is we are sending
// the client the wrong coordinates to zone back to. Often times it is the x,y,z of the destination zone
// because we saved the destination x,y,z on the client profile before we rejected the zone request.
// we're using rewind location because it should be where the client relatively was before we rejected the zone request.
// it also prevents the client from getting caught up in a zone loop because if we sent them exactly back to where they
// originated the request we could end up in a situation where the client is caught in a zone loop.
m_Position.x = m_RewindLocation.x;
m_Position.y = m_RewindLocation.y;
m_Position.z = m_RewindLocation.z;
zc2->x = m_Position.x;
zc2->y = m_Position.y;
zc2->z = m_Position.z;
LogZoning(
"(zc2) Client [{}] char_name [{}] zoning to [{}] ({}) cancelled instance_id [{}] x [{}] y [{}] z [{}] zone_reason [{}] success [{}]",
GetCleanName(),
zc2->char_name,
ZoneName(zc2->zoneID) ? ZoneName(zc2->zoneID) : "Unknown",
zc2->zoneID,
zc2->instanceID,
zc2->x,
zc2->y,
zc2->z,
zc2->zone_reason,
zc2->success
);
outapp->priority = 6;
FastQueuePacket(&outapp);
//reset to unsolicited.
zone_mode = ZoneUnsolicited;
// reset since we're not zoning anymore
bZoning = false;
// remove save position lock
m_lock_save_position = false;
}
void Client::SendZoneError(ZoneChange_Struct *zc, int8 err)
{
LogError("Zone [{}] is not available because target wasn't found or character insufficent level", zc->zoneID);
cheat_manager.SetExemptStatus(Port, true);
EQApplicationPacket *outapp = nullptr;
outapp = new EQApplicationPacket(OP_ZoneChange, sizeof(ZoneChange_Struct));
ZoneChange_Struct *zc2 = (ZoneChange_Struct*)outapp->pBuffer;
strcpy(zc2->char_name, zc->char_name);
zc2->zoneID = zc->zoneID;
zc2->success = err;
outapp->priority = 6;
FastQueuePacket(&outapp);
LogZoning(
"Client [{}] char_name [{}] zoning to [{}] ({}) (error) instance_id [{}] x [{}] y [{}] z [{}] zone_reason [{}] success [{}]",
GetCleanName(),
zc2->char_name,
ZoneName(zc2->zoneID) ? ZoneName(zc2->zoneID) : "Unknown",
zc2->zoneID,
zc2->instanceID,
zc2->x,
zc2->y,
zc2->z,
zc2->zone_reason,
zc2->success
);
//reset to unsolicited.
zone_mode = ZoneUnsolicited;
// reset since we're not zoning anymore
bZoning = false;
// remove save position lock
m_lock_save_position = false;
}
void Client::DoZoneSuccess(ZoneChange_Struct *zc, uint16 zone_id, uint32 instance_id, float dest_x, float dest_y, float dest_z, float dest_h, int8 ignore_r) {
//this is called once the client is fully allowed to zone here
//it takes care of all the activities which occur when a client zones out
SendLogoutPackets();
/* Dont clear aggro until the zone is successful */
entity_list.RemoveFromHateLists(this);
if(GetPet())
entity_list.RemoveFromHateLists(GetPet());
if (GetPendingDzInviteID() != 0)
{
// live re-invites if client zoned with a pending invite, save pending invite info in world
if (auto dz = DynamicZone::FindDynamicZoneByID(GetPendingDzInviteID(), DynamicZoneType::Expedition))
{
dz->SendWorldPendingInvite(m_dz_invite, GetName());
}
}
LogZoning(
"Client [{}] char_name [{}] zoning to [{}] ({}) instance_id [{}] x [{}] y [{}] z [{}] zone_reason [{}] success [{}]",
GetCleanName(),
zc->char_name,
ZoneName(zone_id) ? ZoneName(zone_id) : "Unknown",
zone_id,
instance_id,
dest_x,
dest_y,
dest_z,
zc->zone_reason,
zc->success
);
//set the player's coordinates in the new zone so they have them
//when they zone into it
m_Position.x = dest_x; //these coordinates will now be saved when ~client is called
m_Position.y = dest_y;
m_Position.z = dest_z;
m_Position.w = dest_h; // Cripp: fix for zone heading
m_pp.heading = dest_h;
m_pp.zone_id = zone_id;
m_pp.zoneInstance = instance_id;
// save character position
m_pp.x = m_Position.x;
m_pp.y = m_Position.y;
m_pp.z = m_Position.z;
m_pp.heading = m_Position.w;
SaveCharacterData();
m_lock_save_position = true;
if (zone_id == zone->GetZoneID() && instance_id == zone->GetInstanceID()) {
// No need to ask worldserver if we're zoning to ourselves (most
// likely to a bind point), also fixes a bug since the default response was failure
auto outapp = new EQApplicationPacket(OP_ZoneChange, sizeof(ZoneChange_Struct));
ZoneChange_Struct* zc2 = (ZoneChange_Struct*) outapp->pBuffer;
strcpy(zc2->char_name, GetName());
zc2->zoneID = zone_id;
zc2->instanceID = instance_id;
zc2->success = 1;
outapp->priority = 6;
FastQueuePacket(&outapp);
} else {
// vesuvias - zoneing to another zone so we need to the let the world server
//handle things with the client for a while
auto pack = new ServerPacket(ServerOP_ZoneToZoneRequest, sizeof(ZoneToZone_Struct));
ZoneToZone_Struct *ztz = (ZoneToZone_Struct *)pack->pBuffer;
ztz->response = 0;
ztz->current_zone_id = zone->GetZoneID();
ztz->current_instance_id = zone->GetInstanceID();
ztz->requested_zone_id = zone_id;
ztz->requested_instance_id = instance_id;
ztz->admin = admin;
ztz->ignorerestrictions = ignore_r;
strcpy(ztz->name, GetName());
ztz->guild_id = GuildID();
worldserver.SendPacket(pack);
safe_delete(pack);
}
//reset to unsolicited.
zone_mode = ZoneUnsolicited;
m_ZoneSummonLocation = glm::vec4();
zonesummon_id = 0;
zonesummon_ignorerestrictions = 0;
// this simply resets the zone shutdown timer
zone->ResetShutdownTimer();
}
void Client::MovePC(const char* zonename, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm) {
ProcessMovePC(ZoneID(zonename), 0, x, y, z, heading, ignorerestrictions, zm);
}
//designed for in zone moving
void Client::MovePC(float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm) {
ProcessMovePC(zone->GetZoneID(), zone->GetInstanceID(), x, y, z, heading, ignorerestrictions, zm);
}
void Client::MovePC(uint32 zoneID, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm) {
ProcessMovePC(zoneID, 0, x, y, z, heading, ignorerestrictions, zm);
}
void Client::MovePC(uint32 zoneID, uint32 instanceID, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm){
ProcessMovePC(zoneID, instanceID, x, y, z, heading, ignorerestrictions, zm);
}
void Client::MoveZone(const char *zone_short_name, const glm::vec4 &location) {
const bool use_coordinates = (
location.x != 0.0f ||
location.y != 0.0f ||
location.z != 0.0f ||
location.w != 0.0f
);
const ZoneMode zone_type = use_coordinates ? ZoneSolicited : ZoneToSafeCoords;
ProcessMovePC(ZoneID(zone_short_name), 0, location.x, location.y, location.z, location.w, 3, zone_type);
}
void Client::MoveZoneGroup(const char *zone_short_name, const glm::vec4 &location) {
Group* g = GetGroup();
if (!g) {
MoveZone(zone_short_name, location);
} else {
for (const auto& gm : g->members) {
if (gm && gm->IsClient()) {
Client* c = gm->CastToClient();
c->MoveZone(zone_short_name, location);
}
}
}
}
void Client::MoveZoneRaid(const char *zone_short_name, const glm::vec4 &location) {
Raid* r = GetRaid();
if (!r) {
MoveZone(zone_short_name, location);
} else {
for (const auto& rm : r->members) {
if (rm.member && rm.member->IsClient()) {
Client* c = rm.member->CastToClient();
c->MoveZone(zone_short_name, location);
}
}
}
}
void Client::MoveZoneInstance(uint16 instance_id, const glm::vec4 &location) {
if (!database.CheckInstanceByCharID(instance_id, CharacterID())) {
database.AddClientToInstance(instance_id, CharacterID());
}
const bool use_coordinates = (
location.x != 0.0f ||
location.y != 0.0f ||
location.z != 0.0f ||
location.w != 0.0f
);
const ZoneMode zone_type = use_coordinates ? ZoneSolicited : ZoneToSafeCoords;
ProcessMovePC(database.GetInstanceZoneID(instance_id), instance_id, location.x, location.y, location.z, location.w, 3, zone_type);
}
void Client::MoveZoneInstanceGroup(uint16 instance_id, const glm::vec4 &location) {
Group* g = GetGroup();
if (!g) {
MoveZoneInstance(instance_id, location);
} else {
for (const auto& gm : g->members) {
if (gm && gm->IsClient()) {
Client* c = gm->CastToClient();
c->MoveZoneInstance(instance_id, location);
}
}
}
}
void Client::MoveZoneInstanceRaid(uint16 instance_id, const glm::vec4 &location) {
Raid* r = GetRaid();
if (!r) {
MoveZoneInstance(instance_id, location);
} else {
for (const auto& rm : r->members) {
if (rm.member && rm.member->IsClient()) {
Client* c = rm.member->CastToClient();
c->MoveZoneInstance(instance_id, location);
}
}
}
}
void Client::ProcessMovePC(uint32 zoneID, uint32 instance_id, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm)
{
// From what I have read, dragged corpses should stay with the player for Intra-zone summons etc, but we can
// implement that later.
ClearDraggedCorpses();
// Added to ensure that if a player is moved (ported, gmmove, etc) and they are an active trader or buyer, they will
// be removed from future transactions.
if (IsTrader()) {
TraderEndTrader();
}
if (IsBuyer()) {
ToggleBuyerMode(false);
}
if(zoneID == 0)
zoneID = zone->GetZoneID();
if(zoneID == zone->GetZoneID() && instance_id == zone->GetInstanceID()) {
// TODO: Determine if this condition is necessary.
if(IsAIControlled()) {
GMMove(x, y, z);
return;
}
if(zm != SummonPC && GetPetID() != 0) {
//if they have a pet and they are staying in zone, move with them
Mob *p = GetPet();
if(p != nullptr){
p->SetPetOrder(PetOrder::Follow);
p->GMMove(x+15, y, z); //so it dosent have to run across the map.
}
}
}
switch(zm) {
case GateToBindPoint:
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case EvacToSafeCoords:
case ZoneToSafeCoords:
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case GMSummon:
Message(Chat::Yellow, "You have been summoned by a GM!");
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case GMHiddenSummon:
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case ZoneToBindPoint:
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case ZoneSolicited:
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case SummonPC:
MessageString(Chat::Yellow, PLAYER_SUMMONED);
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
case Rewind:
Message(Chat::Yellow, "Rewinding to previous location.");
ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm);
break;
default:
LogError("Received a request to perform an unsupported client zone operation");
break;
}
}
void Client::ZonePC(uint32 zoneID, uint32 instance_id, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm) {
bool ReadyToZone = true;
int iZoneNameLength = 0;
const char* pShortZoneName = nullptr;
char* pZoneName = nullptr;
pShortZoneName = ZoneName(zoneID);
auto zd = GetZoneVersionWithFallback(zoneID, zone->GetInstanceVersion());
if (zd) {
pZoneName = strcpy(new char[zd->long_name.length() + 1], zd->long_name.c_str());
}
auto r = WorldContentService::Instance()->FindZone(zoneID, instance_id);
if (r.zone_id) {
zoneID = r.zone_id;
instance_id = r.instance.id;
LogZoning(
"Client caught HandleZoneRoutingMiddleware [{}] zone_id [{}] instance_id [{}] x [{}] y [{}] z [{}] heading [{}] ignorerestrictions [{}] zone_mode [{}]",
GetCleanName(),
zoneID,
instance_id,
x,
y,
z,
heading,
ignorerestrictions,
static_cast<int>(zm)
);
// If we are zoning to the same zone, we need to use the current instance ID if it is not specified.
if (WorldContentService::Instance()->IsInPublicStaticInstance(instance_id) && zoneID == zone->GetZoneID() && instance_id == 0) {
instance_id = zone->GetInstanceID();
}
}
// 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(),
zoneID,
instance_id,
x,
y,
z,
heading,
ignorerestrictions,
static_cast<int>(zm)
);
cheat_manager.SetExemptStatus(Port, true);
if(!pZoneName) {
Message(Chat::Red, "Invalid zone number specified");
safe_delete_array(pZoneName);
return;
}
iZoneNameLength = strlen(pZoneName);
glm::vec4 zone_safe_point;
switch(zm) {
case EvacToSafeCoords:
case ZoneToSafeCoords:
zone_safe_point = zone->GetSafePoint();
x = zone_safe_point.x;
y = zone_safe_point.y;
z = zone_safe_point.z;
heading = zone_safe_point.w;
break;
case GMSummon:
case GMHiddenSummon:
m_Position = glm::vec4(x, y, z, heading);
m_ZoneSummonLocation = m_Position;
zonesummon_id = zoneID;
zonesummon_ignorerestrictions = 1;
break;
case ZoneSolicited:
m_ZoneSummonLocation = glm::vec4(x, y, z, heading);
zonesummon_id = zoneID;
zonesummon_ignorerestrictions = ignorerestrictions;
break;
case GateToBindPoint:
x = m_Position.x = m_pp.binds[0].x;
y = m_Position.y = m_pp.binds[0].y;
z = m_Position.z = m_pp.binds[0].z;
heading = m_pp.binds[0].heading;
break;
case ZoneToBindPoint:
x = m_Position.x = m_pp.binds[0].x;
y = m_Position.y = m_pp.binds[0].y;
z = m_Position.z = m_pp.binds[0].z;
heading = m_pp.binds[0].heading;
zonesummon_ignorerestrictions = 1;
LogDebug("Player [{}] has died and will be zoned to bind point in zone: [{}] at LOC x=[{}], y=[{}], z=[{}], heading=[{}]",
GetName(), pZoneName, m_pp.binds[0].x, m_pp.binds[0].y, m_pp.binds[0].z, m_pp.binds[0].heading);
break;
case SummonPC:
m_ZoneSummonLocation = glm::vec4(x, y, z, heading);
m_Position = m_ZoneSummonLocation;
break;
case Rewind:
LogDebug("[{}] has requested a /rewind from [{}], [{}], [{}], to [{}], [{}], [{}] in [{}]", GetName(),
m_Position.x, m_Position.y, m_Position.z,
m_RewindLocation.x, m_RewindLocation.y, m_RewindLocation.z, zone->GetShortName());
m_ZoneSummonLocation = glm::vec4(x, y, z, heading);
m_Position = m_ZoneSummonLocation;
break;
default:
LogError("Client::ZonePC() received a reguest to perform an unsupported client zone operation");
ReadyToZone = false;
break;
}
if (ReadyToZone)
{
//if client is looting, we need to send an end loot
if (IsLooting())
{
Entity* entity = entity_list.GetID(entity_id_being_looted);
if (entity == 0)
{
LogError("OP_EndLootRequest: Corpse with id of {} not found for {}.", entity_id_being_looted, GetCleanName());
if (ClientVersion() >= EQ::versions::ClientVersion::SoD)
Corpse::SendEndLootErrorPacket(this);
else
Corpse::SendLootReqErrorPacket(this);
}
else if (!entity->IsCorpse())
{
LogError("OP_EndLootRequest: Entity with id of {} was not corpse for {}.", entity_id_being_looted, GetCleanName());
Corpse::SendLootReqErrorPacket(this);
}
else
{
Corpse::SendEndLootErrorPacket(this);
entity->CastToCorpse()->EndLoot(this, nullptr);
}
SetLooting(0);
}
zone_mode = zm;
if (zm == ZoneToBindPoint) {
auto outapp = new EQApplicationPacket(OP_ZonePlayerToBind,
sizeof(ZonePlayerToBind_Struct) + iZoneNameLength);
ZonePlayerToBind_Struct* gmg = (ZonePlayerToBind_Struct*) outapp->pBuffer;
// If we are SoF and later and are respawning from hover, we want the real zone ID, else zero to use the old hack.
//
if(zone->GetZoneID() == zoneID) {
if ((ClientVersionBit() & EQ::versions::maskSoFAndLater) && (!RuleB(Character, RespawnFromHover) || !IsHoveringForRespawn()))
gmg->bind_zone_id = 0;
else
gmg->bind_zone_id = zoneID;
} else {
gmg->bind_zone_id = zoneID;
}
gmg->x = x;
gmg->y = y;
gmg->z = z;
gmg->heading = heading;
strcpy(gmg->zone_name, pZoneName);
outapp->priority = 6;
FastQueuePacket(&outapp);
}
else if (zm == ZoneSolicited || zm == ZoneToSafeCoords) {
auto outapp = new EQApplicationPacket(OP_RequestClientZoneChange, sizeof(RequestClientZoneChange_Struct));
RequestClientZoneChange_Struct* gmg = (RequestClientZoneChange_Struct*) outapp->pBuffer;
gmg->zone_id = zoneID;
gmg->x = x;
gmg->y = y;
gmg->z = z;
gmg->heading = heading;
gmg->instance_id = instance_id;
gmg->type = 0x01; //an observed value, not sure of meaning
LogZoning(
"Player [{}] has requested zoning to zone_id [{}] instance_id [{}] x [{}] y [{}] z [{}] heading [{}]",
GetCleanName(),
zoneID,
instance_id,
x,
y,
z,
heading
);
outapp->priority = 6;
FastQueuePacket(&outapp);
}
else if(zm == EvacToSafeCoords) {
auto outapp = new EQApplicationPacket(OP_RequestClientZoneChange, sizeof(RequestClientZoneChange_Struct));
RequestClientZoneChange_Struct* gmg = (RequestClientZoneChange_Struct*) outapp->pBuffer;
// if we are in the same zone we want to evac to, client will not send OP_ZoneChange back to do an actual
// zoning of the client, so we have to send a viable zoneid that the client *could* zone to to make it believe
// we are leaving the zone, even though we are not. We have to do this because we are missing the correct op code
// and struct that should be used for evac/succor.
// 213 is Plane of War
// 76 is orignial Plane of Hate
// WildcardX 27 January 2008. Tested this for 6.2 and Titanium clients.
if (GetZoneID() == 1) {
gmg->zone_id = 2;
} else {
gmg->zone_id = 1;
}
gmg->x = x;
gmg->y = y;
gmg->z = z;
gmg->heading = heading;
gmg->instance_id = instance_id;
gmg->type = 0x01; // '0x01' was an observed value for the type field, not sure of meaning
// we hide the real zoneid we want to evac/succor to here
zonesummon_id = zoneID;
outapp->priority = 6;
FastQueuePacket(&outapp);
}
else {
if(zoneID == GetZoneID()) {
//properly handle proximities
entity_list.ProcessMove(this, glm::vec3(m_Position));
m_Proximity = glm::vec3(m_Position);
//send out updates to people in zone.
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
}
auto outapp =
new EQApplicationPacket(OP_RequestClientZoneChange, sizeof(RequestClientZoneChange_Struct));
RequestClientZoneChange_Struct* gmg = (RequestClientZoneChange_Struct*) outapp->pBuffer;
gmg->zone_id = zoneID;
gmg->x = x;
gmg->y = y;
gmg->z = z;
gmg->heading = heading;
gmg->instance_id = instance_id;
gmg->type = 0x01; //an observed value, not sure of meaning
outapp->priority = 6;
FastQueuePacket(&outapp);
}
LogDebug("Player [{}] has requested a zoning to LOC x=[{}], y=[{}], z=[{}], heading=[{}] in zoneid=[{}]", GetName(), x, y, z, heading, zoneID);
//Clear zonesummon variables if we're zoning to our own zone
//Client wont generate a zone change packet to the server in this case so
//They aren't needed and it keeps behavior on next zone attempt from being undefined.
if(zoneID == zone->GetZoneID() && instance_id == zone->GetInstanceID())
{
if(zm != EvacToSafeCoords && zm != ZoneToSafeCoords && zm != ZoneToBindPoint)
{
m_ZoneSummonLocation = glm::vec4();
zonesummon_id = 0;
zonesummon_ignorerestrictions = 0;
zone_mode = ZoneUnsolicited;
}
}
}
safe_delete_array(pZoneName);
}
void Client::GoToSafeCoords(uint16 zone_id, uint16 instance_id)
{
if (zone_id == 0) {
zone_id = zone->GetZoneID();
}
MovePC(zone_id, instance_id, 0.0f, 0.0f, 0.0f, 0.0f, 0, ZoneToSafeCoords);
}
void Mob::Gate(uint8 bind_number) {
GoToBind(bind_number);
if (RuleB(NPC, NPCHealOnGate) && IsNPC() && GetHPRatio() <= RuleR(NPC, NPCHealOnGateAmount)) {
auto HealAmount = (RuleR(NPC, NPCHealOnGateAmount) / 100);
SetHP(int64(GetMaxHP() * HealAmount));
}
}
void Client::Gate(uint8 bind_number) {
Mob::Gate(bind_number);
}
void NPC::Gate(uint8 bind_number) {
entity_list.MessageCloseString(this, true, RuleI(Range, SpellMessages), Chat::Spells, GATES, GetCleanName());
Mob::Gate(bind_number);
}
void Client::SetBindPoint(int bind_number, int to_zone, int to_instance, const glm::vec3 &location)
{
if (bind_number < 0 || bind_number >= 4) {
bind_number = 0;
}
if (to_zone == -1) {
m_pp.binds[bind_number].zone_id = zone->GetZoneID();
m_pp.binds[bind_number].instance_id = (zone->GetInstanceID() != 0 && zone->IsInstancePersistent())? zone->GetInstanceID() : 0;
m_pp.binds[bind_number].x = m_Position.x;
m_pp.binds[bind_number].y = m_Position.y;
m_pp.binds[bind_number].z = m_Position.z;
m_pp.binds[bind_number].heading = GetHeading();
}
else {
m_pp.binds[bind_number].zone_id = to_zone;
m_pp.binds[bind_number].instance_id = to_instance;
m_pp.binds[bind_number].x = location.x;
m_pp.binds[bind_number].y = location.y;
m_pp.binds[bind_number].z = location.z;
m_pp.binds[bind_number].heading = GetHeading();
}
database.SaveCharacterBinds(this);
}
void Client::SetBindPoint2(int bind_number, int to_zone, int to_instance, const glm::vec4 &location)
{
if (bind_number < 0 || bind_number >= 4)
bind_number = 0;
if (to_zone == -1) {
m_pp.binds[bind_number].zone_id = zone->GetZoneID();
m_pp.binds[bind_number].instance_id = (zone->GetInstanceID() != 0 && zone->IsInstancePersistent()) ? zone->GetInstanceID() : 0;
m_pp.binds[bind_number].x = m_Position.x;
m_pp.binds[bind_number].y = m_Position.y;
m_pp.binds[bind_number].z = m_Position.z;
m_pp.binds[bind_number].heading = m_Position.w;
} else {
m_pp.binds[bind_number].zone_id = to_zone;
m_pp.binds[bind_number].instance_id = to_instance;
m_pp.binds[bind_number].x = location.x;
m_pp.binds[bind_number].y = location.y;
m_pp.binds[bind_number].z = location.z;
m_pp.binds[bind_number].heading = location.w;
}
database.SaveCharacterBinds(this);
}
void Client::GoToBind(uint8 bind_number) {
// if the bind number is invalid, use the primary bind
if(bind_number > 4)
bind_number = 0;
// move the client, which will zone them if needed.
// ignore restrictions on the zone request..?
if(bind_number == 0)
MovePC(
m_pp.binds[0].zone_id,
m_pp.binds[0].instance_id,
0.0f,
0.0f,
0.0f,
0.0f,
1,
GateToBindPoint
);
else
MovePC(
m_pp.binds[bind_number].zone_id,
m_pp.binds[bind_number].instance_id,
m_pp.binds[bind_number].x,
m_pp.binds[bind_number].y,
m_pp.binds[bind_number].z,
m_pp.binds[bind_number].heading,
1
);
}
void Client::GoToDeath() {
MovePC(m_pp.binds[0].zone_id, m_pp.binds[0].instance_id, 0.0f, 0.0f, 0.0f, 0.0f, 1, ZoneToBindPoint);
}
void Client::ClearZoneFlag(uint32 zone_id)
{
if (!HasZoneFlag(zone_id)) {
return;
}
zone_flags.erase(zone_id);
ZoneFlagsRepository::DeleteWhere(
database,
fmt::format(
"`charID` = {} AND `zoneID` = {}",
CharacterID(),
zone_id
)
);
}
bool Client::HasZoneFlag(uint32 zone_id) const
{
return zone_flags.find(zone_id) != zone_flags.end();
}
void Client::LoadZoneFlags()
{
const auto& l = ZoneFlagsRepository::GetWhere(
database,
fmt::format(
"`charID` = {}",
CharacterID()
)
);
zone_flags.clear();
for (const auto& e : l) {
zone_flags.insert(e.zoneID);
}
}
void Client::SendZoneFlagInfo(Client* to) const
{
if (zone_flags.empty()) {
to->Message(
Chat::White,
fmt::format(
"{} {} no zone flags.",
to->GetTargetDescription(const_cast<Mob*>(CastToMob()), TargetDescriptionType::UCYou),
to == this ? "have" : "has"
).c_str()
);
return;
}
to->Message(
Chat::White,
fmt::format(
"{} {} the following zone flags:",
to->GetTargetDescription(const_cast<Mob*>(CastToMob()), TargetDescriptionType::UCYou),
to == this ? "have" : "has"
).c_str()
);
uint32 flag_count = 0;
for (const auto& zone_id : zone_flags) {
const uint32 flag_number = (flag_count + 1);
const auto& z = GetZone(zone_id);
if (z) {
const std::string& flag_name = z->flag_needed;
to->Message(
Chat::White,
fmt::format(
"Flag {} | Zone: {} ({}) ID: {}{}",
flag_number,
z->long_name,
z->short_name,
zone_id,
(
!flag_name.empty() ?
fmt::format(
" Flag Required: {}",
flag_name
) :
""
)
).c_str()
);
flag_count++;
}
}
to->Message(
Chat::White,
fmt::format(
"{} Zone flag{} found for {}.",
flag_count,
flag_count != 1 ? "s" : "",
to->GetTargetDescription(const_cast<Mob*>(CastToMob()))
).c_str()
);
}
void Client::SetZoneFlag(uint32 zone_id)
{
if (HasZoneFlag(zone_id)) {
return;
}
zone_flags.insert(zone_id);
ZoneFlagsRepository::InsertOne(
database,
ZoneFlagsRepository::ZoneFlags{
.charID = static_cast<int32_t>(CharacterID()),
.zoneID = static_cast<int32_t>(zone_id)
}
);
}
void Client::ClearPEQZoneFlag(uint32 zone_id) {
if (!HasPEQZoneFlag(zone_id)) {
return;
}
peqzone_flags.erase(zone_id);
if (!CharacterPeqzoneFlagsRepository::DeleteFlag(database, CharacterID(), zone_id)) {
LogError("MySQL Error while trying to clear PEQZone flag for [{}]", GetName());
}
}
bool Client::HasPEQZoneFlag(uint32 zone_id) const {
return peqzone_flags.find(zone_id) != peqzone_flags.end();
}
void Client::LoadPEQZoneFlags() {
const auto l = CharacterPeqzoneFlagsRepository::GetWhere(
database,
fmt::format(
"id = {}",
CharacterID()
)
);
if (l.empty()) {
return;
}
peqzone_flags.clear();
for (const auto& f : l) {
peqzone_flags.insert(f.zone_id);
}
}
void Client::SendPEQZoneFlagInfo(Client *to) const {
if (peqzone_flags.empty()) {
to->Message(
Chat::White,
fmt::format(
"{} {} no PEQZone Flags.",
to == this ? "You" : GetName(),
to == this ? "have" : "has"
).c_str()
);
return;
}
to->Message(
Chat::White,
fmt::format(
"{} {} the following PEQZone Flags:",
to == this ? "You" : GetName(),
to == this ? "have" : "has"
).c_str()
);
int flag_count = 0;
for (const auto& zone_id : peqzone_flags) {
int flag_number = (flag_count + 1);
std::string zone_short_name = ZoneName(zone_id, true);
if (zone_short_name != "UNKNOWN") {
std::string zone_long_name = ZoneLongName(zone_id);
to->Message(
Chat::White,
fmt::format(
"Flag {} | Zone: {} ({}) ID: {}",
flag_number,
zone_long_name,
zone_short_name,
zone_id
).c_str()
);
flag_count++;
}
}
to->Message(
Chat::White,
fmt::format(
"{} {} {} PEQZone Flags.",
to == this ? "You" : GetName(),
to == this ? "have" : "has",
flag_count
).c_str()
);
}
void Client::SetPEQZoneFlag(uint32 zone_id) {
if (HasPEQZoneFlag(zone_id)) {
return;
}
peqzone_flags.insert(zone_id);
auto f = CharacterPeqzoneFlagsRepository::NewEntity();
f.id = CharacterID();
f.zone_id = zone_id;
if (!CharacterPeqzoneFlagsRepository::InsertOne(database, f).id) {
LogError("MySQL Error while trying to set zone flag for [{}]", GetName());
}
}
bool Client::CanEnterZone(const std::string& zone_short_name, int16 instance_version) {
//check some critial rules to see if this char needs to be booted from the zone
//only enforce rules here which are serious enough to warrant being kicked from
//the zone
if (Admin() >= RuleI(GM, MinStatusToZoneAnywhere)) {
return true;
}
auto z = GetZoneVersionWithFallback(
zone_short_name.empty() ? ZoneID(zone->GetShortName()) : ZoneID(zone_short_name),
instance_version == -1 ? zone->GetInstanceVersion() : instance_version
);
if (!z) {
return false;
}
if (!GetGM() && GetLevel() < z->min_level) {
LogInfo(
"Character [{}] does not meet minimum level requirement ([{}] < [{}])!",
GetCleanName(),
GetLevel(),
z->min_level
);
return false;
}
if (!GetGM() && GetLevel() > z->max_level) {
LogInfo(
"Character [{}] does not meet maximum level requirement ([{}] > [{}])!",
GetCleanName(),
GetLevel(),
z->max_level
);
return false;
}
if (Admin() < z->min_status) {
LogInfo(
"Character [{}] does not meet minimum status requirement ([{}] < [{}])!",
GetCleanName(),
Admin(),
z->min_status
);
return false;
}
if (
!GetGM() &&
!z->flag_needed.empty() &&
Strings::IsNumber(z->flag_needed) &&
Strings::ToBool(z->flag_needed) &&
!HasZoneFlag(z->zoneidnumber)
) {
LogInfo(
"Character [{}] does not have the flag to be in this zone [{}]!",
GetCleanName(),
z->flag_needed
);
return false;
}
return true;
}