From feb4cc37c6c38a51a38c4e96c9bf9e609d086c6a Mon Sep 17 00:00:00 2001 From: Uleat Date: Mon, 3 Sep 2018 20:57:20 -0400 Subject: [PATCH] Rework of 'invsnapshot' command and implementation of automatic inventory snapshots --- changelog.txt | 12 + common/database.cpp | 23 +- common/database.h | 3 +- common/features.h | 3 +- common/shareddb.cpp | 4 +- utils/sql/character_table_list.txt | 1 + utils/sql/data_tables.txt | 2 +- utils/sql/db_update_manifest.txt | 4 +- .../sql/git/bots/bots_db_update_manifest.txt | 2 +- .../2018_08_13_bots_inventory_update.sql | 2 +- .../required/2018_08_13_inventory_update.sql | 33 +- .../2018_08_13_inventory_version_update.sql | 62 ++- utils/sql/user_tables.txt | 1 + zone/client.cpp | 2 +- zone/client_process.cpp | 13 + zone/command.cpp | 356 +++++++++++++++- zone/zonedb.cpp | 390 +++++++++++++++--- zone/zonedb.h | 12 +- 18 files changed, 826 insertions(+), 99 deletions(-) diff --git a/changelog.txt b/changelog.txt index d98bb965f..e9623f293 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,17 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 09/03/2018 == +Uleat: Rework of 'invsnapshot' command and implementation of automatic inventory snapshots. + - Inventory snapshots are now taken automatically using the interval rule values - if snapshots are enabled + - Command 'invsnapshot' now has more options available to include a restore feature + -- A pop-up help menu is available + -- argument 'capture' is available to anyone with status high enough to register the command + -- Advanced options are only available to players with 150 status or greater + -- argument 'list' provides a list of "timestamp : item count" entries + -- argument 'parse' displays a "slot : item id : item name" listing of valid snapshots by timestamp + -- argument 'compare' shows a 'difference' comparison of "snapshot-to-inventory" changes + -- argument 'restore' applies a saved snapshot to the player's inventory (with a pre-clearing call) + == 08/13/2018 == Uleat: Activation of RoF+ clients' two additional general slots and integration of SoF+ clients' PowerSource slot - Inventory 'Possessions' main slots are now contiguous and implemented to RoF2 standards diff --git a/common/database.cpp b/common/database.cpp index 447037ae7..b6c752dd1 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -2111,10 +2111,27 @@ void Database::LoadLogSettings(EQEmuLogSys::LogSettings* log_settings) } } -void Database::ClearInvSnapshots(bool use_rule) -{ +int Database::CountInvSnapshots() { + std::string query = StringFormat("SELECT COUNT(*) FROM (SELECT * FROM `inventory_snapshots` a GROUP BY `charid`, `time_index`) b"); + auto results = QueryDatabase(query); + + if (!results.Success()) + return -1; + + auto row = results.begin(); + + int64 count = atoll(row[0]); + if (count > INT_MAX) + return -2; + if (count < 0) + return -3; + + return count; +} + +void Database::ClearInvSnapshots(bool from_now) { uint32 del_time = time(nullptr); - if (use_rule) { del_time -= RuleI(Character, InvSnapshotHistoryD) * 86400; } + if (!from_now) { del_time -= RuleI(Character, InvSnapshotHistoryD) * 86400; } std::string query = StringFormat("DELETE FROM inventory_snapshots WHERE time_index <= %lu", (unsigned long)del_time); QueryDatabase(query); diff --git a/common/database.h b/common/database.h index c6d90e422..307390edf 100644 --- a/common/database.h +++ b/common/database.h @@ -264,7 +264,8 @@ public: void SetLFP(uint32 CharID, bool LFP); void SetLoginFlags(uint32 CharID, bool LFP, bool LFG, uint8 firstlogon); - void ClearInvSnapshots(bool use_rule = true); + int CountInvSnapshots(); + void ClearInvSnapshots(bool from_now = false); /* EQEmuLogSys */ void LoadLogSettings(EQEmuLogSys::LogSettings* log_settings); diff --git a/common/features.h b/common/features.h index a80fb9f3c..38f238617 100644 --- a/common/features.h +++ b/common/features.h @@ -268,7 +268,8 @@ enum { commandBanPlayers = 100, //can set bans on players commandChangeDatarate = 201, //edit client's data rate commandZoneToCoords = 0, //can #zone with coords - commandInterrogateInv = 100 //below this == only log on error state and self-only target dump + commandInterrogateInv = 100, //below this == only log on error state and self-only target dump + commandInvSnapshot = 150 //ability to clear/restore snapshots }; //default states for logging flag on NPCs and clients (having NPCs on by default is prolly a bad idea) diff --git a/common/shareddb.cpp b/common/shareddb.cpp index 2d0e50125..9e0cd5f7e 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -720,10 +720,10 @@ bool SharedDatabase::GetInventory(uint32 char_id, EQEmu::InventoryProfile *inv) if (cv_conflict) { char char_name[64] = ""; GetCharName(char_id, char_name); - Log(Logs::General, Logs::Client_Login, + Log(Logs::Moderate, Logs::Client_Login, "ClientVersion conflict during inventory load at zone entry for '%s' (charid: %u, inver: %s)", char_name, char_id, EQEmu::versions::MobVersionName(inv->InventoryVersion()) - ); // this can be changed to moderate after live testing + ); } // Retrieve shared inventory diff --git a/utils/sql/character_table_list.txt b/utils/sql/character_table_list.txt index 4d70051ec..8a47abf67 100644 --- a/utils/sql/character_table_list.txt +++ b/utils/sql/character_table_list.txt @@ -31,6 +31,7 @@ friends guild_members instance_list_player inventory +inventory_snapshots keyring mail player_titlesets diff --git a/utils/sql/data_tables.txt b/utils/sql/data_tables.txt index 0638c3bd5..8738c716b 100644 --- a/utils/sql/data_tables.txt +++ b/utils/sql/data_tables.txt @@ -1,5 +1,5 @@ command_settings -inventory_version +inventory_versions launcher rule_sets rule_values diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index b844ddfa8..916833777 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -379,8 +379,8 @@ 9123|2018_07_07_data_buckets.sql|SHOW TABLES LIKE 'data_buckets'|empty| 9124|2018_07_09_tasks.sql|SHOW COLUMNS FROM `tasks` LIKE 'type'|empty| 9125|2018_07_20_task_emote.sql|SHOW COLUMNS FROM `tasks` LIKE 'completion_emote'|empty| -9126|2018_08_13_inventory_version_update.sql|SHOW COLUMNS FROM `inventory_version` LIKE 'bot_step'|empty| -9127|2018_08_13_inventory_update.sql|SELECT * FROM `inventory_version` WHERE `version` = 2 and `step` = 0|not_empty| +9126|2018_08_13_inventory_version_update.sql|SHOW TABLES LIKE 'inventory_versions'|empty| +9127|2018_08_13_inventory_update.sql|SELECT * FROM `inventory_versions` WHERE `version` = 2 and `step` = 0|not_empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/bots/bots_db_update_manifest.txt b/utils/sql/git/bots/bots_db_update_manifest.txt index 3e7b3032b..3496ce8d2 100644 --- a/utils/sql/git/bots/bots_db_update_manifest.txt +++ b/utils/sql/git/bots/bots_db_update_manifest.txt @@ -18,7 +18,7 @@ 9017|2017_03_26_bots_spells_id_fix_for_saved_shadowknight_bots.sql|SELECT * FROM `bot_data` WHERE `class` = '5' AND `spells_id` = '3004'|not_empty| 9018|2018_02_02_Bot_Spells_Min_Max_HP.sql|SHOW COLUMNS FROM `bot_spells_entries` LIKE 'min_hp'|empty| 9019|2018_04_12_bots_stop_melee_level.sql|SHOW COLUMNS FROM `bot_data` LIKE 'stop_melee_level'|empty| -9020|2018_08_13_bots_inventory_update.sql|SELECT * FROM `inventory_version` WHERE `version` = 2 and `bot_step` = 0|not_empty| +9020|2018_08_13_bots_inventory_update.sql|SELECT * FROM `inventory_versions` WHERE `version` = 2 and `bot_step` = 0|not_empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/bots/required/2018_08_13_bots_inventory_update.sql b/utils/sql/git/bots/required/2018_08_13_bots_inventory_update.sql index 07f5446aa..94ca630c2 100644 --- a/utils/sql/git/bots/required/2018_08_13_bots_inventory_update.sql +++ b/utils/sql/git/bots/required/2018_08_13_bots_inventory_update.sql @@ -2,4 +2,4 @@ UPDATE `bot_inventories` SET `slot_id` = 22 WHERE `slot_id` = 21; -- adjust ammo slot UPDATE `bot_inventories` SET `slot_id` = 21 WHERE `slot_id` = 9999; -- adjust powersource slot -UPDATE `inventory_version` SET `bot_step` = 1 WHERE `version` = 2; \ No newline at end of file +UPDATE `inventory_versions` SET `bot_step` = 1 WHERE `version` = 2; \ No newline at end of file diff --git a/utils/sql/git/required/2018_08_13_inventory_update.sql b/utils/sql/git/required/2018_08_13_inventory_update.sql index 646b1f80e..7021b234c 100644 --- a/utils/sql/git/required/2018_08_13_inventory_update.sql +++ b/utils/sql/git/required/2018_08_13_inventory_update.sql @@ -1,3 +1,14 @@ +-- create inventory v1 backup +SELECT @pre_timestamp := UNIX_TIMESTAMP(NOW()); +INSERT INTO `inventory_snapshots_v1_bak` + (`time_index`,`charid`,`slotid`,`itemid`,`charges`,`color`,`augslot1`,`augslot2`,`augslot3`,`augslot4`, + `augslot5`,`augslot6`,`instnodrop`,`custom_data`,`ornamenticon`,`ornamentidfile`,`ornament_hero_model`) +SELECT + @pre_timestamp, `charid`, `slotid`, `itemid`, `charges`, `color`, `augslot1`, `augslot2`, `augslot3`, `augslot4`, + `augslot5`,`augslot6`,`instnodrop`,`custom_data`,`ornamenticon`,`ornamentidfile`,`ornament_hero_model` +FROM `inventory`; + + -- update equipable slots in `items` table SELECT 'pre-transform count..', (SELECT COUNT(id) FROM `items` WHERE `slots` & (3 << 21)) total, @@ -21,15 +32,6 @@ UPDATE `inventory` SET `slotid` = 22 WHERE `slotid` = 21; -- adjust ammo slot UPDATE `inventory` SET `slotid` = 21 WHERE `slotid` = 9999; -- adjust powersource slot --- update `inventory_snapshots` slots -UPDATE `inventory_snapshots` SET `slotid` = 33 WHERE `slotid` = 30; -- adjust cursor -UPDATE `inventory_snapshots` SET `slotid` = (`slotid` + 20) WHERE `slotid` >= 331 AND `slotid` <= 340; -- adjust cursor bags -UPDATE `inventory_snapshots` SET `slotid` = (`slotid` + 1) WHERE `slotid` >= 22 AND `slotid` <= 29; -- adjust general slots --- current general bags remain the same -UPDATE `inventory_snapshots` SET `slotid` = 22 WHERE `slotid` = 21; -- adjust ammo slot -UPDATE `inventory_snapshots` SET `slotid` = 21 WHERE `slotid` = 9999; -- adjust powersource slot - - -- update `character_corpse_items` slots UPDATE `character_corpse_items` SET `equip_slot` = 33 WHERE `equip_slot` = 30; -- adjust cursor UPDATE `character_corpse_items` SET `equip_slot` = (`equip_slot` + 20) WHERE `equip_slot` >= 331 AND `equip_slot` <= 340; -- adjust cursor bags @@ -42,4 +44,15 @@ UPDATE `character_corpse_items` SET `equip_slot` = 21 WHERE `equip_slot` = 9999; -- update `character_pet_inventory` slots UPDATE `character_pet_inventory` SET `slot` = 22 WHERE `slot` = 21; -- adjust ammo slot -UPDATE `inventory_version` SET `step` = 1 WHERE `version` = 2; +UPDATE `inventory_versions` SET `step` = 1 WHERE `version` = 2; + + +-- create initial inventory v2 snapshots +SELECT @post_timestamp := UNIX_TIMESTAMP(NOW()); +INSERT INTO `inventory_snapshots` + (`time_index`,`charid`,`slotid`,`itemid`,`charges`,`color`,`augslot1`,`augslot2`,`augslot3`,`augslot4`, + `augslot5`,`augslot6`,`instnodrop`,`custom_data`,`ornamenticon`,`ornamentidfile`,`ornament_hero_model`) +SELECT + @post_timestamp, `charid`, `slotid`, `itemid`, `charges`, `color`, `augslot1`, `augslot2`, `augslot3`, `augslot4`, + `augslot5`,`augslot6`,`instnodrop`,`custom_data`,`ornamenticon`,`ornamentidfile`,`ornament_hero_model` +FROM `inventory`; diff --git a/utils/sql/git/required/2018_08_13_inventory_version_update.sql b/utils/sql/git/required/2018_08_13_inventory_version_update.sql index ac7a7263c..f8fc33162 100644 --- a/utils/sql/git/required/2018_08_13_inventory_version_update.sql +++ b/utils/sql/git/required/2018_08_13_inventory_version_update.sql @@ -1 +1,61 @@ -ALTER TABLE `inventory_version` ADD COLUMN `bot_step` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `step`; +DROP TABLE IF EXISTS `inventory_version`; +DROP TABLE IF EXISTS `inventory_snapshots`; + + +CREATE TABLE `inventory_versions` ( + `version` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `step` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `bot_step` INT(11) UNSIGNED NOT NULL DEFAULT '0' +) +COLLATE='latin1_swedish_ci' +ENGINE=MyISAM; + +INSERT INTO `inventory_versions` VALUES (2, 0, 0); + + +CREATE TABLE `inventory_snapshots` ( + `time_index` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `charid` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `slotid` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `itemid` INT(11) UNSIGNED NULL DEFAULT '0', + `charges` SMALLINT(3) UNSIGNED NULL DEFAULT '0', + `color` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `augslot1` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot2` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot3` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot4` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot5` MEDIUMINT(7) UNSIGNED NULL DEFAULT '0', + `augslot6` MEDIUMINT(7) NOT NULL DEFAULT '0', + `instnodrop` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `custom_data` TEXT NULL, + `ornamenticon` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `ornamentidfile` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `ornament_hero_model` INT(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`time_index`, `charid`, `slotid`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB; + + +CREATE TABLE `inventory_snapshots_v1_bak` ( + `time_index` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `charid` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `slotid` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `itemid` INT(11) UNSIGNED NULL DEFAULT '0', + `charges` SMALLINT(3) UNSIGNED NULL DEFAULT '0', + `color` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `augslot1` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot2` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot3` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot4` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0', + `augslot5` MEDIUMINT(7) UNSIGNED NULL DEFAULT '0', + `augslot6` MEDIUMINT(7) NOT NULL DEFAULT '0', + `instnodrop` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `custom_data` TEXT NULL, + `ornamenticon` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `ornamentidfile` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `ornament_hero_model` INT(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`time_index`, `charid`, `slotid`) +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB; diff --git a/utils/sql/user_tables.txt b/utils/sql/user_tables.txt index a210aa381..941e30c47 100644 --- a/utils/sql/user_tables.txt +++ b/utils/sql/user_tables.txt @@ -52,6 +52,7 @@ guild_members hackers instance_list_player inventory +inventory_snapshots item_tick keyring launcher_zones diff --git a/zone/client.cpp b/zone/client.cpp index 969d215ba..cd5dbbea1 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -679,7 +679,7 @@ bool Client::Save(uint8 iCommitNow) { // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { - if (database.SaveCharacterInventorySnapshot(CharacterID())) { + if (database.SaveCharacterInvSnapshot(CharacterID())) { SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); } else { diff --git a/zone/client_process.cpp b/zone/client_process.cpp index d16864ffc..d53abe769 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -242,6 +242,19 @@ bool Client::Process() { } } + if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { + if (database.SaveCharacterInvSnapshot(CharacterID())) { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + Log(Logs::Moderate, Logs::Inventory, "Successful inventory snapshot taken of %s - setting next interval for %i minute%s.", + GetName(), RuleI(Character, InvSnapshotMinIntervalM), (RuleI(Character, InvSnapshotMinIntervalM) == 1 ? "" : "s")); + } + else { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); + Log(Logs::Moderate, Logs::Inventory, "Failed to take inventory snapshot of %s - retrying in %i minute%s.", + GetName(), RuleI(Character, InvSnapshotMinRetryM), (RuleI(Character, InvSnapshotMinRetryM) == 1 ? "" : "s")); + } + } + /* Build a close range list of NPC's */ if (npc_close_scan_timer.Check()) { close_mobs.clear(); diff --git a/zone/command.cpp b/zone/command.cpp index a6761cbcf..98faec71a 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -172,7 +172,6 @@ int command_init(void) command_add("castspell", "[spellid] - Cast a spell", 50, command_castspell) || command_add("chat", "[channel num] [message] - Send a channel message to all zones", 200, command_chat) || command_add("checklos", "- Check for line of sight to your target", 50, command_checklos) || - command_add("clearinvsnapshots", "[use rule] - Clear inventory snapshot history (true - elapsed entries, false - all entries)", 200, command_clearinvsnapshots) || command_add("corpse", "- Manipulate corpses, use with no arguments for help", 50, command_corpse) || command_add("corpsefix", "Attempts to bring corpses from underneath the ground within close proximity of the player", 0, command_corpsefix) || command_add("crashtest", "- Crash the zoneserver", 255, command_crashtest) || @@ -239,7 +238,7 @@ int command_init(void) command_add("instance", "- Modify Instances", 200, command_instance) || command_add("interrogateinv", "- use [help] argument for available options", 0, command_interrogateinv) || command_add("interrupt", "[message id] [color] - Interrupt your casting. Arguments are optional.", 50, command_interrupt) || - command_add("invsnapshot", "- Takes an inventory snapshot of your current target", 80, command_invsnapshot) || + command_add("invsnapshot", "- Manipulates inventory snapshots for your current target", 80, command_invsnapshot) || command_add("invul", "[on/off] - Turn player target's or your invulnerable flag on or off", 80, command_invul) || command_add("ipban", "[IP address] - Ban IP by character name", 200, command_ipban) || command_add("iplookup", "[charname] - Look up IP address of charname", 200, command_iplookup) || @@ -2944,31 +2943,344 @@ void command_interrogateinv(Client *c, const Seperator *sep) void command_invsnapshot(Client *c, const Seperator *sep) { - auto t = c->GetTarget(); - if (!t || !t->IsClient()) { - c->Message(0, "Target must be a client"); + if (!c) + return; + + if (sep->argnum == 0 || strcmp(sep->arg[1], "help") == 0) { + std::string window_title = "Inventory Snapshot Argument Help Menu"; + + std::string window_text = + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + if (c->Admin() >= commandInvSnapshot) + window_text.append( + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ); + + window_text.append( + "
Usage:#invsnapshot arguments
(required optional)
helpthis menu
capturetakes snapshot of character inventory
gcountreturns global snapshot count
gclear
now
delete all snapshots - rule
delete all snapshots - now
countreturns character snapshot count
clear
now
delete character snapshots - rule
delete character snapshots - now
list
count
lists entry ids for current character
limits to count
parsetstmpdisplays slots and items in snapshot
comparetstmpcompares inventory against snapshot
restoretstmprestores slots and items in snapshot
" + ); + + c->SendPopupToClient(window_title.c_str(), window_text.c_str()); + return; } - if (database.SaveCharacterInventorySnapshot(((Client*)t)->CharacterID())) { - c->SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); - c->Message(0, "Successful inventory snapshot taken of %s", t->GetName()); - } - else { - c->SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); - c->Message(0, "Failed to take inventory snapshot of %s", t->GetName()); - } -} + if (c->Admin() >= commandInvSnapshot) { // global arguments -void command_clearinvsnapshots(Client *c, const Seperator *sep) -{ - if (strcmp(sep->arg[1], "false") == 0) { - database.ClearInvSnapshots(false); - c->Message(0, "Inventory snapshots cleared using current time"); + if (strcmp(sep->arg[1], "gcount") == 0) { + auto is_count = database.CountInvSnapshots(); + c->Message(0, "There %s %i inventory snapshot%s.", (is_count == 1 ? "is" : "are"), is_count, (is_count == 1 ? "" : "s")); + + return; + } + + if (strcmp(sep->arg[1], "gclear") == 0) { + if (strcmp(sep->arg[2], "now") == 0) { + database.ClearInvSnapshots(true); + c->Message(0, "Inventory snapshots cleared using current time."); + } + else { + database.ClearInvSnapshots(); + c->Message(0, "Inventory snapshots cleared using RuleI(Character, InvSnapshotHistoryD) (%i day%s).", + RuleI(Character, InvSnapshotHistoryD), (RuleI(Character, InvSnapshotHistoryD) == 1 ? "" : "s")); + } + + return; + } } - else { - database.ClearInvSnapshots(); - c->Message(0, "Inventory snapshots cleared using RuleI(Character, InvSnapshotHistoryD) (%i days)", RuleI(Character, InvSnapshotHistoryD)); + + if (!c->GetTarget() || !c->GetTarget()->IsClient()) { + c->Message(0, "Target must be a client."); + return; + } + + auto tc = (Client*)c->GetTarget(); + + if (strcmp(sep->arg[1], "capture") == 0) { + if (database.SaveCharacterInvSnapshot(tc->CharacterID())) { + tc->SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + c->Message(0, "Successful inventory snapshot taken of %s - setting next interval for %i minute%s.", + tc->GetName(), RuleI(Character, InvSnapshotMinIntervalM), (RuleI(Character, InvSnapshotMinIntervalM) == 1 ? "" : "s")); + } + else { + tc->SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); + c->Message(0, "Failed to take inventory snapshot of %s - retrying in %i minute%s.", + tc->GetName(), RuleI(Character, InvSnapshotMinRetryM), (RuleI(Character, InvSnapshotMinRetryM) == 1 ? "" : "s")); + } + + return; + } + + if (c->Admin() >= commandInvSnapshot) { + if (strcmp(sep->arg[1], "count") == 0) { + auto is_count = database.CountCharacterInvSnapshots(tc->CharacterID()); + c->Message(0, "%s (id: %u) has %i inventory snapshot%s.", tc->GetName(), tc->CharacterID(), is_count, (is_count == 1 ? "" : "s")); + + return; + } + + if (strcmp(sep->arg[1], "clear") == 0) { + if (strcmp(sep->arg[2], "now") == 0) { + database.ClearCharacterInvSnapshots(tc->CharacterID(), true); + c->Message(0, "%s\'s (id: %u) inventory snapshots cleared using current time.", tc->GetName(), tc->CharacterID()); + } + else { + database.ClearCharacterInvSnapshots(tc->CharacterID()); + c->Message(0, "%s\'s (id: %u) inventory snapshots cleared using RuleI(Character, InvSnapshotHistoryD) (%i day%s).", + tc->GetName(), tc->CharacterID(), RuleI(Character, InvSnapshotHistoryD), (RuleI(Character, InvSnapshotHistoryD) == 1 ? "" : "s")); + } + + return; + } + + if (strcmp(sep->arg[1], "list") == 0) { + std::list> is_list; + database.ListCharacterInvSnapshots(tc->CharacterID(), is_list); + + if (is_list.empty()) { + c->Message(0, "No inventory snapshots for %s (id: %u)", tc->GetName(), tc->CharacterID()); + return; + } + + auto list_count = 0; + if (sep->IsNumber(2)) + list_count = atoi(sep->arg[2]); + if (list_count < 1 || list_count > is_list.size()) + list_count = is_list.size(); + + std::string window_title = StringFormat("Snapshots for %s", tc->GetName()); + + std::string window_text = + "" + "" + "" + "" + ""; + + for (auto iter : is_list) { + if (!list_count) + break; + + window_text.append(StringFormat( + "" + "" + "" + "", + iter.first, + iter.second + )); + + --list_count; + } + + window_text.append( + "
TimestampEntry Count
%u%i
" + ); + + c->SendPopupToClient(window_title.c_str(), window_text.c_str()); + + return; + } + + if (strcmp(sep->arg[1], "parse") == 0) { + if (!sep->IsNumber(2)) { + c->Message(0, "A timestamp is required to use this option."); + return; + } + + uint32 timestamp = atoul(sep->arg[2]); + + if (!database.ValidateCharacterInvSnapshotTimestamp(tc->CharacterID(), timestamp)) { + c->Message(0, "No inventory snapshots for %s (id: %u) exist at %u.", tc->GetName(), tc->CharacterID(), timestamp); + return; + } + + std::list> parse_list; + database.ParseCharacterInvSnapshot(tc->CharacterID(), timestamp, parse_list); + + std::string window_title = StringFormat("Snapshot Parse for %s @ %u", tc->GetName(), timestamp); + + std::string window_text = "Slot: ItemID - Description
"; + + for (auto iter : parse_list) { + auto item_data = database.GetItem(iter.second); + std::string window_line = StringFormat("%i: %u - %s
", iter.first, iter.second, (item_data ? item_data->Name : "[error]")); + + if (window_text.length() + window_line.length() < 4095) { + window_text.append(window_line); + } + else { + c->Message(0, "Too many snapshot entries to list..."); + break; + } + } + + c->SendPopupToClient(window_title.c_str(), window_text.c_str()); + + return; + } + + if (strcmp(sep->arg[1], "compare") == 0) { + if (!sep->IsNumber(2)) { + c->Message(0, "A timestamp is required to use this option."); + return; + } + + uint32 timestamp = atoul(sep->arg[2]); + + if (!database.ValidateCharacterInvSnapshotTimestamp(tc->CharacterID(), timestamp)) { + c->Message(0, "No inventory snapshots for %s (id: %u) exist at %u.", tc->GetName(), tc->CharacterID(), timestamp); + return; + } + + std::list> inv_compare_list; + database.DivergeCharacterInventoryFromInvSnapshot(tc->CharacterID(), timestamp, inv_compare_list); + + std::list> iss_compare_list; + database.DivergeCharacterInvSnapshotFromInventory(tc->CharacterID(), timestamp, iss_compare_list); + + std::string window_title = StringFormat("Snapshot Comparison for %s @ %u", tc->GetName(), timestamp); + + std::string window_text = "Slot: (action) Snapshot -> Inventory
"; + + auto inv_iter = inv_compare_list.begin(); + auto iss_iter = iss_compare_list.begin(); + + while (true) { + std::string window_line; + + if (inv_iter == inv_compare_list.end() && iss_iter == iss_compare_list.end()) { + break; + } + else if (inv_iter != inv_compare_list.end() && iss_iter == iss_compare_list.end()) { + window_line = StringFormat("%i: (delete) [empty] -> %u
", inv_iter->first, inv_iter->second); + ++inv_iter; + } + else if (inv_iter == inv_compare_list.end() && iss_iter != iss_compare_list.end()) { + window_line = StringFormat("%i: (insert) %u -> [empty]
", iss_iter->first, iss_iter->second); + ++iss_iter; + } + else { + if (inv_iter->first < iss_iter->first) { + window_line = StringFormat("%i: (delete) [empty] -> %u
", inv_iter->first, inv_iter->second); + ++inv_iter; + } + else if (inv_iter->first > iss_iter->first) { + window_line = StringFormat("%i: (insert) %u -> [empty]
", iss_iter->first, iss_iter->second); + ++iss_iter; + } + else { + window_line = StringFormat("%i: (replace) %u -> %u
", iss_iter->first, iss_iter->second, inv_iter->second); + ++inv_iter; + ++iss_iter; + } + } + + if (window_text.length() + window_line.length() < 4095) { + window_text.append(window_line); + } + else { + c->Message(0, "Too many comparison entries to list..."); + break; + } + } + + c->SendPopupToClient(window_title.c_str(), window_text.c_str()); + + return; + } + + if (strcmp(sep->arg[1], "restore") == 0) { + if (!sep->IsNumber(2)) { + c->Message(0, "A timestamp is required to use this option."); + return; + } + + uint32 timestamp = atoul(sep->arg[2]); + + if (!database.ValidateCharacterInvSnapshotTimestamp(tc->CharacterID(), timestamp)) { + c->Message(0, "No inventory snapshots for %s (id: %u) exist at %u.", tc->GetName(), tc->CharacterID(), timestamp); + return; + } + + if (database.SaveCharacterInvSnapshot(tc->CharacterID())) { + tc->SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + } + else { + c->Message(13, "Failed to take pre-restore inventory snapshot of %s (id: %u).", + tc->GetName(), tc->CharacterID()); + return; + } + + if (database.RestoreCharacterInvSnapshot(tc->CharacterID(), timestamp)) { + // cannot delete all valid item slots from client..so, we worldkick + tc->WorldKick(); // self restores update before the 'kick' is processed + + c->Message(0, "Successfully applied snapshot %u to %s's (id: %u) inventory.", + timestamp, tc->GetName(), tc->CharacterID()); + } + else { + c->Message(13, "Failed to apply snapshot %u to %s's (id: %u) inventory.", + timestamp, tc->GetName(), tc->CharacterID()); + } + + return; + } } } diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index f79288126..cb0c81d3a 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1171,7 +1171,7 @@ bool ZoneDatabase::LoadCharacterData(uint32 character_id, PlayerProfile_Struct* m_epp->aa_effects = atoi(row[r]); r++; // "`e_aa_effects`, " m_epp->perAA = atoi(row[r]); r++; // "`e_percent_to_aa`, " m_epp->expended_aa = atoi(row[r]); r++; // "`e_expended_aa_spent`, " - m_epp->last_invsnapshot_time = atoi(row[r]); r++; // "`e_last_invsnapshot` " + m_epp->last_invsnapshot_time = atoul(row[r]); r++; // "`e_last_invsnapshot` " m_epp->next_invsnapshot_time = m_epp->last_invsnapshot_time + (RuleI(Character, InvSnapshotMinIntervalM) * 60); } return true; @@ -1567,56 +1567,6 @@ bool ZoneDatabase::SaveCharacterLeadershipAA(uint32 character_id, PlayerProfile_ return true; } -bool ZoneDatabase::SaveCharacterInventorySnapshot(uint32 character_id){ - uint32 time_index = time(nullptr); - std::string query = StringFormat( - "INSERT INTO inventory_snapshots (" - " time_index," - " charid," - " slotid," - " itemid," - " charges," - " color," - " augslot1," - " augslot2," - " augslot3," - " augslot4," - " augslot5," - " augslot6," - " instnodrop," - " custom_data," - " ornamenticon," - " ornamentidfile," - " ornament_hero_model" - ")" - " SELECT" - " %u," - " charid," - " slotid," - " itemid," - " charges," - " color," - " augslot1," - " augslot2," - " augslot3," - " augslot4," - " augslot5," - " augslot6," - " instnodrop," - " custom_data," - " ornamenticon," - " ornamentidfile," - " ornament_hero_model" - " FROM inventory" - " WHERE charid = %u", - time_index, - character_id - ); - auto results = database.QueryDatabase(query); - Log(Logs::General, Logs::None, "ZoneDatabase::SaveCharacterInventorySnapshot %i (%s)", character_id, (results.Success() ? "pass" : "fail")); - return results.Success(); -} - bool ZoneDatabase::SaveCharacterData(uint32 character_id, uint32 account_id, PlayerProfile_Struct* pp, ExtendedProfile_Struct* m_epp){ /* If this is ever zero - the client hasn't fully loaded and potentially crashed during zone */ @@ -2043,6 +1993,344 @@ bool ZoneDatabase::NoRentExpired(const char* name){ return (seconds>1800); } +bool ZoneDatabase::SaveCharacterInvSnapshot(uint32 character_id) { + uint32 time_index = time(nullptr); + std::string query = StringFormat( + "INSERT " + "INTO" + " `inventory_snapshots` " + "(`time_index`," + " `charid`," + " `slotid`," + " `itemid`," + " `charges`," + " `color`," + " `augslot1`," + " `augslot2`," + " `augslot3`," + " `augslot4`," + " `augslot5`," + " `augslot6`," + " `instnodrop`," + " `custom_data`," + " `ornamenticon`," + " `ornamentidfile`," + " `ornament_hero_model`" + ") " + "SELECT" + " %u," + " `charid`," + " `slotid`," + " `itemid`," + " `charges`," + " `color`," + " `augslot1`," + " `augslot2`," + " `augslot3`," + " `augslot4`," + " `augslot5`," + " `augslot6`," + " `instnodrop`," + " `custom_data`," + " `ornamenticon`," + " `ornamentidfile`," + " `ornament_hero_model` " + "FROM" + " `inventory` " + "WHERE" + " `charid` = %u", + time_index, + character_id + ); + auto results = database.QueryDatabase(query); + Log(Logs::Moderate, Logs::Inventory, "ZoneDatabase::SaveCharacterInventorySnapshot %i (%s)", character_id, (results.Success() ? "pass" : "fail")); + return results.Success(); +} + +int ZoneDatabase::CountCharacterInvSnapshots(uint32 character_id) { + std::string query = StringFormat( + "SELECT" + " COUNT(*) " + "FROM " + "(" + "SELECT * FROM" + " `inventory_snapshots` a " + "WHERE" + " `charid` = %u " + "GROUP BY" + " `time_index`" + ") b", + character_id + ); + auto results = QueryDatabase(query); + + if (!results.Success()) + return -1; + + auto row = results.begin(); + + int64 count = atoll(row[0]); + if (count > INT_MAX) + return -2; + if (count < 0) + return -3; + + return count; +} + +void ZoneDatabase::ClearCharacterInvSnapshots(uint32 character_id, bool from_now) { + uint32 del_time = time(nullptr); + if (!from_now) { del_time -= RuleI(Character, InvSnapshotHistoryD) * 86400; } + + std::string query = StringFormat( + "DELETE " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `charid` = %u " + "AND" + " `time_index` <= %lu", + character_id, + (unsigned long)del_time + ); + QueryDatabase(query); +} + +void ZoneDatabase::ListCharacterInvSnapshots(uint32 character_id, std::list> &is_list) { + std::string query = StringFormat( + "SELECT" + " `time_index`," + " COUNT(*) " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `charid` = %u " + "GROUP BY" + " `time_index` " + "ORDER BY" + " `time_index` " + "DESC", + character_id + ); + auto results = QueryDatabase(query); + + if (!results.Success()) + return; + + for (auto row : results) + is_list.push_back(std::pair(atoul(row[0]), atoi(row[1]))); +} + +bool ZoneDatabase::ValidateCharacterInvSnapshotTimestamp(uint32 character_id, uint32 timestamp) { + if (!character_id || !timestamp) + return false; + + std::string query = StringFormat( + "SELECT" + " * " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `charid` = %u " + "AND" + " `time_index` = %u " + "LIMIT 1", + character_id, + timestamp + ); + auto results = QueryDatabase(query); + + if (!results.Success() || results.RowCount() == 0) + return false; + + return true; +} + +void ZoneDatabase::ParseCharacterInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &parse_list) { + std::string query = StringFormat( + "SELECT" + " `slotid`," + " `itemid` " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `charid` = %u " + "AND" + " `time_index` = %u " + "ORDER BY" + " `slotid`", + character_id, + timestamp + ); + auto results = QueryDatabase(query); + + if (!results.Success()) + return; + + for (auto row : results) + parse_list.push_back(std::pair(atoi(row[0]), atoul(row[1]))); +} + +void ZoneDatabase::DivergeCharacterInvSnapshotFromInventory(uint32 character_id, uint32 timestamp, std::list> &compare_list) { + std::string query = StringFormat( + "SELECT" + " slotid," + " itemid " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `time_index` = %u " + "AND" + " `charid` = %u " + "AND" + " `slotid` NOT IN " + "(" + "SELECT" + " a.`slotid` " + "FROM" + " `inventory_snapshots` a " + "JOIN" + " `inventory` b " + "USING" + " (`slotid`, `itemid`) " + "WHERE" + " a.`time_index` = %u " + "AND" + " a.`charid` = %u " + "AND" + " b.`charid` = %u" + ")", + timestamp, + character_id, + timestamp, + character_id, + character_id + ); + auto results = QueryDatabase(query); + + if (!results.Success()) + return; + + for (auto row : results) + compare_list.push_back(std::pair(atoi(row[0]), atoul(row[1]))); +} + +void ZoneDatabase::DivergeCharacterInventoryFromInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &compare_list) { + std::string query = StringFormat( + "SELECT" + " `slotid`," + " `itemid` " + "FROM" + " `inventory` " + "WHERE" + " `charid` = %u " + "AND" + " `slotid` NOT IN " + "(" + "SELECT" + " a.`slotid` " + "FROM" + " `inventory` a " + "JOIN" + " `inventory_snapshots` b " + "USING" + " (`slotid`, `itemid`) " + "WHERE" + " b.`time_index` = %u " + "AND" + " b.`charid` = %u " + "AND" + " a.`charid` = %u" + ")", + character_id, + timestamp, + character_id, + character_id + ); + auto results = QueryDatabase(query); + + if (!results.Success()) + return; + + for (auto row : results) + compare_list.push_back(std::pair(atoi(row[0]), atoul(row[1]))); +} + +bool ZoneDatabase::RestoreCharacterInvSnapshot(uint32 character_id, uint32 timestamp) { + // we should know what we're doing by the time we call this function..but, + // this is to prevent inventory deletions where no timestamp entries exists + if (!ValidateCharacterInvSnapshotTimestamp(character_id, timestamp)) { + Log(Logs::General, Logs::Error, "ZoneDatabase::RestoreCharacterInvSnapshot() called for id: %u without valid snapshot entries @ %u", character_id, timestamp); + return false; + } + + std::string query = StringFormat( + "DELETE " + "FROM" + " `inventory` " + "WHERE" + " `charid` = %u", + character_id + ); + auto results = database.QueryDatabase(query); + if (!results.Success()) + return false; + + query = StringFormat( + "INSERT " + "INTO" + " `inventory` " + "(`charid`," + " `slotid`," + " `itemid`," + " `charges`," + " `color`," + " `augslot1`," + " `augslot2`," + " `augslot3`," + " `augslot4`," + " `augslot5`," + " `augslot6`," + " `instnodrop`," + " `custom_data`," + " `ornamenticon`," + " `ornamentidfile`," + " `ornament_hero_model`" + ") " + "SELECT" + " `charid`," + " `slotid`," + " `itemid`," + " `charges`," + " `color`," + " `augslot1`," + " `augslot2`," + " `augslot3`," + " `augslot4`," + " `augslot5`," + " `augslot6`," + " `instnodrop`," + " `custom_data`," + " `ornamenticon`," + " `ornamentidfile`," + " `ornament_hero_model` " + "FROM" + " `inventory_snapshots` " + "WHERE" + " `charid` = %u " + "AND" + " `time_index` = %u", + character_id, + timestamp + ); + results = database.QueryDatabase(query); + + Log(Logs::General, Logs::Inventory, "ZoneDatabase::RestoreCharacterInvSnapshot() %s snapshot for %u @ %u", + (results.Success() ? "restored" : "failed to restore"), character_id, timestamp); + + return results.Success(); +} + const NPCType* ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load /*= false*/) { const NPCType *npc = nullptr; diff --git a/zone/zonedb.h b/zone/zonedb.h index 8943f6f01..78f78ab83 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -315,7 +315,6 @@ public: bool SaveCharacterBandolier(uint32 character_id, uint8 bandolier_id, uint8 bandolier_slot, uint32 item_id, uint32 icon, const char* bandolier_name); bool SaveCharacterPotionBelt(uint32 character_id, uint8 potion_id, uint32 item_id, uint32 icon); bool SaveCharacterLeadershipAA(uint32 character_id, PlayerProfile_Struct* pp); - bool SaveCharacterInventorySnapshot(uint32 character_id); /* Character Data Deletes */ bool DeleteCharacterSpell(uint32 character_id, uint32 spell_id, uint32 slot_id); @@ -328,7 +327,16 @@ public: /* Character Inventory */ bool NoRentExpired(const char* name); - + bool SaveCharacterInvSnapshot(uint32 character_id); + int CountCharacterInvSnapshots(uint32 character_id); + void ClearCharacterInvSnapshots(uint32 character_id, bool from_now = false); + void ListCharacterInvSnapshots(uint32 character_id, std::list> &is_list); + bool ValidateCharacterInvSnapshotTimestamp(uint32 character_id, uint32 timestamp); + void ParseCharacterInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &parse_list); + void DivergeCharacterInvSnapshotFromInventory(uint32 character_id, uint32 timestamp, std::list> &compare_list); + void DivergeCharacterInventoryFromInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &compare_list); + bool RestoreCharacterInvSnapshot(uint32 character_id, uint32 timestamp); + /* Corpses */ bool DeleteItemOffCharacterCorpse(uint32 db_id, uint32 equip_slot, uint32 item_id); uint32 GetCharacterCorpseItemCount(uint32 corpse_id);