Add DynamicZone class for expedition instancing

Add DynamicZone sql table schema

Add DynamicZones logging category

Modify CreateExpedition to take DynamicZone and ExpeditionRequest objects

Implement DynamicZone compass, safereturn, and zone-in coordinates.

Implement live-like DynamicZone instance kick timer for removed members

Implement updating multiple client compasses (supports existing quest compass)

fix: Send client compass update after entering zones to clear existing compass

Implement Client::MovePCDynamicZone to invoke DynamicZoneSwitchListWnd
when entering a zone where client has multiple dynamic zones assigned

Implement OP_DzChooseZoneReply handling

Add Lua api methods for expedition's associated dynamic zone

Add #dz list gm command to list current DynamicZone instances from database
This commit is contained in:
hg 2020-04-18 14:11:28 -04:00
parent f74605d339
commit 8eef2ae089
35 changed files with 1416 additions and 185 deletions

View File

@ -119,6 +119,7 @@ namespace Logs {
ZonePoints,
Loot,
Expeditions,
DynamicZones,
MaxCategoryID /* Don't Remove this */
};
@ -197,6 +198,7 @@ namespace Logs {
"ZonePoints",
"Loot",
"Expeditions",
"DynamicZones",
};
}

View File

@ -616,6 +616,16 @@
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__);\
@ -976,6 +986,12 @@
#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)

View File

@ -788,6 +788,12 @@ 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, UseDatabaseToVerifyLeaderCommands, false, "Use database instead of zone cache to verify Expedition leader for commands")
RULE_BOOL(Expedition, EmptyDzShutdownEnabled, true, "Enable early instance shutdown after last member of expedition removed")
RULE_INT(Expedition, EmptyDzShutdownDelaySeconds, 900, "Seconds to set dynamic zone instance expiration if early shutdown enabled")
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

View File

@ -151,6 +151,11 @@
#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_DzCharacterChange 0x0450
#define ServerOP_LSInfo 0x1000
#define ServerOP_LSStatus 0x1001
@ -2053,6 +2058,25 @@ struct ServerDzCommand_Struct {
char remove_name[64]; // used for swap command
};
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 instance_id;
uint8 remove; // 0: added 1: removed
uint32 character_id;
};
#pragma pack()
#endif

View File

