From 0ff79e03df0eea300901b63576057f49363d2e0c Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Mon, 29 Jan 2024 21:55:34 -0600 Subject: [PATCH] MSDP Support, Server Side. Client side pending. --- .../MSDPLibrary.fs | 2 - .../KestrelMockServer.cs | 18 ++- .../Handlers/MSDPServerHandler.cs | 103 ++++----------- .../Handlers/MSDPServerModel.cs | 117 ++++++++++++++++++ .../Interpreters/TelnetStandardInterpreter.cs | 2 +- .../TelnetNegotiationCore.csproj | 1 + 6 files changed, 155 insertions(+), 88 deletions(-) create mode 100644 TelnetNegotiationCore/Handlers/MSDPServerModel.cs diff --git a/TelnetNegotiationCore.Functional/MSDPLibrary.fs b/TelnetNegotiationCore.Functional/MSDPLibrary.fs index cd50ed9..1b6a122 100644 --- a/TelnetNegotiationCore.Functional/MSDPLibrary.fs +++ b/TelnetNegotiationCore.Functional/MSDPLibrary.fs @@ -58,7 +58,6 @@ module MSDPLibrary = [(byte)Trigger.MSDP_VAR] @ (encoding.GetBytes(key) |> List.ofArray) @ [(byte)Trigger.MSDP_VAL] @ value ) |> List.concat [(byte)Trigger.MSDP_TABLE_OPEN] @ parsedObj @ [(byte)Trigger.MSDP_TABLE_CLOSE] - | JsonValueKind.Array -> let parsedArr = jsonNode.AsArray() @@ -66,7 +65,6 @@ module MSDPLibrary = |> List.ofSeq |> List.concat [(byte)Trigger.MSDP_ARRAY_OPEN] @ parsedArr @ [(byte)Trigger.MSDP_ARRAY_CLOSE] - | JsonValueKind.String -> encoding.GetBytes(jsonNode.AsValue().ToString()) |> List.ofArray | JsonValueKind.Number -> encoding.GetBytes(jsonNode.AsValue().ToString()) |> List.ofArray | JsonValueKind.True -> encoding.GetBytes("1") |> List.ofArray diff --git a/TelnetNegotiationCore.TestServer/KestrelMockServer.cs b/TelnetNegotiationCore.TestServer/KestrelMockServer.cs index e6bc6a8..e482d5e 100644 --- a/TelnetNegotiationCore.TestServer/KestrelMockServer.cs +++ b/TelnetNegotiationCore.TestServer/KestrelMockServer.cs @@ -53,7 +53,7 @@ public Task SignalNAWSAsync(int height, int width) return Task.CompletedTask; } - private async Task SignalMSDPAsync(MSDPServerHandler handler, TelnetInterpreter telnet, string config) => + private static async Task SignalMSDPAsync(MSDPServerHandler handler, TelnetInterpreter telnet, string config) => await handler.HandleAsync(telnet, config); public static async Task WriteBackAsync(byte[] writeback, Encoding encoding, TelnetInterpreter telnet) @@ -66,13 +66,25 @@ public static async Task WriteBackAsync(byte[] writeback, Encoding encoding, Tel Console.WriteLine(encoding.GetString(writeback)); } + private async Task MSDPUpdateBehavior(string resetVariable) + { + _Logger.LogDebug("MSDP Reset Request: {@Reset}", resetVariable); + await Task.CompletedTask; + } + public async override Task OnConnectedAsync(ConnectionContext connection) { using (_Logger.BeginScope(new Dictionary { { "ConnectionId", connection.ConnectionId } })) { _Logger.LogInformation("{ConnectionId} connected", connection.ConnectionId); - var msdpHandler = new MSDPServerHandler([]); + var MSDPHandler = new MSDPServerHandler(new MSDPServerModel(MSDPUpdateBehavior) + { + Commands = () => ["help", "stats", "info"], + Configurable_Variables = () => ["CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"], + Reportable_Variables = () => ["ROOM"], + Sendable_Variables = () => ["ROOM"], + }); var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger) { @@ -80,7 +92,7 @@ public async override Task OnConnectedAsync(ConnectionContext connection) SignalOnGMCPAsync = SignalGMCPAsync, SignalOnMSSPAsync = SignalMSSPAsync, SignalOnNAWSAsync = SignalNAWSAsync, - SignalOnMSDPAsync = (telnet, config) => SignalMSDPAsync(msdpHandler, telnet, config), + SignalOnMSDPAsync = (telnet, config) => SignalMSDPAsync(MSDPHandler, telnet, config), CallbackNegotiationAsync = (x) => WriteToOutputStreamAsync(x, connection.Transport.Output), CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } } diff --git a/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs b/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs index 0a0298c..52959e9 100644 --- a/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs +++ b/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs @@ -14,70 +14,12 @@ namespace TelnetNegotiationCore.Handlers /// We need a way to Observe changes to the functions to properly support the REPORT action. /// This is not currently implemented. /// - /// - /// A dictionary that resolves Variables, commands, and lists. This has a very specific setup, and comes with a contract. - /// - /// TODO: We can do this better somehow. - /// - /// https://tintin.mudhalla.net/protocols/msdp/ - /// Reportable MSDP Variables - /// These variables are mere suggestions for MUDs wanting to implement MSDP.By using these reportable variables it'll be easier to create MSDP scripts that need little or no modification to work across MUDs. If you create your own set of variables in addition to these, it's suggested to create an extended online specification that describes your variables and their behavior.The SPECIFICATION variable can be used to link people to the web page. - /// - /// General - /// "ACCOUNT_NAME" Name of the player account. - /// "CHARACTER_NAME" Name of the player character. - /// "SERVER_ID" Name of the MUD, or an otherwise unique ID. - /// "SERVER_TIME" The time on the server using either military or civilian time. - /// "SPECIFICATION" URL to the MUD's online MSDP specification, if any. - /// - /// Character - /// "AFFECTS" Current affects in array format. - /// "ALIGNMENT" Current alignment. - /// "EXPERIENCE" Current total experience points. Use 0-100 for percentages. - /// "EXPERIENCE_MAX" Current maximum experience points. Use 100 for percentages. - /// "EXPERIENCE_TNL" Current total experience points Till Next Level.Use 0-100 for percentages. - /// "EXPERIENCE_TNL_MAX" Current maximum experience points Till Next Level.Use 100 for percentages. - /// "HEALTH" Current health points. - /// "HEALTH_MAX" Current maximum health points. - /// "LEVEL" Current level. - /// "MANA" Current mana points. - /// "MANA_MAX" Current maximum mana points. - /// "MONEY" Current amount of money. - /// "MOVEMENT" Current movement points. - /// "MOVEMENT_MAX" Current maximum movement points. - /// - /// Combat - /// "OPPONENT_LEVEL" Level of opponent. - /// "OPPONENT_HEALTH" Current health points of opponent.Use 0-100 for percentages. - /// "OPPONENT_HEALTH_MAX" Current maximum health points of opponent. Use 100 for percentages. - /// "OPPONENT_NAME" Name of opponent. - /// "OPPONENT_STRENGTH" Relative strength of opponent, like the consider mud command. - /// Mapping - /// Indentation indicates the variable is nested within the parent variable using a table. - /// "ROOM" - /// "VNUM" A number uniquely identifying the room. - /// "NAME" The name of the room. - /// "AREA" The area the room is in. - /// "COORDS" - /// "X" The X coordinate of the room. - /// "Y" The Y coordinate of the room. - /// "Z" The Z coordinate of the room. - /// "TERRAIN" The terrain type of the room. Forest, Ocean, etc. - /// "EXITS" Nested abbreviated exit directions (n, e, w, etc) and corresponding destination VNUMs. - /// - /// World - /// "WORLD_TIME" The in game time on the MUD using either military or civilian time. - /// - /// Configurable MSDP Variables - /// Configurable variables are variables on the server that can be altered by the client. Implementing configurable variable support is optional. - /// General - /// "CLIENT_NAME" Name of the MUD client. - /// "CLIENT_VERSION" Version of the MUD client. - /// "PLUGIN_ID" Unique ID of the MSDP plugin/script. + /// + /// A dictionary that resolves Variables, commands, and lists. /// - public class MSDPServerHandler(Dictionary> resolvers) + public class MSDPServerHandler(MSDPServerModel model) { - private Dictionary> Resolvers { get; } = resolvers; + public MSDPServerModel Data { get; private init; } = model; public async Task HandleAsync(TelnetInterpreter telnet, string clientJson) { @@ -119,19 +61,19 @@ public async Task HandleAsync(TelnetInterpreter telnet, string clientJson) /// /// Telnet Interpreter to callback with. /// The item to report - private async Task HandleListRequestAsync(TelnetInterpreter telnet, string item) - { - // TODO: Do something to check if ITEM is an Array or a single String here. - var found = Resolvers.TryGetValue($"LIST.{item}", out var list); - var jsonString = $"{{{item}:{(found ? list : "null")}}}"; - await telnet.CallbackNegotiationAsync(MSDPLibrary.Report(jsonString, telnet.CurrentEncoding)); - } + private async Task HandleListRequestAsync(TelnetInterpreter telnet, string item) => + await ExecuteOnAsync(item, async (val) => + await (Data.Lists.TryGetValue(val, out Func> value) + ? telnet.CallbackNegotiationAsync( + MSDPLibrary.Report(JsonSerializer.Serialize(value()), + telnet.CurrentEncoding)) + : Task.CompletedTask)); private async Task HandleReportRequestAsync(TelnetInterpreter telnet, string item) => await ExecuteOnAsync(item, async (val) => { await HandleSendRequestAsync(telnet, val); - // TODO: Implement a Reporting system. + Data.Report(val, async (newVal) => await HandleSendRequestAsync(telnet, newVal)); }); /// @@ -141,11 +83,9 @@ await ExecuteOnAsync(item, async (val) => /// /// Item to reset private async Task HandleResetRequestAsync(string item) => - await ExecuteOnAsync(item, async (val) => + await ExecuteOnAsync(item, async (var) => { - // TODO: Reset it? - // TODO: Enable resetting other items we can LIST? - var found = Resolvers.TryGetValue($"REPORTABLE_VARIABLES.{val}", out var list); + var found = Data.Reportable_Variables().TryGetValue(var, out var list); await Task.CompletedTask; }); @@ -158,10 +98,10 @@ await ExecuteOnAsync(item, async (val) => /// Telnet interpreter to send back negotiation with /// The item to send private async Task HandleSendRequestAsync(TelnetInterpreter telnet, string item) => - await ExecuteOnAsync(item, async (val) => + await ExecuteOnAsync(item, async (var) => { - var found = Resolvers.TryGetValue($"REPORTABLE_VARIABLES.{val}", out var list); - var jsonString = $"{{{val}:{(found ? list : "null")}}}"; + var found = Data.Sendable_Variables().TryGetValue(var, out var val); + var jsonString = $"{{{var}:{(found ? val : "null")}}}"; await telnet.CallbackNegotiationAsync(MSDPLibrary.Report(jsonString, telnet.CurrentEncoding)); }); @@ -170,13 +110,12 @@ await ExecuteOnAsync(item, async (val) => /// /// The item to stop reporting on private async Task HandleUnReportRequestAsync(string item) => - await ExecuteOnAsync(item, async (val) => + await ExecuteOnAsync(item, async (var) => { - // TODO: Remove them from the list of variables being reported. - var found = Resolvers.TryGetValue($"REPORTED_VARIABLES.{item}", out var list); + Data.UnReport(var); await Task.CompletedTask; }); - + private async Task ExecuteOnAsync(string item, Func function) { string[] items; @@ -188,7 +127,7 @@ private async Task ExecuteOnAsync(string item, Func function) { items = [item]; } - foreach(var val in items) + foreach (var val in items) { await function(val); } diff --git a/TelnetNegotiationCore/Handlers/MSDPServerModel.cs b/TelnetNegotiationCore/Handlers/MSDPServerModel.cs new file mode 100644 index 0000000..1993946 --- /dev/null +++ b/TelnetNegotiationCore/Handlers/MSDPServerModel.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TelnetNegotiationCore.Handlers +{ + /// + /// https://tintin.mudhalla.net/protocols/msdp/ + /// Reportable MSDP Variables + /// These variables are mere suggestions for MUDs wanting to implement MSDP.By using these reportable variables it'll be easier to create MSDP scripts that need little or no modification to work across MUDs. If you create your own set of variables in addition to these, it's suggested to create an extended online specification that describes your variables and their behavior.The SPECIFICATION variable can be used to link people to the web page. + /// + /// General + /// "ACCOUNT_NAME" Name of the player account. + /// "CHARACTER_NAME" Name of the player character. + /// "SERVER_ID" Name of the MUD, or an otherwise unique ID. + /// "SERVER_TIME" The time on the server using either military or civilian time. + /// "SPECIFICATION" URL to the MUD's online MSDP specification, if any. + /// + /// Character + /// "AFFECTS" Current affects in array format. + /// "ALIGNMENT" Current alignment. + /// "EXPERIENCE" Current total experience points. Use 0-100 for percentages. + /// "EXPERIENCE_MAX" Current maximum experience points. Use 100 for percentages. + /// "EXPERIENCE_TNL" Current total experience points Till Next Level.Use 0-100 for percentages. + /// "EXPERIENCE_TNL_MAX" Current maximum experience points Till Next Level.Use 100 for percentages. + /// "HEALTH" Current health points. + /// "HEALTH_MAX" Current maximum health points. + /// "LEVEL" Current level. + /// "MANA" Current mana points. + /// "MANA_MAX" Current maximum mana points. + /// "MONEY" Current amount of money. + /// "MOVEMENT" Current movement points. + /// "MOVEMENT_MAX" Current maximum movement points. + /// + /// Combat + /// "OPPONENT_LEVEL" Level of opponent. + /// "OPPONENT_HEALTH" Current health points of opponent.Use 0-100 for percentages. + /// "OPPONENT_HEALTH_MAX" Current maximum health points of opponent. Use 100 for percentages. + /// "OPPONENT_NAME" Name of opponent. + /// "OPPONENT_STRENGTH" Relative strength of opponent, like the consider mud command. + /// Mapping + /// Indentation indicates the variable is nested within the parent variable using a table. + /// "ROOM" + /// "VNUM" A number uniquely identifying the room. + /// "NAME" The name of the room. + /// "AREA" The area the room is in. + /// "COORDS" + /// "X" The X coordinate of the room. + /// "Y" The Y coordinate of the room. + /// "Z" The Z coordinate of the room. + /// "TERRAIN" The terrain type of the room. Forest, Ocean, etc. + /// "EXITS" Nested abbreviated exit directions (n, e, w, etc) and corresponding destination VNUMs. + /// + /// World + /// "WORLD_TIME" The in game time on the MUD using either military or civilian time. + /// + /// Configurable MSDP Variables + /// Configurable variables are variables on the server that can be altered by the client. Implementing configurable variable support is optional. + /// General + /// "CLIENT_NAME" Name of the MUD client. + /// "CLIENT_VERSION" Version of the MUD client. + /// "PLUGIN_ID" Unique ID of the MSDP plugin/script. + /// + public class MSDPServerModel + { + /// + /// What lists we can report on. + /// + public Dictionary>> Lists { get; private init; } + + public Func> Commands { get; set; } = () => []; + + public Func> Configurable_Variables { get; set; } = () => []; + + public Func> Reportable_Variables { get; set; } = () => []; + + public Dictionary> Reported_Variables => []; + + public Func> Sendable_Variables { get; set; } = () => []; + + public Func ResetCallbackAsync { get; } + + /// + /// Creates the MSDP Server Model. + /// Define each public variable to implement MSDP. + /// + /// Function to call when a client wishes to set a server variable. + public MSDPServerModel(Func resetCallback) + { + Lists = new() + { + { "COMMANDS", Commands}, + { "CONFIGURABLE_VARIABLES", Configurable_Variables}, + { "REPORTABLE_VARIABLES", Reportable_Variables}, + { "REPORTED_VARIABLES", () => Reported_Variables.Select( x=> x.Key).ToHashSet() }, + { "SENDABLE_VARIABLES", Sendable_Variables} + }; + + ResetCallbackAsync = resetCallback; + } + + public async Task ResetAsync(string configurableVariable) => + await ResetCallbackAsync(configurableVariable); + + public void Report(string reportableVariable, Func function) => + Reported_Variables.Add(reportableVariable, function); + + public void UnReport(string reportableVariable) => + Reported_Variables.Remove(reportableVariable); + + public async Task NotifyChangeAsync(string reportableVariable, string newValue) => + await (Reported_Variables.TryGetValue(reportableVariable, out var function) + ? function(newValue) + : Task.CompletedTask); + } +} diff --git a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs index d3044f5..f8bbccf 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs @@ -104,7 +104,7 @@ public TelnetInterpreter(TelnetMode mode, ILogger logger) SupportedCharacterSets = new Lazy(CharacterSets, true); var li = new List, StateMachine>> { - SetupSafeNegotiation, SetupEORNegotiation, SetupMSSPNegotiation, SetupGMCPNegotiation, SetupTelnetTerminalType, SetupCharsetNegotiation, SetupNAWS, SetupStandardProtocol + SetupSafeNegotiation, SetupEORNegotiation, SetupMSSPNegotiation, SetupMSDPNegotiation, SetupGMCPNegotiation, SetupTelnetTerminalType, SetupCharsetNegotiation, SetupNAWS, SetupStandardProtocol }.AggregateRight(TelnetStateMachine, (func, stateMachine) => func(stateMachine)); if (logger.IsEnabled(LogLevel.Trace)) diff --git a/TelnetNegotiationCore/TelnetNegotiationCore.csproj b/TelnetNegotiationCore/TelnetNegotiationCore.csproj index f6f6ddd..a7dd08c 100644 --- a/TelnetNegotiationCore/TelnetNegotiationCore.csproj +++ b/TelnetNegotiationCore/TelnetNegotiationCore.csproj @@ -52,6 +52,7 @@ +