โ ๏ธ This is a requirement before any further games can be added.
Revision
Hierarchy
The way the hierarchy of how connections are established feels a bit off, as players can have direct message channels that can also be subscribed to a game server. The new approach makes both players and connections inherit from a base class called IReceiver
, which is a base class that subscribes to an IBroadcast
, the core foundation of what is displayed and determines the game state of a receiver. In this approach, you can now detect if a receiver is a direct message channel, and games can now prohibit the initialization of receiver connections if they are initialized from a direct message channel.
The new structure framework is as shown:
public interface IBroadcast { }
public interface IReceiver { }
public interface IPlayer : IReceiver { }
public interface IConnection : IReceiver { }
Groups
Because of this new change style, receiver grouping is now handled by the subscription. As a result, specifying group IDs are no longer required.
Server Structure
The game server structure can remain as is, but instead adding a new method called GetReceivers()
, which would return the summed result of all receivers, which include players now. This can be used to group up multiple channels to be subscribed to a single broadcast, so that updating the broadcast updates all receivers connected to it instead. Likewise, each receiver can receive individual messages if needed, such as notifying a single player about a specific event in a game that nobody else should see.
Broadcast Initialization
Since broadcasts are now the primary focus for handling content between receivers, building a collection of broadcasts are now going to be in a class called GameChannel
, which stores the previous DisplayContent
and InputController
as mentioned before. This way, all new broadcast copies are initialized with a GameChannel
reference, from which their contents are copied onto the broadcast, so that each broadcast can update and modify the content within.
Original Approach
Framework
The GameBase
class is currently a bit sloppy on how it handles things, and is even missing plenty of handling methods for certain events. This includes:
- A player joining mid-game
- A player leaving mid-game
- A player being removed for idling too long
- A server connection being removed mid-game
- A game being soft-locked (nothing is updated for 3-5 minutes)
Tasks
The new approach for what each method should return is now going to ensure that all methods return as a Task
, to retain thread safety and locking for games that can potentially freeze. Some may return results (an example being Task<IReadOnlyList<TPlayer> OnBuildPlayers(in IEnumerable<Player>)
, but this new approach will be handy for future use when it comes to asynchronous handling.
Variant Generics
The new design of the GameBase
would include methods relating to these features, which would require the GameManager
to now provide support for said methods. Likewise, to further support the new game property concept, the way player data is handled is going to have to be modified. As such, the GameBase
class will now have a generic variant, utilizing a new IPlayerData
interface to work with on it's inferred version as well. An example of this would be GameBase<TriviaPlayer>
, where TriviaPlayer
would store all of the properties that is needed for the TriviaGame
class, while at the same time, keeping the other methods that already exist for flexibility. Building players in this new approach become much easier to handle.
Sessions
Due to this new approach, game sessions as a whole can easily just be the game itself. As such, the GameSession
class is going to merge with the generic GameBase
class to provide all of the methods related to queuing actions, specifying spectator frequency, activity bar, and player data. This should make it much easier to handle in games.
Connections
The way game connections are handled are a bit messy at the moment, as none of them represent or inherit a base interface. Instead of ServerConnection
and PlayerChannel
being separate, the new approach is to implement an interface called IConnection
, which will specify all required properties for every connection. The structure of IConnection
is as follows:
public interface IConnection
{
// If a connection is disabled, is it completely ignored from updating and input handling
bool Disabled { get; }
// These are used to determine which broadcast the connection is pointing towards
string Group { get; }
int Frequency { get; }
GameState State { get; }
// This would be base representative values that the connection can modify at any point in time.
// These values would be marked as protected, as these don't need to be exposed to a common use case
TextDisplay Display { get; }
InputController Controller { get; }
// These are base endpoint values that are required for each connection
Discord.IMessageChannel Channel { get; } // The channel that the message is stored in, used to recover in case of deletion
Discord.IUserMessage Message { get; } // The message that this connection is displaying
bool CanSendMessages { get; } // Determines if a connection is allowed to receive messages
// These methods indirectly handle the connection display content
Task<bool> UpdateAsync();
Task<bool> DestroyAsync();
Task<bool> ReplaceAsync();
}
The new TextDisplay
and InputController
classes are specified in further depth below. The goal of this approach is to be able to create a base framework for connections to further simplify approaches.
Players
While the current state of players is decent, there are a couple of areas that it falls short in:
- Sending messages to players becomes a hassle, as there's no real easy way to allow them to have a content or broadcast
- Determining if a player can support direct messages isn't handled before the game, which can lead to issues
The new approach would be to make the Player
class inherit IConnection
. While this sounds odd at first, this would now allow all players to easily be able have a direct channel content override. This can now be used to check if a player can receive messages, which can be toggled off at any point in time that a message being sent is returned as an HTTP Error 50007: Forbidden
. This value can then be used for verification before a game starts that requires all players to have direct messages open. If any player ends up setting it up as forbidden mid-game, the player will be kicked immediately, with the new game method OnPlayerRemoved
being invoked in place of it to handle the player's properties beforehand. A couple of methods originally specified in PlayerChannel
will be transferred over to the Player
class now to reduce overall complexity.
To further improve handling for games, a new interface called IPlayerData
will be included. This works exactly the same as PlayerData
, but instead of having the properties be stored in a List<GameProperty>
, the values would now be stored in the inheriting class, where all properties that are marked are included in the set, now returning an IReadOnlyList<GameProperty>
instead. The positive to this approach is that handling player data during games will become much easier.
Broadcasts
This overhaul is going to completely revamp server connections that point to broadcasts are handled. Due to the ability of retaining data for a class by object reference, referencing broadcasts become much easier to implement. The new approach for broadcasts (aside from renaming it to better fit from DisplayBroadcast
to GameBroadcast
) is to essentially be a compact storage for what is currently being displayed for this broadcast alongside what everyone connected to this broadcast can control. While that is currently the case, it isn't modular at the moment.
This would modify GameBroadcast
to store the visual display in TextDisplay
(previously DisplayContent
), and to store all input reading in InputController
(previously a property List<IInput> Inputs
). The same approach can then be applied to all generic IConnection
values now, so that each individual connection can explicitly specify their own displays and input. If either is left null, it will instead refer to the broadcast that the connection is currently pointing to.
The TextDisplay
class will follow this structure:
public class TextDisplay
{
public IEnumerable<ITextComponent> Components { get; }
// ContentOverride is used to override what the components are currently displaying
public string ContentOverride { get; set; }
// This method is where all of the components are drawn
public override string ToString();
}
Likewise, the InputController
class will follow this structure:
public class InputController
{
// As before, this is the collection of possible inputs that this controller stores
public IEnumerable<IInput> Inputs { get; }
// This is used to completely prevent input from being read
public bool Disabled { get; set; }
}
Properties
How game properties are stored and retrieved become very tedious. As an example of how annoying it can get, here's an example:
OnExecute = delegate(InputContext ctx)
{
var data = ctx.Session.DataOf(ctx.Invoker.Id);
if (data.ValueOf<bool>("has_answered"))
return;
var answerSelected = CurrentAnswers.ElementAt(0);
data.SetValue("has_answered", true);
if (answerSelected.IsCorrect)
{
data.AddToValue("streak", 1);
data.AddToValue(TriviaVars.TotalCorrect, 1);
}
else
{
data.ResetProperty("streak");
}
ctx.Session.AddToValue("players_answered", 1);
data.SetValue("answer_position", ctx.Session.ValueOf<int>("players_answered"));
ctx.Session.InvokeAction("try_get_question_result");
}
The reason for this is that all values that a game requires is stored in the GameSession
, with its original purpose being to remove the requirement of needing to keep a GameBase
in memory. It was obvious that ended up not being case, which completely removes the need for this in the first place. After weighing the pros and cons, I've found that:
+ Games don't need to create custom classes for each type, as all values are stored in PlayerData
+ The complexity of type inheritance is simplified, as type casting doesn't really need to occur in relation to games overall
- Retrieving properties become drastically more difficult than it should be, as a method needs to be called on each occasion
- Retrieving properties immediately require casting for each stored value, which can easily cause errors on minor invalid type specification
To help keep the positives of this approach in play, I've decided to go with an Attribute-based approach, which would allow the creator of the game to explicitly specify which properties they would want to be able to retrieve globally by marking the property with a IncludePropertyAttribute
.
To further improve upon this framework, this would include ReadOnlyAttribute
as another feature to make sure that the property that is read is read-only, meaning that it cannot be modified during a game session, but can be modified before-hand.
Other methods would be implemented, such as PropertyExists(string)
to check if a property exists before comparing, and PropertyEquals(string, object)
to indirectly check if the property equals the specified value without explicit casting before returning.
This can be fine-tuned to throw an error on an invalid type specification, but by default, it would simply return false if invalid. Sadly, casting is still a requirement for methods such as ValueOf<T>(string)
, since it can't be implicitly inferred. The generic ValueOf(string)
is still a plausible approach for those who can do something else with the value though.
To clean up the game base framework, it would split GameBase
into two variations, GameBase
, and GameBase<TPlayer>
. The properties that the game stores would now be specified in the class itself, but as mentioned earlier, can be marked as a property that is retrieved when wanting to view a collection of properties. Here is a rough example of this approach:
public sealed class TriviaGame : GameBase<TriviaPlayer>
{
[IncludeProperty("players_answered")]
public int PlayersAnswered { get; private set; }
}
Now instead of having to use session.ValueOf<int>("players_answered")
, this is now a simple PlayersAnswered
. The goal of this approach is to reduce complexity when handling the game mechanics itself, while at the same time, retaining the flexibility of handling properties outside of the GameBase
classes for other features, such as criteria for a challenge.
Returning to that previously verbose method, it can reduced down to this now:
OnExecute = delegate(InputContext ctx)
{
// TriviaPlayer would inherit a form of IPlayerData, which would still provide the methods relating to game properties
TriviaPlayer data = ctx.Session.DataOf(ctx.Invoker.Id);
if (data.HasAnswered)
return;
var selectedAnswer = CurrentAnswers.ElementAt(0);
data.HasAnswered = true;
data.SetValue("is_correct", answerSelected.IsCorrect);
if (selectedAnswer.IsCorrect)
{
data.Streak++;
data.TotalCorrect++;
}
else
{
data.Streak = 0;
}
PlayersAnswered++;
data.AnswerPosition = PlayersAnswered;
ctx.Session.InvokeAction("try_get_question_result");
}