@ -0,0 +1,25 @@
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`),
CONSTRAINT `FK_dynamic_zones_instance_list` FOREIGN KEY (`instance_id`) REFERENCES `instance_list` (`id`) ON DELETE CASCADE
)
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB
;

View File

@ -30,23 +30,27 @@
extern ClientList client_list;
extern ZSList zoneserver_list;
void Expedition::PurgeEmptyExpeditions()
void Expedition::PurgeExpiredExpeditions()
{
std::string query = SQL(
DELETE expedition FROM expedition_details expedition
LEFT JOIN instance_list ON expedition.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
) AS expedition_members
ON expedition_members.expedition_id = expedition.id
WHERE expedition_members.expedition_id IS NULL OR expedition_members.member_count <= 0
WHERE
expedition.instance_id 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())
{
LogExpeditions("Failed to purge empty expeditions");
LogExpeditions("Failed to purge expired and empty expeditions");
}
}

View File

@ -25,7 +25,7 @@ class ServerPacket;
namespace Expedition
{
void PurgeEmptyExpeditions();
void PurgeExpiredExpeditions();
void PurgeExpiredCharacterLockouts();
void AddPlayer(ServerPacket* pack);
void MakeLeader(ServerPacket* pack);

View File

@ -431,7 +431,7 @@ int main(int argc, char** argv) {
PurgeInstanceTimer.Start(450000);
LogInfo("Purging expired expeditions");
Expedition::PurgeEmptyExpeditions(); //database.PurgeExpiredExpeditions();
Expedition::PurgeExpiredExpeditions();
Expedition::PurgeExpiredCharacterLockouts();
LogInfo("Loading char create info");
@ -604,7 +604,7 @@ int main(int argc, char** argv) {
if (PurgeInstanceTimer.Check()) {
database.PurgeExpiredInstances();
database.PurgeAllDeletedDataBuckets();
Expedition::PurgeEmptyExpeditions();
Expedition::PurgeExpiredExpeditions();
Expedition::PurgeExpiredCharacterLockouts();
}

View File

@ -1375,6 +1375,9 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
case ServerOP_ExpeditionMemberChange:
case ServerOP_ExpeditionMemberSwap:
case ServerOP_ExpeditionMemberStatus:
case ServerOP_ExpeditionDzCompass:
case ServerOP_ExpeditionDzSafeReturn:
case ServerOP_ExpeditionDzZoneIn:
{
zoneserver_list.SendPacket(pack);
break;
@ -1394,6 +1397,16 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
Expedition::MakeLeader(pack);
break;
}
case ServerOP_DzCharacterChange:
{
auto buf = reinterpret_cast<ServerDzCharacter_Struct*>(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);

View File

@ -22,6 +22,7 @@ SET(zone_sources
corpse.cpp
data_bucket.cpp
doors.cpp
dynamiczone.cpp
effects.cpp
embparser.cpp
embparser_api.cpp
@ -170,6 +171,7 @@ SET(zone_headers
corpse.h
data_bucket.h
doors.h
dynamiczone.h
embparser.h
embperl.h
embxs.h

View File

@ -43,6 +43,7 @@ extern volatile bool RunLoops;
#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"
@ -267,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));
@ -6155,24 +6157,19 @@ void Client::CheckEmoteHail(Mob *target, const char* message)
void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count)
{
uint32 entry_size = sizeof(DynamicZoneCompassEntry_Struct) * count;
auto outapp = new EQApplicationPacket(OP_DzCompass, sizeof(DynamicZoneCompass_Struct) + entry_size);
auto outbuf = reinterpret_cast<DynamicZoneCompass_Struct*>(outapp->pBuffer);
outbuf->client_id = 0;
outbuf->count = count;
if (count) {
outbuf->entries[0].dz_zone_id = 0;
outbuf->entries[0].dz_instance_id = 0;
outbuf->entries[0].dz_type = 0;
outbuf->entries[0].x = in_x;
outbuf->entries[0].y = in_y;
outbuf->entries[0].z = in_z;
if (count == 0)
{
m_quest_compass.zone_id = 0;
}
else
{
m_quest_compass.zone_id = zone ? zone->GetZoneID() : 0;
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()
@ -9564,6 +9561,8 @@ void Client::UpdateExpeditionInfoAndLockouts()
{
// this is processed by client after entering a zone
// todo: live re-invites if client zoned with a pending invite window open
SendDzCompassUpdate();
auto expedition = GetExpedition();
if (expedition)
{
@ -9571,7 +9570,7 @@ void Client::UpdateExpeditionInfoAndLockouts()
// live only adds lockouts obtained during the active expedition to new
// members once they zone into the expedition's dynamic zone instance
if (zone && /*zone->GetInstanceID() && zone->GetInstanceID()*/zone->GetZoneID() == expedition->GetInstanceID())
if (expedition->GetDynamicZone().IsCurrentZoneDzInstance())
{
ExpeditionDatabase::AssignPendingLockouts(CharacterID(), expedition->GetName());
expedition->SetMemberStatus(this, ExpeditionMemberStatus::InDynamicZone);
@ -9581,13 +9580,17 @@ void Client::UpdateExpeditionInfoAndLockouts()
expedition->SetMemberStatus(this, ExpeditionMemberStatus::Online);
}
}
Expedition::LoadAllClientLockouts(this);
}
Expedition* Client::CreateExpedition(
std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer)
std::string zone_name, uint32 version, uint32 duration,
std::string expedition_name, uint32 min_players, uint32 max_players, bool has_replay_timer)
{
return Expedition::TryCreate(this, name, min_players, max_players, has_replay_timer);
DynamicZone dz_instance{ zone_name, version, duration, DynamicZoneType::Expedition };
ExpeditionRequest request{ expedition_name, min_players, max_players, has_replay_timer };
return Expedition::TryCreate(this, dz_instance, request);
}
Expedition* Client::GetExpedition() const
@ -9616,30 +9619,6 @@ std::vector<ExpeditionLockoutTimer> Client::GetExpeditionLockouts(const std::str
return lockouts;
}
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::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db)
{
// todo: support for account based lockouts like live AoC expeditions
@ -9744,3 +9723,201 @@ void Client::SendExpeditionLockoutTimers()
}
QueuePacket(outapp.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()
{
// a client may be associated with multiple dynamic zones with compasses
// in the same zone. any systems that use dynamic zones need checked here
std::vector<DynamicZoneCompassEntry_Struct> compass_entries;
Expedition* expedition = GetExpedition();
if (expedition)
{
auto compass = expedition->GetDynamicZone().GetCompassLocation();
if (zone && zone->GetZoneID() == compass.zone_id)
{
DynamicZoneCompassEntry_Struct entry;
entry.dz_zone_id = static_cast<uint16_t>(expedition->GetDynamicZone().GetZoneID());
entry.dz_instance_id = static_cast<uint16_t>(expedition->GetDynamicZone().GetInstanceID());
entry.dz_type = static_cast<uint8_t>(expedition->GetDynamicZone().GetType());
entry.x = compass.x;
entry.y = compass.y;
entry.z = compass.z;
compass_entries.emplace_back(entry);
}
}
// todo: shared tasks, missions, and quests with an associated dz
// compass set via MarkSingleCompassLocation()
if (zone && zone->GetZoneID() == m_quest_compass.zone_id)
{
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<uint32_t>(compass_entries.size());
uint32 entries_size = sizeof(DynamicZoneCompassEntry_Struct) * count;
uint32 outsize = sizeof(DynamicZoneCompass_Struct) + entries_size;
auto outapp = std::unique_ptr<EQApplicationPacket>(new EQApplicationPacket(OP_DzCompass, outsize));
auto outbuf = reinterpret_cast<DynamicZoneCompass_Struct*>(outapp->pBuffer);
outbuf->client_id = 0;
outbuf->count = count;
memcpy(outbuf->entries, compass_entries.data(), entries_size);
QueuePacket(outapp.get());
}
void Client::GoToDzSafeReturnOrBind(const DynamicZoneLocation& safereturn)
{
LogDynamicZonesDetail(
"Sending character [{}] in zone [{}]:[{}] to safereturn [{}] at ([{}], [{}], [{}], [{}]) or bind",
CharacterID(),
zone ? zone->GetZoneID() : 0,
zone ? zone->GetInstanceID() : 0,
safereturn.zone_id,
safereturn.x,
safereturn.y,
safereturn.z,
safereturn.heading
);
if (safereturn.zone_id == 0)
{
GoToBind();
}
else
{
MovePC(safereturn.zone_id, 0, safereturn.x, safereturn.y, safereturn.z, safereturn.heading);
}
}
void Client::MovePCDynamicZone(uint32 zone_id)
{
if (zone_id == 0)
{
return;
}
// check client systems for any associated dynamic zones to the requested zone id
std::vector<DynamicZoneChooseZoneEntry_Struct> client_dzs;
DynamicZone single_dz;
Expedition* expedition = GetExpedition();
if (expedition && expedition->GetDynamicZone().GetZoneID() == zone_id)
{
single_dz = expedition->GetDynamicZone();
DynamicZoneChooseZoneEntry_Struct dz;
dz.dz_zone_id = expedition->GetDynamicZone().GetZoneID();
dz.dz_instance_id = expedition->GetDynamicZone().GetInstanceID();
dz.dz_type = static_cast<uint8_t>(expedition->GetDynamicZone().GetType());
//dz.unknown_id2 = expedition->GetDynamicZone().GetRealID();
strn0cpy(dz.description, expedition->GetName().c_str(), sizeof(dz.description));
strn0cpy(dz.leader_name, expedition->GetLeaderName().c_str(), sizeof(dz.leader_name));
client_dzs.emplace_back(dz);
}
// todo: check for Missions (Shared Tasks), Quests, or Tasks that have associated dzs to zone_id
if (client_dzs.empty())
{
MessageString(Chat::Red, DYNAMICZONE_WAY_IS_BLOCKED); // unconfirmed message
}
else if (client_dzs.size() == 1)
{
if (single_dz.GetInstanceID() == 0)
{
LogDynamicZones("Character [{}] has dz for zone [{}] with no instance id", CharacterID(), zone_id);
}
else
{
DynamicZoneLocation zonein = single_dz.GetZoneInLocation();
ZoneMode zone_mode = ZoneMode::ZoneToSafeCoords;
if (single_dz.HasZoneInLocation())
{
zone_mode = ZoneMode::ZoneSolicited;
}
MovePC(zone_id, single_dz.GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode);
}
}
else if (client_dzs.size() > 1)
{
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<uint32_t>(client_dzs.size());
uint32 entries_size = sizeof(DynamicZoneChooseZoneEntry_Struct) * count;
uint32 outsize = sizeof(DynamicZoneChooseZone_Struct) + entries_size;
auto outapp = std::unique_ptr<EQApplicationPacket>(new EQApplicationPacket(OP_DzChooseZone, outsize));
auto outbuf = reinterpret_cast<DynamicZoneChooseZone_Struct*>(outapp->pBuffer);
outbuf->client_id = 0;
outbuf->count = count;
memcpy(outbuf->choices, client_dzs.data(), entries_size);
QueuePacket(outapp.get());
}
}
void Client::MovePCDynamicZone(const std::string& zone_name)
{
auto zone_id = ZoneID(zone_name.c_str());
MovePCDynamicZone(zone_id);
}

View File

@ -54,6 +54,7 @@ namespace EQ
#include "aggromanager.h"
#include "common.h"
#include "dynamiczone.h"
#include "merc.h"
#include "mob.h"
#include "qglobals.h"
@ -1116,10 +1117,13 @@ public:
void AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db = false);
void AddNewExpeditionLockout(const std::string& expedition_name, const std::string& event_name, uint32_t duration);
Expedition* CreateExpedition(std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer = false);
Expedition* CreateExpedition(
std::string zone_name, uint32 version, uint32 duration, std::string expedition_name,
uint32 min_players, uint32 max_players, bool has_replay_timer = 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 ExpeditionLockoutTimer* GetExpeditionLockout(
const std::string& expedition_name, const std::string& event_name, bool include_expired = false) const;
const std::vector<ExpeditionLockoutTimer>& GetExpeditionLockouts() const { return m_expedition_lockouts; };
std::vector<ExpeditionLockoutTimer> GetExpeditionLockouts(const std::string& expedition_name);
uint32 GetPendingExpeditionInviteID() const { return m_pending_expedition_invite_id; }
@ -1131,6 +1135,11 @@ public:
void SetExpeditionID(uint32 expedition_id) { m_expedition_id = expedition_id; };
void UpdateExpeditionInfoAndLockouts();
void DzListTimers();
void SetDzRemovalTimer(bool enable_timer);
void SendDzCompassUpdate();
void GoToDzSafeReturnOrBind(const DynamicZoneLocation& safereturn);
void MovePCDynamicZone(uint32 zone_id);
void MovePCDynamicZone(const std::string& zone_name);
void CalcItemScale();
bool CalcItemScale(uint32 slot_x, uint32 slot_y); // behavior change: 'slot_y' is now [RANGE]_END and not [RANGE]_END + 1
@ -1585,6 +1594,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;
@ -1688,8 +1698,8 @@ private:
uint32 m_expedition_id = 0;
uint32 m_pending_expedition_invite_id = 0;
Expedition* m_expedition = nullptr;
std::vector<ExpeditionLockoutTimer> m_expedition_lockouts;
DynamicZoneLocation m_quest_compass;
#ifdef BOTS

View File

@ -5637,9 +5637,30 @@ void Client::Handle_OP_DzAddPlayer(const EQApplicationPacket *app)
void Client::Handle_OP_DzChooseZoneReply(const EQApplicationPacket *app)
{
// todo: implement
LogExpeditionsModerate("Handle_OP_DzChooseZoneReply");
auto dzmsg = reinterpret_cast<DynamicZoneChooseZoneReply_Struct*>(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;
}
DynamicZone dz = DynamicZone::LoadDzFromDatabase(dzmsg->dz_instance_id);
DynamicZoneLocation loc = dz.GetZoneInLocation();
ZoneMode zone_mode = ZoneMode::ZoneToSafeCoords;
if (dz.HasZoneInLocation())
{
zone_mode = ZoneMode::ZoneSolicited;
}
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)

View File

@ -161,6 +161,16 @@ bool Client::Process() {
if (TaskPeriodic_Timer.Check() && taskstate)
taskstate->TaskPeriodicChecks(this);
if (dynamiczone_removal_timer.Check())
{
dynamiczone_removal_timer.Disable();
if (zone && zone->GetInstanceID() != 0)
{
DynamicZone dz = DynamicZone::LoadDzFromDatabase(zone->GetInstanceID());
GoToDzSafeReturnOrBind(dz.GetSafeReturnLocation());
}
}
if (linkdead_timer.Check()) {
LeaveGroup();
Save();

View File

@ -6837,22 +6837,24 @@ void command_dz(Client* c, const Seperator* sep)
{
if (strcasecmp(sep->arg[2], "list") == 0)
{
c->Message(Chat::White, "Total Active Expeditions: [%u]", static_cast<uint32>(zone->expedition_cache.size()));
c->Message(Chat::White, fmt::format("Total Active Expeditions: [{}]", zone->expedition_cache.size()).c_str());
for (const auto& expedition : zone->expedition_cache)
{
c->Message(
Chat::White, "Expedition id: [%u]: leader: [%s] instance id: [%u] members: [%u]",
c->Message(Chat::White, fmt::format(
"Expedition id: [{}]: leader: [{}] instance id: [{}] members: [{}]",
expedition.second->GetID(),
expedition.second->GetLeaderName().c_str(),
expedition.second->GetLeaderName(),
expedition.second->GetInstanceID(),
expedition.second->GetMemberCount()
);
).c_str());
}
}
else if (strcasecmp(sep->arg[2], "reload") == 0)
{
Expedition::CacheAllFromDatabase();
c->Message(Chat::White, "Reloaded [%u] expeditions to cache from database.", static_cast<uint32>(zone->expedition_cache.size()));
c->Message(Chat::White, fmt::format(
"Reloaded [{}] expeditions to cache from database.", zone->expedition_cache.size()
).c_str());
}
}
else if (strcasecmp(sep->arg[1], "destroy") == 0)
@ -6870,12 +6872,53 @@ void command_dz(Client* c, const Seperator* sep)
}
}
}
else if (strcasecmp(sep->arg[1], "list") == 0)
{
std::string query = SQL(
SELECT
dynamic_zones.type,
instance_list.id,
instance_list.zone,
instance_list.version,
instance_list.start_time,
instance_list.duration,
COUNT(instance_list.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;
);
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[4], nullptr, 10);
auto duration = strtoul(row[5], nullptr, 10);
auto expire_time = std::chrono::system_clock::from_time_t(start_time + duration);
bool is_expired = std::chrono::system_clock::now() > expire_time;
c->Message(Chat::White, fmt::format(
"type: [{}] instance: [{}] zone: [{}] version: [{}] members: [{}] expired: [{}]",
strtoul(row[0], nullptr, 10),
strtoul(row[1], nullptr, 10),
strtoul(row[2], nullptr, 10),
strtoul(row[3], nullptr, 10),
strtoul(row[6], nullptr, 10),
is_expired
).c_str());
}
}
}
else
{
c->Message(Chat::White, "#dz usage:");
c->Message(Chat::White, "#dz cache list - list expeditions in current zone cache");
c->Message(Chat::White, "#dz cache reload - reload zone cache from database");
c->Message(Chat::White, "#dz destroy <expedition_id> - destroy expedition globally (must be in cache)");
c->Message(Chat::White, "#dz list - list all dynamic zones with corresponding instance ids from database");
}
}

View File

@ -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;

507
zone/dynamiczone.cpp Normal file
View File

@ -0,0 +1,507 @@
/**
* 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"
#include <chrono>
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);
}
}
DynamicZone DynamicZone::LoadDzFromDatabase(uint32_t instance_id)
{
DynamicZone dynamic_zone;
if (instance_id != 0)
{
dynamic_zone.LoadFromDatabase(instance_id);
}
return dynamic_zone;
}
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;
}
auto start_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
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);
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_start_time = static_cast<uint32_t>(start_time);
m_never_expires = false;
m_expire_time = std::chrono::system_clock::from_time_t(m_start_time + m_duration);
return m_instance_id;
}
void DynamicZone::LoadFromDatabase(uint32_t instance_id)
{
if (instance_id == 0)
{
return;
}
if (m_instance_id)
{
LogDynamicZones(
"Loading instance data for [{}] failed, instance id [{}] data already loaded",
instance_id, m_instance_id
);
return;
}
LogDynamicZonesDetail("Loading dz instance [{}] from database", instance_id);
std::string query = fmt::format(SQL(
SELECT
instance_list.zone,
instance_list.version,
instance_list.start_time,
instance_list.duration,
instance_list.never_expires,
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
WHERE dynamic_zones.instance_id = {};
), instance_id);
auto results = database.QueryDatabase(query);
if (results.Success() && results.RowCount() > 0)
{
auto row = results.begin();
m_instance_id = instance_id;
m_zone_id = strtoul(row[0], nullptr, 10);
m_version = strtoul(row[1], nullptr, 10);
m_start_time = strtoul(row[2], nullptr, 10);
m_duration = strtoul(row[3], nullptr, 10);
m_never_expires = (strtoul(row[4], nullptr, 10) != 0);
m_type = static_cast<DynamicZoneType>(strtoul(row[5], nullptr, 10));
m_expire_time = std::chrono::system_clock::from_time_t(m_start_time + m_duration);
m_compass.zone_id = strtoul(row[6], nullptr, 10);
m_compass.x = strtof(row[7], nullptr);
m_compass.y = strtof(row[8], nullptr);
m_compass.z = strtof(row[9], nullptr);
m_safereturn.zone_id = strtoul(row[10], nullptr, 10);
m_safereturn.x = strtof(row[11], nullptr);
m_safereturn.y = strtof(row[12], nullptr);
m_safereturn.z = strtof(row[13], nullptr);
m_safereturn.heading = strtof(row[14], nullptr);
m_zonein.x = strtof(row[15], nullptr);
m_zonein.y = strtof(row[16], nullptr);
m_zonein.z = strtof(row[17], nullptr);
m_zonein.heading = strtof(row[18], nullptr);
m_has_zonein = (strtoul(row[19], 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<uint8_t>(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::DeleteFromDatabase()
{
LogDynamicZonesDetail("Deleting dz instance [{}] from database", m_instance_id);
if (m_instance_id != 0)
{
std::string query = fmt::format(SQL(
DELETE FROM dynamic_zones WHERE instance_id = {};
), 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()
{
// caller has to notify clients of instance change since we don't hold members here
if (m_instance_id != 0)
{
database.RemoveClientsFromInstance(m_instance_id);
}
}
void DynamicZone::SaveInstanceMembersToDatabase(const std::unordered_set<uint32_t> character_ids)
{
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);
auto results = database.QueryDatabase(query);
if (!results.Success())
{
LogDynamicZones("Failed to save instance members to database");
}
}
}
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<ServerPacket>(new ServerPacket(ServerOP_DzCharacterChange, packsize));
auto packbuf = reinterpret_cast<ServerDzCharacter_Struct*>(pack->pBuffer);
packbuf->instance_id = GetInstanceID();
packbuf->remove = removed;
packbuf->character_id = character_id;
worldserver.SendPacket(pack.get());
}
}
void DynamicZone::UpdateExpireTime(uint32_t seconds)
{
if (GetInstanceID() == 0)
{
return;
}
m_duration = seconds;
m_expire_time = std::chrono::system_clock::now() + std::chrono::seconds(seconds);
auto new_duration = std::chrono::system_clock::to_time_t(m_expire_time) - m_start_time;
std::string query = fmt::format(SQL(
UPDATE instance_list SET duration = {} WHERE id = {};
), new_duration, GetInstanceID());
auto results = database.QueryDatabase(query);
if (results.Success())
{
uint32_t packsize = sizeof(ServerInstanceUpdateTime_Struct);
auto pack = std::unique_ptr<ServerPacket>(new ServerPacket(ServerOP_InstanceUpdateTime, packsize));
auto packbuf = reinterpret_cast<ServerInstanceUpdateTime_Struct*>(pack->pBuffer);
packbuf->instance_id = GetInstanceID();
packbuf->new_duration = seconds;
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);
}
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<uint32_t>(std::chrono::duration_cast<std::chrono::seconds>(remaining).count());
}
return 0;
}
void DynamicZone::HandleWorldMessage(ServerPacket* pack)
{
switch (pack->opcode)
{
case ServerOP_DzCharacterChange:
{
auto buf = reinterpret_cast<ServerDzCharacter_Struct*>(pack->pBuffer);
Client* client = entity_list.GetClientByCharID(buf->character_id);
if (client)
{
client->SetDzRemovalTimer(buf->remove); // instance kick timer
}
break;
}
}
}

117
zone/dynamiczone.h Normal file
View File

@ -0,0 +1,117 @@
/**
* 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 <chrono>
#include <cstdint>
#include <string>
#include <unordered_set>
#include <vector>
class ServerPacket;
enum class DynamicZoneType : uint8_t // DynamicZoneActiveType
{
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() {}
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(DynamicZoneType type) : m_type(type) { }
static DynamicZone LoadDzFromDatabase(uint32_t instance_id);
static void HandleWorldMessage(ServerPacket* pack);
DynamicZoneType GetType() const { return m_type; }
DynamicZoneLocation GetCompassLocation() const { return m_compass; }
DynamicZoneLocation GetSafeReturnLocation() const { return m_safereturn; }
DynamicZoneLocation GetZoneInLocation() const { return m_zonein; }
uint32_t CreateInstance();
void AddCharacter(uint32_t character_id);
void SaveInstanceMembersToDatabase(const std::unordered_set<uint32_t> character_ids);
uint64_t GetExpireTime() const { return std::chrono::system_clock::to_time_t(m_expire_time); }
uint16_t GetInstanceID() const { return static_cast<uint16_t>(m_instance_id); };
//uint32_t GetRealID() const { return (m_instance_id << 16) | (m_zone_id & 0xffff); }
uint32_t GetSecondsRemaining() const;
uint16_t GetZoneID() const { return static_cast<uint16_t>(m_zone_id); };
uint32_t GetZoneVersion() const { return m_version; };
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; }
void RemoveAllCharacters();
void RemoveCharacter(uint32_t character_id);
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 UpdateExpireTime(uint32_t seconds);
void LoadFromDatabase(uint32_t instance_id);
uint32_t SaveToDatabase();
private:
void DeleteFromDatabase();
void SaveCompassToDatabase();
void SaveSafeReturnToDatabase();
void SaveZoneInLocationToDatabase();
uint32_t m_zone_id = 0;
uint32_t m_instance_id = 0;
uint32_t m_version = 0;
uint32_t m_start_time = 0;
uint32_t m_duration = 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::time_point<std::chrono::system_clock> m_expire_time; //uint64_t m_expire_time = 0;
};
#endif

View File

@ -5202,3 +5202,26 @@ std::unordered_map<uint16, Mob *> &EntityList::GetCloseMobList(Mob *mob, float d
return mob_list;
}
void EntityList::GateAllClientsToSafeReturn()
{
DynamicZone dz;
if (zone)
{
dz = DynamicZone::LoadDzFromDatabase(zone->GetInstanceID());
LogDynamicZones(
"Sending all clients in zone: [{}] instance: [{}] to dz safereturn or bind",
zone->GetZoneID(), zone->GetInstanceID()
);
}
for (const auto& client_list_iter : client_list)
{
Client* client = client_list_iter.second;
if (client)
{
// falls back to gating clients to bind if dz invalid
client->GoToDzSafeReturnOrBind(dz.GetSafeReturnLocation());
}
}
}

View File

@ -49,6 +49,7 @@ class Raid;
class Spawn2;
class Trap;
struct DynamicZoneSafeReturn;
struct GuildBankItemUpdate_Struct;
struct NewSpawn_Struct;
struct QGlobal;
@ -496,6 +497,8 @@ public:
void UpdateFindableNPCState(NPC *n, bool Remove);
void HideCorpses(Client *c, uint8 CurrentMode, uint8 NewMode);
void GateAllClientsToSafeReturn();
uint16 GetClientCount();
void GetMobList(std::list<Mob*> &m_list);
void GetNPCList(std::list<NPC*> &n_list);

View File

@ -26,8 +26,8 @@
#include "groups.h"
#include "raids.h"
#include "string_ids.h"
#include "zonedb.h"
#include "worldserver.h"
#include "zonedb.h"
#include "../common/eqemu_logsys.h"
extern WorldServer worldserver;
@ -46,10 +46,11 @@ const uint32_t Expedition::REPLAY_TIMER_ID = std::numeric_limits<uint32_t>::max(
const uint32_t Expedition::EVENT_TIMER_ID = 1;
Expedition::Expedition(
uint32_t id, std::string expedition_name, const ExpeditionMember& leader,
uint32_t min_players, uint32_t max_players, bool replay_timer
uint32_t id, const DynamicZone& dynamic_zone, std::string expedition_name,
const ExpeditionMember& leader, uint32_t min_players, uint32_t max_players, bool replay_timer
) :
m_id(id),
m_dynamiczone(dynamic_zone),
m_expedition_name(expedition_name),
m_leader(leader),
m_min_players(min_players),
@ -59,7 +60,7 @@ Expedition::Expedition(
}
Expedition* Expedition::TryCreate(
Client* requester, std::string name, uint32_t min_players, uint32_t max_players, bool replay_timer)
Client* requester, DynamicZone& dynamiczone, ExpeditionRequest& request)
{
if (!requester || !zone)
{
@ -67,10 +68,25 @@ Expedition* Expedition::TryCreate(
}
// request parses leader, members list, and lockouts while validating
ExpeditionRequest request(requester, name, min_players, max_players, replay_timer);
if (!request.Validate())
if (!request.Validate(requester))
{
LogExpeditionsModerate("Creation of [{}] by [{}] denied", name, requester->GetName());
LogExpeditionsModerate(
"Creation of [{}] by [{}] denied", request.GetExpeditionName(), requester->GetName()
);
return nullptr;
}
if (dynamiczone.GetInstanceID() == 0)
{
dynamiczone.CreateInstance();
}
if (dynamiczone.GetInstanceID() == 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;
}
@ -78,19 +94,33 @@ Expedition* Expedition::TryCreate(
// unique expedition ids are created from database via auto-increment column
auto expedition_id = ExpeditionDatabase::InsertExpedition(
name, leader.char_id, min_players, max_players, replay_timer
dynamiczone.GetInstanceID(),
request.GetExpeditionName(),
leader.char_id,
request.GetMinPlayers(),
request.GetMaxPlayers(),
request.HasReplayTimer()
);
if (expedition_id)
{
auto expedition = std::make_unique<Expedition>(
expedition_id, name, leader, min_players, max_players, replay_timer
);
dynamiczone.SaveToDatabase();
auto expedition = std::unique_ptr<Expedition>(new Expedition(
expedition_id,
dynamiczone,
request.GetExpeditionName(),
leader,
request.GetMinPlayers(),
request.GetMaxPlayers(),
request.HasReplayTimer()
));
LogExpeditions(
"Created [{}] ({}) leader: [{}] minplayers: [{}] maxplayers: [{}]",
"Created [{}] ({}) instance id: [{}] leader: [{}] minplayers: [{}] maxplayers: [{}]",
expedition->GetID(),
expedition->GetName(),
expedition->GetInstanceID(),
expedition->GetLeaderName(),
expedition->GetMinPlayers(),
expedition->GetMaxPlayers()
@ -104,7 +134,7 @@ Expedition* Expedition::TryCreate(
Client* leader_client = request.GetLeaderClient();
Client::SendCrossZoneMessageString(
leader_client, leader.name, Chat::Yellow, EXPEDITION_AVAILABLE, { name }
leader_client, leader.name, Chat::Yellow, EXPEDITION_AVAILABLE, { request.GetExpeditionName() }
);
auto inserted = zone->expedition_cache.emplace(expedition_id, std::move(expedition));
@ -129,15 +159,19 @@ void Expedition::CacheExpeditions(MySQLRequestResult& results)
{
auto leader_id = static_cast<uint32_t>(strtoul(row[3], nullptr, 10));
ExpeditionMember leader{ leader_id, row[7] }; // id, name
auto instance_id = row[1] ? strtoul(row[1], nullptr, 10) : 0; // can be null from fk constraint
std::unique_ptr<Expedition> expedition = std::make_unique<Expedition>(
DynamicZone dynamic_zone = DynamicZone::LoadDzFromDatabase(instance_id);
std::unique_ptr<Expedition> expedition = std::unique_ptr<Expedition>(new Expedition(
expedition_id,
dynamic_zone,
row[2], // expedition name
leader, // expedition leader
strtoul(row[4], nullptr, 10), // min_players
strtoul(row[5], nullptr, 10), // max_players
(strtoul(row[6], nullptr, 10) != 0) // has_replay_timer
);
));
expedition->LoadMembers();
expedition->SendUpdatesToZoneMembers();
@ -145,7 +179,6 @@ void Expedition::CacheExpeditions(MySQLRequestResult& results)
// don't bother caching empty expeditions
if (expedition->GetMemberCount() > 0)
{
expedition->SendWorldGetOnlineMembers();
zone->expedition_cache.emplace(expedition_id, std::move(expedition));
}
}
@ -214,7 +247,7 @@ bool Expedition::CacheAllFromDatabase()
auto end = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::duration<float>>(end - start);
LogExpeditions("Caching [{}] expeditions took {}s", zone->expedition_cache.size(), elapsed.count());
LogExpeditions("Caching [{}] expedition(s) took {}s", zone->expedition_cache.size(), elapsed.count());
return true;
}
@ -251,8 +284,9 @@ void Expedition::LoadMembers()
{
auto character_id = strtoul(row[0], nullptr, 10);
bool is_current_member = strtoul(row[1], nullptr, 10);
AddInternalMember(row[2], character_id, is_current_member, true);
AddInternalMember(row[2], character_id, ExpeditionMemberStatus::Offline, is_current_member);
}
SendWorldGetOnlineMembers();
}
}
@ -270,6 +304,7 @@ void Expedition::SaveMembers(ExpeditionRequest& request)
m_member_id_history.emplace(member.char_id);
}
ExpeditionDatabase::InsertMembers(m_id, m_members);
m_dynamiczone.SaveInstanceMembersToDatabase(m_member_id_history); // all are current members here
}
Expedition* Expedition::FindCachedExpeditionByCharacterID(uint32_t character_id)
@ -317,9 +352,13 @@ Expedition* Expedition::FindCachedExpeditionByID(uint32_t expedition_id)
Expedition* Expedition::FindExpeditionByInstanceID(uint32_t instance_id)
{
// ask database since it may have expired
auto expedition_id = ExpeditionDatabase::GetExpeditionIDFromInstanceID(instance_id);
return Expedition::FindCachedExpeditionByID(expedition_id);
if (instance_id)
{
// ask database since it may have expired
auto expedition_id = ExpeditionDatabase::GetExpeditionIDFromInstanceID(instance_id);
return Expedition::FindCachedExpeditionByID(expedition_id);
}
return nullptr;
}
bool Expedition::HasLockout(const std::string& event_name)
@ -329,7 +368,7 @@ bool Expedition::HasLockout(const std::string& event_name)
bool Expedition::HasReplayLockout()
{
return (m_lockouts.find(DZ_REPLAY_TIMER_NAME) != m_lockouts.end());
return HasLockout(DZ_REPLAY_TIMER_NAME);
}
bool Expedition::HasMember(uint32_t character_id)
@ -419,7 +458,7 @@ void Expedition::AddInternalLockout(ExpeditionLockoutTimer&& lockout_timer)
}
void Expedition::AddInternalMember(
const std::string& char_name, uint32_t character_id, bool is_current_member, bool offline)
const std::string& char_name, uint32_t character_id, ExpeditionMemberStatus status, bool is_current_member)
{
if (is_current_member)
{
@ -430,7 +469,6 @@ void Expedition::AddInternalMember(
if (it == m_members.end())
{
auto status = offline ? ExpeditionMemberStatus::Offline : ExpeditionMemberStatus::Online;
m_members.emplace_back(ExpeditionMember{character_id, char_name, status});
}
}
@ -446,6 +484,7 @@ bool Expedition::AddMember(const std::string& add_char_name, uint32_t add_char_i
}
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);
@ -453,8 +492,24 @@ bool Expedition::AddMember(const std::string& add_char_name, uint32_t add_char_i
return true;
}
void Expedition::RemoveAllMembers()
void Expedition::RemoveAllMembers(bool enable_removal_timers, bool update_dz_expire_time)
{
m_dynamiczone.RemoveAllCharacters();
if (enable_removal_timers)
{
// expedition holds member list (not dz) so inform dz members to start kick timers
for (const auto& member : m_members)
{
m_dynamiczone.SendInstanceCharacterChange(member.char_id, true);
}
}
if (update_dz_expire_time && RuleB(Expedition, EmptyDzShutdownEnabled))
{
m_dynamiczone.UpdateExpireTime(RuleI(Expedition, EmptyDzShutdownDelaySeconds));
}
ExpeditionDatabase::DeleteAllMembers(m_id);
ExpeditionDatabase::DeleteExpedition(m_id);
@ -471,6 +526,7 @@ bool Expedition::RemoveMember(const std::string& remove_char_name)
}
ExpeditionDatabase::UpdateMemberRemoved(m_id, member.char_id);
m_dynamiczone.RemoveCharacter(member.char_id);
ProcessMemberRemoved(member.name, member.char_id);
SendWorldMemberChanged(member.name, member.char_id, true);
@ -480,11 +536,14 @@ bool Expedition::RemoveMember(const std::string& remove_char_name)
{
ChooseNewLeader();
}
if (m_members.empty())
else if (m_members.empty())
{
// cache removal will occur in world message handler
ExpeditionDatabase::DeleteExpedition(m_id);
if (RuleB(Expedition, EmptyDzShutdownEnabled))
{
m_dynamiczone.UpdateExpireTime(RuleI(Expedition, EmptyDzShutdownDelaySeconds));
}
}
return true;
@ -506,6 +565,8 @@ void Expedition::SwapMember(Client* add_client, const std::string& remove_char_n
// make remove and add atomic to avoid racing with separate world messages
ExpeditionDatabase::UpdateMemberRemoved(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());
@ -710,44 +771,36 @@ void Expedition::DzInviteResponse(
add_client->GetName(), accepted, has_swap_name, swap_remove_name
);
// if client accepts the invite we need to re-confirm there's no conflicts
// a null leader_client is handled by SendLeaderMessage fallbacks
// note current leader receives invite reply messages (if leader changed)
bool was_swap_invite = (has_swap_name && !swap_remove_name.empty());
// null leader_client is handled by SendLeaderMessage fallbacks
Client* leader_client = entity_list.GetClientByCharID(m_leader.char_id);
bool has_conflicts = false;
if (accepted)
if (!accepted)
{
has_conflicts = ProcessAddConflicts(leader_client, add_client, was_swap_invite);
SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_DECLINED, { add_client->GetName() });
return;
}
bool was_swap_invite = (has_swap_name && !swap_remove_name.empty());
bool has_conflicts = ProcessAddConflicts(leader_client, add_client, was_swap_invite);
// error if swapping and character was already removed before the accept
if (accepted && was_swap_invite && !HasMember(swap_remove_name))
if (was_swap_invite && !HasMember(swap_remove_name))
{
has_conflicts = true;
}
if (accepted && !has_conflicts)
{
SendLeaderMessage(leader_client, Chat::Yellow, EXPEDITION_INVITE_ACCEPTED, { add_client->GetName() });
}
else if (accepted)
if (has_conflicts)
{
SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_ERROR, { add_client->GetName() });
}
else
{
SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_DECLINED, { add_client->GetName() });
}
SendLeaderMessage(leader_client, Chat::Yellow, EXPEDITION_INVITE_ACCEPTED, { add_client->GetName() });
if (accepted && !has_conflicts)
{
// insert pending lockouts client will receive when entering dynamic zone.
// only lockouts missing from the client when they join are added. all
// missing lockouts are not applied on entering instance because client may
// have a lockout that expires after joining and shouldn't receive it again.
// only lockouts missing from client when they join are added. client may
// have a lockout that expires after joining and shouldn't receive it again
ExpeditionDatabase::DeletePendingLockouts(add_client->CharacterID());
std::vector<ExpeditionLockoutTimer> pending_lockouts;
@ -771,7 +824,10 @@ void Expedition::DzInviteResponse(
}
}
ExpeditionDatabase::InsertCharacterLockouts(add_client->CharacterID(), pending_lockouts, false, true);
bool add_immediately = m_dynamiczone.IsCurrentZoneDzInstance();
ExpeditionDatabase::InsertCharacterLockouts(
add_client->CharacterID(), pending_lockouts, false, !add_immediately);
if (was_swap_invite)
{
@ -781,6 +837,11 @@ void Expedition::DzInviteResponse(
{
AddMember(add_client->GetName(), add_client->CharacterID());
}
if (m_dynamiczone.IsCurrentZoneDzInstance())
{
SetMemberStatus(add_client, ExpeditionMemberStatus::InDynamicZone);
}
}
}
@ -1083,11 +1144,12 @@ void Expedition::ProcessMemberAdded(std::string char_name, uint32_t added_char_i
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());
}
AddInternalMember(char_name, added_char_id);
AddInternalMember(char_name, added_char_id, ExpeditionMemberStatus::Online);
SendUpdatesToZoneMembers(); // live sends full update when member added
}
@ -1116,6 +1178,7 @@ void Expedition::ProcessMemberRemoved(std::string removed_char_name, uint32_t re
{
ExpeditionDatabase::DeletePendingLockouts(member_client->CharacterID());
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()
@ -1163,7 +1226,6 @@ void Expedition::SendUpdatesToZoneMembers(bool clear)
{
if (!m_members.empty())
{
//auto outapp_compass = CreateCompassPacket();
auto outapp_info = CreateInfoPacket(clear);
auto outapp_members = CreateMemberListPacket(clear);
@ -1173,6 +1235,7 @@ void Expedition::SendUpdatesToZoneMembers(bool clear)
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();
@ -1222,8 +1285,8 @@ std::unique_ptr<EQApplicationPacket> Expedition::CreateInvitePacket(
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();
outbuf->dz_zone_id = m_dynamiczone.GetZoneID();
outbuf->dz_instance_id = m_dynamiczone.GetInstanceID();
return outapp;
}
@ -1385,6 +1448,24 @@ void Expedition::SendWorldMemberStatus(uint32_t character_id, ExpeditionMemberSt
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<ServerPacket>(new ServerPacket(server_opcode, pack_size));
auto buf = reinterpret_cast<ServerDzLocation_Struct*>(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)
{
@ -1439,13 +1520,16 @@ void Expedition::HandleWorldMessage(ServerPacket* pack)
case ServerOP_ExpeditionDeleted:
{
auto buf = reinterpret_cast<ServerExpeditionID_Struct*>(pack->pBuffer);
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id);
if (zone && expedition)
{
auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id);
if (expedition)
if (!zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
{
expedition->SendUpdatesToZoneMembers(true);
}
// remove even from sender zone
zone->expedition_cache.erase(buf->expedition_id);
}
break;
}
@ -1538,14 +1622,14 @@ void Expedition::HandleWorldMessage(ServerPacket* pack)
{
for (uint32_t i = 0; i < buf->count; ++i)
{
auto entry = reinterpret_cast<ServerExpeditionCharacterEntry_Struct*>(&buf->entries[i]);
auto is_online = entry->character_online;
auto member = reinterpret_cast<ServerExpeditionCharacterEntry_Struct*>(&buf->entries[i]);
auto is_online = member->character_online;
auto status = is_online ? ExpeditionMemberStatus::Online : ExpeditionMemberStatus::Offline;
if (is_online && expedition->GetInstanceID() == entry->character_instance_id)
if (is_online && expedition->GetDynamicZone().IsInstanceID(member->character_instance_id))
{
status = ExpeditionMemberStatus::InDynamicZone;
}
expedition->UpdateMemberStatus(entry->character_id, status);
expedition->UpdateMemberStatus(member->character_id, status);
}
}
break;
@ -1583,5 +1667,87 @@ void Expedition::HandleWorldMessage(ServerPacket* pack)
}
break;
}
case ServerOP_ExpeditionDzCompass:
case ServerOP_ExpeditionDzSafeReturn:
case ServerOP_ExpeditionDzZoneIn:
{
auto buf = reinterpret_cast<ServerDzLocation_Struct*>(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;
}
}
}
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::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);
}
}

View File

@ -21,6 +21,7 @@
#ifndef EXPEDITION_H
#define EXPEDITION_H
#include "dynamiczone.h"
#include "expedition_lockout_timer.h"
#include <cstdint>
#include <memory>
@ -38,16 +39,6 @@ class ServerPacket;
extern const char* const DZ_YOU_NOT_ASSIGNED;
extern const char* const EXPEDITION_OTHER_BELONGS;
enum class DynamicZoneType : uint8_t // DynamicZoneActiveType
{
None = 0,
Expedition,
Tutorial,
Task,
Mission,
Quest
};
enum class ExpeditionMemberStatus : uint8_t
{
Unknown = 0,
@ -73,11 +64,12 @@ class Expedition
{
public:
Expedition() = delete;
Expedition(uint32_t id, std::string expedition_name, const ExpeditionMember& leader,
Expedition(
uint32_t id, const DynamicZone& dz, std::string expedition_name, const ExpeditionMember& leader,
uint32_t min_players, uint32_t max_players, bool replay_timer);
static Expedition* TryCreate(
Client* requester, std::string name, uint32_t min_players, uint32_t max_players, bool replay_timer);
static Expedition* TryCreate(Client* requester, DynamicZone& dynamiczone, ExpeditionRequest& request);
static void CacheFromDatabase(uint32_t expedition_id);
static bool CacheAllFromDatabase();
static void CacheExpeditions(MySQLRequestResult& results);
@ -89,10 +81,12 @@ public:
static void HandleWorldMessage(ServerPacket* pack);
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<uint32_t>(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::unordered_map<std::string, ExpeditionLockoutTimer>& GetLockouts() const { return m_lockouts; }
@ -101,7 +95,7 @@ public:
bool AddMember(const std::string& add_char_name, uint32_t add_char_id);
bool HasMember(const std::string& name);
bool HasMember(uint32_t character_id);
void RemoveAllMembers();
void RemoveAllMembers(bool enable_removal_timers = true, bool update_dz_expire_time = true);
bool RemoveMember(const std::string& remove_char_name);
void SetMemberStatus(Client* client, ExpeditionMemberStatus status);
void SetNewLeader(uint32_t new_leader_id, const std::string& new_leader_name);
@ -125,19 +119,18 @@ public:
void DzQuit(Client* requester);
void DzKickPlayers(Client* requester);
#if 0
bool AssignInstance(uint32_t instance_id, bool update_db = true);
uint32_t CreateInstance(std::string zone, uint32_t version, uint32_t duration); // m_dynamiczone
#endif
uint32_t GetInstanceID() const { return 77; /*return m_instance_id;*/ } // todo: GetDynamicZoneID()
DynamicZoneType GetType() const { return DynamicZoneType::Expedition; } // m_dynamiczone
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 SetDzZoneInLocation(float x, float y, float z, float heading, bool update_db = false);
static const uint32_t REPLAY_TIMER_ID;
static const uint32_t EVENT_TIMER_ID;
private:
void AddInternalLockout(ExpeditionLockoutTimer&& lockout_timer);
void AddInternalMember(const std::string& char_name, uint32_t char_id, bool is_current_member = true, bool offline = false);
void AddInternalMember(const std::string& char_name, uint32_t char_id, ExpeditionMemberStatus status, bool is_current_member = true);
bool ChooseNewLeader();
bool ConfirmLeaderCommand(Client* requester);
void LoadMembers();
@ -152,6 +145,7 @@ private:
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<std::string>& parameters = {});
void SendUpdatesToZoneMembers(bool clear = false);
void SendWorldDzLocationUpdate(uint16_t server_opcode, const DynamicZoneLocation& location);
void SendWorldExpeditionUpdate(bool destroyed = false);
void SendWorldGetOnlineMembers();
void SendWorldAddPlayerInvite(const std::string& inviter_name, const std::string& swap_remove_name, const std::string& add_name);
@ -174,11 +168,11 @@ private:
std::unique_ptr<EQApplicationPacket> CreateLeaderNamePacket();
uint32_t m_id = 0;
//uint32_t m_instance_id = 0; // todo: DynamicZone m_dynamiczone
uint32_t m_min_players = 0;
uint32_t m_max_players = 0;
bool m_has_replay_timer = false;
std::string m_expedition_name;
DynamicZone m_dynamiczone { DynamicZoneType::Expedition };
ExpeditionMember m_leader;
std::vector<ExpeditionMember> m_members; // current members
std::unordered_set<uint32_t> m_member_id_history; // track past members to allow invites for replay timer bypass

View File

@ -26,17 +26,17 @@
#include <fmt/core.h>
uint32_t ExpeditionDatabase::InsertExpedition(
const std::string& expedition_name, uint32_t leader_id,
uint32_t instance_id, const std::string& expedition_name, uint32_t leader_id,
uint32_t min_players, uint32_t max_players, bool has_replay_lockout)
{
LogExpeditionsDetail("Inserting new expedition [{}] leader [{}]", expedition_name, leader_id);
std::string query = fmt::format(SQL(
INSERT INTO expedition_details
(expedition_name, leader_id, min_players, max_players, has_replay_timer)
(instance_id, expedition_name, leader_id, min_players, max_players, has_replay_timer)
VALUES
('{}', {}, {}, {}, {});
), expedition_name, leader_id, min_players, max_players, has_replay_lockout);
({}, '{}', {}, {}, {}, {});
), instance_id, expedition_name, leader_id, min_players, max_players, has_replay_lockout);
auto results = database.QueryDatabase(query);
if (!results.Success())

View File

@ -36,7 +36,7 @@ class MySQLRequestResult;
namespace ExpeditionDatabase
{
uint32_t InsertExpedition(
const std::string& expedition_name, uint32_t leader_id,
uint32_t instance_id, const std::string& expedition_name, uint32_t leader_id,
uint32_t min_players, uint32_t max_players, bool has_replay_lockout);
MySQLRequestResult LoadExpedition(uint32_t expedition_id);
MySQLRequestResult LoadAllExpeditions();

View File

@ -38,10 +38,8 @@ struct ExpeditionRequestConflict
};
ExpeditionRequest::ExpeditionRequest(
Client* requester, std::string expedition_name, uint32_t min_players,
uint32_t max_players, bool has_replay_timer
std::string expedition_name, uint32_t min_players, uint32_t max_players, bool has_replay_timer
) :
m_requester(requester),
m_expedition_name(expedition_name),
m_min_players(min_players),
m_max_players(max_players),
@ -49,8 +47,9 @@ ExpeditionRequest::ExpeditionRequest(
{
}
bool ExpeditionRequest::Validate()
bool ExpeditionRequest::Validate(Client* requester)
{
m_requester = requester;
if (!m_requester)
{
return false;
@ -353,7 +352,7 @@ bool ExpeditionRequest::IsPlayerCountValidated(uint32_t member_count)
bool requirements_met = true;
auto bypass_status = RuleI(Expedition, MinStatusToBypassPlayerCountRequirements);
auto gm_bypass = (m_requester->GetGM() && m_requester->Admin() >= bypass_status);
auto gm_bypass = (m_requester && m_requester->GetGM() && m_requester->Admin() >= bypass_status);
if (!gm_bypass && (member_count < m_min_players || member_count > m_max_players))
{

View File

@ -37,14 +37,17 @@ struct ExpeditionMember;
class ExpeditionRequest
{
public:
ExpeditionRequest(Client* requester, std::string expedition_name,
uint32_t min_players, uint32_t max_players, bool has_replay_timer);
ExpeditionRequest(std::string expedition_name, uint32_t min_players, uint32_t max_players, bool has_replay_timer);
bool Validate();
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; }
uint32_t GetMinPlayers() const { return m_min_players; }
uint32_t GetMaxPlayers() const { return m_max_players; }
bool HasReplayTimer() const { return m_has_replay_timer; }
std::vector<ExpeditionMember> TakeMembers() && { return std::move(m_members); }
std::unordered_map<std::string, ExpeditionLockoutTimer> TakeLockouts() && { return std::move(m_lockouts); }

View File

@ -1646,14 +1646,14 @@ int Lua_Client::GetClientMaxLevel() {
return self->GetClientMaxLevel();
}
Lua_Expedition Lua_Client::CreateExpedition(std::string name, uint32 min_players, uint32 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) {
Lua_Safe_Call_Class(Lua_Expedition);
return self->CreateExpedition(name, min_players, max_players);
return self->CreateExpedition(zone_name, version, duration, expedition_name, min_players, max_players);
}
Lua_Expedition Lua_Client::CreateExpedition(std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer) {
Lua_Expedition Lua_Client::CreateExpedition(std::string zone_name, uint32 version, uint32 duration, std::string expedition_name, uint32 min_players, uint32 max_players, bool has_replay_timer) {
Lua_Safe_Call_Class(Lua_Expedition);
return self->CreateExpedition(name, min_players, max_players, has_replay_timer);
return self->CreateExpedition(zone_name, version, duration, expedition_name, min_players, max_players, has_replay_timer);
}
Lua_Expedition Lua_Client::GetExpedition() {
@ -1717,6 +1717,16 @@ bool Lua_Client::HasExpeditionLockout(std::string expedition_name, std::string e
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(std::string zone_name) {
Lua_Safe_Call_Void();
return self->MovePCDynamicZone(zone_name);
}
luabind::scope lua_register_client() {
return luabind::class_<Lua_Client, Lua_Mob>("Client")
.def(luabind::constructor<>())
@ -2024,14 +2034,16 @@ luabind::scope lua_register_client() {
.def("DisableAreaRegens", &Lua_Client::DisableAreaRegens)
.def("SetClientMaxLevel", (void(Lua_Client::*)(int))&Lua_Client::SetClientMaxLevel)
.def("GetClientMaxLevel", (int(Lua_Client::*)(void))&Lua_Client::GetClientMaxLevel)
.def("CreateExpedition", (Lua_Expedition(Lua_Client::*)(std::string, uint32, uint32))&Lua_Client::CreateExpedition)
.def("CreateExpedition", (Lua_Expedition(Lua_Client::*)(std::string, uint32, uint32, bool))&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("AddExpeditionLockout", (void(Lua_Client::*)(std::string, std::string, uint32))&Lua_Client::AddExpeditionLockout)
.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("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::*)(std::string))&Lua_Client::MovePCDynamicZone);
}
luabind::scope lua_register_inventory_where() {

View File

@ -339,14 +339,16 @@ public:
void SetClientMaxLevel(int value);
int GetClientMaxLevel();
Lua_Expedition CreateExpedition(std::string name, uint32 min_players, uint32 max_players);
Lua_Expedition CreateExpedition(std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer);
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 has_replay_timer);
Lua_Expedition GetExpedition();
luabind::object GetExpeditionLockouts(lua_State* L);
luabind::object GetExpeditionLockouts(lua_State* L, std::string expedition_name);
void AddExpeditionLockout(std::string expedition_name, std::string event_name, uint32 seconds);
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(std::string zone_name);
};
#endif

View File

@ -41,6 +41,11 @@ uint32_t Lua_Expedition::GetID() {
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();
@ -85,9 +90,14 @@ std::string Lua_Expedition::GetName() {
return self->GetName();
}
int Lua_Expedition::GetType() {
int Lua_Expedition::GetSecondsRemaining() {
Lua_Safe_Call_Int();
return static_cast<int>(self->GetType());
return self->GetDynamicZone().GetSecondsRemaining();
}
int Lua_Expedition::GetZoneID() {
Lua_Safe_Call_Int();
return self->GetDynamicZone().GetZoneID();
}
bool Lua_Expedition::HasLockout(std::string event_name) {
@ -105,6 +115,31 @@ void Lua_Expedition::RemoveLockout(std::string event_name) {
self->RemoveLockout(event_name);
}
void Lua_Expedition::SetCompass(uint32_t zone_id, float x, float y, float z) {
Lua_Safe_Call_Void();
return 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();
return self->SetDzCompass(zone_name, x, y, z, true);
}
void Lua_Expedition::SetSafeReturn(uint32_t zone_id, float x, float y, float z, float heading) {
Lua_Safe_Call_Void();
return 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();
return self->SetDzSafeReturn(zone_name, x, y, z, heading, true);
}
void Lua_Expedition::SetZoneInLocation(float x, float y, float z, float heading) {
Lua_Safe_Call_Void();
return self->SetDzZoneInLocation(x, y, z, heading, true);
}
luabind::scope lua_register_expedition() {
return luabind::class_<Lua_Expedition>("Expedition")
.def(luabind::constructor<>())
@ -113,15 +148,22 @@ luabind::scope lua_register_expedition() {
.def("AddLockout", (void(Lua_Expedition::*)(std::string, uint32_t))&Lua_Expedition::AddLockout)
.def("AddReplayLockout", (void(Lua_Expedition::*)(uint32_t))&Lua_Expedition::AddReplayLockout)
.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("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("GetType", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetType)
.def("GetSecondsRemaining", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetSecondsRemaining)
.def("GetZoneID", (int(Lua_Expedition::*)(void))&Lua_Expedition::GetZoneID)
.def("HasLockout", (bool(Lua_Expedition::*)(std::string))&Lua_Expedition::HasLockout)
.def("HasReplayLockout", (bool(Lua_Expedition::*)())&Lua_Expedition::HasReplayLockout)
.def("RemoveLockout", (void(Lua_Expedition::*)(std::string))&Lua_Expedition::RemoveLockout);
.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("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("SetZoneInLocation", (void(Lua_Expedition::*)(float, float, float, float))&Lua_Expedition::SetZoneInLocation);
}
luabind::scope lua_register_expedition_member_status() {
@ -136,17 +178,4 @@ luabind::scope lua_register_expedition_member_status() {
];
}
luabind::scope lua_register_dynamiczone_types() {
return luabind::class_<DynamicZoneType>("DynamicZoneType")
.enum_("constants")
[
luabind::value("None", static_cast<int>(DynamicZoneType::None)),
luabind::value("Expedition", static_cast<int>(DynamicZoneType::Expedition)),
luabind::value("Tutorial", static_cast<int>(DynamicZoneType::Tutorial)),
luabind::value("Task", static_cast<int>(DynamicZoneType::Task)),
luabind::value("Mission", static_cast<int>(DynamicZoneType::Mission)),
luabind::value("Quest", static_cast<int>(DynamicZoneType::Quest))
];
}
#endif // LUA_EQEMU

View File

@ -38,7 +38,6 @@ namespace luabind {
using adl::object;
}
luabind::scope lua_register_dynamiczone_types();
luabind::scope lua_register_expedition();
luabind::scope lua_register_expedition_member_status();
@ -57,15 +56,22 @@ public:
void AddLockout(std::string event_name, uint32_t seconds);
void AddReplayLockout(uint32_t seconds);
uint32_t GetID();
int GetInstanceID();
std::string GetLeaderName();
uint32_t GetMemberCount();
luabind::object GetMembers(lua_State* L);
std::string GetName();
int GetType();
int GetSecondsRemaining();
int GetZoneID();
luabind::object GetLockouts(lua_State* L);
bool HasLockout(std::string event_name);
bool HasReplayLockout();
void RemoveLockout(std::string event_name);
void SetCompass(uint32 zone_id, float x, float y, float z);
void SetCompass(std::string zone_name, float x, float y, float z);
void SetSafeReturn(uint32 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 SetZoneInLocation(float x, float y, float z, float heading);
};
#endif // LUA_EQEMU

View File

@ -1110,7 +1110,6 @@ void LuaParser::MapFunctions(lua_State *L) {
lua_register_ruleb(),
lua_register_journal_speakmode(),
lua_register_journal_mode(),
lua_register_dynamiczone_types(),
lua_register_expedition(),
lua_register_expedition_member_status()
];

View File

@ -299,6 +299,7 @@
#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.
@ -314,6 +315,7 @@
#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

View File

@ -2909,10 +2909,18 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
case ServerOP_ExpeditionGetOnlineMembers:
case ServerOP_ExpeditionDzAddPlayer:
case ServerOP_ExpeditionDzMakeLeader:
case ServerOP_ExpeditionDzCompass:
case ServerOP_ExpeditionDzSafeReturn:
case ServerOP_ExpeditionDzZoneIn:
{
Expedition::HandleWorldMessage(pack);
break;
}
case ServerOP_DzCharacterChange:
{
DynamicZone::HandleWorldMessage(pack);
break;
}
default: {
std::cout << " Unknown ZSopcode:" << (int)pack->opcode;
std::cout << " size:" << pack->size << std::endl;

View File

@ -1491,7 +1491,14 @@ bool Zone::Process() {
{
if(Instance_Timer->Check())
{
entity_list.GateAllClients();
// if this is a dynamic zone instance notify system associated with it
Expedition* expedition = Expedition::FindExpeditionByInstanceID(GetInstanceID());
if (expedition)
{
expedition->RemoveAllMembers(false, false); // entity list will teleport clients out immediately
}
// todo: move corpses to non-instanced version of dz at same coords (if no graveyard)
entity_list.GateAllClientsToSafeReturn();
database.DeleteInstance(GetInstanceID());
Instance_Shutdown_Timer = new Timer(20000); //20 seconds
}

View File

@ -406,4 +406,3 @@ private:
};
#endif