Skip to content

Commit

Permalink
MSDP Support, Server Side. Client side pending.
Browse files Browse the repository at this point in the history
  • Loading branch information
HarryCordewener committed Jan 30, 2024
1 parent a60073b commit 0ff79e0
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 88 deletions.
2 changes: 0 additions & 2 deletions TelnetNegotiationCore.Functional/MSDPLibrary.fs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,13 @@ 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()
|> Seq.map (fun prop -> [(byte)Trigger.MSDP_VAL] @ parseJsonValue(prop))
|> 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
Expand Down
18 changes: 15 additions & 3 deletions TelnetNegotiationCore.TestServer/KestrelMockServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -66,21 +66,33 @@ 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<string, object> { { "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)
{
CallbackOnSubmitAsync = WriteBackAsync,
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") }
}
Expand Down
103 changes: 21 additions & 82 deletions TelnetNegotiationCore/Handlers/MSDPServerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </remarks>
/// <param name="resolvers">
/// 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.
/// <param name="model">
/// A dictionary that resolves Variables, commands, and lists.
/// </param>
public class MSDPServerHandler(Dictionary<string, Func<string>> resolvers)
public class MSDPServerHandler(MSDPServerModel model)
{
private Dictionary<string, Func<string>> Resolvers { get; } = resolvers;
public MSDPServerModel Data { get; private init; } = model;

public async Task HandleAsync(TelnetInterpreter telnet, string clientJson)
{
Expand Down Expand Up @@ -119,19 +61,19 @@ public async Task HandleAsync(TelnetInterpreter telnet, string clientJson)
/// </summary>
/// <param name="telnet">Telnet Interpreter to callback with.</param>
/// <param name="item">The item to report</param>
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<HashSet<string>> 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));
});

/// <summary>
Expand All @@ -141,11 +83,9 @@ await ExecuteOnAsync(item, async (val) =>
/// </summary>
/// <param name="item">Item to reset</param>
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;
});

Expand All @@ -158,10 +98,10 @@ await ExecuteOnAsync(item, async (val) =>
/// <param name="telnet">Telnet interpreter to send back negotiation with</param>
/// <param name="item">The item to send</param>
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));
});

Expand All @@ -170,13 +110,12 @@ await ExecuteOnAsync(item, async (val) =>
/// </summary>
/// <param name="item">The item to stop reporting on</param>
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<string, Task> function)
{
string[] items;
Expand All @@ -188,7 +127,7 @@ private async Task ExecuteOnAsync(string item, Func<string, Task> function)
{
items = [item];
}
foreach(var val in items)
foreach (var val in items)
{
await function(val);
}
Expand Down
117 changes: 117 additions & 0 deletions TelnetNegotiationCore/Handlers/MSDPServerModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TelnetNegotiationCore.Handlers
{
/// <summary>
/// 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.
/// </summary>
public class MSDPServerModel
{
/// <summary>
/// What lists we can report on.
/// </summary>
public Dictionary<string, Func<HashSet<string>>> Lists { get; private init; }

public Func<HashSet<string>> Commands { get; set; } = () => [];

public Func<HashSet<string>> Configurable_Variables { get; set; } = () => [];

public Func<HashSet<string>> Reportable_Variables { get; set; } = () => [];

public Dictionary<string, Func<string, Task>> Reported_Variables => [];

public Func<HashSet<string>> Sendable_Variables { get; set; } = () => [];

public Func<string, Task> ResetCallbackAsync { get; }

/// <summary>
/// Creates the MSDP Server Model.
/// Define each public variable to implement MSDP.
/// </summary>
/// <param name="setCallback">Function to call when a client wishes to set a server variable.</param>
public MSDPServerModel(Func<string,Task> 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<string, Task> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public TelnetInterpreter(TelnetMode mode, ILogger logger)
SupportedCharacterSets = new Lazy<byte[]>(CharacterSets, true);

var li = new List<Func<StateMachine<State, Trigger>, StateMachine<State, Trigger>>> {
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))
Expand Down
1 change: 1 addition & 0 deletions TelnetNegotiationCore/TelnetNegotiationCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<PackageReference Include="OneOf" Version="3.0.255" />
<PackageReference Include="stateless" Version="5.13.0" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 0ff79e0

Please sign in to comment.