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 =
+ "
"
+ ""
+ "| Usage: | "
+ " | "
+ "#invsnapshot arguments (required optional) | "
+ "
"
+ ""
+ "| help | "
+ " | "
+ "this menu | "
+ "
"
+ ""
+ "| capture | "
+ " | "
+ "takes snapshot of character inventory | "
+ "
";
+
+ if (c->Admin() >= commandInvSnapshot)
+ window_text.append(
+ ""
+ "| gcount | "
+ " | "
+ "returns global snapshot count | "
+ "
"
+ ""
+ "| gclear | "
+ " now | "
+ "delete all snapshots - rule delete all snapshots - now | "
+ "
"
+ ""
+ "| count | "
+ " | "
+ "returns character snapshot count | "
+ "
"
+ ""
+ "| clear | "
+ " now | "
+ "delete character snapshots - rule delete character snapshots - now | "
+ "
"
+ ""
+ "| list | "
+ " count | "
+ "lists entry ids for current character limits to count | "
+ "
"
+ ""
+ "| parse | "
+ "tstmp | "
+ "displays slots and items in snapshot | "
+ "
"
+ ""
+ "| compare | "
+ "tstmp | "
+ "compares inventory against snapshot | "
+ "
"
+ ""
+ "| restore | "
+ "tstmp | "
+ "restores slots and items in snapshot | "
+ "
"
+ );
+
+ window_text.append(
+ "
"
+ );
+
+ 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 =
+ ""
+ ""
+ "| Timestamp | "
+ "Entry Count | "
+ "
";
+
+ for (auto iter : is_list) {
+ if (!list_count)
+ break;
+
+ window_text.append(StringFormat(
+ ""
+ "| %u | "
+ "%i | "
+ "
",
+ iter.first,
+ iter.second
+ ));
+
+ --list_count;
+ }
+
+ window_text.append(
+ "
"
+ );
+
+ 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);