[Quest API] Add Last Login and First Login Flags to EVENT_CONNECT (#4866)

* [Quest API] Add Last Login and First Login Flags to EVENT_CONNECT

* Push

* Update base_character_data_repository.h

* Update version field

---------

Co-authored-by: Chris Miles <akkadius1@gmail.com>
This commit is contained in:
Alex King 2025-05-14 21:46:25 -04:00 committed by GitHub
parent c36c336bc7
commit 3b617a6652
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 166 additions and 78 deletions

View File

@ -1095,13 +1095,13 @@ void Database::SetLFP(uint32 character_id, bool is_lfp)
CharacterDataRepository::UpdateOne(*this, e);
}
void Database::SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 first_logon)
void Database::SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 ingame)
{
auto e = CharacterDataRepository::FindOne(*this, character_id);
e.firstlogon = first_logon;
e.lfg = is_lfg ? 1 : 0;
e.lfp = is_lfp ? 1 : 0;
e.ingame = ingame;
e.lfg = is_lfg ? 1 : 0;
e.lfp = is_lfp ? 1 : 0;
CharacterDataRepository::UpdateOne(*this, e);
}
@ -1115,11 +1115,11 @@ void Database::SetLFG(uint32 character_id, bool is_lfg)
CharacterDataRepository::UpdateOne(*this, e);
}
void Database::SetFirstLogon(uint32 character_id, uint8 first_logon)
void Database::SetIngame(uint32 character_id, uint8 ingame)
{
auto e = CharacterDataRepository::FindOne(*this, character_id);
e.firstlogon = first_logon;
e.ingame = ingame;
CharacterDataRepository::UpdateOne(*this, e);
}

View File

@ -263,7 +263,7 @@ public:
bool SaveTime(int8 minute, int8 hour, int8 day, int8 month, int16 year);
void ClearMerchantTemp();
void ClearPTimers(uint32 character_id);
void SetFirstLogon(uint32 character_id, uint8 first_logon);
void SetIngame(uint32 character_id, uint8 ingame);
void SetLFG(uint32 character_id, bool is_lfg);
void SetLFP(uint32 character_id, bool is_lfp);
void SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 first_logon);

View File

@ -7098,7 +7098,20 @@ ALTER TABLE `npc_types`
ADD COLUMN `npc_tint_id` SMALLINT UNSIGNED NULL DEFAULT '0' AFTER `multiquest_enabled`;
)",
.content_schema_update = true
}
},
ManifestEntry{
.version = 9323,
.description = "2025_04_16_character_data_first_login.sql",
.check = "SHOW COLUMNS FROM `character_data` LIKE 'first_login'",
.condition = "empty",
.match = "",
.sql = R"(
ALTER TABLE `character_data`
CHANGE COLUMN `firstlogon` `ingame` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `xtargets`,
ADD COLUMN `first_login` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `xtargets`;
)",
.content_schema_update = false
},
// -- template; copy/paste this when you need to create a new entry
// ManifestEntry{
// .version = 9228,

View File

