using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
namespace TcgEngine.Server
{
///
/// Main server script for the matchmaker
/// will receive player request and then match players together and send them the game uid and game url to connect to
///
public class ServerMatchmaker : MonoBehaviour
{
[Header("Matchmaker")]
public string[] servers;
private Dictionary client_list = new Dictionary(); //List of clients
private Dictionary matchmaking_players = new Dictionary(); //Get deleted every 20 sec
private Dictionary matched_players = new Dictionary(); //user_id -> match
private List valid_users = new List(); //temporary array
private float matchmake_timer = 0f;
private static ServerMatchmaker _instance;
protected virtual void Awake()
{
_instance = this;
Application.runInBackground = true;
}
protected virtual void Start()
{
TcgNetwork network = TcgNetwork.Get();
network.onClientJoin += OnClientConnected;
network.onClientQuit += OnClientDisconnected;
Messaging.ListenMsg("matchmaking", ReceiveMatchmaking);
Messaging.ListenMsg("matchmaking_list", ReceiveMatchmakingList);
Messaging.ListenMsg("match_list", ReceiveMatchList);
if (!network.IsActive())
{
network.StartServer(NetworkData.Get().port);
}
}
protected virtual void Update()
{
//Matchmaking
matchmake_timer += Time.deltaTime;
if (matchmake_timer > 20f)
{
matchmake_timer = 0f;
matchmaking_players.Clear(); //Delete and restart, to make sure you only keep recent players
}
}
protected virtual void OnClientConnected(ulong client_id)
{
ClientData iclient = new ClientData(client_id);
client_list[client_id] = iclient;
}
protected virtual void OnClientDisconnected(ulong client_id)
{
if (client_list.ContainsKey(client_id))
{
ClientData iclient = client_list[client_id];
if(iclient.username != null)
matchmaking_players.Remove(iclient.user_id);
client_list.Remove(client_id);
}
}
protected virtual void ReceiveMatchmaking(ulong client_id, FastBufferReader reader)
{
ClientData iclient = GetClient(client_id);
reader.ReadNetworkSerializable(out MsgMatchmaking msg);
if (iclient == null || string.IsNullOrWhiteSpace(msg.user_id) || string.IsNullOrWhiteSpace(msg.username))
return;
string user_id = msg.user_id;
bool is_refresh = msg.refresh;
iclient.user_id = msg.user_id;
iclient.username = msg.username;
//Restart matching
if (!is_refresh)
matched_players.Remove(user_id);
//Check if already matched
if (matched_players.ContainsKey(user_id))
{
MatchData match = matched_players[user_id];
if (!match.ended)
{
SendMatchmakingResponse(iclient, match, msg.group, match.players.Length); //Was already matched, return saved result!
return;
}
}
//Data
MatchPlayerData pdata = new MatchPlayerData();
pdata.user_id = msg.user_id;
pdata.username = msg.username;
pdata.group = msg.group;
pdata.elo_rank = msg.elo;
pdata.nb_players = msg.players;
//Add to matchking players
if (!matchmaking_players.ContainsKey(user_id))
matchmaking_players.Add(user_id, pdata);
//Start searching for other valid players
float wait_max = 20f;
int variance_max = 2000;
bool friendly = msg.group.StartsWith("u_");
float wait_timer = msg.time;
float wait_value = Mathf.Clamp01(wait_timer / wait_max);
int elo_variance = Mathf.RoundToInt(wait_value * variance_max);
valid_users.Clear();
valid_users.Add(pdata); //Add self
foreach (KeyValuePair opair in matchmaking_players)
{
string auser_id = opair.Key;
MatchPlayerData adata = opair.Value;
int diff = Mathf.Abs(adata.elo_rank - msg.elo);
bool same_group = adata.group == msg.group;
bool same_players = adata.nb_players == msg.players;
bool valid_elo = friendly || diff < elo_variance;
if (auser_id != user_id && valid_elo && same_group && same_players)
{
valid_users.Add(adata);
}
}
//Not enough players found, send current count
if (valid_users.Count < msg.players)
{
SendMatchmakingResponse(iclient, null, msg.group, valid_users.Count);
return; //Not enough valid users
}
//Match success, send result
string prefix = msg.group.Length >= 2 ? msg.group.Substring(0, 2) : "";
string game_code = prefix + GameTool.GenerateRandomID(12, 15);
string game_url = ""; //Empty url means it will use the default NetworkData url set on the client
if (servers.Length > 0)
game_url = servers[Random.Range(0, servers.Length)];
int pindex = 0;
MatchData nmatch = new MatchData(msg.group, game_code, game_url, msg.players);
foreach (MatchPlayerData vuser in valid_users)
{
if (pindex < nmatch.players.Length)
{
matchmaking_players.Remove(vuser.user_id);
matched_players[vuser.user_id] = nmatch;
nmatch.players[pindex] = vuser.username;
pindex++;
}
}
//Send response to current request
if (matched_players.ContainsKey(user_id))
{
SendMatchmakingResponse(iclient, nmatch, nmatch.group, nmatch.players.Length); //Just matched to new player!
}
}
protected virtual void SendMatchmakingResponse(ClientData iclient, MatchData match, string group, int players)
{
MatchmakingResult msg_match = new MatchmakingResult();
msg_match.success = match != null;
msg_match.players = players;
msg_match.group = group;
msg_match.game_uid = match != null ? match.game_uid : "";
msg_match.server_url = match != null ? match.server_url : "";
Messaging.SendObject("matchmaking", iclient.client_id, msg_match, NetworkDelivery.Reliable);
}
protected virtual void ReceiveMatchmakingList(ulong client_id, FastBufferReader reader)
{
reader.ReadNetworkSerializable(out MsgMatchmakingList msg);
List items = new List();
foreach (KeyValuePair pair in matchmaking_players)
{
if (string.IsNullOrEmpty(msg.username) || pair.Key == msg.username)
{
MatchPlayerData pdata = pair.Value;
MatchmakingListItem item = new MatchmakingListItem();
item.group = pdata.group;
item.user_id = pdata.user_id;
item.username = pdata.username;
items.Add(item);
}
}
MatchmakingList msg_list = new MatchmakingList();
msg_list.items = items.ToArray();
Messaging.SendObject("matchmaking_list", client_id, msg_list, NetworkDelivery.Reliable);
}
protected virtual void ReceiveMatchList(ulong client_id, FastBufferReader reader)
{
reader.ReadNetworkSerializable(out MsgMatchmakingList msg);
List items = new List();
foreach (KeyValuePair pair in matched_players)
{
if (!pair.Value.ended)
{
if (string.IsNullOrEmpty(msg.username) || Contains(pair.Value.players, msg.username))
{
MatchData pdata = pair.Value;
MatchListItem item = new MatchListItem();
item.group = pair.Value.group;
item.username = msg.username;
item.game_uid = pdata.game_uid;
item.game_url = pdata.server_url;
items.Add(item);
}
}
}
MatchList msg_list = new MatchList();
msg_list.items = items.ToArray();
Messaging.SendObject("match_list", client_id, msg_list, NetworkDelivery.Reliable);
}
private bool Contains(string[] users, string user)
{
foreach (string auser in users)
{
if (auser == user)
return true;
}
return false;
}
public void EndMatch(string uid)
{
foreach (KeyValuePair pair in matched_players)
{
if (pair.Value.game_uid == uid)
pair.Value.ended = true;
}
}
public ClientData GetClient(ulong client_id)
{
if (client_list.ContainsKey(client_id))
return client_list[client_id];
return null;
}
public ClientData GetClientByUser(string username)
{
foreach (KeyValuePair pair in client_list)
{
if (pair.Value.username == username)
return pair.Value;
}
return null;
}
public ulong ServerID { get { return TcgNetwork.Get().ServerID; } }
public NetworkMessaging Messaging { get { return TcgNetwork.Get().Messaging; } }
public static ServerMatchmaker Get()
{
return _instance;
}
}
public class MatchPlayerData
{
public string user_id;
public string username;
public string group;
public int elo_rank;
public int nb_players;
}
public class MatchData
{
public string group;
public string game_uid;
public string server_url;
public bool ended = false;
public string[] players;
public MatchData(string grp, string uid, string url, int players) { group = grp; game_uid = uid; server_url = url; this.players = new string[players]; }
}
}