using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using SteamSDK;
using Torch.Server.ViewModels;
using VRage.Game;
using VRage.Library.Collections;
using VRage.Network;
using VRage.Utils;
namespace Torch.Server
{
///
/// Provides a proxy to the game's multiplayer-related functions.
///
public class MultiplayerManager
{
public event Action PlayerJoined;
public event Action PlayerLeft;
public event Action ChatMessageReceived;
public MTObservableCollection PlayersView { get; } = new MTObservableCollection();
public MTObservableCollection ChatView { get; } = new MTObservableCollection();
public PlayerInfo LocalPlayer { get; private set; }
private TorchServer _server;
internal MultiplayerManager(TorchServer server)
{
_server = server;
_server.Server.SessionLoaded += OnSessionLoaded;
}
public void KickPlayer(ulong steamId) => _server.Server.BeginGameAction(() => MyMultiplayer.Static.KickClient(steamId));
public void BanPlayer(ulong steamId, bool banned = true)
{
_server.Server.BeginGameAction(() =>
{
MyMultiplayer.Static.BanClient(steamId, banned);
if (_gameOwnerIds.ContainsKey(steamId))
MyMultiplayer.Static.BanClient(_gameOwnerIds[steamId], banned);
});
}
///
/// Send a message in chat.
///
public void SendMessage(string message)
{
MyMultiplayer.Static.SendChatMessage(message);
ChatView.Add(new ChatItemInfo(LocalPlayer, message));
}
private void OnSessionLoaded()
{
LocalPlayer = new PlayerInfo(MyMultiplayer.Static.ServerId) { Name = "Server", State = ConnectionState.Connected };
MyMultiplayer.Static.ChatMessageReceived += OnChatMessage;
MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft;
MySession.Static.Players.PlayerRequesting += OnPlayerRequesting;
//TODO: Move these with the methods?
RemoveHandlers();
SteamSDK.SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse;
SteamSDK.SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus;
_members = (List)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
_waitingForGroup = (HashSet)typeof(MyDedicatedServerBase).GetField("m_waitingForGroup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
}
private void OnChatMessage(ulong steamId, string message, ChatEntryTypeEnum chatType)
{
var player = PlayersView.FirstOrDefault(p => p.SteamId == steamId);
if (player == null || player == LocalPlayer)
return;
var info = new ChatItemInfo(player, message);
ChatView.Add(info);
ChatMessageReceived?.Invoke(info);
}
///
/// Invoked when a client logs in and hits the respawn screen.
///
private void OnPlayerRequesting(PlayerRequestArgs args)
{
var steamId = args.PlayerId.SteamId;
var player = new PlayerInfo(steamId) {State = ConnectionState.Connected};
PlayersView.Add(player);
PlayerJoined?.Invoke(player);
}
private void OnClientKicked(ulong steamId)
{
OnClientLeft(steamId, ChatMemberStateChangeEnum.Kicked);
}
private void OnClientLeft(ulong steamId, ChatMemberStateChangeEnum stateChange)
{
var player = PlayersView.FirstOrDefault(p => p.SteamId == steamId);
if (player == null)
return;
player.State = (ConnectionState)stateChange;
PlayersView.Remove(player);
PlayerLeft?.Invoke(player);
}
//TODO: Split the following into a new file?
//These methods override some Keen code to allow us full control over client authentication.
//This lets us have a server set to private (admins only) or friends (friends of all listed admins)
private List _members;
private HashSet _waitingForGroup;
private HashSet _waitingForFriends;
private Dictionary _gameOwnerIds = new Dictionary();
///
/// Removes Keen's hooks into some Steam events so we have full control over client authentication
///
private static void RemoveHandlers()
{
var eventField = typeof(GameServer).GetField("ValidateAuthTicketResponse", BindingFlags.NonPublic | BindingFlags.Instance);
var eventDel = eventField?.GetValue(SteamServerAPI.Instance.GameServer) as MulticastDelegate;
if (eventDel != null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_ValidateAuthTicketResponse")
{
SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse -= handle as ValidateAuthTicketResponse;
}
}
}
eventField = typeof(GameServer).GetField("UserGroupStatus", BindingFlags.NonPublic | BindingFlags.Instance);
eventDel = eventField?.GetValue(SteamServerAPI.Instance.GameServer) as MulticastDelegate;
if (eventDel != null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_UserGroupStatus")
{
SteamServerAPI.Instance.GameServer.UserGroupStatus -= handle as UserGroupStatus;
}
}
}
}
//Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, AuthSessionResponseEnum response, ulong ownerSteamID)
{
MyLog.Default.WriteLineAndConsole($"{Logger.Prefix} Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}");
if (steamID != ownerSteamID)
{
MyLog.Default.WriteLineAndConsole($"User {steamID} is using a game owned by {ownerSteamID}. Tracking...");
_gameOwnerIds[steamID] = ownerSteamID;
if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID))
{
MyLog.Default.WriteLineAndConsole($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}...");
UserRejected(steamID, JoinResult.BannedByAdmins);
BanPlayer(steamID, true);
}
}
if (response == AuthSessionResponseEnum.OK)
{
if (MySession.Static.MaxPlayers > 0 && _members.Count - 1 >= MySession.Static.MaxPlayers)
{
UserRejected(steamID, JoinResult.ServerFull);
}
else if (MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) || MySandboxGame.ConfigDedicated.Administrators.Contains(MyDedicatedServerBase.ConvertSteamIDFrom64(steamID)))
{
UserAccepted(steamID);
}
else if (MySandboxGame.ConfigDedicated.GroupID == 0)
{
switch (MySession.Static.OnlineMode)
{
case MyOnlineModeEnum.PUBLIC:
UserAccepted(steamID);
break;
case MyOnlineModeEnum.PRIVATE:
UserRejected(steamID, JoinResult.NotInGroup);
break;
case MyOnlineModeEnum.FRIENDS:
//TODO: actually verify friendship
UserRejected(steamID, JoinResult.NotInGroup);
break;
}
}
else if (SteamServerAPI.Instance.GetAccountType(MySandboxGame.ConfigDedicated.GroupID) != AccountType.Clan)
{
UserRejected(steamID, JoinResult.GroupIdInvalid);
}
else if (SteamServerAPI.Instance.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
{
// Returns false when there's no connection to Steam
_waitingForGroup.Add(steamID);
}
else
{
UserRejected(steamID, JoinResult.SteamServersOffline);
}
}
else
{
JoinResult joinResult = JoinResult.TicketInvalid;
switch (response)
{
case AuthSessionResponseEnum.AuthTicketCanceled:
joinResult = JoinResult.TicketCanceled;
break;
case AuthSessionResponseEnum.AuthTicketInvalidAlreadyUsed:
joinResult = JoinResult.TicketAlreadyUsed;
break;
case AuthSessionResponseEnum.LoggedInElseWhere:
joinResult = JoinResult.LoggedInElseWhere;
break;
case AuthSessionResponseEnum.NoLicenseOrExpired:
joinResult = JoinResult.NoLicenseOrExpired;
break;
case AuthSessionResponseEnum.UserNotConnectedToSteam:
joinResult = JoinResult.UserNotConnected;
break;
case AuthSessionResponseEnum.VACBanned:
joinResult = JoinResult.VACBanned;
break;
case AuthSessionResponseEnum.VACCheckTimedOut:
joinResult = JoinResult.VACCheckTimedOut;
break;
}
UserRejected(steamID, joinResult);
}
}
private void UserGroupStatus(ulong userId, ulong groupId, bool member, bool officier)
{
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Remove(userId))
{
if ((member || officier))
{
UserAccepted(userId);
}
else
{
UserRejected(userId, JoinResult.NotInGroup);
}
}
}
private void UserAccepted(ulong steamId)
{
//TODO: Raise user joined event here
typeof(MyDedicatedServerBase).GetMethod("UserAccepted", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId});
}
private void UserRejected(ulong steamId, JoinResult reason)
{
typeof(MyDedicatedServerBase).GetMethod("UserRejected", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId, reason});
}
}
}