Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ModMessageSystem #420

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Nautilus/Utility/ModMessages/BasicModMessageReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace Nautilus.Utility.ModMessages;

/// <summary>
/// Basic implementation of the abstract <see cref="ModMessageReader"/> that runs an action when a message with the given subject is received.
/// </summary>
public sealed class BasicModMessageReader : ModMessageReader
{
private string _subject;

private Action<object[]> _action;

/// <summary>
/// Creates a message reader that runs the given <paramref name="action"/> when a message with the given <paramref name="subject"/> is received.
/// </summary>
/// <param name="subject">The subject that this reader is looking for.</param>
/// <param name="action">The action that is run for any message with the given <paramref name="subject"/>.</param>
public BasicModMessageReader(string subject, Action<object[]> action)
{
_subject = subject;
_action = action;
}

/// <inheritdoc/>
protected internal override void OnReceiveMessage(ModMessage message)
{
if (message.Subject == _subject)
{
_action.Invoke(message.Contents);
}
}
}
29 changes: 29 additions & 0 deletions Nautilus/Utility/ModMessages/GlobalMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Nautilus.Utility.ModMessages;

internal class GlobalMessage
{
// the message's data

private ModMessage Message { get; }

// a list of all the addresses that have already received the message

private readonly List<string> _sentAddresses = new();

public GlobalMessage(ModMessage message)
{
Message = message;
}

public bool TrySendMessageToInbox(ModInbox inbox)
{
if (!inbox.AcceptsGlobalMessages || !inbox.IsAcceptingMessages || _sentAddresses.Contains(inbox.Address))
return false;

inbox.ReceiveMessage(Message);
_sentAddresses.Add(inbox.Address);
return true;
}
}
99 changes: 99 additions & 0 deletions Nautilus/Utility/ModMessages/ModInbox.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;

namespace Nautilus.Utility.ModMessages;

/// <summary>
/// An object with an address. Receives mail and allows it to be read at any time. Any held messages (messages sent before this inbox was created) will not be read
/// until the <see cref="ReadAnyHeldMessages"/> method is called.
/// </summary>
public class ModInbox
{
private List<ModMessageReader> _messageReaders = new List<ModMessageReader>();

/// <summary>
/// The address of this inbox. Conventionally should match the mod's GUID.
/// </summary>
public string Address { get; }

/// <summary>
/// If <see langword="false"/>, this inbox will not automatically read messages and will instead put any received messages on hold. If you are setting this property to
/// <see langword="true"/> at a later time, you still need to call <see cref="ReadAnyHeldMessages"/> to catch up.
/// </summary>
public bool IsAcceptingMessages { get; set; }

/// <summary>
/// Determines whether this inbox can receive global messages or not.
/// </summary>
public bool AcceptsGlobalMessages { get; set; }

/// <summary>
/// Constructs an inbox with the given <paramref name="address"/>.
/// </summary>
/// <param name="address">The address of this inbox. Other mods will use this string to contact this mod. Conventionally should match the mod's GUID. This parameter should NOT be
/// changed if any other mod is already using it!</param>
/// <param name="acceptsGlobalMessages">Determines whether this inbox can receive global messages or not.</param>
/// <param name="acceptingAllMessages">If <see langword="false"/>, this inbox will not automatically read messages and will instead put any received messages on hold.</param>
public ModInbox(string address, bool acceptsGlobalMessages = false, bool acceptingAllMessages = true)
{
Address = address;
AcceptsGlobalMessages = acceptsGlobalMessages;
IsAcceptingMessages = acceptingAllMessages;
}

/// <summary>
/// Adds an object that reads and handles any received messages.
/// </summary>
/// <param name="reader">The instance of the <see cref="ModMessageReader"/> to register.</param>
public void AddMessageReader(ModMessageReader reader)
{
_messageReaders.Add(reader);
}

internal void ReceiveMessage(ModMessage message)
{
foreach (var reader in _messageReaders)
{
try
{
reader.OnReceiveMessage(message);
}
catch (Exception e)
{
InternalLogger.Error("Exception caught in messaging system: " + e);
}
}
}

internal bool ReceiveDataRequest(ModMessage message, out object returnValue)
{
foreach (var reader in _messageReaders)
{
try
{
if (reader.TryHandleDataRequest(message, out returnValue))
return true;
}
catch (Exception e)
{
InternalLogger.Error("Exception caught in messaging system: " + e);
}
}
returnValue = null;
return false;
}

/// <summary>
/// Reads any messages that were sent to this address before the inbox was created. This will NOT do anything if <see cref="IsAcceptingMessages"/> is <see langword="false"/>!!!
/// </summary>
public void ReadAnyHeldMessages()
{
if (!IsAcceptingMessages)
{
InternalLogger.Warn($"Calling ReadAnyHeldMessages on inbox '{Address}' when it is not accepting messages!");
return;
}

ModMessageSystem.SendHeldMessagesToInbox(this);
}
}
35 changes: 35 additions & 0 deletions Nautilus/Utility/ModMessages/ModMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Nautilus.Utility.ModMessages;

