Skip to content

Commit

Permalink
Let's make sure we can also write MSDP from a JSON object.
Browse files Browse the repository at this point in the history
  • Loading branch information
HarryCordewener committed Jan 29, 2024
1 parent 72153be commit 5b8c69e
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 15 deletions.
36 changes: 35 additions & 1 deletion TelnetNegotiationCore.Functional/MSDPLibrary.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace TelnetNegotiationCore.Functional

open System.Text
open System.Text.Json
open System.Text.Json.Nodes

module MSDPLibrary =
type Trigger =
Expand Down Expand Up @@ -42,4 +44,36 @@ module MSDPLibrary =

let public MSDPScan(array: seq<byte>, encoding) =
let (result, _) = MSDPScanTailRec(Map<string,obj> [], array, encoding)
result
result

let parseJsonRoot (jsonRootNode: JsonNode, encoding: Encoding) =
let rec parseJsonValue (jsonNode: JsonNode) =
match jsonNode.GetValueKind() with
| JsonValueKind.Object ->
let parsedObj =
jsonNode.AsObject()
|> Seq.map (fun prop ->
let key = prop.Key
let value = parseJsonValue prop.Value
[(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
| JsonValueKind.False -> encoding.GetBytes("0") |> List.ofArray
| JsonValueKind.Null -> encoding.GetBytes("null") |> List.ofArray
| _ -> failwith "Invalid JSON value"
parseJsonValue jsonRootNode

let public Report(jsonString: string, encoding: Encoding) =
parseJsonRoot(JsonValue.Parse(jsonString), encoding)
9 changes: 8 additions & 1 deletion TelnetNegotiationCore.TestServer/KestrelMockServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO.Pipelines;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using TelnetNegotiationCore.Handlers;

namespace TelnetNegotiationCore.TestServer
{
Expand Down Expand Up @@ -52,6 +53,9 @@ public Task SignalNAWSAsync(int height, int width)
return Task.CompletedTask;
}

private 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)
{
var str = encoding.GetString(writeback);
Expand All @@ -68,12 +72,15 @@ public async override Task OnConnectedAsync(ConnectionContext connection)
{
_Logger.LogInformation("{ConnectionId} connected", connection.ConnectionId);

var msdpHandler = new MSDPServerHandler([]);

var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger)
{
CallbackOnSubmitAsync = (w, e, t) => WriteBackAsync(w, e, t),
CallbackOnSubmitAsync = WriteBackAsync,
SignalOnGMCPAsync = SignalGMCPAsync,
SignalOnMSSPAsync = SignalMSSPAsync,
SignalOnNAWSAsync = SignalNAWSAsync,
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
166 changes: 160 additions & 6 deletions TelnetNegotiationCore/Handlers/MSDPServerHandler.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using TelnetNegotiationCore.Functional;
using TelnetNegotiationCore.Interpreters;

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.
*/

/// <summary>
/// A simple handler for MSDP that creates a workflow for responding with MSDP information.
/// </summary>
public class MSDPServerHandler
/// <remarks>
/// We need a way to Observe changes to the functions to properly support the REPORT action.
/// This is not currently implemented.
/// </remarks>
public class MSDPServerHandler(Dictionary<string, Func<string>> resolvers)
{
public MSDPServerHandler() { }
public async Task HandleAsync(TelnetInterpreter telnet, string clientJson)
{
var json = JsonSerializer.Deserialize<dynamic>(clientJson);

if (json.LIST != null)
{
await HandleListRequestAsync(telnet, (string)json.LIST);
}
else if (json.REPORT != null)
{
await HandleReportRequestAsync(telnet, (string)json.REPORT);
}
else if (json.RESET != null)
{
await HandleResetRequestAsync((string)json.RESET);
}
else if (json.SEND != null)
{
await HandleSendRequestAsync(telnet, (string)json.SEND);
}
else if (json.UNREPORT != null)
{
await HandleUnReportRequestAsync((string)json.UNREPORT);
}

await Task.CompletedTask;
}

/// <summary>
/// Handles a request to LIST a single item. For safety, also supports a list of lists.
/// Should at least support:
/// "COMMANDS" Request an array of commands supported by the server.
/// "LISTS" Request an array of lists supported by the server.
/// "CONFIGURABLE_VARIABLES" Request an array of variables the client can configure.
/// "REPORTABLE_VARIABLES" Request an array of variables the server will report.
/// "REPORTED_VARIABLES" Request an array of variables currently being reported.
/// "SENDABLE_VARIABLES" Request an array of variables the server will send.
/// </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));

Check failure on line 122 in TelnetNegotiationCore/Handlers/MSDPServerHandler.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'Microsoft.FSharp.Collections.FSharpList<byte>' to 'byte[]'

Check failure on line 122 in TelnetNegotiationCore/Handlers/MSDPServerHandler.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'Microsoft.FSharp.Collections.FSharpList<byte>' to 'byte[]'
}

private async Task HandleReportRequestAsync(TelnetInterpreter telnet, string item)
{
// TODO: Do something to check if ITEM is an Array or a single String here.
// TODO: Implement a Reporting system.
await HandleSendRequestAsync(telnet, item);
}

/// <summary>
/// The RESET command works like the LIST command, and can be used to reset groups of variables to their initial state.
/// Most commonly RESET will be called with REPORTABLE_VARIABLES or REPORTED_VARIABLES as the argument,
/// though any LIST option can be used.
/// </summary>
/// <param name="item">Item to reset</param>
private async Task HandleResetRequestAsync(string item)
{
// TODO: Do something to check if ITEM is an Array or a single String here.
// TODO: Reset it?
// TODO: Enable resetting other items we can LIST?
var found = resolvers.TryGetValue($"REPORTABLE_VARIABLES.{item}", out var list);
await Task.CompletedTask;
}

/// <summary>
/// The SEND command can be used by either side, but should typically be used by the client.
/// After the client has received a list of variables, or otherwise knows which variables exist,
/// it can request the server to send those variables and their values with the SEND command.
/// The value of the SEND command should be a list of variables the client wants returned.
/// </summary>
/// <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)
{
// TODO: Do something to check if ITEM is an Array or a single String here.
var found = resolvers.TryGetValue($"REPORTABLE_VARIABLES.{item}", out var list);
var jsonString = $"{{{item}:{(found ? list : "null")}}}";
await telnet.CallbackNegotiationAsync(MSDPLibrary.Report(jsonString, telnet.CurrentEncoding));

Check failure on line 160 in TelnetNegotiationCore/Handlers/MSDPServerHandler.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'Microsoft.FSharp.Collections.FSharpList<byte>' to 'byte[]'

Check failure on line 160 in TelnetNegotiationCore/Handlers/MSDPServerHandler.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'Microsoft.FSharp.Collections.FSharpList<byte>' to 'byte[]'
}

public Task HandleAsync(string clientJson)
/// <summary>
/// The UNREPORT command is used to remove the report status of variables after the use of the REPORT command.
/// </summary>
/// <param name="item">The item to stop reporting on</param>
private async Task HandleUnReportRequestAsync(string item)
{
throw new NotImplementedException();
var found = resolvers.TryGetValue($"REPORTED_VARIABLES.{item}", out var list);
// TODO: Remove them from the list of variables being reported.
// TODO: Do something to check if ITEM is an Array or a single String here.
await Task.CompletedTask;
}
}
}
13 changes: 8 additions & 5 deletions TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,15 @@ private async Task CompleteGMCPNegotiation(StateMachine<State, Trigger>.Transiti

// TODO: Consideration: a version of this that sends back a Dynamic or other similar object.
var package = CurrentEncoding.GetString(packageBytes);
var info =
package == "MSDP"
? JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(packageBytes, CurrentEncoding))
: CurrentEncoding.GetString(packageBytes);

await (SignalOnGMCPAsync?.Invoke((Package: package, Info: info)) ?? Task.CompletedTask);
if(package == "MSDP")
{
await (SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(packageBytes, CurrentEncoding))) ?? Task.CompletedTask);
}
else
{
await (SignalOnGMCPAsync?.Invoke((Package: package, Info: CurrentEncoding.GetString(packageBytes))) ?? Task.CompletedTask);
}
}

/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OneOf;
Expand All @@ -19,7 +20,7 @@ public partial class TelnetInterpreter
{
private List<byte> _currentMSDPInfo;

public Func<MSDPConfig, Task> SignalOnMSDPAsync { get; init; }
public Func<TelnetInterpreter, string, Task> SignalOnMSDPAsync { get; init; }

/// <summary>
/// Mud Server Status Protocol will provide information to the requestee about the server's contents.
Expand Down Expand Up @@ -94,7 +95,7 @@ private StateMachine<State, Trigger> SetupMSDPNegotiation(StateMachine<State, Tr

private void CaptureMSDPValue(OneOf<byte, Trigger> b) => _currentMSDPInfo.Add(b.AsT0);

private void ReadMSDPValues() => Functional.MSDPLibrary.MSDPScan(_currentMSDPInfo.Skip(1), CurrentEncoding);
private void ReadMSDPValues() => SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(_currentMSDPInfo.Skip(1), CurrentEncoding)));

/// <summary>
/// Announce we do MSDP negotiation to the client.
Expand Down

0 comments on commit 5b8c69e

Please sign in to comment.