@ -115,7 +115,8 @@ public:
uint8_t lfg;
std::string mailkey;
uint8_t xtargets;
int8_t firstlogon;
uint8_t ingame;
uint32_t first_login;
uint32_t e_aa_effects;
uint32_t e_percent_to_aa;
uint32_t e_expended_aa_spent;
@ -230,7 +231,8 @@ public:
"lfg",
"mailkey",
"xtargets",
"firstlogon",
"ingame",
"first_login",
"e_aa_effects",
"e_percent_to_aa",
"e_expended_aa_spent",
@ -341,7 +343,8 @@ public:
"lfg",
"mailkey",
"xtargets",
"firstlogon",
"ingame",
"first_login",
"e_aa_effects",
"e_percent_to_aa",
"e_expended_aa_spent",
@ -486,7 +489,8 @@ public:
e.lfg = 0;
e.mailkey = "";
e.xtargets = 5;
e.firstlogon = 0;
e.ingame = 0;
e.first_login = 0;
e.e_aa_effects = 0;
e.e_percent_to_aa = 0;
e.e_expended_aa_spent = 0;
@ -627,15 +631,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10);
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0;
e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
return e;
}
@ -764,15 +769,16 @@ public:
v.push_back(columns[93] + " = " + std::to_string(e.lfg));
v.push_back(columns[94] + " = '" + Strings::Escape(e.mailkey) + "'");
v.push_back(columns[95] + " = " + std::to_string(e.xtargets));
v.push_back(columns[96] + " = " + std::to_string(e.firstlogon));
v.push_back(columns[97] + " = " + std::to_string(e.e_aa_effects));
v.push_back(columns[98] + " = " + std::to_string(e.e_percent_to_aa));
v.push_back(columns[99] + " = " + std::to_string(e.e_expended_aa_spent));
v.push_back(columns[100] + " = " + std::to_string(e.aa_points_spent_old));
v.push_back(columns[101] + " = " + std::to_string(e.aa_points_old));
v.push_back(columns[102] + " = " + std::to_string(e.e_last_invsnapshot));
v.push_back(columns[103] + " = FROM_UNIXTIME(" + (e.deleted_at > 0 ? std::to_string(e.deleted_at) : "null") + ")");
v.push_back(columns[104] + " = " + std::to_string(e.illusion_block));
v.push_back(columns[96] + " = " + std::to_string(e.ingame));
v.push_back(columns[97] + " = " + std::to_string(e.first_login));
v.push_back(columns[98] + " = " + std::to_string(e.e_aa_effects));
v.push_back(columns[99] + " = " + std::to_string(e.e_percent_to_aa));
v.push_back(columns[100] + " = " + std::to_string(e.e_expended_aa_spent));
v.push_back(columns[101] + " = " + std::to_string(e.aa_points_spent_old));
v.push_back(columns[102] + " = " + std::to_string(e.aa_points_old));
v.push_back(columns[103] + " = " + std::to_string(e.e_last_invsnapshot));
v.push_back(columns[104] + " = FROM_UNIXTIME(" + (e.deleted_at > 0 ? std::to_string(e.deleted_at) : "null") + ")");
v.push_back(columns[105] + " = " + std::to_string(e.illusion_block));
auto results = db.QueryDatabase(
fmt::format(
@ -890,7 +896,8 @@ public:
v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon));
v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent));
@ -1024,7 +1031,8 @@ public:
v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon));
v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent));
@ -1162,15 +1170,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10);
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0;
e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
all_entries.push_back(e);
}
@ -1291,15 +1300,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10);
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0;
e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
all_entries.push_back(e);
}
@ -1470,7 +1480,8 @@ public:
v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon));
v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent));
@ -1597,7 +1608,8 @@ public:
v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon));
v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent));

View File

@ -42,7 +42,7 @@
* Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt
*/
#define CURRENT_BINARY_DATABASE_VERSION 9322
#define CURRENT_BINARY_DATABASE_VERSION 9323
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054
#endif

View File

@ -2082,7 +2082,8 @@ private:
uint16 trader_id;
uint16 customer_id;
uint32 account_creation;
uint8 firstlogon;
bool first_login;
bool ingame;
uint32 mercid; // current merc
uint8 mercSlot; // selected merc slot
time_t m_trader_transaction_date;

View File