/// <summary>
/// An instance of a message that is either sent instantly or held until received.
/// </summary>
public readonly struct ModMessage
{
/// <summary>
/// The address of the <see cref="ModInbox"/> that the message will go to. In C# terms, this is analogous to the class name.
/// </summary>
public string Recipient { get; }

/// <summary>
/// The subject of the message. Determines the purpose of a message. In C# terms, this is analogous to the method name.
/// </summary>
public string Subject { get; }

/// <summary>
/// Any arbitrary data sent through the message. Optional. In C# terms, this is analogous to the method's parameters.
/// </summary>
public object[] Contents { get; }

/// <summary>
/// Creates an instance of a message.
/// </summary>
/// <param name="recipient">The address of the <see cref="ModInbox"/> that the message will go to. In C# terms, this is analogous to the class name.</param>
/// <param name="subject">The subject of the message. Determines the purpose of a message. In C# terms, this is analogous to the method name.</param>
/// <param name="contents">Any arbitrary data sent through the message. Optional. In C# terms, this is analogous to the method's parameters.</param>
public ModMessage(string recipient, string subject, object[] contents)
{
Recipient = recipient;
Subject = subject;
Contents = contents;
}
}
24 changes: 24 additions & 0 deletions Nautilus/Utility/ModMessages/ModMessageReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Nautilus.Utility.ModMessages;

/// <summary>
/// The base class of any object that receives mod messages and handles them.
/// </summary>
public abstract class ModMessageReader
{
/// <summary>
/// Called when any message is received.
/// </summary>
protected internal abstract void OnReceiveMessage(ModMessage message);

/// <summary>
/// Called when data is requested. Similar to a normal message, but has a return value. Unlike normal messages, data requests can NOT be held.
/// </summary>
/// <param name="message">The basic message data for this request.</param>
/// <param name="returnValue">The object that is returned from this method, if any. Otherwise should be <see langword="default"/>.</param>
/// <returns>True if this method is willing to respond to the message's particular subject, false otherwise. If TRUE is returned, all other readers will be ignored.</returns>
protected internal virtual bool TryHandleDataRequest(ModMessage message, out object returnValue)
{
returnValue = default;
return false;
}
}
115 changes: 115 additions & 0 deletions Nautilus/Utility/ModMessages/ModMessageSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Collections.Generic;

namespace Nautilus.Utility.ModMessages;

