commit c332b89bf775d6deec30924b28e4c77c59f0c142 Author: EntenKoeniq <81123713+EntenKoeniq@users.noreply.github.com> Date: Wed Jul 7 13:36:25 2021 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b9de1ab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://spenden.pp-h.eu/68454276-da3c-47c7-b95a-7fa443706a44'] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08f611d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/bin +**/obj +**/packages +.vs/* \ No newline at end of file diff --git a/Client/Chat.cs b/Client/Chat.cs new file mode 100644 index 0000000..905a935 --- /dev/null +++ b/Client/Chat.cs @@ -0,0 +1,143 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; + +using GTA; +using GTA.Native; + +namespace CoopClient +{ + public class Chat + { + private readonly Scaleform MainScaleForm; + + public string CurrentInput { get; set; } + + private bool CurrentFocused { get; set; } + public bool Focused + { + get { return CurrentFocused; } + set + { + MainScaleForm.CallFunction("SET_FOCUS", value ? 2 : 1, 2, "ALL"); + + CurrentFocused = value; + } + } + + public Chat() + { + MainScaleForm = new Scaleform("multiplayer_chat"); + } + + public void Init() + { + MainScaleForm.CallFunction("SET_FOCUS", 2, 2, "ALL"); + MainScaleForm.CallFunction("SET_FOCUS", 1, 2, "ALL"); + } + + public void Clear() + { + MainScaleForm.CallFunction("RESET"); + } + + public void Tick() + { + MainScaleForm.Render2D(); + + if (!CurrentFocused) + { + return; + } + + Function.Call(Hash.DISABLE_ALL_CONTROL_ACTIONS, 0); + } + + public void AddMessage(string sender, string msg) + { + MainScaleForm.CallFunction("ADD_MESSAGE", sender + ":", msg); + } + + public void OnKeyDown(Keys key) + { + if (key == Keys.Escape) + { + Focused = false; + CurrentInput = ""; + return; + } + + if (key == Keys.PageUp) + { + MainScaleForm.CallFunction("PAGE_UP"); + } + else if (key == Keys.PageDown) + { + MainScaleForm.CallFunction("PAGE_DOWN"); + } + + string keyChar = GetCharFromKey(key, Game.IsKeyPressed(Keys.ShiftKey), false); + + if (keyChar.Length == 0) + { + return; + } + + switch (keyChar[0]) + { + case (char)8: + if (CurrentInput.Length > 0) + { + MainScaleForm.CallFunction("SET_FOCUS", 1, 2, "ALL"); + MainScaleForm.CallFunction("SET_FOCUS", 2, 2, "ALL"); + + CurrentInput = CurrentInput.Substring(0, CurrentInput.Length - 1); + MainScaleForm.CallFunction("ADD_TEXT", CurrentInput); + } + return; + case (char)13: + MainScaleForm.CallFunction("ADD_TEXT", "ENTER"); + + if (!string.IsNullOrWhiteSpace(CurrentInput)) + { + Main.MainNetworking.SendChatMessage(CurrentInput); + } + + Focused = false; + CurrentInput = ""; + return; + default: + CurrentInput += keyChar; + MainScaleForm.CallFunction("ADD_TEXT", keyChar); + return; + } + } + + [DllImport("user32.dll")] + public static extern int ToUnicodeEx(uint virtualKeyCode, uint scanCode, byte[] keyboardState, + [Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] + StringBuilder receivingBuffer, + int bufferSize, uint flags, IntPtr kblayout); + + public static string GetCharFromKey(Keys key, bool shift, bool altGr) + { + StringBuilder buf = new StringBuilder(256); + byte[] keyboardState = new byte[256]; + + if (shift) + { + keyboardState[(int)Keys.ShiftKey] = 0xff; + } + + if (altGr) + { + keyboardState[(int)Keys.ControlKey] = 0xff; + keyboardState[(int)Keys.Menu] = 0xff; + } + + ToUnicodeEx((uint)key, 0, keyboardState, buf, 256, 0, InputLanguage.CurrentInputLanguage.Handle); + return buf.ToString(); + } + } +} diff --git a/Client/CoopClient.csproj b/Client/CoopClient.csproj new file mode 100644 index 0000000..121b9e6 --- /dev/null +++ b/Client/CoopClient.csproj @@ -0,0 +1,85 @@ + + + + + Debug + AnyCPU + {EF56D109-1F22-43E0-9DFF-CFCFB94E0681} + Library + Properties + CoopClient + CoopClient + v4.8 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\Libs\Release\LemonUI.SHVDN3.dll + + + ..\Libs\Release\Lidgren.Network.dll + + + ..\packages\protobuf-net.2.4.6\lib\net40\protobuf-net.dll + + + False + ..\Libs\Release\ScriptHookVDotNet3.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Client/Entities/EntitiesNPC.cs b/Client/Entities/EntitiesNPC.cs new file mode 100644 index 0000000..560888c --- /dev/null +++ b/Client/Entities/EntitiesNPC.cs @@ -0,0 +1,7 @@ +namespace CoopClient.Entities +{ + public class EntitiesNpc : EntitiesPed + { + public int LastUpdateReceived { get; set; } + } +} diff --git a/Client/Entities/EntitiesPed.cs b/Client/Entities/EntitiesPed.cs new file mode 100644 index 0000000..480f9b0 --- /dev/null +++ b/Client/Entities/EntitiesPed.cs @@ -0,0 +1,354 @@ +using System; +using System.Drawing; +using System.Collections.Generic; + +using GTA; +using GTA.Native; +using GTA.Math; + +using LemonUI.Elements; + +namespace CoopClient +{ + public class EntitiesPed + { + private bool AllDataAvailable = false; + public bool LastSyncWasFull { get; set; } = false; + + public Ped Character { get; set; } + public int Health { get; set; } + private int LastModelHash = 0; + public int ModelHash { get; set; } + private Dictionary LastProps = new Dictionary(); + public Dictionary Props { get; set; } + public Vector3 Position { get; set; } + public Vector3 Rotation { get; set; } + public Vector3 Velocity { get; set; } + public byte Speed { get; set; } + private bool LastIsJumping = false; + public bool IsJumping { get; set; } + public bool IsRagdoll { get; set; } + public bool IsOnFire { get; set; } + public Vector3 AimCoords { get; set; } + public bool IsAiming { get; set; } + public bool IsShooting { get; set; } + public bool IsReloading { get; set; } + public int CurrentWeaponHash { get; set; } + + private Blip PedBlip; + + public void DisplayLocally(string username) + { + /* + * username: string + * string: null + * ped: npc + * string: value + * ped: player + */ + + // Check beforehand whether ped has all the required data + if (!AllDataAvailable) + { + if (!LastSyncWasFull) + { + return; + } + + AllDataAvailable = true; + } + + #region NOT_IN_RANGE + if (!Game.Player.Character.IsInRange(Position, 250f)) + { + if (Character != null && Character.Exists()) + { + Character.Kill(); + Character.Delete(); + } + + if (username != null) + { + if (PedBlip == null || !PedBlip.Exists()) + { + PedBlip = World.CreateBlip(Position); + PedBlip.Color = BlipColor.White; + PedBlip.Scale = 0.8f; + PedBlip.Name = username; + } + else + { + PedBlip.Position = Position; + } + } + + return; + } + #endregion + + #region IS_IN_RANGE + if (PedBlip != null && PedBlip.Exists()) + { + PedBlip.Delete(); + } + + bool characterExist = Character != null && Character.Exists(); + + if (!characterExist) + { + CreateCharacter(username); + } + else if (LastSyncWasFull) + { + if (ModelHash != LastModelHash) + { + if (characterExist) + { + Character.Kill(); + Character.Delete(); + } + + CreateCharacter(username); + } + else if (Props != LastProps) + { + foreach (KeyValuePair prop in Props) + { + Function.Call(Hash.SET_PED_COMPONENT_VARIATION, Character.Handle, prop.Key, prop.Value, 0, 0); + } + + LastProps = Props; + } + } + + if (username != null && Character.IsInRange(Game.Player.Character.Position, 20f)) + { + float sizeOffset; + if (GameplayCamera.IsFirstPersonAimCamActive) + { + Vector3 targetPos = Character.Bones[Bone.IKHead].Position + new Vector3(0, 0, 0.10f) + (Character.Velocity / Game.FPS); + + Function.Call(Hash.SET_DRAW_ORIGIN, targetPos.X, targetPos.Y, targetPos.Z, 0); + + sizeOffset = Math.Max(1f - ((GameplayCamera.Position - Character.Position).Length() / 30f), 0.30f); + } + else + { + Vector3 targetPos = Character.Bones[Bone.IKHead].Position + new Vector3(0, 0, 0.35f) + (Character.Velocity / Game.FPS); + + Function.Call(Hash.SET_DRAW_ORIGIN, targetPos.X, targetPos.Y, targetPos.Z, 0); + + sizeOffset = Math.Max(1f - ((GameplayCamera.Position - Character.Position).Length() / 25f), 0.25f); + } + + new ScaledText(new PointF(0, 0), username, 0.4f * sizeOffset, GTA.UI.Font.ChaletLondon) + { + Outline = true, + Alignment = GTA.UI.Alignment.Center + }.Draw(); + + Function.Call(Hash.CLEAR_DRAW_ORIGIN); + } + + if (IsOnFire && !Character.IsOnFire) + { + Character.IsInvincible = false; + + Function.Call(Hash.START_ENTITY_FIRE, Character); + } + else if (!IsOnFire && Character.IsOnFire) + { + Function.Call(Hash.STOP_ENTITY_FIRE, Character); + + Character.IsInvincible = true; + + if (Character.IsDead) + { + Character.Resurrect(); + } + } + + if (Character.IsDead) + { + if (Health <= 0) + { + return; + } + + Character.IsInvincible = true; + Character.Resurrect(); + } + else if (Character.Health != Health) + { + Character.Health = Health; + + if (Health <= 0 && !Character.IsDead) + { + Character.IsInvincible = false; + Character.Kill(); + return; + } + } + + if (IsJumping && !LastIsJumping) + { + Character.Task.Jump(); + } + + LastIsJumping = IsJumping; + + if (IsRagdoll && !Character.IsRagdoll) + { + Character.CanRagdoll = true; + Character.Ragdoll(); + + return; + } + else if (!IsRagdoll && Character.IsRagdoll) + { + Character.CancelRagdoll(); + Character.CanRagdoll = false; + } + + if (IsJumping || IsOnFire) + { + return; + } + + if (IsReloading && !Character.IsReloading) + { + Character.Task.ClearAll(); + Character.Task.ReloadWeapon(); + } + + if (IsReloading) + { + return; + } + + if (Character.Weapons.Current.Hash != (WeaponHash)CurrentWeaponHash) + { + Character.Weapons.RemoveAll(); + Character.Weapons.Give((WeaponHash)CurrentWeaponHash, -1, true, true); + } + + if (IsShooting) + { + Function.Call(Hash.SET_PED_INFINITE_AMMO_CLIP, Character, true); + + if (!Character.IsInRange(Position, 0.5f)) + { + Function.Call(Hash.TASK_GO_TO_COORD_WHILE_AIMING_AT_COORD, Character, Position.X, Position.Y, + Position.Z, AimCoords.X, AimCoords.Y, AimCoords.Z, 3f, true, 2f, 2f, false, 0, false, + unchecked((int)FiringPattern.FullAuto)); + } + else + { + Function.Call(Hash.TASK_SHOOT_AT_COORD, Character, AimCoords.X, AimCoords.Y, AimCoords.Z, 1500, (uint)FiringPattern.FullAuto); + } + } + else if (IsAiming) + { + if (!Character.IsInRange(Position, 0.5f)) + { + Function.Call(Hash.TASK_GO_TO_COORD_WHILE_AIMING_AT_COORD, Character, Position.X, Position.Y, + Position.Z, AimCoords.X, AimCoords.Y, AimCoords.Z, 3f, false, 2f, 2f, false, 512, false, + unchecked((int)FiringPattern.FullAuto)); + } + else + { + Character.Task.AimAt(AimCoords, -1); + } + } + else + { + WalkTo(); + } + #endregion + } + + private void CreateCharacter(string username) + { + LastModelHash = ModelHash; + LastProps = Props; + + Character = World.CreatePed(new Model(ModelHash), Position, Rotation.Z); + Character.RelationshipGroup = Main.RelationshipGroup; + Character.BlockPermanentEvents = true; + Character.CanRagdoll = false; + Character.IsInvincible = true; + Character.Health = Health; + + if (username != null) + { + // Add a new blip for the ped + Character.AddBlip(); + Character.AttachedBlip.Color = BlipColor.White; + Character.AttachedBlip.Scale = 0.8f; + Character.AttachedBlip.Name = username; + } + + foreach (KeyValuePair prop in Props) + { + Function.Call(Hash.SET_PED_COMPONENT_VARIATION, Character.Handle, prop.Key, prop.Value, 0, 0); + } + } + + private bool LastMoving; + private void WalkTo() + { + if (!Character.IsInRange(Position, 7.0f) && (LastMoving = true)) + { + Character.Position = Position; + Character.Rotation = Rotation; + } + else + { + Vector3 predictPosition = Position + (Position - Character.Position) + Velocity; + float range = predictPosition.DistanceToSquared(Character.Position); + + switch (Speed) + { + case 1: + if ((!Character.IsWalking || range > 0.25f) && (LastMoving = true)) + { + float nrange = range * 2; + if (nrange > 1.0f) + { + nrange = 1.0f; + } + + Character.Task.GoStraightTo(predictPosition); + Function.Call(Hash.SET_PED_DESIRED_MOVE_BLEND_RATIO, Character, nrange); + } + break; + case 2: + if ((!Character.IsRunning || range > 0.50f) && (LastMoving = true)) + { + Character.Task.RunTo(predictPosition, true); + Function.Call(Hash.SET_PED_DESIRED_MOVE_BLEND_RATIO, Character, 1.0f); + } + break; + case 3: + if ((!Character.IsSprinting || range > 0.75f) && (LastMoving = true)) + { + Function.Call(Hash.TASK_GO_STRAIGHT_TO_COORD, Character, predictPosition.X, predictPosition.Y, predictPosition.Z, 3.0f, -1, 0.0f, 0.0f); + Function.Call(Hash.SET_RUN_SPRINT_MULTIPLIER_FOR_PLAYER, Character, 1.49f); + Function.Call(Hash.SET_PED_DESIRED_MOVE_BLEND_RATIO, Character, 1.0f); + } + break; + default: + if (!Character.IsInRange(Position, 0.5f)) + { + Character.Task.RunTo(Position, true, 500); + } + else if (LastMoving && (LastMoving = false)) + { + Character.Task.StandStill(2000); + } + break; + } + } + } + } +} diff --git a/Client/Entities/EntitiesPlayer.cs b/Client/Entities/EntitiesPlayer.cs new file mode 100644 index 0000000..d520161 --- /dev/null +++ b/Client/Entities/EntitiesPlayer.cs @@ -0,0 +1,8 @@ +namespace CoopClient.Entities +{ + public class EntitiesPlayer : EntitiesPed + { + public string SocialClubName { get; set; } + public string Username { get; set; } = "Player"; + } +} diff --git a/Client/Entities/EntitiesThread.cs b/Client/Entities/EntitiesThread.cs new file mode 100644 index 0000000..547606e --- /dev/null +++ b/Client/Entities/EntitiesThread.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using GTA; + +namespace CoopClient.Entities +{ + public class EntitiesThread : Script + { + const int npcThreshold = 2500; // 2.5 seconds timeout + + public EntitiesThread() + { + Tick += OnTick; + Interval = 1000 / 60; + } + + private void OnTick(object sender, EventArgs e) + { + if (Game.IsLoading || !Main.MainNetworking.IsOnServer() || !Main.NpcsAllowed) + { + return; + } + + lock (Main.Npcs) + { + // Remove all NPCs with a last update older than npcThreshold or display this npc + foreach (KeyValuePair npc in new Dictionary(Main.Npcs)) + { + if ((Environment.TickCount - npc.Value.LastUpdateReceived) > npcThreshold) + { + if (npc.Value.Character != null && npc.Value.Character.Exists() && npc.Value.Health > 0) + { + npc.Value.Character.Kill(); + npc.Value.Character.Delete(); + } + + Main.Npcs.Remove(npc.Key); + } + else + { + npc.Value.DisplayLocally(null); + } + } + } + + // Only if that player wants to share his NPCs with others + if (Main.ShareNpcsWithPlayers) + { + // Send all npcs from the current player + foreach (Ped ped in World.GetNearbyPeds(Game.Player.Character.Position, 150f) + .Where(p => p.Handle != Game.Player.Character.Handle && !p.IsDead && p.RelationshipGroup != Main.RelationshipGroup) + .OrderBy(p => (p.Position - Game.Player.Character.Position).Length()) + .Take(10)) // only 10 for now + { + Main.MainNetworking.SendNpcData(ped); + } + } + } + } +} diff --git a/Client/Main.cs b/Client/Main.cs new file mode 100644 index 0000000..9d0b925 --- /dev/null +++ b/Client/Main.cs @@ -0,0 +1,362 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using System.Collections.Generic; + +using CoopClient.Entities; + +using GTA; +using GTA.Native; + +using LemonUI; +using LemonUI.Menus; + +namespace CoopClient +{ + public class Main : Script + { + public static RelationshipGroup RelationshipGroup; + + private bool GameLoaded = false; + + public static readonly string CurrentModVersion = Enum.GetValues(typeof(ModVersion)).Cast().Last().ToString(); + + public static bool ShareNpcsWithPlayers = false; + public static bool NpcsAllowed = false; + + public static Settings MainSettings = Util.ReadSettings(); + public static ObjectPool MainMenuPool = new ObjectPool(); + public static NativeMenu MainMenu = new NativeMenu("GTACoop:R", CurrentModVersion.Replace("_", ".")) + { + UseMouse = false, + Alignment = MainSettings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left + }; + public static NativeMenu MainSettingsMenu = new NativeMenu("GTACoop:R", "Settings", "Go to the settings") + { + UseMouse = false, + Alignment = MainSettings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left + }; + public static Chat MainChat = new Chat(); + public static PlayerList MainPlayerList = new PlayerList(); + + public static Networking MainNetworking = new Networking(); + + public static string LocalPlayerID = null; + public static readonly Dictionary Players = new Dictionary(); + public static readonly Dictionary Npcs = new Dictionary(); + + public Main() + { + Function.Call((Hash)0x0888C3502DBBEEF5); // _LOAD_MP_DLC_MAPS + Function.Call((Hash)0x9BAE5AD2508DF078, true); // _ENABLE_MP_DLC_MAPS + + NativeItem usernameItem = new NativeItem("Username") + { + AltTitle = MainSettings.Username + }; + usernameItem.Activated += (menu, item) => + { + string newUsername = Game.GetUserInput(WindowTitle.EnterMessage20, usernameItem.AltTitle, 20); + if (!string.IsNullOrWhiteSpace(newUsername)) + { + MainSettings.Username = newUsername; + Util.SaveSettings(); + + usernameItem.AltTitle = newUsername; + MainMenuPool.RefreshAll(); + } + }; + + NativeItem serverIpItem = new NativeItem("Server IP") + { + AltTitle = MainSettings.LastServerAddress + }; + serverIpItem.Activated += (menu, item) => + { + string newServerIp = Game.GetUserInput(WindowTitle.EnterMessage60, serverIpItem.AltTitle, 60); + if (!string.IsNullOrWhiteSpace(newServerIp) && newServerIp.Contains(":")) + { + MainSettings.LastServerAddress = newServerIp; + Util.SaveSettings(); + + serverIpItem.AltTitle = newServerIp; + MainMenuPool.RefreshAll(); + } + + }; + + NativeItem serverConnectItem = new NativeItem("Connect"); + serverConnectItem.Activated += (sender, item) => + { + MainNetworking.DisConnectFromServer(MainSettings.LastServerAddress); + }; + + NativeCheckboxItem shareNpcsItem = new NativeCheckboxItem("Share Npcs", ShareNpcsWithPlayers); + shareNpcsItem.CheckboxChanged += (item, check) => + { + ShareNpcsWithPlayers = shareNpcsItem.Checked; + }; + shareNpcsItem.Enabled = false; + + NativeCheckboxItem flipMenuItem = new NativeCheckboxItem("Flip menu", MainSettings.FlipMenu); + flipMenuItem.CheckboxChanged += (item, check) => + { + MainMenu.Alignment = flipMenuItem.Checked ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left; + MainSettingsMenu.Alignment = flipMenuItem.Checked ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left; + + MainSettings.FlipMenu = flipMenuItem.Checked; + Util.SaveSettings(); + }; + + NativeItem aboutItem = new NativeItem("About", "~g~GTACoop~s~:~b~R ~s~by ~o~EntenKoeniq") + { + LeftBadge = new LemonUI.Elements.ScaledTexture("commonmenu", "shop_new_star") + }; + +#if DEBUG + NativeCheckboxItem useDebugItem = new NativeCheckboxItem("Debug", UseDebug); + useDebugItem.CheckboxChanged += (item, check) => + { + UseDebug = useDebugItem.Checked; + + if (!useDebugItem.Checked && DebugSyncPed != null) + { + if (DebugSyncPed.Character.Exists()) + { + DebugSyncPed.Character.Kill(); + DebugSyncPed.Character.Delete(); + } + + DebugSyncPed = null; + FullDebugSync = true; + Players.Remove("DebugKey"); + } + }; +#endif + + MainMenu.Add(usernameItem); + MainMenu.Add(serverIpItem); + MainMenu.Add(serverConnectItem); + MainMenu.AddSubMenu(MainSettingsMenu); + MainSettingsMenu.Add(shareNpcsItem); + MainSettingsMenu.Add(flipMenuItem); +#if DEBUG + MainSettingsMenu.Add(useDebugItem); +#endif + MainMenu.Add(aboutItem); + + MainMenuPool.Add(MainMenu); + MainMenuPool.Add(MainSettingsMenu); + + Tick += OnTick; + KeyDown += OnKeyDown; + } + + private int LastDataSend; + private void OnTick(object sender, EventArgs e) + { + if (Game.IsLoading) + { + return; + } + else if (!GameLoaded && (GameLoaded = true)) + { + RelationshipGroup = World.AddRelationshipGroup("SYNCPED"); + Game.Player.Character.RelationshipGroup = RelationshipGroup; + + Function.Call(Hash.SET_CAN_ATTACK_FRIENDLY, Game.Player.Character, true, true); + Function.Call(Hash.SET_PED_CAN_BE_TARGETTED, true); + } + + MainMenuPool.Process(); + + MainNetworking.ReceiveMessages(); + + if (!MainNetworking.IsOnServer()) + { + return; + } + + if (Game.Player.Character.IsGettingIntoVehicle) + { + GTA.UI.Notification.Show("~y~Vehicles are not sync yet!", true); + } + + MainChat.Tick(); + if (!MainChat.Focused && !MainMenuPool.AreAnyVisible) + { + MainPlayerList.Tick(); + } + + // Display all players + foreach (KeyValuePair player in Players) + { + player.Value.DisplayLocally(player.Value.Username); + } + + if (UseDebug) + { + Debug(); + } + + if ((Environment.TickCount - LastDataSend) >= (1000 / 60)) + { + MainNetworking.SendPlayerData(); + + LastDataSend = Environment.TickCount; + } + } + + private void OnKeyDown(object sender, KeyEventArgs e) + { + if (MainChat.Focused) + { + MainChat.OnKeyDown(e.KeyCode); + return; + } + + switch (e.KeyCode) + { + case Keys.F9: + if (MainMenuPool.AreAnyVisible) + { + MainMenu.Visible = false; + MainSettingsMenu.Visible = false; + } + else + { + MainMenu.Visible = true; + } + break; + case Keys.T: + if (MainNetworking.IsOnServer()) + { + MainChat.Focused = true; + } + break; + case Keys.Y: + if (MainNetworking.IsOnServer()) + { + int time = Environment.TickCount; + + MainPlayerList.Pressed = (time - MainPlayerList.Pressed) < 5000 ? (time - 6000) : time; + } + break; + } + } + + private DateTime ArtificialLagCounter = DateTime.MinValue; + private EntitiesPlayer DebugSyncPed; + private bool FullDebugSync = true; + private bool UseDebug = false; + + private void Debug() + { + var player = Game.Player.Character; + if (!Players.ContainsKey("DebugKey")) + { + Players.Add("DebugKey", new EntitiesPlayer() { SocialClubName = "DEBUG", Username = "DebugPlayer" }); + DebugSyncPed = Players["DebugKey"]; + } + + if (!player.IsInVehicle() && DateTime.Now.Subtract(ArtificialLagCounter).TotalMilliseconds >= 300) + { + ArtificialLagCounter = DateTime.Now; + + #region SPEED + byte speed = 0; + if (Game.Player.Character.IsWalking) + { + speed = 1; + } + else if (Game.Player.Character.IsRunning) + { + speed = 2; + } + else if (Game.Player.Character.IsSprinting || Game.IsControlPressed(GTA.Control.Sprint)) + { + speed = 3; + } + #endregion + + #region SHOOTING - AIMING + bool aiming = player.IsAiming; + bool shooting = player.IsShooting && player.Weapons.Current?.AmmoInClip != 0; + + GTA.Math.Vector3 aimCoord = new GTA.Math.Vector3(); + if (aiming || shooting) + { + aimCoord = Util.RaycastEverything(new GTA.Math.Vector2(0, 0)); + } + #endregion + + #region FLAG + byte? flags = 0; + + if (FullDebugSync) + { + flags |= (byte)PedDataFlags.LastSyncWasFull; + } + + if (aiming) + { + flags |= (byte)PedDataFlags.IsAiming; + } + + if (shooting) + { + flags |= (byte)PedDataFlags.IsShooting; + } + + if (player.IsReloading) + { + flags |= (byte)PedDataFlags.IsReloading; + } + + if (player.IsJumping) + { + flags |= (byte)PedDataFlags.IsJumping; + } + + if (player.IsRagdoll) + { + flags |= (byte)PedDataFlags.IsRagdoll; + } + + if (player.IsOnFire) + { + flags |= (byte)PedDataFlags.IsOnFire; + } + #endregion + + if (FullDebugSync) + { + DebugSyncPed.ModelHash = player.Model.Hash; + DebugSyncPed.Props = Util.GetPedProps(player); + } + DebugSyncPed.Health = player.Health; + DebugSyncPed.Position = player.Position; + DebugSyncPed.Rotation = player.Rotation; + DebugSyncPed.Velocity = player.Velocity; + DebugSyncPed.Speed = speed; + DebugSyncPed.AimCoords = aimCoord; + DebugSyncPed.CurrentWeaponHash = (int)player.Weapons.Current.Hash; + DebugSyncPed.LastSyncWasFull = (flags.Value & (byte)PedDataFlags.LastSyncWasFull) > 0; + DebugSyncPed.IsAiming = (flags.Value & (byte)PedDataFlags.IsAiming) > 0; + DebugSyncPed.IsShooting = (flags.Value & (byte)PedDataFlags.IsShooting) > 0; + DebugSyncPed.IsReloading = (flags.Value & (byte)PedDataFlags.IsReloading) > 0; + DebugSyncPed.IsJumping = (flags.Value & (byte)PedDataFlags.IsJumping) > 0; + DebugSyncPed.IsRagdoll = (flags.Value & (byte)PedDataFlags.IsRagdoll) > 0; + DebugSyncPed.IsOnFire = (flags.Value & (byte)PedDataFlags.IsOnFire) > 0; + } + + if (DebugSyncPed.Character != null && DebugSyncPed.Character.Exists()) + { + Function.Call(Hash.SET_ENTITY_NO_COLLISION_ENTITY, DebugSyncPed.Character.Handle, player.Handle, false); + Function.Call(Hash.SET_ENTITY_NO_COLLISION_ENTITY, player.Handle, DebugSyncPed.Character.Handle, false); + } + + FullDebugSync = !FullDebugSync; + } + } +} diff --git a/Client/Networking.cs b/Client/Networking.cs new file mode 100644 index 0000000..976fb52 --- /dev/null +++ b/Client/Networking.cs @@ -0,0 +1,556 @@ +using System; + +using CoopClient.Entities; + +using Lidgren.Network; + +using GTA; +using GTA.Math; +using GTA.Native; + +namespace CoopClient +{ + public class Networking + { + public NetClient Client; + + public void DisConnectFromServer(string address) + { + if (IsOnServer()) + { + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + new PlayerDisconnectPacket() { Player = Main.LocalPlayerID }.PacketToNetOutGoingMessage(outgoingMessage); + Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered); + Client.FlushSendQueue(); + + Client.Disconnect("Disconnected"); + } + else + { + // 6d4ec318f1c43bd62fe13d5a7ab28650 = GTACOOP:R + NetPeerConfiguration config = new NetPeerConfiguration("6d4ec318f1c43bd62fe13d5a7ab28650") + { + AutoFlushSendQueue = false + }; + + Client = new NetClient(config); + + Client.Start(); + + string[] ip = address.Split(':'); + + // Send HandshakePacket + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + new HandshakePacket() + { + ID = string.Empty, + SocialClubName = Game.Player.Name, + Username = Main.MainSettings.Username, + ModVersion = Main.CurrentModVersion, + NpcsAllowed = false + }.PacketToNetOutGoingMessage(outgoingMessage); + + Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage); + } + } + + public bool IsOnServer() + { + return Client?.ConnectionStatus == NetConnectionStatus.Connected; + } + + public void ReceiveMessages() + { + if (Client == null) + { + return; + } + + NetIncomingMessage message; + + while ((message = Client.ReadMessage()) != null) + { + switch (message.MessageType) + { + case NetIncomingMessageType.StatusChanged: + NetConnectionStatus status = (NetConnectionStatus)message.ReadByte(); + + string reason = message.ReadString(); + + switch (status) + { + case NetConnectionStatus.InitiatedConnect: + Main.MainMenu.Items[0].Enabled = false; + Main.MainMenu.Items[1].Enabled = false; + Main.MainMenu.Items[2].Enabled = false; + GTA.UI.Notification.Show("~y~Trying to connect..."); + break; + case NetConnectionStatus.Connected: + if (message.SenderConnection.RemoteHailMessage.ReadByte() != (byte)PacketTypes.HandshakePacket) + { + Client.Disconnect("Wrong packet!"); + } + else + { + Packet remoteHailMessagePacket; + remoteHailMessagePacket = new HandshakePacket(); + remoteHailMessagePacket.NetIncomingMessageToPacket(message.SenderConnection.RemoteHailMessage); + + HandshakePacket handshakePacket = (HandshakePacket)remoteHailMessagePacket; + Main.LocalPlayerID = handshakePacket.ID; + Main.NpcsAllowed = handshakePacket.NpcsAllowed; + + foreach (Ped entity in World.GetAllPeds()) + { + if (entity.Handle != Game.Player.Character.Handle) + { + entity.Kill(); + entity.Delete(); + } + } + + foreach (Vehicle vehicle in World.GetAllVehicles()) + { + if (Game.Player.Character.CurrentVehicle?.Handle != vehicle.Handle) + { + vehicle.Delete(); + } + } + + Function.Call(Hash.SET_GARBAGE_TRUCKS, 0); + Function.Call(Hash.SET_RANDOM_BOATS, 0); + Function.Call(Hash.SET_RANDOM_TRAINS, 0); + + Main.MainMenu.Items[2].Enabled = true; + Main.MainMenu.Items[2].Title = "Disconnect"; + Main.MainSettingsMenu.Items[0].Enabled = Main.NpcsAllowed; + + Main.MainChat.Init(); + Main.MainPlayerList.Init(Main.MainSettings.Username); + + // Send player connect packet + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + new PlayerConnectPacket() + { + Player = Main.LocalPlayerID, + SocialClubName = string.Empty, + Username = string.Empty + }.PacketToNetOutGoingMessage(outgoingMessage); + Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered); + Client.FlushSendQueue(); + + GTA.UI.Notification.Show("~g~Connected!"); + } + break; + case NetConnectionStatus.Disconnected: + GTA.UI.Notification.Show("~r~" + reason); + + // Reset all values + FullPlayerSync = true; + + Main.NpcsAllowed = false; + + if (Main.MainChat.Focused) + { + Main.MainChat.Focused = false; + } + + Main.MainChat.Clear(); + + Main.MainMenu.Items[0].Enabled = true; + Main.MainMenu.Items[1].Enabled = true; + Main.MainMenu.Items[2].Enabled = true; + Main.MainMenu.Items[2].Title = "Connect"; + Main.MainSettingsMenu.Items[0].Enabled = false; + + Main.Players.Clear(); + Main.Npcs.Clear(); + + Vector3 pos = Game.Player.Character.Position; + Function.Call(Hash.CLEAR_AREA_OF_PEDS, pos.X, pos.Y, pos.Z, 300.0f, 0); + Function.Call(Hash.CLEAR_AREA_OF_VEHICLES, pos.X, pos.Y, pos.Z, 300.0f, 0); + break; + } + break; + case NetIncomingMessageType.Data: + byte packetType = message.ReadByte(); + + Packet packet; + + switch (packetType) + { + case (byte)PacketTypes.PlayerConnectPacket: + packet = new PlayerConnectPacket(); + packet.NetIncomingMessageToPacket(message); + PlayerConnect((PlayerConnectPacket)packet); + break; + case (byte)PacketTypes.PlayerDisconnectPacket: + packet = new PlayerDisconnectPacket(); + packet.NetIncomingMessageToPacket(message); + PlayerDisconnect((PlayerDisconnectPacket)packet); + break; + case (byte)PacketTypes.FullSyncPlayerPacket: + packet = new FullSyncPlayerPacket(); + packet.NetIncomingMessageToPacket(message); + FullSyncPlayer((FullSyncPlayerPacket)packet); + break; + case (byte)PacketTypes.FullSyncNpcPacket: + packet = new FullSyncNpcPacket(); + packet.NetIncomingMessageToPacket(message); + FullSyncNpc((FullSyncNpcPacket)packet); + break; + case (byte)PacketTypes.LightSyncPlayerPacket: + packet = new LightSyncPlayerPacket(); + packet.NetIncomingMessageToPacket(message); + LightSyncPlayer((LightSyncPlayerPacket)packet); + break; + case (byte)PacketTypes.ChatMessagePacket: + packet = new ChatMessagePacket(); + packet.NetIncomingMessageToPacket(message); + + ChatMessagePacket chatMessagePacket = (ChatMessagePacket)packet; + Main.MainChat.AddMessage(chatMessagePacket.Username, chatMessagePacket.Message); + break; + } + break; + case NetIncomingMessageType.DebugMessage: + case NetIncomingMessageType.ErrorMessage: + case NetIncomingMessageType.WarningMessage: + case NetIncomingMessageType.VerboseDebugMessage: + break; + default: + break; + } + + Client.Recycle(message); + } + } + + #region GET + private void PlayerConnect(PlayerConnectPacket packet) + { + EntitiesPlayer player = new EntitiesPlayer() + { + SocialClubName = packet.SocialClubName, + Username = packet.Username + }; + + Main.Players.Add(packet.Player, player); + + Main.MainPlayerList.Update(Main.Players, Main.MainSettings.Username); + } + + private void PlayerDisconnect(PlayerDisconnectPacket packet) + { + if (Main.Players.ContainsKey(packet.Player)) + { + Main.Players.Remove(packet.Player); + + Main.MainPlayerList.Update(Main.Players, Main.MainSettings.Username); + } + } + + private void FullSyncPlayer(FullSyncPlayerPacket packet) + { + if (Main.Players.ContainsKey(packet.Player)) + { + EntitiesPlayer player = Main.Players[packet.Player]; + player.ModelHash = packet.ModelHash; + player.Props = packet.Props; + player.Health = packet.Health; + player.Position = packet.Position.ToVector(); + player.Rotation = packet.Rotation.ToVector(); + player.Velocity = packet.Velocity.ToVector(); + player.Speed = packet.Speed; + player.AimCoords = packet.AimCoords.ToVector(); + player.LastSyncWasFull = (packet.Flag.Value & (byte)PedDataFlags.LastSyncWasFull) > 0; + player.IsAiming = (packet.Flag.Value & (byte)PedDataFlags.IsAiming) > 0; + player.IsShooting = (packet.Flag.Value & (byte)PedDataFlags.IsShooting) > 0; + player.IsReloading = (packet.Flag.Value & (byte)PedDataFlags.IsReloading) > 0; + player.IsJumping = (packet.Flag.Value & (byte)PedDataFlags.IsJumping) > 0; + player.IsRagdoll = (packet.Flag.Value & (byte)PedDataFlags.IsRagdoll) > 0; + player.IsOnFire = (packet.Flag.Value & (byte)PedDataFlags.IsOnFire) > 0; + } + } + + private void FullSyncNpc(FullSyncNpcPacket packet) + { + if (Main.Npcs.ContainsKey(packet.ID)) + { + EntitiesNpc npc = Main.Npcs[packet.ID]; + npc.LastUpdateReceived = Environment.TickCount; + npc.ModelHash = packet.ModelHash; + npc.Props = packet.Props; + npc.Health = packet.Health; + npc.Position = packet.Position.ToVector(); + npc.Rotation = packet.Rotation.ToVector(); + npc.Velocity = packet.Velocity.ToVector(); + npc.Speed = packet.Speed; + npc.AimCoords = packet.AimCoords.ToVector(); + npc.LastSyncWasFull = (packet.Flag.Value & (byte)PedDataFlags.LastSyncWasFull) > 0; + npc.IsAiming = (packet.Flag.Value & (byte)PedDataFlags.IsAiming) > 0; + npc.IsShooting = (packet.Flag.Value & (byte)PedDataFlags.IsShooting) > 0; + npc.IsReloading = (packet.Flag.Value & (byte)PedDataFlags.IsReloading) > 0; + npc.IsJumping = (packet.Flag.Value & (byte)PedDataFlags.IsJumping) > 0; + npc.IsRagdoll = (packet.Flag.Value & (byte)PedDataFlags.IsRagdoll) > 0; + npc.IsOnFire = (packet.Flag.Value & (byte)PedDataFlags.IsOnFire) > 0; + } + else + { + Main.Npcs.Add(packet.ID, new EntitiesNpc() + { + LastUpdateReceived = Environment.TickCount, + ModelHash = packet.ModelHash, + Props = packet.Props, + Health = packet.Health, + Position = packet.Position.ToVector(), + Rotation = packet.Rotation.ToVector(), + Velocity = packet.Velocity.ToVector(), + Speed = packet.Speed, + AimCoords = packet.AimCoords.ToVector(), + LastSyncWasFull = (packet.Flag.Value & (byte)PedDataFlags.LastSyncWasFull) > 0, + IsAiming = (packet.Flag.Value & (byte)PedDataFlags.IsAiming) > 0, + IsShooting = (packet.Flag.Value & (byte)PedDataFlags.IsShooting) > 0, + IsReloading = (packet.Flag.Value & (byte)PedDataFlags.IsReloading) > 0, + IsJumping = (packet.Flag.Value & (byte)PedDataFlags.IsJumping) > 0, + IsRagdoll = (packet.Flag.Value & (byte)PedDataFlags.IsRagdoll) > 0, + IsOnFire = (packet.Flag.Value & (byte)PedDataFlags.IsOnFire) > 0 + }); + } + } + + private void LightSyncPlayer(LightSyncPlayerPacket packet) + { + if (Main.Players.ContainsKey(packet.Player)) + { + EntitiesPlayer player = Main.Players[packet.Player]; + player.Health = packet.Health; + player.Position = packet.Position.ToVector(); + player.Rotation = packet.Rotation.ToVector(); + player.Velocity = packet.Velocity.ToVector(); + player.Speed = packet.Speed; + player.LastSyncWasFull = (packet.Flag.Value & (byte)PedDataFlags.LastSyncWasFull) > 0; + player.IsAiming = (packet.Flag.Value & (byte)PedDataFlags.IsAiming) > 0; + player.IsShooting = (packet.Flag.Value & (byte)PedDataFlags.IsShooting) > 0; + player.IsReloading = (packet.Flag.Value & (byte)PedDataFlags.IsReloading) > 0; + player.IsJumping = (packet.Flag.Value & (byte)PedDataFlags.IsJumping) > 0; + player.IsRagdoll = (packet.Flag.Value & (byte)PedDataFlags.IsRagdoll) > 0; + player.IsOnFire = (packet.Flag.Value & (byte)PedDataFlags.IsOnFire) > 0; + } + } + #endregion + + #region SEND + private bool FullPlayerSync = true; + public void SendPlayerData() + { + Ped player = Game.Player.Character; + + #region SPEED + byte speed = 0; + if (Game.Player.Character.IsWalking) + { + speed = 1; + } + else if (Game.Player.Character.IsRunning) + { + speed = 2; + } + else if (Game.Player.Character.IsSprinting) + { + speed = 3; + } + #endregion + + #region SHOOTING - AIMING + bool aiming = player.IsAiming; + bool shooting = player.IsShooting && player.Weapons.Current?.AmmoInClip != 0; + + Vector3 aimCoord = new Vector3(); + if (aiming || shooting) + { + aimCoord = Util.RaycastEverything(new Vector2(0, 0)); + } + #endregion + + #region Flags + byte? flags = 0; + + if (FullPlayerSync) + { + flags |= (byte)PedDataFlags.LastSyncWasFull; + } + + if (aiming) + { + flags |= (byte)PedDataFlags.IsAiming; + } + + if (shooting) + { + flags |= (byte)PedDataFlags.IsShooting; + } + + if (player.IsReloading) + { + flags |= (byte)PedDataFlags.IsReloading; + } + + if (player.IsJumping) + { + flags |= (byte)PedDataFlags.IsJumping; + } + + if (player.IsRagdoll) + { + flags |= (byte)PedDataFlags.IsRagdoll; + } + + if (player.IsOnFire) + { + flags |= (byte)PedDataFlags.IsOnFire; + } + #endregion + + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + + if (FullPlayerSync) + { + new FullSyncPlayerPacket() + { + Player = Main.LocalPlayerID, + ModelHash = player.Model.Hash, + Props = Util.GetPedProps(player), + Health = player.Health, + Position = player.Position.ToLVector(), + Rotation = player.Rotation.ToLVector(), + Velocity = player.Velocity.ToLVector(), + Speed = speed, + AimCoords = aimCoord.ToLVector(), + CurrentWeaponHash = (int)player.Weapons.Current.Hash, + Flag = flags + }.PacketToNetOutGoingMessage(outgoingMessage); + } + else + { + new LightSyncPlayerPacket() + { + Player = Main.LocalPlayerID, + Health = player.Health, + Position = player.Position.ToLVector(), + Rotation = player.Rotation.ToLVector(), + Velocity = player.Velocity.ToLVector(), + Speed = speed, + AimCoords = aimCoord.ToLVector(), + CurrentWeaponHash = (int)player.Weapons.Current.Hash, + Flag = flags + }.PacketToNetOutGoingMessage(outgoingMessage); + } + + Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered); + Client.FlushSendQueue(); + + FullPlayerSync = !FullPlayerSync; + } + + public void SendNpcData(Ped npc) + { + #region SPEED + byte speed = 0; + if (npc.IsWalking) + { + speed = 1; + } + else if (npc.IsRunning) + { + speed = 2; + } + else if (npc.IsSprinting) + { + speed = 3; + } + #endregion + + #region SHOOTING - AIMING + bool aiming = npc.IsAiming; + bool shooting = npc.IsShooting && npc.Weapons.Current?.AmmoInClip != 0; + + Vector3 aimCoord = new Vector3(); + if (aiming || shooting) + { + aimCoord = Util.GetLastWeaponImpact(npc); + } + #endregion + + #region Flags + byte? flags = 0; + + // FullSync = true + flags |= (byte)PedDataFlags.LastSyncWasFull; + + if (shooting) + { + flags |= (byte)PedDataFlags.IsShooting; + } + + if (aiming) + { + flags |= (byte)PedDataFlags.IsAiming; + } + + if (npc.IsReloading) + { + flags |= (byte)PedDataFlags.IsReloading; + } + + if (npc.IsJumping) + { + flags |= (byte)PedDataFlags.IsJumping; + } + + if (npc.IsRagdoll) + { + flags |= (byte)PedDataFlags.IsRagdoll; + } + + if (npc.IsOnFire) + { + flags |= (byte)PedDataFlags.IsOnFire; + } + #endregion + + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + + new FullSyncNpcPacket() + { + ID = Main.LocalPlayerID + npc.Handle, + ModelHash = npc.Model.Hash, + Props = Util.GetPedProps(npc), + Health = npc.Health, + Position = npc.Position.ToLVector(), + Rotation = npc.Rotation.ToLVector(), + Velocity = npc.Velocity.ToLVector(), + Speed = speed, + AimCoords = aimCoord.ToLVector(), + CurrentWeaponHash = (int)npc.Weapons.Current.Hash, + Flag = flags + }.PacketToNetOutGoingMessage(outgoingMessage); + + Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered); + Client.FlushSendQueue(); + } + + public void SendChatMessage(string message) + { + NetOutgoingMessage outgoingMessage = Client.CreateMessage(); + new ChatMessagePacket() + { + Username = Main.MainSettings.Username, + Message = message + }.PacketToNetOutGoingMessage(outgoingMessage); + Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered); + Client.FlushSendQueue(); + } + #endregion + } +} diff --git a/Client/Packets.cs b/Client/Packets.cs new file mode 100644 index 0000000..565fe51 --- /dev/null +++ b/Client/Packets.cs @@ -0,0 +1,478 @@ +using System; +using System.IO; +using System.Collections.Generic; + +using Lidgren.Network; +using ProtoBuf; + +using GTA.Math; + +namespace CoopClient +{ + #region CLIENT-ONLY + public static class VectorExtensions + { + public static LVector3 ToLVector(this Vector3 vec) + { + return new LVector3() + { + X = vec.X, + Y = vec.Y, + Z = vec.Z, + }; + } + } + #endregion + + [ProtoContract] + public struct LVector3 + { + #region CLIENT-ONLY + public Vector3 ToVector() + { + return new Vector3(X, Y, Z); + } + #endregion + + public LVector3(float X, float Y, float Z) + { + this.X = X; + this.Y = Y; + this.Z = Z; + } + + [ProtoMember(1)] + public float X { get; set; } + + [ProtoMember(2)] + public float Y { get; set; } + + [ProtoMember(3)] + public float Z { get; set; } + } + + public enum ModVersion + { + V0_1_0 + } + + public enum PacketTypes + { + HandshakePacket, + PlayerConnectPacket, + PlayerDisconnectPacket, + FullSyncPlayerPacket, + FullSyncNpcPacket, + LightSyncPlayerPacket, + ChatMessagePacket + } + + [Flags] + public enum PedDataFlags + { + LastSyncWasFull = 1 << 0, + IsAiming = 1 << 1, + IsShooting = 1 << 2, + IsReloading = 1 << 3, + IsJumping = 1 << 4, + IsRagdoll = 1 << 5, + IsOnFire = 1 << 6 + } + + public interface IPacket + { + void PacketToNetOutGoingMessage(NetOutgoingMessage message); + void NetIncomingMessageToPacket(NetIncomingMessage message); + } + + public abstract class Packet : IPacket + { + public abstract void PacketToNetOutGoingMessage(NetOutgoingMessage message); + public abstract void NetIncomingMessageToPacket(NetIncomingMessage message); + } + + [ProtoContract] + public class HandshakePacket : Packet + { + [ProtoMember(1)] + public string ID { get; set; } + + [ProtoMember(2)] + public string SocialClubName { get; set; } + + [ProtoMember(3)] + public string Username { get; set; } + + [ProtoMember(4)] + public string ModVersion { get; set; } + + [ProtoMember(5)] + public bool NpcsAllowed { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.HandshakePacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + HandshakePacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + ID = data.ID; + SocialClubName = data.SocialClubName; + Username = data.Username; + ModVersion = data.ModVersion; + NpcsAllowed = data.NpcsAllowed; + } + } + + [ProtoContract] + public class PlayerConnectPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public string SocialClubName { get; set; } + + [ProtoMember(3)] + public string Username { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.PlayerConnectPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + PlayerConnectPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + SocialClubName = data.SocialClubName; + Username = data.Username; + } + } + + [ProtoContract] + public class PlayerDisconnectPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.PlayerDisconnectPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + PlayerDisconnectPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + } + } + + [ProtoContract] + public class FullSyncPlayerPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public int ModelHash { get; set; } + + [ProtoMember(3)] + public Dictionary Props { get; set; } + + [ProtoMember(4)] + public int Health { get; set; } + + [ProtoMember(5)] + public LVector3 Position { get; set; } + + [ProtoMember(6)] + public LVector3 Rotation { get; set; } + + [ProtoMember(7)] + public LVector3 Velocity { get; set; } + + [ProtoMember(8)] + public byte Speed { get; set; } + + [ProtoMember(9)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(10)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(11)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.FullSyncPlayerPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + FullSyncPlayerPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + ModelHash = data.ModelHash; + Props = data.Props; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class FullSyncNpcPacket : Packet + { + [ProtoMember(1)] + public string ID { get; set; } + + [ProtoMember(2)] + public int ModelHash { get; set; } + + [ProtoMember(3)] + public Dictionary Props { get; set; } + + [ProtoMember(4)] + public int Health { get; set; } + + [ProtoMember(5)] + public LVector3 Position { get; set; } + + [ProtoMember(6)] + public LVector3 Rotation { get; set; } + + [ProtoMember(7)] + public LVector3 Velocity { get; set; } + + [ProtoMember(8)] + public byte Speed { get; set; } + + [ProtoMember(9)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(10)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(11)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.FullSyncNpcPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + FullSyncNpcPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + ID = data.ID; + ModelHash = data.ModelHash; + Props = data.Props; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class LightSyncPlayerPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public int Health { get; set; } + + [ProtoMember(3)] + public LVector3 Position { get; set; } + + [ProtoMember(4)] + public LVector3 Rotation { get; set; } + + [ProtoMember(5)] + public LVector3 Velocity { get; set; } + + [ProtoMember(6)] + public byte Speed { get; set; } + + [ProtoMember(7)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(8)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(9)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.LightSyncPlayerPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + LightSyncPlayerPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class ChatMessagePacket : Packet + { + [ProtoMember(1)] + public string Username { get; set; } + + [ProtoMember(2)] + public string Message { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.ChatMessagePacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + ChatMessagePacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Username = data.Username; + Message = data.Message; + } + } +} diff --git a/Client/PlayerList.cs b/Client/PlayerList.cs new file mode 100644 index 0000000..5412278 --- /dev/null +++ b/Client/PlayerList.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +using CoopClient.Entities; + +using GTA; +using GTA.Native; + +namespace CoopClient +{ + public class PlayerList + { + private readonly Scaleform MainScaleform = new Scaleform("mp_mm_card_freemode"); + public int Pressed { get; set; } + + public void Init(string localUsername) + { + MainScaleform.CallFunction("SET_DATA_SLOT_EMPTY", 0); + MainScaleform.CallFunction("SET_DATA_SLOT", 0, "", localUsername, 116, 0, 0, "", "", 2, "", "", ' '); + MainScaleform.CallFunction("SET_TITLE", "Player list", "1 players"); + MainScaleform.CallFunction("DISPLAY_VIEW"); + } + + public void Update(Dictionary players, string LocalUsername) + { + MainScaleform.CallFunction("SET_DATA_SLOT_EMPTY", 0); + MainScaleform.CallFunction("SET_DATA_SLOT", 0, "", LocalUsername, 116, 0, 0, "", "", 2, "", "", ' '); + + int i = 1; + foreach (KeyValuePair player in players) + { + MainScaleform.CallFunction("SET_DATA_SLOT", i++, "", player.Value.Username, 116, 0, i - 1, "", "", 2, "", "", ' '); + } + + MainScaleform.CallFunction("SET_TITLE", "Player list", (players.Count + 1) + " players"); + MainScaleform.CallFunction("DISPLAY_VIEW"); + } + + public void Tick() + { + if ((Environment.TickCount - Pressed) < 5000) + { + Function.Call(Hash.DRAW_SCALEFORM_MOVIE, MainScaleform.Handle, 0.122f, 0.3f, 0.28f, 0.6f, 255, 255, 255, 255, 0); + } + } + } +} diff --git a/Client/Properties/AssemblyInfo.cs b/Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..63552ab --- /dev/null +++ b/Client/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Client")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Client")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ef56d109-1f22-43e0-9dff-cfcfb94e0681")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Client/Settings.cs b/Client/Settings.cs new file mode 100644 index 0000000..4cbeb72 --- /dev/null +++ b/Client/Settings.cs @@ -0,0 +1,9 @@ +namespace CoopClient +{ + public class Settings + { + public string Username { get; set; } = "Player"; + public string LastServerAddress { get; set; } = "127.0.0.1:4499"; + public bool FlipMenu { get; set; } = false; + } +} diff --git a/Client/Util.cs b/Client/Util.cs new file mode 100644 index 0000000..1464fdf --- /dev/null +++ b/Client/Util.cs @@ -0,0 +1,189 @@ +using System; +using System.IO; +using System.Xml.Serialization; +using System.Collections.Generic; + +using GTA; +using GTA.Native; +using GTA.Math; + +namespace CoopClient +{ + class Util + { + public static Dictionary GetPedProps(Ped ped) + { + Dictionary result = new Dictionary(); + for (int i = 0; i < 11; i++) + { + int mod = Function.Call(Hash.GET_PED_DRAWABLE_VARIATION, ped.Handle, i); + result.Add(i, mod); + } + return result; + } + + public static Settings ReadSettings() + { + XmlSerializer ser = new XmlSerializer(typeof(Settings)); + + string path = Directory.GetCurrentDirectory() + "\\scripts\\CoopSettings.xml"; + Settings settings = null; + + if (File.Exists(path)) + { + using (FileStream stream = File.OpenRead(path)) + { + settings = (Settings)ser.Deserialize(stream); + } + + using (FileStream stream = new FileStream(path, File.Exists(path) ? FileMode.Truncate : FileMode.Create, FileAccess.ReadWrite)) + { + ser.Serialize(stream, settings); + } + } + else + { + using (FileStream stream = File.OpenWrite(path)) + { + ser.Serialize(stream, settings = new Settings()); + } + } + + return settings; + } + + public static void SaveSettings() + { + try + { + string path = Directory.GetCurrentDirectory() + "\\scripts\\CoopSettings.xml"; + + using (FileStream stream = new FileStream(path, File.Exists(path) ? FileMode.Truncate : FileMode.Create, FileAccess.ReadWrite)) + { + XmlSerializer ser = new XmlSerializer(typeof(Settings)); + ser.Serialize(stream, Main.MainSettings); + } + } + catch (Exception ex) + { + GTA.UI.Notification.Show("Error saving player settings: " + ex.Message); + } + } + + public static Vector3 GetLastWeaponImpact(Ped ped) + { + OutputArgument coord = new OutputArgument(); + if (!Function.Call(Hash.GET_PED_LAST_WEAPON_IMPACT_COORD, ped.Handle, coord)) + { + return new Vector3(); + } + + return coord.GetResult(); + } + + public static double DegToRad(double deg) + { + return deg * Math.PI / 180.0; + } + + public static Vector3 RotationToDirection(Vector3 rotation) + { + double z = DegToRad(rotation.Z); + double x = DegToRad(rotation.X); + double num = Math.Abs(Math.Cos(x)); + + return new Vector3 + { + X = (float)(-Math.Sin(z) * num), + Y = (float)(Math.Cos(z) * num), + Z = (float)Math.Sin(x) + }; + } + + public static bool WorldToScreenRel(Vector3 worldCoords, out Vector2 screenCoords) + { + OutputArgument num1 = new OutputArgument(); + OutputArgument num2 = new OutputArgument(); + + if (!Function.Call(Hash.GET_SCREEN_COORD_FROM_WORLD_COORD, worldCoords.X, worldCoords.Y, worldCoords.Z, num1, num2)) + { + screenCoords = new Vector2(); + return false; + } + + screenCoords = new Vector2((num1.GetResult() - 0.5f) * 2, (num2.GetResult() - 0.5f) * 2); + return true; + } + + public static Vector3 ScreenRelToWorld(Vector3 camPos, Vector3 camRot, Vector2 coord) + { + Vector3 camForward = RotationToDirection(camRot); + Vector3 rotUp = camRot + new Vector3(10, 0, 0); + Vector3 rotDown = camRot + new Vector3(-10, 0, 0); + Vector3 rotLeft = camRot + new Vector3(0, 0, -10); + Vector3 rotRight = camRot + new Vector3(0, 0, 10); + + Vector3 camRight = RotationToDirection(rotRight) - RotationToDirection(rotLeft); + Vector3 camUp = RotationToDirection(rotUp) - RotationToDirection(rotDown); + + double rollRad = -DegToRad(camRot.Y); + + Vector3 camRightRoll = camRight * (float)Math.Cos(rollRad) - camUp * (float)Math.Sin(rollRad); + Vector3 camUpRoll = camRight * (float)Math.Sin(rollRad) + camUp * (float)Math.Cos(rollRad); + + Vector3 point3D = camPos + camForward * 10.0f + camRightRoll + camUpRoll; + if (!WorldToScreenRel(point3D, out Vector2 point2D)) + { + return camPos + camForward * 10.0f; + } + + Vector3 point3DZero = camPos + camForward * 10.0f; + if (!WorldToScreenRel(point3DZero, out Vector2 point2DZero)) + { + return camPos + camForward * 10.0f; + } + + const double eps = 0.001; + if (Math.Abs(point2D.X - point2DZero.X) < eps || Math.Abs(point2D.Y - point2DZero.Y) < eps) + { + return camPos + camForward * 10.0f; + } + + float scaleX = (coord.X - point2DZero.X) / (point2D.X - point2DZero.X); + float scaleY = (coord.Y - point2DZero.Y) / (point2D.Y - point2DZero.Y); + + return camPos + camForward * 10.0f + camRightRoll * scaleX + camUpRoll * scaleY; + } + + public static Vector3 RaycastEverything(Vector2 screenCoord) + { + Vector3 camPos = GameplayCamera.Position; + Vector3 camRot = GameplayCamera.Rotation; + const float raycastToDist = 100.0f; + const float raycastFromDist = 1f; + + Vector3 target3D = ScreenRelToWorld(camPos, camRot, screenCoord); + Vector3 source3D = camPos; + + Entity ignoreEntity = Game.Player.Character; + if (Game.Player.Character.IsInVehicle()) + { + ignoreEntity = Game.Player.Character.CurrentVehicle; + } + + Vector3 dir = target3D - source3D; + dir.Normalize(); + RaycastResult raycastResults = World.Raycast(source3D + dir * raycastFromDist, + source3D + dir * raycastToDist, + (IntersectFlags)(1 | 16 | 256 | 2 | 4 | 8), // | peds + vehicles + ignoreEntity); + + if (raycastResults.DidHit) + { + return raycastResults.HitPosition; + } + + return camPos + dir * raycastToDist; + } + } +} diff --git a/Client/WorldThread.cs b/Client/WorldThread.cs new file mode 100644 index 0000000..91a69c6 --- /dev/null +++ b/Client/WorldThread.cs @@ -0,0 +1,26 @@ +using System; + +using GTA; +using GTA.Native; + +namespace CoopClient +{ + public class WorldThread : Script + { + public WorldThread() + { + Tick += OnTick; + Interval = 1000 / 60; + } + + public static void OnTick(object sender, EventArgs e) + { + if (Game.IsLoading) + { + return; + } + + Function.Call((Hash)0xB96B00E976BE977F, 0.0f); // _SET_WAVES_INTENSITY + } + } +} diff --git a/Client/packages.config b/Client/packages.config new file mode 100644 index 0000000..bd81797 --- /dev/null +++ b/Client/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/GTACoop.sln b/GTACoop.sln new file mode 100644 index 0000000..f3c7c1e --- /dev/null +++ b/GTACoop.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31423.177 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoopServer", "Server\CoopServer.csproj", "{84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoopClient", "Client\CoopClient.csproj", "{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}.Debug|x64.ActiveCfg = Debug|Any CPU + {84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}.Debug|x64.Build.0 = Debug|Any CPU + {84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}.Release|x64.ActiveCfg = Release|Any CPU + {84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}.Release|x64.Build.0 = Release|Any CPU + {EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Debug|x64.Build.0 = Debug|Any CPU + {EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Release|x64.ActiveCfg = Release|Any CPU + {EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6CC7EA75-E4FF-4534-8EB6-0AEECF2620B7} + EndGlobalSection +EndGlobal diff --git a/Images/LOGO.png b/Images/LOGO.png new file mode 100644 index 0000000..4296928 Binary files /dev/null and b/Images/LOGO.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8709f6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Nick-I. A. (EntenKoeniq) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Libs/Release/LemonUI.SHVDN3.dll b/Libs/Release/LemonUI.SHVDN3.dll new file mode 100644 index 0000000..10d7652 Binary files /dev/null and b/Libs/Release/LemonUI.SHVDN3.dll differ diff --git a/Libs/Release/Lidgren.Network.dll b/Libs/Release/Lidgren.Network.dll new file mode 100644 index 0000000..2ac6d2b Binary files /dev/null and b/Libs/Release/Lidgren.Network.dll differ diff --git a/Libs/Release/ScriptHookVDotNet3.dll b/Libs/Release/ScriptHookVDotNet3.dll new file mode 100644 index 0000000..5727489 Binary files /dev/null and b/Libs/Release/ScriptHookVDotNet3.dll differ diff --git a/MasterServer/blocked.txt b/MasterServer/blocked.txt new file mode 100644 index 0000000..e69de29 diff --git a/MasterServer/index.mjs b/MasterServer/index.mjs new file mode 100644 index 0000000..cdefd41 --- /dev/null +++ b/MasterServer/index.mjs @@ -0,0 +1,86 @@ +import { createServer } from 'net'; +import { readFileSync } from 'fs'; + +var serverList = []; +const blockedIps = readFileSync('blocked.txt', 'utf-8').split('\n'); + +const server = createServer(); + +server.on('connection', (socket) => +{ + if (blockedIps.includes(socket.remoteAddress)) + { + console.log(`IP '${socket.remoteAddress}' blocked`); + socket.destroy(); + return; + } + + var lastData = 0; + + const remoteAddress = socket.remoteAddress + ":" + socket.remotePort; + + socket.on('data', async (data) => + { + // NOT SPAM! + if (lastData !== 0 && (Date.now() - lastData) < 14500) + { + console.log("[WARNING] Spam from %s", remoteAddress); + socket.destroy(); + return; + } + + lastData = Date.now(); + + var incomingMessage; + try + { + incomingMessage = await JSON.parse(data.toString()); + } + catch + { + socket.destroy(); + return; + } + + if (incomingMessage.method) + { + if (incomingMessage.method === 'POST' && incomingMessage.data) + { + // Check if the server is already in the serverList + const alreadyExist = serverList.some((val) => + { + const found = val.remoteAddress === remoteAddress; + + if (found) + { + // Replace old data with new data + val.data = { ...val.data, ...incomingMessage.data }; + } + + return found; + }); + + // Server doesn't exist in serverList so add the server + if (!alreadyExist) + { + serverList.push({ remoteAddress: remoteAddress, data: incomingMessage.data }); + } + return; + } + else if (incomingMessage.method === 'GET') + { + socket.write(JSON.stringify(serverList)); + return; + } + } + + // method or data does not exist or method is not POST or GET + socket.destroy(); + }); + + socket.on('close', () => serverList = serverList.filter(val => val.remoteAddress !== remoteAddress)); + + socket.on('error', (e) => { /*console.error(e)*/ }); +}); + +server.listen(11000, () => console.log("MasterServer started!")); \ No newline at end of file diff --git a/MasterServer/package.json b/MasterServer/package.json new file mode 100644 index 0000000..88c8376 --- /dev/null +++ b/MasterServer/package.json @@ -0,0 +1,11 @@ +{ + "name": "masterserver", + "version": "1.0.0", + "description": "", + "main": "index.mjs", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "EntenKoeniq", + "license": "MIT" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc3aebe --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +

+ GTACoop:R Image +

+ +# 🌐 GTACoop:R +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] + +| :warning: The original GTACoop can be found [HERE](https://gtacoop.com/) | +| --- | + +This modification was completely rewritten and NOT revised + +# 📋 Requirements +- Visual Studio 2022 + - Untested on other development environments +- .NET 6.0 & Framework 4.8 + +# 📚 Libraries +- [ScriptHookVDotNet3](https://github.com/crosire/scripthookvdotnet/tree/0333095099a20a266c4f17dc52d21c608d1082de) +- [LemonUI.SHVDN3](https://github.com/justalemon/LemonUI/tree/a29f73120fc4f473cdfd14104aaef77f1a1b76e5) +- [Lidgren Network](https://github.com/lidgren/lidgren-network-gen3/tree/f99b006d9af8a9a230ba7c5ce0320fc727ebae0c) +- [Protobuf-net](https://www.nuget.org/packages/protobuf-net/2.4.6) + +# ♻️ Synchronization +- Player & Npc + - Model + - Props + - Health + - Movement (Not finished yet) + - Weapons (Not finished yet) + +# 📝 License +This project is licensed under [MIT license](https://github.com/EntenKoeniq/GTACoop-R/blob/main/LICENSE) + +[contributors-shield]: https://img.shields.io/github/contributors/EntenKoeniq/GTACoop-R.svg?style=for-the-badge +[contributors-url]: https://github.com/EntenKoeniq/GTACoop-R/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/EntenKoeniq/GTACoop-R.svg?style=for-the-badge +[forks-url]: https://github.com/EntenKoeniq/GTACoop-R/network/members +[stars-shield]: https://img.shields.io/github/stars/EntenKoeniq/GTACoop-R.svg?style=for-the-badge +[stars-url]: https://github.com/EntenKoeniq/GTACoop-R/stargazers +[issues-shield]: https://img.shields.io/github/issues/EntenKoeniq/GTACoop-R.svg?style=for-the-badge +[issues-url]: https://github.com/EntenKoeniq/GTACoop-R/issues diff --git a/Server/Allowlist.cs b/Server/Allowlist.cs new file mode 100644 index 0000000..1179313 --- /dev/null +++ b/Server/Allowlist.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace CoopServer +{ + public class Allowlist + { + public List SocialClubName { get; set; } = new(); + } +} diff --git a/Server/Blocklist.cs b/Server/Blocklist.cs new file mode 100644 index 0000000..0efe5c0 --- /dev/null +++ b/Server/Blocklist.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace CoopServer +{ + public class Blocklist + { + public List SocialClubName { get; set; } = new(); + public List Username { get; set; } = new(); + public List IP { get; set; } = new(); + } +} diff --git a/Server/CoopServer.csproj b/Server/CoopServer.csproj new file mode 100644 index 0000000..d85ab8d --- /dev/null +++ b/Server/CoopServer.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + + + + + + + + + ..\Libs\Release\Lidgren.Network.dll + + + + diff --git a/Server/Entities/EntitiesPed.cs b/Server/Entities/EntitiesPed.cs new file mode 100644 index 0000000..2316779 --- /dev/null +++ b/Server/Entities/EntitiesPed.cs @@ -0,0 +1,12 @@ +namespace CoopServer.Entities +{ + struct EntitiesPed + { + public LVector3 Position { get; set; } + + public bool IsInRangeOf(LVector3 position, float distance) + { + return LVector3.Subtract(Position, position).Length() < distance; + } + } +} diff --git a/Server/Entities/EntitiesPlayer.cs b/Server/Entities/EntitiesPlayer.cs new file mode 100644 index 0000000..2bc6f6b --- /dev/null +++ b/Server/Entities/EntitiesPlayer.cs @@ -0,0 +1,9 @@ +namespace CoopServer.Entities +{ + class EntitiesPlayer + { + public string SocialClubName { get; set; } + public string Username { get; set; } + public EntitiesPed Ped = new(); + } +} diff --git a/Server/Logging.cs b/Server/Logging.cs new file mode 100644 index 0000000..4760639 --- /dev/null +++ b/Server/Logging.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; + +namespace CoopServer +{ + class Logging + { + private static readonly object _lock = new(); + + public static void Info(string message) + { + lock (_lock) + { + string msg = string.Format("[{0}] [INFO] {1}", Date(), message); + + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(msg); + Console.ResetColor(); + + using StreamWriter sw = new("log.txt", true); + sw.WriteLine(msg); + } + } + + public static void Warning(string message) + { + lock (_lock) + { + string msg = string.Format("[{0}] [WARNING] {1}", Date(), message); + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg); + Console.ResetColor(); + + using StreamWriter sw = new("log.txt", true); + sw.WriteLine(msg); + } + } + + public static void Error(string message) + { + lock (_lock) + { + string msg = string.Format("[{0}] [ERROR] {1}", Date(), message); + + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(msg); + Console.ResetColor(); + + using StreamWriter sw = new("log.txt", true); + sw.WriteLine(msg); + } + } + + public static void Debug(string message) + { + if (!Server.MainSettings.DebugMode) + { + return; + } + + lock (_lock) + { + string msg = string.Format("[{0}] [DEBUG] {1}", Date(), message); + + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(msg); + Console.ResetColor(); + + using StreamWriter sw = new("log.txt", true); + sw.WriteLine(msg); + } + } + private static string Date() + { + return DateTime.Now.ToString(); + } + } +} diff --git a/Server/Packets.cs b/Server/Packets.cs new file mode 100644 index 0000000..affa92c --- /dev/null +++ b/Server/Packets.cs @@ -0,0 +1,459 @@ +using System; +using System.IO; +using System.Collections.Generic; + +using Lidgren.Network; +using ProtoBuf; + +namespace CoopServer +{ + [ProtoContract] + public struct LVector3 + { + public LVector3(float X, float Y, float Z) + { + this.X = X; + this.Y = Y; + this.Z = Z; + } + + [ProtoMember(1)] + public float X { get; set; } + + [ProtoMember(2)] + public float Y { get; set; } + + [ProtoMember(3)] + public float Z { get; set; } + + #region SERVER-ONLY + public float Length() => (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z)); + public static LVector3 Subtract(LVector3 pos1, LVector3 pos2) => new(pos1.X - pos2.X, pos1.Y - pos2.Y, pos1.Z - pos2.Z); + #endregion + } + + public enum ModVersion + { + V0_1_0 + } + + public enum PacketTypes + { + HandshakePacket, + PlayerConnectPacket, + PlayerDisconnectPacket, + FullSyncPlayerPacket, + FullSyncNpcPacket, + LightSyncPlayerPacket, + ChatMessagePacket + } + + [Flags] + public enum PedDataFlags + { + LastSyncWasFull = 1 << 0, + IsAiming = 1 << 1, + IsShooting = 1 << 2, + IsReloading = 1 << 3, + IsJumping = 1 << 4, + IsRagdoll = 1 << 5, + IsOnFire = 1 << 6 + } + + public interface IPacket + { + void PacketToNetOutGoingMessage(NetOutgoingMessage message); + void NetIncomingMessageToPacket(NetIncomingMessage message); + } + + public abstract class Packet : IPacket + { + public abstract void PacketToNetOutGoingMessage(NetOutgoingMessage message); + public abstract void NetIncomingMessageToPacket(NetIncomingMessage message); + } + + [ProtoContract] + public class HandshakePacket : Packet + { + [ProtoMember(1)] + public string ID { get; set; } + + [ProtoMember(2)] + public string SocialClubName { get; set; } + + [ProtoMember(3)] + public string Username { get; set; } + + [ProtoMember(4)] + public string ModVersion { get; set; } + + [ProtoMember(5)] + public bool NpcsAllowed { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.HandshakePacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + HandshakePacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + ID = data.ID; + SocialClubName = data.SocialClubName; + Username = data.Username; + ModVersion = data.ModVersion; + NpcsAllowed = data.NpcsAllowed; + } + } + + [ProtoContract] + public class PlayerConnectPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public string SocialClubName { get; set; } + + [ProtoMember(3)] + public string Username { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.PlayerConnectPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + PlayerConnectPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + SocialClubName = data.SocialClubName; + Username = data.Username; + } + } + + [ProtoContract] + public class PlayerDisconnectPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.PlayerDisconnectPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + PlayerDisconnectPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + } + } + + [ProtoContract] + public class FullSyncPlayerPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public int ModelHash { get; set; } + + [ProtoMember(3)] + public Dictionary Props { get; set; } + + [ProtoMember(4)] + public int Health { get; set; } + + [ProtoMember(5)] + public LVector3 Position { get; set; } + + [ProtoMember(6)] + public LVector3 Rotation { get; set; } + + [ProtoMember(7)] + public LVector3 Velocity { get; set; } + + [ProtoMember(8)] + public byte Speed { get; set; } + + [ProtoMember(9)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(10)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(11)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.FullSyncPlayerPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + FullSyncPlayerPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + ModelHash = data.ModelHash; + Props = data.Props; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class FullSyncNpcPacket : Packet + { + [ProtoMember(1)] + public string ID { get; set; } + + [ProtoMember(2)] + public int ModelHash { get; set; } + + [ProtoMember(3)] + public Dictionary Props { get; set; } + + [ProtoMember(4)] + public int Health { get; set; } + + [ProtoMember(5)] + public LVector3 Position { get; set; } + + [ProtoMember(6)] + public LVector3 Rotation { get; set; } + + [ProtoMember(7)] + public LVector3 Velocity { get; set; } + + [ProtoMember(8)] + public byte Speed { get; set; } + + [ProtoMember(9)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(10)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(11)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.FullSyncNpcPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + FullSyncNpcPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + ID = data.ID; + ModelHash = data.ModelHash; + Props = data.Props; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class LightSyncPlayerPacket : Packet + { + [ProtoMember(1)] + public string Player { get; set; } + + [ProtoMember(2)] + public int Health { get; set; } + + [ProtoMember(3)] + public LVector3 Position { get; set; } + + [ProtoMember(4)] + public LVector3 Rotation { get; set; } + + [ProtoMember(5)] + public LVector3 Velocity { get; set; } + + [ProtoMember(6)] + public byte Speed { get; set; } + + [ProtoMember(7)] + public LVector3 AimCoords { get; set; } + + [ProtoMember(8)] + public int CurrentWeaponHash { get; set; } + + [ProtoMember(9)] + public byte? Flag { get; set; } = 0; + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.LightSyncPlayerPacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + LightSyncPlayerPacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Player = data.Player; + Health = data.Health; + Position = data.Position; + Rotation = data.Rotation; + Velocity = data.Velocity; + Speed = data.Speed; + AimCoords = data.AimCoords; + CurrentWeaponHash = data.CurrentWeaponHash; + Flag = data.Flag; + } + } + + [ProtoContract] + public class ChatMessagePacket : Packet + { + [ProtoMember(1)] + public string Username { get; set; } + + [ProtoMember(2)] + public string Message { get; set; } + + public override void PacketToNetOutGoingMessage(NetOutgoingMessage message) + { + message.Write((byte)PacketTypes.ChatMessagePacket); + + byte[] result; + using (MemoryStream stream = new MemoryStream()) + { + Serializer.Serialize(stream, this); + result = stream.ToArray(); + } + + message.Write(result.Length); + message.Write(result); + } + + public override void NetIncomingMessageToPacket(NetIncomingMessage message) + { + int len = message.ReadInt32(); + + ChatMessagePacket data; + using (MemoryStream stream = new MemoryStream(message.ReadBytes(len))) + { + data = Serializer.Deserialize(stream); + } + + Username = data.Username; + Message = data.Message; + } + } +} diff --git a/Server/Program.cs b/Server/Program.cs new file mode 100644 index 0000000..668d186 --- /dev/null +++ b/Server/Program.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +namespace CoopServer +{ + class Program + { + static void Main(string[] args) + { + try + { + Console.Title = "GTACoop:R Server"; + + if (File.Exists("log.txt")) + { + File.WriteAllText("log.txt", string.Empty); + } + + _ = new Server(); + } + catch (Exception e) + { + Logging.Error(e.ToString()); + Console.ReadLine(); + } + } + } +} diff --git a/Server/Server.cs b/Server/Server.cs new file mode 100644 index 0000000..b721f00 --- /dev/null +++ b/Server/Server.cs @@ -0,0 +1,500 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Net; +using System.Net.Sockets; +using System.Text; + +using Lidgren.Network; + +using CoopServer.Entities; + +namespace CoopServer +{ + class MasterServer + { + private Thread MainThread; + + public void Start() + { + MainThread = new Thread(Listen); + MainThread.Start(); + } + + private void Listen() + { + try + { + IPHostEntry host = Dns.GetHostEntry(Server.MainSettings.MasterServer); + IPAddress ipAddress = host.AddressList[0]; + IPEndPoint remoteEP = new(ipAddress, 11000); + + // Create a TCP/IP socket + Socket sender = new(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + sender.Connect(remoteEP); + + Logging.Info("Server connected to MasterServer"); + + while (sender.Connected) + { + // Encode the data string into a byte array + byte[] msg = Encoding.ASCII.GetBytes( + "{ \"method\": \"POST\", \"data\": { " + + "\"Port\": \"" + Server.MainSettings.ServerPort + "\", " + + "\"Name\": \"" + Server.MainSettings.ServerName + "\", " + + "\"Version\": \"" + Server.CurrentModVersion.Replace("_", ".") + "\", " + + "\"Players\": " + Server.MainNetServer.ConnectionsCount + ", " + + "\"MaxPlayers\": " + Server.MainSettings.MaxPlayers + ", " + + "\"NpcsAllowed\": \"" + Server.MainSettings.NpcsAllowed + "\" } }"); + + // Send the data + sender.Send(msg); + + // Sleep for 15 seconds + Thread.Sleep(15000); + } + } + catch (SocketException se) + { + Logging.Error(se.Message); + } + catch (Exception e) + { + Logging.Error(e.Message); + } + } + } + + class Server + { + public static readonly string CurrentModVersion = Enum.GetValues(typeof(ModVersion)).Cast().Last().ToString(); + + public static readonly Settings MainSettings = Util.Read("CoopSettings.xml"); + private readonly Blocklist MainBlocklist = Util.Read("Blocklist.xml"); + private readonly Allowlist MainAllowlist = Util.Read("Allowlist.xml"); + + public static NetServer MainNetServer; + + private readonly MasterServer MainMasterServer = new(); + + private static readonly Dictionary Players = new(); + + public Server() + { + // 6d4ec318f1c43bd62fe13d5a7ab28650 = GTACOOP:R + NetPeerConfiguration config = new("6d4ec318f1c43bd62fe13d5a7ab28650") + { + MaximumConnections = MainSettings.MaxPlayers, + Port = MainSettings.ServerPort + }; + + config.EnableMessageType(NetIncomingMessageType.ConnectionApproval); + + MainNetServer = new NetServer(config); + MainNetServer.Start(); + + Logging.Info(string.Format("Server listening on {0}:{1}", config.LocalAddress.ToString(), config.Port)); + + if (MainSettings.AnnounceSelf) + { + MainMasterServer.Start(); + } + + Listen(); + } + + private void Listen() + { + Logging.Info("Listening for clients"); + + while (!Console.KeyAvailable || Console.ReadKey().Key != ConsoleKey.Escape) + { + // 16 milliseconds to sleep to reduce CPU usage + Thread.Sleep(1000 / 60); + + NetIncomingMessage message; + + while ((message = MainNetServer.ReadMessage()) != null) + { + switch (message.MessageType) + { + case NetIncomingMessageType.ConnectionApproval: + Logging.Info("New incoming connection from: " + message.SenderConnection.RemoteEndPoint.ToString()); + if (message.ReadByte() != (byte)PacketTypes.HandshakePacket) + { + message.SenderConnection.Deny("Wrong packet!"); + } + else + { + Packet approvalPacket; + approvalPacket = new HandshakePacket(); + approvalPacket.NetIncomingMessageToPacket(message); + GetHandshake(message.SenderConnection, (HandshakePacket)approvalPacket); + } + break; + case NetIncomingMessageType.StatusChanged: + NetConnectionStatus status = (NetConnectionStatus)message.ReadByte(); + + string reason = message.ReadString(); + string player = NetUtility.ToHexString(message.SenderConnection.RemoteUniqueIdentifier); + //Logging.Debug(NetUtility.ToHexString(message.SenderConnection.RemoteUniqueIdentifier) + " " + status + ": " + reason); + + switch (status) + { + case NetConnectionStatus.Connected: + //Logging.Info("New incoming connection from: " + message.SenderConnection.RemoteEndPoint.ToString()); + break; + case NetConnectionStatus.Disconnected: + if (Players.ContainsKey(player)) + { + SendPlayerDisconnectPacket(new PlayerDisconnectPacket() { Player = player }, reason); + } + break; + } + break; + case NetIncomingMessageType.Data: + // Get packet type + byte type = message.ReadByte(); + + // Create packet + Packet packet; + + switch (type) + { + case (byte)PacketTypes.PlayerConnectPacket: + packet = new PlayerConnectPacket(); + packet.NetIncomingMessageToPacket(message); + SendPlayerConnectPacket(message.SenderConnection, (PlayerConnectPacket)packet); + break; + case (byte)PacketTypes.PlayerDisconnectPacket: + packet = new PlayerDisconnectPacket(); + packet.NetIncomingMessageToPacket(message); + SendPlayerDisconnectPacket((PlayerDisconnectPacket)packet); + break; + case (byte)PacketTypes.FullSyncPlayerPacket: + packet = new FullSyncPlayerPacket(); + packet.NetIncomingMessageToPacket(message); + FullSyncPlayer((FullSyncPlayerPacket)packet); + break; + case (byte)PacketTypes.FullSyncNpcPacket: + if (MainSettings.NpcsAllowed) + { + packet = new FullSyncNpcPacket(); + packet.NetIncomingMessageToPacket(message); + FullSyncNpc(message.SenderConnection, (FullSyncNpcPacket)packet); + } + else + { + Logging.Warning(Players[NetUtility.ToHexString(message.SenderConnection.RemoteUniqueIdentifier)].Username + " tries to send Npcs!"); + message.SenderConnection.Disconnect("Npcs are not allowed!"); + } + break; + case (byte)PacketTypes.LightSyncPlayerPacket: + packet = new LightSyncPlayerPacket(); + packet.NetIncomingMessageToPacket(message); + LightSyncPlayer((LightSyncPlayerPacket)packet); + break; + case (byte)PacketTypes.ChatMessagePacket: + packet = new ChatMessagePacket(); + packet.NetIncomingMessageToPacket(message); + SendChatMessage((ChatMessagePacket)packet); + break; + default: + Logging.Error("Unhandled Data / Packet type"); + break; + } + break; + case NetIncomingMessageType.ErrorMessage: + Logging.Error(message.ReadString()); + break; + case NetIncomingMessageType.WarningMessage: + Logging.Warning(message.ReadString()); + break; + case NetIncomingMessageType.DebugMessage: + case NetIncomingMessageType.VerboseDebugMessage: + Logging.Debug(message.ReadString()); + break; + default: + Logging.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType, message.LengthBytes, message.DeliveryMethod, message.SequenceChannel)); + break; + } + + MainNetServer.Recycle(message); + } + } + } + + // Return a list of all connections but not the local connection + private static List FilterAllLocal(string local) + { + return new List(MainNetServer.Connections.FindAll(e => !NetUtility.ToHexString(e.RemoteUniqueIdentifier).Equals(local))); + } + + // Get all players in range of ... + private static List GetAllInRange(LVector3 position, float range, string local = null) + { + if (local == null) + { + return new List(MainNetServer.Connections.FindAll(e => Players[NetUtility.ToHexString(e.RemoteUniqueIdentifier)].Ped.IsInRangeOf(position, range))); + } + else + { + return new List(MainNetServer.Connections.FindAll(e => + { + string target = NetUtility.ToHexString(e.RemoteUniqueIdentifier); + return target != local && Players[target].Ped.IsInRangeOf(position, range); + })); + } + } + + // Before we approve the connection, we must shake hands + private void GetHandshake(NetConnection local, HandshakePacket packet) + { + string localPlayerID = NetUtility.ToHexString(local.RemoteUniqueIdentifier); + + Logging.Debug("New handshake from: [" + packet.SocialClubName + " | " + packet.Username + "]"); + + if (string.IsNullOrWhiteSpace(packet.Username)) + { + local.Deny("Username is empty or contains spaces!"); + return; + } + else if (packet.Username.Any(p => !char.IsLetterOrDigit(p))) + { + local.Deny("Username contains special chars!"); + return; + } + + if (MainSettings.Allowlist) + { + if (!MainAllowlist.SocialClubName.Contains(packet.SocialClubName)) + { + local.Deny("This Social Club name is not on the allow list!"); + return; + } + } + + if (packet.ModVersion != CurrentModVersion) + { + local.Deny("Please update GTACoop:R to " + CurrentModVersion.Replace("_", ".")); + return; + } + + if (MainBlocklist.SocialClubName.Contains(packet.SocialClubName)) + { + local.Deny("This Social Club name has been blocked by this server!"); + return; + } + else if (MainBlocklist.Username.Contains(packet.Username)) + { + local.Deny("This Username has been blocked by this server!"); + return; + } + else if (MainBlocklist.IP.Contains(local.RemoteEndPoint.ToString().Split(":")[0])) + { + local.Deny("This IP was blocked by this server!"); + return; + } + + foreach (KeyValuePair player in Players) + { + if (player.Value.SocialClubName == packet.SocialClubName) + { + local.Deny("The name of the Social Club is already taken!"); + return; + } + else if (player.Value.Username == packet.Username) + { + local.Deny("Username is already taken!"); + return; + } + } + + // Add the player to Players + Players.Add(localPlayerID, + new EntitiesPlayer() + { + SocialClubName = packet.SocialClubName, + Username = packet.Username + } + ); + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + + // Create a new handshake packet + new HandshakePacket() + { + ID = localPlayerID, + SocialClubName = string.Empty, + Username = string.Empty, + ModVersion = string.Empty, + NpcsAllowed = MainSettings.NpcsAllowed + }.PacketToNetOutGoingMessage(outgoingMessage); + + // Accept the connection and send back a new handshake packet with the connection ID + local.Approve(outgoingMessage); + + Logging.Info("New player [" + packet.SocialClubName + " | " + packet.Username + "] connected!"); + } + + // The connection has been approved, now we need to send all other players to the new player and the new player to all players + private static void SendPlayerConnectPacket(NetConnection local, PlayerConnectPacket packet) + { + if (!string.IsNullOrEmpty(MainSettings.WelcomeMessage)) + { + SendChatMessage(new ChatMessagePacket() { Username = "Server", Message = MainSettings.WelcomeMessage }, new List() { local }); + } + + List playerList = FilterAllLocal(packet.Player); + if (playerList.Count == 0) + { + return; + } + + // Send all players to local + playerList.ForEach(targetPlayer => + { + string targetPlayerID = NetUtility.ToHexString(targetPlayer.RemoteUniqueIdentifier); + + EntitiesPlayer targetEntity = Players[targetPlayerID]; + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new PlayerConnectPacket() + { + Player = targetPlayerID, + SocialClubName = targetEntity.SocialClubName, + Username = targetEntity.Username + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, local, NetDeliveryMethod.ReliableOrdered, 0); + }); + + // Send local to all players + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new PlayerConnectPacket() + { + Player = packet.Player, + SocialClubName = Players[packet.Player].SocialClubName, + Username = Players[packet.Player].Username + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, playerList, NetDeliveryMethod.ReliableOrdered, 0); + } + + // Send all players a message that someone has left the server + private static void SendPlayerDisconnectPacket(PlayerDisconnectPacket packet, string reason = "Disconnected") + { + List playerList = FilterAllLocal(packet.Player); + + if (playerList.Count != 0) + { + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + packet.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, playerList, NetDeliveryMethod.ReliableOrdered, 0); + } + + Logging.Info(Players[packet.Player].Username + " left the server, reason: " + reason); + Players.Remove(packet.Player); + } + + private static void FullSyncPlayer(FullSyncPlayerPacket packet) + { + Players[packet.Player].Ped.Position = packet.Position; + + List playerList = FilterAllLocal(packet.Player); + + if (playerList.Count == 0) + { + return; + } + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new FullSyncPlayerPacket() + { + Player = packet.Player, + ModelHash = packet.ModelHash, + Props = packet.Props, + Health = packet.Health, + Position = packet.Position, + Rotation = packet.Rotation, + Velocity = packet.Velocity, + Speed = packet.Speed, + AimCoords = packet.AimCoords, + CurrentWeaponHash = packet.CurrentWeaponHash, + Flag = packet.Flag + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, playerList, NetDeliveryMethod.ReliableOrdered, 0); + } + + private static void FullSyncNpc(NetConnection local, FullSyncNpcPacket packet) + { + List playerList = GetAllInRange(packet.Position, 300f, NetUtility.ToHexString(local.RemoteUniqueIdentifier)); + + // No connection found in this area + if (playerList.Count == 0) + { + return; + } + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new FullSyncNpcPacket() + { + ID = packet.ID, + ModelHash = packet.ModelHash, + Props = packet.Props, + Health = packet.Health, + Position = packet.Position, + Rotation = packet.Rotation, + Velocity = packet.Velocity, + Speed = packet.Speed, + AimCoords = packet.AimCoords, + CurrentWeaponHash = packet.CurrentWeaponHash, + Flag = packet.Flag + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, playerList, NetDeliveryMethod.ReliableOrdered, 0); + } + + private static void LightSyncPlayer(LightSyncPlayerPacket packet) + { + Players[packet.Player].Ped.Position = packet.Position; + + List playerList = FilterAllLocal(packet.Player); + + if (playerList.Count == 0) + { + return; + } + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new FullSyncPlayerPacket() + { + Player = packet.Player, + Health = packet.Health, + Position = packet.Position, + Rotation = packet.Rotation, + Velocity = packet.Velocity, + Speed = packet.Speed, + AimCoords = packet.AimCoords, + CurrentWeaponHash = packet.CurrentWeaponHash, + Flag = packet.Flag + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, playerList, NetDeliveryMethod.ReliableOrdered, 0); + } + + // Send a message to targets or all players + private static void SendChatMessage(ChatMessagePacket packet, List targets = null) + { + string filteredMessage = packet.Message.Replace("~", ""); + + Logging.Info(packet.Username + ": " + filteredMessage); + + NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage(); + new ChatMessagePacket() + { + Username = packet.Username, + Message = filteredMessage + }.PacketToNetOutGoingMessage(outgoingMessage); + MainNetServer.SendMessage(outgoingMessage, targets ?? MainNetServer.Connections, NetDeliveryMethod.ReliableOrdered, 0); + } + } +} diff --git a/Server/Settings.cs b/Server/Settings.cs new file mode 100644 index 0000000..38dc0de --- /dev/null +++ b/Server/Settings.cs @@ -0,0 +1,15 @@ +namespace CoopServer +{ + public class Settings + { + public int ServerPort { get; set; } = 4499; + public int MaxPlayers { get; set; } = 16; + public string ServerName { get; set; } = "GTACoop:R server"; + public string WelcomeMessage { get; set; } = "Welcome on this server :)"; + public bool Allowlist { get; set; } = false; + public bool NpcsAllowed { get; set; } = true; + public string MasterServer { get; set; } = "localhost"; + public bool AnnounceSelf { get; set; } = true; + public bool DebugMode { get; set; } = false; + } +} diff --git a/Server/Util.cs b/Server/Util.cs new file mode 100644 index 0000000..a88d20a --- /dev/null +++ b/Server/Util.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Xml.Serialization; + +namespace CoopServer +{ + class Util + { + public static T Read(string file) where T : new() + { + XmlSerializer ser = new(typeof(T)); + + string path = Directory.GetCurrentDirectory() + "\\" + file; + T settings; + + if (File.Exists(path)) + { + using (FileStream stream = File.OpenRead(path)) + { + settings = (T)ser.Deserialize(stream); + } + + using (FileStream stream = new(path, File.Exists(path) ? FileMode.Truncate : FileMode.Create, FileAccess.ReadWrite)) + { + ser.Serialize(stream, settings); + } + } + else + { + using FileStream stream = File.OpenWrite(path); + ser.Serialize(stream, settings = new T()); + } + + return settings; + } + } +}