Mirror
Mirror copied to clipboard
More modding support and customization - example for my response message
Hello, some of you knows me already from discord. They like my stuff what I try also they hate me because I break mirror or telepathy sometimes. I very often push things to the limit to analyse it and also to achieve my goal with hacky tricks. So here I face another problem and want to include the idea.
It would be cool if mirror has some possibilities to change few core stuff from the outside or make possible to add extensions without touching the core files of mirror. In this case: Short: Allow Custom message handler. So mirror reads the msgid but does not deserialize it. It just forwards the reader and the id to a custom from user made message system.
Long: Why we would need this? As example I take my callback message system which I call network response message (Don't know better name for now). The usage:
NetworkResponse.Client.Send(new SelectTeamMessage() { team = team }, (response) => {
if ( response.state == ResponseState.OK )
{
Debug.Log($"Team changed to {team}")
}
});
The basic idea is that I can send message and get response without to make the messagebase classes (for the developer, the core itself does it).
Why not use NetworkClient.RegisterHandler/NetworkServer.RegisterHandler and why they would be useful?
Well an example from my latest project: UI
selectA.onClick.AddListener(() => GameModeCommunicator.RequestTeamSelect(Team.Human, OnRequestTeam));
selectB.onClick.AddListener(() => GameModeCommunicator.RequestTeamSelect(Team.Alien, OnRequestTeam));
As you see it makes my workflow easier. The code above will call method with return values if button is clicked. The RequestTeamSelect (real) method is just this: GameModeCommunicator
public static void RequestTeamSelect(Team team, Action<Team, bool> callback)
{
NetworkResponse.Client.Send(new SelectTeamMessage() { team = team }, (response) => callback.Invoke(team, response.state == ResponseState.OK));
}
I do not need to use NetworkClient.RegisterHandler here. Als you can use same request for multiple and different things.
Of course the server would need to register SelectTeamMessage but the handler can send as answer ResponseMessage to the client back. The ResponseMessage and SelectTeamMessage are an inheritance of RequestMessageBase which cointains responseId. Later the client can identify which handler should be invoked.
But lets see first how it would look if we wouldn't use this technique (in my real project). its splitted into two scripts. UI script and GameModeCommunicator script.
GameModeCommunicator
public class SelectTeamResponseMessage : MessageBase
{
public Team team;
public bool IsOk; // could be enum but ok for this demonstration
}
static void RegisterClientHandler()
{
NetworkClient.RegisterHandler<SelectTeamResponseMessage>(Client_OnSelectTeamResponse);
}
public static event Action<Team, bool> OnSelectTeamResponseEvent;
static void Client_OnSelectTeamResponse(NetworkConnection conn, SelectTeamResponseMessage msg)
{
OnSelectTeamResponseEvent?.Invoke(msg.team, msg.IsOk);
}
public static void RequestTeamSelect2(Team team)
{
NetworkClient.Send(new SelectTeamMessage() { team = team });
}
UI
selectB.onClick.AddListener(() => GameModeCommunicator.RequestTeamSelect2(Team.Alien));
GameModeCommunicator.OnSelectTeamResponseEvent += OnRequestTeam;
Maybe you saw it already. I am forced to make a new delegate inside the script itself, new messagebase class and also I cannot work with the team variable so I was forced to use the SelectTeamResponseMessage and declare there. I could otherwise also make that the UI can remember what team we requested but what if you click twice on different team buttons but the server has a delay?
Now lets see what is changed if we use the "extension". GameModeCommunicator
public static void RequestTeamSelect(Team team, Action<Team, bool> callback)
{
NetworkResponse.Client.Send(new SelectTeamMessage() { team = team }, (response) => callback.Invoke(team, response.state == ResponseState.OK));
}
UI
selectB.onClick.AddListener(() => GameModeCommunicator.RequestTeamSelect(Team.Alien, OnRequestTeam));
Huge difference. But of course the core is behind the static NetworkResponse.Client class which takes lots of work.
So you ask now "what is the problem if it works", right?
Lets see the server Reponse part - in this case the server:
static void RegisterServerHandler()
{
NetworkServer.RegisterHandler<SelectTeamMessage>(Server_OnClienSelectTeam);
}
static void Server_OnClienSelectTeam(NetworkConnection conn, SelectTeamMessage msg)
{
ResponseMessage responseMsg = new ResponseMessage();
responseMsg.responseId = msg.responseId;
responseMsg.state = ResponseState.Failed;
if (NetworkServer.connections.ContainsKey(conn.connectionId))
{
if(conn.identity.TryGetComponent(out Player player))
{
if (player.team == msg.team)
{
responseMsg.message = "Already in same team";
} else
{
responseMsg.state = ResponseState.OK;
player.team = msg.team;
}
}
}
conn.Send(responseMsg);
}
As you see this can work for basic stuff. Very important: After the callback handler is invoked on the client the callback handler is removed from the list. Timeout is not implemented yet.
I could dig deeper to even remove responseId. For this I prepared Server.ResponseClient(conn, request, response) while the request is SelectTeamMessage. I am ok for this. Problem developer could use conn.Send and then it does not work. Same for normal messages but its ok. Maybe you do not want a response but still use the message.
Now. I am perfectionist. I want to have more automatic way and also allow more custom messages. So I try to write my own handler for the message but I always stuck at the same problem. Want I tried to achieve is the handler should not know NetworkConnection but the core should now. So if I use Server.ResponseSend(response) it will know who will get the message. The client would send first this before that happens
NetworkResponse.Client.Send<TestResponseMessage, TestSendMessage>(new TestSendMessage() { num = 10 }, (response) =>
{
Debug.Log(response.customMessage);
});
Here I would say I request via TestSendMessage and want to get TestResponseMessage as response message. So in this case TestResponseMessage has new field customMessage (its string). Thanks for the auto gen code it helps to achieve most things very easy. But the idea does not work because mirror has closed door.
So basic idea to avoid the "door" was to serialize all what I need. But on the other side mirror does not need to deserialize everything. Just the basic message. The rest of the deserialization I would care with that custom code. But I cannot because I can't touch NetworkReader. What I can do is I can make public NetworkReader in the message itself and assign while it gets deserialized but it does not look the right way.
But even if I would this.. I would need to use ResponseMessage and then cast to right type. That will not work because I need to touch NetworkWriter as well to write values which does ResponseMessage not contain but the TestResponseMessage does it.
Ok then I had an idea. Why not use Register and Unregister of NetworkClient/Server for each request. I even would save time without making my own. There we go again. I am not able to see if the handler or better the msgID is registered or not. Also this does not work if the request was requested twice. The old handler would get replaced and you would invoke the response via over the new handler.
The last idea would need to modify mirrors core script. What if we would have something like NetworkServer.RegisterRouteMessage<TestResponseMessage>(OnTestResponseMessage). Basically mirror would read the msgid and also look if this is routed so do not deserialize. From here a custom script could write own message handler and deserialization.
I also thought to use transport itself but if I am not wrong mirror always would wonder what message came in but its not registered but also it make conflicts if the msgid would be same.
When I made own matchmaking I used the transport itself. I faced few problems but I was lucky you implemented in the latest update (I think it was in version 13) the things what I needed.
So it would be very nice if there would be kind of these supports in the feature. I its not highly requested thing but would make even easier to develope few custom extensions.
An example with your NetworkAuthenticator. This also could work without to have by default because NetworkManager offers many posibility.
Back to the response message system. What it does is, if the sender send a message it will get a id an the id which will get to receiver and the receiver will send back with same id. The sender will save the call back with the id and later we can check if the id exists. If yes, so invoke. So it works to both direction. For now I just use ResponseMessage with string and enum field and return request was ok or not.