first commit
This commit is contained in:
19
Attributes/PacketHandlerAttribute.cs
Normal file
19
Attributes/PacketHandlerAttribute.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class PacketHandlerAttribute : Attribute
|
||||
{
|
||||
public ushort packetId;
|
||||
|
||||
public PacketHandlerAttribute(ushort packetId)
|
||||
{
|
||||
this.packetId = packetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Attributes/TransmittableAttribute.cs
Normal file
14
Attributes/TransmittableAttribute.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class TransmittableAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
34
Constants.cs
Normal file
34
Constants.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
/// <summary>
|
||||
/// Kingdoms and Castles "Contants" (World instances, etc)
|
||||
///
|
||||
/// _T is a Trasnform
|
||||
/// _O is a GameObject
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly MainMenuMode MainMenuMode = GameState.inst.mainMenuMode;
|
||||
public static readonly PlayingMode PlayingMode = GameState.inst.playingMode;
|
||||
public static readonly World World = GameState.inst.world;
|
||||
|
||||
#region "UI"
|
||||
public static readonly Transform MainMenuUI_T = MainMenuMode.mainMenuUI.transform;
|
||||
public static readonly GameObject MainMenuUI_O = MainMenuMode.mainMenuUI;
|
||||
|
||||
/* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent;
|
||||
public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/
|
||||
|
||||
public static readonly Transform ChooseModeUI_T = MainMenuMode.chooseModeUI.transform;
|
||||
public static readonly GameObject ChooseModeUI_O = MainMenuMode.chooseModeUI;
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
16
Enums/Difficulty.cs
Normal file
16
Enums/Difficulty.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Enums
|
||||
{
|
||||
public enum Difficulty
|
||||
{
|
||||
Paxlon,
|
||||
Sommern,
|
||||
Vintar,
|
||||
Falle
|
||||
}
|
||||
}
|
||||
59
Enums/MenuState.cs
Normal file
59
Enums/MenuState.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Enums
|
||||
{
|
||||
public enum MenuState
|
||||
{
|
||||
Uninitialized,
|
||||
// Token: 0x040022F1 RID: 8945
|
||||
Menu,
|
||||
// Token: 0x040022F2 RID: 8946
|
||||
ChooseMode,
|
||||
// Token: 0x040022F3 RID: 8947
|
||||
ChooseDifficulty,
|
||||
// Token: 0x040022F4 RID: 8948
|
||||
NewMap,
|
||||
// Token: 0x040022F5 RID: 8949
|
||||
NameAndBanner,
|
||||
// Token: 0x040022F6 RID: 8950
|
||||
PauseMenu,
|
||||
// Token: 0x040022F7 RID: 8951
|
||||
SettingsMenu,
|
||||
// Token: 0x040022F8 RID: 8952
|
||||
Save,
|
||||
// Token: 0x040022F9 RID: 8953
|
||||
Load,
|
||||
// Token: 0x040022FA RID: 8954
|
||||
QuitConfirm,
|
||||
// Token: 0x040022FB RID: 8955
|
||||
ExitConfirm,
|
||||
// Token: 0x040022FC RID: 8956
|
||||
LoadError,
|
||||
// Token: 0x040022FD RID: 8957
|
||||
SendSave,
|
||||
// Token: 0x040022FE RID: 8958
|
||||
Credits,
|
||||
// Token: 0x040022FF RID: 8959
|
||||
Failure,
|
||||
// Token: 0x04002300 RID: 8960
|
||||
KeepDestroyed,
|
||||
// Token: 0x04002301 RID: 8961
|
||||
BannerSelect,
|
||||
// Token: 0x04002302 RID: 8962
|
||||
GameWorkshopUI,
|
||||
// Token: 0x04002303 RID: 8963
|
||||
RivalChoiceUI,
|
||||
// Token: 0x04002304 RID: 8964
|
||||
KingdomShareFromMenu,
|
||||
// Token: 0x04002305 RID: 8965
|
||||
KingdomShareFromGame,
|
||||
|
||||
ServerBrowser,
|
||||
|
||||
ServerLobby
|
||||
}
|
||||
}
|
||||
49
Enums/Packets.cs
Normal file
49
Enums/Packets.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Enums
|
||||
{
|
||||
public enum Packets
|
||||
{
|
||||
ClientConnected = 25,
|
||||
PlayerList = 26,
|
||||
ChatSystemMessage = 27,
|
||||
ChatMessage = 28,
|
||||
ServerSettings = 29,
|
||||
PlayerReady = 30,
|
||||
PlayerBanner = 31,
|
||||
KingdomName = 32,
|
||||
StartGame = 33,
|
||||
WorldSeed = 34,
|
||||
|
||||
|
||||
Building = 50,
|
||||
BuildingOnPlacement = 51,
|
||||
|
||||
World = 70,
|
||||
WorldPlace = 71,
|
||||
FellTree = 72,
|
||||
ShakeTree = 73,
|
||||
GrowTree = 74,
|
||||
UpdateConstruction = 75,
|
||||
SetSpeed = 76,
|
||||
CompleteBuild = 77,
|
||||
WorldPlaceBatch = 78,
|
||||
ChangeWeather = 79,
|
||||
ShowModal = 80,
|
||||
ServerHandshake = 81,
|
||||
SpawnSiegeDragon = 82,
|
||||
SpawnMamaDragon = 83,
|
||||
SpawnBabyDragon = 84,
|
||||
SaveTransferPacket = 85,
|
||||
UpdateState = 86,
|
||||
BuildingStatePacket = 87,
|
||||
AddVillager = 88,
|
||||
SetupInitialWorkers = 89,
|
||||
VillagerTeleportTo = 90,
|
||||
PlaceKeepRandomly = 91
|
||||
}
|
||||
}
|
||||
27
ErrorCodeMessages.cs
Normal file
27
ErrorCodeMessages.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public static class ErrorCodeMessages
|
||||
{
|
||||
private static readonly Dictionary<string, string> errorMessages = new Dictionary<string, string>
|
||||
{
|
||||
{ "TimedOut", "Your connectioned has timed out." },
|
||||
{ "Disconnected", "Lost connection to server." },
|
||||
{ "NoConnection", "Failed to connect to server." },
|
||||
};
|
||||
|
||||
public static string GetMessage(Enum errorCode)
|
||||
{
|
||||
if (errorMessages.TryGetValue(errorCode.ToString(), out string message))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
return errorCode.ToString(); // Fallback message
|
||||
}
|
||||
}
|
||||
}
|
||||
109
KCClient.cs
Normal file
109
KCClient.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Harmony;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.Packets.Lobby;
|
||||
using KCM.Packets.Network;
|
||||
using Riptide;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.KCServer;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class KCClient : MonoBehaviour
|
||||
{
|
||||
public static Client client = new Client(Main.steamClient);
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public static KCClient inst { get; set; }
|
||||
|
||||
|
||||
static KCClient()
|
||||
{
|
||||
client.Connected += Client_Connected;
|
||||
client.ConnectionFailed += Client_ConnectionFailed;
|
||||
client.Disconnected += Client_Disconnected;
|
||||
client.MessageReceived += PacketHandler.HandlePacket;
|
||||
}
|
||||
|
||||
private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
|
||||
{
|
||||
Main.helper.Log("Client disconnected event start");
|
||||
try
|
||||
{
|
||||
if (e.Message != null)
|
||||
{
|
||||
Main.helper.Log(e.Message.ToString());
|
||||
MessageReceivedEventArgs eargs = new MessageReceivedEventArgs(null, (ushort)Enums.Packets.ShowModal, e.Message);
|
||||
|
||||
if (eargs.MessageId == (ushort)Enums.Packets.ShowModal)
|
||||
{
|
||||
ShowModal modalPacket = (ShowModal)PacketHandler.DeserialisePacket(eargs);
|
||||
|
||||
modalPacket.HandlePacketClient();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
GameState.inst.SetNewMode(GameState.inst.mainMenuMode);
|
||||
ModalManager.ShowModal("Disconnected from Server", ErrorCodeMessages.GetMessage(e.Reason), "Okay", true, () => { Main.TransitionTo(MenuState.ServerBrowser); });
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error handling disconnection message");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
Main.helper.Log("Client disconnected event end");
|
||||
}
|
||||
|
||||
private static void Client_ConnectionFailed(object sender, ConnectionFailedEventArgs e)
|
||||
{
|
||||
Main.helper.Log($"Connection failed: {e.Reason}");
|
||||
|
||||
ModalManager.ShowModal("Failed to connect", ErrorCodeMessages.GetMessage(e.Reason));
|
||||
}
|
||||
|
||||
private static void Client_Connected(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
public KCClient(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public static void Connect(string ip)
|
||||
{
|
||||
Main.helper.Log("Trying to connect to: " + ip);
|
||||
client.Connect(ip, useMessageHandlers: false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
client.Update();
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
|
||||
helper.Log("Preload run in client");
|
||||
}
|
||||
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
91
KCPlayer.cs
Normal file
91
KCPlayer.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using KCM.Attributes;
|
||||
using Riptide;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class KCPlayer
|
||||
{
|
||||
[Transmittable]
|
||||
public ushort id;
|
||||
[Transmittable]
|
||||
public string name;
|
||||
|
||||
public string steamId;
|
||||
|
||||
|
||||
[Transmittable]
|
||||
public string kingdomName;
|
||||
[Transmittable]
|
||||
public int banner = 0;
|
||||
[Transmittable]
|
||||
public bool ready = false;
|
||||
|
||||
public Player inst;
|
||||
public GameObject gameObject;
|
||||
|
||||
|
||||
public KCPlayer(string name, ushort id, string steamId)
|
||||
{
|
||||
if (id != KCClient.client.Id)
|
||||
{
|
||||
gameObject = new GameObject($"Client Player ({id} {name})");
|
||||
|
||||
inst = gameObject.AddComponent<Player>();
|
||||
var irrigation = gameObject.AddComponent<IrrigationManager>();
|
||||
var lmo = gameObject.AddComponent<LandmassOwner>();
|
||||
|
||||
inst.irrigation = irrigation;
|
||||
|
||||
inst.PlayerLandmassOwner = lmo;
|
||||
inst.PlayerLandmassOwner.teamId = id * 10 + 2;
|
||||
|
||||
inst.hazardPayWarmup = new Timer(5f);
|
||||
inst.hazardPayWarmup.Enabled = false;
|
||||
|
||||
bool[] flagsArr = new bool[38];
|
||||
for (int i = 0; i < flagsArr.Length; i++)
|
||||
flagsArr[i] = true;
|
||||
|
||||
var field = typeof(Player).GetField("defaultEnabledFlags", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
field.SetValue(inst, flagsArr);
|
||||
|
||||
|
||||
|
||||
Player oldPlayer = Player.inst;
|
||||
Player.inst = inst;
|
||||
|
||||
inst.Reset();
|
||||
|
||||
Player.inst = oldPlayer;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
gameObject = Player.inst.gameObject;
|
||||
inst = Player.inst;
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.steamId = steamId;
|
||||
this.kingdomName = " ";
|
||||
}
|
||||
|
||||
public KCPlayer(ushort id, Player player)
|
||||
{
|
||||
gameObject = player.gameObject;
|
||||
inst = player;
|
||||
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
121
KCServer.cs
Normal file
121
KCServer.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Riptide;
|
||||
using Harmony;
|
||||
using System.Reflection;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.Packets.Lobby;
|
||||
using KCM.ServerLobby;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.Network;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class KCServer : MonoBehaviour
|
||||
{
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static bool started = false;
|
||||
|
||||
static KCServer()
|
||||
{
|
||||
//server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined"));
|
||||
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
}
|
||||
|
||||
public static void StartServer()
|
||||
{
|
||||
server = new Server(Main.steamServer);
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
|
||||
server.Start(0, 25, useMessageHandlers: false);
|
||||
|
||||
server.ClientConnected += (obj, ev) =>
|
||||
{
|
||||
Main.helper.Log("Client connected");
|
||||
|
||||
if (server.ClientCount > LobbyHandler.ServerSettings.MaxPlayers)
|
||||
{
|
||||
ShowModal showModal = new ShowModal() { title = "Failed to connect", message = "Server is full." };
|
||||
|
||||
showModal.Send(ev.Client.Id);
|
||||
|
||||
server.DisconnectClient(ev.Client.Id); //, PacketHandler.SerialisePacket(showModal)
|
||||
return;
|
||||
}
|
||||
|
||||
ev.Client.CanQualityDisconnect = false;
|
||||
|
||||
Main.helper.Log("Client ID is: " + ev.Client.Id);
|
||||
|
||||
new ServerHandshake() { clientId = ev.Client.Id, loadingSave = LobbyManager.loadingSave }.Send(ev.Client.Id);
|
||||
};
|
||||
|
||||
server.ClientDisconnected += (obj, ev) =>
|
||||
{
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.",
|
||||
}.SendToAll();
|
||||
|
||||
Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).steamId);
|
||||
Destroy(LobbyHandler.playerEntries.Select(x => x.GetComponent<PlayerEntryScript>()).Where(x => x.Client == ev.Client.Id).FirstOrDefault().gameObject);
|
||||
|
||||
Main.helper.Log($"Client disconnected. {ev.Reason}");
|
||||
};
|
||||
|
||||
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
|
||||
|
||||
|
||||
//Main.kCPlayers.Add(1, new KCPlayer(1, Player.inst));
|
||||
|
||||
//Player.inst = Main.GetPlayer();
|
||||
}
|
||||
|
||||
/*[MessageHandler(25)]
|
||||
public static void ClientJoined(ushort id, Message message)
|
||||
{
|
||||
var name = message.GetString();
|
||||
|
||||
Main.helper.Log(id.ToString());
|
||||
Main.helper.Log($"User connected: {name}");
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
players.Add(id, new KCPlayer(name, id, Player.inst));
|
||||
}
|
||||
else
|
||||
{
|
||||
players.Add(id, new KCPlayer(name, id));
|
||||
}
|
||||
}*/
|
||||
|
||||
public static bool IsRunning { get { return server.IsRunning; } }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
server.Update();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
helper.Log("server?");
|
||||
|
||||
helper.Log("Preload run in server");
|
||||
}
|
||||
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
261
LoadSaveOverrides/MultiplayerSaveContainer.cs
Normal file
261
LoadSaveOverrides/MultiplayerSaveContainer.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using Assets.Code;
|
||||
using Riptide;
|
||||
using Riptide.Transports;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.LoadSaveOverrides
|
||||
{
|
||||
[Serializable]
|
||||
public class MultiplayerSaveContainer : LoadSaveContainer
|
||||
{
|
||||
public Dictionary<string, Player.PlayerSaveData> players = new Dictionary<string, Player.PlayerSaveData>();
|
||||
public Dictionary<string, string> kingdomNames = new Dictionary<string, string>();
|
||||
|
||||
public new MultiplayerSaveContainer Pack(object obj)
|
||||
{
|
||||
this.CameraSaveData = new Cam.CamSaveData().Pack(Cam.inst);
|
||||
this.TownNameSaveData = new TownNameUI.TownNameSaveData().Pack(TownNameUI.inst);
|
||||
|
||||
Main.helper.Log($"Saving data for {Main.kCPlayers.Count} ({KCServer.server.ClientCount}) players.");
|
||||
|
||||
//this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst);
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
{
|
||||
Main.helper.Log($"Attempting to pack data for: " + player.name + $"({player.steamId})");
|
||||
Main.helper.Log($"{player.inst.ToString()} {player.inst?.gameObject.name}");
|
||||
this.players.Add(player.steamId, new Player.PlayerSaveData().Pack(player.inst));
|
||||
kingdomNames.Add(player.steamId, player.kingdomName);
|
||||
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
}
|
||||
|
||||
this.WorldSaveData = new World.WorldSaveData().Pack(World.inst);
|
||||
this.FishSystemSaveData = new FishSystem.FishSystemSaveData().Pack(FishSystem.inst);
|
||||
this.JobSystemSaveData = new JobSystem.JobSystemSaveData().Pack(JobSystem.inst);
|
||||
this.FreeResourceManagerSaveData = new FreeResourceManager.FreeResourceManagerSaveData().Pack(FreeResourceManager.inst);
|
||||
this.WeatherSaveData = new Weather.WeatherSaveData().Pack(Weather.inst);
|
||||
this.FireManagerSaveData = new FireManager.FireManagerSaveData().Pack(FireManager.inst);
|
||||
this.DragonSpawnSaveData = new DragonSpawn.DragonSpawnSaveData().Pack(DragonSpawn.inst);
|
||||
this.UnitSystemSaveData = new UnitSystem.UnitSystemSaveData().Pack(UnitSystem.inst);
|
||||
this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(RaiderSystem.inst);
|
||||
this.ShipSystemSaveData = new ShipSystem.ShipSystemSaveData().Pack(ShipSystem.inst);
|
||||
this.AIBrainsSaveData = new AIBrainsContainer.SaveData().Pack(AIBrainsContainer.inst);
|
||||
this.SiegeMonsterSaveData = new SiegeMonster.SiegeMonsterSaveData().Pack(null);
|
||||
this.CartSystemSaveData = new CartSystem.CartSystemSaveData().Pack(CartSystem.inst);
|
||||
this.SiegeCatapultSystemSaveData = new SiegeCatapultSystem.SiegeCatapultSystemSaveData().Pack(SiegeCatapultSystem.inst);
|
||||
this.OrdersManagerSaveData = new OrdersManager.OrdersManagerSaveData().Pack(OrdersManager.inst);
|
||||
this.CustomSaveData = LoadSave.CustomSaveData_DontAccessDirectly;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override object Unpack(object obj)
|
||||
{
|
||||
//original Player reset was up here
|
||||
foreach (var kvp in players)
|
||||
{
|
||||
|
||||
KCPlayer player;
|
||||
|
||||
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
|
||||
{
|
||||
player = new KCPlayer("", 50, kvp.Key);
|
||||
player.kingdomName = kingdomNames[kvp.Key];
|
||||
|
||||
Main.kCPlayers.Add(kvp.Key, player);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
player.inst.Reset();
|
||||
|
||||
|
||||
AIBrainsContainer.inst.ClearAIs();
|
||||
this.CameraSaveData.Unpack(Cam.inst);
|
||||
this.WorldSaveData.Unpack(World.inst);
|
||||
|
||||
bool flag = this.FishSystemSaveData != null;
|
||||
if (flag)
|
||||
{
|
||||
this.FishSystemSaveData.Unpack(FishSystem.inst);
|
||||
}
|
||||
this.TownNameSaveData.Unpack(TownNameUI.inst);
|
||||
|
||||
|
||||
//TownNameUI.inst.townName = kingdomNames[Main.PlayerSteamID];
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
Main.helper.Log("Unpacking player data");
|
||||
|
||||
Player.PlayerSaveData clientPlayerData = null;
|
||||
|
||||
foreach (var kvp in players)
|
||||
{
|
||||
if (kvp.Key == SteamUser.GetSteamID().ToString())
|
||||
{
|
||||
Main.helper.Log("Found current client player data. ID: " + SteamUser.GetSteamID().ToString());
|
||||
|
||||
clientPlayerData = kvp.Value;
|
||||
}
|
||||
else
|
||||
{ // Maybe ??
|
||||
Main.helper.Log("Loading player data: " + kvp.Key);
|
||||
|
||||
|
||||
KCPlayer player;
|
||||
|
||||
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
|
||||
{
|
||||
player = new KCPlayer("", 50, kvp.Key);
|
||||
Main.kCPlayers.Add(kvp.Key, player);
|
||||
}
|
||||
|
||||
Player oldPlayer = Player.inst;
|
||||
Player.inst = player.inst;
|
||||
Main.helper.Log($"Number of landmasses: {World.inst.NumLandMasses}");
|
||||
|
||||
//Reset was here before unpack
|
||||
kvp.Value.Unpack(player.inst);
|
||||
|
||||
Player.inst = oldPlayer;
|
||||
|
||||
|
||||
player.banner = player.inst.PlayerLandmassOwner.bannerIdx;
|
||||
player.kingdomName = TownNameUI.inst.townName;
|
||||
}
|
||||
}
|
||||
|
||||
clientPlayerData.Unpack(Player.inst); // Unpack the current client player last so that loading of villagers works correctly.
|
||||
|
||||
Main.helper.Log("unpacked player data");
|
||||
Main.helper.Log("Setting banner and name");
|
||||
|
||||
var client = Main.kCPlayers[SteamUser.GetSteamID().ToString()];
|
||||
|
||||
|
||||
client.banner = Player.inst.PlayerLandmassOwner.bannerIdx;
|
||||
client.kingdomName = TownNameUI.inst.townName;
|
||||
|
||||
Main.helper.Log("Finished unpacking player data");
|
||||
|
||||
/*
|
||||
* Not even going to bother fixing AI brains save data yet, not in short-term roadmap
|
||||
*/
|
||||
|
||||
/*bool flag2 = this.AIBrainsSaveData != null;
|
||||
if (flag2)
|
||||
{
|
||||
this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst);
|
||||
}*/
|
||||
|
||||
Main.helper.Log("Unpacking free resource manager");
|
||||
this.FreeResourceManagerSaveData.Unpack(FreeResourceManager.inst);
|
||||
Main.helper.Log("Unpacking job system");
|
||||
this.JobSystemSaveData.Unpack(JobSystem.inst);
|
||||
Main.helper.Log("Unpacking weather");
|
||||
this.WeatherSaveData.Unpack(Weather.inst);
|
||||
Main.helper.Log("Unpacking fire manager");
|
||||
this.FireManagerSaveData.Unpack(FireManager.inst);
|
||||
Main.helper.Log("Unpacking dragon spawn");
|
||||
this.DragonSpawnSaveData.Unpack(DragonSpawn.inst);
|
||||
Main.helper.Log("Unpacking unit system");
|
||||
bool flag3 = this.UnitSystemSaveData != null;
|
||||
if (flag3)
|
||||
{
|
||||
this.UnitSystemSaveData.Unpack(UnitSystem.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking siege monster");
|
||||
bool flag4 = this.SiegeMonsterSaveData != null;
|
||||
if (flag4)
|
||||
{
|
||||
this.SiegeMonsterSaveData.Unpack(null);
|
||||
}
|
||||
Main.helper.Log("Unpacking siege catapult system");
|
||||
bool flag5 = this.SiegeCatapultSystemSaveData != null;
|
||||
if (flag5)
|
||||
{
|
||||
this.SiegeCatapultSystemSaveData.Unpack(SiegeCatapultSystem.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking ship system");
|
||||
bool flag6 = this.ShipSystemSaveData != null;
|
||||
if (flag6)
|
||||
{
|
||||
this.ShipSystemSaveData.Unpack(ShipSystem.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking cart system");
|
||||
bool flag7 = this.CartSystemSaveData != null;
|
||||
if (flag7)
|
||||
{
|
||||
this.CartSystemSaveData.Unpack(CartSystem.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking raid system");
|
||||
bool flag8 = this.RaidSystemSaveData2 != null;
|
||||
if (flag8)
|
||||
{
|
||||
this.RaidSystemSaveData2.Unpack(RaiderSystem.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking orders manager");
|
||||
bool flag9 = this.OrdersManagerSaveData != null;
|
||||
if (flag9)
|
||||
{
|
||||
this.OrdersManagerSaveData.Unpack(OrdersManager.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking AI brains");
|
||||
bool flag10 = this.AIBrainsSaveData != null;
|
||||
if (flag10)
|
||||
{
|
||||
this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking custom save data");
|
||||
bool flag11 = this.CustomSaveData != null;
|
||||
if (flag11)
|
||||
{
|
||||
LoadSave.CustomSaveData_DontAccessDirectly = this.CustomSaveData;
|
||||
}
|
||||
Main.helper.Log("Unpacking done");
|
||||
|
||||
|
||||
World.inst.UpscaleFeatures();
|
||||
Player.inst.RefreshVisibility(true);
|
||||
for (int i = 0; i < Player.inst.Buildings.Count; i++)
|
||||
{
|
||||
Player.inst.Buildings.data[i].UpdateMaterialSelection();
|
||||
}
|
||||
|
||||
// Player.inst.loadTickDelay = 1;
|
||||
Type playerType = typeof(Player);
|
||||
FieldInfo loadTickDelayField = playerType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(Player.inst, 1);
|
||||
}
|
||||
|
||||
// UnitSystem.inst.loadTickDelay = 1;
|
||||
Type unitSystemType = typeof(UnitSystem);
|
||||
loadTickDelayField = unitSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(UnitSystem.inst, 1);
|
||||
}
|
||||
|
||||
// JobSystem.inst.loadTickDelay = 1;
|
||||
Type jobSystemType = typeof(JobSystem);
|
||||
loadTickDelayField = jobSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(JobSystem.inst, 1);
|
||||
}
|
||||
|
||||
Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}");
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
LoadSaveOverrides/MultiplayerSaveDeserializationBinder.cs
Normal file
29
LoadSaveOverrides/MultiplayerSaveDeserializationBinder.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.LoadSaveOverrides
|
||||
{
|
||||
sealed class MultiplayerSaveDeserializationBinder : SerializationBinder
|
||||
{
|
||||
public override Type BindToType(string assemblyName, string typeName)
|
||||
{
|
||||
Type typeToDeserialize = null;
|
||||
|
||||
// For each assemblyName/typeName that you want to deserialize to
|
||||
// a different type, set typeToDeserialize to the desired type.
|
||||
String exeAssembly = Assembly.GetExecutingAssembly().FullName;
|
||||
|
||||
|
||||
// The following line of code returns the type.
|
||||
typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
|
||||
typeName, exeAssembly));
|
||||
|
||||
return typeToDeserialize;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
ModalManager.cs
Normal file
68
ModalManager.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class ModalManager
|
||||
{
|
||||
static GameObject modalInst;
|
||||
static bool instantiated = false;
|
||||
|
||||
static TMPro.TextMeshProUGUI tmpTitle;
|
||||
static TMPro.TextMeshProUGUI tmpDescription;
|
||||
static Button acceptButton;
|
||||
|
||||
static ModalManager()
|
||||
{
|
||||
if (!instantiated)
|
||||
{
|
||||
modalInst = GameObject.Instantiate(PrefabManager.modalUIPrefab, Constants.MainMenuUI_T);
|
||||
modalInst.SetActive(false);
|
||||
|
||||
acceptButton = modalInst.transform.Find("Modal/Container/Button").GetComponent<UnityEngine.UI.Button>();
|
||||
|
||||
|
||||
|
||||
tmpTitle = modalInst.transform.Find("Modal/Container/Title").GetComponent<TextMeshProUGUI>();
|
||||
tmpDescription = modalInst.transform.Find("Modal/Container/Description").GetComponent<TextMeshProUGUI>();
|
||||
|
||||
instantiated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("ModalManager is a singleton and may only be instantiated once");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShowModal(string title, string message, string buttonText = "Okay", bool withButton = true, Action action = null)
|
||||
{
|
||||
tmpTitle.text = title;
|
||||
tmpDescription.text = message;
|
||||
|
||||
acceptButton.gameObject.SetActive(withButton);
|
||||
|
||||
acceptButton.gameObject.GetComponentInChildren<TextMeshProUGUI>().text = buttonText;
|
||||
|
||||
acceptButton.onClick.RemoveAllListeners();
|
||||
|
||||
acceptButton.onClick.AddListener(() =>
|
||||
{
|
||||
modalInst.SetActive(false); // Clicked okay
|
||||
action?.Invoke();
|
||||
});
|
||||
|
||||
modalInst.SetActive(true);
|
||||
}
|
||||
|
||||
public static void HideModal()
|
||||
{
|
||||
modalInst.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Packets/Game/Dragon/SpawnBabyDragonPacket.cs
Normal file
26
Packets/Game/Dragon/SpawnBabyDragonPacket.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.Dragon
|
||||
{
|
||||
public class SpawnBabyDragonPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.SpawnBabyDragon;
|
||||
|
||||
public Vector3 start { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
DragonSpawn.inst.SpawnBabyDragon(start);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Packets/Game/Dragon/SpawnMamaDragonPacket.cs
Normal file
26
Packets/Game/Dragon/SpawnMamaDragonPacket.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.Dragon
|
||||
{
|
||||
public class SpawnMamaDragonPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.SpawnMamaDragon;
|
||||
|
||||
public Vector3 start { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
DragonSpawn.inst.SpawnMamaDragon(start);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Packets/Game/Dragon/SpawnSiegeDragonPacket.cs
Normal file
26
Packets/Game/Dragon/SpawnSiegeDragonPacket.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.Dragon
|
||||
{
|
||||
public class SpawnSiegeDragonPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.SpawnSiegeDragon;
|
||||
|
||||
public Vector3 start { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
DragonSpawn.inst.SpawnSiegeDragon(start);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Packets/Game/GameBuilding/CompleteBuild.cs
Normal file
27
Packets/Game/GameBuilding/CompleteBuild.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameBuilding
|
||||
{
|
||||
public class CompleteBuild : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.CompleteBuild;
|
||||
|
||||
public Guid buildingId { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
Player.inst.GetBuilding(buildingId).CompleteBuild();
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Packets/Game/GameBuilding/UpdateConstruction.cs
Normal file
29
Packets/Game/GameBuilding/UpdateConstruction.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameBuilding
|
||||
{
|
||||
public class UpdateConstruction : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.UpdateConstruction;
|
||||
|
||||
public Guid buildingId { get; set; }
|
||||
public float constructionProgress { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
//Main.helper.Log($"Received packet from: {clientId} receiving client is {KCClient.client.Id}");
|
||||
Player.inst.GetBuilding(buildingId).constructionProgress = constructionProgress;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Packets/Game/GamePlayer/AddVillagerPacket.cs
Normal file
42
Packets/Game/GamePlayer/AddVillagerPacket.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GamePlayer
|
||||
{
|
||||
public class AddVillagerPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.AddVillager;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
Main.helper.Log("Received add villager packet from " + player.name + $"({player.id})");
|
||||
|
||||
Villager v = Villager.CreateVillager();
|
||||
v.guid = guid;
|
||||
|
||||
player.inst.Workers.Add(v);
|
||||
player.inst.Homeless.Add(v);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling add villager packet: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Packets/Game/GamePlayer/SetupInitialWorkersPacket.cs
Normal file
35
Packets/Game/GamePlayer/SetupInitialWorkersPacket.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GamePlayer
|
||||
{
|
||||
public class SetupInitialWorkersPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.SetupInitialWorkers;
|
||||
|
||||
public Guid keepGuid { get; set; }
|
||||
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
/*Keep keep = player.inst.GetBuilding(keepGuid).GetComponent<Keep>();
|
||||
if (keep == null)
|
||||
{
|
||||
Main.helper.Log("Keep not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
player.inst.SetupInitialWorkers(keep);*/
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Packets/Game/GameTrees/FellTree.cs
Normal file
30
Packets/Game/GameTrees/FellTree.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameTrees
|
||||
{
|
||||
public class FellTree : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.FellTree;
|
||||
|
||||
public int idx { get; set; }
|
||||
|
||||
public int x { get; set; }
|
||||
public int z { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Cell cell = World.inst.GetCellData(x, z);
|
||||
|
||||
TreeSystem.inst.FellTree(cell, idx);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Packets/Game/GameTrees/GrowTree.cs
Normal file
28
Packets/Game/GameTrees/GrowTree.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameTrees
|
||||
{
|
||||
public class GrowTree : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.GrowTree;
|
||||
|
||||
public int X { get; set; }
|
||||
public int Z { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Cell cell = World.inst.GetCellData(X, Z);
|
||||
|
||||
TreeSystem.inst.GrowTree(cell);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Packets/Game/GameTrees/ShakeTree.cs
Normal file
25
Packets/Game/GameTrees/ShakeTree.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameTrees
|
||||
{
|
||||
public class ShakeTree : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ShakeTree;
|
||||
|
||||
public int idx { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
TreeSystem.inst.ShakeTree(idx);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Packets/Game/GameVillager/VillagerTeleportTo.cs
Normal file
38
Packets/Game/GameVillager/VillagerTeleportTo.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameVillager
|
||||
{
|
||||
public class VillagerTeleportTo : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.VillagerTeleportTo;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
public Vector3 pos { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
try
|
||||
{
|
||||
Villager.villagers.data.Where(x => x.guid == guid).FirstOrDefault().TeleportTo(pos);
|
||||
|
||||
Main.helper.Log($"Teleporting villager to {pos.ToString()}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling villager teleport packet: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Packets/Game/GameWeather/ChangeWeather.cs
Normal file
25
Packets/Game/GameWeather/ChangeWeather.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game.GameWeather
|
||||
{
|
||||
public class ChangeWeather : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ChangeWeather;
|
||||
|
||||
public int weatherType { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Weather.CurrentWeather = ((Weather.WeatherType)weatherType);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
145
Packets/Game/GameWorld/WorldPlace.cs
Normal file
145
Packets/Game/GameWorld/WorldPlace.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameWorld
|
||||
{
|
||||
public class WorldPlace : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.WorldPlace;
|
||||
|
||||
public string customName { get; set; }
|
||||
public Guid guid { get; set; }
|
||||
public string uniqueName { get; set; }
|
||||
public Quaternion rotation { get; set; }
|
||||
public Vector3 globalPosition { get; set; }
|
||||
public Vector3 localPosition { get; set; }
|
||||
public bool built { get; set; }
|
||||
public bool placed { get; set; }
|
||||
public bool open { get; set; }
|
||||
public bool doBuildAnimation { get; set; }
|
||||
public bool constructionPaused { get; set; }
|
||||
public float constructionProgress { get; set; }
|
||||
public float life { get; set; }
|
||||
public float ModifiedMaxLife { get; set; }
|
||||
public int yearBuilt { get; set; }
|
||||
public float decayProtection { get; set; }
|
||||
public bool seenByPlayer { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) return; //prevent double placing on same client
|
||||
|
||||
PlaceBuilding();
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//PlaceBuilding();
|
||||
|
||||
//SendToAll(clientId);
|
||||
}
|
||||
|
||||
public void PlaceBuilding()
|
||||
{
|
||||
Main.helper.Log("Received place building packet for " + uniqueName + " from " + player.name + $"({player.id})");
|
||||
|
||||
//var originalPlayer = Player.inst;
|
||||
//Player.inst = player.inst;
|
||||
|
||||
Building.BuildingSaveData structureData = new Building.BuildingSaveData()
|
||||
{
|
||||
uniqueName = uniqueName,
|
||||
customName = customName,
|
||||
guid = guid,
|
||||
rotation = rotation,
|
||||
globalPosition = globalPosition,
|
||||
localPosition = localPosition,
|
||||
built = built,
|
||||
placed = placed,
|
||||
open = open,
|
||||
doBuildAnimation = doBuildAnimation,
|
||||
constructionPaused = constructionPaused,
|
||||
constructionProgress = constructionProgress,
|
||||
life = life,
|
||||
ModifiedMaxLife = ModifiedMaxLife,
|
||||
//CollectForBuild = CollectForBuild,
|
||||
yearBuilt = yearBuilt,
|
||||
decayProtection = decayProtection,
|
||||
seenByPlayer = seenByPlayer
|
||||
};
|
||||
|
||||
|
||||
//Player originalInst = Player.inst;
|
||||
//Player.inst = player.inst;
|
||||
|
||||
Building Building = GameState.inst.GetPlaceableByUniqueName(structureData.uniqueName);
|
||||
bool flag = Building;
|
||||
if (flag)
|
||||
{
|
||||
Building building = UnityEngine.Object.Instantiate<Building>(Building);
|
||||
building.transform.position = structureData.globalPosition;
|
||||
Main.helper.Log("Building init");
|
||||
building.Init();
|
||||
building.transform.SetParent(player.inst.buildingContainer.transform, true);
|
||||
Main.helper.Log("Building unpack");
|
||||
structureData.Unpack(building);
|
||||
|
||||
Main.helper.Log(player.inst.ToString());
|
||||
Main.helper.Log((player.inst.PlayerLandmassOwner == null).ToString());
|
||||
Main.helper.Log(building.LandMass().ToString());
|
||||
Main.helper.Log("Player add Building unpacked");
|
||||
player.inst.AddBuilding(building);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
player.inst.PlayerLandmassOwner.TakeOwnership(building.LandMass());
|
||||
bool flag2 = building.GetComponent<Keep>() != null && building.TeamID() == player.inst.PlayerLandmassOwner.teamId;
|
||||
Main.helper.Log("Set keep " + flag2);
|
||||
if (flag2)
|
||||
{
|
||||
player.inst.keep = building.GetComponent<Keep>();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log(e.Message);
|
||||
}
|
||||
|
||||
Main.helper.Log("Place from load");
|
||||
Cell cell = World.inst.PlaceFromLoad(building);
|
||||
Main.helper.Log("unpack stage 2");
|
||||
structureData.UnpackStage2(building);
|
||||
|
||||
building.SetVisibleForFog(false);
|
||||
|
||||
Main.helper.Log("Landmass owner take ownership");
|
||||
|
||||
Main.helper.Log($"{player.id} (team {player.inst.PlayerLandmassOwner.teamId}) banner: {player.inst.PlayerLandmassOwner.bannerIdx} Placed building {building.name} at {building.transform.position}");
|
||||
|
||||
|
||||
//Player.inst = originalInst; // Reset player back to normal // Might not be needed anymore with player ref patches?
|
||||
|
||||
|
||||
Main.helper.Log($"Host player Landmass Names Count: {Player.inst.LandMassNames.Count}, Contents: {string.Join(", ", Player.inst.LandMassNames)}");
|
||||
Main.helper.Log($"Client player ({player.name}) Landmass Names Count: {player.inst.LandMassNames.Count}, Contents: {string.Join(", ", player.inst.LandMassNames)}");
|
||||
|
||||
player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
Player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
|
||||
//Player.inst = originalPlayer;
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log(structureData.uniqueName + " failed to load correctly");
|
||||
}
|
||||
//building.Init();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
102
Packets/Game/PlaceKeepRandomly.cs
Normal file
102
Packets/Game/PlaceKeepRandomly.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Analytics;
|
||||
|
||||
namespace KCM.Packets.Game
|
||||
{
|
||||
public class PlaceKeepRandomly : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.PlaceKeepRandomly;
|
||||
|
||||
public int landmassIdx { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
Building keep = UnityEngine.Object.Instantiate<Building>(GameState.inst.GetPlaceableByUniqueName(World.keepName));
|
||||
|
||||
keep.Init();
|
||||
|
||||
|
||||
Cell[] cells = World.inst.GetCellsData().Where(x => x.landMassIdx == landmassIdx).ToArray();
|
||||
Cell keepCell = null;
|
||||
|
||||
|
||||
foreach (Cell cell in cells)
|
||||
{
|
||||
Cell nearbyStoneCell = FindNearbyStoneCell(cells, cell.x, cell.z, landmassIdx, 15); // Place keep within 15 tiles of stone
|
||||
Cell nearbyWaterCell = FindNearbyWaterCell(cells, cell.x, cell.z, landmassIdx, 6); // Do not place keep within 6 tiles of water
|
||||
|
||||
|
||||
Cell clearCell = FindClearCell(cells, cell.x, cell.z, landmassIdx, 4); // cells in 4 by 4 radius are clear?
|
||||
|
||||
if (clearCell != null & nearbyStoneCell != null && nearbyWaterCell == null && cell.Type == ResourceType.None)
|
||||
{
|
||||
Console.WriteLine($"Nearby stone cell found at ({nearbyStoneCell.x}, {nearbyStoneCell.z})");
|
||||
keepCell = cell;
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
keep.transform.position = keepCell.Position;
|
||||
|
||||
keep.SendMessage("OnPlayerPlacement", SendMessageOptions.DontRequireReceiver);
|
||||
|
||||
|
||||
Player.inst.PlayerLandmassOwner.TakeOwnership(keep.LandMass());
|
||||
Player.inst.keep = keep.GetComponent<Keep>();
|
||||
Player.inst.RefreshVisibility(true);
|
||||
RandomPlacement(keep);
|
||||
|
||||
} catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error placing keep randomly: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RandomPlacement(Building keep) // This is a hack so I can detect when its being called by this packet
|
||||
{
|
||||
World.inst.Place(keep);
|
||||
|
||||
Cam.inst.SetTrackingPos(keep.GetPosition());
|
||||
}
|
||||
|
||||
private static Cell FindNearbyStoneCell(Cell[] cells, int x, int z, int landmassIdx, int radius)
|
||||
{
|
||||
return cells.FirstOrDefault(cell => IsResourceInRadius(cell, x, z, radius, ResourceType.Stone));
|
||||
}
|
||||
|
||||
private static Cell FindNearbyWaterCell(Cell[] cells, int x, int z, int landmassIdx, int radius)
|
||||
{
|
||||
return cells.FirstOrDefault(cell => IsResourceInRadius(cell, x, z, radius, ResourceType.Water));
|
||||
}
|
||||
private static Cell FindClearCell(Cell[] cells, int x, int z, int landmassIdx, int radius)
|
||||
{
|
||||
return cells.FirstOrDefault(cell => IsResourceInRadius(cell, x, z, radius, ResourceType.None));
|
||||
}
|
||||
|
||||
private static bool IsResourceInRadius(Cell cell, int x, int z, int radius, ResourceType desiredResource)
|
||||
{
|
||||
bool isWithinRadius = Math.Sqrt((cell.x - x) * (cell.x - x) + (cell.z - z) * (cell.z - z)) <= radius;
|
||||
bool isNotCentralCell = cell.x != x || cell.z != z;
|
||||
bool isStoneType = cell.Type == desiredResource;
|
||||
|
||||
bool isWater = desiredResource == ResourceType.Water ? false : cell.deepWater || cell.Type == ResourceType.Water;
|
||||
|
||||
return isWithinRadius && isNotCentralCell && isStoneType && !isWater;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Packets/Game/SetSpeed.cs
Normal file
28
Packets/Game/SetSpeed.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Game
|
||||
{
|
||||
public class SetSpeed : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.SetSpeed;
|
||||
|
||||
public int speed { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) // Prevent speed softlock
|
||||
return;
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
203
Packets/Handlers/LobbyHandler.cs
Normal file
203
Packets/Handlers/LobbyHandler.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Assets.Code;
|
||||
using Assets;
|
||||
using KCM.Attributes;
|
||||
using KCM.Packets.Lobby;
|
||||
using KCM.Packets.Network;
|
||||
using KCM.ServerLobby;
|
||||
using KCM.ServerLobby.LobbyChat;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Reflection;
|
||||
|
||||
namespace KCM.Packets.Handlers
|
||||
{
|
||||
public class LobbyHandler
|
||||
{
|
||||
public static ServerSettings ServerSettings = new ServerSettings();
|
||||
|
||||
public static List<GameObject> playerEntries = new List<GameObject>();
|
||||
|
||||
|
||||
public static void ClearPlayerList()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (GameObject entry in playerEntries)
|
||||
GameObject.Destroy(entry);
|
||||
|
||||
playerEntries.Clear();
|
||||
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
Main.kCPlayers.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddPlayerEntry(ushort client)
|
||||
{
|
||||
try
|
||||
{
|
||||
GameObject entry = GameObject.Instantiate(PrefabManager.serverLobbyPlayerEntryPrefab, ServerLobbyScript.PlayerListContent);
|
||||
entry.SetActive(true);
|
||||
Main.helper.Log(entry.ToString());
|
||||
var s = entry.AddComponent<PlayerEntryScript>();
|
||||
|
||||
s.Client = client;
|
||||
|
||||
playerEntries.Add(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddSystemMessage(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
GameObject entry = GameObject.Instantiate(PrefabManager.serverChatSystemEntryPrefab, ServerLobbyScript.PlayerChatContent);
|
||||
entry.SetActive(true);
|
||||
chatEntries.Add(entry);
|
||||
var s = entry.AddComponent<SystemEntryScript>();
|
||||
|
||||
|
||||
SnapTo(entry.GetComponent<RectTransform>());
|
||||
|
||||
s.Message = message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<GameObject> chatEntries = new List<GameObject>();
|
||||
|
||||
public static void AddChatMessage(ushort client, string player, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
GameObject entry = GameObject.Instantiate(PrefabManager.serverChatEntryPrefab, ServerLobbyScript.PlayerChatContent);
|
||||
entry.SetActive(true);
|
||||
|
||||
chatEntries.Add(entry);
|
||||
|
||||
var s = entry.AddComponent<ChatEntryScript>();
|
||||
|
||||
SnapTo(entry.GetComponent<RectTransform>());
|
||||
|
||||
s.Client = client;
|
||||
s.PlayerName = player;
|
||||
s.Message = message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearChatEntries()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (GameObject entry in chatEntries)
|
||||
GameObject.Destroy(entry);
|
||||
|
||||
chatEntries.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void SnapTo(RectTransform target)
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
target.parent.parent.parent.GetComponent<ScrollRect>().normalizedPosition = new Vector2(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
604
Packets/Handlers/PacketHandler.cs
Normal file
604
Packets/Handlers/PacketHandler.cs
Normal file
@@ -0,0 +1,604 @@
|
||||
using KCM.Attributes;
|
||||
using KCM.Packets.Lobby;
|
||||
using KCM.Packets.Network;
|
||||
using Riptide;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Handlers
|
||||
{
|
||||
public class PacketHandler
|
||||
{
|
||||
public static Dictionary<ushort, PacketRef> Packets = new Dictionary<ushort, PacketRef>();
|
||||
public class PacketRef
|
||||
{
|
||||
public IPacket packet;
|
||||
public PropertyInfo[] properties;
|
||||
|
||||
public PacketRef(IPacket packet, PropertyInfo[] properties)
|
||||
{
|
||||
this.packet = packet;
|
||||
this.properties = properties;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Dictionary<ushort, PacketHandlerDelegate> PacketHandlers = new Dictionary<ushort, PacketHandlerDelegate>();
|
||||
public delegate void PacketHandlerDelegate(IPacket packet);
|
||||
|
||||
public static void Initialise()
|
||||
{
|
||||
try
|
||||
{
|
||||
Main.helper.Log("Loading Packet Handlers...");
|
||||
|
||||
//TO-DO Remove this. Packets now have "handle packet" method
|
||||
#region "Register server packet handlers"
|
||||
|
||||
var serverPacketHandlers = Assembly.GetExecutingAssembly().GetTypes().SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
||||
.Where(m => m.GetCustomAttributes(typeof(PacketHandlerAttribute), false).Length > 0)
|
||||
.ToArray();
|
||||
|
||||
foreach (MethodInfo method in serverPacketHandlers)
|
||||
{
|
||||
PacketHandlerAttribute attribute = method.GetCustomAttribute<PacketHandlerAttribute>();
|
||||
|
||||
|
||||
if (!method.IsStatic)
|
||||
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
||||
|
||||
Delegate packetHandler = Delegate.CreateDelegate(typeof(PacketHandlerDelegate), method, false);
|
||||
if (packetHandler != null)
|
||||
{
|
||||
// It's a message handler for Client instances
|
||||
if (PacketHandlers.ContainsKey(attribute.packetId))
|
||||
{
|
||||
MethodInfo otherMethodWithId = PacketHandlers[attribute.packetId].GetMethodInfo();
|
||||
throw new DuplicateHandlerException(attribute.packetId, method, otherMethodWithId);
|
||||
}
|
||||
else
|
||||
PacketHandlers.Add(attribute.packetId, (PacketHandlerDelegate)packetHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Failed to register handler: {method.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
Main.helper.Log($"Loaded {PacketHandlers.Count} server handlers");
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
Main.helper.Log("Loading packets...");
|
||||
|
||||
var packets = Assembly.GetExecutingAssembly().GetTypes().Where(t => t != null && t.Namespace != null && t.Namespace.StartsWith("KCM.Packets") && !t.IsAbstract && !t.IsInterface && typeof(IPacket).IsAssignableFrom(t)).ToList();
|
||||
|
||||
foreach (var packet in packets)
|
||||
{
|
||||
|
||||
IPacket p = (IPacket)Activator.CreateInstance(packet);
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.Name != "packetId").ToArray();
|
||||
Array.Sort(properties, (x, y) => String.Compare(x.Name, y.Name));
|
||||
ushort id = (ushort)p.GetType().GetProperty("packetId").GetValue(p, null);
|
||||
|
||||
if (p.GetType() == typeof(SaveTransferPacket))
|
||||
{
|
||||
Main.helper.Log("SaveTransferPacket");
|
||||
Main.helper.Log(string.Join("\n", properties.Select(x => x.Name).ToArray()));
|
||||
}
|
||||
|
||||
Packets.Add(id, new PacketRef(p, properties));
|
||||
Main.helper.Log($"- Registered Packet: {id} {packet.FullName}");
|
||||
|
||||
}
|
||||
|
||||
Main.helper.Log($"Loaded {Packets.Count} packets");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandlePacketServer(object sender, MessageReceivedEventArgs messageReceived)
|
||||
{
|
||||
var id = messageReceived.MessageId;
|
||||
|
||||
|
||||
IPacket packet = DeserialisePacket(messageReceived);
|
||||
|
||||
//Main.helper.Log($"Server Received packet {Packets[id].packet.GetType().Name} from {messageReceived.FromConnection.Id}");
|
||||
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
packet.HandlePacketServer();
|
||||
|
||||
((Packet)packet).SendToAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error handling packet {id} {packet.GetType().Name} from {packet.clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandlePacket(object sender, MessageReceivedEventArgs messageReceived)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = messageReceived.MessageId;
|
||||
|
||||
|
||||
//Main.helper.Log($"Client Received packet {Packets[id].packet.GetType().Name} from {messageReceived.FromConnection.Id}");
|
||||
|
||||
IPacket packet = DeserialisePacket(messageReceived);
|
||||
|
||||
//Main.helper.Log($"Client Received packet {Packets[id].packet.GetType().Name} from {packet.clientId}");
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
packet.HandlePacketClient();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error handling packet {id} {packet.GetType().Name} from {packet.clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* if (PacketHandlers.TryGetValue(id, out PacketHandlerDelegate handler))
|
||||
handler(packet);*/
|
||||
|
||||
// Main.helper.Log($"{(KCServer.IsRunning ? "Server" : "Client")} Received packet {id} {packet.GetType().kingdomName}");
|
||||
//Main.helper.Log($"Found handler: {(handler != null).ToString()}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static Message SerialisePacket(IPacket packet)
|
||||
{
|
||||
|
||||
var currentPropName = "";
|
||||
try
|
||||
{
|
||||
var packetRef = Packets[packet.packetId];
|
||||
Message message = Message.Create(MessageSendMode.Reliable, packet.packetId);
|
||||
|
||||
foreach (var prop in packetRef.properties)
|
||||
{
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddInt((int)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddUShort((ushort)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(bool))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddBool((bool)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(int))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddInt((int)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(string))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddString((string)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(float))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddFloat((float)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(double))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddDouble((double)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(byte[]))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
byte[] bytes = (byte[])prop.GetValue(packet, null);
|
||||
message.AddBytes(bytes, true);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<string>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<string> list = (List<string>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddString(item);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<bool>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<bool> list = (List<bool>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddBool(item);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<ushort>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<ushort> list = (List<ushort>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddUShort(item);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<int>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<int> list = (List<int>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddInt(item);
|
||||
}
|
||||
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
Type[] argumentTypes = prop.PropertyType.GetGenericArguments();
|
||||
Type keyType = argumentTypes[0];
|
||||
Type valueType = argumentTypes[1];
|
||||
|
||||
object dictionary = prop.GetValue(packet, null);
|
||||
|
||||
int count = (int)dictionary.GetType().GetProperty("Count").GetValue(dictionary, null);
|
||||
|
||||
var enumerator = ((IEnumerable)dictionary).GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
object key = enumerator.Current.GetType().GetProperty("Key").GetValue(enumerator.Current, null);
|
||||
object value = enumerator.Current.GetType().GetProperty("Value").GetValue(enumerator.Current, null);
|
||||
|
||||
Main.helper.Log($"Key: {key.GetType()}, Value: {value.GetType()}");
|
||||
}
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Vector3))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
Vector3 vector = (Vector3)prop.GetValue(packet, null);
|
||||
message.AddFloat(vector.x);
|
||||
message.AddFloat(vector.y);
|
||||
message.AddFloat(vector.z);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Quaternion))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
Quaternion quaternion = (Quaternion)prop.GetValue(packet, null);
|
||||
message.AddFloat(quaternion.x);
|
||||
message.AddFloat(quaternion.y);
|
||||
message.AddFloat(quaternion.z);
|
||||
message.AddFloat(quaternion.w);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Guid))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
Guid guid = (Guid)prop.GetValue(packet, null);
|
||||
message.AddBytes(guid.ToByteArray());
|
||||
}
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
|
||||
Type itemType = prop.PropertyType.GetGenericArguments()[0];
|
||||
|
||||
var list = prop.GetValue(packet, null) as System.Collections.IList;
|
||||
if (list != null)
|
||||
{
|
||||
message.AddInt(list.Count);
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (itemType.IsClass && itemType != typeof(string) || itemType.IsValueType && !itemType.IsPrimitive)
|
||||
{
|
||||
var fields = itemType.GetFields(); // Get fields
|
||||
Array.Sort(fields, (x, y) => String.Compare(x.Name, y.Name));
|
||||
var properties = itemType.GetProperties(); // Get properties
|
||||
Array.Sort(properties, (x, y) => String.Compare(x.Name, y.Name));
|
||||
|
||||
|
||||
// Serialize fields
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var fieldValue = field.GetValue(item);
|
||||
AddDynamic(message, fieldValue);
|
||||
}
|
||||
|
||||
// Serialize properties
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyValue = property.GetValue(item);
|
||||
AddDynamic(message, propertyValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddDynamic(message, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// You can add more types as needed
|
||||
}
|
||||
|
||||
|
||||
return message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Failed to serialise packet {packet.packetId} {packet.GetType().Name} at {currentPropName}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void AddDynamic(Message message, object value)
|
||||
{
|
||||
if (value is int intValue)
|
||||
message.AddInt(intValue);
|
||||
else if (value is string stringValue)
|
||||
message.AddString(stringValue);
|
||||
else if (value is bool boolValue)
|
||||
message.AddBool(boolValue);
|
||||
else if (value is float floatValue)
|
||||
message.AddFloat(floatValue);
|
||||
else if (value is double doubleValue)
|
||||
message.AddDouble(doubleValue);
|
||||
else if (value is Vector3 vector)
|
||||
{
|
||||
message.AddFloat(vector.x);
|
||||
message.AddFloat(vector.y);
|
||||
message.AddFloat(vector.z);
|
||||
}
|
||||
else if (value is Quaternion quaternion)
|
||||
{
|
||||
message.AddFloat(quaternion.x);
|
||||
message.AddFloat(quaternion.y);
|
||||
message.AddFloat(quaternion.z);
|
||||
message.AddFloat(quaternion.w);
|
||||
}
|
||||
else if (value is Guid guid)
|
||||
message.AddBytes(guid.ToByteArray());
|
||||
// Add more type checks as necessary
|
||||
else
|
||||
throw new NotImplementedException($"Type {value.GetType()} serialization not implemented.");
|
||||
}
|
||||
|
||||
|
||||
public static IPacket DeserialisePacket(MessageReceivedEventArgs messageReceived)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = messageReceived.Message;
|
||||
var packetRef = Packets[messageReceived.MessageId];
|
||||
IPacket p = (IPacket)Activator.CreateInstance(packetRef.packet.GetType());
|
||||
|
||||
|
||||
foreach (var prop in packetRef.properties)
|
||||
{
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
int enumValue = message.GetInt();
|
||||
string enumName = Enum.GetName(prop.PropertyType, enumValue);
|
||||
|
||||
prop.SetValue(p, Enum.Parse(prop.PropertyType, enumName));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
prop.SetValue(p, message.GetUShort());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(bool))
|
||||
{
|
||||
prop.SetValue(p, message.GetBool());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(int))
|
||||
{
|
||||
prop.SetValue(p, message.GetInt());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(string))
|
||||
{
|
||||
prop.SetValue(p, message.GetString());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(float))
|
||||
{
|
||||
prop.SetValue(p, message.GetFloat());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(double))
|
||||
{
|
||||
prop.SetValue(p, message.GetDouble());
|
||||
}
|
||||
else if (prop.PropertyType == typeof(byte[]))
|
||||
{
|
||||
byte[] bytes = message.GetBytes();
|
||||
|
||||
prop.SetValue(p, bytes);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<string>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<string> list = new List<string>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(message.GetString());
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<bool>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<bool> list = new List<bool>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(message.GetBool());
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<ushort>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<ushort> list = new List<ushort>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(message.GetUShort());
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<int>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<int> list = new List<int>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(message.GetInt());
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
IDictionary dictionary = (IDictionary)prop.GetValue(p, null);
|
||||
Type[] argumentTypes = prop.PropertyType.GetGenericArguments();
|
||||
Type keyType = argumentTypes[0];
|
||||
Type valueType = argumentTypes[1];
|
||||
|
||||
|
||||
message.AddInt(dictionary.Count);
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
|
||||
//Serialize(entry.Key, message); // Implement this method based on the type of 'Key'
|
||||
//Serialize(entry.Value, message); // Implement this method based on the type of 'Value'
|
||||
}
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Vector3))
|
||||
{
|
||||
Vector3 vector = new Vector3(message.GetFloat(), message.GetFloat(), message.GetFloat());
|
||||
prop.SetValue(p, vector);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Quaternion))
|
||||
{
|
||||
Quaternion quaternion = new Quaternion(message.GetFloat(), message.GetFloat(), message.GetFloat(), message.GetFloat());
|
||||
prop.SetValue(p, quaternion);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(Guid))
|
||||
{
|
||||
Guid guid = new Guid(message.GetBytes());
|
||||
prop.SetValue(p, guid);
|
||||
}
|
||||
// You can add more types as needed
|
||||
}
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
//if (!p.GetType().Name.Contains("Update"))
|
||||
//Main.helper.Log($"Received packet {messageReceived.MessageId} {p.GetType().Name} from {messageReceived.FromConnection.Id}");
|
||||
//Main.helper.Log("Setting packet client id to: " + messageReceived.FromConnection.Id + " for packet: " + p.GetType().Name);
|
||||
//p.clientId = messageReceived.FromConnection.Id;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Packets/IPacket.cs
Normal file
17
Packets/IPacket.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets
|
||||
{
|
||||
public interface IPacket
|
||||
{
|
||||
ushort packetId { get; }
|
||||
ushort clientId { get; set; }
|
||||
|
||||
void HandlePacketServer();
|
||||
void HandlePacketClient();
|
||||
}
|
||||
}
|
||||
32
Packets/Lobby/ChatMessage.cs
Normal file
32
Packets/Lobby/ChatMessage.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class ChatMessage : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ChatMessage;
|
||||
|
||||
public string PlayerName { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//Main.helper.Log("Received chat packet: " + Message);
|
||||
|
||||
//SendToAll(KCClient.client.Id);
|
||||
//LobbyHandler.AddChatMessage(clientId, PlayerName, Message);
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Main.helper.Log("Received chat packet: " + Message);
|
||||
|
||||
LobbyHandler.AddChatMessage(clientId, PlayerName, Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Packets/Lobby/ChatSystemMessage.cs
Normal file
26
Packets/Lobby/ChatSystemMessage.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class ChatSystemMessage : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ChatSystemMessage;
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//LobbyHandler.AddSystemMessage(Message);
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
LobbyHandler.AddSystemMessage(Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Packets/Lobby/KingdomName.cs
Normal file
38
Packets/Lobby/KingdomName.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class KingdomName : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.KingdomName;
|
||||
|
||||
public string kingdomName { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
Main.helper.Log("Received kingdom name packet");
|
||||
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
player.kingdomName = kingdomName;
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
|
||||
Main.helper.Log("Received kingdom name packet");
|
||||
|
||||
player.kingdomName = kingdomName;
|
||||
|
||||
Main.helper.Log($"Player {player.name} has joined with their kingdom {player.kingdomName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Packets/Lobby/PlayerBanner.cs
Normal file
31
Packets/Lobby/PlayerBanner.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class PlayerBanner : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.PlayerBanner;
|
||||
|
||||
public int banner { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
//player.banner = banner;
|
||||
//player.inst.PlayerLandmassOwner.SetBannerIdx(banner);
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
player.banner = banner;
|
||||
player.inst.PlayerLandmassOwner.SetBannerIdx(banner);
|
||||
|
||||
Main.helper.Log($"Player {clientId} ({player.id}) has set banner to {player.banner}");
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Packets/Lobby/PlayerList.cs
Normal file
58
Packets/Lobby/PlayerList.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class PlayerList : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.PlayerList;
|
||||
|
||||
public List<bool> playersReady { get; set; }
|
||||
public List<string> playersName { get; set; }
|
||||
public List<int> playersBanner { get; set; }
|
||||
public List<ushort> playersId { get; set; }
|
||||
public List<string> playersKingdomName { get; set; }
|
||||
|
||||
public List<string> steamIds { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
LobbyHandler.ClearPlayerList();
|
||||
|
||||
for (int i = 0; i < playersId.Count; i++)
|
||||
{
|
||||
|
||||
Main.helper.Log("PlayerList: " + playersName[i] + " " + playersId[i] + " " + steamIds[i]);
|
||||
|
||||
Main.kCPlayers.Add(steamIds[i], new KCPlayer(playersName[i], playersId[i], steamIds[i])
|
||||
{
|
||||
name = playersName[i],
|
||||
ready = playersReady[i],
|
||||
banner = playersBanner[i],
|
||||
kingdomName = playersKingdomName[i]
|
||||
});
|
||||
|
||||
|
||||
if (Main.clientSteamIds.ContainsKey(playersId[i]))
|
||||
Main.clientSteamIds[playersId[i]] = steamIds[i];
|
||||
else
|
||||
Main.clientSteamIds.Add(playersId[i], steamIds[i]);
|
||||
|
||||
Main.kCPlayers[steamIds[i]].inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
|
||||
LobbyHandler.AddPlayerEntry(playersId[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Packets/Lobby/PlayerReady.cs
Normal file
28
Packets/Lobby/PlayerReady.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class PlayerReady : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.PlayerReady;
|
||||
|
||||
public bool IsReady { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
IsReady = !player.ready;
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
player.ready = IsReady;
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
player.ready = IsReady;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Packets/Lobby/SaveTransferPacket.cs
Normal file
114
Packets/Lobby/SaveTransferPacket.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class SaveTransferPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.SaveTransferPacket;
|
||||
|
||||
public static byte[] saveData = new byte[1];
|
||||
public static bool[] chunksReceived = new bool[1];
|
||||
public static bool loadingSave = false;
|
||||
public static int received = 0;
|
||||
|
||||
|
||||
public int chunkId { get; set; }
|
||||
public int chunkSize { get; set; }
|
||||
|
||||
public int saveSize { get; set; }
|
||||
public int saveDataIndex { get; set; }
|
||||
public int totalChunks { get; set; }
|
||||
|
||||
public byte[] saveDataChunk { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
float savePercent = (float)received / (float)saveSize;
|
||||
|
||||
// Initialize saveData and chunksReceived on the first packet received
|
||||
if (saveData.Length == 1)
|
||||
{
|
||||
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
loadingSave = true;
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
|
||||
// save percentage
|
||||
|
||||
|
||||
saveData = new byte[saveSize];
|
||||
chunksReceived = new bool[totalChunks];
|
||||
}
|
||||
|
||||
|
||||
// Copy the chunk data into the correct position in saveData
|
||||
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
|
||||
|
||||
// Mark this chunk as received
|
||||
chunksReceived[chunkId] = true;
|
||||
|
||||
// Seek to the next position to write to
|
||||
received += chunkSize;
|
||||
|
||||
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
|
||||
|
||||
if (chunkId + 1 == totalChunks)
|
||||
{
|
||||
Main.helper.Log($"Received last save transfer packet.");
|
||||
|
||||
Main.helper.Log(WhichIsNotComplete());
|
||||
}
|
||||
|
||||
// Check if all chunks have been received
|
||||
if (IsTransferComplete())
|
||||
{
|
||||
// Handle completed transfer here
|
||||
Main.helper.Log("Save Transfer complete!");
|
||||
|
||||
LoadSaveLoadHook.saveBytes = saveData;
|
||||
LoadSaveLoadHook.memoryStreamHook = true;
|
||||
|
||||
LoadSave.Load();
|
||||
|
||||
|
||||
LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTransferComplete()
|
||||
{
|
||||
return chunksReceived.All(x => x == true);
|
||||
}
|
||||
|
||||
public static string WhichIsNotComplete()
|
||||
{
|
||||
string notComplete = "";
|
||||
for (int i = 0; i < chunksReceived.Length; i++)
|
||||
{
|
||||
if (!chunksReceived[i])
|
||||
{
|
||||
notComplete += i + ", ";
|
||||
}
|
||||
}
|
||||
return notComplete;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Packets/Lobby/ServerSettings.cs
Normal file
48
Packets/Lobby/ServerSettings.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class ServerSettings : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ServerSettings;
|
||||
|
||||
public string ServerName { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public string Password { get; set; }
|
||||
public int Difficulty { get; set; }
|
||||
public string WorldSeed { get; set; }
|
||||
public World.MapSize WorldSize { get; set; }
|
||||
public World.MapBias WorldType { get; set; }
|
||||
public World.MapRiverLakes WorldRivers { get; set; }
|
||||
public int PlacementType { get; set; }
|
||||
public bool FogOfWar { get; set; }
|
||||
|
||||
public ServerSettings() { this.MaxPlayers = 2; this.Password = " "; this.WorldRivers = World.MapRiverLakes.Some; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//SetServerSettings();
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
SetServerSettings();
|
||||
}
|
||||
|
||||
public void SetServerSettings()
|
||||
{
|
||||
|
||||
LobbyHandler.ServerSettings = this;
|
||||
|
||||
World.inst.mapSize = WorldSize;
|
||||
World.inst.mapBias = WorldType;
|
||||
World.inst.mapRiverLakes = WorldRivers;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Packets/Lobby/StartGame.cs
Normal file
103
Packets/Lobby/StartGame.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using KCM.Enums;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class StartGame : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.StartGame;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Main.helper.Log(GameState.inst.mainMenuMode.ToString());
|
||||
|
||||
// Hide server lobby
|
||||
Main.TransitionTo((MenuState)200);
|
||||
|
||||
// This is run when user clicks "accept" on choose your map screeen
|
||||
|
||||
try
|
||||
{
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
|
||||
try
|
||||
{
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LobbyManager.loadingSave = false;
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exception here
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Start();
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//Start();
|
||||
|
||||
|
||||
/*AIBrainsContainer.PreStartAIConfig aiConfig = new AIBrainsContainer.PreStartAIConfig();
|
||||
int count = 0;
|
||||
for (int i = 0; i < RivalKingdomSettingsUI.inst.rivalItems.Length; i++)
|
||||
{
|
||||
RivalItemUI r = RivalKingdomSettingsUI.inst.rivalItems[i];
|
||||
bool flag = r.Enabled && !r.Locked;
|
||||
if (flag)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int idx = 0;
|
||||
aiConfig.startData = new AIBrainsContainer.PreStartAIConfig.AIStartData[count];
|
||||
for (int j = 0; j < RivalKingdomSettingsUI.inst.rivalItems.Length; j++)
|
||||
{
|
||||
RivalItemUI item = RivalKingdomSettingsUI.inst.rivalItems[j];
|
||||
bool flag2 = item.Enabled && !item.Locked;
|
||||
if (flag2)
|
||||
{
|
||||
aiConfig.startData[idx] = new AIBrainsContainer.PreStartAIConfig.AIStartData();
|
||||
aiConfig.startData[idx].landmass = item.flag.landmass;
|
||||
aiConfig.startData[idx].bioCode = item.bannerIdx;
|
||||
aiConfig.startData[idx].personalityKey = PersonalityCollection.aiPersonalityKeys[0];
|
||||
aiConfig.startData[idx].skillLevel = item.GetSkillLevel();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
AIBrainsContainer.inst.aiStartInfo = aiConfig;
|
||||
bool isControllerActive = GamepadControl.inst.isControllerActive;
|
||||
if (isControllerActive)
|
||||
{
|
||||
ConsoleCursorMenu.inst.PrepForGamepad();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Packets/Lobby/WorldSeed.cs
Normal file
46
Packets/Lobby/WorldSeed.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
{
|
||||
public class WorldSeed : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.WorldSeed;
|
||||
public int Seed { get; set; }
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//SetWorldSeed();
|
||||
}
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
SetWorldSeed();
|
||||
}
|
||||
|
||||
public void SetWorldSeed()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
player.inst.Reset();
|
||||
|
||||
World.inst.Generate(Seed);
|
||||
Vector3 center = World.inst.GetCellData(World.inst.GridWidth / 2, World.inst.GridHeight / 2).Center;
|
||||
Cam.inst.SetTrackingPos(center);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Set world seed packet error");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
Main.helper.Log(e.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
Packets/Network/ClientConnected.cs
Normal file
145
Packets/Network/ClientConnected.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.Packets.Lobby;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Network
|
||||
{
|
||||
public class ClientConnected : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ClientConnected;
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string SteamId { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
|
||||
Main.helper.Log("Client Player Connected: " + Name + " Id: " + clientId + " SteamID: " + SteamId);
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(SteamId, out player))
|
||||
{
|
||||
player.id = clientId;
|
||||
player.name = Name;
|
||||
player.steamId = SteamId;
|
||||
}
|
||||
else
|
||||
Main.kCPlayers.Add(SteamId, new KCPlayer(Name, clientId, SteamId));
|
||||
|
||||
|
||||
if (Main.clientSteamIds.ContainsKey(clientId))
|
||||
Main.clientSteamIds[clientId] = SteamId;
|
||||
else
|
||||
Main.clientSteamIds.Add(clientId, SteamId);
|
||||
|
||||
|
||||
if (!SaveTransferPacket.loadingSave)
|
||||
LobbyHandler.AddPlayerEntry(clientId);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
Main.helper.Log("Server Player Connected: " + Name + " Id: " + clientId + " SteamID: " + SteamId);
|
||||
|
||||
List<KCPlayer> list = Main.kCPlayers.Select(x => x.Value).OrderBy(x => x.id).ToList();
|
||||
|
||||
if (list.Count > 0)
|
||||
new PlayerList()
|
||||
{
|
||||
playersBanner = list.Select(x => x.banner).ToList(),
|
||||
playersReady = list.Select(x => x.ready).ToList(),
|
||||
playersName = list.Select(x => x.name).ToList(),
|
||||
playersId = list.Select(x => x.id).ToList(),
|
||||
playersKingdomName = list.Select(x => x.kingdomName).ToList(),
|
||||
steamIds = list.Select(x => x.steamId).ToList()
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
Message = $"{Name} has joined the server."
|
||||
}.SendToAll();
|
||||
|
||||
LobbyHandler.ServerSettings.SendToAll(KCClient.client.Id);
|
||||
|
||||
|
||||
if (LobbyManager.loadingSave)
|
||||
{
|
||||
if (clientId == KCClient.client.Id)
|
||||
return;
|
||||
|
||||
byte[] bytes = LoadSaveLoadAtPathHook.saveData;
|
||||
int chunkSize = 900; // 900 bytes per chunk to fit within packet size limit
|
||||
|
||||
List<byte[]> chunks = SplitByteArrayIntoChunks(bytes, chunkSize);
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
|
||||
int sent = 0;
|
||||
int packetsSent = 0;
|
||||
|
||||
for (int i = 0; i < chunks.Count; i++)
|
||||
{
|
||||
var chunk = chunks[i];
|
||||
|
||||
|
||||
new SaveTransferPacket()
|
||||
{
|
||||
saveSize = bytes.Length,
|
||||
saveDataChunk = chunk,
|
||||
chunkId = i,
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = chunks.Count
|
||||
}.Send(clientId);
|
||||
|
||||
Main.helper.Log(" ");
|
||||
|
||||
packetsSent++;
|
||||
sent += chunk.Length;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Sent {packetsSent} save data chunks to client");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
new WorldSeed()
|
||||
{
|
||||
Seed = World.inst.seed
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<byte[]> SplitByteArrayIntoChunks(byte[] source, int chunkSize)
|
||||
{
|
||||
var chunks = new List<byte[]>();
|
||||
int sourceLength = source.Length;
|
||||
|
||||
for (int i = 0; i < sourceLength; i += chunkSize)
|
||||
{
|
||||
// Calculate the length of the current chunk, as the last chunk may be smaller than chunkSize
|
||||
int currentChunkSize = Math.Min(chunkSize, sourceLength - i);
|
||||
|
||||
// Create a chunk array of the correct size
|
||||
byte[] chunk = new byte[currentChunkSize];
|
||||
|
||||
// Copy a segment of the source array into the chunk array
|
||||
Array.Copy(source, i, chunk, 0, currentChunkSize);
|
||||
|
||||
// Add the chunk to the list of chunks
|
||||
chunks.Add(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Packets/Network/ServerHandshake.cs
Normal file
69
Packets/Network/ServerHandshake.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Lobby;
|
||||
using Riptide;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace KCM.Packets.Network
|
||||
{
|
||||
public class ServerHandshake : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ServerHandshake;
|
||||
|
||||
public bool loadingSave { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
ModalManager.HideModal();
|
||||
|
||||
Main.TransitionTo(Enums.MenuState.ServerLobby);
|
||||
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
Cam.inst.desiredDist = 80f;
|
||||
Cam.inst.desiredPhi = 45f;
|
||||
CloudSystem.inst.threshold1 = 0.6f;
|
||||
CloudSystem.inst.threshold2 = 0.8f;
|
||||
CloudSystem.inst.BaseFreq = 4.5f;
|
||||
Weather.inst.SetSeason(Weather.Season.Summer);
|
||||
|
||||
//inst = new KCClient(KCServer.IsRunning ? "Ryan" : "Orion");
|
||||
KCClient.inst = new KCClient(SteamFriends.GetPersonaName());
|
||||
|
||||
Main.helper.Log("Sending client connected. Client ID is: " + clientId);
|
||||
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID));
|
||||
|
||||
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;
|
||||
|
||||
if (loadingSave && KCServer.IsRunning)
|
||||
Main.TransitionTo(MenuState.Load);
|
||||
else if (!loadingSave)
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);
|
||||
|
||||
}
|
||||
|
||||
|
||||
new KingdomName() { kingdomName = TownNameUI.inst.townName, clientId = clientId }.Send();
|
||||
|
||||
new ClientConnected()
|
||||
{
|
||||
clientId = clientId,
|
||||
Name = KCClient.inst.Name,
|
||||
SteamId = Main.PlayerSteamID
|
||||
}.Send();
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Packets/Packet.cs
Normal file
113
Packets/Packet.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using KCM.Packets.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets
|
||||
{
|
||||
public abstract class Packet : IPacket
|
||||
{
|
||||
public abstract ushort packetId { get; }
|
||||
public ushort clientId { get; set; }
|
||||
|
||||
public KCPlayer player
|
||||
{
|
||||
get
|
||||
{
|
||||
KCPlayer p = null;
|
||||
|
||||
if (!Main.clientSteamIds.ContainsKey(clientId))
|
||||
return null;
|
||||
|
||||
//Main.helper.Log($"SteamID: {Main.GetPlayerByClientID(clientId).steamId} for {clientId} ({Main.GetPlayerByClientID(clientId).id})");
|
||||
|
||||
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
|
||||
return p;
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SendToAll(ushort exceptToClient = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (exceptToClient == 0)
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.SendToAll(PacketHandler.SerialisePacket(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (KCServer.IsRunning && exceptToClient != 0)
|
||||
KCServer.server.SendToAll(PacketHandler.SerialisePacket(this), exceptToClient);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error sending packet to all {packetId} {this.GetType().Name} from {clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(ushort toClient = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCClient.client.IsConnected && toClient == 0)
|
||||
{
|
||||
this.clientId = KCClient.client.Id;
|
||||
KCClient.client.Send(PacketHandler.SerialisePacket(this));
|
||||
}
|
||||
else if (KCServer.IsRunning && toClient != 0)
|
||||
{
|
||||
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error sending packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void HandlePacketServer();
|
||||
public abstract void HandlePacketClient();
|
||||
}
|
||||
}
|
||||
30
Packets/ShowModal.cs
Normal file
30
Packets/ShowModal.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.Packets
|
||||
{
|
||||
public class ShowModal : Packet
|
||||
{
|
||||
public override ushort packetId => (int)Enums.Packets.ShowModal;
|
||||
|
||||
public string title { get; set; }
|
||||
public string message { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Main.helper.Log("Opening Modal");
|
||||
Main.helper.Log("Title: " + title);
|
||||
Main.helper.Log("Message: " + message);
|
||||
|
||||
ModalManager.ShowModal(title, message);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Packets/State/BuildingStatePacket.cs
Normal file
115
Packets/State/BuildingStatePacket.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.State
|
||||
{
|
||||
public class BuildingStatePacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.BuildingStatePacket;
|
||||
|
||||
public string customName { get; set; }
|
||||
public Guid guid { get; set; }
|
||||
public string uniqueName { get; set; }
|
||||
public Quaternion rotation { get; set; }
|
||||
public Vector3 globalPosition { get; set; }
|
||||
public Vector3 localPosition { get; set; }
|
||||
public bool built { get; set; }
|
||||
public bool placed { get; set; }
|
||||
public bool open { get; set; }
|
||||
public bool doBuildAnimation { get; set; }
|
||||
public bool constructionPaused { get; set; }
|
||||
public float constructionProgress { get; set; }
|
||||
public float resourceProgress { get; set; }
|
||||
public float life { get; set; }
|
||||
public float ModifiedMaxLife { get; set; }
|
||||
public int yearBuilt { get; set; }
|
||||
public float decayProtection { get; set; }
|
||||
public bool seenByPlayer { get; set; }
|
||||
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) return; //prevent double placing on same client
|
||||
|
||||
//Main.helper.Log("Received building state packet for: " + uniqueName + " from " + Main.kCPlayers[Main.GetPlayerByClientID(clientId).steamId].name + $"({clientId})");
|
||||
|
||||
|
||||
Building building = player.inst.GetBuilding(guid);
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
Main.helper.Log("Building not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//PrintProperties();
|
||||
|
||||
building.UniqueName = uniqueName;
|
||||
building.customName = customName;
|
||||
|
||||
|
||||
building.transform.position = this.globalPosition;
|
||||
building.transform.GetChild(0).rotation = this.rotation;
|
||||
building.transform.GetChild(0).localPosition = this.localPosition;
|
||||
|
||||
SetPrivateFieldValue(building, "built", built);
|
||||
SetPrivateFieldValue(building, "placed", placed);
|
||||
SetPrivateFieldValue(building, "resourceProgress", resourceProgress);
|
||||
|
||||
|
||||
building.Open = open;
|
||||
building.doBuildAnimation = doBuildAnimation;
|
||||
building.constructionPaused = constructionPaused;
|
||||
building.constructionProgress = constructionProgress;
|
||||
building.Life = life;
|
||||
building.ModifiedMaxLife = ModifiedMaxLife;
|
||||
|
||||
|
||||
//building.yearBuilt = yearBuilt;
|
||||
SetPrivateFieldValue(building, "yearBuilt", yearBuilt);
|
||||
|
||||
building.decayProtection = decayProtection;
|
||||
//building.seenByPlayer = seenByPlayer;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error setting building state");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void SetPrivateFieldValue(object obj, string fieldName, object value)
|
||||
{
|
||||
Type type = obj.GetType();
|
||||
FieldInfo field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
field.SetValue(obj, value);
|
||||
}
|
||||
|
||||
public void PrintProperties()
|
||||
{
|
||||
Type type = typeof(BuildingStatePacket);
|
||||
|
||||
foreach (PropertyInfo property in type.GetProperties())
|
||||
{
|
||||
object value = property.GetValue(this);
|
||||
string propertyName = property.Name;
|
||||
|
||||
Main.helper.Log($"{propertyName}: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
54
PrefabManager.cs
Normal file
54
PrefabManager.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class PrefabManager
|
||||
{
|
||||
public static AssetBundle assetBundle;
|
||||
public static GameObject serverBrowserPrefab;
|
||||
public static GameObject serverEntryItemPrefab;
|
||||
|
||||
public static GameObject serverLobbyPrefab;
|
||||
public static GameObject serverLobbyPlayerEntryPrefab;
|
||||
public static GameObject serverChatEntryPrefab;
|
||||
public static GameObject serverChatSystemEntryPrefab;
|
||||
|
||||
public static GameObject modalUIPrefab;
|
||||
|
||||
public void PreScriptLoad(KCModHelper _helper)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Main.helper = _helper;
|
||||
|
||||
assetBundle = KCModHelper.LoadAssetBundle(_helper.modPath, "serverbrowserpkg");
|
||||
|
||||
Main.helper.Log(String.Join(", ", assetBundle.GetAllAssetNames()));
|
||||
|
||||
serverBrowserPrefab = assetBundle.LoadAsset("assets/workspace/serverbrowser.prefab") as GameObject;
|
||||
serverEntryItemPrefab = assetBundle.LoadAsset("assets/workspace/serverentryitem.prefab") as GameObject;
|
||||
|
||||
|
||||
serverLobbyPrefab = assetBundle.LoadAsset("assets/workspace/serverlobby.prefab") as GameObject;
|
||||
serverLobbyPlayerEntryPrefab = assetBundle.LoadAsset("assets/workspace/serverlobbyplayerentry.prefab") as GameObject;
|
||||
serverChatEntryPrefab = assetBundle.LoadAsset("assets/workspace/serverchatentry.prefab") as GameObject;
|
||||
serverChatSystemEntryPrefab = assetBundle.LoadAsset("assets/workspace/serverchatsystementry.prefab") as GameObject;
|
||||
|
||||
modalUIPrefab = assetBundle.LoadAsset("assets/workspace/modalui.prefab") as GameObject;
|
||||
|
||||
Main.helper.Log("Loaded assets");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ReflectionHelper/ReflectionHelper.cs
Normal file
39
ReflectionHelper/ReflectionHelper.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
public static class ReflectionHelper
|
||||
{
|
||||
public static void ClearPrivateListField<T>(object classInstance, string fieldName)
|
||||
{
|
||||
// Get the Type object representing the class of the instance
|
||||
Type classType = classInstance.GetType();
|
||||
|
||||
// Get the FieldInfo for the specified field name, assuming it's private and an instance field
|
||||
FieldInfo fieldInfo = classType.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
// Get the value of the field (the instance of the list) from the class instance
|
||||
object fieldValue = fieldInfo.GetValue(classInstance);
|
||||
|
||||
// Check if the field is actually a List<T>
|
||||
if (fieldValue is List<T> listInstance)
|
||||
{
|
||||
// Get the MethodInfo for the Clear method
|
||||
MethodInfo clearMethodInfo = typeof(List<T>).GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Invoke the Clear method on the list instance
|
||||
clearMethodInfo?.Invoke(listInstance, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("The specified field is not a List<T>.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("The specified field was not found in the class instance.", nameof(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
418
Riptide/Client.cs
Normal file
418
Riptide/Client.cs
Normal file
@@ -0,0 +1,418 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>A client that can connect to a <see cref="Server"/>.</summary>
|
||||
public class Client : Peer
|
||||
{
|
||||
/// <summary>Invoked when a connection to the server is established.</summary>
|
||||
public event EventHandler Connected;
|
||||
/// <summary>Invoked when a connection to the server fails to be established.</summary>
|
||||
public event EventHandler<ConnectionFailedEventArgs> ConnectionFailed;
|
||||
/// <summary>Invoked when a message is received.</summary>
|
||||
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
||||
/// <summary>Invoked when disconnected from the server.</summary>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
/// <summary>Invoked when another <i>non-local</i> client connects.</summary>
|
||||
public event EventHandler<ClientConnectedEventArgs> ClientConnected;
|
||||
/// <summary>Invoked when another <i>non-local</i> client disconnects.</summary>
|
||||
public event EventHandler<ClientDisconnectedEventArgs> ClientDisconnected;
|
||||
|
||||
/// <summary>The client's numeric ID.</summary>
|
||||
public ushort Id => connection.Id;
|
||||
/// <inheritdoc cref="Connection.RTT"/>
|
||||
public short RTT => connection.RTT;
|
||||
/// <inheritdoc cref="Connection.SmoothRTT"/>
|
||||
/// <remarks>This value is slower to accurately represent lasting changes in latency than <see cref="RTT"/>, but it is less susceptible to changing drastically due to significant—but temporary—jumps in latency.</remarks>
|
||||
public short SmoothRTT => connection.SmoothRTT;
|
||||
/// <summary>Sets the client's <see cref="Connection.TimeoutTime"/>.</summary>
|
||||
public override int TimeoutTime
|
||||
{
|
||||
set
|
||||
{
|
||||
defaultTimeout = value;
|
||||
connection.TimeoutTime = defaultTimeout;
|
||||
}
|
||||
}
|
||||
/// <summary>Whether or not the client is currently <i>not</i> trying to connect, pending, nor actively connected.</summary>
|
||||
public bool IsNotConnected => connection is null || connection.IsNotConnected;
|
||||
/// <summary>Whether or not the client is currently in the process of connecting.</summary>
|
||||
public bool IsConnecting => !(connection is null) && connection.IsConnecting;
|
||||
/// <summary>Whether or not the client's connection is currently pending (waiting to be accepted/rejected by the server).</summary>
|
||||
public bool IsPending => !(connection is null) && connection.IsPending;
|
||||
/// <summary>Whether or not the client is currently connected.</summary>
|
||||
public bool IsConnected => !(connection is null) && connection.IsConnected;
|
||||
/// <summary>The client's connection to a server.</summary>
|
||||
// Not an auto property because properties can't be passed as ref/out parameters. Could
|
||||
// use a local variable in the Connect method, but that's arguably not any cleaner. This
|
||||
// property will also probably only be used rarely from outside the class/library.
|
||||
public Connection Connection => connection;
|
||||
/// <summary>Encapsulates a method that handles a message from a server.</summary>
|
||||
/// <param name="message">The message that was received.</param>
|
||||
public delegate void MessageHandler(Message message);
|
||||
|
||||
/// <inheritdoc cref="Connection"/>
|
||||
private Connection connection;
|
||||
/// <summary>How many connection attempts have been made so far.</summary>
|
||||
private int connectionAttempts;
|
||||
/// <summary>How many connection attempts to make before giving up.</summary>
|
||||
private int maxConnectionAttempts;
|
||||
/// <inheritdoc cref="Server.messageHandlers"/>
|
||||
private Dictionary<ushort, MessageHandler> messageHandlers;
|
||||
/// <summary>The underlying transport's client that is used for sending and receiving data.</summary>
|
||||
private IClient transport;
|
||||
/// <summary>The message sent when connecting. May include custom data.</summary>
|
||||
private Message connectMessage;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
/// <param name="transport">The transport to use for sending and receiving data.</param>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Client(IClient transport, string logName = "CLIENT") : base(logName)
|
||||
{
|
||||
this.transport = transport;
|
||||
}
|
||||
/// <summary>Handles initial setup using the built-in UDP transport.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Client(string logName = "CLIENT") : this(new Transports.Udp.UdpClient(), logName) { }
|
||||
|
||||
/// <summary>Disconnects the client if it's connected and swaps out the transport it's using.</summary>
|
||||
/// <param name="newTransport">The new transport to use for sending and receiving data.</param>
|
||||
/// <remarks>This method does not automatically reconnect to the server. To continue communicating with the server, <see cref="Connect(string, int, byte, Message, bool)"/> must be called again.</remarks>
|
||||
public void ChangeTransport(IClient newTransport)
|
||||
{
|
||||
Disconnect();
|
||||
transport = newTransport;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to connect to a server at the given host address.</summary>
|
||||
/// <param name="hostAddress">The host address to connect to.</param>
|
||||
/// <param name="maxConnectionAttempts">How many connection attempts to make before giving up.</param>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to use when building <see cref="messageHandlers"/>.</param>
|
||||
/// <param name="message">Data that should be sent to the server with the connection attempt. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
/// <param name="useMessageHandlers">Whether or not the client should use the built-in message handler system.</param>
|
||||
/// <remarks>
|
||||
/// <para>Riptide's default transport expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>. If you are using a different transport, check the relevant documentation for what information it requires in the host address.</para>
|
||||
/// <para>Setting <paramref name="useMessageHandlers"/> to <see langword="false"/> will disable the automatic detection and execution of methods with the <see cref="MessageHandlerAttribute"/>, which is beneficial if you prefer to handle messages via the <see cref="MessageReceived"/> event.</para>
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> if a connection attempt will be made. <see langword="false"/> if an issue occurred (such as <paramref name="hostAddress"/> being in an invalid format) and a connection attempt will <i>not</i> be made.</returns>
|
||||
public bool Connect(string hostAddress, int maxConnectionAttempts = 5, byte messageHandlerGroupId = 0, Message message = null, bool useMessageHandlers = true)
|
||||
{
|
||||
Disconnect();
|
||||
|
||||
SubToTransportEvents();
|
||||
|
||||
if (!transport.Connect(hostAddress, out connection, out string connectError))
|
||||
{
|
||||
RiptideLogger.Log(LogType.Error, LogName, connectError);
|
||||
UnsubFromTransportEvents();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.maxConnectionAttempts = maxConnectionAttempts;
|
||||
connectionAttempts = 0;
|
||||
connection.Initialize(this, defaultTimeout);
|
||||
IncreaseActiveCount();
|
||||
this.useMessageHandlers = useMessageHandlers;
|
||||
if (useMessageHandlers)
|
||||
CreateMessageHandlersDictionary(messageHandlerGroupId);
|
||||
|
||||
connectMessage = Message.Create(MessageHeader.Connect);
|
||||
if (message != null)
|
||||
{
|
||||
if (message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting connection attempt data!");
|
||||
|
||||
connectMessage.AddMessage(message);
|
||||
message.Release();
|
||||
}
|
||||
|
||||
StartTime();
|
||||
Heartbeat();
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Connecting to {connection}...");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Subscribes appropriate methods to the transport's events.</summary>
|
||||
private void SubToTransportEvents()
|
||||
{
|
||||
transport.Connected += TransportConnected;
|
||||
transport.ConnectionFailed += TransportConnectionFailed;
|
||||
transport.DataReceived += HandleData;
|
||||
transport.Disconnected += TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes methods from all of the transport's events.</summary>
|
||||
private void UnsubFromTransportEvents()
|
||||
{
|
||||
transport.Connected -= TransportConnected;
|
||||
transport.ConnectionFailed -= TransportConnectionFailed;
|
||||
transport.DataReceived -= HandleData;
|
||||
transport.Disconnected -= TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreateMessageHandlersDictionary(byte messageHandlerGroupId)
|
||||
{
|
||||
MethodInfo[] methods = FindMessageHandlers();
|
||||
|
||||
messageHandlers = new Dictionary<ushort, MessageHandler>(methods.Length);
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>();
|
||||
if (attribute.GroupId != messageHandlerGroupId)
|
||||
continue;
|
||||
|
||||
if (!method.IsStatic)
|
||||
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
||||
|
||||
Delegate clientMessageHandler = Delegate.CreateDelegate(typeof(MessageHandler), method, false);
|
||||
if (clientMessageHandler != null)
|
||||
{
|
||||
// It's a message handler for Client instances
|
||||
if (messageHandlers.ContainsKey(attribute.MessageId))
|
||||
{
|
||||
MethodInfo otherMethodWithId = messageHandlers[attribute.MessageId].GetMethodInfo();
|
||||
throw new DuplicateHandlerException(attribute.MessageId, method, otherMethodWithId);
|
||||
}
|
||||
else
|
||||
messageHandlers.Add(attribute.MessageId, (MessageHandler)clientMessageHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's not a message handler for Client instances, but it might be one for Server instances
|
||||
if (Delegate.CreateDelegate(typeof(Server.MessageHandler), method, false) == null)
|
||||
throw new InvalidHandlerSignatureException(method.DeclaringType, method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Heartbeat()
|
||||
{
|
||||
if (IsConnecting)
|
||||
{
|
||||
// If still trying to connect, send connect messages instead of heartbeats
|
||||
if (connectionAttempts < maxConnectionAttempts)
|
||||
{
|
||||
Send(connectMessage, false);
|
||||
connectionAttempts++;
|
||||
}
|
||||
else
|
||||
LocalDisconnect(DisconnectReason.NeverConnected);
|
||||
}
|
||||
else if (IsPending)
|
||||
{
|
||||
// If waiting for the server to accept/reject the connection attempt
|
||||
if (connection.HasConnectAttemptTimedOut)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.TimedOut);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (IsConnected)
|
||||
{
|
||||
// If connected and not timed out, send heartbeats
|
||||
if (connection.HasTimedOut)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.TimedOut);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.SendHeartbeat();
|
||||
}
|
||||
|
||||
ExecuteLater(HeartbeatInterval, new HeartbeatEvent(this));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
transport.Poll();
|
||||
HandleMessages();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Handle(Message message, MessageHeader header, Connection connection)
|
||||
{
|
||||
switch (header)
|
||||
{
|
||||
// User messages
|
||||
case MessageHeader.Unreliable:
|
||||
case MessageHeader.Reliable:
|
||||
OnMessageReceived(message);
|
||||
break;
|
||||
|
||||
// Internal messages
|
||||
case MessageHeader.Ack:
|
||||
connection.HandleAck(message);
|
||||
break;
|
||||
case MessageHeader.Connect:
|
||||
connection.SetPending();
|
||||
break;
|
||||
case MessageHeader.Reject:
|
||||
if (!IsConnected) // Don't disconnect if we are connected
|
||||
LocalDisconnect(DisconnectReason.ConnectionRejected, message, (RejectReason)message.GetByte());
|
||||
break;
|
||||
case MessageHeader.Heartbeat:
|
||||
connection.HandleHeartbeatResponse(message);
|
||||
break;
|
||||
case MessageHeader.Disconnect:
|
||||
LocalDisconnect((DisconnectReason)message.GetByte(), message);
|
||||
break;
|
||||
case MessageHeader.Welcome:
|
||||
if (IsConnecting || IsPending)
|
||||
{
|
||||
connection.HandleWelcome(message);
|
||||
OnConnected();
|
||||
}
|
||||
break;
|
||||
case MessageHeader.ClientConnected:
|
||||
OnClientConnected(message.GetUShort());
|
||||
break;
|
||||
case MessageHeader.ClientDisconnected:
|
||||
OnClientDisconnected(message.GetUShort());
|
||||
break;
|
||||
default:
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Unexpected message header '{header}'! Discarding {message.BytesInUse} bytes.");
|
||||
break;
|
||||
}
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Sends a message to the server.</summary>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public ushort Send(Message message, bool shouldRelease = true) => connection.Send(message, shouldRelease);
|
||||
|
||||
/// <summary>Disconnects from the server.</summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (connection == null || IsNotConnected)
|
||||
return;
|
||||
|
||||
Send(Message.Create(MessageHeader.Disconnect));
|
||||
LocalDisconnect(DisconnectReason.Disconnected);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Disconnect(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
if (connection.IsConnected && connection.CanQualityDisconnect)
|
||||
LocalDisconnect(reason);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the connection.</summary>
|
||||
/// <param name="reason">The reason why the client has disconnected.</param>
|
||||
/// <param name="message">The disconnection or rejection message, potentially containing extra data to be handled externally.</param>
|
||||
/// <param name="rejectReason">The reason why the connection was rejected (<i>if</i> it was rejected).</param>
|
||||
private void LocalDisconnect(DisconnectReason reason, Message message = null, RejectReason rejectReason = RejectReason.NoConnection)
|
||||
{
|
||||
if (IsNotConnected)
|
||||
return;
|
||||
|
||||
UnsubFromTransportEvents();
|
||||
DecreaseActiveCount();
|
||||
|
||||
StopTime();
|
||||
transport.Disconnect();
|
||||
|
||||
connection.LocalDisconnect();
|
||||
|
||||
if (reason == DisconnectReason.NeverConnected)
|
||||
OnConnectionFailed(RejectReason.NoConnection);
|
||||
else if (reason == DisconnectReason.ConnectionRejected)
|
||||
OnConnectionFailed(rejectReason, message);
|
||||
else
|
||||
OnDisconnected(reason, message);
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport establishes a connection.</summary>
|
||||
private void TransportConnected(object sender, EventArgs e) { }
|
||||
|
||||
/// <summary>What to do when the transport fails to connect.</summary>
|
||||
private void TransportConnectionFailed(object sender, EventArgs e)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.NeverConnected);
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport disconnects.</summary>
|
||||
private void TransportDisconnected(object sender, Transports.DisconnectedEventArgs e)
|
||||
{
|
||||
if (connection == e.Connection)
|
||||
LocalDisconnect(e.Reason);
|
||||
}
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
connectMessage.Release();
|
||||
connectMessage = null;
|
||||
RiptideLogger.Log(LogType.Info, LogName, "Connected successfully!");
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
/// <param name="reason">The reason for the connection failure.</param>
|
||||
/// <param name="message">Additional data related to the failed connection attempt.</param>
|
||||
protected virtual void OnConnectionFailed(RejectReason reason, Message message = null)
|
||||
{
|
||||
connectMessage.Release();
|
||||
connectMessage = null;
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Connection to server failed: {Helper.GetReasonString(reason)}.");
|
||||
ConnectionFailed?.Invoke(this, new ConnectionFailedEventArgs(reason, message));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="MessageReceived"/> event and initiates handling of the received message.</summary>
|
||||
/// <param name="message">The received message.</param>
|
||||
protected virtual void OnMessageReceived(Message message)
|
||||
{
|
||||
ushort messageId = (ushort)message.GetVarULong();
|
||||
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(connection, messageId, message));
|
||||
|
||||
if (useMessageHandlers)
|
||||
{
|
||||
if (messageHandlers.TryGetValue(messageId, out MessageHandler messageHandler))
|
||||
messageHandler(message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"No message handler method found for message ID {messageId}!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
/// <param name="message">Additional data related to the disconnection.</param>
|
||||
protected virtual void OnDisconnected(DisconnectReason reason, Message message)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Disconnected from server: {Helper.GetReasonString(reason)}.");
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(reason, message));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientConnected"/> event.</summary>
|
||||
/// <param name="clientId">The numeric ID of the client that connected.</param>
|
||||
protected virtual void OnClientConnected(ushort clientId)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} connected.");
|
||||
ClientConnected?.Invoke(this, new ClientConnectedEventArgs(clientId));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientDisconnected"/> event.</summary>
|
||||
/// <param name="clientId">The numeric ID of the client that disconnected.</param>
|
||||
protected virtual void OnClientDisconnected(ushort clientId)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} disconnected.");
|
||||
ClientDisconnected?.Invoke(this, new ClientDisconnectedEventArgs(clientId));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
648
Riptide/Connection.cs
Normal file
648
Riptide/Connection.cs
Normal file
@@ -0,0 +1,648 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The state of a connection.</summary>
|
||||
internal enum ConnectionState : byte
|
||||
{
|
||||
/// <summary>Not connected. No connection has been established or the connection has been closed.</summary>
|
||||
NotConnected,
|
||||
/// <summary>Connecting. Still trying to establish a connection.</summary>
|
||||
Connecting,
|
||||
/// <summary>Connection is pending. The server is still determining whether or not the connection should be allowed.</summary>
|
||||
Pending,
|
||||
/// <summary>Connected. A connection has been established successfully.</summary>
|
||||
Connected,
|
||||
}
|
||||
|
||||
/// <summary>Represents a connection to a <see cref="Server"/> or <see cref="Client"/>.</summary>
|
||||
public abstract class Connection
|
||||
{
|
||||
/// <summary>Invoked when the notify message with the given sequence ID is successfully delivered.</summary>
|
||||
public Action<ushort> NotifyDelivered;
|
||||
/// <summary>Invoked when the notify message with the given sequence ID is lost.</summary>
|
||||
public Action<ushort> NotifyLost;
|
||||
/// <summary>Invoked when a notify message is received.</summary>
|
||||
public Action<Message> NotifyReceived;
|
||||
/// <summary>Invoked when the reliable message with the given sequence ID is successfully delivered.</summary>
|
||||
public Action<ushort> ReliableDelivered;
|
||||
|
||||
/// <summary>The connection's numeric ID.</summary>
|
||||
public ushort Id { get; internal set; }
|
||||
/// <summary>Whether or not the connection is currently <i>not</i> trying to connect, pending, nor actively connected.</summary>
|
||||
public bool IsNotConnected => state == ConnectionState.NotConnected;
|
||||
/// <summary>Whether or not the connection is currently in the process of connecting.</summary>
|
||||
public bool IsConnecting => state == ConnectionState.Connecting;
|
||||
/// <summary>Whether or not the connection is currently pending (waiting to be accepted/rejected by the server).</summary>
|
||||
public bool IsPending => state == ConnectionState.Pending;
|
||||
/// <summary>Whether or not the connection is currently connected.</summary>
|
||||
public bool IsConnected => state == ConnectionState.Connected;
|
||||
/// <summary>The round trip time (ping) of the connection, in milliseconds. -1 if not calculated yet.</summary>
|
||||
public short RTT
|
||||
{
|
||||
get => _rtt;
|
||||
private set
|
||||
{
|
||||
SmoothRTT = _rtt == -1 ? value : (short)Math.Max(1f, SmoothRTT * 0.7f + value * 0.3f);
|
||||
_rtt = value;
|
||||
}
|
||||
}
|
||||
private short _rtt;
|
||||
/// <summary>The smoothed round trip time (ping) of the connection, in milliseconds. -1 if not calculated yet.</summary>
|
||||
/// <remarks>This value is slower to accurately represent lasting changes in latency than <see cref="RTT"/>, but it is less susceptible to changing drastically due to significant—but temporary—jumps in latency.</remarks>
|
||||
public short SmoothRTT { get; private set; }
|
||||
/// <summary>The time (in milliseconds) after which to disconnect if no heartbeats are received.</summary>
|
||||
public int TimeoutTime { get; set; }
|
||||
/// <summary>Whether or not the connection can time out.</summary>
|
||||
public bool CanTimeout
|
||||
{
|
||||
get => _canTimeout;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
ResetTimeout();
|
||||
|
||||
_canTimeout = value;
|
||||
}
|
||||
}
|
||||
private bool _canTimeout;
|
||||
/// <summary>Whether or not the connection can disconnect due to poor connection quality.</summary>
|
||||
/// <remarks>When this is set to <see langword="false"/>, <see cref="MaxAvgSendAttempts"/>, <see cref="MaxSendAttempts"/>,
|
||||
/// and <see cref="MaxNotifyLoss"/> are ignored and exceeding their values will not trigger a disconnection.</remarks>
|
||||
public bool CanQualityDisconnect;
|
||||
/// <summary>The connection's metrics.</summary>
|
||||
public readonly ConnectionMetrics Metrics;
|
||||
/// <summary>The maximum acceptable average number of send attempts it takes to deliver a reliable message. The connection
|
||||
/// will be closed if this is exceeded more than <see cref="AvgSendAttemptsResilience"/> times in a row.</summary>
|
||||
public int MaxAvgSendAttempts;
|
||||
/// <summary>How many consecutive times <see cref="MaxAvgSendAttempts"/> can be exceeded before triggering a disconnect.</summary>
|
||||
public int AvgSendAttemptsResilience;
|
||||
/// <summary>The absolute maximum number of times a reliable message may be sent. A single message reaching this threshold will cause a disconnection.</summary>
|
||||
public int MaxSendAttempts;
|
||||
/// <summary>The maximum acceptable loss rate of notify messages. The connection will be closed if this is exceeded more than <see cref="NotifyLossResilience"/> times in a row.</summary>
|
||||
public float MaxNotifyLoss;
|
||||
/// <summary>How many consecutive times <see cref="MaxNotifyLoss"/> can be exceeded before triggering a disconnect.</summary>
|
||||
public int NotifyLossResilience;
|
||||
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
internal Peer Peer { get; private set; }
|
||||
/// <summary>Whether or not the connection has timed out.</summary>
|
||||
internal bool HasTimedOut => _canTimeout && Peer.CurrentTime - lastHeartbeat > TimeoutTime;
|
||||
/// <summary>Whether or not the connection attempt has timed out.</summary>
|
||||
internal bool HasConnectAttemptTimedOut => _canTimeout && Peer.CurrentTime - lastHeartbeat > Peer.ConnectTimeoutTime;
|
||||
|
||||
/// <summary>The sequencer for notify messages.</summary>
|
||||
private readonly NotifySequencer notify;
|
||||
/// <summary>The sequencer for reliable messages.</summary>
|
||||
private readonly ReliableSequencer reliable;
|
||||
/// <summary>The currently pending reliably sent messages whose delivery has not been acknowledged yet. Stored by sequence ID.</summary>
|
||||
private readonly Dictionary<ushort, PendingMessage> pendingMessages;
|
||||
/// <summary>The connection's current state.</summary>
|
||||
private ConnectionState state;
|
||||
/// <summary>The number of consecutive times that the <see cref="MaxAvgSendAttempts"/> threshold was exceeded.</summary>
|
||||
private int sendAttemptsViolations;
|
||||
/// <summary>The number of consecutive times that the <see cref="MaxNotifyLoss"/> threshold was exceeded.</summary>
|
||||
private int lossRateViolations;
|
||||
/// <summary>The time at which the last heartbeat was received from the other end.</summary>
|
||||
private long lastHeartbeat;
|
||||
/// <summary>The ID of the last ping that was sent.</summary>
|
||||
private byte lastPingId;
|
||||
/// <summary>The ID of the currently pending ping.</summary>
|
||||
private byte pendingPingId;
|
||||
/// <summary>The time at which the currently pending ping was sent.</summary>
|
||||
private long pendingPingSendTime;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
protected Connection()
|
||||
{
|
||||
Metrics = new ConnectionMetrics();
|
||||
notify = new NotifySequencer(this);
|
||||
reliable = new ReliableSequencer(this);
|
||||
state = ConnectionState.Connecting;
|
||||
_rtt = -1;
|
||||
SmoothRTT = -1;
|
||||
_canTimeout = true;
|
||||
CanQualityDisconnect = true;
|
||||
MaxAvgSendAttempts = 5;
|
||||
AvgSendAttemptsResilience = 64;
|
||||
MaxSendAttempts = 15;
|
||||
MaxNotifyLoss = 0.05f; // 5%
|
||||
NotifyLossResilience = 64;
|
||||
pendingMessages = new Dictionary<ushort, PendingMessage>();
|
||||
}
|
||||
|
||||
/// <summary>Initializes connection data.</summary>
|
||||
/// <param name="peer">The <see cref="Riptide.Peer"/> which this connection belongs to.</param>
|
||||
/// <param name="timeoutTime">The timeout time.</param>
|
||||
internal void Initialize(Peer peer, int timeoutTime)
|
||||
{
|
||||
Peer = peer;
|
||||
TimeoutTime = timeoutTime;
|
||||
}
|
||||
|
||||
/// <summary>Resets the connection's timeout time.</summary>
|
||||
public void ResetTimeout()
|
||||
{
|
||||
lastHeartbeat = Peer.CurrentTime;
|
||||
}
|
||||
|
||||
/// <summary>Sends a message.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <returns>For reliable and notify messages, the sequence ID that the message was sent with. 0 for unreliable messages.</returns>
|
||||
/// <remarks>
|
||||
/// If you intend to continue using the message instance after calling this method, you <i>must</i> set <paramref name="shouldRelease"/>
|
||||
/// to <see langword="false"/>. <see cref="Message.Release"/> can be used to manually return the message to the pool at a later time.
|
||||
/// </remarks>
|
||||
public ushort Send(Message message, bool shouldRelease = true)
|
||||
{
|
||||
ushort sequenceId = 0;
|
||||
if (message.SendMode == MessageSendMode.Notify)
|
||||
{
|
||||
sequenceId = notify.InsertHeader(message);
|
||||
int byteAmount = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, Message.ByteBuffer, 0, byteAmount);
|
||||
Send(Message.ByteBuffer, byteAmount);
|
||||
Metrics.SentNotify(byteAmount);
|
||||
}
|
||||
else if (message.SendMode == MessageSendMode.Unreliable)
|
||||
{
|
||||
int byteAmount = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, Message.ByteBuffer, 0, byteAmount);
|
||||
Send(Message.ByteBuffer, byteAmount);
|
||||
Metrics.SentUnreliable(byteAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
sequenceId = reliable.NextSequenceId;
|
||||
PendingMessage pendingMessage = PendingMessage.Create(sequenceId, message, this);
|
||||
pendingMessages.Add(sequenceId, pendingMessage);
|
||||
pendingMessage.TrySend();
|
||||
Metrics.ReliableUniques++;
|
||||
}
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
|
||||
return sequenceId;
|
||||
}
|
||||
|
||||
/// <summary>Sends data.</summary>
|
||||
/// <param name="dataBuffer">The array containing the data.</param>
|
||||
/// <param name="amount">The number of bytes in the array which should be sent.</param>
|
||||
protected internal abstract void Send(byte[] dataBuffer, int amount);
|
||||
|
||||
/// <summary>Processes a notify message.</summary>
|
||||
/// <param name="dataBuffer">The received data.</param>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="message">The message instance to use.</param>
|
||||
internal void ProcessNotify(byte[] dataBuffer, int amount, Message message)
|
||||
{
|
||||
notify.UpdateReceivedAcks(Converter.UShortFromBits(dataBuffer, Message.HeaderBits), Converter.ByteFromBits(dataBuffer, Message.HeaderBits + 16));
|
||||
|
||||
Metrics.ReceivedNotify(amount);
|
||||
if (notify.ShouldHandle(Converter.UShortFromBits(dataBuffer, Message.HeaderBits + 24)))
|
||||
{
|
||||
Buffer.BlockCopy(dataBuffer, 1, message.Data, 1, amount - 1); // Copy payload
|
||||
NotifyReceived?.Invoke(message);
|
||||
}
|
||||
else
|
||||
Metrics.NotifyDiscarded++;
|
||||
}
|
||||
|
||||
/// <summary>Determines if the message with the given sequence ID should be handled.</summary>
|
||||
/// <param name="sequenceId">The message's sequence ID.</param>
|
||||
/// <returns>Whether or not the message should be handled.</returns>
|
||||
internal bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
return reliable.ShouldHandle(sequenceId);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the connection.</summary>
|
||||
internal void LocalDisconnect()
|
||||
{
|
||||
state = ConnectionState.NotConnected;
|
||||
|
||||
foreach (PendingMessage pendingMessage in pendingMessages.Values)
|
||||
pendingMessage.Clear();
|
||||
|
||||
pendingMessages.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Resends the <see cref="PendingMessage"/> with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the message to resend.</param>
|
||||
private void ResendMessage(ushort sequenceId)
|
||||
{
|
||||
if (pendingMessages.TryGetValue(sequenceId, out PendingMessage pendingMessage))
|
||||
pendingMessage.RetrySend();
|
||||
}
|
||||
|
||||
/// <summary>Clears the <see cref="PendingMessage"/> with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID that was acknowledged.</param>
|
||||
internal void ClearMessage(ushort sequenceId)
|
||||
{
|
||||
if (pendingMessages.TryGetValue(sequenceId, out PendingMessage pendingMessage))
|
||||
{
|
||||
ReliableDelivered?.Invoke(sequenceId);
|
||||
pendingMessage.Clear();
|
||||
pendingMessages.Remove(sequenceId);
|
||||
UpdateSendAttemptsViolations();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Puts the connection in the pending state.</summary>
|
||||
internal void SetPending()
|
||||
{
|
||||
if (IsConnecting)
|
||||
{
|
||||
state = ConnectionState.Pending;
|
||||
ResetTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks the average send attempts (of reliable messages) and updates <see cref="sendAttemptsViolations"/> accordingly.</summary>
|
||||
private void UpdateSendAttemptsViolations()
|
||||
{
|
||||
if (Metrics.RollingReliableSends.Mean > MaxAvgSendAttempts)
|
||||
{
|
||||
sendAttemptsViolations++;
|
||||
if (sendAttemptsViolations >= AvgSendAttemptsResilience)
|
||||
Peer.Disconnect(this, DisconnectReason.PoorConnection);
|
||||
}
|
||||
else
|
||||
sendAttemptsViolations = 0;
|
||||
}
|
||||
|
||||
/// <summary>Checks the loss rate (of notify messages) and updates <see cref="lossRateViolations"/> accordingly.</summary>
|
||||
private void UpdateLossViolations()
|
||||
{
|
||||
if (Metrics.RollingNotifyLossRate > MaxNotifyLoss)
|
||||
{
|
||||
lossRateViolations++;
|
||||
if (lossRateViolations >= NotifyLossResilience)
|
||||
Peer.Disconnect(this, DisconnectReason.PoorConnection);
|
||||
}
|
||||
else
|
||||
lossRateViolations = 0;
|
||||
}
|
||||
|
||||
#region Messages
|
||||
/// <summary>Sends an ack message for the given sequence ID.</summary>
|
||||
/// <param name="forSeqId">The sequence ID to acknowledge.</param>
|
||||
/// <param name="lastReceivedSeqId">The sequence ID of the latest message we've received.</param>
|
||||
/// <param name="receivedSeqIds">Sequence IDs of previous messages that we have (or have not received).</param>
|
||||
private void SendAck(ushort forSeqId, ushort lastReceivedSeqId, Bitfield receivedSeqIds)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Ack);
|
||||
message.AddUShort(lastReceivedSeqId);
|
||||
message.AddUShort(receivedSeqIds.First16);
|
||||
|
||||
if (forSeqId == lastReceivedSeqId)
|
||||
message.AddBool(false);
|
||||
else
|
||||
message.AddBool(true);
|
||||
message.AddUShort(forSeqId);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles an ack message.</summary>
|
||||
/// <param name="message">The ack message to handle.</param>
|
||||
internal void HandleAck(Message message)
|
||||
{
|
||||
ushort remoteLastReceivedSeqId = message.GetUShort();
|
||||
ushort remoteAcksBitField = message.GetUShort();
|
||||
ushort ackedSeqId = message.GetBool() ? message.GetUShort() : remoteLastReceivedSeqId;
|
||||
|
||||
ClearMessage(ackedSeqId);
|
||||
reliable.UpdateReceivedAcks(remoteLastReceivedSeqId, remoteAcksBitField);
|
||||
}
|
||||
|
||||
#region Server
|
||||
/// <summary>Sends a welcome message.</summary>
|
||||
internal void SendWelcome()
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Welcome);
|
||||
message.AddUShort(Id);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles a welcome message on the server.</summary>
|
||||
/// <param name="message">The welcome message to handle.</param>
|
||||
/// <returns>Whether or not the connection is now connected.</returns>
|
||||
internal bool HandleWelcomeResponse(Message message)
|
||||
{
|
||||
if (!IsPending)
|
||||
return false;
|
||||
|
||||
ushort id = message.GetUShort();
|
||||
if (Id != id)
|
||||
RiptideLogger.Log(LogType.Error, Peer.LogName, $"Client has assumed ID {id} instead of {Id}!");
|
||||
|
||||
state = ConnectionState.Connected;
|
||||
ResetTimeout();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Handles a heartbeat message.</summary>
|
||||
/// <param name="message">The heartbeat message to handle.</param>
|
||||
internal void HandleHeartbeat(Message message)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return; // A client that is not yet fully connected should not be sending heartbeats
|
||||
|
||||
RespondHeartbeat(message.GetByte());
|
||||
RTT = message.GetShort();
|
||||
|
||||
ResetTimeout();
|
||||
}
|
||||
|
||||
/// <summary>Sends a heartbeat message.</summary>
|
||||
private void RespondHeartbeat(byte pingId)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Heartbeat);
|
||||
message.AddByte(pingId);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
/// <summary>Handles a welcome message on the client.</summary>
|
||||
/// <param name="message">The welcome message to handle.</param>
|
||||
internal void HandleWelcome(Message message)
|
||||
{
|
||||
Id = message.GetUShort();
|
||||
state = ConnectionState.Connected;
|
||||
ResetTimeout();
|
||||
|
||||
RespondWelcome();
|
||||
}
|
||||
|
||||
/// <summary>Sends a welcome response message.</summary>
|
||||
private void RespondWelcome()
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Welcome);
|
||||
message.AddUShort(Id);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Sends a heartbeat message.</summary>
|
||||
internal void SendHeartbeat()
|
||||
{
|
||||
pendingPingId = lastPingId++;
|
||||
pendingPingSendTime = Peer.CurrentTime;
|
||||
|
||||
Message message = Message.Create(MessageHeader.Heartbeat);
|
||||
message.AddByte(pendingPingId);
|
||||
message.AddShort(RTT);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles a heartbeat message.</summary>
|
||||
/// <param name="message">The heartbeat message to handle.</param>
|
||||
internal void HandleHeartbeatResponse(Message message)
|
||||
{
|
||||
byte pingId = message.GetByte();
|
||||
|
||||
if (pendingPingId == pingId)
|
||||
RTT = (short)Math.Max(1, Peer.CurrentTime - pendingPingSendTime);
|
||||
|
||||
ResetTimeout();
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="NotifyDelivered"/> event.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the delivered message.</param>
|
||||
protected virtual void OnNotifyDelivered(ushort sequenceId)
|
||||
{
|
||||
Metrics.DeliveredNotify();
|
||||
NotifyDelivered?.Invoke(sequenceId);
|
||||
UpdateLossViolations();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="NotifyLost"/> event.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the lost message.</param>
|
||||
protected virtual void OnNotifyLost(ushort sequenceId)
|
||||
{
|
||||
Metrics.LostNotify();
|
||||
NotifyLost?.Invoke(sequenceId);
|
||||
UpdateLossViolations();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Message Sequencing
|
||||
/// <summary>Provides functionality for filtering out duplicate messages and determining delivery/loss status.</summary>
|
||||
private abstract class Sequencer
|
||||
{
|
||||
/// <summary>The next sequence ID to use.</summary>
|
||||
internal ushort NextSequenceId => _nextSequenceId++;
|
||||
private ushort _nextSequenceId = 1;
|
||||
|
||||
/// <summary>The connection this sequencer belongs to.</summary>
|
||||
protected readonly Connection connection;
|
||||
/// <summary>The sequence ID of the latest message that we want to acknowledge.</summary>
|
||||
protected ushort lastReceivedSeqId;
|
||||
/// <summary>Sequence IDs of messages which we have (or have not) received and want to acknowledge.</summary>
|
||||
protected readonly Bitfield receivedSeqIds = new Bitfield();
|
||||
/// <summary>The sequence ID of the latest message that we've received an ack for.</summary>
|
||||
protected ushort lastAckedSeqId;
|
||||
/// <summary>Sequence IDs of messages we sent and which we have (or have not) received acks for.</summary>
|
||||
protected readonly Bitfield ackedSeqIds = new Bitfield(false);
|
||||
|
||||
/// <summary>Initializes the sequencer.</summary>
|
||||
/// <param name="connection">The connection this sequencer belongs to.</param>
|
||||
protected Sequencer(Connection connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>Determines whether or not to handle a message with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID in question.</param>
|
||||
/// <returns>Whether or not to handle the message.</returns>
|
||||
internal abstract bool ShouldHandle(ushort sequenceId);
|
||||
|
||||
/// <summary>Updates which messages we've received acks for.</summary>
|
||||
/// <param name="remoteLastReceivedSeqId">The latest sequence ID that the other end has received.</param>
|
||||
/// <param name="remoteReceivedSeqIds">Sequence IDs which the other end has (or has not) received.</param>
|
||||
internal abstract void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private class NotifySequencer : Sequencer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal NotifySequencer(Connection connection) : base(connection) { }
|
||||
|
||||
/// <summary>Inserts the notify header into the given message.</summary>
|
||||
/// <param name="message">The message to insert the header into.</param>
|
||||
/// <returns>The sequence ID of the message.</returns>
|
||||
internal ushort InsertHeader(Message message)
|
||||
{
|
||||
ushort sequenceId = NextSequenceId;
|
||||
ulong notifyBits = lastReceivedSeqId | ((ulong)receivedSeqIds.First8 << (2 * Converter.BitsPerByte)) | ((ulong)sequenceId << (3 * Converter.BitsPerByte));
|
||||
message.SetBits(notifyBits, 5 * Converter.BitsPerByte, Message.HeaderBits);
|
||||
return sequenceId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Duplicate and out of order messages are filtered out and not handled.</remarks>
|
||||
internal override bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(sequenceId, lastReceivedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The received sequence ID is newer than the previous one
|
||||
receivedSeqIds.ShiftBy(sequenceGap);
|
||||
lastReceivedSeqId = sequenceId;
|
||||
|
||||
if (receivedSeqIds.IsSet(sequenceGap))
|
||||
return false;
|
||||
|
||||
receivedSeqIds.Set(sequenceGap);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The received sequence ID is older than or the same as the previous one (out of order or duplicate message)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(remoteLastReceivedSeqId, lastAckedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
if (sequenceGap > 1)
|
||||
{
|
||||
// Deal with messages in the gap
|
||||
while (sequenceGap > 9) // 9 because a gap of 1 means sequence IDs are consecutive, and notify uses 8 bits for the bitfield. 9 means all 8 bits are in use
|
||||
{
|
||||
lastAckedSeqId++;
|
||||
sequenceGap--;
|
||||
connection.NotifyLost?.Invoke(lastAckedSeqId);
|
||||
}
|
||||
|
||||
int bitCount = sequenceGap - 1;
|
||||
int bit = 1 << bitCount;
|
||||
for (int i = 0; i < bitCount; i++)
|
||||
{
|
||||
lastAckedSeqId++;
|
||||
bit >>= 1;
|
||||
if ((remoteReceivedSeqIds & bit) == 0)
|
||||
connection.OnNotifyLost(lastAckedSeqId);
|
||||
else
|
||||
connection.OnNotifyDelivered(lastAckedSeqId);
|
||||
}
|
||||
}
|
||||
|
||||
lastAckedSeqId = remoteLastReceivedSeqId;
|
||||
connection.OnNotifyDelivered(lastAckedSeqId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private class ReliableSequencer : Sequencer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal ReliableSequencer(Connection connection) : base(connection) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Duplicate messages are filtered out while out of order messages are handled.</remarks>
|
||||
internal override bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
bool doHandle = false;
|
||||
int sequenceGap = Helper.GetSequenceGap(sequenceId, lastReceivedSeqId);
|
||||
|
||||
if (sequenceGap != 0)
|
||||
{
|
||||
// The received sequence ID is different from the previous one
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The received sequence ID is newer than the previous one
|
||||
if (sequenceGap > 64)
|
||||
RiptideLogger.Log(LogType.Warning, connection.Peer.LogName, $"The gap between received sequence IDs was very large ({sequenceGap})!");
|
||||
|
||||
receivedSeqIds.ShiftBy(sequenceGap);
|
||||
lastReceivedSeqId = sequenceId;
|
||||
}
|
||||
else // The received sequence ID is older than the previous one (out of order message)
|
||||
sequenceGap = -sequenceGap;
|
||||
|
||||
doHandle = !receivedSeqIds.IsSet(sequenceGap);
|
||||
receivedSeqIds.Set(sequenceGap);
|
||||
}
|
||||
|
||||
connection.SendAck(sequenceId, lastReceivedSeqId, receivedSeqIds);
|
||||
return doHandle;
|
||||
}
|
||||
|
||||
/// <summary>Updates which messages we've received acks for.</summary>
|
||||
/// <param name="remoteLastReceivedSeqId">The latest sequence ID that the other end has received.</param>
|
||||
/// <param name="remoteReceivedSeqIds">Sequence IDs which the other end has (or has not) received.</param>
|
||||
internal override void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(remoteLastReceivedSeqId, lastAckedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The latest sequence ID that the other end has received is newer than the previous one
|
||||
if (!ackedSeqIds.HasCapacityFor(sequenceGap, out int overflow))
|
||||
{
|
||||
for (int i = 0; i < overflow; i++)
|
||||
{
|
||||
// Resend those messages which haven't been acked and whose sequence IDs are about to be pushed out of the bitfield
|
||||
if (!ackedSeqIds.CheckAndTrimLast(out int checkedPosition))
|
||||
connection.ResendMessage((ushort)(lastAckedSeqId - checkedPosition));
|
||||
else
|
||||
connection.ClearMessage((ushort)(lastAckedSeqId - checkedPosition));
|
||||
}
|
||||
}
|
||||
|
||||
ackedSeqIds.ShiftBy(sequenceGap);
|
||||
lastAckedSeqId = remoteLastReceivedSeqId;
|
||||
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
// Clear any messages that have been newly acknowledged
|
||||
if (!ackedSeqIds.IsSet(i + 1) && (remoteReceivedSeqIds & (1 << i)) != 0)
|
||||
connection.ClearMessage((ushort)(lastAckedSeqId - (i + 1)));
|
||||
}
|
||||
|
||||
ackedSeqIds.Combine(remoteReceivedSeqIds);
|
||||
ackedSeqIds.Set(sequenceGap); // Ensure that the bit corresponding to the previous acked sequence ID is set
|
||||
connection.ClearMessage(remoteLastReceivedSeqId);
|
||||
}
|
||||
else if (sequenceGap < 0)
|
||||
{
|
||||
// The latest sequence ID that the other end has received is older than the previous one (out of order ack)
|
||||
ackedSeqIds.Set(-sequenceGap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The latest sequence ID that the other end has received is the same as the previous one (duplicate ack)
|
||||
ackedSeqIds.Combine(remoteReceivedSeqIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
135
Riptide/EventArgs.cs
Normal file
135
Riptide/EventArgs.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Contains event data for when a client connects to the server.</summary>
|
||||
public class ServerConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The newly connected client.</summary>
|
||||
public readonly Connection Client;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The newly connected client.</param>
|
||||
public ServerConnectedEventArgs(Connection client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a connection fails to be fully established.</summary>
|
||||
public class ServerConnectionFailedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The connection that failed to be established.</summary>
|
||||
public readonly Connection Client;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The connection that failed to be established.</param>
|
||||
public ServerConnectionFailedEventArgs(Connection client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a client disconnects from the server.</summary>
|
||||
public class ServerDisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The client that disconnected.</summary>
|
||||
public readonly Connection Client;
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The client that disconnected.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
public ServerDisconnectedEventArgs(Connection client, DisconnectReason reason)
|
||||
{
|
||||
Client = client;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a message is received.</summary>
|
||||
public class MessageReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The connection from which the message was received.</summary>
|
||||
public readonly Connection FromConnection;
|
||||
/// <summary>The ID of the message.</summary>
|
||||
public readonly ushort MessageId;
|
||||
/// <summary>The received message.</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="fromConnection">The connection from which the message was received.</param>
|
||||
/// <param name="messageId">The ID of the message.</param>
|
||||
/// <param name="message">The received message.</param>
|
||||
public MessageReceivedEventArgs(Connection fromConnection, ushort messageId, Message message)
|
||||
{
|
||||
FromConnection = fromConnection;
|
||||
MessageId = messageId;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a connection attempt to a server fails.</summary>
|
||||
public class ConnectionFailedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The reason for the connection failure.</summary>
|
||||
public readonly RejectReason Reason;
|
||||
/// <summary>Additional data related to the failed connection attempt (if any).</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="reason">The reason for the connection failure.</param>
|
||||
/// <param name="message">Additional data related to the failed connection attempt (if any).</param>
|
||||
public ConnectionFailedEventArgs(RejectReason reason, Message message)
|
||||
{
|
||||
Reason = reason;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when the client disconnects from a server.</summary>
|
||||
public class DisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
/// <summary>Additional data related to the disconnection (if any).</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
/// <param name="message">Additional data related to the disconnection (if any).</param>
|
||||
public DisconnectedEventArgs(DisconnectReason reason, Message message)
|
||||
{
|
||||
Reason = reason;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a non-local client connects to the server.</summary>
|
||||
public class ClientConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The numeric ID of the client that connected.</summary>
|
||||
public readonly ushort Id;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="id">The numeric ID of the client that connected.</param>
|
||||
public ClientConnectedEventArgs(ushort id) => Id = id;
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a non-local client disconnects from the server.</summary>
|
||||
public class ClientDisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The numeric ID of the client that disconnected.</summary>
|
||||
public readonly ushort Id;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="id">The numeric ID of the client that disconnected.</param>
|
||||
public ClientDisconnectedEventArgs(ushort id) => Id = id;
|
||||
}
|
||||
}
|
||||
197
Riptide/Exceptions.cs
Normal file
197
Riptide/Exceptions.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The exception that is thrown when a <see cref="Message"/> does not contain enough unwritten bits to perform an operation.</summary>
|
||||
public class InsufficientCapacityException : Exception
|
||||
{
|
||||
/// <summary>The message with insufficient remaining capacity.</summary>
|
||||
public readonly Message RiptideMessage;
|
||||
/// <summary>The name of the type which could not be added to the message.</summary>
|
||||
public readonly string TypeName;
|
||||
/// <summary>The number of available bits the type requires in order to be added successfully.</summary>
|
||||
public readonly int RequiredBits;
|
||||
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance.</summary>
|
||||
public InsufficientCapacityException() { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public InsufficientCapacityException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public InsufficientCapacityException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="reserveBits">The number of bits which were attempted to be reserved.</param>
|
||||
public InsufficientCapacityException(Message message, int reserveBits) : base(GetErrorMessage(message, reserveBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = "reservation";
|
||||
RequiredBits = reserveBits;
|
||||
}
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="typeName">The name of the type which could not be added to the message.</param>
|
||||
/// <param name="requiredBits">The number of available bits required for the type to be added successfully.</param>
|
||||
public InsufficientCapacityException(Message message, string typeName, int requiredBits) : base(GetErrorMessage(message, typeName, requiredBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = typeName;
|
||||
RequiredBits = requiredBits;
|
||||
}
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="arrayLength">The length of the array which could not be added to the message.</param>
|
||||
/// <param name="typeName">The name of the array's type.</param>
|
||||
/// <param name="requiredBits">The number of available bits required for a single element of the array to be added successfully.</param>
|
||||
public InsufficientCapacityException(Message message, int arrayLength, string typeName, int requiredBits) : base(GetErrorMessage(message, arrayLength, typeName, requiredBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = $"{typeName}[]";
|
||||
RequiredBits = requiredBits * arrayLength;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, int reserveBits)
|
||||
{
|
||||
return $"Cannot reserve {reserveBits} {Helper.CorrectForm(reserveBits, "bit")} in a message with {message.UnwrittenBits} " +
|
||||
$"{Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, string typeName, int requiredBits)
|
||||
{
|
||||
return $"Cannot add a value of type '{typeName}' (requires {requiredBits} {Helper.CorrectForm(requiredBits, "bit")}) to " +
|
||||
$"a message with {message.UnwrittenBits} {Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, int arrayLength, string typeName, int requiredBits)
|
||||
{
|
||||
requiredBits *= arrayLength;
|
||||
return $"Cannot add an array of type '{typeName}[]' with {arrayLength} {Helper.CorrectForm(arrayLength, "element")} (requires {requiredBits} {Helper.CorrectForm(requiredBits, "bit")}) " +
|
||||
$"to a message with {message.UnwrittenBits} {Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when a method with a <see cref="MessageHandlerAttribute"/> is not marked as <see langword="static"/>.</summary>
|
||||
public class NonStaticHandlerException : Exception
|
||||
{
|
||||
/// <summary>The type containing the handler method.</summary>
|
||||
public readonly Type DeclaringType;
|
||||
/// <summary>The name of the handler method.</summary>
|
||||
public readonly string HandlerMethodName;
|
||||
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance.</summary>
|
||||
public NonStaticHandlerException() { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public NonStaticHandlerException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public NonStaticHandlerException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="declaringType">The type containing the handler method.</param>
|
||||
/// <param name="handlerMethodName">The name of the handler method.</param>
|
||||
public NonStaticHandlerException(Type declaringType, string handlerMethodName) : base(GetErrorMessage(declaringType, handlerMethodName))
|
||||
{
|
||||
DeclaringType = declaringType;
|
||||
HandlerMethodName = handlerMethodName;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Type declaringType, string handlerMethodName)
|
||||
{
|
||||
return $"'{declaringType.Name}.{handlerMethodName}' is an instance method, but message handler methods must be static!";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when a method with a <see cref="MessageHandlerAttribute"/> does not have an acceptable message handler method signature (either <see cref="Server.MessageHandler"/> or <see cref="Client.MessageHandler"/>).</summary>
|
||||
public class InvalidHandlerSignatureException : Exception
|
||||
{
|
||||
/// <summary>The type containing the handler method.</summary>
|
||||
public readonly Type DeclaringType;
|
||||
/// <summary>The name of the handler method.</summary>
|
||||
public readonly string HandlerMethodName;
|
||||
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance.</summary>
|
||||
public InvalidHandlerSignatureException() { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public InvalidHandlerSignatureException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public InvalidHandlerSignatureException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="declaringType">The type containing the handler method.</param>
|
||||
/// <param name="handlerMethodName">The name of the handler method.</param>
|
||||
public InvalidHandlerSignatureException(Type declaringType, string handlerMethodName) : base(GetErrorMessage(declaringType, handlerMethodName))
|
||||
{
|
||||
DeclaringType = declaringType;
|
||||
HandlerMethodName = handlerMethodName;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Type declaringType, string handlerMethodName)
|
||||
{
|
||||
return $"'{declaringType.Name}.{handlerMethodName}' doesn't match any acceptable message handler method signatures! Server message handler methods should have a 'ushort' and a '{nameof(Riptide.Message)}' parameter, while client message handler methods should only have a '{nameof(Riptide.Message)}' parameter.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when multiple methods with <see cref="MessageHandlerAttribute"/>s are set to handle messages with the same ID <i>and</i> have the same method signature.</summary>
|
||||
public class DuplicateHandlerException : Exception
|
||||
{
|
||||
/// <summary>The message ID with multiple handler methods.</summary>
|
||||
public readonly ushort Id;
|
||||
/// <summary>The type containing the first handler method.</summary>
|
||||
public readonly Type DeclaringType1;
|
||||
/// <summary>The name of the first handler method.</summary>
|
||||
public readonly string HandlerMethodName1;
|
||||
/// <summary>The type containing the second handler method.</summary>
|
||||
public readonly Type DeclaringType2;
|
||||
/// <summary>The name of the second handler method.</summary>
|
||||
public readonly string HandlerMethodName2;
|
||||
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message.</summary>
|
||||
public DuplicateHandlerException() { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public DuplicateHandlerException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public DuplicateHandlerException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="id">The message ID with multiple handler methods.</param>
|
||||
/// <param name="method1">The first handler method's info.</param>
|
||||
/// <param name="method2">The second handler method's info.</param>
|
||||
public DuplicateHandlerException(ushort id, MethodInfo method1, MethodInfo method2) : base(GetErrorMessage(id, method1, method2))
|
||||
{
|
||||
Id = id;
|
||||
DeclaringType1 = method1.DeclaringType;
|
||||
HandlerMethodName1 = method1.Name;
|
||||
DeclaringType2 = method2.DeclaringType;
|
||||
HandlerMethodName2 = method2.Name;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(ushort id, MethodInfo method1, MethodInfo method2)
|
||||
{
|
||||
return $"Message handler methods '{method1.DeclaringType.Name}.{method1.Name}' and '{method2.DeclaringType.Name}.{method2.Name}' are both set to handle messages with ID {id}! Only one handler method is allowed per message ID!";
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Riptide/IMessageSerializable.cs
Normal file
18
Riptide/IMessageSerializable.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Represents a type that can be added to and retrieved from messages using the <see cref="Message.AddSerializable{T}(T)"/> and <see cref="Message.GetSerializable{T}"/> methods.</summary>
|
||||
public interface IMessageSerializable
|
||||
{
|
||||
/// <summary>Adds the type to the message.</summary>
|
||||
/// <param name="message">The message to add the type to.</param>
|
||||
void Serialize(Message message);
|
||||
/// <summary>Retrieves the type from the message.</summary>
|
||||
/// <param name="message">The message to retrieve the type from.</param>
|
||||
void Deserialize(Message message);
|
||||
}
|
||||
}
|
||||
1922
Riptide/Message.cs
Normal file
1922
Riptide/Message.cs
Normal file
File diff suppressed because it is too large
Load Diff
50
Riptide/MessageHandlerAttribute.cs
Normal file
50
Riptide/MessageHandlerAttribute.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Specifies a method as the message handler for messages with the given ID.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// In order for a method to qualify as a message handler, it <i>must</i> match a valid message handler method signature. <see cref="Server"/>s
|
||||
/// will only use methods marked with this attribute if they match the <see cref="Server.MessageHandler"/> signature, and <see cref="Client"/>s
|
||||
/// will only use methods marked with this attribute if they match the <see cref="Client.MessageHandler"/> signature.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Methods marked with this attribute which match neither of the valid message handler signatures will not be used by <see cref="Server"/>s
|
||||
/// or <see cref="Client"/>s and will cause warnings at runtime.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If you want a <see cref="Server"/> or <see cref="Client"/> to only use a subset of all message handler methods, you can do so by setting up
|
||||
/// custom message handler groups. Simply set the group ID in the <see cref="MessageHandlerAttribute(ushort, byte)"/> constructor and pass the
|
||||
/// same value to the <see cref="Server.Start(ushort, ushort, byte, bool)"/> or <see cref="Client.Connect(string, int, byte, Message, bool)"/> method. This
|
||||
/// will make that <see cref="Server"/> or <see cref="Client"/> only use message handlers which have the same group ID.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class MessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <summary>The ID of the message type which this method is meant to handle.</summary>
|
||||
public readonly ushort MessageId;
|
||||
/// <summary>The ID of the group of message handlers which this method belongs to.</summary>
|
||||
public readonly byte GroupId;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="MessageHandlerAttribute"/> class with the <paramref name="messageId"/> and <paramref name="groupId"/> values.</summary>
|
||||
/// <param name="messageId">The ID of the message type which this method is meant to handle.</param>
|
||||
/// <param name="groupId">The ID of the group of message handlers which this method belongs to.</param>
|
||||
/// <remarks>
|
||||
/// <see cref="Server"/>s will only use this method if its signature matches the <see cref="Server.MessageHandler"/> signature.
|
||||
/// <see cref="Client"/>s will only use this method if its signature matches the <see cref="Client.MessageHandler"/> signature.
|
||||
/// This method will be ignored if its signature matches neither of the valid message handler signatures.
|
||||
/// </remarks>
|
||||
public MessageHandlerAttribute(ushort messageId, byte groupId = 0)
|
||||
{
|
||||
MessageId = messageId;
|
||||
GroupId = groupId;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Riptide/MessageRelayFilter.cs
Normal file
106
Riptide/MessageRelayFilter.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Provides functionality for enabling/disabling automatic message relaying by message type.</summary>
|
||||
public class MessageRelayFilter
|
||||
{
|
||||
/// <summary>The number of bits an int consists of.</summary>
|
||||
private const int BitsPerInt = sizeof(int) * 8;
|
||||
|
||||
/// <summary>An array storing all the bits which represent whether messages of a given ID should be relayed or not.</summary>
|
||||
private int[] filter;
|
||||
|
||||
/// <summary>Creates a filter of a given size.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
/// <remarks>
|
||||
/// <paramref name="size"/> should be set to the value of the largest message ID, plus 1. For example, if a server will
|
||||
/// handle messages with IDs 1, 2, 3, 7, and 8, <paramref name="size"/> should be set to 9 (8 is the largest possible value,
|
||||
/// and 8 + 1 = 9) despite the fact that there are only 5 unique message IDs the server will ever handle.
|
||||
/// </remarks>
|
||||
public MessageRelayFilter(int size) => Set(size);
|
||||
/// <summary>Creates a filter based on an enum of message IDs.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
public MessageRelayFilter(Type idEnum) => Set(GetSizeFromEnum(idEnum));
|
||||
/// <summary>Creates a filter of a given size and enables relaying for the given message IDs.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
/// <param name="idsToEnable">Message IDs to enable auto relaying for.</param>
|
||||
/// <remarks>
|
||||
/// <paramref name="size"/> should be set to the value of the largest message ID, plus 1. For example, if a server will
|
||||
/// handle messages with IDs 1, 2, 3, 7, and 8, <paramref name="size"/> should be set to 9 (8 is the largest possible value,
|
||||
/// and 8 + 1 = 9) despite the fact that there are only 5 unique message IDs the server will ever handle.
|
||||
/// </remarks>
|
||||
public MessageRelayFilter(int size, params ushort[] idsToEnable)
|
||||
{
|
||||
Set(size);
|
||||
EnableIds(idsToEnable);
|
||||
}
|
||||
/// <summary>Creates a filter based on an enum of message IDs and enables relaying for the given message IDs.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
/// <param name="idsToEnable">Message IDs to enable relaying for.</param>
|
||||
public MessageRelayFilter(Type idEnum, params Enum[] idsToEnable)
|
||||
{
|
||||
Set(GetSizeFromEnum(idEnum));
|
||||
EnableIds(idsToEnable.Cast<ushort>().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Enables auto relaying for the given message IDs.</summary>
|
||||
/// <param name="idsToEnable">Message IDs to enable relaying for.</param>
|
||||
private void EnableIds(ushort[] idsToEnable)
|
||||
{
|
||||
for (int i = 0; i < idsToEnable.Length; i++)
|
||||
EnableRelay(idsToEnable[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calculate the filter size necessary to manage all message IDs in the given enum.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
/// <returns>The appropriate filter size.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="idEnum"/> is not an <see cref="Enum"/>.</exception>
|
||||
private int GetSizeFromEnum(Type idEnum)
|
||||
{
|
||||
if (!idEnum.IsEnum)
|
||||
throw new ArgumentException($"Parameter '{nameof(idEnum)}' must be an enum type!", nameof(idEnum));
|
||||
|
||||
return Enum.GetValues(idEnum).Cast<ushort>().Max() + 1;
|
||||
}
|
||||
|
||||
/// <summary>Sets the filter size.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
private void Set(int size)
|
||||
{
|
||||
filter = new int[size / BitsPerInt + (size % BitsPerInt > 0 ? 1 : 0)];
|
||||
}
|
||||
|
||||
/// <summary>Enables auto relaying for the given message ID.</summary>
|
||||
/// <param name="forMessageId">The message ID to enable relaying for.</param>
|
||||
public void EnableRelay(ushort forMessageId)
|
||||
{
|
||||
filter[forMessageId / BitsPerInt] |= 1 << (forMessageId % BitsPerInt);
|
||||
}
|
||||
/// <inheritdoc cref="EnableRelay(ushort)"/>
|
||||
public void EnableRelay(Enum forMessageId) => EnableRelay((ushort)(object)forMessageId);
|
||||
|
||||
/// <summary>Disables auto relaying for the given message ID.</summary>
|
||||
/// <param name="forMessageId">The message ID to enable relaying for.</param>
|
||||
public void DisableRelay(ushort forMessageId)
|
||||
{
|
||||
filter[forMessageId / BitsPerInt] &= ~(1 << (forMessageId % BitsPerInt));
|
||||
}
|
||||
/// <inheritdoc cref="DisableRelay(ushort)"/>
|
||||
public void DisableRelay(Enum forMessageId) => DisableRelay((ushort)(object)forMessageId);
|
||||
|
||||
/// <summary>Checks whether or not messages with the given ID should be relayed.</summary>
|
||||
/// <param name="forMessageId">The message ID to check.</param>
|
||||
/// <returns>Whether or not messages with the given ID should be relayed.</returns>
|
||||
internal bool ShouldRelay(ushort forMessageId)
|
||||
{
|
||||
return (filter[forMessageId / BitsPerInt] & (1 << (forMessageId % BitsPerInt))) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Riptide/Peer.cs
Normal file
242
Riptide/Peer.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The reason the connection attempt was rejected.</summary>
|
||||
public enum RejectReason : byte
|
||||
{
|
||||
/// <summary>No response was received from the server (because the client has no internet connection, the server is offline, no server is listening on the target endpoint, etc.).</summary>
|
||||
NoConnection,
|
||||
/// <summary>The client is already connected.</summary>
|
||||
AlreadyConnected,
|
||||
/// <summary>The server is full.</summary>
|
||||
ServerFull,
|
||||
/// <summary>The connection attempt was rejected.</summary>
|
||||
Rejected,
|
||||
/// <summary>The connection attempt was rejected and custom data may have been included with the rejection message.</summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>The reason for a disconnection.</summary>
|
||||
public enum DisconnectReason : byte
|
||||
{
|
||||
/// <summary>No connection was ever established.</summary>
|
||||
NeverConnected,
|
||||
/// <summary>The connection attempt was rejected by the server.</summary>
|
||||
ConnectionRejected,
|
||||
/// <summary>The active transport detected a problem with the connection.</summary>
|
||||
TransportError,
|
||||
/// <summary>The connection timed out.</summary>
|
||||
/// <remarks>
|
||||
/// This also acts as the fallback reason—if a client disconnects and the message containing the <i>real</i> reason is lost
|
||||
/// in transmission, it can't be resent as the connection will have already been closed. As a result, the other end will time
|
||||
/// out the connection after a short period of time and this will be used as the reason.
|
||||
/// </remarks>
|
||||
TimedOut,
|
||||
/// <summary>The client was forcibly disconnected by the server.</summary>
|
||||
Kicked,
|
||||
/// <summary>The server shut down.</summary>
|
||||
ServerStopped,
|
||||
/// <summary>The disconnection was initiated by the client.</summary>
|
||||
Disconnected,
|
||||
/// <summary>The connection's loss and/or resend rates exceeded the maximum acceptable thresholds, or a reliably sent message could not be delivered.</summary>
|
||||
PoorConnection
|
||||
}
|
||||
|
||||
/// <summary>Provides base functionality for <see cref="Server"/> and <see cref="Client"/>.</summary>
|
||||
public abstract class Peer
|
||||
{
|
||||
/// <summary>The name to use when logging messages via <see cref="RiptideLogger"/>.</summary>
|
||||
public readonly string LogName;
|
||||
/// <summary>Sets the relevant connections' <see cref="Connection.TimeoutTime"/>s.</summary>
|
||||
public abstract int TimeoutTime { set; }
|
||||
/// <summary>The interval (in milliseconds) at which to send and expect heartbeats to be received.</summary>
|
||||
/// <remarks>Changes to this value will only take effect after the next heartbeat is executed.</remarks>
|
||||
public int HeartbeatInterval { get; set; } = 1000;
|
||||
|
||||
/// <summary>The number of currently active <see cref="Server"/> and <see cref="Client"/> instances.</summary>
|
||||
internal static int ActiveCount { get; private set; }
|
||||
|
||||
/// <summary>The time (in milliseconds) for which to wait before giving up on a connection attempt.</summary>
|
||||
internal int ConnectTimeoutTime { get; set; } = 10000;
|
||||
/// <summary>The current time.</summary>
|
||||
internal long CurrentTime { get; private set; }
|
||||
|
||||
/// <summary>Whether or not the peer should use the built-in message handler system.</summary>
|
||||
protected bool useMessageHandlers;
|
||||
/// <summary>The default time (in milliseconds) after which to disconnect if no heartbeats are received.</summary>
|
||||
protected int defaultTimeout = 5000;
|
||||
|
||||
/// <summary>A stopwatch used to track how much time has passed.</summary>
|
||||
private readonly System.Diagnostics.Stopwatch time = new System.Diagnostics.Stopwatch();
|
||||
/// <summary>Received messages which need to be handled.</summary>
|
||||
private readonly Queue<MessageToHandle> messagesToHandle = new Queue<MessageToHandle>();
|
||||
/// <summary>A queue of events to execute, ordered by how soon they need to be executed.</summary>
|
||||
private readonly PriorityQueue<DelayedEvent, long> eventQueue = new PriorityQueue<DelayedEvent, long>();
|
||||
|
||||
/// <summary>Initializes the peer.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Peer(string logName)
|
||||
{
|
||||
LogName = logName;
|
||||
}
|
||||
|
||||
/// <summary>Retrieves methods marked with <see cref="MessageHandlerAttribute"/>.</summary>
|
||||
/// <returns>An array containing message handler methods.</returns>
|
||||
protected MethodInfo[] FindMessageHandlers()
|
||||
{
|
||||
return new MethodInfo[0];
|
||||
/*string thisAssemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a
|
||||
.GetReferencedAssemblies()
|
||||
.Any(n => n.FullName == thisAssemblyName)) // Get only assemblies that reference this assembly
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) // Include instance methods in the search so we can show the developer an error instead of silently not adding instance methods to the dictionary
|
||||
.Where(m => m.GetCustomAttributes(typeof(MessageHandlerAttribute), false).Length > 0)
|
||||
.ToArray();*/
|
||||
}
|
||||
|
||||
/// <summary>Builds a dictionary of message IDs and their corresponding message handler methods.</summary>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to include in the dictionary.</param>
|
||||
protected abstract void CreateMessageHandlersDictionary(byte messageHandlerGroupId);
|
||||
|
||||
/// <summary>Starts tracking how much time has passed.</summary>
|
||||
protected void StartTime()
|
||||
{
|
||||
CurrentTime = 0;
|
||||
time.Restart();
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking how much time has passed.</summary>
|
||||
protected void StopTime()
|
||||
{
|
||||
CurrentTime = 0;
|
||||
time.Reset();
|
||||
eventQueue.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Beats the heart.</summary>
|
||||
internal abstract void Heartbeat();
|
||||
|
||||
/// <summary>Handles any received messages and invokes any delayed events which need to be invoked.</summary>
|
||||
public virtual void Update()
|
||||
{
|
||||
CurrentTime = time.ElapsedMilliseconds;
|
||||
|
||||
while (eventQueue.Count > 0 && eventQueue.PeekPriority() <= CurrentTime)
|
||||
eventQueue.Dequeue().Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Sets up a delayed event to be executed after the given time has passed.</summary>
|
||||
/// <param name="inMS">How long from now to execute the delayed event, in milliseconds.</param>
|
||||
/// <param name="delayedEvent">The delayed event to execute later.</param>
|
||||
internal void ExecuteLater(long inMS, DelayedEvent delayedEvent)
|
||||
{
|
||||
eventQueue.Enqueue(delayedEvent, CurrentTime + inMS);
|
||||
}
|
||||
|
||||
/// <summary>Handles all queued messages.</summary>
|
||||
protected void HandleMessages()
|
||||
{
|
||||
while (messagesToHandle.Count > 0)
|
||||
{
|
||||
MessageToHandle handle = messagesToHandle.Dequeue();
|
||||
Handle(handle.Message, handle.Header, handle.FromConnection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles data received by the transport.</summary>
|
||||
protected void HandleData(object _, DataReceivedEventArgs e)
|
||||
{
|
||||
Message message = Message.Create().Init(e.DataBuffer[0], e.Amount, out MessageHeader header);
|
||||
|
||||
if (message.SendMode == MessageSendMode.Notify)
|
||||
{
|
||||
if (e.Amount < Message.MinNotifyBytes)
|
||||
return;
|
||||
|
||||
e.FromConnection.ProcessNotify(e.DataBuffer, e.Amount, message);
|
||||
}
|
||||
else if (message.SendMode == MessageSendMode.Unreliable)
|
||||
{
|
||||
if (e.Amount > Message.MinUnreliableBytes)
|
||||
Buffer.BlockCopy(e.DataBuffer, 1, message.Data, 1, e.Amount - 1);
|
||||
|
||||
messagesToHandle.Enqueue(new MessageToHandle(message, header, e.FromConnection));
|
||||
e.FromConnection.Metrics.ReceivedUnreliable(e.Amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (e.Amount < Message.MinReliableBytes)
|
||||
return;
|
||||
|
||||
e.FromConnection.Metrics.ReceivedReliable(e.Amount);
|
||||
if (e.FromConnection.ShouldHandle(Converter.UShortFromBits(e.DataBuffer, Message.HeaderBits)))
|
||||
{
|
||||
Buffer.BlockCopy(e.DataBuffer, 1, message.Data, 1, e.Amount - 1);
|
||||
messagesToHandle.Enqueue(new MessageToHandle(message, header, e.FromConnection));
|
||||
}
|
||||
else
|
||||
e.FromConnection.Metrics.ReliableDiscarded++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles a message.</summary>
|
||||
/// <param name="message">The message to handle.</param>
|
||||
/// <param name="header">The message's header type.</param>
|
||||
/// <param name="connection">The connection which the message was received on.</param>
|
||||
protected abstract void Handle(Message message, MessageHeader header, Connection connection);
|
||||
|
||||
/// <summary>Disconnects the connection in question. Necessary for connections to be able to initiate disconnections (like in the case of poor connection quality).</summary>
|
||||
/// <param name="connection">The connection to disconnect.</param>
|
||||
/// <param name="reason">The reason why the connection is being disconnected.</param>
|
||||
internal abstract void Disconnect(Connection connection, DisconnectReason reason);
|
||||
|
||||
/// <summary>Increases <see cref="ActiveCount"/>. For use when a new <see cref="Server"/> or <see cref="Client"/> is started.</summary>
|
||||
protected static void IncreaseActiveCount()
|
||||
{
|
||||
ActiveCount++;
|
||||
}
|
||||
|
||||
/// <summary>Decreases <see cref="ActiveCount"/>. For use when a <see cref="Server"/> or <see cref="Client"/> is stopped.</summary>
|
||||
protected static void DecreaseActiveCount()
|
||||
{
|
||||
ActiveCount--;
|
||||
if (ActiveCount < 0)
|
||||
ActiveCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stores information about a message that needs to be handled.</summary>
|
||||
internal struct MessageToHandle
|
||||
{
|
||||
/// <summary>The message that needs to be handled.</summary>
|
||||
internal readonly Message Message;
|
||||
/// <summary>The message's header type.</summary>
|
||||
internal readonly MessageHeader Header;
|
||||
/// <summary>The connection on which the message was received.</summary>
|
||||
internal readonly Connection FromConnection;
|
||||
|
||||
/// <summary>Handles initialization.</summary>
|
||||
/// <param name="message">The message that needs to be handled.</param>
|
||||
/// <param name="header">The message's header type.</param>
|
||||
/// <param name="fromConnection">The connection on which the message was received.</param>
|
||||
public MessageToHandle(Message message, MessageHeader header, Connection fromConnection)
|
||||
{
|
||||
Message = message;
|
||||
Header = header;
|
||||
FromConnection = fromConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Riptide/PendingMessage.cs
Normal file
136
Riptide/PendingMessage.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Represents a currently pending reliably sent message whose delivery has not been acknowledged yet.</summary>
|
||||
internal class PendingMessage
|
||||
{
|
||||
/// <summary>The time of the latest send attempt.</summary>
|
||||
internal long LastSendTime { get; private set; }
|
||||
|
||||
/// <summary>The multiplier used to determine how long to wait before resending a pending message.</summary>
|
||||
private const float RetryTimeMultiplier = 1.2f;
|
||||
|
||||
/// <summary>A pool of reusable <see cref="PendingMessage"/> instances.</summary>
|
||||
private static readonly List<PendingMessage> pool = new List<PendingMessage>();
|
||||
|
||||
/// <summary>The <see cref="Connection"/> to use to send (and resend) the pending message.</summary>
|
||||
private Connection connection;
|
||||
/// <summary>The contents of the message.</summary>
|
||||
private readonly byte[] data;
|
||||
/// <summary>The length in bytes of the message.</summary>
|
||||
private int size;
|
||||
/// <summary>How many send attempts have been made so far.</summary>
|
||||
private byte sendAttempts;
|
||||
/// <summary>Whether the pending message has been cleared or not.</summary>
|
||||
private bool wasCleared;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
internal PendingMessage()
|
||||
{
|
||||
data = new byte[Message.MaxSize];
|
||||
}
|
||||
|
||||
#region Pooling
|
||||
/// <summary>Retrieves a <see cref="PendingMessage"/> instance and initializes it.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the message.</param>
|
||||
/// <param name="message">The message that is being sent reliably.</param>
|
||||
/// <param name="connection">The <see cref="Connection"/> to use to send (and resend) the pending message.</param>
|
||||
/// <returns>An intialized <see cref="PendingMessage"/> instance.</returns>
|
||||
internal static PendingMessage Create(ushort sequenceId, Message message, Connection connection)
|
||||
{
|
||||
PendingMessage pendingMessage = RetrieveFromPool();
|
||||
pendingMessage.connection = connection;
|
||||
|
||||
message.SetBits(sequenceId, sizeof(ushort) * Converter.BitsPerByte, Message.HeaderBits);
|
||||
pendingMessage.size = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, pendingMessage.data, 0, pendingMessage.size);
|
||||
|
||||
pendingMessage.sendAttempts = 0;
|
||||
pendingMessage.wasCleared = false;
|
||||
return pendingMessage;
|
||||
}
|
||||
|
||||
/// <summary>Retrieves a <see cref="PendingMessage"/> instance from the pool. If none is available, a new instance is created.</summary>
|
||||
/// <returns>A <see cref="PendingMessage"/> instance.</returns>
|
||||
private static PendingMessage RetrieveFromPool()
|
||||
{
|
||||
PendingMessage message;
|
||||
if (pool.Count > 0)
|
||||
{
|
||||
message = pool[0];
|
||||
pool.RemoveAt(0);
|
||||
}
|
||||
else
|
||||
message = new PendingMessage();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>Empties the pool. Does not affect <see cref="PendingMessage"/> instances which are actively pending and therefore not in the pool.</summary>
|
||||
public static void ClearPool()
|
||||
{
|
||||
pool.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Returns the <see cref="PendingMessage"/> instance to the pool so it can be reused.</summary>
|
||||
private void Release()
|
||||
{
|
||||
if (!pool.Contains(this))
|
||||
pool.Add(this); // Only add it if it's not already in the list, otherwise this method being called twice in a row for whatever reason could cause *serious* issues
|
||||
|
||||
// TODO: consider doing something to decrease pool capacity if there are far more
|
||||
// available instance than are needed, which could occur if a large burst of
|
||||
// messages has to be sent for some reason
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>Resends the message.</summary>
|
||||
internal void RetrySend()
|
||||
{
|
||||
if (!wasCleared)
|
||||
{
|
||||
long time = connection.Peer.CurrentTime;
|
||||
if (LastSendTime + (connection.SmoothRTT < 0 ? 25 : connection.SmoothRTT / 2) <= time) // Avoid triggering a resend if the latest resend was less than half a RTT ago
|
||||
TrySend();
|
||||
else
|
||||
connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, time));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Attempts to send the message.</summary>
|
||||
internal void TrySend()
|
||||
{
|
||||
if (sendAttempts >= connection.MaxSendAttempts && connection.CanQualityDisconnect)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, connection.Peer.LogName, $"Could not guarantee delivery of a {(MessageHeader)(data[0] & Message.HeaderBitmask)} message after {sendAttempts} attempts! Disconnecting...");
|
||||
connection.Peer.Disconnect(connection, DisconnectReason.PoorConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.Send(data, size);
|
||||
connection.Metrics.SentReliable(size);
|
||||
|
||||
LastSendTime = connection.Peer.CurrentTime;
|
||||
sendAttempts++;
|
||||
|
||||
connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, connection.Peer.CurrentTime));
|
||||
}
|
||||
|
||||
/// <summary>Clears the message.</summary>
|
||||
internal void Clear()
|
||||
{
|
||||
connection.Metrics.RollingReliableSends.Add(sendAttempts);
|
||||
wasCleared = true;
|
||||
Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
598
Riptide/Server.cs
Normal file
598
Riptide/Server.cs
Normal file
@@ -0,0 +1,598 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>A server that can accept connections from <see cref="Client"/>s.</summary>
|
||||
public class Server : Peer
|
||||
{
|
||||
/// <summary>Invoked when a client connects.</summary>
|
||||
public event EventHandler<ServerConnectedEventArgs> ClientConnected;
|
||||
/// <summary>Invoked when a connection fails to be fully established.</summary>
|
||||
public event EventHandler<ServerConnectionFailedEventArgs> ConnectionFailed;
|
||||
/// <summary>Invoked when a message is received.</summary>
|
||||
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
||||
/// <summary>Invoked when a client disconnects.</summary>
|
||||
public event EventHandler<ServerDisconnectedEventArgs> ClientDisconnected;
|
||||
|
||||
/// <summary>Whether or not the server is currently running.</summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
/// <summary>The local port that the server is running on.</summary>
|
||||
public ushort Port => transport.Port;
|
||||
/// <summary>Sets the default timeout time for future connections and updates the <see cref="Connection.TimeoutTime"/> of all connected clients.</summary>
|
||||
public override int TimeoutTime
|
||||
{
|
||||
set
|
||||
{
|
||||
defaultTimeout = value;
|
||||
foreach (Connection connection in clients.Values)
|
||||
connection.TimeoutTime = defaultTimeout;
|
||||
}
|
||||
}
|
||||
/// <summary>The maximum number of concurrent connections.</summary>
|
||||
public ushort MaxClientCount { get; private set; }
|
||||
/// <summary>The number of currently connected clients.</summary>
|
||||
public int ClientCount => clients.Count;
|
||||
/// <summary>An array of all the currently connected clients.</summary>
|
||||
/// <remarks>The position of each <see cref="Connection"/> instance in the array does <i>not</i> correspond to that client's numeric ID (except by coincidence).</remarks>
|
||||
public Connection[] Clients => clients.Values.ToArray();
|
||||
/// <summary>Encapsulates a method that handles a message from a client.</summary>
|
||||
/// <param name="fromClientId">The numeric ID of the client from whom the message was received.</param>
|
||||
/// <param name="message">The message that was received.</param>
|
||||
public delegate void MessageHandler(ushort fromClientId, Message message);
|
||||
/// <summary>Encapsulates a method that determines whether or not to accept a client's connection attempt.</summary>
|
||||
public delegate void ConnectionAttemptHandler(Connection pendingConnection, Message connectMessage);
|
||||
/// <summary>An optional method which determines whether or not to accept a client's connection attempt.</summary>
|
||||
/// <remarks>The <see cref="Connection"/> parameter is the pending connection and the <see cref="Message"/> parameter is a message containing any additional data the
|
||||
/// client included with the connection attempt. If you choose to subscribe a method to this delegate, you should use it to call either <see cref="Accept(Connection)"/>
|
||||
/// or <see cref="Reject(Connection, Message)"/>. Not doing so will result in the connection hanging until the client times out.</remarks>
|
||||
public ConnectionAttemptHandler HandleConnection;
|
||||
/// <summary>Stores which message IDs have auto relaying enabled. Relaying is disabled entirely when this is <see langword="null"/>.</summary>
|
||||
public MessageRelayFilter RelayFilter;
|
||||
|
||||
/// <summary>Currently pending connections which are waiting to be accepted or rejected.</summary>
|
||||
private readonly List<Connection> pendingConnections;
|
||||
/// <summary>Currently connected clients.</summary>
|
||||
private Dictionary<ushort, Connection> clients;
|
||||
/// <summary>Clients that have timed out and need to be removed from <see cref="clients"/>.</summary>
|
||||
private readonly List<Connection> timedOutClients;
|
||||
/// <summary>Methods used to handle messages, accessible by their corresponding message IDs.</summary>
|
||||
private Dictionary<ushort, MessageHandler> messageHandlers;
|
||||
/// <summary>The underlying transport's server that is used for sending and receiving data.</summary>
|
||||
private IServer transport;
|
||||
/// <summary>All currently unused client IDs.</summary>
|
||||
private Queue<ushort> availableClientIds;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
/// <param name="transport">The transport to use for sending and receiving data.</param>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Server(IServer transport, string logName = "SERVER") : base(logName)
|
||||
{
|
||||
this.transport = transport;
|
||||
pendingConnections = new List<Connection>();
|
||||
clients = new Dictionary<ushort, Connection>();
|
||||
timedOutClients = new List<Connection>();
|
||||
}
|
||||
/// <summary>Handles initial setup using the built-in UDP transport.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Server(string logName = "SERVER") : this(new Transports.Udp.UdpServer(), logName) { }
|
||||
|
||||
/// <summary>Stops the server if it's running and swaps out the transport it's using.</summary>
|
||||
/// <param name="newTransport">The new underlying transport server to use for sending and receiving data.</param>
|
||||
/// <remarks>This method does not automatically restart the server. To continue accepting connections, <see cref="Start(ushort, ushort, byte, bool)"/> must be called again.</remarks>
|
||||
public void ChangeTransport(IServer newTransport)
|
||||
{
|
||||
Stop();
|
||||
transport = newTransport;
|
||||
}
|
||||
|
||||
/// <summary>Starts the server.</summary>
|
||||
/// <param name="port">The local port on which to start the server.</param>
|
||||
/// <param name="maxClientCount">The maximum number of concurrent connections to allow.</param>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to use when building <see cref="messageHandlers"/>.</param>
|
||||
/// <param name="useMessageHandlers">Whether or not the server should use the built-in message handler system.</param>
|
||||
/// <remarks>Setting <paramref name="useMessageHandlers"/> to <see langword="false"/> will disable the automatic detection and execution of methods with the <see cref="MessageHandlerAttribute"/>, which is beneficial if you prefer to handle messages via the <see cref="MessageReceived"/> event.</remarks>
|
||||
public void Start(ushort port, ushort maxClientCount, byte messageHandlerGroupId = 0, bool useMessageHandlers = true)
|
||||
{
|
||||
Stop();
|
||||
|
||||
IncreaseActiveCount();
|
||||
this.useMessageHandlers = useMessageHandlers;
|
||||
if (useMessageHandlers)
|
||||
CreateMessageHandlersDictionary(messageHandlerGroupId);
|
||||
|
||||
MaxClientCount = maxClientCount;
|
||||
clients = new Dictionary<ushort, Connection>(maxClientCount);
|
||||
InitializeClientIds();
|
||||
|
||||
SubToTransportEvents();
|
||||
transport.Start(port);
|
||||
|
||||
StartTime();
|
||||
Heartbeat();
|
||||
IsRunning = true;
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Started on port {port}.");
|
||||
}
|
||||
|
||||
/// <summary>Subscribes appropriate methods to the transport's events.</summary>
|
||||
private void SubToTransportEvents()
|
||||
{
|
||||
transport.Connected += HandleConnectionAttempt;
|
||||
transport.DataReceived += HandleData;
|
||||
transport.Disconnected += TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes methods from all of the transport's events.</summary>
|
||||
private void UnsubFromTransportEvents()
|
||||
{
|
||||
transport.Connected -= HandleConnectionAttempt;
|
||||
transport.DataReceived -= HandleData;
|
||||
transport.Disconnected -= TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreateMessageHandlersDictionary(byte messageHandlerGroupId)
|
||||
{
|
||||
MethodInfo[] methods = FindMessageHandlers();
|
||||
|
||||
messageHandlers = new Dictionary<ushort, MessageHandler>(methods.Length);
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>();
|
||||
if (attribute.GroupId != messageHandlerGroupId)
|
||||
continue;
|
||||
|
||||
if (!method.IsStatic)
|
||||
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
||||
|
||||
Delegate serverMessageHandler = Delegate.CreateDelegate(typeof(MessageHandler), method, false);
|
||||
if (serverMessageHandler != null)
|
||||
{
|
||||
// It's a message handler for Server instances
|
||||
if (messageHandlers.ContainsKey(attribute.MessageId))
|
||||
{
|
||||
MethodInfo otherMethodWithId = messageHandlers[attribute.MessageId].GetMethodInfo();
|
||||
throw new DuplicateHandlerException(attribute.MessageId, method, otherMethodWithId);
|
||||
}
|
||||
else
|
||||
messageHandlers.Add(attribute.MessageId, (MessageHandler)serverMessageHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's not a message handler for Server instances, but it might be one for Client instances
|
||||
if (Delegate.CreateDelegate(typeof(Client.MessageHandler), method, false) == null)
|
||||
throw new InvalidHandlerSignatureException(method.DeclaringType, method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles an incoming connection attempt.</summary>
|
||||
private void HandleConnectionAttempt(object _, ConnectedEventArgs e)
|
||||
{
|
||||
e.Connection.Initialize(this, defaultTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Handles a connect message.</summary>
|
||||
/// <param name="connection">The client that sent the connect message.</param>
|
||||
/// <param name="connectMessage">The connect message.</param>
|
||||
private void HandleConnect(Connection connection, Message connectMessage)
|
||||
{
|
||||
connection.SetPending();
|
||||
|
||||
if (HandleConnection == null)
|
||||
AcceptConnection(connection);
|
||||
else if (ClientCount < MaxClientCount)
|
||||
{
|
||||
if (!clients.ContainsValue(connection) && !pendingConnections.Contains(connection))
|
||||
{
|
||||
pendingConnections.Add(connection);
|
||||
Send(Message.Create(MessageHeader.Connect), connection); // Inform the client we've received the connection attempt
|
||||
HandleConnection(connection, connectMessage); // Externally determines whether to accept
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.AlreadyConnected);
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.ServerFull);
|
||||
}
|
||||
|
||||
/// <summary>Accepts the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to accept.</param>
|
||||
public void Accept(Connection connection)
|
||||
{
|
||||
if (pendingConnections.Remove(connection))
|
||||
AcceptConnection(connection);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't accept connection from {connection} because no such connection was pending!");
|
||||
}
|
||||
|
||||
/// <summary>Rejects the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to reject.</param>
|
||||
/// <param name="message">Data that should be sent to the client being rejected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void Reject(Connection connection, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting rejection data!");
|
||||
|
||||
if (pendingConnections.Remove(connection))
|
||||
Reject(connection, message == null ? RejectReason.Rejected : RejectReason.Custom, message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't reject connection from {connection} because no such connection was pending!");
|
||||
}
|
||||
|
||||
/// <summary>Accepts the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to accept.</param>
|
||||
private void AcceptConnection(Connection connection)
|
||||
{
|
||||
if (ClientCount < MaxClientCount)
|
||||
{
|
||||
if (!clients.ContainsValue(connection))
|
||||
{
|
||||
ushort clientId = GetAvailableClientId();
|
||||
connection.Id = clientId;
|
||||
clients.Add(clientId, connection);
|
||||
connection.ResetTimeout();
|
||||
connection.SendWelcome();
|
||||
return;
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.AlreadyConnected);
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.ServerFull);
|
||||
}
|
||||
|
||||
/// <summary>Rejects the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to reject.</param>
|
||||
/// <param name="reason">The reason why the connection is being rejected.</param>
|
||||
/// <param name="rejectMessage">Data that should be sent to the client being rejected.</param>
|
||||
private void Reject(Connection connection, RejectReason reason, Message rejectMessage = null)
|
||||
{
|
||||
if (reason != RejectReason.AlreadyConnected)
|
||||
{
|
||||
// Sending a reject message about the client already being connected could theoretically be exploited to obtain information
|
||||
// on other connected clients, although in practice that seems very unlikely. However, under normal circumstances, clients
|
||||
// should never actually encounter a scenario where they are "already connected".
|
||||
|
||||
Message message = Message.Create(MessageHeader.Reject);
|
||||
message.AddByte((byte)reason);
|
||||
if (reason == RejectReason.Custom)
|
||||
message.AddMessage(rejectMessage);
|
||||
|
||||
for (int i = 0; i < 3; i++) // Send the rejection message a few times to increase the odds of it arriving
|
||||
connection.Send(message, false);
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
connection.ResetTimeout(); // Keep the connection alive for a moment so the same client can't immediately attempt to connect again
|
||||
connection.LocalDisconnect();
|
||||
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Rejected connection from {connection}: {Helper.GetReasonString(reason)}.");
|
||||
}
|
||||
|
||||
/// <summary>Checks if clients have timed out.</summary>
|
||||
internal override void Heartbeat()
|
||||
{
|
||||
foreach (Connection connection in clients.Values)
|
||||
if (connection.HasTimedOut)
|
||||
timedOutClients.Add(connection);
|
||||
|
||||
foreach (Connection connection in pendingConnections)
|
||||
if (connection.HasConnectAttemptTimedOut)
|
||||
timedOutClients.Add(connection);
|
||||
|
||||
foreach (Connection connection in timedOutClients)
|
||||
LocalDisconnect(connection, DisconnectReason.TimedOut);
|
||||
|
||||
timedOutClients.Clear();
|
||||
|
||||
ExecuteLater(HeartbeatInterval, new HeartbeatEvent(this));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
transport.Poll();
|
||||
HandleMessages();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Handle(Message message, MessageHeader header, Connection connection)
|
||||
{
|
||||
switch (header)
|
||||
{
|
||||
// User messages
|
||||
case MessageHeader.Unreliable:
|
||||
case MessageHeader.Reliable:
|
||||
OnMessageReceived(message, connection);
|
||||
break;
|
||||
|
||||
// Internal messages
|
||||
case MessageHeader.Ack:
|
||||
connection.HandleAck(message);
|
||||
break;
|
||||
case MessageHeader.Connect:
|
||||
HandleConnect(connection, message);
|
||||
break;
|
||||
case MessageHeader.Heartbeat:
|
||||
connection.HandleHeartbeat(message);
|
||||
break;
|
||||
case MessageHeader.Disconnect:
|
||||
LocalDisconnect(connection, DisconnectReason.Disconnected);
|
||||
break;
|
||||
case MessageHeader.Welcome:
|
||||
if (connection.HandleWelcomeResponse(message))
|
||||
OnClientConnected(connection);
|
||||
break;
|
||||
default:
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Unexpected message header '{header}'! Discarding {message.BytesInUse} bytes received from {connection}.");
|
||||
break;
|
||||
}
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Sends a message to a given client.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="toClient">The numeric ID of the client to send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void Send(Message message, ushort toClient, bool shouldRelease = true)
|
||||
{
|
||||
if (clients.TryGetValue(toClient, out Connection connection))
|
||||
Send(message, connection, shouldRelease);
|
||||
}
|
||||
/// <summary>Sends a message to a given client.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="toClient">The client to send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public ushort Send(Message message, Connection toClient, bool shouldRelease = true) => toClient.Send(message, shouldRelease);
|
||||
|
||||
/// <summary>Sends a message to all connected clients.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void SendToAll(Message message, bool shouldRelease = true)
|
||||
{
|
||||
foreach (Connection client in clients.Values)
|
||||
client.Send(message, false);
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
}
|
||||
/// <summary>Sends a message to all connected clients except the given one.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="exceptToClientId">The numeric ID of the client to <i>not</i> send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void SendToAll(Message message, ushort exceptToClientId, bool shouldRelease = true)
|
||||
{
|
||||
foreach (Connection client in clients.Values)
|
||||
if (client.Id != exceptToClientId)
|
||||
client.Send(message, false);
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the client with the given ID, if a client with that ID is currently connected.</summary>
|
||||
/// <param name="id">The ID of the client to retrieve.</param>
|
||||
/// <param name="client">The retrieved client.</param>
|
||||
/// <returns><see langword="true"/> if a client with the given ID was connected; otherwise <see langword="false"/>.</returns>
|
||||
public bool TryGetClient(ushort id, out Connection client) => clients.TryGetValue(id, out client);
|
||||
|
||||
/// <summary>Disconnects a specific client.</summary>
|
||||
/// <param name="id">The numeric ID of the client to disconnect.</param>
|
||||
/// <param name="message">Data that should be sent to the client being disconnected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void DisconnectClient(ushort id, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting disconnection data!");
|
||||
|
||||
if (clients.TryGetValue(id, out Connection client))
|
||||
{
|
||||
SendDisconnect(client, DisconnectReason.Kicked, message);
|
||||
LocalDisconnect(client, DisconnectReason.Kicked);
|
||||
}
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't disconnect client {id} because it wasn't connected!");
|
||||
}
|
||||
|
||||
/// <summary>Disconnects the given client.</summary>
|
||||
/// <param name="client">The client to disconnect.</param>
|
||||
/// <param name="message">Data that should be sent to the client being disconnected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void DisconnectClient(Connection client, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting disconnection data!");
|
||||
|
||||
if (clients.ContainsKey(client.Id))
|
||||
{
|
||||
SendDisconnect(client, DisconnectReason.Kicked, message);
|
||||
LocalDisconnect(client, DisconnectReason.Kicked);
|
||||
}
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't disconnect client {client.Id} because it wasn't connected!");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Disconnect(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
if (connection.IsConnected && connection.CanQualityDisconnect)
|
||||
LocalDisconnect(connection, reason);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the given connection.</summary>
|
||||
/// <param name="client">The client to disconnect.</param>
|
||||
/// <param name="reason">The reason why the client is being disconnected.</param>
|
||||
private void LocalDisconnect(Connection client, DisconnectReason reason)
|
||||
{
|
||||
if (client.Peer != this)
|
||||
return; // Client does not belong to this Server instance
|
||||
|
||||
transport.Close(client);
|
||||
|
||||
if (clients.Remove(client.Id))
|
||||
availableClientIds.Enqueue(client.Id);
|
||||
|
||||
if (client.IsConnected)
|
||||
OnClientDisconnected(client, reason); // Only run if the client was ever actually connected
|
||||
else if (client.IsPending)
|
||||
OnConnectionFailed(client);
|
||||
|
||||
client.LocalDisconnect();
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport disconnects a client.</summary>
|
||||
private void TransportDisconnected(object sender, Transports.DisconnectedEventArgs e)
|
||||
{
|
||||
LocalDisconnect(e.Connection, e.Reason);
|
||||
}
|
||||
|
||||
/// <summary>Stops the server.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (!IsRunning)
|
||||
return;
|
||||
|
||||
pendingConnections.Clear();
|
||||
byte[] disconnectBytes = { (byte)MessageHeader.Disconnect, (byte)DisconnectReason.ServerStopped };
|
||||
foreach (Connection client in clients.Values)
|
||||
client.Send(disconnectBytes, disconnectBytes.Length);
|
||||
clients.Clear();
|
||||
|
||||
transport.Shutdown();
|
||||
UnsubFromTransportEvents();
|
||||
|
||||
DecreaseActiveCount();
|
||||
|
||||
StopTime();
|
||||
IsRunning = false;
|
||||
RiptideLogger.Log(LogType.Info, LogName, "Server stopped.");
|
||||
}
|
||||
|
||||
/// <summary>Initializes available client IDs.</summary>
|
||||
private void InitializeClientIds()
|
||||
{
|
||||
if (MaxClientCount > ushort.MaxValue - 1)
|
||||
throw new Exception($"A server's max client count may not exceed {ushort.MaxValue - 1}!");
|
||||
|
||||
availableClientIds = new Queue<ushort>(MaxClientCount);
|
||||
for (ushort i = 1; i <= MaxClientCount; i++)
|
||||
availableClientIds.Enqueue(i);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves an available client ID.</summary>
|
||||
/// <returns>The client ID. 0 if none were available.</returns>
|
||||
private ushort GetAvailableClientId()
|
||||
{
|
||||
if (availableClientIds.Count > 0)
|
||||
return availableClientIds.Dequeue();
|
||||
|
||||
RiptideLogger.Log(LogType.Error, LogName, "No available client IDs, assigned 0!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#region Messages
|
||||
/// <summary>Sends a disconnect message.</summary>
|
||||
/// <param name="client">The client to send the disconnect message to.</param>
|
||||
/// <param name="reason">Why the client is being disconnected.</param>
|
||||
/// <param name="disconnectMessage">Optional custom data that should be sent to the client being disconnected.</param>
|
||||
private void SendDisconnect(Connection client, DisconnectReason reason, Message disconnectMessage)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Disconnect);
|
||||
message.AddByte((byte)reason);
|
||||
|
||||
if (reason == DisconnectReason.Kicked && disconnectMessage != null)
|
||||
message.AddMessage(disconnectMessage);
|
||||
|
||||
Send(message, client);
|
||||
}
|
||||
|
||||
/// <summary>Sends a client connected message.</summary>
|
||||
/// <param name="newClient">The newly connected client.</param>
|
||||
private void SendClientConnected(Connection newClient)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.ClientConnected);
|
||||
message.AddUShort(newClient.Id);
|
||||
|
||||
SendToAll(message, newClient.Id);
|
||||
}
|
||||
|
||||
/// <summary>Sends a client disconnected message.</summary>
|
||||
/// <param name="id">The numeric ID of the client that disconnected.</param>
|
||||
private void SendClientDisconnected(ushort id)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.ClientDisconnected);
|
||||
message.AddUShort(id);
|
||||
|
||||
SendToAll(message);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="ClientConnected"/> event.</summary>
|
||||
/// <param name="client">The newly connected client.</param>
|
||||
protected virtual void OnClientConnected(Connection client)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {client.Id} ({client}) connected successfully!");
|
||||
SendClientConnected(client);
|
||||
ClientConnected?.Invoke(this, new ServerConnectedEventArgs(client));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
/// <param name="connection">The connection that failed to be fully established.</param>
|
||||
protected virtual void OnConnectionFailed(Connection connection)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {connection} stopped responding before the connection was fully established!");
|
||||
ConnectionFailed?.Invoke(this, new ServerConnectionFailedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="MessageReceived"/> event and initiates handling of the received message.</summary>
|
||||
/// <param name="message">The received message.</param>
|
||||
/// <param name="fromConnection">The client from which the message was received.</param>
|
||||
protected virtual void OnMessageReceived(Message message, Connection fromConnection)
|
||||
{
|
||||
ushort messageId = (ushort)message.GetVarULong();
|
||||
if (RelayFilter != null && RelayFilter.ShouldRelay(messageId))
|
||||
{
|
||||
// The message should be automatically relayed to clients instead of being handled on the server
|
||||
SendToAll(message, fromConnection.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(fromConnection, messageId, message));
|
||||
|
||||
if (useMessageHandlers)
|
||||
{
|
||||
if (messageHandlers.TryGetValue(messageId, out MessageHandler messageHandler))
|
||||
messageHandler(fromConnection.Id, message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"No message handler method found for message ID {messageId}!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientDisconnected"/> event.</summary>
|
||||
/// <param name="connection">The client that disconnected.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected virtual void OnClientDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {connection.Id} ({connection}) disconnected: {Helper.GetReasonString(reason)}.");
|
||||
SendClientDisconnected(connection.Id);
|
||||
ClientDisconnected?.Invoke(this, new ServerDisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
61
Riptide/Transports/EventArgs.cs
Normal file
61
Riptide/Transports/EventArgs.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Contains event data for when a server's transport successfully establishes a connection to a client.</summary>
|
||||
public class ConnectedEventArgs
|
||||
{
|
||||
/// <summary>The newly established connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The newly established connection.</param>
|
||||
public ConnectedEventArgs(Connection connection)
|
||||
{
|
||||
Connection = connection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport receives data.</summary>
|
||||
public class DataReceivedEventArgs
|
||||
{
|
||||
/// <summary>An array containing the received data.</summary>
|
||||
public readonly byte[] DataBuffer;
|
||||
/// <summary>The number of bytes that were received.</summary>
|
||||
public readonly int Amount;
|
||||
/// <summary>The connection which the data was received from.</summary>
|
||||
public readonly Connection FromConnection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="dataBuffer">An array containing the received data.</param>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="fromConnection">The connection which the data was received from.</param>
|
||||
public DataReceivedEventArgs(byte[] dataBuffer, int amount, Connection fromConnection)
|
||||
{
|
||||
DataBuffer = dataBuffer;
|
||||
Amount = amount;
|
||||
FromConnection = fromConnection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport initiates or detects a disconnection.</summary>
|
||||
public class DisconnectedEventArgs
|
||||
{
|
||||
/// <summary>The closed connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
public DisconnectedEventArgs(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Connection = connection;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Riptide/Transports/IClient.cs
Normal file
28
Riptide/Transports/IClient.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's client must implement.</summary>
|
||||
public interface IClient : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler Connected;
|
||||
/// <summary>Invoked when a connection attempt fails at the transport level.</summary>
|
||||
event EventHandler ConnectionFailed;
|
||||
|
||||
/// <summary>Starts the transport and attempts to connect to the given host address.</summary>
|
||||
/// <param name="hostAddress">The host address to connect to.</param>
|
||||
/// <param name="connection">The pending connection. <see langword="null"/> if an issue occurred.</param>
|
||||
/// <param name="connectError">The error message associated with the issue that occurred, if any.</param>
|
||||
/// <returns><see langword="true"/> if a connection attempt will be made. <see langword="false"/> if an issue occurred (such as <paramref name="hostAddress"/> being in an invalid format) and a connection attempt will <i>not</i> be made.</returns>
|
||||
bool Connect(string hostAddress, out Connection connection, out string connectError);
|
||||
|
||||
/// <summary>Closes the connection to the server.</summary>
|
||||
void Disconnect();
|
||||
}
|
||||
}
|
||||
50
Riptide/Transports/IPeer.cs
Normal file
50
Riptide/Transports/IPeer.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>The header type of a <see cref="Message"/>.</summary>
|
||||
public enum MessageHeader : byte
|
||||
{
|
||||
/// <summary>An unreliable user message.</summary>
|
||||
Unreliable,
|
||||
/// <summary>An internal unreliable ack message.</summary>
|
||||
Ack,
|
||||
/// <summary>An internal unreliable connect message.</summary>
|
||||
Connect,
|
||||
/// <summary>An internal unreliable connection rejection message.</summary>
|
||||
Reject,
|
||||
/// <summary>An internal unreliable heartbeat message.</summary>
|
||||
Heartbeat,
|
||||
/// <summary>An internal unreliable disconnect message.</summary>
|
||||
Disconnect,
|
||||
|
||||
/// <summary>A notify message.</summary>
|
||||
Notify,
|
||||
|
||||
/// <summary>A reliable user message.</summary>
|
||||
Reliable,
|
||||
/// <summary>An internal reliable welcome message.</summary>
|
||||
Welcome,
|
||||
/// <summary>An internal reliable client connected message.</summary>
|
||||
ClientConnected,
|
||||
/// <summary>An internal reliable client disconnected message.</summary>
|
||||
ClientDisconnected,
|
||||
}
|
||||
|
||||
/// <summary>Defines methods, properties, and events which every transport's server <i>and</i> client must implement.</summary>
|
||||
public interface IPeer
|
||||
{
|
||||
/// <summary>Invoked when data is received by the transport.</summary>
|
||||
event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
/// <summary>Invoked when a disconnection is initiated or detected by the transport.</summary>
|
||||
event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>Initiates handling of any received messages.</summary>
|
||||
void Poll();
|
||||
}
|
||||
}
|
||||
30
Riptide/Transports/IServer.cs
Normal file
30
Riptide/Transports/IServer.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's server must implement.</summary>
|
||||
public interface IServer : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler<ConnectedEventArgs> Connected;
|
||||
|
||||
/// <inheritdoc cref="Server.Port"/>
|
||||
ushort Port { get; }
|
||||
|
||||
/// <summary>Starts the transport and begins listening for incoming connections.</summary>
|
||||
/// <param name="port">The local port on which to listen for connections.</param>
|
||||
void Start(ushort port);
|
||||
|
||||
/// <summary>Closes an active connection.</summary>
|
||||
/// <param name="connection">The connection to close.</param>
|
||||
void Close(Connection connection);
|
||||
|
||||
/// <summary>Closes all existing connections and stops listening for new connections.</summary>
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
120
Riptide/Transports/Tcp/TcpClient.cs
Normal file
120
Riptide/Transports/Tcp/TcpClient.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>A client which can connect to a <see cref="TcpServer"/>.</summary>
|
||||
public class TcpClient : TcpPeer, IClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler ConnectionFailed;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <summary>The connection to the server.</summary>
|
||||
private TcpConnection tcpConnection;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>.</remarks>
|
||||
public bool Connect(string hostAddress, out Connection connection, out string connectError)
|
||||
{
|
||||
connectError = $"Invalid host address '{hostAddress}'! IP and port should be separated by a colon, for example: '127.0.0.1:7777'.";
|
||||
if (!ParseHostAddress(hostAddress, out IPAddress ip, out ushort port))
|
||||
{
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(ip, port);
|
||||
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
SendBufferSize = socketBufferSize,
|
||||
ReceiveBufferSize = socketBufferSize,
|
||||
NoDelay = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
socket.Connect(remoteEndPoint); // TODO: do something about the fact that this is a blocking call
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// The connection failed, but invoking the transports ConnectionFailed event from
|
||||
// inside this method will cause problems, so we're just goint to eat the exception,
|
||||
// call OnConnected(), and let Riptide detect that no connection was established.
|
||||
}
|
||||
|
||||
connection = tcpConnection = new TcpConnection(socket, remoteEndPoint, this);
|
||||
OnConnected();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parses <paramref name="hostAddress"/> into <paramref name="ip"/> and <paramref name="port"/>, if possible.</summary>
|
||||
/// <param name="hostAddress">The host address to parse.</param>
|
||||
/// <param name="ip">The retrieved IP.</param>
|
||||
/// <param name="port">The retrieved port.</param>
|
||||
/// <returns>Whether or not <paramref name="hostAddress"/> was in a valid format.</returns>
|
||||
private bool ParseHostAddress(string hostAddress, out IPAddress ip, out ushort port)
|
||||
{
|
||||
string[] ipAndPort = hostAddress.Split(':');
|
||||
string ipString = "";
|
||||
string portString = "";
|
||||
if (ipAndPort.Length > 2)
|
||||
{
|
||||
// There was more than one ':' in the host address, might be IPv6
|
||||
ipString = string.Join(":", ipAndPort.Take(ipAndPort.Length - 1));
|
||||
portString = ipAndPort[ipAndPort.Length - 1];
|
||||
}
|
||||
else if (ipAndPort.Length == 2)
|
||||
{
|
||||
// IPv4
|
||||
ipString = ipAndPort[0];
|
||||
portString = ipAndPort[1];
|
||||
}
|
||||
|
||||
port = 0; // Need to make sure a value is assigned in case IP parsing fails
|
||||
return IPAddress.TryParse(ipString, out ip) && ushort.TryParse(portString, out port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Poll()
|
||||
{
|
||||
if (tcpConnection != null)
|
||||
tcpConnection.Receive();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Disconnect()
|
||||
{
|
||||
socket.Close();
|
||||
tcpConnection = null;
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
protected virtual void OnConnectionFailed()
|
||||
{
|
||||
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnDataReceived(int amount, TcpConnection fromConnection)
|
||||
{
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Riptide/Transports/Tcp/TcpConnection.cs
Normal file
195
Riptide/Transports/Tcp/TcpConnection.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>Represents a connection to a <see cref="TcpServer"/> or <see cref="TcpClient"/>.</summary>
|
||||
public class TcpConnection : Connection, IEquatable<TcpConnection>
|
||||
{
|
||||
/// <summary>The endpoint representing the other end of the connection.</summary>
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
|
||||
/// <summary>Whether or not the server has received a connection attempt from this connection.</summary>
|
||||
internal bool DidReceiveConnect;
|
||||
|
||||
/// <summary>The socket to use for sending and receiving.</summary>
|
||||
private readonly Socket socket;
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
private readonly TcpPeer peer;
|
||||
/// <summary>An array to receive message size values into.</summary>
|
||||
private readonly byte[] sizeBytes = new byte[sizeof(int)];
|
||||
/// <summary>The size of the next message to be received.</summary>
|
||||
private int nextMessageSize;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
/// <param name="socket">The socket to use for sending and receiving.</param>
|
||||
/// <param name="remoteEndPoint">The endpoint representing the other end of the connection.</param>
|
||||
/// <param name="peer">The local peer this connection is associated with.</param>
|
||||
internal TcpConnection(Socket socket, IPEndPoint remoteEndPoint, TcpPeer peer)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
this.socket = socket;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Sending 0 bytes is not allowed!");
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.Connected)
|
||||
{
|
||||
Converter.FromInt(amount, peer.SendBuffer, 0);
|
||||
Array.Copy(dataBuffer, 0, peer.SendBuffer, sizeof(int), amount); // TODO: consider sending length separately with an extra socket.Send call instead of copying the data an extra time
|
||||
socket.Send(peer.SendBuffer, amount + sizeof(int), SocketFlags.None);
|
||||
}
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// May want to consider triggering a disconnect here (perhaps depending on the type
|
||||
// of SocketException)? Timeout should catch disconnections, but disconnecting
|
||||
// explicitly might be better...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Polls the socket and checks if any data was received.</summary>
|
||||
internal void Receive()
|
||||
{
|
||||
bool tryReceiveMore = true;
|
||||
while (tryReceiveMore)
|
||||
{
|
||||
int byteCount = 0;
|
||||
try
|
||||
{
|
||||
if (nextMessageSize > 0)
|
||||
{
|
||||
// We already have a size value
|
||||
tryReceiveMore = TryReceiveMessage(out byteCount);
|
||||
}
|
||||
else if (socket.Available >= sizeof(int))
|
||||
{
|
||||
// We have enough bytes for a complete size value
|
||||
socket.Receive(sizeBytes, sizeof(int), SocketFlags.None);
|
||||
nextMessageSize = Converter.ToInt(sizeBytes, 0);
|
||||
|
||||
if (nextMessageSize > 0)
|
||||
tryReceiveMore = TryReceiveMessage(out byteCount);
|
||||
}
|
||||
else
|
||||
tryReceiveMore = false;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
switch (ex.SocketErrorCode)
|
||||
{
|
||||
case SocketError.Interrupted:
|
||||
case SocketError.NotSocket:
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
break;
|
||||
case SocketError.ConnectionReset:
|
||||
peer.OnDisconnected(this, DisconnectReason.Disconnected);
|
||||
break;
|
||||
case SocketError.TimedOut:
|
||||
peer.OnDisconnected(this, DisconnectReason.TimedOut);
|
||||
break;
|
||||
case SocketError.MessageSize:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
}
|
||||
|
||||
if (byteCount > 0)
|
||||
peer.OnDataReceived(byteCount, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Receives a message, if all of its data is ready to be received.</summary>
|
||||
/// <param name="receivedByteCount">How many bytes were received.</param>
|
||||
/// <returns>Whether or not all of the message's data was ready to be received.</returns>
|
||||
private bool TryReceiveMessage(out int receivedByteCount)
|
||||
{
|
||||
if (socket.Available >= nextMessageSize)
|
||||
{
|
||||
// We have enough bytes to read the complete message
|
||||
receivedByteCount = socket.Receive(peer.ReceiveBuffer, nextMessageSize, SocketFlags.None);
|
||||
nextMessageSize = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
receivedByteCount = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Closes the connection.</summary>
|
||||
internal void Close()
|
||||
{
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as TcpConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TcpConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return RemoteEndPoint.Equals(other.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -288961498 + EqualityComparer<IPEndPoint>.Default.GetHashCode(RemoteEndPoint);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator ==(TcpConnection left, TcpConnection right)
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator !=(TcpConnection left, TcpConnection right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
}
|
||||
56
Riptide/Transports/Tcp/TcpPeer.cs
Normal file
56
Riptide/Transports/Tcp/TcpPeer.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>Provides base send & receive functionality for <see cref="TcpServer"/> and <see cref="TcpClient"/>.</summary>
|
||||
public abstract class TcpPeer
|
||||
{
|
||||
/// <inheritdoc cref="IPeer.Disconnected"/>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>An array that incoming data is received into.</summary>
|
||||
internal readonly byte[] ReceiveBuffer;
|
||||
/// <summary>An array that outgoing data is sent out of.</summary>
|
||||
internal readonly byte[] SendBuffer;
|
||||
|
||||
/// <summary>The default size used for the socket's send and receive buffers.</summary>
|
||||
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
|
||||
/// <summary>The size to use for the socket's send and receive buffers.</summary>
|
||||
protected readonly int socketBufferSize;
|
||||
/// <summary>The main socket, either used for listening for connections or for sending and receiving data.</summary>
|
||||
protected Socket socket;
|
||||
/// <summary>The minimum size that may be used for the socket's send and receive buffers.</summary>
|
||||
private const int MinSocketBufferSize = 256 * 1024; // 256KB
|
||||
|
||||
/// <summary>Initializes the transport.</summary>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
protected TcpPeer(int socketBufferSize = DefaultSocketBufferSize)
|
||||
{
|
||||
if (socketBufferSize < MinSocketBufferSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
|
||||
|
||||
this.socketBufferSize = socketBufferSize;
|
||||
ReceiveBuffer = new byte[Message.MaxSize];
|
||||
SendBuffer = new byte[Message.MaxSize + sizeof(int)]; // Need room for the entire message plus the message length (since this is TCP)
|
||||
}
|
||||
|
||||
/// <summary>Handles received data.</summary>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="fromConnection">The connection from which the data was received.</param>
|
||||
protected internal abstract void OnDataReceived(int amount, TcpConnection fromConnection);
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected internal virtual void OnDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Riptide/Transports/Tcp/TcpServer.cs
Normal file
157
Riptide/Transports/Tcp/TcpServer.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>A server which can accept connections from <see cref="TcpClient"/>s.</summary>
|
||||
public class TcpServer : TcpPeer, IServer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Port { get; private set; }
|
||||
/// <summary>The maximum number of pending connections to allow at any given time.</summary>
|
||||
public int MaxPendingConnections { get; private set; } = 5;
|
||||
|
||||
/// <summary>Whether or not the server is running.</summary>
|
||||
private bool isRunning = false;
|
||||
/// <summary>The currently open connections, accessible by their endpoints.</summary>
|
||||
private Dictionary<IPEndPoint, TcpConnection> connections;
|
||||
/// <summary>Connections that have been closed and need to be removed from <see cref="connections"/>.</summary>
|
||||
private readonly List<IPEndPoint> closedConnections = new List<IPEndPoint>();
|
||||
/// <summary>The IP address to bind the socket to.</summary>
|
||||
private readonly IPAddress listenAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TcpServer(int socketBufferSize = DefaultSocketBufferSize) : this(IPAddress.IPv6Any, socketBufferSize) { }
|
||||
|
||||
/// <summary>Initializes the transport, binding the socket to a specific IP address.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
public TcpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(socketBufferSize)
|
||||
{
|
||||
this.listenAddress = listenAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<IPEndPoint, TcpConnection>();
|
||||
|
||||
StartListening(port);
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for connections on the given port.</summary>
|
||||
/// <param name="port">The port to listen on.</param>
|
||||
private void StartListening(ushort port)
|
||||
{
|
||||
if (isRunning)
|
||||
StopListening();
|
||||
|
||||
IPEndPoint localEndPoint = new IPEndPoint(listenAddress, port);
|
||||
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
SendBufferSize = socketBufferSize,
|
||||
ReceiveBufferSize = socketBufferSize,
|
||||
NoDelay = true,
|
||||
};
|
||||
socket.Bind(localEndPoint);
|
||||
socket.Listen(MaxPendingConnections);
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Poll()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
Accept();
|
||||
foreach (TcpConnection connection in connections.Values)
|
||||
connection.Receive();
|
||||
|
||||
foreach (IPEndPoint endPoint in closedConnections)
|
||||
connections.Remove(endPoint);
|
||||
|
||||
closedConnections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Accepts any pending connections.</summary>
|
||||
private void Accept()
|
||||
{
|
||||
if (socket.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
Socket acceptedSocket = socket.Accept();
|
||||
IPEndPoint fromEndPoint = (IPEndPoint)acceptedSocket.RemoteEndPoint;
|
||||
if (!connections.ContainsKey(fromEndPoint))
|
||||
{
|
||||
TcpConnection newConnection = new TcpConnection(acceptedSocket, fromEndPoint, this);
|
||||
connections.Add(fromEndPoint, newConnection);
|
||||
OnConnected(newConnection);
|
||||
}
|
||||
else
|
||||
acceptedSocket.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stops listening for connections.</summary>
|
||||
private void StopListening()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
isRunning = false;
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is TcpConnection tcpConnection)
|
||||
{
|
||||
closedConnections.Add(tcpConnection.RemoteEndPoint);
|
||||
tcpConnection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Shutdown()
|
||||
{
|
||||
StopListening();
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
/// <param name="connection">The successfully established connection.</param>
|
||||
protected virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnDataReceived(int amount, TcpConnection fromConnection)
|
||||
{
|
||||
if ((MessageHeader)(ReceiveBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect)
|
||||
{
|
||||
if (fromConnection.DidReceiveConnect)
|
||||
return;
|
||||
|
||||
fromConnection.DidReceiveConnect = true;
|
||||
}
|
||||
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Riptide/Transports/Udp/UdpClient.cs
Normal file
111
Riptide/Transports/Udp/UdpClient.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>A client which can connect to a <see cref="UdpServer"/>.</summary>
|
||||
public class UdpClient : UdpPeer, IClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler ConnectionFailed;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <summary>The connection to the server.</summary>
|
||||
private UdpConnection udpConnection;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UdpClient(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>.</remarks>
|
||||
public bool Connect(string hostAddress, out Connection connection, out string connectError)
|
||||
{
|
||||
connectError = $"Invalid host address '{hostAddress}'! IP and port should be separated by a colon, for example: '127.0.0.1:7777'.";
|
||||
if (!ParseHostAddress(hostAddress, out IPAddress ip, out ushort port))
|
||||
{
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((mode == SocketMode.IPv4Only && ip.AddressFamily == AddressFamily.InterNetworkV6) || (mode == SocketMode.IPv6Only && ip.AddressFamily == AddressFamily.InterNetwork))
|
||||
{
|
||||
// The IP address isn't in an acceptable format for the current socket mode
|
||||
if (mode == SocketMode.IPv4Only)
|
||||
connectError = "Connecting to IPv6 addresses is not allowed when running in IPv4 only mode!";
|
||||
else
|
||||
connectError = "Connecting to IPv4 addresses is not allowed when running in IPv6 only mode!";
|
||||
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
OpenSocket();
|
||||
|
||||
connection = udpConnection = new UdpConnection(new IPEndPoint(mode == SocketMode.IPv4Only ? ip : ip.MapToIPv6(), port), this);
|
||||
OnConnected(); // UDP is connectionless, so from the transport POV everything is immediately ready to send/receive data
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parses <paramref name="hostAddress"/> into <paramref name="ip"/> and <paramref name="port"/>, if possible.</summary>
|
||||
/// <param name="hostAddress">The host address to parse.</param>
|
||||
/// <param name="ip">The retrieved IP.</param>
|
||||
/// <param name="port">The retrieved port.</param>
|
||||
/// <returns>Whether or not <paramref name="hostAddress"/> was in a valid format.</returns>
|
||||
private bool ParseHostAddress(string hostAddress, out IPAddress ip, out ushort port)
|
||||
{
|
||||
string[] ipAndPort = hostAddress.Split(':');
|
||||
string ipString = "";
|
||||
string portString = "";
|
||||
if (ipAndPort.Length > 2)
|
||||
{
|
||||
// There was more than one ':' in the host address, might be IPv6
|
||||
ipString = string.Join(":", ipAndPort.Take(ipAndPort.Length - 1));
|
||||
portString = ipAndPort[ipAndPort.Length - 1];
|
||||
}
|
||||
else if (ipAndPort.Length == 2)
|
||||
{
|
||||
// IPv4
|
||||
ipString = ipAndPort[0];
|
||||
portString = ipAndPort[1];
|
||||
}
|
||||
|
||||
port = 0; // Need to make sure a value is assigned in case IP parsing fails
|
||||
return IPAddress.TryParse(ipString, out ip) && ushort.TryParse(portString, out port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Disconnect()
|
||||
{
|
||||
CloseSocket();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
protected virtual void OnConnectionFailed()
|
||||
{
|
||||
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
|
||||
{
|
||||
if (udpConnection.RemoteEndPoint.Equals(fromEndPoint) && !udpConnection.IsNotConnected)
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, udpConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Riptide/Transports/Udp/UdpConnection.cs
Normal file
80
Riptide/Transports/Udp/UdpConnection.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>Represents a connection to a <see cref="UdpServer"/> or <see cref="UdpClient"/>.</summary>
|
||||
public class UdpConnection : Connection, IEquatable<UdpConnection>
|
||||
{
|
||||
/// <summary>The endpoint representing the other end of the connection.</summary>
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
private readonly UdpPeer peer;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
/// <param name="remoteEndPoint">The endpoint representing the other end of the connection.</param>
|
||||
/// <param name="peer">The local peer this connection is associated with.</param>
|
||||
internal UdpConnection(IPEndPoint remoteEndPoint, UdpPeer peer)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
peer.Send(dataBuffer, amount, RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as UdpConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(UdpConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return RemoteEndPoint.Equals(other.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -288961498 + EqualityComparer<IPEndPoint>.Default.GetHashCode(RemoteEndPoint);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator ==(UdpConnection left, UdpConnection right)
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator !=(UdpConnection left, UdpConnection right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
}
|
||||
185
Riptide/Transports/Udp/UdpPeer.cs
Normal file
185
Riptide/Transports/Udp/UdpPeer.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>The kind of socket to create.</summary>
|
||||
public enum SocketMode
|
||||
{
|
||||
/// <summary>Dual-mode. Works with both IPv4 and IPv6.</summary>
|
||||
Both,
|
||||
/// <summary>IPv4 only mode.</summary>
|
||||
IPv4Only,
|
||||
/// <summary>IPv6 only mode.</summary>
|
||||
IPv6Only
|
||||
}
|
||||
|
||||
/// <summary>Provides base send & receive functionality for <see cref="UdpServer"/> and <see cref="UdpClient"/>.</summary>
|
||||
public abstract class UdpPeer
|
||||
{
|
||||
/// <inheritdoc cref="IPeer.Disconnected"/>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>The default size used for the socket's send and receive buffers.</summary>
|
||||
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
|
||||
/// <summary>The minimum size that may be used for the socket's send and receive buffers.</summary>
|
||||
private const int MinSocketBufferSize = 256 * 1024; // 256KB
|
||||
/// <summary>How long to wait for a packet, in microseconds.</summary>
|
||||
private const int ReceivePollingTime = 500000; // 0.5 seconds
|
||||
|
||||
/// <summary>Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</summary>
|
||||
protected readonly SocketMode mode;
|
||||
/// <summary>The size to use for the socket's send and receive buffers.</summary>
|
||||
private readonly int socketBufferSize;
|
||||
/// <summary>The array that incoming data is received into.</summary>
|
||||
private readonly byte[] receivedData;
|
||||
/// <summary>The socket to use for sending and receiving.</summary>
|
||||
private Socket socket;
|
||||
/// <summary>Whether or not the transport is running.</summary>
|
||||
private bool isRunning;
|
||||
/// <summary>A reusable endpoint.</summary>
|
||||
private EndPoint remoteEndPoint;
|
||||
|
||||
/// <summary>Initializes the transport.</summary>
|
||||
/// <param name="mode">Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
protected UdpPeer(SocketMode mode, int socketBufferSize)
|
||||
{
|
||||
if (socketBufferSize < MinSocketBufferSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
|
||||
|
||||
this.mode = mode;
|
||||
this.socketBufferSize = socketBufferSize;
|
||||
receivedData = new byte[Message.MaxSize];
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IPeer.Poll"/>
|
||||
public void Poll()
|
||||
{
|
||||
Receive();
|
||||
}
|
||||
|
||||
/// <summary>Opens the socket and starts the transport.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to, if any.</param>
|
||||
/// <param name="port">The port to bind the socket to.</param>
|
||||
protected void OpenSocket(IPAddress listenAddress = null, ushort port = 0)
|
||||
{
|
||||
if (isRunning)
|
||||
CloseSocket();
|
||||
|
||||
if (mode == SocketMode.IPv4Only)
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
else if (mode == SocketMode.IPv6Only)
|
||||
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) { DualMode = false };
|
||||
else
|
||||
socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
IPAddress any = socket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any;
|
||||
socket.SendBufferSize = socketBufferSize;
|
||||
socket.ReceiveBufferSize = socketBufferSize;
|
||||
socket.Bind(new IPEndPoint(listenAddress == null ? any : listenAddress, port));
|
||||
remoteEndPoint = new IPEndPoint(any, 0);
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/// <summary>Closes the socket and stops the transport.</summary>
|
||||
protected void CloseSocket()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
isRunning = false;
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <summary>Polls the socket and checks if any data was received.</summary>
|
||||
private void Receive()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
bool tryReceiveMore = true;
|
||||
while (tryReceiveMore)
|
||||
{
|
||||
int byteCount = 0;
|
||||
try
|
||||
{
|
||||
if (socket.Available > 0 && socket.Poll(ReceivePollingTime, SelectMode.SelectRead))
|
||||
byteCount = socket.ReceiveFrom(receivedData, SocketFlags.None, ref remoteEndPoint);
|
||||
else
|
||||
tryReceiveMore = false;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
switch (ex.SocketErrorCode)
|
||||
{
|
||||
case SocketError.Interrupted:
|
||||
case SocketError.NotSocket:
|
||||
isRunning = false;
|
||||
break;
|
||||
case SocketError.ConnectionReset:
|
||||
case SocketError.MessageSize:
|
||||
case SocketError.TimedOut:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
isRunning = false;
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
if (byteCount > 0)
|
||||
OnDataReceived(receivedData, byteCount, (IPEndPoint)remoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a given endpoint.</summary>
|
||||
/// <param name="dataBuffer">The array containing the data.</param>
|
||||
/// <param name="numBytes">The number of bytes in the array which should be sent.</param>
|
||||
/// <param name="toEndPoint">The endpoint to send the data to.</param>
|
||||
internal void Send(byte[] dataBuffer, int numBytes, IPEndPoint toEndPoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isRunning)
|
||||
socket.SendTo(dataBuffer, numBytes, SocketFlags.None, toEndPoint);
|
||||
}
|
||||
catch(SocketException)
|
||||
{
|
||||
// May want to consider triggering a disconnect here (perhaps depending on the type
|
||||
// of SocketException)? Timeout should catch disconnections, but disconnecting
|
||||
// explicitly might be better...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles received data.</summary>
|
||||
/// <param name="dataBuffer">A byte array containing the received data.</param>
|
||||
/// <param name="amount">The number of bytes in <paramref name="dataBuffer"/> used by the received data.</param>
|
||||
/// <param name="fromEndPoint">The endpoint from which the data was received.</param>
|
||||
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint);
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected virtual void OnDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Riptide/Transports/Udp/UdpServer.cs
Normal file
93
Riptide/Transports/Udp/UdpServer.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>A server which can accept connections from <see cref="UdpClient"/>s.</summary>
|
||||
public class UdpServer : UdpPeer, IServer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
/// <summary>The currently open connections, accessible by their endpoints.</summary>
|
||||
private Dictionary<IPEndPoint, Connection> connections;
|
||||
/// <summary>The IP address to bind the socket to, if any.</summary>
|
||||
private readonly IPAddress listenAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UdpServer(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
|
||||
|
||||
/// <summary>Initializes the transport, binding the socket to a specific IP address.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
public UdpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(SocketMode.Both, socketBufferSize)
|
||||
{
|
||||
this.listenAddress = listenAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<IPEndPoint, Connection>();
|
||||
|
||||
OpenSocket(listenAddress, port);
|
||||
}
|
||||
|
||||
/// <summary>Decides what to do with a connection attempt.</summary>
|
||||
/// <param name="fromEndPoint">The endpoint the connection attempt is coming from.</param>
|
||||
/// <returns>Whether or not the connection attempt was from a new connection.</returns>
|
||||
private bool HandleConnectionAttempt(IPEndPoint fromEndPoint)
|
||||
{
|
||||
if (connections.ContainsKey(fromEndPoint))
|
||||
return false;
|
||||
|
||||
UdpConnection connection = new UdpConnection(fromEndPoint, this);
|
||||
connections.Add(fromEndPoint, connection);
|
||||
OnConnected(connection);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is UdpConnection udpConnection)
|
||||
connections.Remove(udpConnection.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Shutdown()
|
||||
{
|
||||
CloseSocket();
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
/// <param name="connection">The successfully established connection.</param>
|
||||
protected virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
|
||||
{
|
||||
if ((MessageHeader)(dataBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect && !HandleConnectionAttempt(fromEndPoint))
|
||||
return;
|
||||
|
||||
if (connections.TryGetValue(fromEndPoint, out Connection connection) && !connection.IsNotConnected)
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
150
Riptide/Utils/Bitfield.cs
Normal file
150
Riptide/Utils/Bitfield.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Provides functionality for managing and manipulating a collection of bits.</summary>
|
||||
internal class Bitfield
|
||||
{
|
||||
/// <summary>The first 8 bits stored in the bitfield.</summary>
|
||||
internal byte First8 => (byte)segments[0];
|
||||
/// <summary>The first 16 bits stored in the bitfield.</summary>
|
||||
internal ushort First16 => (ushort)segments[0];
|
||||
|
||||
/// <summary>The number of bits which fit into a single segment.</summary>
|
||||
private const int SegmentSize = sizeof(uint) * 8;
|
||||
/// <summary>The segments of the bitfield.</summary>
|
||||
private readonly List<uint> segments;
|
||||
/// <summary>Whether or not the bitfield's capacity should dynamically adjust when shifting.</summary>
|
||||
private readonly bool isDynamicCapacity;
|
||||
/// <summary>The current number of bits being stored.</summary>
|
||||
private int count;
|
||||
/// <summary>The current capacity.</summary>
|
||||
private int capacity;
|
||||
|
||||
/// <summary>Creates a bitfield.</summary>
|
||||
/// <param name="isDynamicCapacity">Whether or not the bitfield's capacity should dynamically adjust when shifting.</param>
|
||||
internal Bitfield(bool isDynamicCapacity = true)
|
||||
{
|
||||
segments = new List<uint>(4) { 0 };
|
||||
capacity = segments.Count * SegmentSize;
|
||||
this.isDynamicCapacity = isDynamicCapacity;
|
||||
}
|
||||
|
||||
/// <summary>Checks if the bitfield has capacity for the given number of bits.</summary>
|
||||
/// <param name="amount">The number of bits for which to check if there is capacity.</param>
|
||||
/// <param name="overflow">The number of bits from <paramref name="amount"/> which there is no capacity for.</param>
|
||||
/// <returns>Whether or not there is sufficient capacity.</returns>
|
||||
internal bool HasCapacityFor(int amount, out int overflow)
|
||||
{
|
||||
overflow = count + amount - capacity;
|
||||
return overflow < 0;
|
||||
}
|
||||
|
||||
/// <summary>Shifts the bitfield by the given amount.</summary>
|
||||
/// <param name="amount">How much to shift by.</param>
|
||||
internal void ShiftBy(int amount)
|
||||
{
|
||||
int segmentShift = amount / SegmentSize; // How many WHOLE segments we have to shift by
|
||||
int bitShift = amount % SegmentSize; // How many bits we have to shift by
|
||||
|
||||
if (!isDynamicCapacity)
|
||||
count = Math.Min(count + amount, SegmentSize);
|
||||
else if (!HasCapacityFor(amount, out int _))
|
||||
{
|
||||
Trim();
|
||||
count += amount;
|
||||
|
||||
if (count > capacity)
|
||||
{
|
||||
int increaseBy = segmentShift + 1;
|
||||
for (int i = 0; i < increaseBy; i++)
|
||||
segments.Add(0);
|
||||
|
||||
capacity = segments.Count * SegmentSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
count += amount;
|
||||
|
||||
int s = segments.Count - 1;
|
||||
segments[s] <<= bitShift;
|
||||
s -= 1 + segmentShift;
|
||||
while (s > -1)
|
||||
{
|
||||
ulong shiftedBits = (ulong)segments[s] << bitShift;
|
||||
segments[s] = (uint)shiftedBits;
|
||||
|
||||
segments[s + 1 + segmentShift] |= (uint)(shiftedBits >> SegmentSize);
|
||||
s--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks the last bit in the bitfield, and trims it if it is set to 1.</summary>
|
||||
/// <param name="checkedPosition">The checked bit's position in the bitfield.</param>
|
||||
/// <returns>Whether or not the checked bit was set.</returns>
|
||||
internal bool CheckAndTrimLast(out int checkedPosition)
|
||||
{
|
||||
checkedPosition = count;
|
||||
uint bitToCheck = (uint)(1 << ((count - 1) % SegmentSize));
|
||||
bool isSet = (segments[segments.Count - 1] & bitToCheck) != 0;
|
||||
count--;
|
||||
return isSet;
|
||||
}
|
||||
|
||||
/// <summary>Trims all bits from the end of the bitfield until an unset bit is encountered.</summary>
|
||||
private void Trim()
|
||||
{
|
||||
while (count > 0 && IsSet(count))
|
||||
count--;
|
||||
}
|
||||
|
||||
/// <summary>Sets the given bit to 1.</summary>
|
||||
/// <param name="bit">The bit to set.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="bit"/> is less than 1.</exception>
|
||||
internal void Set(int bit)
|
||||
{
|
||||
if (bit < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(bit), $"'{nameof(bit)}' must be greater than zero!");
|
||||
|
||||
bit--;
|
||||
int s = bit / SegmentSize;
|
||||
uint bitToSet = (uint)(1 << (bit % SegmentSize));
|
||||
if (s < segments.Count)
|
||||
segments[s] |= bitToSet;
|
||||
}
|
||||
|
||||
/// <summary>Checks if the given bit is set to 1.</summary>
|
||||
/// <param name="bit">The bit to check.</param>
|
||||
/// <returns>Whether or not the bit is set.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="bit"/> is less than 1.</exception>
|
||||
internal bool IsSet(int bit)
|
||||
{
|
||||
if (bit > count)
|
||||
return true;
|
||||
|
||||
if (bit < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(bit), $"'{nameof(bit)}' must be greater than zero!");
|
||||
|
||||
bit--;
|
||||
int s = bit / SegmentSize;
|
||||
uint bitToCheck = (uint)(1 << (bit % SegmentSize));
|
||||
if (s < segments.Count)
|
||||
return (segments[s] & bitToCheck) != 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Combines this bitfield with the given bits.</summary>
|
||||
/// <param name="other">The bits to OR into the bitfield.</param>
|
||||
internal void Combine(ushort other)
|
||||
{
|
||||
segments[0] |= other;
|
||||
}
|
||||
}
|
||||
}
|
||||
202
Riptide/Utils/ConnectionMetrics.cs
Normal file
202
Riptide/Utils/ConnectionMetrics.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Tracks and manages various metrics of a <see cref="Connection"/>.</summary>
|
||||
public class ConnectionMetrics
|
||||
{
|
||||
/// <summary>The total number of bytes received across all send modes since the last <see cref="Reset"/> call, including those in duplicate and, in
|
||||
/// the case of notify messages, out-of-order packets. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int BytesIn => UnreliableBytesIn + NotifyBytesIn + ReliableBytesIn;
|
||||
/// <summary>The total number of bytes sent across all send modes since the last <see cref="Reset"/> call, including those in automatic resends.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int BytesOut => UnreliableBytesOut + NotifyBytesOut + ReliableBytesOut;
|
||||
/// <summary>The total number of messages received across all send modes since the last <see cref="Reset"/> call, including duplicate and out-of-order notify messages.</summary>
|
||||
public int MessagesIn => UnreliableIn + NotifyIn + ReliableIn;
|
||||
/// <summary>The total number of messages sent across all send modes since the last <see cref="Reset"/> call, including automatic resends.</summary>
|
||||
public int MessagesOut => UnreliableOut + NotifyOut + ReliableOut;
|
||||
|
||||
/// <summary>The total number of bytes received in unreliable messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int UnreliableBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in unreliable messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int UnreliableBytesOut { get; internal set; }
|
||||
/// <summary>The number of unreliable messages received since the last <see cref="Reset"/> call.</summary>
|
||||
public int UnreliableIn { get; private set; }
|
||||
/// <summary>The number of unreliable messages sent since the last <see cref="Reset"/> call.</summary>
|
||||
public int UnreliableOut { get; internal set; }
|
||||
|
||||
/// <summary>The total number of bytes received in notify messages since the last <see cref="Reset"/> call, including those in duplicate and out-of-order packets.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int NotifyBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in notify messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int NotifyBytesOut { get; internal set; }
|
||||
/// <summary>The number of notify messages received since the last <see cref="Reset"/> call, including duplicate and out-of-order ones.</summary>
|
||||
public int NotifyIn { get; private set; }
|
||||
/// <summary>The number of notify messages sent since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyOut { get; internal set; }
|
||||
/// <summary>The number of duplicate or out-of-order notify messages which were received, but discarded (not handled) since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyDiscarded { get; internal set; }
|
||||
/// <summary>The number of notify messages lost since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyLost { get; private set; }
|
||||
/// <summary>The number of notify messages delivered since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyDelivered { get; private set; }
|
||||
/// <summary>The number of notify messages lost of the last 64 notify messages to be lost or delivered.</summary>
|
||||
public int RollingNotifyLost { get; private set; }
|
||||
/// <summary>The number of notify messages delivered of the last 64 notify messages to be lost or delivered.</summary>
|
||||
public int RollingNotifyDelivered { get; private set; }
|
||||
/// <summary>The loss rate (0-1) among the last 64 notify messages.</summary>
|
||||
public float RollingNotifyLossRate => RollingNotifyLost / 64f;
|
||||
|
||||
/// <summary>The total number of bytes received in reliable messages since the last <see cref="Reset"/> call, including those in duplicate packets.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int ReliableBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in reliable messages since the last <see cref="Reset"/> call, including those in automatic resends.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int ReliableBytesOut { get; internal set; }
|
||||
/// <summary>The number of reliable messages received since the last <see cref="Reset"/> call, including duplicates.</summary>
|
||||
public int ReliableIn { get; private set; }
|
||||
/// <summary>The number of reliable messages sent since the last <see cref="Reset"/> call, including automatic resends (each resend adds to this value).</summary>
|
||||
public int ReliableOut { get; internal set; }
|
||||
/// <summary>The number of duplicate reliable messages which were received, but discarded (and not handled) since the last <see cref="Reset"/> call.</summary>
|
||||
public int ReliableDiscarded { get; internal set; }
|
||||
/// <summary>The number of unique reliable messages sent since the last <see cref="Reset"/> call.
|
||||
/// A message only counts towards this the first time it is sent—subsequent resends are not counted.</summary>
|
||||
public int ReliableUniques { get; internal set; }
|
||||
/// <summary>The number of send attempts that were required to deliver recent reliable messages.</summary>
|
||||
public readonly RollingStat RollingReliableSends;
|
||||
|
||||
/// <summary>The left-most bit of a <see cref="ulong"/>, used to store the oldest value in the <see cref="notifyLossTracker"/>.</summary>
|
||||
private const ulong ULongLeftBit = 1ul << 63;
|
||||
/// <summary>Which recent notify messages were lost. Each bit corresponds to a message.</summary>
|
||||
private ulong notifyLossTracker;
|
||||
/// <summary>How many of the <see cref="notifyLossTracker"/>'s bits are in use.</summary>
|
||||
private int notifyBufferCount;
|
||||
|
||||
/// <summary>Initializes metrics.</summary>
|
||||
public ConnectionMetrics()
|
||||
{
|
||||
Reset();
|
||||
RollingNotifyDelivered = 0;
|
||||
RollingNotifyLost = 0;
|
||||
notifyLossTracker = 0;
|
||||
notifyBufferCount = 0;
|
||||
RollingReliableSends = new RollingStat(64);
|
||||
}
|
||||
|
||||
/// <summary>Resets all non-rolling metrics to 0.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
UnreliableBytesIn = 0;
|
||||
UnreliableBytesOut = 0;
|
||||
UnreliableIn = 0;
|
||||
UnreliableOut = 0;
|
||||
|
||||
NotifyBytesIn = 0;
|
||||
NotifyBytesOut = 0;
|
||||
NotifyIn = 0;
|
||||
NotifyOut = 0;
|
||||
NotifyDiscarded = 0;
|
||||
NotifyLost = 0;
|
||||
NotifyDelivered = 0;
|
||||
|
||||
ReliableBytesIn = 0;
|
||||
ReliableBytesOut = 0;
|
||||
ReliableIn = 0;
|
||||
ReliableOut = 0;
|
||||
ReliableDiscarded = 0;
|
||||
ReliableUniques = 0;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving an unreliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedUnreliable(int byteCount)
|
||||
{
|
||||
UnreliableBytesIn += byteCount;
|
||||
UnreliableIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending an unreliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentUnreliable(int byteCount)
|
||||
{
|
||||
UnreliableBytesOut += byteCount;
|
||||
UnreliableOut++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving a notify message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedNotify(int byteCount)
|
||||
{
|
||||
NotifyBytesIn += byteCount;
|
||||
NotifyIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending a notify message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentNotify(int byteCount)
|
||||
{
|
||||
NotifyBytesOut += byteCount;
|
||||
NotifyOut++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with delivering a notify message.</summary>
|
||||
internal void DeliveredNotify()
|
||||
{
|
||||
NotifyDelivered++;
|
||||
|
||||
if (notifyBufferCount < 64)
|
||||
{
|
||||
RollingNotifyDelivered++;
|
||||
notifyBufferCount++;
|
||||
}
|
||||
else if ((notifyLossTracker & ULongLeftBit) == 0)
|
||||
{
|
||||
// The one being removed from the buffer was not delivered
|
||||
RollingNotifyDelivered++;
|
||||
RollingNotifyLost--;
|
||||
}
|
||||
|
||||
notifyLossTracker <<= 1;
|
||||
notifyLossTracker |= 1;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with losing a notify message.</summary>
|
||||
internal void LostNotify()
|
||||
{
|
||||
NotifyLost++;
|
||||
|
||||
if (notifyBufferCount < 64)
|
||||
{
|
||||
RollingNotifyLost++;
|
||||
notifyBufferCount++;
|
||||
}
|
||||
else if ((notifyLossTracker & ULongLeftBit) != 0)
|
||||
{
|
||||
// The one being removed from the buffer was delivered
|
||||
RollingNotifyDelivered--;
|
||||
RollingNotifyLost++;
|
||||
}
|
||||
|
||||
notifyLossTracker <<= 1;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving a reliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedReliable(int byteCount)
|
||||
{
|
||||
ReliableBytesIn += byteCount;
|
||||
ReliableIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending a reliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentReliable(int byteCount)
|
||||
{
|
||||
ReliableBytesOut += byteCount;
|
||||
ReliableOut++;
|
||||
}
|
||||
}
|
||||
}
|
||||
954
Riptide/Utils/Converter.cs
Normal file
954
Riptide/Utils/Converter.cs
Normal file
@@ -0,0 +1,954 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Provides functionality for converting bits and bytes to various value types and vice versa.</summary>
|
||||
public class Converter
|
||||
{
|
||||
/// <summary>The number of bits in a byte.</summary>
|
||||
public const int BitsPerByte = 8;
|
||||
/// <summary>The number of bits in a ulong.</summary>
|
||||
public const int BitsPerULong = sizeof(ulong) * BitsPerByte;
|
||||
|
||||
#region Zig Zag Encoding
|
||||
/// <summary>Zig zag encodes <paramref name="value"/>.</summary>
|
||||
/// <param name="value">The value to encode.</param>
|
||||
/// <returns>The zig zag-encoded value.</returns>
|
||||
/// <remarks>Zig zag encoding allows small negative numbers to be represented as small positive numbers. All positive numbers are doubled and become even numbers,
|
||||
/// while all negative numbers become positive odd numbers. In contrast, simply casting a negative value to its unsigned counterpart would result in a large positive
|
||||
/// number which uses the high bit, rendering compression via <see cref="Message.AddVarULong(ulong)"/> and <see cref="Message.GetVarULong"/> ineffective.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ZigZagEncode(int value)
|
||||
{
|
||||
return (value >> 31) ^ (value << 1);
|
||||
}
|
||||
/// <inheritdoc cref="ZigZagEncode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ZigZagEncode(long value)
|
||||
{
|
||||
return (value >> 63) ^ (value << 1);
|
||||
}
|
||||
|
||||
/// <summary>Zig zag decodes <paramref name="value"/>.</summary>
|
||||
/// <param name="value">The value to decode.</param>
|
||||
/// <returns>The zig zag-decoded value.</returns>
|
||||
/// <inheritdoc cref="ZigZagEncode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ZigZagDecode(int value)
|
||||
{
|
||||
return (value >> 1) ^ -(value & 1);
|
||||
}
|
||||
/// <inheritdoc cref="ZigZagDecode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ZigZagDecode(long value)
|
||||
{
|
||||
return (value >> 1) ^ -(value & 1);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Bits
|
||||
/// <summary>Takes <paramref name="amount"/> bits from <paramref name="bitfield"/> and writes them into <paramref name="array"/>, starting at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="bitfield">The bitfield from which to write the bits into the array.</param>
|
||||
/// <param name="amount">The number of bits to write.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The bit position in the array at which to start writing.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(byte bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
byte mask = (byte)((1 << amount) - 1);
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
array[pos + 1] = (byte)((bitfield >> (8 - bit)) | (array[pos + 1] & (inverseMask >> (8 - bit))));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ushort bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
ushort mask = (ushort)((1 << amount) - 1);
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(uint bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
uint mask = (1u << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
uint inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
array[pos + 2] = (byte)((bitfield >> 16) | (array[pos + 2] & (inverseMask >> 16)));
|
||||
array[pos + 3] = (byte)((bitfield >> 24) | (array[pos + 3] & (inverseMask >> 24)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
array[pos + 3] = (byte)((bitfield >> 16) | (array[pos + 3] & (inverseMask >> 16)));
|
||||
array[pos + 4] = (byte)((bitfield >> 24) | (array[pos + 4] & ~(mask >> (32 - bit)))); // This one can't use inverseMask because it would have incorrectly zeroed bits
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ulong bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
ulong mask = (1ul << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
ulong inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
array[pos + 2] = (byte)((bitfield >> 16) | (array[pos + 2] & (inverseMask >> 16)));
|
||||
array[pos + 3] = (byte)((bitfield >> 24) | (array[pos + 3] & (inverseMask >> 24)));
|
||||
array[pos + 4] = (byte)((bitfield >> 32) | (array[pos + 4] & (inverseMask >> 32)));
|
||||
array[pos + 5] = (byte)((bitfield >> 40) | (array[pos + 5] & (inverseMask >> 40)));
|
||||
array[pos + 6] = (byte)((bitfield >> 48) | (array[pos + 6] & (inverseMask >> 48)));
|
||||
array[pos + 7] = (byte)((bitfield >> 56) | (array[pos + 7] & (inverseMask >> 56)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
array[pos + 3] = (byte)((bitfield >> 16) | (array[pos + 3] & (inverseMask >> 16)));
|
||||
array[pos + 4] = (byte)((bitfield >> 24) | (array[pos + 4] & (inverseMask >> 24)));
|
||||
array[pos + 5] = (byte)((bitfield >> 32) | (array[pos + 5] & (inverseMask >> 32)));
|
||||
array[pos + 6] = (byte)((bitfield >> 40) | (array[pos + 6] & (inverseMask >> 40)));
|
||||
array[pos + 7] = (byte)((bitfield >> 48) | (array[pos + 7] & (inverseMask >> 48)));
|
||||
array[pos + 8] = (byte)((bitfield >> 56) | (array[pos + 8] & ~(mask >> (64 - bit)))); // This one can't use inverseMask because it would have incorrectly zeroed bits
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ulong bitfield, int amount, ulong[] array, int startBit)
|
||||
{
|
||||
ulong mask = (1ul << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = bitfield | array[pos] & ~mask;
|
||||
else
|
||||
{
|
||||
array[pos] = (bitfield << bit) | (array[pos] & ~(mask << bit));
|
||||
if (bit + amount >= BitsPerULong)
|
||||
array[pos + 1] = (bitfield >> (64 - bit)) | (array[pos + 1] & ~(mask >> (64 - bit)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starting at <paramref name="startBit"/>, reads <paramref name="amount"/> bits from <paramref name="array"/> into <paramref name="bitfield"/>.</summary>
|
||||
/// <param name="amount">The number of bits to read.</param>
|
||||
/// <param name="array">The array to read the bits from.</param>
|
||||
/// <param name="startBit">The bit position in the array at which to start reading.</param>
|
||||
/// <param name="bitfield">The bitfield into which to write the bits from the array.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out byte bitfield)
|
||||
{
|
||||
bitfield = ByteFromBits(array, startBit);
|
||||
bitfield &= (byte)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out ushort bitfield)
|
||||
{
|
||||
bitfield = UShortFromBits(array, startBit);
|
||||
bitfield &= (ushort)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out uint bitfield)
|
||||
{
|
||||
bitfield = UIntFromBits(array, startBit);
|
||||
bitfield &= (1u << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out ulong bitfield)
|
||||
{
|
||||
bitfield = ULongFromBits(array, startBit);
|
||||
bitfield &= (1ul << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out byte bitfield)
|
||||
{
|
||||
bitfield = ByteFromBits(array, startBit);
|
||||
bitfield &= (byte)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out ushort bitfield)
|
||||
{
|
||||
bitfield = UShortFromBits(array, startBit);
|
||||
bitfield &= (ushort)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out uint bitfield)
|
||||
{
|
||||
bitfield = UIntFromBits(array, startBit);
|
||||
bitfield &= (1u << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out ulong bitfield)
|
||||
{
|
||||
bitfield = ULongFromBits(array, startBit);
|
||||
bitfield &= (1ul << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Byte/SByte
|
||||
/// <summary>Converts <paramref name="value"/> to 8 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="sbyte"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SByteToBits(sbyte value, byte[] array, int startBit) => ByteToBits((byte)value, array, startBit);
|
||||
/// <inheritdoc cref="SByteToBits(sbyte, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SByteToBits(sbyte value, ulong[] array, int startBit) => ByteToBits((byte)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 8 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="byte"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ByteToBits(byte value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
array[pos + 1] = (byte)(value >> (8 - bit));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="ByteToBits(byte, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ByteToBits(byte value, ulong[] array, int startBit) => ToBits(value, BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 8 bits at <paramref name="startBit"/> in <paramref name="array"/> to an <see cref="sbyte"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="sbyte"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static sbyte SByteFromBits(byte[] array, int startBit) => (sbyte)ByteFromBits(array, startBit);
|
||||
/// <inheritdoc cref="SByteFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static sbyte SByteFromBits(ulong[] array, int startBit) => (sbyte)ByteFromBits(array, startBit);
|
||||
/// <summary>Converts the 8 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="byte"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="byte"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte ByteFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
byte value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return (byte)(value | (array[pos + 1] << (8 - bit)));
|
||||
}
|
||||
/// <inheritdoc cref="ByteFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte ByteFromBits(ulong[] array, int startBit) => (byte)FromBits(BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Bool
|
||||
/// <summary>Converts <paramref name="value"/> to a bit and writes it into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="bool"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bit into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bit.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BoolToBit(bool value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = 0;
|
||||
|
||||
if (value)
|
||||
array[pos] |= (byte)(1 << bit);
|
||||
}
|
||||
/// <inheritdoc cref="BoolToBit(bool, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BoolToBit(bool value, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = 0;
|
||||
|
||||
if (value)
|
||||
array[pos] |= 1ul << bit;
|
||||
}
|
||||
|
||||
/// <summary>Converts the bit at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="bool"/>.</summary>
|
||||
/// <param name="array">The array to convert the bit from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bit.</param>
|
||||
/// <returns>The converted <see cref="bool"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool BoolFromBit(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
return (array[pos] & (1 << bit)) != 0;
|
||||
}
|
||||
/// <inheritdoc cref="BoolFromBit(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool BoolFromBit(ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
return (array[pos] & (1ul << bit)) != 0;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Short/UShort
|
||||
/// <summary>Converts a given <see cref="short"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="short"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromShort(short value, byte[] array, int startIndex) => FromUShort((ushort)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="ushort"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="ushort"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromUShort(ushort value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 1] = (byte)value;
|
||||
array[startIndex ] = (byte)(value >> 8);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 2 bytes in the array at <paramref name="startIndex"/> to a <see cref="short"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="short"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ToShort(byte[] array, int startIndex) => (short)ToUShort(array, startIndex);
|
||||
/// <summary>Converts the 2 bytes in the array at <paramref name="startIndex"/> to a <see cref="ushort"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="ushort"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort ToUShort(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return (ushort)(array[startIndex + 1] | (array[startIndex ] << 8));
|
||||
#else
|
||||
return (ushort)(array[startIndex ] | (array[startIndex + 1] << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 16 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="short"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ShortToBits(short value, byte[] array, int startBit) => UShortToBits((ushort)value, array, startBit);
|
||||
/// <inheritdoc cref="ShortToBits(short, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ShortToBits(short value, ulong[] array, int startBit) => UShortToBits((ushort)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 16 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="ushort"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UShortToBits(ushort value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="UShortToBits(ushort, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UShortToBits(ushort value, ulong[] array, int startBit) => ToBits(value, sizeof(ushort) * BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 16 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="short"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="short"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ShortFromBits(byte[] array, int startBit) => (short)UShortFromBits(array, startBit);
|
||||
/// <inheritdoc cref="ShortFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ShortFromBits(ulong[] array, int startBit) => (short)UShortFromBits(array, startBit);
|
||||
/// <summary>Converts the 16 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ushort"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ushort"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort UShortFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
ushort value = (ushort)(array[pos] | (array[pos + 1] << 8));
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return (ushort)(value | (array[pos + 2] << (16 - bit)));
|
||||
}
|
||||
/// <inheritdoc cref="UShortFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort UShortFromBits(ulong[] array, int startBit) => (ushort)FromBits(sizeof(ushort) * BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Int/UInt
|
||||
/// <summary>Converts a given <see cref="int"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="int"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromInt(int value, byte[] array, int startIndex) => FromUInt((uint)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="uint"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="uint"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromUInt(uint value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 3] = (byte)value;
|
||||
array[startIndex + 2] = (byte)(value >> 8);
|
||||
array[startIndex + 1] = (byte)(value >> 16);
|
||||
array[startIndex ] = (byte)(value >> 24);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
array[startIndex + 2] = (byte)(value >> 16);
|
||||
array[startIndex + 3] = (byte)(value >> 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="int"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="int"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ToInt(byte[] array, int startIndex) => (int)ToUInt(array, startIndex);
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="uint"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="uint"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint ToUInt(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return (uint)(array[startIndex + 3] | (array[startIndex + 2] << 8) | (array[startIndex + 1] << 16) | (array[startIndex ] << 24));
|
||||
#else
|
||||
return (uint)(array[startIndex ] | (array[startIndex + 1] << 8) | (array[startIndex + 2] << 16) | (array[startIndex + 3] << 24));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="int"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IntToBits(int value, byte[] array, int startBit) => UIntToBits((uint)value, array, startBit);
|
||||
/// <inheritdoc cref="IntToBits(int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IntToBits(int value, ulong[] array, int startBit) => UIntToBits((uint)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="uint"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UIntToBits(uint value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
array[pos + 2] = (byte)(value >> 16);
|
||||
array[pos + 3] = (byte)(value >> 24);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
array[pos + 3] = (byte)(value >> 16);
|
||||
array[pos + 4] = (byte)(value >> 24);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="UIntToBits(uint, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UIntToBits(uint value, ulong[] array, int startBit) => ToBits(value, sizeof(uint) * BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to an <see cref="int"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="int"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int IntFromBits(byte[] array, int startBit) => (int)UIntFromBits(array, startBit);
|
||||
/// <inheritdoc cref="IntFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int IntFromBits(ulong[] array, int startBit) => (int)UIntFromBits(array, startBit);
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="uint"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="uint"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint UIntFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
uint value = (uint)(array[pos] | (array[pos + 1] << 8) | (array[pos + 2] << 16) | (array[pos + 3] << 24));
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | (uint)(array[pos + 4] << (32 - bit));
|
||||
}
|
||||
/// <inheritdoc cref="UIntFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint UIntFromBits(ulong[] array, int startBit) => (uint)FromBits(sizeof(uint) * BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Long/ULong
|
||||
/// <summary>Converts a given <see cref="long"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="long"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromLong(long value, byte[] array, int startIndex) => FromULong((ulong)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="ulong"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="ulong"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromULong(ulong value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 7] = (byte)value;
|
||||
array[startIndex + 6] = (byte)(value >> 8);
|
||||
array[startIndex + 5] = (byte)(value >> 16);
|
||||
array[startIndex + 4] = (byte)(value >> 24);
|
||||
array[startIndex + 3] = (byte)(value >> 32);
|
||||
array[startIndex + 2] = (byte)(value >> 40);
|
||||
array[startIndex + 1] = (byte)(value >> 48);
|
||||
array[startIndex ] = (byte)(value >> 56);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
array[startIndex + 2] = (byte)(value >> 16);
|
||||
array[startIndex + 3] = (byte)(value >> 24);
|
||||
array[startIndex + 4] = (byte)(value >> 32);
|
||||
array[startIndex + 5] = (byte)(value >> 40);
|
||||
array[startIndex + 6] = (byte)(value >> 48);
|
||||
array[startIndex + 7] = (byte)(value >> 56);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="long"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="long"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ToLong(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, longLength);
|
||||
#endif
|
||||
return BitConverter.ToInt64(array, startIndex);
|
||||
}
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="ulong"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ToULong(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, ulongLength);
|
||||
#endif
|
||||
return BitConverter.ToUInt64(array, startIndex);
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="long"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LongToBits(long value, byte[] array, int startBit) => ULongToBits((ulong)value, array, startBit);
|
||||
/// <inheritdoc cref="LongToBits(long, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LongToBits(long value, ulong[] array, int startBit) => ULongToBits((ulong)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="ulong"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ULongToBits(ulong value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
array[pos + 2] = (byte)(value >> 16);
|
||||
array[pos + 3] = (byte)(value >> 24);
|
||||
array[pos + 4] = (byte)(value >> 32);
|
||||
array[pos + 5] = (byte)(value >> 40);
|
||||
array[pos + 6] = (byte)(value >> 48);
|
||||
array[pos + 7] = (byte)(value >> 56);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
array[pos + 3] = (byte)(value >> 16);
|
||||
array[pos + 4] = (byte)(value >> 24);
|
||||
array[pos + 5] = (byte)(value >> 32);
|
||||
array[pos + 6] = (byte)(value >> 40);
|
||||
array[pos + 7] = (byte)(value >> 48);
|
||||
array[pos + 8] = (byte)(value >> 56);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="ULongToBits(ulong, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ULongToBits(ulong value, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else
|
||||
{
|
||||
array[pos ] |= value << bit;
|
||||
array[pos + 1] = value >> (BitsPerULong - bit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="long"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="long"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long LongFromBits(byte[] array, int startBit) => (long)ULongFromBits(array, startBit);
|
||||
/// <inheritdoc cref="LongFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long LongFromBits(ulong[] array, int startBit) => (long)ULongFromBits(array, startBit);
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ulong"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ULongFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
ulong value = BitConverter.ToUInt64(array, pos);
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | ((ulong)array[pos + 8] << (64 - bit));
|
||||
}
|
||||
/// <inheritdoc cref="ULongFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ULongFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
ulong value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | (array[pos + 1] << (BitsPerULong - bit));
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to <paramref name="valueSize"/> bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.
|
||||
/// Meant for values which fit into a <see cref="ulong"/>, not for <see cref="ulong"/>s themselves.</summary>
|
||||
/// <param name="value">The value to convert.</param>
|
||||
/// <param name="valueSize">The size in bits of the value being converted.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ToBits(ulong value, int valueSize, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else if (bit + valueSize < BitsPerULong)
|
||||
array[pos] |= value << bit;
|
||||
else
|
||||
{
|
||||
array[pos] |= value << bit;
|
||||
array[pos + 1] = value >> (BitsPerULong - bit);
|
||||
}
|
||||
}
|
||||
/// <summary>Converts the <paramref name="valueSize"/> bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ulong"/>.
|
||||
/// Meant for values which fit into a <see cref="ulong"/>, not for <see cref="ulong"/>s themselves.</summary>
|
||||
/// <param name="valueSize">The size in bits of the value being converted.</param>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong FromBits(int valueSize, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
ulong value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
if (bit + valueSize < BitsPerULong)
|
||||
return value;
|
||||
|
||||
return value | (array[pos + 1] << (BitsPerULong - bit));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Float
|
||||
/// <summary>Converts a given <see cref="float"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="float"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromFloat(float value, byte[] array, int startIndex)
|
||||
{
|
||||
FloatConverter converter = new FloatConverter { FloatValue = value };
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 3] = converter.Byte0;
|
||||
array[startIndex + 2] = converter.Byte1;
|
||||
array[startIndex + 1] = converter.Byte2;
|
||||
array[startIndex ] = converter.Byte3;
|
||||
#else
|
||||
array[startIndex ] = converter.Byte0;
|
||||
array[startIndex + 1] = converter.Byte1;
|
||||
array[startIndex + 2] = converter.Byte2;
|
||||
array[startIndex + 3] = converter.Byte3;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="float"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="float"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float ToFloat(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return new FloatConverter { Byte3 = array[startIndex], Byte2 = array[startIndex + 1], Byte1 = array[startIndex + 2], Byte0 = array[startIndex + 3] }.FloatValue;
|
||||
#else
|
||||
return new FloatConverter { Byte0 = array[startIndex], Byte1 = array[startIndex + 1], Byte2 = array[startIndex + 2], Byte3 = array[startIndex + 3] }.FloatValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="float"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FloatToBits(float value, byte[] array, int startBit)
|
||||
{
|
||||
UIntToBits(new FloatConverter { FloatValue = value }.UIntValue, array, startBit);
|
||||
}
|
||||
/// <inheritdoc cref="FloatToBits(float, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FloatToBits(float value, ulong[] array, int startBit)
|
||||
{
|
||||
UIntToBits(new FloatConverter { FloatValue = value }.UIntValue, array, startBit);
|
||||
}
|
||||
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="float"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="float"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float FloatFromBits(byte[] array, int startBit)
|
||||
{
|
||||
return new FloatConverter { UIntValue = UIntFromBits(array, startBit) }.FloatValue;
|
||||
}
|
||||
/// <inheritdoc cref="FloatFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float FloatFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
return new FloatConverter { UIntValue = UIntFromBits(array, startBit) }.FloatValue;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Double
|
||||
/// <summary>Converts a given <see cref="double"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="double"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromDouble(double value, byte[] array, int startIndex)
|
||||
{
|
||||
DoubleConverter converter = new DoubleConverter { DoubleValue = value };
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 7] = converter.Byte0;
|
||||
array[startIndex + 6] = converter.Byte1;
|
||||
array[startIndex + 5] = converter.Byte2;
|
||||
array[startIndex + 4] = converter.Byte3;
|
||||
array[startIndex + 3] = converter.Byte4;
|
||||
array[startIndex + 2] = converter.Byte5;
|
||||
array[startIndex + 1] = converter.Byte6;
|
||||
array[startIndex ] = converter.Byte7;
|
||||
#else
|
||||
array[startIndex ] = converter.Byte0;
|
||||
array[startIndex + 1] = converter.Byte1;
|
||||
array[startIndex + 2] = converter.Byte2;
|
||||
array[startIndex + 3] = converter.Byte3;
|
||||
array[startIndex + 4] = converter.Byte4;
|
||||
array[startIndex + 5] = converter.Byte5;
|
||||
array[startIndex + 6] = converter.Byte6;
|
||||
array[startIndex + 7] = converter.Byte7;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="double"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="double"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double ToDouble(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, doubleLength);
|
||||
#endif
|
||||
return BitConverter.ToDouble(array, startIndex);
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="double"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DoubleToBits(double value, byte[] array, int startBit)
|
||||
{
|
||||
ULongToBits(new DoubleConverter { DoubleValue = value }.ULongValue, array, startBit);
|
||||
}
|
||||
/// <inheritdoc cref="DoubleToBits(double, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DoubleToBits(double value, ulong[] array, int startBit)
|
||||
{
|
||||
ULongToBits(new DoubleConverter { DoubleValue = value }.ULongValue, array, startBit);
|
||||
}
|
||||
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="double"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="double"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double DoubleFromBits(byte[] array, int startBit)
|
||||
{
|
||||
return new DoubleConverter { ULongValue = ULongFromBits(array, startBit) }.DoubleValue;
|
||||
}
|
||||
/// <inheritdoc cref="DoubleFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double DoubleFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
return new DoubleConverter { ULongValue = ULongFromBits(array, startBit) }.DoubleValue;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct FloatConverter
|
||||
{
|
||||
[FieldOffset(0)] public byte Byte0;
|
||||
[FieldOffset(1)] public byte Byte1;
|
||||
[FieldOffset(2)] public byte Byte2;
|
||||
[FieldOffset(3)] public byte Byte3;
|
||||
|
||||
[FieldOffset(0)] public float FloatValue;
|
||||
|
||||
[FieldOffset(0)] public uint UIntValue;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct DoubleConverter
|
||||
{
|
||||
[FieldOffset(0)] public byte Byte0;
|
||||
[FieldOffset(1)] public byte Byte1;
|
||||
[FieldOffset(2)] public byte Byte2;
|
||||
[FieldOffset(3)] public byte Byte3;
|
||||
[FieldOffset(4)] public byte Byte4;
|
||||
[FieldOffset(5)] public byte Byte5;
|
||||
[FieldOffset(6)] public byte Byte6;
|
||||
[FieldOffset(7)] public byte Byte7;
|
||||
|
||||
[FieldOffset(0)] public double DoubleValue;
|
||||
|
||||
[FieldOffset(0)] public ulong ULongValue;
|
||||
}
|
||||
}
|
||||
61
Riptide/Utils/DelayedEvents.cs
Normal file
61
Riptide/Utils/DelayedEvents.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Executes an action when invoked.</summary>
|
||||
internal abstract class DelayedEvent
|
||||
{
|
||||
/// <summary>Executes the action.</summary>
|
||||
public abstract void Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Resends a <see cref="PendingMessage"/> when invoked.</summary>
|
||||
internal class ResendEvent : DelayedEvent
|
||||
{
|
||||
/// <summary>The message to resend.</summary>
|
||||
private readonly PendingMessage message;
|
||||
/// <summary>The time at which the resend event was queued.</summary>
|
||||
private readonly long initiatedAtTime;
|
||||
|
||||
/// <summary>Initializes the event.</summary>
|
||||
/// <param name="message">The message to resend.</param>
|
||||
/// <param name="initiatedAtTime">The time at which the resend event was queued.</param>
|
||||
public ResendEvent(PendingMessage message, long initiatedAtTime)
|
||||
{
|
||||
this.message = message;
|
||||
this.initiatedAtTime = initiatedAtTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke()
|
||||
{
|
||||
if (initiatedAtTime == message.LastSendTime) // If this isn't the case then the message has been resent already
|
||||
message.RetrySend();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Executes a heartbeat when invoked.</summary>
|
||||
internal class HeartbeatEvent : DelayedEvent
|
||||
{
|
||||
/// <summary>The peer whose heart to beat.</summary>
|
||||
private readonly Peer peer;
|
||||
|
||||
/// <summary>Initializes the event.</summary>
|
||||
/// <param name="peer">The peer whose heart to beat.</param>
|
||||
public HeartbeatEvent(Peer peer)
|
||||
{
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke()
|
||||
{
|
||||
peer.Heartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Riptide/Utils/Extensions.cs
Normal file
23
Riptide/Utils/Extensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Contains extension methods for various classes.</summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>Takes the <see cref="IPEndPoint"/>'s IP address and port number and converts it to a string, accounting for whether the address is an IPv4 or IPv6 address.</summary>
|
||||
/// <returns>A string containing the IP address and port number of the endpoint.</returns>
|
||||
public static string ToStringBasedOnIPFormat(this IPEndPoint endPoint)
|
||||
{
|
||||
if (endPoint.Address.IsIPv4MappedToIPv6)
|
||||
return $"{endPoint.Address.MapToIPv4()}:{endPoint.Port}";
|
||||
|
||||
return endPoint.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Riptide/Utils/Helper.cs
Normal file
113
Riptide/Utils/Helper.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Contains miscellaneous helper methods.</summary>
|
||||
internal class Helper
|
||||
{
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.NeverConnected"/>.</summary>
|
||||
private const string DCNeverConnected = "Never connected";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.TransportError"/>.</summary>
|
||||
private const string DCTransportError = "Transport error";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.TimedOut"/>.</summary>
|
||||
private const string DCTimedOut = "Timed out";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.Kicked"/>.</summary>
|
||||
private const string DCKicked = "Kicked";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.ServerStopped"/>.</summary>
|
||||
private const string DCServerStopped = "Server stopped";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.Disconnected"/>.</summary>
|
||||
private const string DCDisconnected = "Disconnected";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.PoorConnection"/>.</summary>
|
||||
private const string DCPoorConnection = "Poor connection";
|
||||
/// <summary>The text to log when disconnected or rejected due to an unknown reason.</summary>
|
||||
private const string UnknownReason = "Unknown reason";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.NoConnection"/>.</summary>
|
||||
private const string CRNoConnection = "No connection";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.AlreadyConnected"/>.</summary>
|
||||
private const string CRAlreadyConnected = "This client is already connected";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.ServerFull"/>.</summary>
|
||||
private const string CRServerFull = "Server is full";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.Rejected"/>.</summary>
|
||||
private const string CRRejected = "Rejected";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.Custom"/>.</summary>
|
||||
private const string CRCustom = "Rejected (with custom data)";
|
||||
|
||||
/// <summary>Determines whether <paramref name="singular"/> or <paramref name="plural"/> form should be used based on the <paramref name="amount"/>.</summary>
|
||||
/// <param name="amount">The amount that <paramref name="singular"/> and <paramref name="plural"/> refer to.</param>
|
||||
/// <param name="singular">The singular form.</param>
|
||||
/// <param name="plural">The plural form.</param>
|
||||
/// <returns><paramref name="singular"/> if <paramref name="amount"/> is 1; otherwise <paramref name="plural"/>.</returns>
|
||||
internal static string CorrectForm(int amount, string singular, string plural = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(plural))
|
||||
plural = $"{singular}s";
|
||||
|
||||
return amount == 1 ? singular : plural;
|
||||
}
|
||||
|
||||
/// <summary>Calculates the signed gap between sequence IDs, accounting for wrapping.</summary>
|
||||
/// <param name="seqId1">The new sequence ID.</param>
|
||||
/// <param name="seqId2">The previous sequence ID.</param>
|
||||
/// <returns>The signed gap between the two given sequence IDs. A positive gap means <paramref name="seqId1"/> is newer than <paramref name="seqId2"/>. A negative gap means <paramref name="seqId1"/> is older than <paramref name="seqId2"/>.</returns>
|
||||
internal static int GetSequenceGap(ushort seqId1, ushort seqId2)
|
||||
{
|
||||
int gap = seqId1 - seqId2;
|
||||
if (Math.Abs(gap) <= 32768) // Difference is small, meaning sequence IDs are close together
|
||||
return gap;
|
||||
else // Difference is big, meaning sequence IDs are far apart
|
||||
return (seqId1 <= 32768 ? ushort.MaxValue + 1 + seqId1 : seqId1) - (seqId2 <= 32768 ? ushort.MaxValue + 1 + seqId2 : seqId2);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the appropriate reason string for the given <see cref="DisconnectReason"/>.</summary>
|
||||
/// <param name="forReason">The <see cref="DisconnectReason"/> to retrieve the string for.</param>
|
||||
/// <returns>The appropriate reason string.</returns>
|
||||
internal static string GetReasonString(DisconnectReason forReason)
|
||||
{
|
||||
switch (forReason)
|
||||
{
|
||||
case DisconnectReason.NeverConnected:
|
||||
return DCNeverConnected;
|
||||
case DisconnectReason.TransportError:
|
||||
return DCTransportError;
|
||||
case DisconnectReason.TimedOut:
|
||||
return DCTimedOut;
|
||||
case DisconnectReason.Kicked:
|
||||
return DCKicked;
|
||||
case DisconnectReason.ServerStopped:
|
||||
return DCServerStopped;
|
||||
case DisconnectReason.Disconnected:
|
||||
return DCDisconnected;
|
||||
case DisconnectReason.PoorConnection:
|
||||
return DCPoorConnection;
|
||||
default:
|
||||
return $"{UnknownReason} '{forReason}'";
|
||||
}
|
||||
}
|
||||
/// <summary>Retrieves the appropriate reason string for the given <see cref="RejectReason"/>.</summary>
|
||||
/// <param name="forReason">The <see cref="RejectReason"/> to retrieve the string for.</param>
|
||||
/// <returns>The appropriate reason string.</returns>
|
||||
internal static string GetReasonString(RejectReason forReason)
|
||||
{
|
||||
switch (forReason)
|
||||
{
|
||||
case RejectReason.NoConnection:
|
||||
return CRNoConnection;
|
||||
case RejectReason.AlreadyConnected:
|
||||
return CRAlreadyConnected;
|
||||
case RejectReason.ServerFull:
|
||||
return CRServerFull;
|
||||
case RejectReason.Rejected:
|
||||
return CRRejected;
|
||||
case RejectReason.Custom:
|
||||
return CRCustom;
|
||||
default:
|
||||
return $"{UnknownReason} '{forReason}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Riptide/Utils/PriorityQueue.cs
Normal file
158
Riptide/Utils/PriorityQueue.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
// PriorityQueue unfortunately doesn't exist in .NET Standard 2.1
|
||||
/// <summary>Represents a collection of items that have a value and a priority. On dequeue, the item with the lowest priority value is removed.</summary>
|
||||
/// <typeparam name="TElement">Specifies the type of elements in the queue.</typeparam>
|
||||
/// <typeparam name="TPriority">Specifies the type of priority associated with enqueued elements.</typeparam>
|
||||
public class PriorityQueue<TElement, TPriority>
|
||||
{
|
||||
/// <summary>Gets the number of elements contained in the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
private const int DefaultCapacity = 8;
|
||||
private Entry<TElement, TPriority>[] heap;
|
||||
private readonly IComparer<TPriority> comparer;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PriorityQueue{TElement, TPriority}"/> class.</summary>
|
||||
/// <param name="capacity">Initial capacity to allocate for the underlying heap array.</param>
|
||||
public PriorityQueue(int capacity = DefaultCapacity)
|
||||
{
|
||||
heap = new Entry<TElement, TPriority>[capacity];
|
||||
comparer = Comparer<TPriority>.Default;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PriorityQueue{TElement, TPriority}"/> class with the specified custom priority comparer.</summary>
|
||||
/// <param name="comparer">Custom comparer dictating the ordering of elements.</param>
|
||||
/// <param name="capacity">Initial capacity to allocate for the underlying heap array.</param>
|
||||
public PriorityQueue(IComparer<TPriority> comparer, int capacity = DefaultCapacity)
|
||||
{
|
||||
heap = new Entry<TElement, TPriority>[capacity];
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified element and associated priority to the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
/// <param name="element">The element to add.</param>
|
||||
/// <param name="priority">The priority with which to associate the new element.</param>
|
||||
public void Enqueue(TElement element, TPriority priority)
|
||||
{
|
||||
if (Count == heap.Length)
|
||||
{
|
||||
// Resizing is necessary
|
||||
Entry<TElement, TPriority>[] temp = new Entry<TElement, TPriority>[Count * 2];
|
||||
Array.Copy(heap, temp, heap.Length);
|
||||
heap = temp;
|
||||
}
|
||||
|
||||
int index = Count;
|
||||
while (index > 0)
|
||||
{
|
||||
int parentIndex = GetParentIndex(index);
|
||||
if (comparer.Compare(priority, heap[parentIndex].Priority) < 0)
|
||||
{
|
||||
heap[index] = heap[parentIndex];
|
||||
index = parentIndex;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
heap[index] = new Entry<TElement, TPriority>(element, priority);
|
||||
Count++;
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the lowest priority element.</summary>
|
||||
public TElement Dequeue()
|
||||
{
|
||||
TElement returnValue = heap[0].Element;
|
||||
|
||||
if (Count > 1)
|
||||
{
|
||||
int parent = 0;
|
||||
int leftChild = GetLeftChildIndex(parent);
|
||||
|
||||
while (leftChild < Count)
|
||||
{
|
||||
int rightChild = leftChild + 1;
|
||||
int bestChild = (rightChild < Count && comparer.Compare(heap[rightChild].Priority, heap[leftChild].Priority) < 0) ? rightChild : leftChild;
|
||||
|
||||
heap[parent] = heap[bestChild];
|
||||
parent = bestChild;
|
||||
leftChild = GetLeftChildIndex(parent);
|
||||
}
|
||||
|
||||
heap[parent] = heap[Count - 1];
|
||||
}
|
||||
|
||||
Count--;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Removes the lowest priority element from the <see cref="PriorityQueue{TElement, TPriority}"/> and copies it and its associated priority to the <paramref name="element"/> and <paramref name="priority"/> arguments.</summary>
|
||||
/// <param name="element">When this method returns, contains the removed element.</param>
|
||||
/// <param name="priority">When this method returns, contains the priority associated with the removed element.</param>
|
||||
/// <returns>true if the element is successfully removed; false if the <see cref="PriorityQueue{TElement, TPriority}"/> is empty.</returns>
|
||||
public bool TryDequeue(out TElement element, out TPriority priority)
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
priority = heap[0].Priority;
|
||||
element = Dequeue();
|
||||
return true;
|
||||
}
|
||||
{
|
||||
element = default(TElement);
|
||||
priority = default(TPriority);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the lowest priority element.</summary>
|
||||
public TElement Peek()
|
||||
{
|
||||
return heap[0].Element;
|
||||
}
|
||||
|
||||
/// <summary>Returns the priority of the lowest priority element.</summary>
|
||||
public TPriority PeekPriority()
|
||||
{
|
||||
return heap[0].Priority;
|
||||
}
|
||||
|
||||
/// <summary>Removes all elements from the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(heap, 0, heap.Length);
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
private static int GetParentIndex(int index)
|
||||
{
|
||||
return (index - 1) / 2;
|
||||
}
|
||||
|
||||
private static int GetLeftChildIndex(int index)
|
||||
{
|
||||
return (index * 2) + 1;
|
||||
}
|
||||
|
||||
private struct Entry<TEle, TPrio>
|
||||
{
|
||||
internal readonly TEle Element;
|
||||
internal readonly TPrio Priority;
|
||||
|
||||
public Entry(TEle element, TPrio priority)
|
||||
{
|
||||
Element = element;
|
||||
Priority = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Riptide/Utils/RiptideLogger.cs
Normal file
126
Riptide/Utils/RiptideLogger.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Defines log message types.</summary>
|
||||
public enum LogType
|
||||
{
|
||||
/// <summary>Logs that are used for investigation during development.</summary>
|
||||
Debug,
|
||||
/// <summary>Logs that provide general information about application flow.</summary>
|
||||
Info,
|
||||
/// <summary>Logs that highlight abnormal or unexpected events in the application flow.</summary>
|
||||
Warning,
|
||||
/// <summary>Logs that highlight problematic events in the application flow which will cause unexpected behavior if not planned for.</summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>Provides functionality for logging messages.</summary>
|
||||
public class RiptideLogger
|
||||
{
|
||||
/// <summary>Whether or not <see cref="LogType.Debug"/> messages will be logged.</summary>
|
||||
public static bool IsDebugLoggingEnabled => logMethods.ContainsKey(LogType.Debug);
|
||||
/// <summary>Whether or not <see cref="LogType.Info"/> messages will be logged.</summary>
|
||||
public static bool IsInfoLoggingEnabled => logMethods.ContainsKey(LogType.Info);
|
||||
/// <summary>Whether or not <see cref="LogType.Warning"/> messages will be logged.</summary>
|
||||
public static bool IsWarningLoggingEnabled => logMethods.ContainsKey(LogType.Warning);
|
||||
/// <summary>Whether or not <see cref="LogType.Error"/> messages will be logged.</summary>
|
||||
public static bool IsErrorLoggingEnabled => logMethods.ContainsKey(LogType.Error);
|
||||
/// <summary>Encapsulates a method used to log messages.</summary>
|
||||
/// <param name="log">The message to log.</param>
|
||||
public delegate void LogMethod(string log);
|
||||
|
||||
/// <summary>Log methods, accessible by their <see cref="LogType"/></summary>
|
||||
private static readonly Dictionary<LogType, LogMethod> logMethods = new Dictionary<LogType, LogMethod>(4);
|
||||
/// <summary>Whether or not to include timestamps when logging messages.</summary>
|
||||
private static bool includeTimestamps;
|
||||
/// <summary>The format to use for timestamps.</summary>
|
||||
private static string timestampFormat;
|
||||
|
||||
/// <summary>Initializes <see cref="RiptideLogger"/> with all log types enabled.</summary>
|
||||
/// <param name="logMethod">The method to use when logging all types of messages.</param>
|
||||
/// <param name="includeTimestamps">Whether or not to include timestamps when logging messages.</param>
|
||||
/// <param name="timestampFormat">The format to use for timestamps.</param>
|
||||
public static void Initialize(LogMethod logMethod, bool includeTimestamps, string timestampFormat = "HH:mm:ss") => Initialize(logMethod, logMethod, logMethod, logMethod, includeTimestamps, timestampFormat);
|
||||
/// <summary>Initializes <see cref="RiptideLogger"/> with the supplied log methods.</summary>
|
||||
/// <param name="debugMethod">The method to use when logging debug messages. Set to <see langword="null"/> to disable debug logs.</param>
|
||||
/// <param name="infoMethod">The method to use when logging info messages. Set to <see langword="null"/> to disable info logs.</param>
|
||||
/// <param name="warningMethod">The method to use when logging warning messages. Set to <see langword="null"/> to disable warning logs.</param>
|
||||
/// <param name="errorMethod">The method to use when logging error messages. Set to <see langword="null"/> to disable error logs.</param>
|
||||
/// <param name="includeTimestamps">Whether or not to include timestamps when logging messages.</param>
|
||||
/// <param name="timestampFormat">The format to use for timestamps.</param>
|
||||
public static void Initialize(LogMethod debugMethod, LogMethod infoMethod, LogMethod warningMethod, LogMethod errorMethod, bool includeTimestamps, string timestampFormat = "HH:mm:ss")
|
||||
{
|
||||
logMethods.Clear();
|
||||
|
||||
if (debugMethod != null)
|
||||
logMethods.Add(LogType.Debug, debugMethod);
|
||||
if (infoMethod != null)
|
||||
logMethods.Add(LogType.Info, infoMethod);
|
||||
if (warningMethod != null)
|
||||
logMethods.Add(LogType.Warning, warningMethod);
|
||||
if (errorMethod != null)
|
||||
logMethods.Add(LogType.Error, errorMethod);
|
||||
|
||||
RiptideLogger.includeTimestamps = includeTimestamps;
|
||||
RiptideLogger.timestampFormat = timestampFormat;
|
||||
}
|
||||
|
||||
/// <summary>Enables logging for messages of the given <see cref="LogType"/>.</summary>
|
||||
/// <param name="logType">The type of message to enable logging for.</param>
|
||||
/// <param name="logMethod">The method to use when logging this type of message.</param>
|
||||
public static void EnableLoggingFor(LogType logType, LogMethod logMethod)
|
||||
{
|
||||
if (logMethods.ContainsKey(logType))
|
||||
logMethods[logType] = logMethod;
|
||||
else
|
||||
logMethods.Add(logType, logMethod);
|
||||
}
|
||||
|
||||
/// <summary>Disables logging for messages of the given <see cref="LogType"/>.</summary>
|
||||
/// <param name="logType">The type of message to enable logging for.</param>
|
||||
public static void DisableLoggingFor(LogType logType) => logMethods.Remove(logType);
|
||||
|
||||
/// <summary>Logs a message.</summary>
|
||||
/// <param name="logType">The type of log message that is being logged.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void Log(LogType logType, string message)
|
||||
{
|
||||
if (logMethods.TryGetValue(logType, out LogMethod logMethod))
|
||||
{
|
||||
if (includeTimestamps)
|
||||
logMethod($"[{GetTimestamp(DateTime.Now)}]: {message}");
|
||||
else
|
||||
logMethod(message);
|
||||
}
|
||||
}
|
||||
/// <summary>Logs a message.</summary>
|
||||
/// <param name="logType">The type of log message that is being logged.</param>
|
||||
/// <param name="logName">Who is logging this message.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void Log(LogType logType, string logName, string message)
|
||||
{
|
||||
if (logMethods.TryGetValue(logType, out LogMethod logMethod))
|
||||
{
|
||||
if (includeTimestamps)
|
||||
logMethod($"[{GetTimestamp(DateTime.Now)}] ({logName}): {message}");
|
||||
else
|
||||
logMethod($"({logName}): {message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts a <see cref="DateTime"/> object to a formatted timestamp string.</summary>
|
||||
/// <param name="time">The time to format.</param>
|
||||
/// <returns>The formatted timestamp.</returns>
|
||||
private static string GetTimestamp(DateTime time)
|
||||
{
|
||||
return time.ToString(timestampFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Riptide/Utils/RollingStat.cs
Normal file
93
Riptide/Utils/RollingStat.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Represents a rolling series of numbers.</summary>
|
||||
public class RollingStat
|
||||
{
|
||||
/// <summary>The position in the array of the latest item.</summary>
|
||||
private int index;
|
||||
/// <summary>How many of the array's slots are in use.</summary>
|
||||
private int slotsFilled;
|
||||
/// <inheritdoc cref="Mean"/>
|
||||
private double mean;
|
||||
/// <summary>The sum of the mean subtracted from each value in the array.</summary>
|
||||
private double sumOfSquares;
|
||||
/// <summary>The array used to store the values.</summary>
|
||||
private readonly double[] array;
|
||||
|
||||
/// <summary>The mean of the stat's values.</summary>
|
||||
public double Mean => mean;
|
||||
/// <summary>The variance of the stat's values.</summary>
|
||||
public double Variance => slotsFilled > 1 ? sumOfSquares / (slotsFilled - 1) : 0;
|
||||
/// <summary>The standard deviation of the stat's values.</summary>
|
||||
public double StandardDev
|
||||
{
|
||||
get
|
||||
{
|
||||
double variance = Variance;
|
||||
if (variance >= double.Epsilon)
|
||||
{
|
||||
double root = Math.Sqrt(variance);
|
||||
return double.IsNaN(root) ? 0 : root;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes the stat.</summary>
|
||||
/// <param name="sampleSize">The number of values to store.</param>
|
||||
public RollingStat(int sampleSize)
|
||||
{
|
||||
index = 0;
|
||||
slotsFilled = 0;
|
||||
mean = 0;
|
||||
sumOfSquares = 0;
|
||||
array = new double[sampleSize];
|
||||
}
|
||||
|
||||
/// <summary>Adds a new value to the stat.</summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public void Add(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
return;
|
||||
|
||||
index %= array.Length;
|
||||
double oldMean = mean;
|
||||
double oldValue = array[index];
|
||||
array[index] = value;
|
||||
index++;
|
||||
|
||||
if (slotsFilled == array.Length)
|
||||
{
|
||||
double delta = value - oldValue;
|
||||
mean += delta / slotsFilled;
|
||||
sumOfSquares += delta * (value - mean + (oldValue - oldMean));
|
||||
}
|
||||
else
|
||||
{
|
||||
slotsFilled++;
|
||||
double delta = value - oldMean;
|
||||
mean += delta / slotsFilled;
|
||||
sumOfSquares += delta * (value - mean);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
if (slotsFilled == array.Length)
|
||||
return string.Join(",", array);
|
||||
|
||||
return string.Join(",", array.Take(slotsFilled));
|
||||
}
|
||||
}
|
||||
}
|
||||
184
RiptideSteamTransport/KCMSteamManager.cs
Normal file
184
RiptideSteamTransport/KCMSteamManager.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
// The SteamManager is designed to work with Steamworks.NET
|
||||
// This file is released into the public domain.
|
||||
// Where that dedication is not recognized you are granted a perpetual,
|
||||
// irrevocable license to copy and modify this file as you see fit.
|
||||
//
|
||||
// Version: 1.0.12
|
||||
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
|
||||
#endif
|
||||
|
||||
using UnityEngine;
|
||||
#if !DISABLESTEAMWORKS
|
||||
using System.Collections;
|
||||
using Steamworks;
|
||||
using KCM;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
#endif
|
||||
|
||||
//
|
||||
// The SteamManager provides a base implementation of Steamworks.NET on which you can build upon.
|
||||
// It handles the basics of starting up and shutting down the SteamAPI for use.
|
||||
//
|
||||
[DisallowMultipleComponent]
|
||||
public class KCMSteamManager : MonoBehaviour {
|
||||
#if !DISABLESTEAMWORKS
|
||||
protected static bool s_EverInitialized = false;
|
||||
|
||||
protected static KCMSteamManager s_instance;
|
||||
public static KCMSteamManager Instance {
|
||||
get {
|
||||
if (s_instance == null) {
|
||||
return new GameObject("KCMSteamManager").AddComponent<KCMSteamManager>();
|
||||
}
|
||||
else {
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool m_bInitialized = false;
|
||||
public static bool Initialized {
|
||||
get {
|
||||
return Instance.m_bInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
protected SteamAPIWarningMessageHook_t m_SteamAPIWarningMessageHook;
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(SteamAPIWarningMessageHook_t))]
|
||||
protected static void SteamAPIDebugTextHook(int nSeverity, System.Text.StringBuilder pchDebugText) {
|
||||
Main.helper.Log(pchDebugText.ToString());
|
||||
}
|
||||
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
// In case of disabled Domain Reload, reset static members before entering Play Mode.
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void InitOnPlayMode()
|
||||
{
|
||||
s_EverInitialized = false;
|
||||
s_instance = null;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected virtual void Awake() {
|
||||
// Only one instance of SteamManager at a time!
|
||||
Main.helper.Log("Steam awake");
|
||||
if (s_instance != null) {
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
s_instance = this;
|
||||
|
||||
if(s_EverInitialized) {
|
||||
// This is almost always an error.
|
||||
// The most common case where this happens is when SteamManager gets destroyed because of Application.Quit(),
|
||||
// and then some Steamworks code in some other OnDestroy gets called afterwards, creating a new SteamManager.
|
||||
// You should never call Steamworks functions in OnDestroy, always prefer OnDisable if possible.
|
||||
Main.helper.Log("Tried to Initialize the SteamAPI twice in one session!");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We want our SteamManager Instance to persist across scenes.
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (!Packsize.Test()) {
|
||||
Main.helper.Log("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.");
|
||||
}
|
||||
|
||||
if (!DllCheck.Test()) {
|
||||
Main.helper.Log("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.");
|
||||
}
|
||||
|
||||
try {
|
||||
// If Steam is not running or the game wasn't started through Steam, SteamAPI_RestartAppIfNecessary starts the
|
||||
// Steam client and also launches this game again if the User owns it. This can act as a rudimentary form of DRM.
|
||||
|
||||
// Once you get a Steam AppID assigned by Valve, you need to replace AppId_t.Invalid with it and
|
||||
// remove steam_appid.txt from the game depot. eg: "(AppId_t)480" or "new AppId_t(480)".
|
||||
// See the Valve documentation for more information: https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
|
||||
if (SteamAPI.RestartAppIfNecessary((AppId_t)569480)) {
|
||||
//Application.Quit();
|
||||
Main.helper.Log("Attempted to restart app");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (System.DllNotFoundException e) { // We catch this exception here, as it will be the first occurrence of it.
|
||||
Main.helper.Log("[Steamworks.NET] Could not load [lib]steam_api.dll/so/dylib. It's likely not in the correct location. Refer to the README for more details.\n" + e);
|
||||
|
||||
//Application.Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initializes the Steamworks API.
|
||||
// If this returns false then this indicates one of the following conditions:
|
||||
// [*] The Steam client isn't running. A running Steam client is required to provide implementations of the various Steamworks interfaces.
|
||||
// [*] The Steam client couldn't determine the App ID of game. If you're running your application from the executable or debugger directly then you must have a [code-inline]steam_appid.txt[/code-inline] in your game directory next to the executable, with your app ID in it and nothing else. Steam will look for this file in the current working directory. If you are running your executable from a different directory you may need to relocate the [code-inline]steam_appid.txt[/code-inline] file.
|
||||
// [*] Your application is not running under the same OS user context as the Steam client, such as a different user or administration access level.
|
||||
// [*] Ensure that you own a license for the App ID on the currently active Steam account. Your game must show up in your Steam library.
|
||||
// [*] Your App ID is not completely set up, i.e. in Release State: Unavailable, or it's missing default packages.
|
||||
// Valve's documentation for this is located here:
|
||||
// https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
|
||||
m_bInitialized = SteamAPI.Init();
|
||||
if (!m_bInitialized) {
|
||||
Main.helper.Log("[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
s_EverInitialized = true;
|
||||
}
|
||||
|
||||
// This should only ever get called on first load and after an Assembly reload, You should never Disable the Steamworks Manager yourself.
|
||||
protected virtual void OnEnable() {
|
||||
if (s_instance == null) {
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
if (!m_bInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_SteamAPIWarningMessageHook == null) {
|
||||
// Set up our callback to receive warning messages from Steam.
|
||||
// You must launch with "-debug_steamapi" in the launch args to receive warnings.
|
||||
m_SteamAPIWarningMessageHook = new SteamAPIWarningMessageHook_t(SteamAPIDebugTextHook);
|
||||
SteamClient.SetWarningMessageHook(m_SteamAPIWarningMessageHook);
|
||||
}
|
||||
}
|
||||
|
||||
// OnApplicationQuit gets called too early to shutdown the SteamAPI.
|
||||
// Because the SteamManager should be persistent and never disabled or destroyed we can shutdown the SteamAPI here.
|
||||
// Thus it is not recommended to perform any Steamworks work in other OnDestroy functions as the order of execution can not be garenteed upon Shutdown. Prefer OnDisable().
|
||||
protected virtual void OnDestroy() {
|
||||
if (s_instance != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_instance = null;
|
||||
|
||||
if (!m_bInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
SteamAPI.Shutdown();
|
||||
}
|
||||
|
||||
protected virtual void Update() {
|
||||
if (!m_bInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run Steam client callbacks
|
||||
SteamAPI.RunCallbacks();
|
||||
}
|
||||
#else
|
||||
public static bool Initialized {
|
||||
get {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif //!DISABLESTEAMWORKS
|
||||
}
|
||||
175
RiptideSteamTransport/LobbyManager.cs
Normal file
175
RiptideSteamTransport/LobbyManager.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using KCM;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Handlers;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Demos.Steam.PlayerHosted
|
||||
{
|
||||
public class LobbyManager : MonoBehaviour
|
||||
{
|
||||
private static LobbyManager _singleton;
|
||||
internal static LobbyManager Singleton
|
||||
{
|
||||
get => _singleton;
|
||||
private set
|
||||
{
|
||||
if (_singleton == null)
|
||||
_singleton = value;
|
||||
else if (_singleton != value)
|
||||
{
|
||||
Debug.Log($"{nameof(LobbyManager)} instance already exists, destroying object!");
|
||||
Destroy(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Callback<LobbyCreated_t> lobbyCreated;
|
||||
protected Callback<GameLobbyJoinRequested_t> gameLobbyJoinRequested;
|
||||
protected Callback<LobbyEnter_t> lobbyEnter;
|
||||
|
||||
private const string HostAddressKey = "HostAddress";
|
||||
private CSteamID lobbyId;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Singleton = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
|
||||
if (!KCMSteamManager.Initialized)
|
||||
{
|
||||
Main.helper.Log("Steam is not initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
lobbyCreated = Callback<LobbyCreated_t>.Create(OnLobbyCreated);
|
||||
gameLobbyJoinRequested = Callback<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
|
||||
lobbyEnter = Callback<LobbyEnter_t>.Create(OnLobbyEnter);
|
||||
|
||||
}
|
||||
|
||||
public static bool loadingSave = false;
|
||||
|
||||
internal void CreateLobby(bool loadingSave = false)
|
||||
{
|
||||
var result = SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, 25);
|
||||
|
||||
LobbyManager.loadingSave = loadingSave;
|
||||
|
||||
}
|
||||
|
||||
private void OnLobbyCreated(LobbyCreated_t callback)
|
||||
{
|
||||
|
||||
if (callback.m_eResult != EResult.k_EResultOK)
|
||||
{
|
||||
//UIManager.Singleton.LobbyCreationFailed();
|
||||
Main.helper.Log("Create lobby failed");
|
||||
return;
|
||||
}
|
||||
|
||||
lobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||
//UIManager.Singleton.LobbyCreationSucceeded(callback.m_ulSteamIDLobby);
|
||||
|
||||
//NetworkManager.Singleton.Server.Start(0, 5, NetworkManager.PlayerHostedDemoMessageHandlerGroupId);
|
||||
|
||||
|
||||
KCServer.StartServer();
|
||||
|
||||
Main.TransitionTo(MenuState.ServerLobby);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
Main.helper.Log("About to call connect");
|
||||
KCClient.Connect("127.0.0.1");
|
||||
|
||||
World.inst.Generate();
|
||||
ServerLobbyScript.WorldSeed.text = World.inst.GetTextSeed();
|
||||
|
||||
LobbyHandler.ClearPlayerList();
|
||||
|
||||
/*Cam.inst.desiredDist = 80f;
|
||||
Cam.inst.desiredPhi = 45f;
|
||||
CloudSystem.inst.threshold1 = 0.6f;
|
||||
CloudSystem.inst.threshold2 = 0.8f;
|
||||
CloudSystem.inst.BaseFreq = 4.5f;
|
||||
Weather.inst.SetSeason(Weather.Season.Summer);
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.NameAndBanner);*/
|
||||
|
||||
ServerBrowser.registerServer = true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
//NetworkManager.Singleton.Client.Connect("127.0.0.1", messageHandlerGroupId: NetworkManager.PlayerHostedDemoMessageHandlerGroupId);
|
||||
}
|
||||
|
||||
internal void JoinLobby(ulong lobbyId)
|
||||
{
|
||||
SteamMatchmaking.JoinLobby(new CSteamID(lobbyId));
|
||||
}
|
||||
|
||||
private void OnGameLobbyJoinRequested(GameLobbyJoinRequested_t callback)
|
||||
{
|
||||
SteamMatchmaking.JoinLobby(callback.m_steamIDLobby);
|
||||
}
|
||||
|
||||
private void OnLobbyEnter(LobbyEnter_t callback)
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
lobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||
CSteamID hostId = SteamMatchmaking.GetLobbyOwner(lobbyId);
|
||||
|
||||
KCClient.Connect(hostId.ToString());
|
||||
//UIManager.Singleton.LobbyEntered();
|
||||
}
|
||||
|
||||
public void LeaveLobby()
|
||||
{
|
||||
//NetworkManager.Singleton.StopServer();
|
||||
//NetworkManager.Singleton.DisconnectClient();
|
||||
SteamMatchmaking.LeaveLobby(lobbyId);
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
KCClient.client.Disconnect();
|
||||
|
||||
Main.helper.Log("clear players");
|
||||
Main.kCPlayers.Clear();
|
||||
LobbyHandler.ClearPlayerList();
|
||||
LobbyHandler.ClearChatEntries();
|
||||
Main.helper.Log("end clear players");
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.ServerBrowser);
|
||||
ServerBrowser.registerServer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
204
RiptideSteamTransport/Transport/SteamClient.cs
Normal file
204
RiptideSteamTransport/Transport/SteamClient.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
// This file is provided under The MIT License as part of RiptideSteamTransport.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/tom-weiland/RiptideSteamTransport/blob/main/LICENSE.md
|
||||
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public class SteamClient : SteamPeer, IClient
|
||||
{
|
||||
public event EventHandler Connected;
|
||||
public event EventHandler ConnectionFailed;
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
private const string LocalHostName = "localhost";
|
||||
private const string LocalHostIP = "127.0.0.1";
|
||||
|
||||
private SteamConnection steamConnection;
|
||||
private SteamServer localServer;
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> connectionStatusChanged;
|
||||
|
||||
public SteamClient(SteamServer localServer = null)
|
||||
{
|
||||
this.localServer = localServer;
|
||||
}
|
||||
|
||||
public void ChangeLocalServer(SteamServer newLocalServer)
|
||||
{
|
||||
localServer = newLocalServer;
|
||||
}
|
||||
|
||||
public bool Connect(string hostAddress, out Connection connection, out string connectError)
|
||||
{
|
||||
connection = null;
|
||||
|
||||
try
|
||||
{
|
||||
//SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
connectError = $"Couldn't connect: {ex}";
|
||||
return false;
|
||||
}
|
||||
|
||||
connectError = $"Invalid host address '{hostAddress}'! Expected '{LocalHostIP}' or '{LocalHostName}' for local connections, or a valid Steam ID.";
|
||||
if (hostAddress == LocalHostIP || hostAddress == LocalHostName)
|
||||
{
|
||||
if (localServer == null)
|
||||
{
|
||||
connectError = $"No locally running server was specified to connect to! Either pass a {nameof(SteamServer)} instance to your {nameof(SteamClient)}'s constructor or call its {nameof(SteamClient.ChangeLocalServer)} method before attempting to connect locally.";
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
connection = steamConnection = ConnectLocal();
|
||||
return true;
|
||||
}
|
||||
else if (ulong.TryParse(hostAddress, out ulong hostId))
|
||||
{
|
||||
connection = steamConnection = TryConnect(new CSteamID(hostId));
|
||||
return connection != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private SteamConnection ConnectLocal()
|
||||
{
|
||||
Debug.Log($"{LogName}: Connecting to locally running server...");
|
||||
|
||||
connectionStatusChanged = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
CSteamID playerSteamId = SteamUser.GetSteamID();
|
||||
|
||||
SteamNetworkingIdentity clientIdentity = new SteamNetworkingIdentity();
|
||||
clientIdentity.SetSteamID(playerSteamId);
|
||||
SteamNetworkingIdentity serverIdentity = new SteamNetworkingIdentity();
|
||||
serverIdentity.SetSteamID(playerSteamId);
|
||||
|
||||
SteamNetworkingSockets.CreateSocketPair(out HSteamNetConnection connectionToClient, out HSteamNetConnection connectionToServer, false, ref clientIdentity, ref serverIdentity);
|
||||
|
||||
localServer.Add(new SteamConnection(playerSteamId, connectionToClient, this));
|
||||
OnConnected();
|
||||
return new SteamConnection(playerSteamId, connectionToServer, this);
|
||||
}
|
||||
|
||||
private SteamConnection TryConnect(CSteamID hostId)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionStatusChanged = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
|
||||
SteamNetworkingIdentity serverIdentity = new SteamNetworkingIdentity();
|
||||
serverIdentity.SetSteamID(hostId);
|
||||
|
||||
SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { };
|
||||
HSteamNetConnection connectionToServer = SteamNetworkingSockets.ConnectP2P(ref serverIdentity, 0, options.Length, options);
|
||||
|
||||
ConnectTimeout();
|
||||
return new SteamConnection(hostId, connectionToServer, this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
OnConnectionFailed();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ConnectTimeout() // TODO: confirm if this is needed, Riptide *should* take care of timing out the connection
|
||||
{
|
||||
Task timeOutTask = Task.Delay(6000); // TODO: use Riptide Client's TimeoutTime
|
||||
await Task.WhenAny(timeOutTask);
|
||||
|
||||
if (!steamConnection.IsConnected)
|
||||
OnConnectionFailed();
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t callback)
|
||||
{
|
||||
if (!callback.m_hConn.Equals(steamConnection.SteamNetConnection))
|
||||
{
|
||||
// When connecting via local loopback connection to a locally running SteamServer (aka
|
||||
// this player is also the host), other external clients that attempt to connect seem
|
||||
// to trigger ConnectionStatusChanged callbacks for the locally connected client. Not
|
||||
// 100% sure why this is the case, but returning out of the callback here when the
|
||||
// connection doesn't match that between local client & server avoids the problem.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (callback.m_info.m_eState)
|
||||
{
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
|
||||
OnConnected();
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Closed by peer", false);
|
||||
OnDisconnected(DisconnectReason.Disconnected);
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Problem detected", false);
|
||||
OnDisconnected(DisconnectReason.TransportError);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"{LogName}: Connection state changed - {callback.m_info.m_eState} | {callback.m_info.m_szEndDebug}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Poll()
|
||||
{
|
||||
if (steamConnection != null)
|
||||
Receive(steamConnection);
|
||||
}
|
||||
|
||||
// TODO: disable nagle so this isn't needed
|
||||
//public void Flush()
|
||||
//{
|
||||
// foreach (SteamConnection connection in connections.Values)
|
||||
// SteamNetworkingSockets.FlushMessagesOnConnection(connection.SteamNetConnection);
|
||||
//}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (connectionStatusChanged != null)
|
||||
{
|
||||
connectionStatusChanged.Dispose();
|
||||
connectionStatusChanged = null;
|
||||
}
|
||||
|
||||
SteamNetworkingSockets.CloseConnection(steamConnection.SteamNetConnection, 0, "Disconnected", false);
|
||||
steamConnection = null;
|
||||
}
|
||||
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnConnectionFailed()
|
||||
{
|
||||
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection)
|
||||
{
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, fromConnection));
|
||||
}
|
||||
|
||||
protected virtual void OnDisconnected(DisconnectReason reason)
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(steamConnection, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
72
RiptideSteamTransport/Transport/SteamConnection.cs
Normal file
72
RiptideSteamTransport/Transport/SteamConnection.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// This file is provided under The MIT License as part of RiptideSteamTransport.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/tom-weiland/RiptideSteamTransport/blob/main/LICENSE.md
|
||||
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public class SteamConnection : Connection, IEquatable<SteamConnection>
|
||||
{
|
||||
public readonly CSteamID SteamId;
|
||||
public readonly HSteamNetConnection SteamNetConnection;
|
||||
|
||||
internal bool DidReceiveConnect;
|
||||
|
||||
private readonly SteamPeer peer;
|
||||
|
||||
internal SteamConnection(CSteamID steamId, HSteamNetConnection steamNetConnection, SteamPeer peer)
|
||||
{
|
||||
SteamId = steamId;
|
||||
SteamNetConnection = steamNetConnection;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
peer.Send(dataBuffer, amount, SteamNetConnection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => SteamNetConnection.ToString();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as SteamConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(SteamConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return SteamNetConnection.Equals(other.SteamNetConnection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -721414014 + EqualityComparer<HSteamNetConnection>.Default.GetHashCode(SteamNetConnection);
|
||||
}
|
||||
|
||||
public static bool operator ==(SteamConnection left, SteamConnection right)
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SteamConnection left, SteamConnection right) => !(left == right);
|
||||
}
|
||||
}
|
||||
69
RiptideSteamTransport/Transport/SteamPeer.cs
Normal file
69
RiptideSteamTransport/Transport/SteamPeer.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// This file is provided under The MIT License as part of RiptideSteamTransport.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/tom-weiland/RiptideSteamTransport/blob/main/LICENSE.md
|
||||
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public abstract class SteamPeer
|
||||
{
|
||||
/// <summary>The name to use when logging messages via <see cref="Utils.RiptideLogger"/>.</summary>
|
||||
public const string LogName = "STEAM";
|
||||
|
||||
protected const int MaxMessages = 256;
|
||||
|
||||
private readonly byte[] receiveBuffer;
|
||||
|
||||
protected SteamPeer()
|
||||
{
|
||||
receiveBuffer = new byte[Message.MaxSize + sizeof(ushort)];
|
||||
}
|
||||
|
||||
protected void Receive(SteamConnection fromConnection)
|
||||
{
|
||||
IntPtr[] ptrs = new IntPtr[MaxMessages]; // TODO: remove allocation?
|
||||
|
||||
// TODO: consider using poll groups -> https://partner.steamgames.com/doc/api/ISteamNetworkingSockets#functions_poll_groups
|
||||
int messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(fromConnection.SteamNetConnection, ptrs, MaxMessages);
|
||||
if (messageCount > 0)
|
||||
{
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
SteamNetworkingMessage_t data = Marshal.PtrToStructure<SteamNetworkingMessage_t>(ptrs[i]);
|
||||
|
||||
if (data.m_cbSize > 0)
|
||||
{
|
||||
int byteCount = data.m_cbSize;
|
||||
if (data.m_cbSize > receiveBuffer.Length)
|
||||
{
|
||||
Debug.LogWarning($"{LogName}: Can't fully handle {data.m_cbSize} bytes because it exceeds the maximum of {receiveBuffer.Length}. Data will be incomplete!");
|
||||
byteCount = receiveBuffer.Length;
|
||||
}
|
||||
|
||||
Marshal.Copy(data.m_pData, receiveBuffer, 0, data.m_cbSize);
|
||||
OnDataReceived(receiveBuffer, byteCount, fromConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Send(byte[] dataBuffer, int numBytes, HSteamNetConnection toConnection)
|
||||
{
|
||||
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
|
||||
IntPtr pDataBuffer = handle.AddrOfPinnedObject();
|
||||
|
||||
EResult result = SteamNetworkingSockets.SendMessageToConnection(toConnection, pDataBuffer, (uint)numBytes, Constants.k_nSteamNetworkingSend_Unreliable, out long _);
|
||||
if (result != EResult.k_EResultOK)
|
||||
Debug.LogWarning($"{LogName}: Failed to send {numBytes} bytes - {result}");
|
||||
|
||||
handle.Free();
|
||||
}
|
||||
|
||||
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection);
|
||||
}
|
||||
}
|
||||
160
RiptideSteamTransport/Transport/SteamServer.cs
Normal file
160
RiptideSteamTransport/Transport/SteamServer.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// This file is provided under The MIT License as part of RiptideSteamTransport.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/tom-weiland/RiptideSteamTransport/blob/main/LICENSE.md
|
||||
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public class SteamServer : SteamPeer, IServer
|
||||
{
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
private Dictionary<CSteamID, SteamConnection> connections;
|
||||
private HSteamListenSocket listenSocket;
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> connectionStatusChanged;
|
||||
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<CSteamID, SteamConnection>();
|
||||
|
||||
connectionStatusChanged = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
|
||||
SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { };
|
||||
listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(port, options.Length, options);
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t callback)
|
||||
{
|
||||
CSteamID clientSteamId = callback.m_info.m_identityRemote.GetSteamID();
|
||||
switch (callback.m_info.m_eState)
|
||||
{
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting:
|
||||
Accept(callback.m_hConn);
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
|
||||
Add(new SteamConnection(clientSteamId, callback.m_hConn, this));
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Closed by peer", false);
|
||||
OnDisconnected(clientSteamId, DisconnectReason.Disconnected);
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Problem detected", false);
|
||||
OnDisconnected(clientSteamId, DisconnectReason.TransportError);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"{LogName}: {clientSteamId}'s connection state changed - {callback.m_info.m_eState}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Add(SteamConnection connection)
|
||||
{
|
||||
if (!connections.ContainsKey(connection.SteamId))
|
||||
{
|
||||
connections.Add(connection.SteamId, connection);
|
||||
OnConnected(connection);
|
||||
}
|
||||
else
|
||||
Debug.Log($"{LogName}: Connection from {connection.SteamId} could not be accepted: Already connected");
|
||||
}
|
||||
|
||||
private void Accept(HSteamNetConnection connection)
|
||||
{
|
||||
EResult result = SteamNetworkingSockets.AcceptConnection(connection);
|
||||
if (result != EResult.k_EResultOK)
|
||||
Debug.LogWarning($"{LogName}: Connection could not be accepted: {result}");
|
||||
}
|
||||
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is SteamConnection steamConnection)
|
||||
{
|
||||
SteamNetworkingSockets.CloseConnection(steamConnection.SteamNetConnection, 0, "Disconnected by server", false);
|
||||
connections.Remove(steamConnection.SteamId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Poll()
|
||||
{
|
||||
foreach (SteamConnection connection in connections.Values)
|
||||
Receive(connection);
|
||||
}
|
||||
|
||||
// TODO: disable nagle so this isn't needed
|
||||
//public void Flush()
|
||||
//{
|
||||
// foreach (SteamConnection connection in connections.Values)
|
||||
// SteamNetworkingSockets.FlushMessagesOnConnection(connection.SteamNetConnection);
|
||||
//}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
if (connectionStatusChanged != null)
|
||||
{
|
||||
connectionStatusChanged.Dispose();
|
||||
connectionStatusChanged = null;
|
||||
}
|
||||
|
||||
foreach (SteamConnection connection in connections.Values)
|
||||
SteamNetworkingSockets.CloseConnection(connection.SteamNetConnection, 0, "Server stopped", false);
|
||||
|
||||
connections.Clear();
|
||||
SteamNetworkingSockets.CloseListenSocket(listenSocket);
|
||||
}
|
||||
|
||||
protected internal virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection)
|
||||
{
|
||||
if ((MessageHeader)dataBuffer[0] == MessageHeader.Connect)
|
||||
{
|
||||
if (fromConnection.DidReceiveConnect)
|
||||
return;
|
||||
|
||||
fromConnection.DidReceiveConnect = true;
|
||||
}
|
||||
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, fromConnection));
|
||||
}
|
||||
|
||||
protected virtual void OnDisconnected(CSteamID steamId, DisconnectReason reason)
|
||||
{
|
||||
if (connections.TryGetValue(steamId, out SteamConnection connection))
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
connections.Remove(steamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
493
ServerBrowser/ServerBrowser.cs
Normal file
493
ServerBrowser/ServerBrowser.cs
Normal file
@@ -0,0 +1,493 @@
|
||||
using Harmony;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Handlers;
|
||||
using Newtonsoft.Json;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class ServerBrowser : MonoBehaviour
|
||||
{
|
||||
public static GameObject serverBrowserRef = null;
|
||||
public static Transform serverBrowserContentRef = null;
|
||||
|
||||
public static GameObject serverLobbyRef = null;
|
||||
public static Transform serverLobbyPlayerRef = null;
|
||||
public static Transform serverLobbyChatRef = null;
|
||||
|
||||
public static List<GameObject> ServerEntries = new List<GameObject>();
|
||||
public static ServerResponse ServerResponse = new ServerResponse();
|
||||
|
||||
private string databaseId = "6563181402855cfc8b87"; // Replace with your database ID
|
||||
private string collectionId = "servers"; // Replace with your collection ID
|
||||
private string projectId = "kcmmasterserver"; // Replace with your project ID
|
||||
string apiKey = "f80c8f7f5c07a4d4600a7d9954529a8a7897de58c08d9c2b24eaf638dd66e7007917840cfeea5d2673ad397336b9d68ca48375ca6e918c41ddfbdb84a96fa009e9976dacfbaa0a3a8effd79f862f1ea249822e17d26e111c5da48e20ceb0065421fc7fca7e630172a003cc89dd00c5a636b443bc7c8d85149384db9d6d5f6df6"; // Replace with your API key
|
||||
|
||||
private string serverID = string.Empty;
|
||||
|
||||
public static GameObject inst { get; private set; }
|
||||
public void Awake()
|
||||
{
|
||||
inst = serverBrowserRef;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
inst = serverBrowserRef;
|
||||
StartCoroutine(LobbyHeartbeat());
|
||||
}
|
||||
|
||||
public static bool registerServer = false;
|
||||
int interval = 0;
|
||||
|
||||
IEnumerator LobbyHeartbeat()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string url = $"https://base.ryanpalmer.tech/v1/databases/{databaseId}/collections/{collectionId}/documents";
|
||||
|
||||
#region "Get Servers (for browser)"
|
||||
if (serverBrowserRef != null)
|
||||
{
|
||||
WebRequest request = WebRequest.Create(url);
|
||||
request.Method = "GET";
|
||||
request.Headers["X-Appwrite-Project"] = projectId;
|
||||
request.Headers["X-Appwrite-Key"] = apiKey;
|
||||
|
||||
Task task = Task.Run(async () =>
|
||||
{
|
||||
using (WebResponse response = await request.GetResponseAsync())
|
||||
{
|
||||
using (Stream stream = response.GetResponseStream())
|
||||
{
|
||||
try
|
||||
{
|
||||
StreamReader reader = new StreamReader(stream);
|
||||
string responseText = reader.ReadToEnd();
|
||||
|
||||
ServerResponse serverResponse = JsonConvert.DeserializeObject<ServerResponse>(responseText);
|
||||
|
||||
ServerResponse = serverResponse;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
DestroyServerEntries();
|
||||
|
||||
foreach (ServerEntry serverEntry in ServerResponse.Documents)
|
||||
{
|
||||
GameObject entry = Instantiate(PrefabManager.serverEntryItemPrefab, serverBrowserContentRef);
|
||||
var s = entry.AddComponent<ServerEntryScript>();
|
||||
|
||||
s.Name = serverEntry.Name;
|
||||
s.Host = serverEntry.Host;
|
||||
s.MaxPlayers = serverEntry.MaxPlayers;
|
||||
s.Locked = serverEntry.Locked;
|
||||
s.PlayerCount = serverEntry.PlayerCount;
|
||||
s.Difficulty = serverEntry.Difficulty;
|
||||
s.Port = serverEntry.Port;
|
||||
s.IPAddress = serverEntry.IPAddress;
|
||||
s.PlayerId = serverEntry.PlayerId;
|
||||
|
||||
ServerEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region "Register Server"
|
||||
if (registerServer)
|
||||
{
|
||||
//Main.helper.Log("Register server");
|
||||
registerServer = false;
|
||||
|
||||
Task<WebResponse> registerTask = Task.Run(() =>
|
||||
{
|
||||
WebRequest request = WebRequest.Create(url);
|
||||
request.Method = "POST";
|
||||
request.ContentType = "application/json";
|
||||
request.Headers["X-Appwrite-Project"] = projectId;
|
||||
request.Headers["X-Appwrite-Key"] = apiKey;
|
||||
|
||||
serverID = SteamUser.GetSteamID().ToString();
|
||||
|
||||
string postData = JsonConvert.SerializeObject(new
|
||||
{
|
||||
documentId = serverID,
|
||||
data = new
|
||||
{
|
||||
Name = LobbyHandler.ServerSettings.ServerName,
|
||||
PlayerId = serverID,
|
||||
Host = KCClient.inst.Name,
|
||||
PlayerCount = KCServer.server.ClientCount,
|
||||
MaxPlayers = LobbyHandler.ServerSettings.MaxPlayers,
|
||||
Difficulty = Enum.GetName(typeof(Difficulty), LobbyHandler.ServerSettings.Difficulty),
|
||||
Heartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture),
|
||||
//IPAddress = "127.0.0.1",
|
||||
Port = 7777,
|
||||
Locked = false
|
||||
}
|
||||
});
|
||||
|
||||
Main.helper.Log(postData);
|
||||
|
||||
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
|
||||
{
|
||||
streamWriter.Write(postData);
|
||||
}
|
||||
|
||||
return request.GetResponse();
|
||||
});
|
||||
|
||||
// Wait until the task is completed
|
||||
yield return new WaitUntil(() => registerTask.IsCompleted);
|
||||
|
||||
if (registerTask.Exception != null)
|
||||
{
|
||||
Main.helper.Log("Register error");
|
||||
Main.helper.Log($"Task Exception: {registerTask.Exception}");
|
||||
Main.helper.Log($"Task InnerException: {registerTask.Exception.InnerException}");
|
||||
using (WebResponse response = registerTask.Result)
|
||||
{
|
||||
using (Stream dataStream = response.GetResponseStream())
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(dataStream))
|
||||
{
|
||||
string responseFromServer = reader.ReadToEnd();
|
||||
//Main.helper.Log(responseFromServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (WebResponse response = registerTask.Result)
|
||||
{
|
||||
using (Stream dataStream = response.GetResponseStream())
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(dataStream))
|
||||
{
|
||||
string responseFromServer = reader.ReadToEnd();
|
||||
//Main.helper.Log(responseFromServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region "Heartbeat"
|
||||
if (interval >= 8 && KCServer.IsRunning)
|
||||
{
|
||||
//Main.helper.Log("Commence heartbeat");
|
||||
Task<WebResponse> heartbeatTask = Task.Run(() =>
|
||||
{
|
||||
WebRequest request = WebRequest.Create(url + "/" + serverID);
|
||||
request.Method = "PATCH";
|
||||
request.ContentType = "application/json";
|
||||
request.Headers["X-Appwrite-Project"] = projectId;
|
||||
request.Headers["X-Appwrite-Key"] = apiKey;
|
||||
|
||||
// Create the request body
|
||||
string postData = JsonConvert.SerializeObject(new
|
||||
{
|
||||
data = new
|
||||
{
|
||||
Name = LobbyHandler.ServerSettings.ServerName,
|
||||
Host = KCClient.inst.Name,
|
||||
PlayerCount = KCServer.server.ClientCount,
|
||||
MaxPlayers = LobbyHandler.ServerSettings.MaxPlayers,
|
||||
Difficulty = Enum.GetName(typeof(Difficulty), LobbyHandler.ServerSettings.Difficulty),
|
||||
Heartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture),
|
||||
//IPAddress = "127.0.0.1",
|
||||
Locked = LobbyHandler.ServerSettings.Locked
|
||||
}
|
||||
});
|
||||
|
||||
Main.helper.Log(postData);
|
||||
|
||||
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
|
||||
{
|
||||
streamWriter.Write(postData);
|
||||
}
|
||||
|
||||
return request.GetResponse();
|
||||
});
|
||||
|
||||
// Wait until the task is completed
|
||||
yield return new WaitUntil(() => heartbeatTask.IsCompleted);
|
||||
|
||||
if (heartbeatTask.Exception != null)
|
||||
{
|
||||
Main.helper.Log("Heartbeat error");
|
||||
Main.helper.Log($"Task Exception: {heartbeatTask.Exception.InnerException}");
|
||||
}
|
||||
else
|
||||
{
|
||||
using (WebResponse response = heartbeatTask.Result)
|
||||
{
|
||||
using (Stream dataStream = response.GetResponseStream())
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(dataStream))
|
||||
{
|
||||
string responseFromServer = reader.ReadToEnd();
|
||||
//Main.helper.Log(responseFromServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Main.helper.Log("Master server heartbeat");
|
||||
interval = 0;
|
||||
}
|
||||
interval += 1;
|
||||
#endregion
|
||||
|
||||
yield return new WaitForSecondsRealtime(2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DestroyServerEntries()
|
||||
{
|
||||
foreach (GameObject entry in ServerEntries)
|
||||
Destroy(entry);
|
||||
|
||||
ServerEntries.Clear();
|
||||
}
|
||||
|
||||
public static Transform KCMUICanvas { get; set; }
|
||||
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
{
|
||||
Main.helper.Log("Serverbrowser scene loaded");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
GameObject kcmUICanvas = Instantiate(Constants.MainMenuUI_T.Find("TopLevelUICanvas").gameObject);
|
||||
|
||||
for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
|
||||
Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
|
||||
|
||||
kcmUICanvas.name = "KCMUICanvas";
|
||||
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T);
|
||||
|
||||
KCMUICanvas = kcmUICanvas.transform;
|
||||
|
||||
serverBrowserRef = GameObject.Instantiate(PrefabManager.serverBrowserPrefab, KCMUICanvas.transform);
|
||||
serverBrowserRef.SetActive(false);
|
||||
serverBrowserContentRef = serverBrowserRef.transform.Find("Container/Scroll View/Viewport/Content");
|
||||
|
||||
//hides player name prompt
|
||||
serverBrowserRef.transform.Find("Container/PlayerName").gameObject.SetActive(false);
|
||||
|
||||
|
||||
|
||||
serverLobbyRef = GameObject.Instantiate(PrefabManager.serverLobbyPrefab, KCMUICanvas.transform);
|
||||
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
|
||||
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
|
||||
serverLobbyRef.SetActive(false);
|
||||
//browser.transform.position = new Vector3(0, 0, 0);
|
||||
|
||||
|
||||
var lobbyScript = serverLobbyRef.GetComponent<ServerLobbyScript>();
|
||||
if (lobbyScript == null)
|
||||
lobbyScript = serverLobbyRef.AddComponent<ServerLobbyScript>();
|
||||
|
||||
|
||||
Main.helper.Log($"{lobbyScript == null}");
|
||||
|
||||
|
||||
//Create Server
|
||||
serverBrowserRef.transform.Find("Container/Create").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
|
||||
//KCServer.StartServer();
|
||||
Main.helper.Log((LobbyManager.Singleton == null).ToString());
|
||||
LobbyManager.Singleton.CreateLobby();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Load Server
|
||||
serverBrowserRef.transform.Find("Container/Load").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
LobbyManager.Singleton.CreateLobby(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Back to Main Menu
|
||||
serverBrowserRef.transform.Find("Container/Back").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.Menu);
|
||||
});
|
||||
|
||||
|
||||
//Back to server browser
|
||||
serverLobbyRef.transform.Find("Container/Back").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
|
||||
LobbyManager.Singleton.LeaveLobby();
|
||||
LobbyManager.loadingSave = false;
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
helper.Log("Hello?");
|
||||
try
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
helper.Log("Preload run in serverbrowser");
|
||||
}
|
||||
}
|
||||
|
||||
public class ServerResponse
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public List<ServerEntry> Documents { get; set; }
|
||||
}
|
||||
|
||||
public class ServerEntry
|
||||
{
|
||||
public int PlayerCount { get; set; }
|
||||
public DateTime Heartbeat { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Host { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public string PlayerId { get; set; }
|
||||
public string Id { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public List<object> Permissions { get; set; }
|
||||
public string DatabaseId { get; set; }
|
||||
public string CollectionId { get; set; }
|
||||
}
|
||||
}
|
||||
81
ServerBrowser/ServerEntryScript.cs
Normal file
81
ServerBrowser/ServerEntryScript.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using KCM.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class ServerEntryScript : MonoBehaviour
|
||||
{
|
||||
public int PlayerCount { get; set; }
|
||||
public DateTime Heartbeat { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Host { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
public string PlayerId { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Id { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public List<object> Permissions { get; set; }
|
||||
public string DatabaseId { get; set; }
|
||||
public string CollectionId { get; set; }
|
||||
|
||||
|
||||
public void Start()
|
||||
{
|
||||
transform.Find("Panel/ServerName").GetComponent<TextMeshProUGUI>().text = Name;
|
||||
transform.Find("Panel/ServerHost").GetComponent<TextMeshProUGUI>().text = Host;
|
||||
transform.Find("ServerDifficulty").GetComponent<TextMeshProUGUI>().text = Difficulty;
|
||||
|
||||
transform.Find("ServerLocked").gameObject.SetActive(Locked);
|
||||
transform.Find("ServerPlayers").GetComponent<TextMeshProUGUI>().text = $"{PlayerCount}/{MaxPlayers}";
|
||||
|
||||
transform.Find("Join").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string joiningId = (Environment.MachineName == "DESKTOP-ILSB1VC" && PlayerId == "76561198327621045") ? "127.0.0.1" : PlayerId;
|
||||
Main.helper.Log($"Joining id is: " + joiningId);
|
||||
KCClient.client.Connect(joiningId, useMessageHandlers: false);
|
||||
|
||||
Main.helper.Log("Set lobby script after connecting");
|
||||
|
||||
var lobbyScript = ServerBrowser.serverLobbyRef.GetComponent<ServerLobbyScript>();
|
||||
if (lobbyScript == null)
|
||||
lobbyScript = ServerBrowser.serverLobbyRef.AddComponent<ServerLobbyScript>();
|
||||
|
||||
lobbyScript.SetDetails(this);
|
||||
|
||||
ModalManager.ShowModal("Connecting to server", "Please wait while we connect to the server", "", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ServerLobby/LobbyChat/ChatEntry.cs
Normal file
12
ServerLobby/LobbyChat/ChatEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
public class ChatEntry
|
||||
{
|
||||
}
|
||||
}
|
||||
81
ServerLobby/LobbyChat/ChatEntryScript.cs
Normal file
81
ServerLobby/LobbyChat/ChatEntryScript.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
public class ChatEntryScript : MonoBehaviour
|
||||
{
|
||||
public ushort Client { get; set; }
|
||||
public string PlayerName { get; set; }
|
||||
public string Message { get; set; }
|
||||
RawImage banner { get; set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = PlayerName;
|
||||
transform.Find("PlayerMessage").GetComponent<TextMeshProUGUI>().text = Message;
|
||||
|
||||
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
|
||||
|
||||
InvokeRepeating("SetValues", 0, 0.25f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValues()
|
||||
{
|
||||
try
|
||||
{
|
||||
KCPlayer player;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ServerLobby/LobbyChat/SystemEntry.cs
Normal file
12
ServerLobby/LobbyChat/SystemEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
internal class SystemEntry
|
||||
{
|
||||
}
|
||||
}
|
||||
41
ServerLobby/LobbyChat/SystemEntryScript.cs
Normal file
41
ServerLobby/LobbyChat/SystemEntryScript.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
public class SystemEntryScript : MonoBehaviour
|
||||
{
|
||||
public string Message { get; set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
transform.Find("PlayerMessage").GetComponent<TextMeshProUGUI>().text = Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
ServerLobby/PlayerEntryScript.cs
Normal file
69
ServerLobby/PlayerEntryScript.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using KCM.Enums;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.Handlers;
|
||||
using Riptide;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM.ServerLobby
|
||||
{
|
||||
public class PlayerEntryScript : MonoBehaviour
|
||||
{
|
||||
public ushort Client { get; set; }
|
||||
|
||||
RawImage banner { get; set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
SetValues();
|
||||
|
||||
InvokeRepeating("SetValues", 0, 0.25f);
|
||||
|
||||
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
|
||||
|
||||
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
|
||||
});
|
||||
}
|
||||
|
||||
public void SetValues()
|
||||
{
|
||||
try
|
||||
{
|
||||
KCPlayer player;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
|
||||
transform.Find("Ready").gameObject.SetActive(player.ready);
|
||||
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
417
ServerLobby/ServerLobbyScript.cs
Normal file
417
ServerLobby/ServerLobbyScript.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using KCM.Packets.Game;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.Packets.Lobby;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public class ServerLobbyScript : MonoBehaviour
|
||||
{
|
||||
public static ServerEntryScript serverDetails { get; set; }
|
||||
|
||||
public static Transform PlayerListContent { get; set; }
|
||||
public static Transform PlayerChatContent { get; set; }
|
||||
|
||||
public static Button StartGameButton { get; set; }
|
||||
|
||||
public static TMP_InputField ChatInput { get; set; }
|
||||
public static Button ChatSendButton { get; set; }
|
||||
|
||||
public static TMP_InputField ServerName { get; set; }
|
||||
|
||||
public static TMP_InputField MaxPlayers { get; set; }
|
||||
public static Toggle Locked { get; set; }
|
||||
public static TMP_InputField Password { get; set; }
|
||||
|
||||
public static TMP_Dropdown Difficulty { get; set; }
|
||||
|
||||
public static TMP_InputField WorldSeed { get; set; }
|
||||
public static Button NewWorldButton { get; set; }
|
||||
|
||||
public static TMP_Dropdown WorldSize { get; set; }
|
||||
public static TMP_Dropdown WorldType { get; set; }
|
||||
public static TMP_Dropdown WorldRivers { get; set; }
|
||||
|
||||
public static TMP_Dropdown PlacementType { get; set; }
|
||||
public static Toggle FogOfWarToggle { get; set; }
|
||||
|
||||
public static ServerLobbyScript inst { get; set; }
|
||||
|
||||
|
||||
public static GameObject LoadingSave { get; set; }
|
||||
public static Image ProgressBar { get; set; }
|
||||
public static TextMeshProUGUI ProgressBarText { get; set; }
|
||||
public static TextMeshProUGUI ProgressText { get; set; }
|
||||
|
||||
enum Difficulties
|
||||
{
|
||||
Paxlon,
|
||||
Sommern,
|
||||
Vintar,
|
||||
Falle
|
||||
}
|
||||
|
||||
bool awake = false;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Main.helper.Log("ServerLobby start called");
|
||||
inst = this;
|
||||
}
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
Main.helper.Log("ServerLobby awake called");
|
||||
try
|
||||
{
|
||||
inst = this;
|
||||
PlayerListContent = transform.Find("Container/PlayerList/Viewport/Content");
|
||||
Main.helper.Log(PlayerListContent.ToString());
|
||||
|
||||
PlayerChatContent = transform.Find("Container/PlayerChat/Viewport/Content");
|
||||
Main.helper.Log(PlayerChatContent.ToString());
|
||||
ChatInput = transform.Find("Container/TextMeshPro - InputField").GetComponent<TMP_InputField>();
|
||||
Main.helper.Log(ChatInput.ToString());
|
||||
ChatSendButton = transform.Find("Container/SendMessage").GetComponent<Button>();
|
||||
Main.helper.Log(ChatSendButton.ToString());
|
||||
|
||||
|
||||
StartGameButton = transform.Find("Container/Start").GetComponent<Button>();
|
||||
|
||||
|
||||
ServerName = transform.Find("Container/ServerSettings/ServerName").GetComponentInChildren<TMP_InputField>();
|
||||
ServerName.text = $"{SteamFriends.GetPersonaName()}'s Server";
|
||||
Main.helper.Log(ServerName.ToString());
|
||||
MaxPlayers = transform.Find("Container/ServerSettings/ServerAccess/MaxPlayers").GetComponentInChildren<TMP_InputField>();
|
||||
MaxPlayers.text = "2";
|
||||
Main.helper.Log(ServerName.ToString());
|
||||
Locked = transform.Find("Container/ServerSettings/ServerAccess/Toggle").GetComponent<Toggle>();
|
||||
Locked.isOn = false;
|
||||
Main.helper.Log(Locked.ToString());
|
||||
Password = transform.Find("Container/ServerSettings/ServerAccess/Password").GetComponentInChildren<TMP_InputField>();
|
||||
Main.helper.Log(Password.ToString());
|
||||
Password.text = " ";
|
||||
|
||||
Difficulty = transform.Find("Container/ServerSettings/Difficulty").GetComponentInChildren<TMP_Dropdown>();
|
||||
Difficulty.value = 0;
|
||||
Main.helper.Log(Difficulty.ToString());
|
||||
|
||||
WorldSeed = transform.Find("Container/ServerSettings/WorldSettings/SeedInput").GetComponent<TMP_InputField>();
|
||||
Main.helper.Log(WorldSeed.ToString());
|
||||
WorldSeed.text = " ";
|
||||
NewWorldButton = transform.Find("Container/ServerSettings/WorldSettings/NewMap").GetComponent<Button>();
|
||||
Main.helper.Log(ChatSendButton.ToString());
|
||||
|
||||
WorldSize = transform.Find("Container/ServerSettings/WorldSettings/WorldSize").GetComponentInChildren<TMP_Dropdown>();
|
||||
Main.helper.Log(WorldSize.ToString());
|
||||
WorldType = transform.Find("Container/ServerSettings/WorldSettings/WorldType").GetComponentInChildren<TMP_Dropdown>();
|
||||
WorldType.value = 1;
|
||||
Main.helper.Log(WorldType.ToString());
|
||||
WorldRivers = transform.Find("Container/ServerSettings/WorldSettings/WorldRivers").GetComponentInChildren<TMP_Dropdown>();
|
||||
WorldRivers.value = 1;
|
||||
Main.helper.Log(WorldRivers.ToString());
|
||||
|
||||
|
||||
FogOfWarToggle = transform.Find("Container/ServerSettings/WorldSettings/FogOfWarToggle").GetComponent<Toggle>();
|
||||
FogOfWarToggle.isOn = true;
|
||||
|
||||
PlacementType = transform.Find("Container/ServerSettings/WorldSettings/Placement").GetComponentInChildren<TMP_Dropdown>();
|
||||
PlacementType.value = 0;
|
||||
|
||||
PlacementType.enabled = true;
|
||||
FogOfWarToggle.enabled = false;
|
||||
FogOfWarToggle.gameObject.SetActive(false);
|
||||
|
||||
Main.helper.Log("Loading save parent");
|
||||
LoadingSave = transform.Find("LoadingSave").gameObject;
|
||||
Main.helper.Log("Loading save progress bar mask");
|
||||
ProgressBar = transform.Find("LoadingSave/Window/Progress Bar/Mask").GetComponent<Image>();
|
||||
Main.helper.Log("Loading save progress bar text");
|
||||
ProgressBarText = transform.Find("LoadingSave/Window/Progress Bar").GetComponentInChildren<TextMeshProUGUI>();
|
||||
Main.helper.Log("Loading save progress text");
|
||||
ProgressText = transform.Find("LoadingSave/Window/Information").GetComponent<TextMeshProUGUI>();
|
||||
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
|
||||
Main.helper.Log("Disable all");
|
||||
//StartGameButton.gameObject.SetActive(false);
|
||||
StartGameButton.onClick.RemoveAllListeners();
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
{
|
||||
new PlayerReady()
|
||||
{
|
||||
IsReady = true
|
||||
}.Send();
|
||||
});
|
||||
|
||||
ServerName.DeactivateInputField();
|
||||
ServerName.enabled = false;
|
||||
ServerName.interactable = false;
|
||||
MaxPlayers.DeactivateInputField();
|
||||
MaxPlayers.enabled = false;
|
||||
MaxPlayers.interactable = false;
|
||||
|
||||
Locked.enabled = false;
|
||||
Locked.interactable = false;
|
||||
Password.DeactivateInputField();
|
||||
Password.enabled = false;
|
||||
Password.interactable = false;
|
||||
|
||||
Difficulty.enabled = false;
|
||||
Difficulty.interactable = false;
|
||||
|
||||
WorldSeed.enabled = false;
|
||||
WorldSeed.interactable = false;
|
||||
NewWorldButton.enabled = false;
|
||||
NewWorldButton.interactable = false;
|
||||
|
||||
WorldSize.enabled = false;
|
||||
WorldSize.interactable = false;
|
||||
WorldType.enabled = false;
|
||||
WorldType.interactable = false;
|
||||
WorldRivers.enabled = false;
|
||||
WorldRivers.interactable = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
StartGameButton.onClick.RemoveAllListeners();
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
{
|
||||
new StartGame().SendToAll();
|
||||
|
||||
if (PlacementType.value == 0 && !LobbyManager.loadingSave)
|
||||
{
|
||||
List<int> takenIdxs = new List<int>();
|
||||
foreach (KCPlayer kcPlayer in Main.kCPlayers.Values)
|
||||
{
|
||||
var idx = SRand.Range(0, World.inst.NumLandMasses);
|
||||
while (takenIdxs.Contains(idx))
|
||||
{
|
||||
Main.helper.Log($"LANDMASS: {idx} IS ALREADY TAKEN");
|
||||
idx = SRand.Range(0, World.inst.NumLandMasses);
|
||||
}
|
||||
takenIdxs.Add(idx);
|
||||
|
||||
Main.helper.Log($"SENDING PLACE KEEP RANDOMLY FOR {kcPlayer.name} ON LANDMASS: {idx}");
|
||||
|
||||
new PlaceKeepRandomly()
|
||||
{
|
||||
landmassIdx = idx
|
||||
}.Send(kcPlayer.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
ChatSendButton.onClick.AddListener(() =>
|
||||
{
|
||||
if (ChatInput.text.Length > 0)
|
||||
{
|
||||
new ChatMessage()
|
||||
{
|
||||
PlayerName = KCClient.inst.Name,
|
||||
Message = ChatInput.text
|
||||
}.Send();
|
||||
|
||||
ChatInput.text = "";
|
||||
}
|
||||
});
|
||||
|
||||
NewWorldButton.onClick.AddListener(() =>
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
if (player.inst.PlayerLandmassOwner.teamId != Player.inst.PlayerLandmassOwner.teamId)
|
||||
player.inst.Reset();
|
||||
|
||||
Main.helper.Log("new world button");
|
||||
World.inst.Generate();
|
||||
WorldSeed.text = World.inst.GetTextSeed();
|
||||
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
player.inst.SetupJobPriorities();
|
||||
|
||||
new WorldSeed()
|
||||
{
|
||||
Seed = World.inst.seed
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SetValues();
|
||||
InvokeRepeating("SetValues", 0, 1f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Return))
|
||||
{
|
||||
ChatSendButton.onClick.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void FixedUpdate()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void SetValues()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
|
||||
ServerName.text = LobbyHandler.ServerSettings.ServerName;
|
||||
MaxPlayers.text = LobbyHandler.ServerSettings.MaxPlayers.ToString();
|
||||
Locked.isOn = LobbyHandler.ServerSettings.Locked;
|
||||
Difficulty.value = LobbyHandler.ServerSettings.Difficulty;
|
||||
WorldSeed.text = LobbyHandler.ServerSettings.WorldSeed;
|
||||
WorldSize.value = (int)LobbyHandler.ServerSettings.WorldSize;
|
||||
WorldType.value = (int)LobbyHandler.ServerSettings.WorldType;
|
||||
WorldRivers.value = (int)LobbyHandler.ServerSettings.WorldRivers;
|
||||
|
||||
World.inst.mapBias = LobbyHandler.ServerSettings.WorldType;
|
||||
World.inst.mapRiverLakes = LobbyHandler.ServerSettings.WorldRivers;
|
||||
World.inst.mapSize = LobbyHandler.ServerSettings.WorldSize;
|
||||
|
||||
|
||||
Player.inst.difficulty = (Player.Difficulty)Difficulty.value;
|
||||
}
|
||||
else if (KCServer.IsRunning)
|
||||
{
|
||||
LobbyHandler.ServerSettings.ServerName = ServerName.text;
|
||||
LobbyHandler.ServerSettings.MaxPlayers = int.Parse(MaxPlayers.text.Length == 0 ? "0" : MaxPlayers.text);
|
||||
LobbyHandler.ServerSettings.Password = Password.text;
|
||||
LobbyHandler.ServerSettings.Locked = Locked.isOn;
|
||||
LobbyHandler.ServerSettings.Difficulty = Difficulty.value;
|
||||
LobbyHandler.ServerSettings.WorldSeed = WorldSeed.text;
|
||||
LobbyHandler.ServerSettings.WorldSize = (World.MapSize)WorldSize.value;
|
||||
LobbyHandler.ServerSettings.WorldType = (World.MapBias)WorldType.value;
|
||||
LobbyHandler.ServerSettings.WorldRivers = (World.MapRiverLakes)WorldRivers.value;
|
||||
|
||||
World.inst.mapBias = LobbyHandler.ServerSettings.WorldType;
|
||||
World.inst.mapRiverLakes = LobbyHandler.ServerSettings.WorldRivers;
|
||||
World.inst.mapSize = LobbyHandler.ServerSettings.WorldSize;
|
||||
|
||||
Player.inst.difficulty = (Player.Difficulty)Difficulty.value;
|
||||
|
||||
StartGameButton.interactable = Main.kCPlayers.Values.Skip(1).All(player => player.ready);
|
||||
NewWorldButton.interactable = !LobbyManager.loadingSave;
|
||||
|
||||
if (Main.kCPlayers.Count > 0)
|
||||
LobbyHandler.ServerSettings.SendToAll(KCClient.client.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDetails(ServerEntryScript details)
|
||||
{
|
||||
try
|
||||
{
|
||||
Main.helper.Log("set details???");
|
||||
serverDetails = details;
|
||||
|
||||
ServerName.text = serverDetails.Name;
|
||||
MaxPlayers.text = serverDetails.MaxPlayers.ToString();
|
||||
Locked.isOn = serverDetails.Locked;
|
||||
Difficulty.value = (int)Enum.Parse(typeof(Difficulties), serverDetails.Difficulty);
|
||||
|
||||
Main.helper.Log("set details");
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
StateManagement/BuildingState/BuildingStateManager.cs
Normal file
60
StateManagement/BuildingState/BuildingStateManager.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.State;
|
||||
using KCM.StateManagement.Observers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.BuildingState
|
||||
{
|
||||
public class BuildingStateManager
|
||||
{
|
||||
|
||||
public static void BuildingStateChanged(object sender, StateUpdateEventArgs args)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static void SendBuildingUpdate(object sender, StateUpdateEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Observer observer = (Observer)sender;
|
||||
|
||||
Building building = (Building)observer.state;
|
||||
|
||||
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
|
||||
|
||||
new BuildingStatePacket()
|
||||
{
|
||||
customName = building.customName,
|
||||
guid = building.guid,
|
||||
uniqueName = building.UniqueName,
|
||||
rotation = building.transform.GetChild(0).rotation,
|
||||
globalPosition = building.transform.position,
|
||||
localPosition = building.transform.GetChild(0).localPosition,
|
||||
built = building.IsBuilt(),
|
||||
placed = building.IsPlaced(),
|
||||
open = building.Open,
|
||||
doBuildAnimation = building.doBuildAnimation,
|
||||
constructionPaused = building.constructionPaused,
|
||||
constructionProgress = building.constructionProgress,
|
||||
resourceProgress = (float)building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(building),
|
||||
life = building.Life,
|
||||
ModifiedMaxLife = building.ModifiedMaxLife,
|
||||
yearBuilt = building.YearBuilt,
|
||||
decayProtection = building.decayProtection,
|
||||
seenByPlayer = building.seenByPlayer
|
||||
}.Send();
|
||||
} catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("ERror sending building state packet");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
StateManagement/Observers/IObserver.cs
Normal file
30
StateManagement/Observers/IObserver.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.Observers
|
||||
{
|
||||
public interface IObserver
|
||||
{
|
||||
List<FieldInfo> monitoredFields { get; set; }
|
||||
List<PropertyInfo> monitoredProperties { get; set; }
|
||||
|
||||
int updateInterval { get; set; }
|
||||
long lastUpdate { get; set; }
|
||||
Dictionary<string, object> values { get; set; }
|
||||
GameObject observerObject { get; set; }
|
||||
|
||||
void Initialise<T>(T instance, string[] monitoredFields, GameObject observerObject, int updateInterval);
|
||||
|
||||
void Update();
|
||||
|
||||
void StateChanged(string name, object value);
|
||||
|
||||
EventHandler<StateUpdateEventArgs> StateUpdated { get; set; }
|
||||
}
|
||||
}
|
||||
325
StateManagement/Observers/Observer.cs
Normal file
325
StateManagement/Observers/Observer.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using Language.Lua;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using UnityEngine;
|
||||
using static UnityEngine.PlayerLoop.PreUpdate;
|
||||
|
||||
namespace KCM.StateManagement.Observers
|
||||
{
|
||||
public class Observer : MonoBehaviour, IObserver
|
||||
{
|
||||
public object state { get; set; }
|
||||
|
||||
public List<FieldInfo> monitoredFields { get; set; }
|
||||
public List<PropertyInfo> monitoredProperties { get; set; }
|
||||
public long lastUpdate { get; set; }
|
||||
public int updateInterval { get; set; }
|
||||
public long currentMs => DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
||||
public Dictionary<string, object> values { get; set; }
|
||||
public Dictionary<string, object> changedValues { get; set; }
|
||||
|
||||
public GameObject observerObject { get; set; }
|
||||
public EventHandler<StateUpdateEventArgs> StateUpdated { get; set; }
|
||||
public EventHandler<StateUpdateEventArgs> SendUpdate { get; set; }
|
||||
|
||||
|
||||
|
||||
public long lastPacket { get; set; }
|
||||
public int packetInterval = 300;
|
||||
|
||||
public class ListTypeVariables
|
||||
{
|
||||
public List<FieldInfo> fields;
|
||||
public List<PropertyInfo> properties;
|
||||
|
||||
public ListTypeVariables(Type type)
|
||||
{
|
||||
|
||||
fields = new List<FieldInfo>();
|
||||
properties = new List<PropertyInfo>();
|
||||
|
||||
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
|
||||
foreach (var field in type.GetFields(bindingFlags).OrderBy(field => field.Name))
|
||||
fields.Add(field);
|
||||
|
||||
foreach (var prop in type.GetProperties(bindingFlags).OrderBy(prop => prop.Name))
|
||||
properties.Add(prop);
|
||||
|
||||
Main.helper.Log($"ListTypeVariables: {type.Name} has {fields.Count} fields and {properties.Count} properties");
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, ListTypeVariables> listVariables = new Dictionary<string, ListTypeVariables>();
|
||||
|
||||
public void Initialise<T>(T state, string[] monitoredFields, GameObject observerObject, int updateInterval = 100)
|
||||
{
|
||||
this.state = state;
|
||||
this.monitoredFields = new List<FieldInfo>();
|
||||
this.monitoredProperties = new List<PropertyInfo>();
|
||||
this.lastUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
this.observerObject = observerObject;
|
||||
this.updateInterval = updateInterval;
|
||||
this.values = new Dictionary<string, object>();
|
||||
this.changedValues = new Dictionary<string, object>();
|
||||
|
||||
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
foreach (var field in monitoredFields)
|
||||
{
|
||||
if (state.GetType().GetField(field, bindingFlags) != null)
|
||||
this.monitoredFields.Add(state.GetType().GetField(field, bindingFlags));
|
||||
else if (state.GetType().GetProperty(field, bindingFlags) != null)
|
||||
this.monitoredProperties.Add(state.GetType().GetProperty(field, bindingFlags));
|
||||
}
|
||||
|
||||
|
||||
// This will store all the fields and properties of the list type on start for efficient access and later comparison
|
||||
foreach (var field in this.monitoredFields)
|
||||
{
|
||||
|
||||
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
var listType = field.FieldType.GetGenericArguments()[0];
|
||||
|
||||
listVariables.Add(field.Name, new ListTypeVariables(listType));
|
||||
}
|
||||
|
||||
if (field.FieldType.IsGenericType &&
|
||||
field.FieldType.GetGenericTypeDefinition() == typeof(ArrayExt<>))
|
||||
{
|
||||
var listType = field.FieldType.GetField("data").FieldType.GetElementType();
|
||||
listVariables.Add(field.Name, new ListTypeVariables(listType));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in this.monitoredProperties)
|
||||
{
|
||||
|
||||
if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
var listType = prop.PropertyType.GetGenericArguments()[0];
|
||||
|
||||
if (listType.IsPrimitive)
|
||||
continue;
|
||||
|
||||
listVariables.Add(prop.Name, new ListTypeVariables(listType));
|
||||
}
|
||||
|
||||
if (prop.PropertyType.IsGenericType &&
|
||||
prop.PropertyType.GetGenericTypeDefinition() == typeof(ArrayExt<>))
|
||||
{
|
||||
var listType = prop.PropertyType.GetField("data").FieldType.GetElementType();
|
||||
listVariables.Add(prop.Name, new ListTypeVariables(listType));
|
||||
}
|
||||
}
|
||||
|
||||
Main.helper.Log($"Observer created for {state.GetType().Name} with {this.monitoredFields.Count} fields, {this.monitoredProperties.Count} properties, and {listVariables.Count} non-primitive list variables");
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (this.state == null)
|
||||
return;
|
||||
|
||||
if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds);
|
||||
return;
|
||||
|
||||
foreach (var field in monitoredFields)
|
||||
UpdateValue(field.Name, field.GetValue(state));
|
||||
|
||||
foreach (var prop in monitoredProperties)
|
||||
UpdateValue(prop.Name, prop.GetValue(state));
|
||||
|
||||
|
||||
|
||||
if ((currentMs - lastPacket > packetInterval) && changedValues.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendUpdate?.Invoke(this, null);
|
||||
lastPacket = currentMs;
|
||||
changedValues.Clear();
|
||||
} catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error sending update: {e.Message}");
|
||||
Main.helper.Log($"Stack trace: {e.StackTrace}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateValue(string name, object value)
|
||||
{
|
||||
if (values.ContainsKey(name))
|
||||
{
|
||||
if (!AreEqual(name, values[name], value))
|
||||
{
|
||||
StateChanged(name, value);
|
||||
|
||||
if (isArrayExt(values[name]))
|
||||
values[name].GetType().GetField("data").SetValue(values[name], value);
|
||||
else
|
||||
values[name] = value;
|
||||
|
||||
this.lastUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
||||
if (isArrayExt(values[name]))
|
||||
changedValues[name].GetType().GetField("data").SetValue(values[name], value);
|
||||
else
|
||||
changedValues[name] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!changedValues.ContainsKey(name))
|
||||
changedValues.Add(name, value);
|
||||
|
||||
values.Add(name, value);
|
||||
StateChanged(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
public class StateUpdateEventArgs : EventArgs
|
||||
{
|
||||
public string name;
|
||||
public object value;
|
||||
}
|
||||
|
||||
public void StateChanged(string name, object value)
|
||||
{
|
||||
StateUpdated?.Invoke(this, new StateUpdateEventArgs { name = name, value = value });
|
||||
|
||||
//Main.helper.Log($"{name} state changed to {value}");
|
||||
}
|
||||
|
||||
bool IsListButNotOfPrimitives(object obj)
|
||||
{
|
||||
return obj.GetType().IsGenericType &&
|
||||
obj.GetType().GetGenericTypeDefinition() == typeof(List<>) &&
|
||||
!obj.GetType().GetGenericArguments()[0].IsPrimitive;
|
||||
}
|
||||
|
||||
bool isArrayExt(object obj)
|
||||
{
|
||||
return obj.GetType().IsGenericType &&
|
||||
obj.GetType().GetGenericTypeDefinition() == typeof(ArrayExt<>);
|
||||
}
|
||||
|
||||
object[] ToObjectArray(object obj)
|
||||
{
|
||||
return ((IEnumerable)obj).Cast<object>().ToArray();
|
||||
}
|
||||
|
||||
public bool AreEqual(string fieldName, object a, object b)
|
||||
{
|
||||
object current = b;
|
||||
object previous = a;
|
||||
|
||||
if (isArrayExt(a) && isArrayExt(b))
|
||||
{
|
||||
object dataA = a.GetType().GetField("data").GetValue(previous);
|
||||
object dataB = b.GetType().GetField("data").GetValue(current);
|
||||
|
||||
|
||||
return DeepArrayTypeEqualsCheck(fieldName, (Array)dataA, (Array)dataB);
|
||||
}
|
||||
|
||||
if (IsListButNotOfPrimitives(a) && IsListButNotOfPrimitives(b))
|
||||
{
|
||||
var aAsObjectArray = ToObjectArray(a);
|
||||
var bAsObjectArray = ToObjectArray(b);
|
||||
|
||||
return DeepArrayTypeEqualsCheck(fieldName, aAsObjectArray, bAsObjectArray);
|
||||
}
|
||||
|
||||
if ((current.GetType().IsArray && previous.GetType().IsArray) && !(current.GetType().IsPrimitive && previous.GetType().IsPrimitive))
|
||||
return DeepArrayTypeEqualsCheck(fieldName, (Array)a, (Array)b);
|
||||
|
||||
// Check if both are null or are the same instance
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
if (current.GetType().IsArray && previous.GetType().IsArray)
|
||||
{
|
||||
// Check for single-dimensional arrays
|
||||
if (current is Array aArray && previous is Array bArray)
|
||||
{
|
||||
// Different lengths mean they are not equal
|
||||
if (aArray.Length != bArray.Length) return false;
|
||||
|
||||
// Handle 2D arrays specifically
|
||||
if (aArray.Rank == 2 && bArray.Rank == 2)
|
||||
{
|
||||
return Are2DArraysEqual(aArray, bArray);
|
||||
}
|
||||
// Handle 1D arrays
|
||||
else if (aArray.Rank == 1 && bArray.Rank == 1)
|
||||
{
|
||||
return Enumerable.SequenceEqual(aArray.Cast<object>(), bArray.Cast<object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default Equals for other types
|
||||
return Equals(current, previous);
|
||||
}
|
||||
|
||||
private bool Are2DArraysEqual(Array a, Array b)
|
||||
{
|
||||
if (a.GetLength(0) != b.GetLength(0) || a.GetLength(1) != b.GetLength(1)) return false;
|
||||
|
||||
for (int i = 0; i < a.GetLength(0); i++)
|
||||
{
|
||||
for (int j = 0; j < a.GetLength(1); j++)
|
||||
{
|
||||
if (!Equals(a.GetValue(i, j), b.GetValue(i, j))) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DeepArrayTypeEqualsCheck(string varName, Array a, Array b)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check for reference equality and nulls
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
// Compare counts
|
||||
if (a.Length != b.Length) return false;
|
||||
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
foreach (var field in listVariables[varName].fields)
|
||||
{
|
||||
if (!Equals(field.GetValue(a.GetValue(i)), field.GetValue(b.GetValue(i))))
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var prop in listVariables[varName].properties)
|
||||
{
|
||||
if (!Equals(prop.GetValue(a.GetValue(i)), prop.GetValue(b.GetValue(i))))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error comparing {varName} arrays: {e.Message}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
StateManagement/Observers/StateObserver.cs
Normal file
40
StateManagement/Observers/StateObserver.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.Observers
|
||||
{
|
||||
public class StateObserver
|
||||
{
|
||||
public static Dictionary<int, IObserver> observers = new Dictionary<int, IObserver>();
|
||||
|
||||
public static void RegisterObserver<T>(T instance, string[] monitoredFields, EventHandler<StateUpdateEventArgs> eventHandler = null, EventHandler<StateUpdateEventArgs> sendUpdateHandler = null)
|
||||
{
|
||||
if (observers.ContainsKey(instance.GetHashCode()))
|
||||
return;
|
||||
|
||||
var observerObject = new GameObject($"{instance.GetHashCode()} {instance.GetType().Name} State Observer");
|
||||
|
||||
var observer = observerObject.AddComponent<Observer>();
|
||||
|
||||
|
||||
observer.Initialise(instance, monitoredFields, observerObject);
|
||||
|
||||
|
||||
if (eventHandler != null)
|
||||
observer.StateUpdated += eventHandler;
|
||||
|
||||
|
||||
if (sendUpdateHandler != null)
|
||||
observer.SendUpdate += sendUpdateHandler;
|
||||
|
||||
|
||||
observers.Add(instance.GetHashCode(), observer);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
129
UI/KCUIElement.cs
Normal file
129
UI/KCUIElement.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using I2.Loc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM.UI
|
||||
{
|
||||
public class KCUIElement
|
||||
{
|
||||
public Transform t = null;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => t.name;
|
||||
set => t.name = value;
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => t.GetComponentInChildren<TextMeshProUGUI>().text;
|
||||
set => t.GetComponentInChildren<TextMeshProUGUI>().text = value;
|
||||
}
|
||||
|
||||
|
||||
public Vector3 LocalPosition
|
||||
{
|
||||
get => Transform.localPosition;
|
||||
set => Transform.localPosition = value;
|
||||
}
|
||||
|
||||
public Vector3 Position
|
||||
{
|
||||
get => Transform.position;
|
||||
set => Transform.position = value;
|
||||
}
|
||||
|
||||
public Vector3 Size
|
||||
{
|
||||
get => Transform.localScale;
|
||||
set => Transform.localScale = value;
|
||||
}
|
||||
|
||||
public GameObject GameObject
|
||||
{
|
||||
get => t.gameObject;
|
||||
}
|
||||
|
||||
public Transform Transform
|
||||
{
|
||||
get => t;
|
||||
}
|
||||
|
||||
public bool FirstSibling
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Transform.SetAsFirstSibling();
|
||||
}
|
||||
}
|
||||
|
||||
public bool LastSibling
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Transform.SetAsLastSibling();
|
||||
}
|
||||
}
|
||||
|
||||
public int SiblingIndex
|
||||
{
|
||||
set => Transform.SetSiblingIndex(value);
|
||||
}
|
||||
|
||||
public KCUIElement(Transform e, Transform parent = null)
|
||||
{
|
||||
|
||||
if (parent == null)
|
||||
t = GameObject.Instantiate(e);
|
||||
else
|
||||
t = GameObject.Instantiate(e, parent);
|
||||
|
||||
/*foreach (Transform child in e.GetComponentInChildren<Transform>())
|
||||
{
|
||||
createChild(child, t);
|
||||
}*/
|
||||
|
||||
t.gameObject.SetActive(true);
|
||||
|
||||
//foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
|
||||
// GameObject.Destroy(Localize);
|
||||
|
||||
//Button.onClick = new Button.ButtonClickedEvent();
|
||||
}
|
||||
|
||||
public static void createChild(Transform child, Transform parent)
|
||||
{
|
||||
var t = GameObject.Instantiate(child, parent);
|
||||
|
||||
foreach (Transform c in child.GetComponentInChildren<Transform>())
|
||||
{
|
||||
createChild(c, t);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
PropertyInfo[] _PropertyInfos = t.GetType().GetProperties();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var info in _PropertyInfos)
|
||||
{
|
||||
var value = info.GetValue(Transform, null) ?? "(null)";
|
||||
sb.AppendLine(info.Name + ": " + value.ToString());
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user