/// <summary>
/// A messaging system for cross-mod communication with <see cref="ModInbox"/> instances. Allows for ultra-soft dependencies and attempts to eliminate race conditions.
/// </summary>
public static class ModMessageSystem
{
// address - inbox
private static Dictionary<string, ModInbox> _inboxes = new Dictionary<string, ModInbox>();

// recipient - message
private static Dictionary<string, List<ModMessage>> _heldMessages = new Dictionary<string, List<ModMessage>>();

private static List<GlobalMessage> _globalMessages = new List<GlobalMessage>();

/// <summary>
/// Sends a single message to a <see cref="ModInbox"/>. If the message is not read immediately, it will be held until read.
/// </summary>
/// <param name="recipient">The address of the <see cref="ModInbox"/> that the message will go to. In C# terms, this is analogous to the class name.</param>
/// <param name="subject">The subject of the message. Determines the purpose of a message. In C# terms, this is analogous to the method name.</param>
/// <param name="contents">Any arbitrary data sent through the message. Optional. In C# terms, this is analogous to the method's parameters.</param>
public static void Send(string recipient, string subject, params object[] contents)
{
Send(new ModMessage(recipient, subject, contents));
}

/// <summary>
/// Sends a global message to every <see cref="ModInbox"/> that exists, and even to ones that will exist in the future.
/// If a message is not read immediately by any inbox, it will be held until read.
/// </summary>
/// <param name="subject">The subject of the message. Determines the purpose of a message. In C# terms, this is analogous to the method name.</param>
/// <param name="contents">Any arbitrary data sent through the message. Optional. In C# terms, this is analogous to the method's parameters.</param>
public static void SendGlobal(string subject, params object[] contents)
{
var globalMessage = new GlobalMessage(new ModMessage(null, subject, contents));
foreach (var inbox in _inboxes.Values)
{
globalMessage.TrySendMessageToInbox(inbox);
}
_globalMessages.Add(globalMessage);
}

/// <summary>
/// Sends a single message to a <see cref="ModInbox"/>. If the message is not read immediately, it will be held until read.
/// </summary>
/// <param name="messageInstance">The message to send.</param>
public static void Send(ModMessage messageInstance)
{
if (_inboxes.TryGetValue(messageInstance.Recipient, out var inbox) && inbox.IsAcceptingMessages)
{
inbox.ReceiveMessage(messageInstance);
return;
}

// add to held messages instead:

if (!_heldMessages.TryGetValue(messageInstance.Recipient, out var heldMessageList))
_heldMessages.Add(messageInstance.Recipient, new List<ModMessage>());

heldMessageList.Add(messageInstance);
}

/// <summary>
/// <para>Sends a single message to a <see cref="ModInbox"/> and attempts to receive a value.</para>
/// <para>If the message is not read immediately (i.e. the inbox is closed or has not been created yet), it will be DELETED, and not held!</para>
/// </summary>
/// <param name="messageInstance">The message to send.</param>
/// <param name="result">The data that is received, if any.</param>
/// <returns>True if any <see cref="ModMessageReader"/> on the receiving end handled the message and returned a value.</returns>
public static bool SendDataRequest(ModMessage messageInstance, out object result)
{
// if the message responds immediately:

if (_inboxes.TryGetValue(messageInstance.Recipient, out var inbox) && inbox.IsAcceptingMessages && inbox.ReceiveDataRequest(messageInstance, out result))
{
return true;
}

// otherwise, who cares? just return false

result = default;
return false;
}

/// <summary>
/// Registers an inbox so that it can receive mail. Please note that this does NOT automatically read any messages on the <paramref name="inbox"/> that were sent before it was
/// registered. For that you must call its <see cref="ModInbox.ReadAnyHeldMessages"/> method.
/// </summary>
/// <param name="inbox">The inbox to register.</param>
public static void RegisterInbox(ModInbox inbox)
{
_inboxes[inbox.Address] = inbox;
}

internal static void SendHeldMessagesToInbox(ModInbox inbox)
{
// this is a necessary check for the sake of consistency
if (!inbox.IsAcceptingMessages)
return;

if (_heldMessages.TryGetValue(inbox.Address, out var messageList))
{
foreach (var message in messageList)
{
inbox.ReceiveMessage(message);
}
}
foreach (var globalMessage in _globalMessages)
{
globalMessage.TrySendMessageToInbox(inbox);
}
}
}