diff --git a/changelog.txt b/changelog.txt index d0d757df0..0fd73aeba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,16 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 09/25/2015 == +Uleat: Implemented 'Inventory Snapshot' feature to track online player inventories at timed intervals. +rules: + 'Character:ActiveInvSnapshots' - active (true) or inactive (false - default) + 'Character:InvSnapshotMinIntervalM' - minimum time between snapshots (in minutes) + 'Character:InvSnapshotMinRetryM' - minimum time to attempt a retry after a failed snapshot (in minutes) + 'Character:InvSnapshotHistoryD' - minimum time to keep snapshot entries (in days) +commands: + '#invsnapshot' - Takes a snapshot of target client's inventory (feature active or inactive) + '#clearinvsnapshots [use rule]' - Clears snapshot entries based on bool argument ([true] - honors the 'InvSnapshotHistoryD' rule, [false] - erases all) + == 08/02/2015 == Shendare: VS2013 query StringFormat glitches when "%f" is passed for the int GetRunSpeed(). Shendare: In CreateNewNPCCommand(), the npc_type_id and spawngroupid are created in the database, but never set in the spawn class, so later it can't delete them with #npcspawn remove or #npcspawn delete. diff --git a/common/database.cpp b/common/database.cpp index b27722fe9..570bcbb43 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -2173,3 +2173,12 @@ void Database::LoadLogSettings(EQEmuLogSys::LogSettings* log_settings) } } } + +void Database::ClearInvSnapshots(bool use_rule) +{ + uint32 del_time = time(nullptr); + if (use_rule) { 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 f3b8ba8c1..1543a14e3 100644 --- a/common/database.h +++ b/common/database.h @@ -248,6 +248,8 @@ public: void SetLFP(uint32 CharID, bool LFP); void SetLoginFlags(uint32 CharID, bool LFP, bool LFG, uint8 firstlogon); + void ClearInvSnapshots(bool use_rule = true); + /* EQEmuLogSys */ void LoadLogSettings(EQEmuLogSys::LogSettings* log_settings); diff --git a/common/extprofile.h b/common/extprofile.h index a8447d4fc..49b8fb8f3 100644 --- a/common/extprofile.h +++ b/common/extprofile.h @@ -54,6 +54,8 @@ struct ExtendedProfile_Struct { uint32 mercTimerRemaining; /* Not Used */ uint8 mercGender; /* Not Used */ int32 mercState; /* Not Used */ + uint32 last_invsnapshot_time; /* Used */ + uint32 next_invsnapshot_time; /* Used */ }; #pragma pack() diff --git a/common/ruletypes.h b/common/ruletypes.h index b3f5e3460..8cb2ed0db 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -119,6 +119,10 @@ RULE_BOOL(Character, MarqueeHPUpdates, false) // Will show Health % in center of RULE_INT(Character, IksarCommonTongue, 95) // 95 By default (live-like?) RULE_INT(Character, OgreCommonTongue, 95) // 95 By default (live-like?) RULE_INT(Character, TrollCommonTongue, 95) // 95 By default (live-like?) +RULE_BOOL(Character, ActiveInvSnapshots, false) // Takes a periodic snapshot of inventory contents from online players +RULE_INT(Character, InvSnapshotMinIntervalM, 180) // Minimum time (in minutes) between inventory snapshots +RULE_INT(Character, InvSnapshotMinRetryM, 30) // Time (in minutes) to re-attempt an inventory snapshot after a failure +RULE_INT(Character, InvSnapshotHistoryD, 30) // Time (in days) to keep snapshot entries RULE_CATEGORY_END() RULE_CATEGORY(Mercs) diff --git a/common/version.h b/common/version.h index 11a18cd0d..74dd0cbd6 100644 --- a/common/version.h +++ b/common/version.h @@ -30,7 +30,7 @@ Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9086 +#define CURRENT_BINARY_DATABASE_VERSION 9087 #define COMPILE_DATE __DATE__ #define COMPILE_TIME __TIME__ #ifndef WIN32 diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 7394c7863..239fdbb67 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -340,6 +340,7 @@ 9084|2015_06_30_runspeed_adjustments.sql|SELECT `runspeed` FROM `npc_types` WHERE `runspeed` > 3|not_empty| 9085|2015_07_01_Marquee_Rule.sql|SELECT * FROM `rule_values` WHERE `rule_name` LIKE '%Character:MarqueeHPUpdates%'|empty| 9086|2015_07_02_aa_rework.sql|SHOW TABLES LIKE 'aa_ranks'|empty| +9087|2015_09_25_inventory_snapshots.sql|SHOW TABLES LIKE 'inventory_snapshots'|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/required/2015_09_25_inventory_snapshots.sql b/utils/sql/git/required/2015_09_25_inventory_snapshots.sql new file mode 100644 index 000000000..12b167e8b --- /dev/null +++ b/utils/sql/git/required/2015_09_25_inventory_snapshots.sql @@ -0,0 +1,46 @@ +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; + +ALTER TABLE `character_data` ADD COLUMN `e_last_invsnapshot` INT(11) UNSIGNED NOT NULL DEFAULT '0'; + +INSERT INTO `rule_values` VALUES +(1, 'Character:ActiveInvSnapshots', 'false', 'Takes a periodic snapshot of inventory contents from online players'), +(2, 'Character:ActiveInvSnapshots', 'false', 'Takes a periodic snapshot of inventory contents from online players'), +(4, 'Character:ActiveInvSnapshots', 'false', 'Takes a periodic snapshot of inventory contents from online players'), +(5, 'Character:ActiveInvSnapshots', 'false', 'Takes a periodic snapshot of inventory contents from online players'), +(10, 'Character:ActiveInvSnapshots', 'false', 'Takes a periodic snapshot of inventory contents from online players'), +(1, 'Character:InvSnapshotMinIntervalM', '180', 'Minimum time (in minutes) between inventory snapshots'), +(2, 'Character:InvSnapshotMinIntervalM', '180', 'Minimum time (in minutes) between inventory snapshots'), +(4, 'Character:InvSnapshotMinIntervalM', '180', 'Minimum time (in minutes) between inventory snapshots'), +(5, 'Character:InvSnapshotMinIntervalM', '180', 'Minimum time (in minutes) between inventory snapshots'), +(10, 'Character:InvSnapshotMinIntervalM', '180', 'Minimum time (in minutes) between inventory snapshots'), +(1, 'Character:InvSnapshotMinRetryM', '30', 'Time (in minutes) to re-attempt an inventory snapshot after a failure'), +(2, 'Character:InvSnapshotMinRetryM', '30', 'Time (in minutes) to re-attempt an inventory snapshot after a failure'), +(4, 'Character:InvSnapshotMinRetryM', '30', 'Time (in minutes) to re-attempt an inventory snapshot after a failure'), +(5, 'Character:InvSnapshotMinRetryM', '30', 'Time (in minutes) to re-attempt an inventory snapshot after a failure'), +(10, 'Character:InvSnapshotMinRetryM', '30', 'Time (in minutes) to re-attempt an inventory snapshot after a failure'), +(1, 'Character:InvSnapshotHistoryD', '30', 'Time (in days) to keep snapshot entries'), +(2, 'Character:InvSnapshotHistoryD', '30', 'Time (in days) to keep snapshot entries'), +(4, 'Character:InvSnapshotHistoryD', '30', 'Time (in days) to keep snapshot entries'), +(5, 'Character:InvSnapshotHistoryD', '30', 'Time (in days) to keep snapshot entries'), +(10, 'Character:InvSnapshotHistoryD', '30', 'Time (in days) to keep snapshot entries'); diff --git a/world/net.cpp b/world/net.cpp index 3032e4c97..7c8597bc5 100644 --- a/world/net.cpp +++ b/world/net.cpp @@ -310,6 +310,8 @@ int main(int argc, char** argv) { database.ClearRaid(); database.ClearRaidDetails(); database.ClearRaidLeader(); + Log.Out(Logs::General, Logs::World_Server, "Clearing inventory snapshots.."); + database.ClearInvSnapshots(); Log.Out(Logs::General, Logs::World_Server, "Loading items.."); if(!database.LoadItems(hotfix_name)) Log.Out(Logs::General, Logs::World_Server, "Error: Could not load item data. But ignoring"); diff --git a/zone/client.cpp b/zone/client.cpp index cce3dbd29..ef0c9bf7d 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -642,6 +642,17 @@ bool Client::Save(uint8 iCommitNow) { m_pp.hunger_level = EQEmu::Clamp(m_pp.hunger_level, 0, 50000); m_pp.thirst_level = EQEmu::Clamp(m_pp.thirst_level, 0, 50000); + + // perform snapshot before SaveCharacterData() so that m_epp will contain the updated time + if (RuleB(Character, ActiveInvSnapshots) && time(nullptr) >= GetNextInvSnapshotTime()) { + if (database.SaveCharacterInventorySnapshot(CharacterID())) { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinIntervalM)); + } + else { + SetNextInvSnapshot(RuleI(Character, InvSnapshotMinRetryM)); + } + } + database.SaveCharacterData(this->CharacterID(), this->AccountID(), &m_pp, &m_epp); /* Save Character Data */ return true; diff --git a/zone/client.h b/zone/client.h index 9fa68350e..885eb1407 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1250,6 +1250,13 @@ public: bool InterrogateInventory(Client* requester, bool log, bool silent, bool allowtrip, bool& error, bool autolog = true); + void SetNextInvSnapshot(uint32 interval_in_min) { + m_epp.last_invsnapshot_time = time(nullptr); + m_epp.next_invsnapshot_time = m_epp.last_invsnapshot_time + (interval_in_min * 60); + } + uint32 GetLastInvSnapshotTime() { return m_epp.last_invsnapshot_time; } + uint32 GetNextInvSnapshotTime() { return m_epp.next_invsnapshot_time; } + //Command #Tune functions virtual int32 Tune_GetMeleeMitDmg(Mob* GM, Mob *attacker, int32 damage, int32 minhit, float mit_rating, float atk_rating); int32 GetMeleeDamage(Mob* other, bool GetMinDamage = false); diff --git a/zone/command.cpp b/zone/command.cpp index 144fada08..5fe6244ad 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -183,6 +183,7 @@ 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("close_shop", nullptr, 100, command_merchantcloseshop) || command_add("connectworld", nullptr,0, command_connectworldserver) || command_add("connectworldserver", "- Make zone attempt to connect to worldserver", 200, command_connectworldserver) || @@ -260,6 +261,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("invul", nullptr,0, command_invul) || command_add("invulnerable", "[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) || @@ -2836,6 +2838,36 @@ void command_interrogateinv(Client *c, const Seperator *sep) c->Message(13, "An unknown error occurred while processing Client::InterrogateInventory()"); } +void command_invsnapshot(Client *c, const Seperator *sep) +{ + auto t = c->GetTarget(); + if (!t || !t->IsClient()) { + c->Message(0, "Target must be a client"); + 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()); + } +} + +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"); + } + else { + database.ClearInvSnapshots(); + c->Message(0, "Inventory snapshots cleared using RuleI(Character, InvSnapshotHistoryD) (%i days)", RuleI(Character, InvSnapshotHistoryD)); + } +} + void command_findnpctype(Client *c, const Seperator *sep) { if(sep->arg[1][0] == 0) { diff --git a/zone/command.h b/zone/command.h index 67f813456..03e30a786 100644 --- a/zone/command.h +++ b/zone/command.h @@ -154,6 +154,8 @@ void command_appearance(Client *c, const Seperator *sep); void command_nukeitem(Client *c, const Seperator *sep); void command_peekinv(Client *c, const Seperator *sep); void command_interrogateinv(Client *c, const Seperator *sep); +void command_invsnapshot(Client *c, const Seperator *sep); +void command_clearinvsnapshots(Client *c, const Seperator *sep); void command_findnpctype(Client *c, const Seperator *sep); void command_findzone(Client *c, const Seperator *sep); void command_viewnpctype(Client *c, const Seperator *sep); diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 08a3dd38a..e92704fe6 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -887,7 +887,8 @@ bool ZoneDatabase::LoadCharacterData(uint32 character_id, PlayerProfile_Struct* "RestTimer, " "`e_aa_effects`, " "`e_percent_to_aa`, " - "`e_expended_aa_spent` " + "`e_expended_aa_spent`, " + "`e_last_invsnapshot` " "FROM " "character_data " "WHERE `id` = %i ", character_id); @@ -983,7 +984,9 @@ bool ZoneDatabase::LoadCharacterData(uint32 character_id, PlayerProfile_Struct* pp->RestTimer = atoi(row[r]); r++; // "RestTimer, " 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->expended_aa = atoi(row[r]); r++; // "`e_expended_aa_spent`, " + m_epp->last_invsnapshot_time = atoi(row[r]); r++; // "`e_last_invsnapshot` " + m_epp->next_invsnapshot_time = m_epp->last_invsnapshot_time + (RuleI(Character, InvSnapshotMinIntervalM) * 60); } return true; } @@ -1377,6 +1380,56 @@ 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.Out(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){ clock_t t = std::clock(); /* Function timer start */ std::string query = StringFormat( @@ -1473,7 +1526,8 @@ bool ZoneDatabase::SaveCharacterData(uint32 character_id, uint32 account_id, Pla " RestTimer, " " e_aa_effects, " " e_percent_to_aa, " - " e_expended_aa_spent " + " e_expended_aa_spent, " + " e_last_invsnapshot " ") " "VALUES (" "%u," // id " id, " @@ -1568,7 +1622,8 @@ bool ZoneDatabase::SaveCharacterData(uint32 character_id, uint32 account_id, Pla "%u," // RestTimer pp->RestTimer, " RestTimer) " "%u," // e_aa_effects "%u," // e_percent_to_aa - "%u" // e_expended_aa_spent + "%u," // e_expended_aa_spent + "%u" // e_last_invsnapshot ")", character_id, // " id, " account_id, // " account_id, " @@ -1662,7 +1717,8 @@ bool ZoneDatabase::SaveCharacterData(uint32 character_id, uint32 account_id, Pla pp->RestTimer, // " RestTimer) " m_epp->aa_effects, m_epp->perAA, - m_epp->expended_aa + m_epp->expended_aa, + m_epp->last_invsnapshot_time ); auto results = database.QueryDatabase(query); Log.Out(Logs::General, Logs::None, "ZoneDatabase::SaveCharacterData %i, done... Took %f seconds", character_id, ((float)(std::clock() - t)) / CLOCKS_PER_SEC); diff --git a/zone/zonedb.h b/zone/zonedb.h index 35275b654..9b2a5105b 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -290,6 +290,7 @@ 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);