diff --git a/DiscordConnector.csproj b/DiscordConnector.csproj index 6db13cb..b2d430b 100644 --- a/DiscordConnector.csproj +++ b/DiscordConnector.csproj @@ -35,7 +35,7 @@ - + diff --git a/Metadata/CHANGELOG.md b/Metadata/CHANGELOG.md index f8e9eee..2fecaea 100644 --- a/Metadata/CHANGELOG.md +++ b/Metadata/CHANGELOG.md @@ -2,6 +2,53 @@ A full changelog for reference. +### Version 1.5.0 + +Features: + +- Using LiteDB for record storage. + +Because of how unreliable storing the records in a "roll-your-own" +database with a JSON file was, and because of the increased flexibility +in what could be stored, I've changed the storage system for the +recorded player stats to use LiteDB. Currently this means records for +join/leave/death/shout/ping will be timestamped, include the position of +the event, have the player name, and the player's steamid. Hopefully +adding this additional information will allow for more customization +options for the users of this mod. + +It is set up to do a migration on first load of the updated plugin, the +steps it follows for that is: + + 1. check if records.json (or configured name) exists + 2. read all records from the file + 3. parse the records + 4. loop through all the records and add them to the database + + Records added this way will have position of zero and a + steamid of 1. + + 5. move the records.json file to records.json.migrated + +If you don't want to have it auto-migrate the records, rename your +records.json or delete it. If the name does not match exactly it will +not migrate the data. + +For the migration steps, it will be outputting log information (at INFO +level) with how many records were migrated and which steps completed. + +- Ranked Lowest Player Leaderbaord + +Added an inverse of the Top Player leaderboard. + +- Custom leaderboard heading messages + +Added configuration for the messages sent at the top of the leaderboard +messages. + +- The variable `%PUBLICIP%` can be used in _any_ message configuration + now. + ### Version 1.4.4 Fixes: diff --git a/Metadata/DiscordConnector-Nexus.readme b/Metadata/DiscordConnector-Nexus.readme index fac8ee7..b43b0eb 100644 --- a/Metadata/DiscordConnector-Nexus.readme +++ b/Metadata/DiscordConnector-Nexus.readme @@ -1,4 +1,4 @@ -DISCORD CONNECTOR - 1.4.0 +DISCORD CONNECTOR - 1.5.0 Connect your Valheim server to a Discord Webhook. Works for both dedicated and client-hosted servers. (Visit the github repo for a how-to guide.) :: REQUIREMENTS :: diff --git a/Metadata/README.md b/Metadata/README.md index 0eab14e..f6d4def 100644 --- a/Metadata/README.md +++ b/Metadata/README.md @@ -52,59 +52,52 @@ records.json 1.2.0+ (PlayerName changed to Key) [{"Category":"death","Values":[{"Key":"Xithyr","Value":13} ... ``` -### Version 1.4.4 - -Fixes: - -- Position being sent with event messages even if event position was disabled in config - -### Version 1.4.3 - -Fixes: - -- Event messages were sending the wrong message (start instead of end and vice-versa) -- Event Stop messages were sending zero coordinates -- If you had enabled first death message and death message (this is default settings), you would -get two messages. This has been changed to merge the messages into one if both settings are on -and it's a player's first death. +### Version 1.5.0 Features: -- Added toggles to enable/disable some event debug messages (all disabled by default) -- Added a toggle to enable/disable a debug message with responses from the webhook (disabled by default) - -### Version 1.4.2 +- Using LiteDB for record storage. -Fixes: +Because of how unreliable storing the records in a "roll-your-own" +database with a JSON file was, and because of the increased flexibility +in what could be stored, I've changed the storage system for the +recorded player stats to use LiteDB. Currently this means records for +join/leave/death/shout/ping will be timestamped, include the position of +the event, have the player name, and the player's steamid. Hopefully +adding this additional information will allow for more customization +options for the users of this mod. -- Least deaths leaderboard wasn't respecting the correct config entry. (THanks @thedefside) +It is set up to do a migration on first load of the updated plugin, the +steps it follows for that is: -### Version 1.4.1 + 1. check if records.json (or configured name) exists + 2. read all records from the file + 3. parse the records + 4. loop through all the records and add them to the database -Fixes: + Records added this way will have position of zero and a + steamid of 1. -- Removed the two debug logging calls for events -- sorry for the log spam! + 5. move the records.json file to records.json.migrated -### Version 1.4.0 +If you don't want to have it auto-migrate the records, rename your +records.json or delete it. If the name does not match exactly it will +not migrate the data. -Features: +For the migration steps, it will be outputting log information (at INFO +level) with how many records were migrated and which steps completed. -- 10 user defined variables that can be used an any messages (%VAR1% thru %VAR10%). These are set in their own configuration file, -`games.nwest.valheim.discordconnector-variables.cfg` which will get generated first time 1.4.0 is run. -- The position of where the player/ping/event coordinates are inserted into messages is configurable using the `%POS%` variable in -the messages config. It won't be replaced if the "send coordinates" toggle is off for that message. If you don't include a `%POS%` -variable, it will append the coordinates as happens with previous versions. +- Ranked Lowest Player Leaderbaord -Fixes: +Added an inverse of the Top Player leaderboard. -- Fixed an off-by-one error in the Top Players leaderboard (the default leaderboard) (Thanks @thedefside) -- Fixed configuration not referencing proper settings (Thanks @thedefside) -- Fixed event messages (now properly functioning on dedicated servers) +- Custom leaderboard heading messages -Breaking Changes: +Added configuration for the messages sent at the top of the leaderboard +messages. -- If you used `%PLAYERS%` in any of the event messages, you need to remove it. With the changes required for the event messages -functionality, it is not supportable at this time. +- The variable `%PUBLICIP%` can be used in _any_ message configuration + now. Full changelog history available on the [Github repository](https://github.com/nwesterhausen/valheim-discordconnector/blob/main/Metadata/CHANGELOG.md) diff --git a/Metadata/manifest.json b/Metadata/manifest.json index c247b6f..a3c46a8 100644 --- a/Metadata/manifest.json +++ b/Metadata/manifest.json @@ -1,6 +1,6 @@ { "name": "DiscordConnector", - "version_number": "1.4.4", + "version_number": "1.5.0", "website_url": "https://discordconnector.valheim.nwest.games/", "description": "Connects your Valheim server to a Discord webhook. Works for both dedicated and client-hosted servers.", "dependencies": ["denikson-BepInExPack_Valheim-5.4.1600"] diff --git a/README.md b/README.md index ff8fd4b..5c4d84f 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,10 @@ The compiled plugin will be in a zip ready for upload at `bin/DiscordConnector.z ### Dependencies -For JSON serialization, I chose to use the System.Text.Json library which is part of -the most recent .NET but can be used with .NET 4.8 which is used in this project. +For JSON serialization, using Newtonsoft.Json + +For data storage/retrieval using [LiteDB](https://www.litedb.org/) +(If you want to read the database file generated, you can use [LitDB Studio](https://github.com/mbdavid/LiteDB.Studio/releases/latest)) ### Contributors diff --git a/docs/changelog.md b/docs/changelog.md index 85eddde..e9b0381 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,53 @@ A full changelog +### Version 1.5.0 + +Features: + +- Using LiteDB for record storage. + +Because of how unreliable storing the records in a "roll-your-own" +database with a JSON file was, and because of the increased flexibility +in what could be stored, I've changed the storage system for the +recorded player stats to use LiteDB. Currently this means records for +join/leave/death/shout/ping will be timestamped, include the position of +the event, have the player name, and the player's steamid. Hopefully +adding this additional information will allow for more customization +options for the users of this mod. + +It is set up to do a migration on first load of the updated plugin, the +steps it follows for that is: + + 1. check if records.json (or configured name) exists + 2. read all records from the file + 3. parse the records + 4. loop through all the records and add them to the database + + Records added this way will have position of zero and a + steamid of 1. + + 5. move the records.json file to records.json.migrated + +If you don't want to have it auto-migrate the records, rename your +records.json or delete it. If the name does not match exactly it will +not migrate the data. + +For the migration steps, it will be outputting log information (at INFO +level) with how many records were migrated and which steps completed. + +- Ranked Lowest Player Leaderbaord + +Added an inverse of the Top Player leaderboard. + +- Custom leaderboard heading messages + +Added configuration for the messages sent at the top of the leaderboard +messages. + +- The variable `%PUBLICIP%` can be used in _any_ message configuration + now. + ### Version 1.4.4 Fixes: diff --git a/src/Config/MessagesConfig.cs b/src/Config/MessagesConfig.cs index a9b53be..a416bd6 100644 --- a/src/Config/MessagesConfig.cs +++ b/src/Config/MessagesConfig.cs @@ -14,6 +14,7 @@ internal class MessagesConfig private const string PLAYER_MESSAGES = "Messages.Player"; private const string PLAYER_FIRSTS_MESSAGES = "Messages.PlayerFirsts"; private const string EVENT_MESSAGES = "Messages.Events"; + private const string BOARD_MESSAGES = "Messages.Leaderbaords"; // Server Messages private ConfigEntry serverLaunchMessage; @@ -42,6 +43,12 @@ internal class MessagesConfig private ConfigEntry eventStopMessage; private ConfigEntry eventResumedMessage; + // Board Messages + private ConfigEntry leaderboardTopPlayersMessage; + private ConfigEntry leaderboardBottomPlayersMessage; + private ConfigEntry leaderboardHighestPlayerMessage; + private ConfigEntry leaderboardLowestPlayerMessage; + public MessagesConfig(ConfigFile configFile) { config = configFile; @@ -173,6 +180,28 @@ private void LoadConfig() "The special string %EVENT_END_MSG% will be replaced with the message that is displayed on the screen when the event ends."); // + Environment.NewLine + // "The special string %PLAYERS% will be replaced with a list of players in the event area."); //! Removed due to unreliability + // Board Messages + leaderboardTopPlayersMessage = config.Bind(BOARD_MESSAGES, + "Leaderboard Heading for Top N Players", + "Top %N% Player Leaderboards:", + "Set the message that is included as a heading when this leaderboard is sent." + Environment.NewLine + + "Include %N% to include the number of rankings returned (the configured number)"); + leaderboardBottomPlayersMessage = config.Bind(BOARD_MESSAGES, + "Leaderboard Heading for Bottom N Players", + "Bottom %N% Player Leaderboards:", + "Set the message that is included as a heading when this leaderboard is sent." + Environment.NewLine + + "Include %N% to include the number of rankings returned (the configured number)"); + leaderboardHighestPlayerMessage = config.Bind(BOARD_MESSAGES, + "Leaderboard Heading for Highest Player", + "Top Performer", + "Set the message that is included as a heading when this leaderboard is sent." + Environment.NewLine + + "Include %N% to include the number of rankings returned (the configured number)"); + leaderboardLowestPlayerMessage = config.Bind(BOARD_MESSAGES, + "Leaderboard Heading for Lowest Player", + "Bottom Performer", + "Set the message that is included as a heading when this leaderboard is sent." + Environment.NewLine + + "Include %N% to include the number of rankings returned (the configured number)"); + config.Save(); } @@ -209,6 +238,13 @@ public string ConfigAsJson() jsonString += $"\"eventPausedMessage\":\"{eventPausedMessage.Value}\","; jsonString += $"\"eventResumedMessage\":\"{eventResumedMessage.Value}\","; jsonString += $"\"eventStopMessage\":\"{eventStopMessage.Value}\""; + jsonString += "},"; + + jsonString += $"\"{BOARD_MESSAGES}\":{{"; + jsonString += $"\"leaderboardTopPlayersMessage\":\"{leaderboardTopPlayersMessage.Value}\","; + jsonString += $"\"leaderboardBottomPlayersMessage\":\"{leaderboardBottomPlayersMessage.Value}\","; + jsonString += $"\"leaderboardHighestPlayerMessage\":\"{leaderboardHighestPlayerMessage.Value}\","; + jsonString += $"\"leaderboardLowestPlayerMessage\":\"{leaderboardLowestPlayerMessage.Value}\""; jsonString += "}"; jsonString += "}"; @@ -254,5 +290,10 @@ private static string GetRandomStringFromValue(ConfigEntry configEntry) public string EventStopMesssage => GetRandomStringFromValue(eventStopMessage); public string EventResumedMesssage => GetRandomStringFromValue(eventResumedMessage); + // Messages.Leaderboards + public string LeaderboardTopPlayerHeading => GetRandomStringFromValue(leaderboardTopPlayersMessage); + public string LeaderboardBottomPlayersHeading => GetRandomStringFromValue(leaderboardBottomPlayersMessage); + public string LeaderboardHighestHeading => GetRandomStringFromValue(leaderboardHighestPlayerMessage); + public string LeaderboardLowestHeading => GetRandomStringFromValue(leaderboardLowestPlayerMessage); } } diff --git a/src/Config/PluginConfig.cs b/src/Config/PluginConfig.cs index 6b6cb02..4ee0934 100644 --- a/src/Config/PluginConfig.cs +++ b/src/Config/PluginConfig.cs @@ -158,13 +158,19 @@ public void ReloadConfig() public bool DebugEveryEventChange => togglesConfig.DebugEveryEventChange; public bool DebugHttpRequestResponse => togglesConfig.DebugHttpRequestResponse; + // Leaderboard Messages + public string LeaderboardTopPlayerHeading => messagesConfig.LeaderboardTopPlayerHeading; + public string LeaderboardBottomPlayersHeading => messagesConfig.LeaderboardBottomPlayersHeading; + public string LeaderboardHighestHeading => messagesConfig.LeaderboardHighestHeading; + public string LeaderboardLowestHeading => messagesConfig.LeaderboardLowestHeading; + public string ConfigAsJson() { string jsonString = "{"; jsonString += $"\"Config.Main\":{mainConfig.ConfigAsJson()},"; jsonString += $"\"Config.Messages\":{messagesConfig.ConfigAsJson()},"; - jsonString += $"\"Config.Toggles\":{togglesConfig.ConfigAsJson()}"; + jsonString += $"\"Config.Toggles\":{togglesConfig.ConfigAsJson()},"; jsonString += $"\"Config.Variables\":{variableConfig.ConfigAsJson()}"; jsonString += "}"; diff --git a/src/Leaderboard/BottomPlayer.cs b/src/Leaderboard/BottomPlayer.cs new file mode 100644 index 0000000..eaafa92 --- /dev/null +++ b/src/Leaderboard/BottomPlayer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace DiscordConnector.Leaderboards +{ + internal class BottomPlayers : Base + { + public override void SendLeaderboard() + { + List> leaderFields = new List>(); + + var deaths = Records.Helper.BottomNResultForCategory(Records.Categories.Death, Plugin.StaticConfig.IncludedNumberOfRankings); + var sessions = Records.Helper.BottomNResultForCategory(Records.Categories.Join, Plugin.StaticConfig.IncludedNumberOfRankings); + var shouts = Records.Helper.BottomNResultForCategory(Records.Categories.Shout, Plugin.StaticConfig.IncludedNumberOfRankings); + var pings = Records.Helper.BottomNResultForCategory(Records.Categories.Ping, Plugin.StaticConfig.IncludedNumberOfRankings); + + + + if (Plugin.StaticConfig.RankedDeathLeaderboardEnabled && deaths.Count > 0) + { + leaderFields.Add(Tuple.Create("Deaths", Leaderboard.RankedCountResultToString(deaths))); + } + if (Plugin.StaticConfig.RankedSessionLeaderboardEnabled && sessions.Count > 0) + { + leaderFields.Add(Tuple.Create("Sessions", Leaderboard.RankedCountResultToString(sessions))); + } + if (Plugin.StaticConfig.RankedShoutLeaderboardEnabled && shouts.Count > 0) + { + leaderFields.Add(Tuple.Create("Shouts", Leaderboard.RankedCountResultToString(shouts))); + } + if (Plugin.StaticConfig.RankedPingLeaderboardEnabled && pings.Count > 0) + { + leaderFields.Add(Tuple.Create("Pings", Leaderboard.RankedCountResultToString(pings))); + } + if (leaderFields.Count > 0) + { + string discordContent = MessageTransformer.FormatLeaderboardHeader(Plugin.StaticConfig.LeaderboardBottomPlayersHeading, Plugin.StaticConfig.IncludedNumberOfRankings); + DiscordApi.SendMessageWithFields(discordContent, leaderFields); + } + else + { + Plugin.StaticLogger.LogInfo("Not sending a leaderboard because theirs either no leaders, or nothing allowed."); + } + } + } +} diff --git a/src/Leaderboard/Leaderboard.cs b/src/Leaderboard/Leaderboard.cs index d928538..f2ac408 100644 --- a/src/Leaderboard/Leaderboard.cs +++ b/src/Leaderboard/Leaderboard.cs @@ -1,4 +1,6 @@ -using System.Timers; +using System; +using System.Collections.Generic; +using System.Timers; namespace DiscordConnector { @@ -7,17 +9,37 @@ internal class Leaderboard private Leaderboards.Base overallHighest; private Leaderboards.Base overallLowest; private Leaderboards.Base topPlayers; + private Leaderboards.Base bottomPlayers; public Leaderboard() { overallHighest = new Leaderboards.OverallHighest(); overallLowest = new Leaderboards.OverallLowest(); topPlayers = new Leaderboards.TopPlayers(); + bottomPlayers = new Leaderboards.BottomPlayers(); } public Leaderboards.Base OverallHighest => overallHighest; public Leaderboards.Base OverallLowest => overallLowest; public Leaderboards.Base TopPlayers => topPlayers; + public Leaderboards.Base BottomPlayers => bottomPlayers; + + + + /// + /// Takes a sorted list and returns a string listing each member on a line prepended with 1, 2, 3, etc. + /// + /// A pre-sorted list of CountResults. + /// String ready to send to discord listing each player and their value. + public static string RankedCountResultToString(List rankings) + { + string res = ""; + for (int i = 0; i < rankings.Count; i++) + { + res += $"{i + 1}: {rankings[i].Name}: {rankings[i].Count}{Environment.NewLine}"; + } + return res; + } } } @@ -30,6 +52,7 @@ internal abstract class Base /// public void SendLeaderboardOnTimer(object sender, ElapsedEventArgs elapsedEventArgs) { + Plugin.StaticLogger.LogDebug($"Running the leaderboard send"); this.SendLeaderboard(); } diff --git a/src/Leaderboard/OverallHighest.cs b/src/Leaderboard/OverallHighest.cs index 3aeb75c..1d3e0d5 100644 --- a/src/Leaderboard/OverallHighest.cs +++ b/src/Leaderboard/OverallHighest.cs @@ -7,31 +7,32 @@ internal class OverallHighest : Base { public override void SendLeaderboard() { - var deathLeader = Plugin.StaticRecords.RetrieveHighest(RecordCategories.Death); - var joinLeader = Plugin.StaticRecords.RetrieveHighest(RecordCategories.Join); - var shoutLeader = Plugin.StaticRecords.RetrieveHighest(RecordCategories.Shout); - var pingLeader = Plugin.StaticRecords.RetrieveHighest(RecordCategories.Ping); + var deathLeader = Records.Helper.TopResultForCategory(Records.Categories.Death); + var joinLeader = Records.Helper.TopResultForCategory(Records.Categories.Join); + var shoutLeader = Records.Helper.TopResultForCategory(Records.Categories.Shout); + var pingLeader = Records.Helper.TopResultForCategory(Records.Categories.Ping); List> leaderFields = new List>(); - if (Plugin.StaticConfig.MostDeathLeaderboardEnabled && deathLeader.Item2 > 0) + if (Plugin.StaticConfig.MostDeathLeaderboardEnabled && deathLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Most Deaths", $"{deathLeader.Item1} ({deathLeader.Item2})")); + leaderFields.Add(Tuple.Create("Most Deaths", $"{deathLeader.Name} ({deathLeader.Count})")); } - if (Plugin.StaticConfig.MostSessionLeaderboardEnabled && joinLeader.Item2 > 0) + if (Plugin.StaticConfig.MostSessionLeaderboardEnabled && joinLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Most Sessions", $"{joinLeader.Item1} ({joinLeader.Item2})")); + leaderFields.Add(Tuple.Create("Most Sessions", $"{joinLeader.Name} ({joinLeader.Count})")); } - if (Plugin.StaticConfig.MostShoutLeaderboardEnabled && shoutLeader.Item2 > 0) + if (Plugin.StaticConfig.MostShoutLeaderboardEnabled && shoutLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Most Shouts", $"{shoutLeader.Item1} ({shoutLeader.Item2})")); + leaderFields.Add(Tuple.Create("Most Shouts", $"{shoutLeader.Name} ({shoutLeader.Count})")); } - if (Plugin.StaticConfig.MostPingLeaderboardEnabled && pingLeader.Item2 > 0) + if (Plugin.StaticConfig.MostPingLeaderboardEnabled && pingLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Most Pings", $"{pingLeader.Item1} ({pingLeader.Item2})")); + leaderFields.Add(Tuple.Create("Most Pings", $"{pingLeader.Name} ({pingLeader.Count})")); } if (leaderFields.Count > 0) { - DiscordApi.SendMessageWithFields("Current Highest stat leader board:", leaderFields); + string discordContent = MessageTransformer.FormatLeaderboardHeader(Plugin.StaticConfig.LeaderboardHighestHeading); + DiscordApi.SendMessageWithFields(discordContent, leaderFields); } else { diff --git a/src/Leaderboard/OverallLowest.cs b/src/Leaderboard/OverallLowest.cs index 6ad46e1..32daefc 100644 --- a/src/Leaderboard/OverallLowest.cs +++ b/src/Leaderboard/OverallLowest.cs @@ -7,31 +7,32 @@ internal class OverallLowest : Base { public override void SendLeaderboard() { - var deathLeader = Plugin.StaticRecords.RetrieveLowest(RecordCategories.Death); - var joinLeader = Plugin.StaticRecords.RetrieveLowest(RecordCategories.Join); - var shoutLeader = Plugin.StaticRecords.RetrieveLowest(RecordCategories.Shout); - var pingLeader = Plugin.StaticRecords.RetrieveLowest(RecordCategories.Ping); + var deathLeader = Records.Helper.BottomResultForCategory(Records.Categories.Death); + var joinLeader = Records.Helper.BottomResultForCategory(Records.Categories.Join); + var shoutLeader = Records.Helper.BottomResultForCategory(Records.Categories.Shout); + var pingLeader = Records.Helper.BottomResultForCategory(Records.Categories.Ping); List> leaderFields = new List>(); - if (Plugin.StaticConfig.LeastDeathLeaderboardEnabled && deathLeader.Item2 > 0) + if (Plugin.StaticConfig.LeastDeathLeaderboardEnabled && deathLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Least Deaths", $"{deathLeader.Item1} ({deathLeader.Item2})")); + leaderFields.Add(Tuple.Create("Least Deaths", $"{deathLeader.Name} ({deathLeader.Count})")); } - if (Plugin.StaticConfig.LeastSessionLeaderboardEnabled && joinLeader.Item2 > 0) + if (Plugin.StaticConfig.LeastSessionLeaderboardEnabled && joinLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Least Sessions", $"{joinLeader.Item1} ({joinLeader.Item2})")); + leaderFields.Add(Tuple.Create("Least Sessions", $"{joinLeader.Name} ({joinLeader.Count})")); } - if (Plugin.StaticConfig.LeastShoutLeaderboardEnabled && shoutLeader.Item2 > 0) + if (Plugin.StaticConfig.LeastShoutLeaderboardEnabled && shoutLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Least Shouts", $"{shoutLeader.Item1} ({shoutLeader.Item2})")); + leaderFields.Add(Tuple.Create("Least Shouts", $"{shoutLeader.Name} ({shoutLeader.Count})")); } - if (Plugin.StaticConfig.LeastPingLeaderboardEnabled && pingLeader.Item2 > 0) + if (Plugin.StaticConfig.LeastPingLeaderboardEnabled && pingLeader.Count > 0) { - leaderFields.Add(Tuple.Create("Least Pings", $"{pingLeader.Item1} ({pingLeader.Item2})")); + leaderFields.Add(Tuple.Create("Least Pings", $"{pingLeader.Name} ({pingLeader.Count})")); } if (leaderFields.Count > 0) { - DiscordApi.SendMessageWithFields("Current leaderboard least stats:", leaderFields); + string discordContent = MessageTransformer.FormatLeaderboardHeader(Plugin.StaticConfig.LeaderboardLowestHeading); + DiscordApi.SendMessageWithFields(discordContent, leaderFields); } else { diff --git a/src/Leaderboard/TopPlayers.cs b/src/Leaderboard/TopPlayers.cs index 31d2d1a..4ed89e7 100644 --- a/src/Leaderboard/TopPlayers.cs +++ b/src/Leaderboard/TopPlayers.cs @@ -7,59 +7,38 @@ internal class TopPlayers : Base { public override void SendLeaderboard() { - var deaths = Plugin.StaticRecords.RetrieveAll(RecordCategories.Death); - var sessions = Plugin.StaticRecords.RetrieveAll(RecordCategories.Join); - var shouts = Plugin.StaticRecords.RetrieveAll(RecordCategories.Shout); - var pings = Plugin.StaticRecords.RetrieveAll(RecordCategories.Ping); - List> leaderFields = new List>(); + + var deaths = Records.Helper.TopNResultForCategory(Records.Categories.Death, Plugin.StaticConfig.IncludedNumberOfRankings); + var sessions = Records.Helper.TopNResultForCategory(Records.Categories.Join, Plugin.StaticConfig.IncludedNumberOfRankings); + var shouts = Records.Helper.TopNResultForCategory(Records.Categories.Shout, Plugin.StaticConfig.IncludedNumberOfRankings); + var pings = Records.Helper.TopNResultForCategory(Records.Categories.Ping, Plugin.StaticConfig.IncludedNumberOfRankings); + if (Plugin.StaticConfig.RankedDeathLeaderboardEnabled && deaths.Count > 0) { - deaths.Sort(Plugin.StaticRecords.HighToLowSort); - leaderFields.Add(Tuple.Create("Top Deaths", TopPlayersFormater(deaths.ToArray()))); + leaderFields.Add(Tuple.Create("Deaths", Leaderboard.RankedCountResultToString(deaths))); } if (Plugin.StaticConfig.RankedSessionLeaderboardEnabled && sessions.Count > 0) { - sessions.Sort(Plugin.StaticRecords.HighToLowSort); - leaderFields.Add(Tuple.Create("Top Sessions", TopPlayersFormater(sessions.ToArray()))); + leaderFields.Add(Tuple.Create("Sessions", Leaderboard.RankedCountResultToString(sessions))); } if (Plugin.StaticConfig.RankedShoutLeaderboardEnabled && shouts.Count > 0) { - shouts.Sort(Plugin.StaticRecords.HighToLowSort); - leaderFields.Add(Tuple.Create("Top Shouts", TopPlayersFormater(shouts.ToArray()))); + leaderFields.Add(Tuple.Create("Shouts", Leaderboard.RankedCountResultToString(shouts))); } if (Plugin.StaticConfig.RankedPingLeaderboardEnabled && pings.Count > 0) { - pings.Sort(Plugin.StaticRecords.HighToLowSort); - leaderFields.Add(Tuple.Create("Top Pings", TopPlayersFormater(pings.ToArray()))); + leaderFields.Add(Tuple.Create("Pings", Leaderboard.RankedCountResultToString(pings))); } if (leaderFields.Count > 0) { - DiscordApi.SendMessageWithFields("Current top player leaderboard:", leaderFields); + string discordContent = MessageTransformer.FormatLeaderboardHeader(Plugin.StaticConfig.LeaderboardTopPlayerHeading, Plugin.StaticConfig.IncludedNumberOfRankings); + DiscordApi.SendMessageWithFields(discordContent, leaderFields); } else { Plugin.StaticLogger.LogInfo("Not sending a leaderboard because theirs either no leaders, or nothing allowed."); } } - - /// - /// Takes a sorted array and returns a string combining the top n results (n as defined in config). - /// - /// A pre-sorted array of (playername, value) Tuples. - /// String ready to send to discord listing each player and their value. - private string TopPlayersFormater(Tuple[] sortedTopPlayers) - { - string result = ""; - for (int i = 0; i < Plugin.StaticConfig.IncludedNumberOfRankings; i++) - { - if (i < sortedTopPlayers.Length) - { - Tuple player = sortedTopPlayers[i]; - result += $"{i + 1}: {player.Item1}: {player.Item2}{Environment.NewLine}"; - } - } - return result; - } } } diff --git a/src/MessageTransformer.cs b/src/MessageTransformer.cs index 84bfcfd..6d3aa86 100644 --- a/src/MessageTransformer.cs +++ b/src/MessageTransformer.cs @@ -22,6 +22,7 @@ internal static class MessageTransformer private const string EVENT_END_MSG = "%EVENT_END_MSG%"; private const string EVENT_MSG = "%EVENT_MSG%"; private const string EVENT_PLAYERS = "%PLAYERS%"; + private const string N = "%N%"; private static string ReplaceVariables(string rawMessage) { return rawMessage @@ -34,12 +35,12 @@ private static string ReplaceVariables(string rawMessage) .Replace(VAR_6, Plugin.StaticConfig.UserVariable6) .Replace(VAR_7, Plugin.StaticConfig.UserVariable7) .Replace(VAR_8, Plugin.StaticConfig.UserVariable8) - .Replace(VAR_9, Plugin.StaticConfig.UserVariable9); + .Replace(VAR_9, Plugin.StaticConfig.UserVariable9) + .Replace(PUBLIC_IP, Plugin.PublicIpAddress); } public static string FormatServerMessage(string rawMessage) { - return MessageTransformer.ReplaceVariables(rawMessage) - .Replace(PUBLIC_IP, Plugin.PublicIpAddress); + return MessageTransformer.ReplaceVariables(rawMessage); } public static string FormatPlayerMessage(string rawMessage, string playerName) @@ -95,5 +96,15 @@ public static string FormatEventEndMessage(string rawMessage, string eventStartM return MessageTransformer.FormatEventMessage(rawMessage, eventStartMsg, eventEndMsg, pos) .Replace(EVENT_MSG, eventEndMsg); } + public static string FormatLeaderboardHeader(string rawMessage) + { + return MessageTransformer.ReplaceVariables(rawMessage); + } + + public static string FormatLeaderboardHeader(string rawMessage, int n) + { + return MessageTransformer.ReplaceVariables(rawMessage) + .Replace(N, n.ToString()); + } } } diff --git a/src/Patches/ChatPatches.cs b/src/Patches/ChatPatches.cs index 4214da4..526c9e6 100644 --- a/src/Patches/ChatPatches.cs +++ b/src/Patches/ChatPatches.cs @@ -16,10 +16,16 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos Plugin.StaticLogger.LogInfo($"Ignored shout from user on muted list. User: {user} Shout: {text}. Index {Plugin.StaticConfig.MutedPlayers.IndexOf(user)}"); return; } + ulong peerSteamID = 0; + ZNetPeer peerInstance = ZNet.instance.GetPeerByPlayerName(user); + if (peerInstance != null) + { + peerSteamID = ((ZSteamSocket)peerInstance.m_socket).GetPeerID().m_SteamID; // Get the SteamID from peer. + } switch (type) { case Talker.Type.Ping: - if (Plugin.StaticConfig.AnnouncePlayerFirstPingEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Ping, user) == 0) + if (Plugin.StaticConfig.AnnouncePlayerFirstPingEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Ping, user) == 0) { DiscordApi.SendMessage( MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstPingMessage, user) @@ -27,7 +33,7 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos } if (Plugin.StaticConfig.StatsPingEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Ping, user, 1); + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Ping, user, peerSteamID, pos); } if (Plugin.StaticConfig.ChatPingEnabled) { @@ -56,7 +62,7 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos { if (!Plugin.IsHeadless()) { - if (Plugin.StaticConfig.AnnouncePlayerFirstJoinEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Join, user) == 0) + if (Plugin.StaticConfig.AnnouncePlayerFirstJoinEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Join, user) == 0) { DiscordApi.SendMessage( MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstJoinMessage, user) @@ -64,7 +70,7 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos } if (Plugin.StaticConfig.StatsJoinEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Join, user, 1); + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Join, user, peerSteamID, pos); } if (Plugin.StaticConfig.PlayerJoinMessageEnabled) { @@ -91,7 +97,7 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos } else { - if (Plugin.StaticConfig.AnnouncePlayerFirstShoutEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Shout, user) == 0) + if (Plugin.StaticConfig.AnnouncePlayerFirstShoutEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Shout, user) == 0) { DiscordApi.SendMessage( MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstShoutMessage, user, text) @@ -99,7 +105,7 @@ private static void Prefix(ref GameObject go, ref long senderID, ref Vector3 pos } if (Plugin.StaticConfig.StatsShoutEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Shout, user, 1); + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Shout, user, peerSteamID, pos); } if (Plugin.StaticConfig.ChatShoutEnabled) { diff --git a/src/Patches/ZNetPatches.cs b/src/Patches/ZNetPatches.cs index 940963c..6ff36e3 100644 --- a/src/Patches/ZNetPatches.cs +++ b/src/Patches/ZNetPatches.cs @@ -50,6 +50,7 @@ private static void Postfix(ZRpc rpc, ZDOID characterID) { return; } + ulong peerSteamID = ((ZSteamSocket)peer.m_socket).GetPeerID().m_SteamID; // Get the SteamID from peer. if (joinedPlayers.IndexOf(peer.m_uid) >= 0) { // Seems that player is dead if character ZDOID id is 0 @@ -61,50 +62,50 @@ private static void Postfix(ZRpc rpc, ZDOID characterID) } if (Plugin.StaticConfig.PlayerDeathMessageEnabled) { - if (Plugin.StaticConfig.AnnouncePlayerFirstDeathEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Death, peer.m_playerName) == 0) - { - string firstDeathMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstDeathMessage, peer.m_playerName); - if (Plugin.StaticConfig.PlayerDeathPosEnabled) - { - if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstDeathMessage.Contains("%POS%")) - { - DiscordApi.SendMessage(firstDeathMessage, peer.m_refPos); - } - else - { - DiscordApi.SendMessage(MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstDeathMessage, peer.m_playerName, peer.m_refPos)); - } - } - else - { - DiscordApi.SendMessage(firstDeathMessage); - } + if (Plugin.StaticConfig.AnnouncePlayerFirstDeathEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Death, peer.m_playerName) == 0) + { + string firstDeathMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstDeathMessage, peer.m_playerName); + if (Plugin.StaticConfig.PlayerDeathPosEnabled) + { + if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstDeathMessage.Contains("%POS%")) + { + DiscordApi.SendMessage(firstDeathMessage, peer.m_refPos); + } + else + { + DiscordApi.SendMessage(MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstDeathMessage, peer.m_playerName, peer.m_refPos)); + } + } + else + { + DiscordApi.SendMessage(firstDeathMessage); + } } - else - { - string message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.DeathMessage, peer.m_playerName); - if (Plugin.StaticConfig.PlayerDeathPosEnabled) - { - if (Plugin.StaticConfig.DiscordEmbedsEnabled || !message.Contains("%POS%")) - { - DiscordApi.SendMessage(message, peer.m_refPos); - } - else - { - message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.DeathMessage, peer.m_playerName, peer.m_refPos); - DiscordApi.SendMessage(message); - } - } - else - { - DiscordApi.SendMessage(message); + else + { + string message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.DeathMessage, peer.m_playerName); + if (Plugin.StaticConfig.PlayerDeathPosEnabled) + { + if (Plugin.StaticConfig.DiscordEmbedsEnabled || !message.Contains("%POS%")) + { + DiscordApi.SendMessage(message, peer.m_refPos); + } + else + { + message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.DeathMessage, peer.m_playerName, peer.m_refPos); + DiscordApi.SendMessage(message); + } + } + else + { + DiscordApi.SendMessage(message); } } - } - + } + if (Plugin.StaticConfig.StatsDeathEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Death, peer.m_playerName, 1); + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Death, peer.m_playerName, peerSteamID, peer.m_refPos); } } else @@ -114,26 +115,26 @@ private static void Postfix(ZRpc rpc, ZDOID characterID) Plugin.StaticLogger.LogDebug($"Added player {peer.m_uid} ({peer.m_playerName}) to joined player list."); if (Plugin.StaticConfig.PlayerJoinMessageEnabled) { - if (Plugin.StaticConfig.AnnouncePlayerFirstJoinEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Join, peer.m_playerName) == 0) - { - string firstJoinMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstJoinMessage, peer.m_playerName); - if (Plugin.StaticConfig.PlayerJoinPosEnabled) - { - if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstJoinMessage.Contains("%POS%")) - { - DiscordApi.SendMessage(firstJoinMessage, peer.m_refPos); - } - else - { - firstJoinMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.JoinMessage, peer.m_playerName, peer.m_refPos); - DiscordApi.SendMessage(firstJoinMessage); - } - } - else - { - DiscordApi.SendMessage( - MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstJoinMessage, peer.m_playerName)); - } + if (Plugin.StaticConfig.AnnouncePlayerFirstJoinEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Join, peer.m_playerName) == 0) + { + string firstJoinMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstJoinMessage, peer.m_playerName); + if (Plugin.StaticConfig.PlayerJoinPosEnabled) + { + if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstJoinMessage.Contains("%POS%")) + { + DiscordApi.SendMessage(firstJoinMessage, peer.m_refPos); + } + else + { + firstJoinMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.JoinMessage, peer.m_playerName, peer.m_refPos); + DiscordApi.SendMessage(firstJoinMessage); + } + } + else + { + DiscordApi.SendMessage( + MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstJoinMessage, peer.m_playerName)); + } } string message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.JoinMessage, peer.m_playerName); if (Plugin.StaticConfig.PlayerJoinPosEnabled) @@ -142,21 +143,21 @@ private static void Postfix(ZRpc rpc, ZDOID characterID) { DiscordApi.SendMessage(message, peer.m_refPos); } - else - { - message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.JoinMessage, peer.m_playerName, peer.m_refPos); - DiscordApi.SendMessage(message); - } + else + { + message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.JoinMessage, peer.m_playerName, peer.m_refPos); + DiscordApi.SendMessage(message); + } } else { DiscordApi.SendMessage(message); } - } - + } + if (Plugin.StaticConfig.StatsJoinEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Join, peer.m_playerName, 1); + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Join, peer.m_playerName, peerSteamID, peer.m_refPos); } } } @@ -172,25 +173,25 @@ private static void Prefix(ZRpc rpc) { if (Plugin.StaticConfig.PlayerLeaveMessageEnabled) { - if (Plugin.StaticConfig.AnnouncePlayerFirstLeaveEnabled && Plugin.StaticRecords.Retrieve(RecordCategories.Leave, peer.m_playerName) == 0) - { - string firstLeaveMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstLeaveMessage, peer.m_playerName); - if (Plugin.StaticConfig.PlayerLeavePosEnabled) - { - if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstLeaveMessage.Contains("%POS%")) - { - DiscordApi.SendMessage(firstLeaveMessage, peer.m_refPos); - } - else - { - firstLeaveMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstLeaveMessage, peer.m_playerName, peer.m_refPos); - DiscordApi.SendMessage(firstLeaveMessage); - } - } - else - { - DiscordApi.SendMessage(firstLeaveMessage); - } + if (Plugin.StaticConfig.AnnouncePlayerFirstLeaveEnabled && Plugin.StaticDatabase.CountOfRecordsByName(Records.Categories.Leave, peer.m_playerName) == 0) + { + string firstLeaveMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstLeaveMessage, peer.m_playerName); + if (Plugin.StaticConfig.PlayerLeavePosEnabled) + { + if (Plugin.StaticConfig.DiscordEmbedsEnabled || !firstLeaveMessage.Contains("%POS%")) + { + DiscordApi.SendMessage(firstLeaveMessage, peer.m_refPos); + } + else + { + firstLeaveMessage = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.PlayerFirstLeaveMessage, peer.m_playerName, peer.m_refPos); + DiscordApi.SendMessage(firstLeaveMessage); + } + } + else + { + DiscordApi.SendMessage(firstLeaveMessage); + } } string message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.LeaveMessage, peer.m_playerName); @@ -200,22 +201,23 @@ private static void Prefix(ZRpc rpc) { DiscordApi.SendMessage(message, peer.m_refPos); } - else - { - message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.LeaveMessage, peer.m_playerName, peer.m_refPos); - DiscordApi.SendMessage(message); - } - + else + { + message = MessageTransformer.FormatPlayerMessage(Plugin.StaticConfig.LeaveMessage, peer.m_playerName, peer.m_refPos); + DiscordApi.SendMessage(message); + } + } else { DiscordApi.SendMessage(message); } - } - + } + if (Plugin.StaticConfig.StatsLeaveEnabled) { - Plugin.StaticRecords.Store(RecordCategories.Leave, peer.m_playerName, 1); + ulong peerSteamID = ((ZSteamSocket)peer.m_socket).GetPeerID().m_SteamID; // Get the SteamID from peer. + Plugin.StaticDatabase.InsertSimpleStatRecord(Records.Categories.Leave, peer.m_playerName, peerSteamID, peer.m_refPos); } } } diff --git a/src/Plugin.cs b/src/Plugin.cs index 4c345e9..6d2dd4e 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -1,5 +1,6 @@ using BepInEx; using BepInEx.Logging; +using DiscordConnector.Records; using HarmonyLib; using UnityEngine; using UnityEngine.Rendering; @@ -11,7 +12,8 @@ public class Plugin : BaseUnityPlugin { internal static ManualLogSource StaticLogger; internal static PluginConfig StaticConfig; - internal static Records StaticRecords; + internal static RecordsOld StaticRecords; + internal static Database StaticDatabase; internal static Leaderboard StaticLeaderboards; internal static EventWatcher StaticEventWatcher; internal static string PublicIpAddress; @@ -21,8 +23,11 @@ public Plugin() { StaticLogger = Logger; StaticConfig = new PluginConfig(Config); - StaticRecords = new Records(Paths.GameRootPath); + StaticDatabase = new Records.Database(Paths.GameRootPath); StaticLeaderboards = new Leaderboard(); + + //! Remove in next major version + StaticRecords = new RecordsOld(Paths.GameRootPath); } private void Awake() @@ -49,8 +54,10 @@ private void Awake() leaderboardTimer.Elapsed += StaticLeaderboards.OverallHighest.SendLeaderboardOnTimer; leaderboardTimer.Elapsed += StaticLeaderboards.OverallLowest.SendLeaderboardOnTimer; leaderboardTimer.Elapsed += StaticLeaderboards.TopPlayers.SendLeaderboardOnTimer; + leaderboardTimer.Elapsed += StaticLeaderboards.BottomPlayers.SendLeaderboardOnTimer; // Interval is learned from config file in minutes leaderboardTimer.Interval = 60 * 1000 * StaticConfig.StatsAnnouncementPeriod; + Plugin.StaticLogger.LogDebug($"Enabling leaderboard timers with interval {leaderboardTimer.Interval}ms"); leaderboardTimer.Start(); } @@ -62,6 +69,7 @@ private void Awake() private void OnDestroy() { _harmony.UnpatchSelf(); + StaticDatabase.Dispose(); } /// diff --git a/src/PluginInfo.cs b/src/PluginInfo.cs index c5e709c..740fd6f 100644 --- a/src/PluginInfo.cs +++ b/src/PluginInfo.cs @@ -17,7 +17,7 @@ internal static class PluginInfo { public const string PLUGIN_ID = "games.nwest.valheim.discordconnector"; public const string PLUGIN_NAME = "Valheim Discord Connector"; - public const string PLUGIN_VERSION = "1.4.4"; + public const string PLUGIN_VERSION = "1.5.0"; public const string PLUGIN_REPO_SHORT = "github: nwesterhausen/valheim-discordconnector"; public const string PLUGIN_AUTHOR = "Nicholas Westerhausen"; } diff --git a/src/Records.cs b/src/Records.cs index d139d4b..b80dd29 100644 --- a/src/Records.cs +++ b/src/Records.cs @@ -1,31 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; using Newtonsoft.Json; namespace DiscordConnector { - /// - /// These are categories used when keeping track of values in the record system. It is a simple system - /// that currently only supports storing string:integer pairings underneath one of these categories. - /// - public static class RecordCategories - { - public const string Death = "death"; - public const string Join = "join"; - public const string Leave = "leave"; - public const string Ping = "ping"; - public const string Shout = "shout"; - - public static string[] All = new string[] { - Death, - Join, - Leave, - Ping, - Shout - }; - } /// /// The individual key:value pairings used in the record system. It supports only string:int pairings. /// @@ -42,11 +21,12 @@ internal class Record public string Category { get; set; } public List Values { get; set; } } - class Records + class RecordsOld { private static string DEFAULT_FILENAME = "records.json"; - private string storepath; - private bool saveEnabled; + private const string MIGRATED_FILENAME = "records.json.migrated"; + private string storepath, movepath; + // private bool saveEnabled; private List recordCache; /// @@ -55,245 +35,29 @@ class Records /// /// The directory to store the json file of records in. /// The name of the file to use for storage. - public Records(string basePath, string fileName = null) + public RecordsOld(string basePath, string fileName = null) { if (fileName == null) { fileName = DEFAULT_FILENAME; } storepath = Path.Combine(basePath, fileName); - saveEnabled = true; - PopulateCache(); - if (!Plugin.StaticConfig.CollectStatsEnabled) - { - Plugin.StaticLogger.LogInfo("Saving stats is disabled, nothing will be recorded."); - } - } - - /// - /// Add to a record for under in the records database. - /// This will not save the record if the is not one defined in RecordCategories. - /// - /// RecordCategories category to store the value under - /// The player's name. - /// How much to increase current stored value by. - public void Store(string key, string playername, int value) - { - if (Plugin.StaticConfig.CollectStatsEnabled) - { - if (Array.IndexOf(RecordCategories.All, key) >= 0) - { - foreach (Record r in recordCache) - { - if (r.Category.Equals(key)) - { - bool stored = false; - foreach (RecordValue v in r.Values) - { - if (v.Key.Equals(playername)) - { - v.Value += value; - stored = true; - } - } - if (!stored) - { - r.Values.Add(new RecordValue() - { - Key = playername, - Value = value - }); - } - } - } - // After adding new data, flush data to disk. - FlushCache().ContinueWith( - t => Plugin.StaticLogger.LogWarning(t.Exception), - TaskContinuationOptions.OnlyOnFaulted); - } - else - { - Plugin.StaticLogger.LogWarning($"Unable to store record of {key} for player {playername} - not considered a valid category."); - } - } - } - /// - /// Get the value stored under at . - /// - /// The RecordCategories category the value is stored under - /// The name of the player - /// This will return 0 if there is no record found for that player. It will return -1 if the category is invalid. - public int Retrieve(string key, string playername) - { - if (!Plugin.StaticConfig.CollectStatsEnabled) - { - return -1; - } - if (Array.IndexOf(RecordCategories.All, key) >= 0) - { - foreach (Record r in recordCache) - { - if (r.Category.Equals(key)) - { - foreach (RecordValue v in r.Values) - { - if (v.Key.Equals(playername)) - { - return v.Value; - } - } - } - } - } - Plugin.StaticLogger.LogWarning($"No stored record for player {playername} under {key}, returning default of 0."); - return 0; - } - - /// - /// Retrieve all stored values under . - /// - /// RecordCategories category to retrieve stored values from - /// A list of (playername, value) tuples. The list will have length 0 if there are no stored records. - public List> RetrieveAll(string key) - { - List> results = new List>(); - if (!Plugin.StaticConfig.CollectStatsEnabled) - { - return results; - } - - if (Array.IndexOf(RecordCategories.All, key) >= 0) - { - foreach (Record r in recordCache) - { - if (r.Category.Equals(key)) - { - foreach (RecordValue v in r.Values) - { - results.Add(Tuple.Create( - v.Key, v.Value - )); - } - } - } - } - - return results; - } - - /// - /// Retrieve the highest stored value under . - /// - /// RecordCategories category to retrieve stored values from - /// A single (playername, value) tuple. - public Tuple RetrieveHighest(string key) - { - if (!Plugin.StaticConfig.CollectStatsEnabled) - { - return Tuple.Create("not allowed", -1); - } - - if (Array.IndexOf(RecordCategories.All, key) >= 0) - { - string player = "no result"; - int records = -1; - foreach (Record r in recordCache) - { - if (r.Category.Equals(key)) - { - foreach (RecordValue v in r.Values) - { - if (v.Value > records) - { - player = v.Key; - records = v.Value; - } - } - } - } - return Tuple.Create(player, records); - } - else - { - return Tuple.Create($"not recording for {key}", -1); - } - } - - /// - /// Retrieve the lowest stored value under . - /// - /// RecordCategories category to retrieve stored values from - /// A single (playername, value) tuple. - public Tuple RetrieveLowest(string key) - { - if (!Plugin.StaticConfig.CollectStatsEnabled) - { - return Tuple.Create("not allowed", -1); - } + movepath = Path.Combine(basePath, MIGRATED_FILENAME); - if (Array.IndexOf(RecordCategories.All, key) >= 0) - { - string player = "no result"; - int records = int.MaxValue; - foreach (Record r in recordCache) - { - if (r.Category.Equals(key)) - { - foreach (RecordValue v in r.Values) - { - if (v.Value < records) - { - player = v.Key; - records = v.Value; - } - } - if (r.Values.Count == 0) - { - records = -1; - } - } - } - return Tuple.Create(player, records); - } - else - { - return Tuple.Create($"not recording for {key}", -1); - } + MigrateRecords(); } - /// - /// (Asynchronous) Writes the in-memory cache of records to disk. - /// - private async Task FlushCache() + private void MigrateRecords() { - if (!saveEnabled) - { - Plugin.StaticLogger.LogDebug("Saving records is disabled due to an error at load time."); - return; - } - if (Plugin.StaticConfig.CollectStatsEnabled) + //! check if storePath exists.. + if (System.IO.File.Exists(storepath)) { - string jsonString = JsonConvert.SerializeObject(recordCache); - - using (var stream = new StreamWriter(@storepath, false)) - { - await stream.WriteAsync(jsonString); - } - - Plugin.StaticLogger.LogDebug($"Flushed cached stats to {storepath}"); - } - } + Plugin.StaticLogger.LogInfo("Migrating from discovered Records.json to LiteDB"); + //! read all records in from storePath if they exist - /// - /// Builds the in-memory cache by reading from disk. - /// - private void PopulateCache() - { - if (File.Exists(storepath)) - { - string jsonString = File.ReadAllText(@storepath); try { + string jsonString = File.ReadAllText(@storepath); recordCache = JsonConvert.DeserializeObject>(jsonString); Plugin.StaticLogger.LogInfo($"Read existing stats from disk {storepath}"); } @@ -301,38 +65,40 @@ private void PopulateCache() { Plugin.StaticLogger.LogWarning($"No content found when reading {storepath} to read saved records. We will start with default values for all records."); Plugin.StaticLogger.LogDebug("File contained null and threw ArgumentNullException"); - InitializeEmptyCache(); + return; } catch (JsonException) { Plugin.StaticLogger.LogError($"Unable to parse the contents of {storepath} as JSON."); Plugin.StaticLogger.LogError("No records will be recorded to disk until existing file is moved, renamed, or deleted."); - saveEnabled = false; + return; + } + + //! store records into LiteDB + foreach (Record r in recordCache) + { + int count = 0; + foreach (RecordValue v in r.Values) + { + for (int i = 0; i < v.Value; i++) + { + Plugin.StaticDatabase.InsertSimpleStatRecord(r.Category, v.Key, 1); + count++; + } + + } + Plugin.StaticLogger.LogInfo($"Migrated {count} {r.Category} records"); + } + + //! move storePath to a new path with MIGRATED_FILENAME + System.IO.File.Move(storepath, movepath); + Plugin.StaticLogger.LogInfo($"Moved existing records.json to {MIGRATED_FILENAME}"); } else { - Plugin.StaticLogger.LogInfo($"Unable to find existing stats data at {storepath}. Creating new {DEFAULT_FILENAME}"); - InitializeEmptyCache(); - } - } - - private void InitializeEmptyCache() - { - recordCache = new List(); - foreach (string category in RecordCategories.All) - { - recordCache.Add(new Record - { - Category = category, - Values = new List() - }); + Plugin.StaticLogger.LogDebug("No records.json found, not migrating."); } - FlushCache().ContinueWith( - t => Plugin.StaticLogger.LogWarning(t.Exception), - TaskContinuationOptions.OnlyOnFaulted); } - - public Comparison> HighToLowSort = (x, y) => y.Item2.CompareTo(x.Item2); } } diff --git a/src/Records/Classes/CountResult.cs b/src/Records/Classes/CountResult.cs new file mode 100644 index 0000000..eb93369 --- /dev/null +++ b/src/Records/Classes/CountResult.cs @@ -0,0 +1,29 @@ + +using LiteDB; + +namespace DiscordConnector.Records +{ + public class CountResult + { + public string Name { get; } + public int Count { get; } + + [BsonCtor] + public CountResult(string name, int count) + { + + Name = name; + Count = count; + } + + public static int CompareByCount(CountResult cr1, CountResult cr2) + { + return cr1.Count.CompareTo(cr2.Count); + } + + public static int CompareByName(CountResult cr1, CountResult cr2) + { + return cr1.Name.CompareTo(cr2.Name); + } + } +} diff --git a/src/Records/Classes/SimpleStat.cs b/src/Records/Classes/SimpleStat.cs new file mode 100644 index 0000000..5a081a1 --- /dev/null +++ b/src/Records/Classes/SimpleStat.cs @@ -0,0 +1,67 @@ + +using LiteDB; + +namespace DiscordConnector.Records +{ + public class Position + { + public float x { get; } + public float y { get; } + public float z { get; } + + public Position() + { + x = 0; + y = 0; + z = 0; + } + public Position(float _x, float _y, float _z) + { + x = _x; + y = _y; + z = _z; + } + + public override string ToString() + { + return $"({x},{y},{z})"; + } + } + public class SimpleStat + { + public ObjectId StatId { get; } + public string Name { get; } + public System.DateTime Date { get; } + public ulong SteamId { get; } + public Position Pos { get; } + + public SimpleStat(string name, ulong steamId) + { + StatId = ObjectId.NewObjectId(); + Name = name; + SteamId = steamId; + Date = System.DateTime.Now; + Pos = new Position(); + } + + public SimpleStat(string name, ulong steamId, float x, float y, float z) + { + StatId = ObjectId.NewObjectId(); + Name = name; + SteamId = steamId; + Date = System.DateTime.Now; + Pos = new Position(x, y, z); + } + + [BsonCtor] + public SimpleStat(ObjectId _id, string name, System.DateTime date, ulong steamId, Position pos) + { + StatId = _id; + Name = name; + Date = date; + SteamId = steamId; + Pos = pos; + } + } + +} diff --git a/src/Records/Database.cs b/src/Records/Database.cs new file mode 100644 index 0000000..40a7e94 --- /dev/null +++ b/src/Records/Database.cs @@ -0,0 +1,227 @@ + +using System.Collections.Generic; +using LiteDB; +using UnityEngine; + +namespace DiscordConnector.Records +{ + internal class Database + { + private const string DB_NAME = "records.db"; + private static string DbPath; + private LiteDatabase db; + private ILiteCollection DeathCollection; + private ILiteCollection JoinCollection; + private ILiteCollection LeaveCollection; + private ILiteCollection ShoutCollection; + private ILiteCollection PingCollection; + + public Database(string rootStorePath) + { + DbPath = System.IO.Path.Combine(rootStorePath, DB_NAME); + Initialize(); + } + + public void Initialize() + { + db = new LiteDatabase(DbPath); + Plugin.StaticLogger.LogDebug($"LiteDB Connection Established to {DbPath}"); + DeathCollection = db.GetCollection("deaths"); + JoinCollection = db.GetCollection("joins"); + LeaveCollection = db.GetCollection("leaves"); + ShoutCollection = db.GetCollection("shouts"); + PingCollection = db.GetCollection("pings"); + } + + public void Dispose() + { + Plugin.StaticLogger.LogDebug("Closing LiteDB connection"); + db.Dispose(); + } + + private void InsertSimpleStatRecord(ILiteCollection collection, string playerName, ulong steamId, Vector3 pos) + { + var newRecord = new SimpleStat( + playerName, + steamId, + pos.x, pos.y, pos.z + ); + collection.Insert(newRecord); + + collection.EnsureIndex(x => x.Name); + collection.EnsureIndex(x => x.SteamId); + } + private void InsertSimpleStatRecord(ILiteCollection collection, string playerName, ulong steamId) + { + InsertSimpleStatRecord(collection, playerName, steamId, Vector3.zero); + } + + private int CountOfRecordsByName(ILiteCollection collection, string playerName) + { + return DeathCollection.Query() + .Where(x => x.Name.Equals(playerName)) + .Count(); + } + + private int CountOfRecordsBySteamId(ILiteCollection collection, ulong steamId) + { + return DeathCollection.Query() + .Where(x => x.SteamId == steamId) + .Count(); + } + + private List CountAllRecordsGroupBySteamId(ILiteCollection collection) + { + return ConvertBsonDocumentCountToDotNet( + collection.Query() + .GroupBy("SteamId") + .Select("{Name: @Key, Count: COUNT(*)}") + .ToList() + ); + } + + private List RetrieveAllRecordsGroupByName(ILiteCollection collection) + { + return ConvertBsonDocumentCountToDotNet( + collection.Query() + .GroupBy("Name") + .Select("{Name: @Key, Count: COUNT(*)}") + .ToList() + ); + } + + private List ConvertBsonDocumentCountToDotNet(List bsonDocuments) + { + List results = new List(); + foreach (BsonDocument doc in bsonDocuments) + { + if (doc.ContainsKey("Name") && doc.ContainsKey("Count")) + { + results.Add(new CountResult( + doc["Name"].AsString, + doc["Count"].AsInt32 + )); + } + } + return results; + } + + public List RetrieveAllRecordsGroupByName(string key) + { + switch (key) + { + case Categories.Death: + return RetrieveAllRecordsGroupByName(DeathCollection); + case Categories.Join: + return RetrieveAllRecordsGroupByName(JoinCollection); + case Categories.Leave: + return RetrieveAllRecordsGroupByName(LeaveCollection); + case Categories.Ping: + return RetrieveAllRecordsGroupByName(PingCollection); + case Categories.Shout: + return RetrieveAllRecordsGroupByName(ShoutCollection); + default: + Plugin.StaticLogger.LogDebug($"RetrieveAllRecordsGroupByName, invalid key '{key}'"); + return new List(); + } + } + public List CountAllRecordsGroupBySteamId(string key) + { + switch (key) + { + case Categories.Death: + return CountAllRecordsGroupBySteamId(DeathCollection); + case Categories.Join: + return CountAllRecordsGroupBySteamId(JoinCollection); + case Categories.Leave: + return CountAllRecordsGroupBySteamId(LeaveCollection); + case Categories.Ping: + return CountAllRecordsGroupBySteamId(PingCollection); + case Categories.Shout: + return CountAllRecordsGroupBySteamId(ShoutCollection); + default: + Plugin.StaticLogger.LogDebug($"CountAllRecordsGroupBySteamId, invalid key '{key}'"); + return new List(); + } + } + + public int CountOfRecordsByName(string key, string playerName) + { + if (!Plugin.StaticConfig.CollectStatsEnabled) + { + return -1; + } + switch (key) + { + case Categories.Death: + return CountOfRecordsByName(DeathCollection, playerName); + case Categories.Join: + return CountOfRecordsByName(JoinCollection, playerName); + case Categories.Leave: + return CountOfRecordsByName(LeaveCollection, playerName); + case Categories.Ping: + return CountOfRecordsByName(PingCollection, playerName); + case Categories.Shout: + return CountOfRecordsByName(ShoutCollection, playerName); + default: + Plugin.StaticLogger.LogDebug($"CountOfRecordsBySteamId, invalid key '{key}'"); + return -2; + } + } + + public int CountOfRecordsBySteamId(string key, ulong steamId) + { + if (!Plugin.StaticConfig.CollectStatsEnabled) + { + return -1; + } + switch (key) + { + case Categories.Death: + return CountOfRecordsBySteamId(DeathCollection, steamId); + case Categories.Join: + return CountOfRecordsBySteamId(JoinCollection, steamId); + case Categories.Leave: + return CountOfRecordsBySteamId(LeaveCollection, steamId); + case Categories.Ping: + return CountOfRecordsBySteamId(PingCollection, steamId); + case Categories.Shout: + return CountOfRecordsBySteamId(ShoutCollection, steamId); + default: + Plugin.StaticLogger.LogDebug($"CountOfRecordsBySteamId, invalid key '{key}'"); + return -2; + } + } + + public void InsertSimpleStatRecord(string key, string playerName, ulong steamId, Vector3 pos) + { + + switch (key) + { + case Categories.Death: + InsertSimpleStatRecord(DeathCollection, playerName, steamId, pos); + break; + case Categories.Join: + InsertSimpleStatRecord(JoinCollection, playerName, steamId, pos); + break; + case Categories.Leave: + InsertSimpleStatRecord(LeaveCollection, playerName, steamId, pos); + break; + case Categories.Ping: + InsertSimpleStatRecord(PingCollection, playerName, steamId, pos); + break; + case Categories.Shout: + InsertSimpleStatRecord(ShoutCollection, playerName, steamId, pos); + break; + default: + Plugin.StaticLogger.LogDebug($"InsertSimpleStatRecord, invalid key '{key}'"); + break; + } + } + public void InsertSimpleStatRecord(string key, string playerName, ulong steamId) + { + InsertSimpleStatRecord(key, playerName, steamId, Vector3.zero); + } + + } +} diff --git a/src/Records/RecordsHelper.cs b/src/Records/RecordsHelper.cs new file mode 100644 index 0000000..1761ffa --- /dev/null +++ b/src/Records/RecordsHelper.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace DiscordConnector.Records +{ + /// + /// These are categories used when keeping track of values in the record system. It is a simple system + /// that currently only supports storing string:integer pairings underneath one of these categories. + /// + public static class Categories + { + public const string Death = "death"; + public const string Join = "join"; + public const string Leave = "leave"; + public const string Ping = "ping"; + public const string Shout = "shout"; + + public readonly static string[] All = new string[] { + Death, + Join, + Leave, + Ping, + Shout + }; + } + + public static class Helper + { + public static List TopNResultForCategory(string key, int n) + { + List queryResults = Plugin.StaticDatabase.RetrieveAllRecordsGroupByName(key); + Plugin.StaticLogger.LogDebug($"TopNResultForCategory {key} n={n}, results={queryResults.Count}"); + if (queryResults.Count == 0) + { + return queryResults; + } + + queryResults.Sort(CountResult.CompareByCount); // sorts lowest to highest + queryResults.Reverse(); // Now high --> low + + if (queryResults.Count <= n) + { + return queryResults; + } + + return queryResults.GetRange(0, n); + } + + public static CountResult TopResultForCategory(string key) + { + var results = Helper.TopNResultForCategory(key, 1); + if (results.Count == 0) + { + return new CountResult("", 0); + } + return results[0]; + } + + public static List BottomNResultForCategory(string key, int n) + { + + List queryResults = Plugin.StaticDatabase.RetrieveAllRecordsGroupByName(key); + Plugin.StaticLogger.LogDebug($"BottomNResultForCategory {key} n={n}, results={queryResults.Count}"); + if (queryResults.Count == 0) + { + return queryResults; + } + + queryResults.Sort(CountResult.CompareByCount); // sorts lowest to highest + + if (queryResults.Count <= n) + { + return queryResults; + } + + return queryResults.GetRange(0, n); + } + + public static CountResult BottomResultForCategory(string key) + { + var results = Helper.BottomNResultForCategory(key, 1); + if (results.Count == 0) + { + return new CountResult("", 0); + } + return results[0]; + } + } +}