@ -807,29 +807,42 @@ void Client::CompleteConnect()
m_last_position_before_bulk_update = GetPosition();
/* This sub event is for if a player logs in for the first time since entering world. */
if (firstlogon == 1) {
TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID()));
BuyerRepository::DeleteBuyer(database, CharacterID());
LogTradingDetail(
"Removed trader abd buyer entries for Character ID {} on first logon to ensure table consistency.",
if (ingame) {
auto e = CharacterDataRepository::FindOne(
database,
CharacterID()
);
bool is_first_login = e.first_login == 0;
RecordPlayerEventLog(PlayerEvent::WENT_ONLINE, PlayerEvent::EmptyEvent{});
if (parse->PlayerHasQuestSub(EVENT_CONNECT)) {
parse->EventPlayer(EVENT_CONNECT, this, "", 0);
const std::string& export_string = fmt::format(
"{} {} {}",
e.last_login,
time(nullptr) - e.last_login,
is_first_login ? 1 : 0
);
parse->EventPlayer(EVENT_CONNECT, this, export_string, 0);
}
/**
* Update last login since this doesn't get updated until a late save later so we can update online status
*/
database.QueryDatabase(
StringFormat(
"UPDATE `character_data` SET `last_login` = UNIX_TIMESTAMP() WHERE id = %u",
if (is_first_login) {
e.first_login = time(nullptr);
TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID()));
BuyerRepository::DeleteBuyer(database, CharacterID());
LogTradingDetail(
"Removed trader abd buyer entries for Character ID {} on first logon to ensure table consistency.",
CharacterID()
)
);
);
}
e.last_login = time(nullptr);
const int updated = CharacterDataRepository::UpdateOne(database, e);
if (!updated) {
LogError("Failed to update login time for character_id [{}]", CharacterID());
}
if (IsPetNameChangeAllowed() && !RuleB(Pets, AlwaysAllowPetRename)) {
InvokeChangePetName(false);
@ -878,7 +891,7 @@ void Client::CompleteConnect()
entity_list.SendFindableNPCList(this);
if (IsInAGuild()) {
if (firstlogon == 1) {
if (ingame) {
guild_mgr.UpdateDbMemberOnline(CharacterID(), true);
SendGuildMembersList();
}
@ -1314,14 +1327,14 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
/* Load Character Data */
query = fmt::format(
"SELECT `lfp`, `lfg`, `xtargets`, `firstlogon`, `guild_id`, `rank`, `exp_enabled`, `tribute_enable`, `extra_haste`, `illusion_block` FROM `character_data` LEFT JOIN `guild_members` ON `id` = `char_id` WHERE `id` = {}",
"SELECT `lfp`, `lfg`, `xtargets`, `first_login`, `guild_id`, `rank`, `exp_enabled`, `tribute_enable`, `extra_haste`, `illusion_block`, `ingame` FROM `character_data` LEFT JOIN `guild_members` ON `id` = `char_id` WHERE `id` = {}",
cid
);
auto results = database.QueryDatabase(query);
for (auto row : results) {
if (row[4] && Strings::ToInt(row[4]) > 0) {
guild_id = Strings::ToInt(row[4]);
guildrank = row[5] ? Strings::ToInt(row[5]) : GUILD_RANK_NONE;
guild_id = Strings::ToInt(row[4]);
guildrank = row[5] ? Strings::ToInt(row[5]) : GUILD_RANK_NONE;
guild_tribute_opt_in = row[7] ? Strings::ToBool(row[7]) : 0;
}
@ -1329,10 +1342,21 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
SetExtraHaste(Strings::ToInt(row[8]), false);
SetIllusionBlock(Strings::ToBool(row[9]));
if (LFP) { LFP = Strings::ToInt(row[0]); }
if (LFG) { LFG = Strings::ToInt(row[1]); }
if (row[3])
firstlogon = Strings::ToInt(row[3]);
if (LFP) {
LFP = Strings::ToInt(row[0]);
}
if (LFG) {
LFG = Strings::ToInt(row[1]);
}
if (row[3]) {
first_login = Strings::ToUnsignedInt(row[3]);
}
if (row[10]) {
ingame = Strings::ToBool(row[10]);
}
}
if (RuleB(Character, SharedBankPlat))
@ -1721,7 +1745,7 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
// Taunt persists when zoning on newer clients, overwrite default.
if (m_ClientVersionBit & EQ::versions::maskUFAndLater) {
if (!firstlogon) {
if (!ingame) {
pet->SetTaunting(m_petinfo.taunting);
}
}

View File

@ -727,7 +727,7 @@ void Client::OnDisconnect(bool hard_disconnect) {
o->trade->Reset();
}
database.SetFirstLogon(CharacterID(), 0); //We change firstlogon status regardless of if a player logs out to zone or not, because we only want to trigger it on their first login from world.
database.SetIngame(CharacterID(), 0); //We change ingame status regardless of if a player logs out to zone or not, because we only want to trigger it on their first login from world.
/* Remove from all proximities */
ClearAllProximities();

View File

@ -2543,6 +2543,14 @@ void PerlembParser::ExportEventVariables(
break;
}
case EVENT_CONNECT: {
Seperator sep(data);
ExportVar(package_name.c_str(), "last_login", sep.arg[0]);
ExportVar(package_name.c_str(), "seconds_since_last_login", sep.arg[1]);
ExportVar(package_name.c_str(), "is_first_login", sep.arg[2]);
break;
}
default: {
break;
}

View File

@ -353,6 +353,7 @@ LuaParser::LuaParser() {
PlayerArgumentDispatch[EVENT_AA_LOSS] = handle_player_aa_loss;
PlayerArgumentDispatch[EVENT_SPELL_BLOCKED] = handle_player_spell_blocked;
PlayerArgumentDispatch[EVENT_READ_ITEM] = handle_player_read_item;
PlayerArgumentDispatch[EVENT_CONNECT] = handle_player_connect;
ItemArgumentDispatch[EVENT_ITEM_CLICK] = handle_item_click;
ItemArgumentDispatch[EVENT_ITEM_CLICK_CAST] = handle_item_click;

View File

@ -1809,6 +1809,26 @@ void handle_player_read_item(
}
}
void handle_player_connect(
QuestInterface *parse,
lua_State* L,
Client* client,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
)
{
Seperator sep(data.c_str());
lua_pushinteger(L, Strings::ToUnsignedInt(sep.arg[0]));
lua_setfield(L, -2, "last_login");
lua_pushinteger(L, Strings::ToUnsignedInt(sep.arg[1]));
lua_setfield(L, -2, "seconds_since_last_login");
lua_pushboolean(L, Strings::ToBool(sep.arg[2]));
lua_setfield(L, -2, "is_first_login");
}
// Item
void handle_item_click(
QuestInterface *parse,

View File

@ -865,6 +865,15 @@ void handle_player_read_item(
std::vector<std::any> *extra_pointers
);
void handle_player_connect(
QuestInterface *parse,
lua_State* L,
Client* client,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
);
// Item
void handle_item_click(
QuestInterface *parse,