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
+[![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;
+ }
+ }
+}