From 0c77874b6e23cf7a5096ba184b9f06a30c26d0f8 Mon Sep 17 00:00:00 2001 From: Nick Westerhausen <2317381+nwesterhausen@users.noreply.github.com> Date: Thu, 30 May 2024 09:23:30 -0400 Subject: [PATCH 1/2] docs: update docs --- docs/changelog.md | 7 +++++++ docs/config/messages.events.md | 9 +++++++++ docs/config/messages.leaderboards.md | 9 ++++++--- docs/config/messages.player.md | 9 +++++++++ docs/config/messages.playerfirsts.md | 9 +++++++++ docs/config/messages.server.md | 11 +++++++---- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index f960b0f..247c2c0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,13 @@ A full changelog of changes, dating all the way back to the first release. +## Version upcoming + +Features + +- Server New Day event and message +- `%DAY_NUMBER%` variable replacement + ## Version 2.2.0 Features diff --git a/docs/config/messages.events.md b/docs/config/messages.events.md index ebdfa84..bb2af14 100644 --- a/docs/config/messages.events.md +++ b/docs/config/messages.events.md @@ -10,6 +10,15 @@ In the event messages, anywhere in the message you can use the string vars `%EVE | `%EVENT_MSG%` | The appropriate start/end message for the event | Event start/stop message | ::: +::: tip Available Dynamic Variables + +| Variable | Replaced with.. | Can be used in.. | +| ------------------- | ----------------------------------------------------------------------------------------- | ------------------- | +| `%VAR1%` - `VAR10%` | Custom variable value (defined in [Custom Variables](/config/variables.html) config file) | Any messages | +| `%PUBLICIP%` | Server's public IP (according to the server) | Any server messages | +| `%DAY_NUMBER%` | Current day number on server | Any messages | +::: + :::details Random Messages All of the message options support having multiple messages defined in a semicolon (`;`) separated list. If you have multiple messages defined for these settings, one gets chosen at random when DiscordConnector decides to send the corresponding message. diff --git a/docs/config/messages.leaderboards.md b/docs/config/messages.leaderboards.md index 040d14c..aad7163 100644 --- a/docs/config/messages.leaderboards.md +++ b/docs/config/messages.leaderboards.md @@ -1,9 +1,12 @@ # Messages.LeaderBoards -::: details Available Custom Variables -These are defined in the [Custom Variables](/config/variables.html) config file. +::: tip Available Dynamic Variables -`%VAR1%`, `%VAR2%`, `%VAR3%`, `%VAR4%`, `%VAR5%`, `%VAR6%`, `%VAR7%`, `%VAR8%`, `%VAR9%`, `%VAR10%` +| Variable | Replaced with.. | Can be used in.. | +| ------------------- | ----------------------------------------------------------------------------------------- | ------------------- | +| `%VAR1%` - `VAR10%` | Custom variable value (defined in [Custom Variables](/config/variables.html) config file) | Any messages | +| `%PUBLICIP%` | Server's public IP (according to the server) | Any server messages | +| `%DAY_NUMBER%` | Current day number on server | Any messages | ::: ## Leader Board Heading for Top N Players diff --git a/docs/config/messages.player.md b/docs/config/messages.player.md index 24acaad..5f2e6fd 100644 --- a/docs/config/messages.player.md +++ b/docs/config/messages.player.md @@ -11,6 +11,15 @@ | `%POS%` | Player's coordinate position | Player join/leave/shout/ping/death messages. | ::: +::: tip Available Dynamic Variables + +| Variable | Replaced with.. | Can be used in.. | +| ------------------- | ----------------------------------------------------------------------------------------- | ------------------- | +| `%VAR1%` - `VAR10%` | Custom variable value (defined in [Custom Variables](/config/variables.html) config file) | Any messages | +| `%PUBLICIP%` | Server's public IP (according to the server) | Any server messages | +| `%DAY_NUMBER%` | Current day number on server | Any messages | +::: + :::details Random Messages All of the message options support having multiple messages defined in a semicolon (`;`) separated list. If you have multiple messages defined for these settings, one gets chosen at random when DiscordConnector decides to send the corresponding message. diff --git a/docs/config/messages.playerfirsts.md b/docs/config/messages.playerfirsts.md index e219677..7902a11 100644 --- a/docs/config/messages.playerfirsts.md +++ b/docs/config/messages.playerfirsts.md @@ -10,6 +10,15 @@ | `%POS%` | Player's coordinate position | Player join/leave/shout/ping/death messages. | ::: +::: tip Available Dynamic Variables + +| Variable | Replaced with.. | Can be used in.. | +| ------------------- | ----------------------------------------------------------------------------------------- | ------------------- | +| `%VAR1%` - `VAR10%` | Custom variable value (defined in [Custom Variables](/config/variables.html) config file) | Any messages | +| `%PUBLICIP%` | Server's public IP (according to the server) | Any server messages | +| `%DAY_NUMBER%` | Current day number on server | Any messages | +::: + :::details Random Messages All of the message options support having multiple messages defined in a semicolon (`;`) separated list. If you have multiple messages defined for these settings, one gets chosen at random when DiscordConnector decides to send the corresponding message. diff --git a/docs/config/messages.server.md b/docs/config/messages.server.md index 47cb0c5..cf1108a 100644 --- a/docs/config/messages.server.md +++ b/docs/config/messages.server.md @@ -1,9 +1,12 @@ # Messages.Server -::: tip Available Predefined Variables (Server) -| Variable | Replaced with.. | Can be used in.. | -| ------------ | ---------------------------------------------------------------------- | ------------------- | -| `%PUBLICIP%` | Server's public IP (obtained from [ifconfig.me](https://ifconfig.me/)) | Any server messages | +::: tip Available Dynamic Variables + +| Variable | Replaced with.. | Can be used in.. | +| ------------------- | ----------------------------------------------------------------------------------------- | ------------------- | +| `%VAR1%` - `VAR10%` | Custom variable value (defined in [Custom Variables](/config/variables.html) config file) | Any messages | +| `%PUBLICIP%` | Server's public IP (according to the server) | Any server messages | +| `%DAY_NUMBER%` | Current day number on server | Any messages | ::: ## Server Launch Message From 8e314cd0ffea1738410ba63dcb112c673a50c750 Mon Sep 17 00:00:00 2001 From: Nick Westerhausen <2317381+nwesterhausen@users.noreply.github.com> Date: Thu, 30 May 2024 10:05:42 -0400 Subject: [PATCH 2/2] docs: add doc comments --- docs/about.md | 1 + src/DiscordApi.cs | 64 +++++++++++++++++++++++++++++++++++++---- src/Records/Database.cs | 64 +++++++++++++++++++++++------------------ src/Utility.cs | 4 +-- 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/docs/about.md b/docs/about.md index 84ec477..a694ce0 100644 --- a/docs/about.md +++ b/docs/about.md @@ -18,6 +18,7 @@ A plugin to connect your Valheim server to Discord. - Server startup (server starting, loading the world) - Server started (world loaded, ready to join) - Server shutting down (server stopping) +- New day starts on server - Player join - Player leave - Player shouting diff --git a/src/DiscordApi.cs b/src/DiscordApi.cs index a296d02..e41034d 100644 --- a/src/DiscordApi.cs +++ b/src/DiscordApi.cs @@ -19,9 +19,9 @@ public static void SendMessage(Webhook.Event ev, string message, UnityEngine.Vec { if (Plugin.StaticConfig.DiscordEmbedsEnabled) { - SendMessageWithFields(ev, message, new List> { + SendMessageWithFields(ev, message, [ Tuple.Create("Coordinates",MessageTransformer.FormatVector3AsPos(pos)) - }); + ]); } else { @@ -36,7 +36,7 @@ public static void SendMessage(Webhook.Event ev, string message, UnityEngine.Vec public static void SendMessage(Webhook.Event ev, string message) { // A simple string message - var payload = new DiscordSimpleWebhook + DiscordSimpleWebhook payload = new() { content = message }; @@ -73,7 +73,7 @@ public static void SendMessageWithFields(Webhook.Event ev, string content = null if (fields != null) { // Convert the fields into JSON Strings - List fieldStrings = new List(); + List fieldStrings = []; foreach (Tuple t in fields) { try @@ -92,7 +92,7 @@ public static void SendMessageWithFields(Webhook.Event ev, string content = null if (fieldStrings.Count > 0) { - // Put the field JSON strings into our payload object + // Put the field JSON strings into our payload object // Fields go under embed as array payloadString += "\"embeds\":[{\"fields\":["; payloadString += string.Join(",", fieldStrings.ToArray()); @@ -113,7 +113,7 @@ public static void SendMessageWithFields(Webhook.Event ev, string content = null payloadString += $"\"content\":\"{content}\""; } - // Finish the payload JSON + // Finish the payload JSON payloadString += "}"; // Use our pre-existing method to send serialized JSON to discord @@ -246,25 +246,77 @@ private static void SendSerializedJson(string serializedJson) } } +/// +/// Simple webhook object is used for messages that contain only a simple string. +/// internal class DiscordSimpleWebhook { + /// + /// The message content to send to Discord. This is considered simple because it is not a series of embeds/fields. + /// + /// This works best for simple messages, and is used for most messages sent by the plugin. + /// + /// E.g. "Player joined the game", "Player left the game", etc. + /// public string content { get; set; } } + +/// +/// Complex webhook object is used for messages that contain more than just a simple string. +/// internal class DiscordComplexWebhook { + /// + /// Message content to send to Discord. This is considered complex because it contains multiple embeds/fields. + /// + /// This is used for POS embeds, and the leaderboards. + /// public DiscordEmbed embeds { get; set; } } + +/// +/// A complex Discord message, containing more than just a simple string. +/// +/// See https://discord.com/developers/docs/resources/channel#embed-object +/// internal class DiscordEmbed { #nullable enable + /// + /// The title of the message. + /// public string? title { get; set; } + /// + /// The description of the message. + /// public string? description { get; set; } + /// + /// A list of fields to include in the message. + /// + /// For leaderboards, each leaderboard that is included is a field. + /// public List? fields { get; set; } #nullable restore } + +/// +/// A field for a Discord message, which allows for fancy formatting. +/// +/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure +/// internal class DiscordField { + /// + /// Name of the field. + /// + /// These are just titled embedded values, where the name is the title. The value is a content string. + /// public string name { get; set; } + /// + /// The string content of the field. + /// + /// For example, the leaderboards are a list with `\n` as a separator, so they appear as an ordered list in Discord. + /// public string value { get; set; } } diff --git a/src/Records/Database.cs b/src/Records/Database.cs index e7a6647..e3647ee 100644 --- a/src/Records/Database.cs +++ b/src/Records/Database.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; using System.Threading.Tasks; using DiscordConnector.LeaderBoards; using LiteDB; @@ -25,14 +24,20 @@ internal class Database private ILiteCollection PlayerToNameCollection; /// - /// Set's up the database using the compiled string `"${PluginInfo.PLUGIN_ID}-records.db"`, which in this - /// case is probably `games.nwest.valheim.discordconnector-records.db`. This method needs to know where to - /// store the database, since that is something that is only known at runtime. + /// Sets up the database path and initializes the database connection. + /// + /// This will set up the database path in a default location: `BepInEx/Config/{PluginInfo.PLUGIN_ID}/records.db` /// /// Directory to save the LiteDB database in. public Database(string rootStorePath) { DbPath = System.IO.Path.Combine(BepInEx.Paths.ConfigPath, PluginInfo.PLUGIN_ID, DB_NAME); + // If rootStorePath has length and is not equal to the default path, use it instead. + // Note: Enabling this would cause the database to store with the game root instead (see Plugin.cs:43) + // if (rootStorePath.Length > 0 && rootStorePath != BepInEx.Paths.ConfigPath) + // { + // DbPath = System.IO.Path.Combine(rootStorePath, DB_NAME); + // } // Check for database in old location and move if necessary string oldDatabase = System.IO.Path.Combine(BepInEx.Paths.ConfigPath, $"{PluginInfo.PLUGIN_ID}-records.db"); @@ -46,11 +51,11 @@ public Database(string rootStorePath) /// /// Method to initialize the database reference and content. - /// + /// /// This method creates the database file (if it doesn't exist) and opens it for reading. - /// + /// /// Because of how LiteDB works (it creates tables as needed), this method simply creates the collection handles - /// used later on as records get added to the database. + /// used later on as records get added to the database. /// public void Initialize() { @@ -123,7 +128,7 @@ private void InsertSimpleStatRecord(ILiteCollection collection, stri { Task.Run(() => { - var newRecord = new SimpleStat( + SimpleStat newRecord = new SimpleStat( playerName, playerHostName, pos.x, pos.y, pos.z @@ -205,7 +210,7 @@ public void InsertSimpleStatRecord(string key, string playerName, string playerH /// /// Insert a simple stat record without position for the provided key into the LiteDB database. - /// + /// /// This method simply wraps the positional InsertSimpleStatRecord method. /// /// What kind of record to insert @@ -218,7 +223,7 @@ public void InsertSimpleStatRecord(string key, string playerName, string playerH /// /// Returns the latest known character name for the given player identifier. - /// + /// /// This first tries to find the name in the player_name table, and failing that references the join table with a query. /// If it has to use the join table to find the name, if it does find a valid name, it adds a record to the player_name /// table to make future lookups faster. @@ -245,7 +250,7 @@ internal string GetLatestCharacterNameForPlayer(string playerHostName) .First(); return playerInfo.CharacterName; } - catch (System.InvalidOperationException) + catch (InvalidOperationException) { // We should never not find the record, since we check for exists above! Plugin.StaticLogger.LogWarning($"Should have found {playerHostName} in player_name table but did not!"); @@ -254,7 +259,7 @@ internal string GetLatestCharacterNameForPlayer(string playerHostName) // Some manual query business to grab the name from the Join table. This section should only get reached for old records, // where the player has not logged in for a while. - var nameQuery = JoinCollection.Query() + List nameQuery = JoinCollection.Query() .Where(x => x.PlayerId.Equals(playerHostName)) .OrderByDescending("Date") .Select("$.Name") @@ -272,7 +277,7 @@ internal string GetLatestCharacterNameForPlayer(string playerHostName) Plugin.StaticLogger.LogDebug($"nameQuery has {nameQuery.Count} results"); } // simplify results to single record - var result = nameQuery[0]; + BsonDocument result = nameQuery[0]; if (Plugin.StaticConfig.DebugDatabaseMethods) { Plugin.StaticLogger.LogDebug($"GetLatestNameForCharacterId {playerHostName} result = {result}"); @@ -317,13 +322,13 @@ public List CountAllRecordsGrouped(string key) private struct JoinLeaveTime { - public System.DateTime Time; + public DateTime Time; public bool IsJoin; } /// /// Provides time online in seconds for all players within the date range provided. By default, it includes all time. - /// + /// /// This looks through the provided simple stat table and counts up time differences between joins and leaves. /// /// Date to start including records from @@ -340,7 +345,7 @@ private List TimeOnlineRecordsGrouped(System.DateTime? startDate = foreach (PlayerToName player in players) { // Create a spot to record total online time for player. - System.TimeSpan onlineTime = System.TimeSpan.FromSeconds(0.0); + TimeSpan onlineTime = TimeSpan.FromSeconds(0.0); var joinsQuery = JoinCollection.Query().Where(x => x.PlayerId.Equals(player.PlayerId) && x.Name.Equals(player.CharacterName)); var leavesQuery = LeaveCollection.Query().Where(x => x.PlayerId.Equals(player.PlayerId) && x.Name.Equals(player.CharacterName)); @@ -370,18 +375,21 @@ private List TimeOnlineRecordsGrouped(System.DateTime? startDate = Plugin.StaticLogger.LogDebug($"{player.PlayerId} as {player.CharacterName} has {joins.Length} joins, {leaves.Length} leaves"); System.DateTime? joinedTime = null; - foreach (JoinLeaveTime joinLeaveTime in sortedJoinLeaves) { + foreach (JoinLeaveTime joinLeaveTime in sortedJoinLeaves) + { if (joinedTime == null) { // Player is not currently joined, so expecting a join. if (joinLeaveTime.IsJoin) { joinedTime = joinLeaveTime.Time; - } else + } + else { Plugin.StaticLogger.LogDebug($"Player {player.CharacterName} left at {joinLeaveTime.Time} but was not joined."); } - } else + } + else { // Player is currently joined, expecting a leave. if (joinLeaveTime.IsJoin) @@ -409,9 +417,9 @@ private List TimeOnlineRecordsGrouped(System.DateTime? startDate = /// /// Get the total count of records in the category for the player (by character name). - /// + /// /// This can be used to figure out if its the first action the player has taken. - /// + /// /// Returns -1 if collecting stats is disabled. /// Returns -2 if the specified category key is invalid /// Returns -3 if the collection in the database has an issue @@ -450,7 +458,7 @@ public List CountAllRecordsGrouped(string key, LeaderBoards.TimeRan return CountAllRecordsGrouped(key); } - var BeginEndDate = DateHelper.StartEndDatesForTimeRange(timeRange); + Tuple BeginEndDate = DateHelper.StartEndDatesForTimeRange(timeRange); return CountRecordsBetweenDatesGrouped(key, BeginEndDate.Item1, BeginEndDate.Item2); } @@ -472,7 +480,7 @@ internal List CountRecordsBetweenDatesGrouped(string key, System.Da return TimeOnlineRecordsGrouped(startDate, endDate, inclusiveStart, inclusiveEnd); default: Plugin.StaticLogger.LogDebug($"CountTodaysRecordsGrouped, invalid key '{key}'"); - return new List(); + return []; } } @@ -494,7 +502,7 @@ internal int CountOfRecordsByName(ILiteCollection collection, string /// /// Return a list of leaders for the collection. - /// + /// /// This looks through the provided simple stat table and counts up results for each player using the method defined in the config. /// /// Simple stat collection to count player totals in @@ -512,7 +520,7 @@ internal List CountAllRecordsGrouped(ILiteCollection co { Plugin.StaticLogger.LogDebug("Collection is empty, skipping."); } - return new List(); + return []; } // Config.RetrievalDiscernmentMethods.BySteamID by default (should be most common), conditionally check for others @@ -530,7 +538,7 @@ internal List CountAllRecordsGrouped(ILiteCollection co SelectClause = "{Name: @Key, Count: Count(*)}"; } - var result = CountResult.ConvertFromBsonDocuments( + List result = CountResult.ConvertFromBsonDocuments( collection.Query() .GroupBy(GroupByClause) .Select(SelectClause) @@ -548,7 +556,7 @@ internal List CountAllRecordsGrouped(ILiteCollection co /// /// Provides count summaries for the collection within the date range provided. By default, it includes the start date. - /// + /// /// This looks through the provided simple stat table and counts up results for each player using the method defined in the config. /// /// Simple stat collection to count player totals in @@ -570,7 +578,7 @@ internal List CountAllRecordsGroupsWhereDate(ILiteCollection(); + return []; } // Config.RetrievalDiscernmentMethods.BySteamID by default (should be most common), conditionally check for others diff --git a/src/Utility.cs b/src/Utility.cs index 48f09b4..825833e 100644 --- a/src/Utility.cs +++ b/src/Utility.cs @@ -5,8 +5,8 @@ internal static class Hashing { public static string GetMD5Checksum(string filename) { - using var md5 = System.Security.Cryptography.MD5.Create(); - using var stream = System.IO.File.OpenRead(filename); + using System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create(); + using System.IO.FileStream stream = System.IO.File.OpenRead(filename); byte[] hash = md5.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", ""); }