116 Commits

Author SHA1 Message Date
87d8102a1a Release profile 2022-06-27 13:32:06 +08:00
bd1f0e1713 Worker 2022-06-27 13:30:35 +08:00
04c9081851 Security fix and merge LemonUI 2022-06-27 13:02:31 +08:00
9cf23cac7f never gonna give~ 2022-06-24 18:25:24 +08:00
46f75c5a21 Update README.md 2022-06-24 16:59:41 +08:00
6167417bf8 Reworked on resource loading, Added RegisterCommands overload to register commands from an instance. 2022-06-24 16:49:59 +08:00
82ab9237f5 Security implementation 2022-06-24 10:33:36 +08:00
82e7cdd785 Multiple server instances in one process is now possible 2022-06-23 14:10:16 +08:00
41385abc7d Rework on SendNativeCall 2022-06-23 09:46:38 +08:00
84171e2949 We're almost here... 2022-06-22 14:18:20 +08:00
42c038d8ab An attempt to fix #12, along with some cleanup and refactor 2022-06-22 10:29:41 +08:00
fe6b42e509 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-22 10:05:11 +08:00
8bc5c11030 Force terminate. 2022-06-22 10:03:45 +08:00
76f7157749 Update README.md 2022-06-22 09:58:18 +08:00
52a242c72a Delete ignored assembly 2022-06-22 09:46:36 +08:00
8a42f7c626 Add ServerScript/ClientScript.CurrentResource 2022-06-22 09:45:17 +08:00
6e2c0a3037 Flush before disposal 2022-06-22 09:03:58 +08:00
1b77234cfe Add flushImmediately to logger 2022-06-22 09:00:35 +08:00
a15785a02f Move PlayerData to RageCoop.Client, remove RageCoop.Core.Logging 2022-06-22 08:58:36 +08:00
1a5f594b36 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-22 08:28:11 +08:00
68b6295126 fix 2022-06-22 08:28:08 +08:00
17d6d86194 Oops 2022-06-21 23:52:30 +08:00
f82134fb12 Fix Client ID not set 2022-06-21 23:43:12 +08:00
37edef425c Add OnPlayerUpdate 2022-06-21 19:06:50 +08:00
30f7a281b0 Resource system 2022-06-21 18:13:30 +08:00
e1606889f7 Add stealth mode sync 2022-06-20 11:08:46 +08:00
33244b930f Remove CLSCompliant attribute 2022-06-20 10:55:12 +08:00
212c004d31 Add license plate sync 2022-06-20 10:27:45 +08:00
190f23240a PopUp 2022-06-19 15:04:38 +08:00
376830d6dc Add RoofState sync. 2022-06-19 13:36:23 +08:00
9a9fb2870c Add weapon tint sync 2022-06-19 11:47:39 +08:00
2a093d1da0 Fix radio not changed when entering vehicle 2022-06-19 11:22:45 +08:00
1a66f14bae Fix component for mp models 2022-06-19 11:12:20 +08:00
887e7f428d shhh~ 2022-06-18 12:06:22 +08:00
cc4f5f46ce Urgh... 2022-06-18 12:04:31 +08:00
13883242e4 Add ped cloth texture and palette sync, closes #11 2022-06-17 19:14:56 +08:00
09a97c43fd Spread out state, networking statistics fix 2022-06-17 18:11:39 +08:00
671b4f9f6f Mixed rotation calculation 2022-06-17 15:26:27 +08:00
1ae913f0d2 Fix vehicle projectile 2022-06-17 11:33:53 +08:00
45f487c9f4 Better weapon damage reproduction 2022-06-17 11:08:49 +08:00
7f1891236d Fix projectile issue. 2022-06-16 11:57:48 +08:00
1c981c331c Blah 2022-06-14 09:46:47 +08:00
0c9213936d Back to old projectile sync 2022-06-14 09:40:55 +08:00
188556991f Don't flush 2022-06-12 18:02:01 +08:00
b6450027b8 Don't flush 2022-06-12 18:00:42 +08:00
82cffea6ed Some fixes 2022-06-12 17:11:14 +08:00
734788f310 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-12 15:39:42 +08:00
45c3ff83cf API cleanup, prepare for CustomEvent, fix server not shutting down 2022-06-12 15:39:32 +08:00
c7655573aa Add linux binary for nightly build 2022-06-12 15:07:10 +08:00
88a4f046d4 Many poops.
Basically working resource system
Improved projectile movement
Better receive messages
Include Lidgren.Network in core
2022-06-11 18:41:10 +08:00
7ccccabe1d Update README.md 2022-06-07 08:38:47 +08:00
3d6dbd5f2a Update nightly-build.yaml 2022-06-07 08:31:25 +08:00
9407d80858 Update nightly-build.yaml 2022-06-07 08:23:16 +08:00
51eaf4f56d Update nightly-build.yaml 2022-06-07 08:11:58 +08:00
4b5ec37987 Update nightly-build.yaml 2022-06-06 20:34:05 +08:00
3f246a8f7d Update nightly-build.yaml 2022-06-06 20:07:57 +08:00
7f4833be81 Update nightly-build.yaml 2022-06-06 18:32:02 +08:00
755615a887 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-06 18:29:58 +08:00
ef32603801 Update nightly-build.yaml 2022-06-06 18:29:50 +08:00
fa80b87f7a Change folder name 2022-06-06 18:27:19 +08:00
f8b7062c61 Update nightly-build.yaml 2022-06-06 17:53:03 +08:00
95c4c42ff0 Rename nightly-build to nightly-build.yaml 2022-06-06 17:50:20 +08:00
13fa206594 Create nightly-build 2022-06-06 17:49:54 +08:00
a1dda2ffa4 Fixed three players issue, some work on API and resources management. 2022-06-06 17:37:11 +08:00
0914afc4ed OK that's enough for today... 2022-06-04 18:16:32 +08:00
9f2cf2eb03 Scripting prototype? 2022-06-04 18:09:42 +08:00
1607b7025c Yeah I figured it out... 2022-06-04 14:13:08 +08:00
2c0b7d4d64 Update version 2022-06-04 12:31:45 +08:00
62dc98f4a7 Remove unnecessary packages 2022-06-04 12:25:01 +08:00
d061b5ecbe Fix NullReferenceException on start. 2022-06-04 12:04:02 +08:00
546f4b16f4 No, 0.5 is better. 2022-06-03 17:56:36 +08:00
5a455d0487 Auto fetch master server address 2022-06-03 16:28:02 +08:00
11d178498f Back to old rotation calculation. 2022-06-03 14:40:41 +08:00
88a51cc154 Add radio station sync. 2022-06-03 13:11:17 +08:00
291ba5691b Request WeaponAsset before swapping weapon. 2022-06-02 16:40:38 +08:00
de63cd1271 Add Tank muzzle flash 2022-06-02 15:51:58 +08:00
8479502d75 Fix scramjet 2022-06-02 14:10:29 +08:00
051b496021 Two muzzles fix 2022-06-02 13:52:18 +08:00
2c6f0dbe3d Don't delete player's vehicle 2022-06-02 13:11:01 +08:00
ffb73c1ce4 Fix savage 2022-06-02 13:09:13 +08:00
c941ca96fc Projectile and stromberg fix. 2022-06-02 12:44:05 +08:00
e2d4ce2159 Weapon fix 2022-06-02 10:34:33 +08:00
a500a4eeed closes #4, add scramjet. 2022-06-02 09:27:39 +08:00
603e8af746 closes #5, add Vigilante. 2022-06-02 09:26:53 +08:00
0ae4e1c122 closes #6, add Apocalypse 380Z 2022-06-02 09:25:23 +08:00
83f260048d closes #7, add Future Shock 380Z. 2022-06-02 09:24:37 +08:00
9b79956c71 closes #8, add Nightmare 380Z 2022-06-02 09:23:44 +08:00
82c6dab4c4 add stromberg 2022-06-02 09:21:46 +08:00
6611ecac18 closes #10, add apocalypse 2022-06-02 09:17:19 +08:00
3fa62f1a69 closes #3, add savage 2022-06-02 09:15:18 +08:00
ad3031ef59 Logger fix 2022-06-02 08:53:01 +08:00
42d057e2c5 Latency update and threading fixes. 2022-06-01 19:05:45 +08:00
87b9a67d0b Update assemby version 2022-06-01 18:01:38 +08:00
4fca28f0e0 Ah~ 2022-06-01 17:57:54 +08:00
8ebf03ca99 Hmm... 2022-06-01 17:57:27 +08:00
f2760b1533 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-01 17:56:02 +08:00
7eec12486f country road, take me home... 2022-06-01 17:55:38 +08:00
d789e4cb0b Update README.md 2022-06-01 16:27:50 +08:00
485497286f Update README.md
some shit
2022-06-01 16:27:10 +08:00
a305fb349f Update README.md 2022-06-01 16:06:02 +08:00
7aa41a6e0c Master server is back! 2022-05-31 23:12:32 -08:00
9d9d421671 Small fix 2022-05-31 19:46:40 -08:00
a8a16fac61 Scripting? 2022-05-31 19:35:01 -08:00
b863b7037a Added ability to disable automatic respawn
Prepare for client side scripting.
2022-05-31 02:16:12 -08:00
a33f839004 opt 2022-05-31 10:11:06 +08:00
a48170fec8 EnterVehicle fix, reduce NPC 2022-05-31 09:55:54 +08:00
2181a52d6c Add Tula 2022-05-31 09:24:28 +08:00
b96d349e4a Lots of sh*t
API cleanup
Complete deluxo transformation sync
blahblahblah
2022-05-31 09:14:30 +08:00
ad698a656d Removed LVector, LQuaternion.
Server side optimization.
Nework optimization (streaming distance).
2022-05-30 14:32:38 +08:00
aefea337f1 Some optimization. 2022-05-30 11:11:40 +08:00
811e7df14e fix for changing owner. 2022-05-29 18:27:07 +08:00
5cc63e688f Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-05-29 18:16:44 +08:00
08aa1370de Add nozzle and deluxo transformation sync. 2022-05-29 18:16:32 +08:00
5d6b99442b Update README.md 2022-05-27 17:08:40 +08:00
65b695cb78 Update README.md 2022-05-27 17:06:41 +08:00
ce94a9c7af Update README.md 2022-05-27 17:01:31 +08:00
276 changed files with 32251 additions and 56450 deletions

50
.github/workflows/nightly-build.yaml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Nightly-build
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
dotnet-version: ['6.0.x']
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ matrix.dotnet-version }}
- name: Restore dependencies
run: dotnet restore
- name: Build client
run: dotnet build RageCoop.Client/RageCoop.Client.csproj --no-restore --configuration Release -o RageCoop.Client/bin/RageCoop
- name: Build server win-x64
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -r win-x64 -o RageCoop.Server/bin/win-x64 -c Release
- name: Build server linux-x64
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -r linux-x64 -o RageCoop.Server/bin/linux-x64 -c Release
- name: Build server linux-arm
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -r linux-arm -o RageCoop.Server/bin/linux-arm -c Release
- uses: actions/upload-artifact@v3
with:
name: RageCoop.Client
path: RageCoop.Client/bin
- uses: actions/upload-artifact@v3
with:
name: RageCoop.Server-win-x64
path: RageCoop.Server/bin/win-x64
- uses: actions/upload-artifact@v3
with:
name: RageCoop.Server-linux-x64
path: RageCoop.Server/bin/linux-x64
- uses: actions/upload-artifact@v3
with:
name: RageCoop.Server-linux-arm
path: RageCoop.Server/bin/linux-arm
- uses: actions/checkout@v2

View File

@ -1,270 +0,0 @@
#undef DEBUG
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using RageCoop.Core;
namespace RageCoop.Client
{
/// <summary>
/// ?
/// </summary>
public static class COOPAPI
{
#region DELEGATES
/// <summary>
/// ?
/// </summary>
/// <param name="connected"></param>
/// <param name="from">The Lidgren-Network net handle</param>
/// <param name="reason"></param>
public delegate void ConnectEvent(bool connected, long from, string reason = null);
/// <summary>
/// ?
/// </summary>
/// <param name="from"></param>
/// <param name="message">The Lidgren-Network net handle</param>
/// <param name="args"></param>
public delegate void ChatMessage(string from, string message, CancelEventArgs args);
/// <summary>
/// ?
/// </summary>
/// <param name="from">The Lidgren-Network net handle</param>
/// <param name="mod"></param>
/// <param name="customID"></param>
/// <param name="bytes"></param>
public delegate void ModEvent(long from, string mod, byte customID, byte[] bytes);
#endregion
#region EVENTS
/// <summary>
/// ?
/// </summary>
public static event ConnectEvent OnConnection;
/// <summary>
/// ?
/// </summary>
public static event ChatMessage OnChatMessage;
/// <summary>
/// ?
/// </summary>
public static event ModEvent OnModPacketReceived;
public static void Connected()
{
OnConnection?.Invoke(true, GetPlayerID());
}
public static void Disconnected(string reason)
{
OnConnection?.Invoke(false, GetPlayerID(), reason);
}
public static void Connected(long netHandle)
{
OnConnection?.Invoke(true, netHandle);
}
public static void Disconnected(long netHandle)
{
OnConnection?.Invoke(false, netHandle);
}
public static void ModPacketReceived(long from, string mod, byte customID, byte[] bytes)
{
OnModPacketReceived?.Invoke(from, mod, customID, bytes);
}
public static bool ChatMessageReceived(string from, string message)
{
CancelEventArgs args = new CancelEventArgs(false);
OnChatMessage?.Invoke(from, message, args);
return args.Cancel;
}
#endregion
/// <summary>
/// Send a local chat message to this player
/// </summary>
/// <param name="from">Username of the player who sent this message</param>
/// <param name="message">The player's message</param>
public static void LocalChatMessage(string from, string message)
{
Main.MainChat.AddMessage(from, message);
}
/// <summary>
/// Connect to any server
/// </summary>
/// <param name="serverAddress">The server address to connect. Example: 127.0.0.1:4499</param>
public static void Connect(string serverAddress)
{
Networking.DisConnectFromServer(serverAddress);
}
/// <summary>
/// ?
/// </summary>
public static void Disconnect()
{
Networking.DisConnectFromServer(null);
}
/// <summary>
/// Check if the player is already on a server
/// </summary>
public static bool IsOnServer()
{
return Networking.IsOnServer;
}
/// <summary>
/// Get the local player's ID
/// </summary>
/// <returns>PlayerID</returns>
public static long GetPlayerID()
{
return Main.LocalPlayerID;
}
/*
/// <summary>
/// Get a player using their Lidgren Network net handle
/// </summary>
/// <param name="handle">Lidgren-Network net handle</param>
public static CharacterEntity GetPed(int ID)
{
lock (Main.Characters)
{
return Main.Characters.ContainsKey(ID) ? Main.Characters[ID] : null;
}
}
*/
/// <summary>
/// Check if a RAGECOOP menu is visible
/// </summary>
public static bool IsMenuVisible()
{
#if NON_INTERACTIVE
return false;
#else
return Main.MainMenu.MenuPool.AreAnyVisible;
#endif
}
/// <summary>
/// Check if the RAGECOOP chat is visible
/// </summary>
public static bool IsChatFocused()
{
return Main.MainChat.Focused;
}
/// <summary>
/// Check if the RAGECOOP list of players is visible
/// </summary>
public static bool IsPlayerListVisible()
{
return Util.GetTickCount64() - PlayerList.Pressed < 5000;
}
/// <summary>
/// Get the version of RAGECOOP
/// </summary>
public static string GetCurrentVersion()
{
return Main.CurrentVersion;
}
/// <summary>
/// Send any data (bytes) to the server
/// </summary>
/// <param name="modName">The name of this modification (script)</param>
/// <param name="customID">The ID to know what the data is</param>
/// <param name="bytes">Your class, structure or whatever in bytes</param>
public static void SendDataToServer(string modName, byte customID, byte[] bytes)
{
Networking.SendModData(-1, modName, customID, bytes);
}
/// <summary>
/// Send any data (bytes) to the all player
/// </summary>
/// <param name="modName">The name of this modification (script)</param>
/// <param name="customID">The ID to know what the data is</param>
/// <param name="bytes">Your class, structure or whatever in bytes</param>
public static void SendDataToAll(string modName, byte customID, byte[] bytes)
{
Networking.SendModData(0, modName, customID, bytes);
}
/// <summary>
/// Send any data (bytes) to a player
/// </summary>
/// <param name="netHandle">The Lidgren Network net handle that receives the data</param>
/// <param name="modName">The name of this modification (script)</param>
/// <param name="customID">The ID to know what the data is</param>
/// <param name="bytes">Your class, structure or whatever in bytes</param>
public static void SendDataToPlayer(long netHandle, string modName, byte customID, byte[] bytes)
{
Networking.SendModData(netHandle, modName, customID, bytes);
}
/// <summary>
/// Get that player's local username
/// </summary>
public static string GetUsername()
{
return Main.Settings.Username;
}
/// <summary>
/// Set a new username for this player
/// </summary>
/// <param name="username">The new username</param>
/// <returns>false if the player already joined a server or the username is null or empty otherwise true</returns>
public static bool SetUsername(string username)
{
if (IsOnServer() || string.IsNullOrEmpty(username))
{
return false;
}
Main.Settings.Username = username;
return true;
}
/// <summary>
/// Enable or disable the local traffic for this player
/// </summary>
/// <param name="enable">true to disable traffic</param>
public static void SetLocalTraffic(bool enable)
{
Main.Settings.DisableTraffic = !enable;
}
/// <summary>
/// Sets the alignment for the player list, if set to true it will align left,
/// otherwise it will align right
/// </summary>
/// <param name="leftAlign">true to move the player list to the left</param>
public static void SetPlayerListLeftAlign(bool leftAlign)
{
PlayerList.LeftAlign = leftAlign;
}
#if DEBUG
/// <summary>
/// ?
/// </summary>
/// <param name="value"></param>
public static void SetDebug(bool value)
{
Main.UseDebug = value;
}
#endif
}
}

View File

@ -1,115 +0,0 @@
using GTA;
using System.Drawing;
using LemonUI;
using LemonUI.Menus;
namespace RageCoop.Client.Menus
{
/// <summary>
/// Don't use it!
/// </summary>
public class RageCoopMenu
{
public ObjectPool MenuPool = new ObjectPool();
public NativeMenu MainMenu = new NativeMenu("RAGECOOP", "MAIN")
{
UseMouse = false,
Alignment = Main.Settings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left
};
#region SUB
public Sub.SettingsMenu SubSettings = new Sub.SettingsMenu();
#endregion
#region ITEMS
private readonly NativeItem _usernameItem = new NativeItem("Username") { AltTitle = Main.Settings.Username };
public readonly NativeItem ServerIpItem = new NativeItem("Server IP") { AltTitle = Main.Settings.LastServerAddress };
private readonly NativeItem _serverConnectItem = new NativeItem("Connect");
private readonly NativeItem _aboutItem = new NativeItem("About", "~y~SOURCE~s~~n~" +
"https://github.com/RAGECOOP~n~" +
"~y~VERSION~s~~n~" +
Main.CurrentVersion.Replace("_", ".")) { LeftBadge = new LemonUI.Elements.ScaledTexture("commonmenu", "shop_new_star") };
#endregion
/// <summary>
/// Don't use it!
/// </summary>
public RageCoopMenu()
{
MainMenu.Banner.Color = Color.FromArgb(225, 0, 0, 0);
MainMenu.Title.Color = Color.FromArgb(255, 165, 0);
_usernameItem.Activated += UsernameActivated;
ServerIpItem.Activated += ServerIpActivated;
_serverConnectItem.Activated += (sender, item) => { Networking.DisConnectFromServer(Main.Settings.LastServerAddress); };
MainMenu.Add(_usernameItem);
MainMenu.Add(ServerIpItem);
MainMenu.Add(_serverConnectItem);
MainMenu.Add(_aboutItem);
MainMenu.AddSubMenu(SubSettings.Menu);
MainMenu.AddSubMenu(DevToolMenu.Menu);
MainMenu.AddSubMenu(DebugMenu.Menu);
MenuPool.Add(MainMenu);
MenuPool.Add(SubSettings.Menu);
MenuPool.Add(DevToolMenu.Menu);
MenuPool.Add(DebugMenu.Menu);
MenuPool.Add(DebugMenu.DiagnosticMenu);
}
public void UsernameActivated(object a, System.EventArgs b)
{
string newUsername = Game.GetUserInput(WindowTitle.EnterMessage20, _usernameItem.AltTitle, 20);
if (!string.IsNullOrWhiteSpace(newUsername))
{
Main.Settings.Username = newUsername;
Util.SaveSettings();
_usernameItem.AltTitle = newUsername;
}
}
public void ServerIpActivated(object a, System.EventArgs b)
{
string newServerIp = Game.GetUserInput(WindowTitle.EnterMessage60, ServerIpItem.AltTitle, 60);
if (!string.IsNullOrWhiteSpace(newServerIp) && newServerIp.Contains(":"))
{
Main.Settings.LastServerAddress = newServerIp;
Util.SaveSettings();
ServerIpItem.AltTitle = newServerIp;
}
}
public void InitiateConnectionMenuSetting()
{
MainMenu.Items[0].Enabled = false;
MainMenu.Items[1].Enabled = false;
MainMenu.Items[2].Enabled = false;
}
public void ConnectedMenuSetting()
{
MainMenu.Items[2].Enabled = true;
MainMenu.Items[2].Title = "Disconnect";
MainMenu.Visible = false;
}
public void DisconnectedMenuSetting()
{
MainMenu.Items[0].Enabled = true;
MainMenu.Items[1].Enabled = true;
MainMenu.Items[2].Enabled = true;
MainMenu.Items[2].Title = "Connect";
SubSettings.Menu.Items[1].Enabled = false;
}
}
}

View File

@ -1,903 +0,0 @@
using System;
using System.IO;
using System.Xml.Serialization;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using RageCoop.Core;
using GTA;
using GTA.Native;
using GTA.Math;
using System.Linq;
using System.Diagnostics;
namespace RageCoop.Client
{
/// <summary>
///
/// </summary>
internal static partial class Util
{
#region -- POINTER --
private static int _steeringAngleOffset { get; set; }
public static unsafe void NativeMemory()
{
IntPtr address;
address = Game.FindPattern("\x74\x0A\xF3\x0F\x11\xB3\x1C\x09\x00\x00\xEB\x25", "xxxxxx????xx");
if (address != IntPtr.Zero)
{
_steeringAngleOffset = *(int*)(address + 6) + 8;
}
// breaks some stuff.
/*
address = Game.FindPattern("\x32\xc0\xf3\x0f\x11\x09", "xxxxxx"); // Weapon / Radio slowdown
if (address != IntPtr.Zero)
{
for (int i = 0; i < 6; i++)
{
*(byte*)(address + i).ToPointer() = 0x90;
}
}
*/
}
public static unsafe void CustomSteeringAngle(this Vehicle veh, float value)
{
IntPtr address = new IntPtr((long)veh.MemoryAddress);
if (address == IntPtr.Zero || _steeringAngleOffset == 0)
{
return;
}
*(float*)(address + _steeringAngleOffset).ToPointer() = value;
}
#endregion
public static Settings ReadSettings()
{
XmlSerializer ser = new XmlSerializer(typeof(Settings));
string path = Directory.GetCurrentDirectory() + "\\Scripts\\RageCoop\\RageCoop.Client.Settings.xml";
Settings settings = null;
if (File.Exists(path))
{
using (FileStream stream = File.OpenRead(path))
{
settings = (RageCoop.Client.Settings)ser.Deserialize(stream);
}
using (FileStream stream = new FileStream(path, FileMode.Truncate, 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\\RageCoop\\RageCoop.Client.Settings.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.Settings);
}
}
catch (Exception ex)
{
GTA.UI.Notification.Show("Error saving player settings: " + ex.Message);
}
}
public static bool IsBetween<T>(this T item, T start, T end)
{
return Comparer<T>.Default.Compare(item, start) >= 0 && Comparer<T>.Default.Compare(item, end) <= 0;
}
public static bool Compare<T, Y>(this Dictionary<T, Y> item, Dictionary<T, Y> item2)
{
if (item == null || item2 == null || item.Count != item2.Count)
{
return false;
}
foreach (KeyValuePair<T, Y> pair in item)
{
if (item2.TryGetValue(pair.Key, out Y value) && Equals(value, pair.Value))
{
continue;
}
// TryGetValue() or Equals failed
return false;
}
// No difference between item and item2
return true;
}
#region MATH
public static Vector3 LinearVectorLerp(Vector3 start, Vector3 end, ulong currentTime, int duration)
{
return new Vector3()
{
X = LinearFloatLerp(start.X, end.X, currentTime, duration),
Y = LinearFloatLerp(start.Y, end.Y, currentTime, duration),
Z = LinearFloatLerp(start.Z, end.Z, currentTime, duration),
};
}
public static float LinearFloatLerp(float start, float end, ulong currentTime, int duration)
{
return (end - start) * currentTime / duration + start;
}
public static float Lerp(float from, float to, float fAlpha)
{
return (from * (1.0f - fAlpha)) + (to * fAlpha); //from + (to - from) * fAlpha
}
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)
};
}
#endregion
public static Model ModelRequest(this int hash)
{
Model model = new Model(hash);
if (!model.IsValid)
{
//GTA.UI.Notification.Show("~y~Not valid!");
return null;
}
if (!model.IsLoaded)
{
return model.Request(1000) ? model : null;
}
return model;
}
#region PED
public static byte GetPedSpeed(this Ped ped)
{
if (ped.IsSprinting)
{
return 3;
}
if (ped.IsRunning)
{
return 2;
}
if (ped.IsWalking)
{
return 1;
}
return 0;
}
public static Dictionary<byte, short> GetPedClothes(this Ped ped)
{
Dictionary<byte, short> result = new Dictionary<byte, short>();
for (byte i = 0; i < 11; i++)
{
short mod = Function.Call<short>(Hash.GET_PED_DRAWABLE_VARIATION, ped.Handle, i);
result.Add(i, mod);
}
return result;
}
public static PedDataFlags GetPedFlags(this Ped ped)
{
PedDataFlags flags = PedDataFlags.None;
if (ped.IsAiming || ped.IsOnTurretSeat())
{
flags |= PedDataFlags.IsAiming;
}
if (ped.IsReloading)
{
flags |= PedDataFlags.IsReloading;
}
if (ped.IsJumping)
{
flags |= PedDataFlags.IsJumping;
}
if (ped.IsRagdoll)
{
flags |= PedDataFlags.IsRagdoll;
}
if (ped.IsOnFire)
{
flags |= PedDataFlags.IsOnFire;
}
if (ped.IsInParachuteFreeFall)
{
flags |= PedDataFlags.IsInParachuteFreeFall;
}
if (ped.ParachuteState == ParachuteState.Gliding)
{
flags |= PedDataFlags.IsParachuteOpen;
}
bool climbingLadder = ped.IsTaskActive(TaskType.CTaskGoToAndClimbLadder);
if (climbingLadder)
{
flags |= PedDataFlags.IsOnLadder;
}
if (ped.IsVaulting && !climbingLadder)
{
flags |= PedDataFlags.IsVaulting;
}
if (ped.IsInCover || ped.IsGoingIntoCover)
{
flags |=PedDataFlags.IsInCover;
}
return flags;
}
public static string[] GetReloadingAnimation(this Ped ped)
{
switch (ped.Weapons.Current.Hash)
{
case WeaponHash.Revolver:
case WeaponHash.RevolverMk2:
case WeaponHash.DoubleActionRevolver:
case WeaponHash.NavyRevolver:
return new string[2] { "anim@weapons@pistol@revolver_str", "reload_aim" };
case WeaponHash.APPistol:
return new string[2] { "weapons@pistol@ap_pistol_str", "reload_aim" };
case WeaponHash.Pistol50:
return new string[2] { "weapons@pistol@pistol_50_str", "reload_aim" };
case WeaponHash.Pistol:
case WeaponHash.PistolMk2:
case WeaponHash.PericoPistol:
case WeaponHash.SNSPistol:
case WeaponHash.SNSPistolMk2:
case WeaponHash.HeavyPistol:
case WeaponHash.VintagePistol:
case WeaponHash.CeramicPistol:
case WeaponHash.MachinePistol:
return new string[2] { "weapons@pistol@pistol_str", "reload_aim" };
case WeaponHash.AssaultRifle:
case WeaponHash.AssaultrifleMk2:
return new string[2] { "weapons@rifle@hi@assault_rifle_str", "reload_aim" };
case WeaponHash.SniperRifle:
return new string[2] { "weapons@rifle@hi@sniper_rifle_str", "reload_aim" };
case WeaponHash.HeavySniper:
case WeaponHash.HeavySniperMk2:
return new string[2] { "weapons@rifle@lo@sniper_heavy_str", "reload_aim" };
case WeaponHash.PumpShotgun:
case WeaponHash.PumpShotgunMk2:
return new string[2] { "weapons@rifle@pump_str", "reload_aim" };
case WeaponHash.Railgun:
return new string[2] { "weapons@rifle@lo@rail_gun_str", "reload_aim" };
case WeaponHash.SawnOffShotgun:
return new string[2] { "weapons@rifle@lo@sawnoff_str", "reload_aim" };
case WeaponHash.AssaultShotgun:
return new string[2] { "weapons@rifle@lo@shotgun_assault_str", "reload_aim" };
case WeaponHash.BullpupShotgun:
return new string[2] { "weapons@rifle@lo@shotgun_bullpup_str", "reload_aim" };
case WeaponHash.AdvancedRifle:
return new string[2] { "weapons@submg@advanced_rifle_str", "reload_aim" };
case WeaponHash.CarbineRifle:
case WeaponHash.CarbineRifleMk2:
case WeaponHash.CompactRifle:
return new string[2] { "weapons@rifle@lo@carbine_str", "reload_aim" };
case WeaponHash.Gusenberg:
return new string[2] { "anim@weapons@machinegun@gusenberg_str", "reload_aim" };
case WeaponHash.Musket:
return new string[2] { "anim@weapons@musket@musket_str", "reload_aim" };
case WeaponHash.FlareGun:
return new string[2] { "anim@weapons@pistol@flare_str", "reload_aim" };
case WeaponHash.SpecialCarbine:
case WeaponHash.SpecialCarbineMk2:
return new string[2] { "anim@weapons@rifle@lo@spcarbine_str", "reload_aim" };
case WeaponHash.CombatPDW:
return new string[2] { "anim@weapons@rifle@lo@pdw_str", "reload_aim" };
case WeaponHash.BullpupRifle:
case WeaponHash.BullpupRifleMk2:
return new string[2] { "anim@weapons@submg@bullpup_rifle_str", "reload_aim" };
case WeaponHash.AssaultSMG:
return new string[2] { "weapons@submg@assault_smg_str", "reload_aim" };
case WeaponHash.MicroSMG:
case WeaponHash.MiniSMG:
return new string[2] { "weapons@submg@lo@micro_smg_str", "reload_aim" };
case WeaponHash.SMG:
case WeaponHash.SMGMk2:
return new string[2] { "weapons@rifle@smg_str", "reload_aim" };
case WeaponHash.GrenadeLauncher:
case WeaponHash.GrenadeLauncherSmoke:
case WeaponHash.CompactGrenadeLauncher:
return new string[2] { "weapons@heavy@grenade_launcher_str", "reload_aim" };
case WeaponHash.RPG:
case WeaponHash.Firework:
return new string[2] { "weapons@heavy@rpg_str", "reload_aim" };
case WeaponHash.CombatMG:
case WeaponHash.CombatMGMk2:
return new string[2] { "weapons@machinegun@combat_mg_str", "reload_aim" };
case WeaponHash.MG:
return new string[2] { "weapons@machinegun@mg_str", "reload_aim" };
default:
Main.Logger.Warning($"~r~Reloading failed! Weapon ~g~[{ped.Weapons.Current.Hash}]~r~ could not be found!");
return null;
}
}
public static VehicleSeat GetNearestSeat(Ped ped, Vehicle veh, float distanceToignoreDoors = 50f)
{
float num = 99f;
int result = -2;
Dictionary<string, int> dictionary = new Dictionary<string, int>();
dictionary.Add("door_dside_f", -1);
dictionary.Add("door_pside_f", 0);
dictionary.Add("door_dside_r", 1);
dictionary.Add("door_pside_r", 2);
foreach (string text in dictionary.Keys)
{
bool flag = veh.Bones[text].Position != Vector3.Zero;
if (flag)
{
float num2 = ped.Position.DistanceTo(Function.Call<Vector3>(Hash.GET_WORLD_POSITION_OF_ENTITY_BONE, new InputArgument[]
{
veh,
veh.Bones[text].Index
}));
bool flag2 = (num2 < distanceToignoreDoors) && (num2 < num)&& IsSeatUsableByPed(ped, veh, dictionary[text]);
if (flag2)
{
num = num2;
result = dictionary[text];
}
}
}
return (VehicleSeat)result;
}
public static bool IsSeatUsableByPed(Ped ped, Vehicle veh, int _seat)
{
VehicleSeat seat = (VehicleSeat)_seat;
bool result = false;
bool flag = veh.IsSeatFree(seat);
if (flag)
{
result = true;
}
else
{
bool isDead = veh.GetPedOnSeat(seat).IsDead;
if (isDead)
{
result = true;
}
else
{
int num = Function.Call<int>(Hash.GET_RELATIONSHIP_BETWEEN_PEDS, new InputArgument[]
{
ped,
veh.GetPedOnSeat(seat)
});
bool flag2 = num > 2;
if (flag2)
{
result = true;
}
}
}
return result;
}
public static bool IsTaskActive(this Ped p,TaskType task)
{
return Function.Call<bool>(Hash.GET_IS_TASK_ACTIVE, p.Handle, task);
}
public static Vector3 GetAimCoord(this Ped p)
{
var weapon = p.Weapons.CurrentWeaponObject;
var v = p.CurrentVehicle;
// Rhino
if (v!=null && v.Model.Hash==782665360)
{
return v.Bones[35].Position+v.Bones[35].ForwardVector*100;
}
if (p.IsOnTurretSeat()) { return p.GetLookingCoord(); }
if (weapon!=null)
{
// Not very accurate, but doesn't matter
Vector3 dir = weapon.RightVector;
return weapon.Position+dir*20;
}
return GetLookingCoord(p);
}
public static Vector3 GetLookingCoord(this Ped p)
{
EntityBone b = p.Bones[Bone.FacialForehead];
Vector3 v = b.UpVector.Normalized;
return b.Position+200*v;
}
public static void StayInCover(this Ped p)
{
Function.Call(Hash.TASK_STAY_IN_COVER, p);
}
public static VehicleSeat GetSeatTryingToEnter(this Ped p)
{
return (VehicleSeat)Function.Call<int>(Hash.GET_SEAT_PED_IS_TRYING_TO_ENTER, p);
}
#endregion
#region VEHICLE
public static VehicleDataFlags GetVehicleFlags(this Vehicle veh)
{
VehicleDataFlags flags = 0;
if (veh.IsEngineRunning)
{
flags |= VehicleDataFlags.IsEngineRunning;
}
if (veh.AreLightsOn)
{
flags |= VehicleDataFlags.AreLightsOn;
}
if (veh.BrakePower >= 0.01f)
{
flags |= VehicleDataFlags.AreBrakeLightsOn;
}
if (veh.AreHighBeamsOn)
{
flags |= VehicleDataFlags.AreHighBeamsOn;
}
if (veh.IsSirenActive)
{
flags |= VehicleDataFlags.IsSirenActive;
}
if (veh.IsDead)
{
flags |= VehicleDataFlags.IsDead;
}
if (Function.Call<bool>(Hash.IS_HORN_ACTIVE, veh.Handle))
{
flags |= VehicleDataFlags.IsHornActive;
}
if (veh.IsSubmarineCar && Function.Call<bool>(Hash._GET_IS_SUBMARINE_VEHICLE_TRANSFORMED, veh.Handle))
{
flags |= VehicleDataFlags.IsTransformed;
}
if (veh.HasRoof && (veh.RoofState == VehicleRoofState.Opened || veh.RoofState == VehicleRoofState.Opening))
{
flags |= VehicleDataFlags.RoofOpened;
}
if (veh.IsAircraft)
{
flags |= VehicleDataFlags.IsAircraft;
}
return flags;
}
public static bool HasFlag(this PedDataFlags flagToCheck,PedDataFlags flag)
{
return (flagToCheck & flag)!=0;
}
public static bool HasFlag(this VehicleDataFlags flagToCheck, VehicleDataFlags flag)
{
return (flagToCheck & flag)!=0;
}
public static Dictionary<uint, bool> GetWeaponComponents(this Weapon weapon)
{
Dictionary<uint, bool> result = null;
if (weapon.Components.Count > 0)
{
result = new Dictionary<uint, bool>();
foreach (var comp in weapon.Components)
{
result.Add((uint)comp.ComponentHash, comp.Active);
}
}
return result;
}
public static Dictionary<int, int> GetVehicleMods(this VehicleModCollection mods)
{
Dictionary<int, int> result = new Dictionary<int, int>();
foreach (VehicleMod mod in mods.ToArray())
{
result.Add((int)mod.Type, mod.Index);
}
return result;
}
public static VehicleDamageModel GetVehicleDamageModel(this Vehicle veh)
{
// Broken windows
byte brokenWindows = 0;
for (int i = 0; i < 8; i++)
{
if (!veh.Windows[(VehicleWindowIndex)i].IsIntact)
{
brokenWindows |= (byte)(1 << i);
}
}
// Broken doors
byte brokenDoors = 0;
byte openedDoors = 0;
foreach (VehicleDoor door in veh.Doors)
{
if (door.IsBroken)
{
brokenDoors |= (byte)(1 << (byte)door.Index);
}
else if (door.IsOpen)
{
openedDoors |= (byte)(1 << (byte)door.Index);
}
}
// Bursted tires
short burstedTires = 0;
foreach (VehicleWheel wheel in veh.Wheels.GetAllWheels())
{
if (wheel.IsBursted)
{
burstedTires |= (short)(1 << (int)wheel.BoneId);
}
}
return new VehicleDamageModel()
{
BrokenDoors = brokenDoors,
OpenedDoors = openedDoors,
BrokenWindows = brokenWindows,
BurstedTires = burstedTires,
LeftHeadLightBroken = (byte)(veh.IsLeftHeadLightBroken ? 1 : 0),
RightHeadLightBroken = (byte)(veh.IsRightHeadLightBroken ? 1 : 0)
};
}
public static Dictionary<int,int> GetPassengers(this Vehicle veh)
{
Dictionary<int,int> ps=new Dictionary<int, int>();
var d = veh.Driver;
if (d!=null&&d.IsSittingInVehicle())
{
ps.Add(-1, d.GetSyncEntity().ID);
}
foreach(Ped p in veh.Passengers)
{
if (p.IsSittingInVehicle())
{
ps.Add((int)p.SeatIndex, (int)p.GetSyncEntity().ID);
}
}
return ps;
}
public static void SetDamageModel(this Vehicle veh, VehicleDamageModel model, bool leavedoors = true)
{
for (int i = 0; i < 8; i++)
{
var door = veh.Doors[(VehicleDoorIndex)i];
if ((model.BrokenDoors & (byte)(1 << i)) != 0)
{
door.Break(leavedoors);
}
else if (door.IsBroken)
{
// The vehicle can only fix a door if the vehicle was completely fixed
veh.Repair();
return;
}
if ((model.OpenedDoors & (byte)(1 << i)) != 0)
{
if ((!door.IsOpen)&&(!door.IsBroken))
{
door.Open();
}
}
else if (door.IsOpen)
{
if (!door.IsBroken) { door.Close(); }
}
if ((model.BrokenWindows & (byte)(1 << i)) != 0)
{
veh.Windows[(VehicleWindowIndex)i].Smash();
}
else if (!veh.Windows[(VehicleWindowIndex)i].IsIntact)
{
veh.Windows[(VehicleWindowIndex)i].Repair();
}
}
foreach (VehicleWheel wheel in veh.Wheels)
{
if ((model.BurstedTires & (short)(1 << (int)wheel.BoneId)) != 0)
{
if (!wheel.IsBursted)
{
wheel.Puncture();
wheel.Burst();
}
}
else if (wheel.IsBursted)
{
wheel.Fix();
}
}
veh.IsLeftHeadLightBroken = model.LeftHeadLightBroken > 0;
veh.IsRightHeadLightBroken = model.RightHeadLightBroken > 0;
}
#endregion
public static void SetOnFire(this Entity e,bool toggle)
{
if (toggle)
{
Function.Call(Hash.START_ENTITY_FIRE, e.Handle);
}
else
{
Function.Call(Hash.STOP_ENTITY_FIRE, e.Handle);
}
}
public static SyncedPed GetSyncEntity(this Ped p)
{
if(p == null) { return null; }
var c = EntityPool.GetPedByHandle(p.Handle);
if(c==null) { EntityPool.Add(c=new SyncedPed(p)); }
return c;
}
public static SyncedVehicle GetSyncEntity(this Vehicle veh)
{
if(veh == null) { return null; }
var v=EntityPool.GetVehicleByHandle(veh.Handle);
if (v==null) { EntityPool.Add(v=new SyncedVehicle(veh)); }
return v;
}
public static double DegToRad(double deg)
{
return deg * Math.PI / 180.0;
}
public static bool IsTurretSeat(this Vehicle veh, int seat)
{
if (!Function.Call<bool>(Hash.DOES_VEHICLE_HAVE_WEAPONS, veh.Handle))
{
return false;
}
switch (seat)
{
case -1:
return (VehicleHash)veh.Model.Hash == VehicleHash.Rhino
|| (VehicleHash)veh.Model.Hash == VehicleHash.Khanjari
|| (VehicleHash)veh.Model.Hash == VehicleHash.FireTruck
|| (VehicleHash)veh.Model.Hash == VehicleHash.Riot2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Cerberus
|| (VehicleHash)veh.Model.Hash == VehicleHash.Cerberus2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Cerberus3;
case 0:
return (VehicleHash)veh.Model.Hash == VehicleHash.Apc;
case 1:
return (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie
|| (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Technical
|| (VehicleHash)veh.Model.Hash == VehicleHash.Technical2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Technical3
|| (VehicleHash)veh.Model.Hash == VehicleHash.HalfTrack
|| (VehicleHash)veh.Model.Hash == VehicleHash.Barrage;
case 2:
return (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie
|| (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Barrage;
case 3:
return (VehicleHash)veh.Model.Hash == VehicleHash.Limo2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Dinghy5;
case 7:
return (VehicleHash)veh.Model.Hash == VehicleHash.Insurgent;
}
return false;
}
public static bool IsOnTurretSeat(this Ped P)
{
if (P.CurrentVehicle == null) { return false; }
return IsTurretSeat(P.CurrentVehicle, (int)P.SeatIndex);
}
[DllImport("kernel32.dll")]
public static extern ulong GetTickCount64();
}
/// <summary>
///
/// </summary>
public static class VectorExtensions
{
/// <summary>
///
/// </summary>
public static Vector3 ToVector(this Quaternion vec)
{
return new Vector3()
{
X = vec.X,
Y = vec.Y,
Z = vec.Z
};
}
/// <summary>
///
/// </summary>
public static Quaternion ToQuaternion(this Vector3 vec, float vW = 0.0f)
{
return new Quaternion()
{
X = vec.X,
Y = vec.Y,
Z = vec.Z,
W = vW
};
}
public static float Denormalize(this float h)
{
return h < 0f ? h + 360f : h;
}
public static float ToRadians(this float val)
{
return (float)(Math.PI / 180) * val;
}
public static Vector3 ToRadians(this Vector3 i)
{
return new Vector3()
{
X = ToRadians(i.X),
Y = ToRadians(i.Y),
Z = ToRadians(i.Z),
};
}
public static Quaternion ToQuaternion(this Vector3 vect)
{
vect = new Vector3()
{
X = vect.X.Denormalize() * -1,
Y = vect.Y.Denormalize() - 180f,
Z = vect.Z.Denormalize() - 180f,
};
vect = vect.ToRadians();
float rollOver2 = vect.Z * 0.5f;
float sinRollOver2 = (float)Math.Sin((double)rollOver2);
float cosRollOver2 = (float)Math.Cos((double)rollOver2);
float pitchOver2 = vect.Y * 0.5f;
float sinPitchOver2 = (float)Math.Sin((double)pitchOver2);
float cosPitchOver2 = (float)Math.Cos((double)pitchOver2);
float yawOver2 = vect.X * 0.5f; // pitch
float sinYawOver2 = (float)Math.Sin((double)yawOver2);
float cosYawOver2 = (float)Math.Cos((double)yawOver2);
Quaternion result = new Quaternion()
{
X = cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2,
Y = cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2,
Z = cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2,
W = sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2
};
return result;
}
/// <summary>
///
/// </summary>
public static LVector3 ToLVector(this Vector3 vec)
{
return new LVector3()
{
X = vec.X,
Y = vec.Y,
Z = vec.Z
};
}
/// <summary>
///
/// </summary>
public static LQuaternion ToLQuaternion(this Quaternion vec)
{
return new LQuaternion()
{
X = vec.X,
Y = vec.Y,
Z = vec.Z,
W = vec.W
};
}
}
}

View File

@ -1,185 +0,0 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
namespace RageCoop.Client
{
public static class DownloadManager
{
private static readonly List<DownloadFile> _downloadFiles = new List<DownloadFile>();
private static readonly Dictionary<byte, FileStream> _streams = new Dictionary<byte, FileStream>();
private static readonly List<byte> _filesFinished = new List<byte>();
public static bool DownloadComplete = false;
public static void AddFile(byte id, string name, long length)
{
string downloadFolder = $"Scripts\\RageCoop\\Resources\\{Main.Settings.LastServerAddress.Replace(":", ".")}";
if (!Directory.Exists(downloadFolder))
{
Directory.CreateDirectory(downloadFolder);
}
if (FileAlreadyExists(downloadFolder, name, length))
{
// Send the server we are already done
Networking.SendDownloadFinish(id);
Cancel(id);
return;
}
if (!new string[] { ".js", ".xml" }.Any(x => x == Path.GetExtension(name)))
{
Cancel(id);
GTA.UI.Notification.Show($"The download of a file from the server was blocked! [{name}]", true);
Main.Logger.Error($"The download of a file from the server was blocked! [{name}]");
return;
}
lock (_downloadFiles)
{
_downloadFiles.Add(new DownloadFile()
{
FileID = id,
FileName = name,
FileLength = length
});
}
lock (_streams)
{
_streams.Add(id, new FileStream($"{downloadFolder}\\{name}", FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite));
}
}
/// <summary>
/// Check if the file already exists and if the size correct otherwise delete this file
/// </summary>
/// <param name="folder"></param>
/// <param name="name"></param>
/// <param name="length"></param>
/// <returns></returns>
private static bool FileAlreadyExists(string folder, string name, long length)
{
string filePath = $"{folder}\\{name}";
if (File.Exists(filePath))
{
if (new FileInfo(filePath).Length == length)
{
return true;
}
// Delete the file because the length is wrong (maybe the file was updated)
File.Delete(filePath);
}
return false;
}
public static void RenderProgress()
{
if (_downloadFiles.Count == 0)
{
return;
}
lock (_downloadFiles) lock (_filesFinished)
{
new LemonUI.Elements.ScaledText(new System.Drawing.PointF(System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width / 2, System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height - 60), $"Downloading files {_filesFinished.Count()} / {_downloadFiles.Count() + _filesFinished.Count()}", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
}
}
public static void Write(byte id, byte[] chunk)
{
lock (_filesFinished)
{
if (_filesFinished.Contains(id))
{
return;
}
}
lock (_streams)
{
FileStream fs = _streams.FirstOrDefault(x => x.Key == id).Value;
if (fs == null)
{
Main.Logger.Error($"Stream for file {id} not found!");
return;
}
fs.Write(chunk, 0, chunk.Length);
lock (_downloadFiles)
{
DownloadFile file = _downloadFiles.FirstOrDefault(x => x.FileID == id);
if (file == null)
{
Main.Logger.Error($"File {id} couldn't be found in the list!");
return;
}
file.FileWritten += chunk.Length;
if (file.FileWritten >= file.FileLength)
{
Cancel(id);
}
}
}
}
public static void Cancel(byte id)
{
lock (_streams) lock (_downloadFiles) lock (_filesFinished)
{
FileStream fs = _streams.ContainsKey(id) ? _streams[id] : null;
if (fs != null)
{
fs.Close();
fs.Dispose();
_streams.Remove(id);
}
if (_downloadFiles.Any(x => x.FileID == id))
{
_downloadFiles.Remove(_downloadFiles.First(x => x.FileID == id));
}
_filesFinished.Add(id);
}
}
public static void Cleanup(bool everything)
{
lock (_streams) lock (_downloadFiles) lock (_filesFinished)
{
foreach (KeyValuePair<byte, FileStream> stream in _streams)
{
stream.Value.Close();
stream.Value.Dispose();
}
_streams.Clear();
_downloadFiles.Clear();
_filesFinished.Clear();
}
if (everything)
{
DownloadComplete = false;
}
}
}
public class DownloadFile
{
public byte FileID { get; set; } = 0;
public string FileName { get; set; } = string.Empty;
public long FileLength { get; set; } = 0;
public long FileWritten { get; set; } = 0;
}
}

View File

@ -1,225 +0,0 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Lidgren.Network;
using RageCoop.Core;
using System.Threading.Tasks;
using System.Threading;
using GTA;
using GTA.Native;
namespace RageCoop.Client
{
internal static partial class Networking
{
public static NetClient Client;
public static float Latency = 0;
public static bool ShowNetworkInfo = false;
public static int BytesReceived = 0;
public static int BytesSend = 0;
private static Thread ReceiveThread;
public static void DisConnectFromServer(string address)
{
if (IsOnServer)
{
Client.Disconnect("Bye!");
}
else
{
// 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP
NetPeerConfiguration config = new NetPeerConfiguration("623c92c287cc392406e7aaaac1c0f3b0")
{
AutoFlushSendQueue = true
};
config.EnableMessageType(NetIncomingMessageType.ConnectionLatencyUpdated);
Client = new NetClient(config);
Client.Start();
string[] ip = new string[2];
int idx = address.LastIndexOf(':');
if (idx != -1)
{
ip[0] = address.Substring(0, idx);
ip[1] = address.Substring(idx + 1);
}
if (ip.Length != 2)
{
throw new Exception("Malformed URL");
}
// Send HandshakePacket
EntityPool.AddPlayer();
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
new Packets.Handshake()
{
PedID = Main.LocalPlayerID,
Username = Main.Settings.Username,
ModVersion = Main.CurrentVersion,
NPCsAllowed = false
}.Pack(outgoingMessage);
Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage);
}
}
public static bool IsOnServer
{
get { return Client?.ConnectionStatus == NetConnectionStatus.Connected; }
}
public static void Start()
{
ReceiveThread=new Thread(() =>
{
while (true)
{
try
{
ReceiveMessages();
}
catch (Exception ex)
{
Main.Logger.Error(ex);
}
Thread.Sleep(5);
}
});
ReceiveThread.Start();
}
#region -- GET --
#region -- PLAYER --
private static void PlayerConnect(Packets.PlayerConnect packet)
{
var p = new PlayerData
{
PedID = packet.PedID,
Username= packet.Username,
};
GTA.UI.Notification.Show($"{p.Username} connected.");
PlayerList.SetPlayer(packet.PedID, packet.Username);
Main.Logger.Debug($"player connected:{p.Username}");
Main.DumpCharacters();
COOPAPI.Connected(packet.PedID);
}
private static void PlayerDisconnect(Packets.PlayerDisconnect packet)
{
var name=PlayerList.GetPlayer(packet.PedID).Username;
GTA.UI.Notification.Show($"{name} left.");
COOPAPI.Disconnected(packet.PedID);
PlayerList.RemovePlayer(packet.PedID);
EntityPool.RemoveAllFromPlayer(packet.PedID);
}
private static object DecodeNativeCall(ulong hash, List<object> args, bool returnValue, byte? returnType = null)
{
List<InputArgument> arguments = new List<InputArgument>();
if (args == null || args.Count == 0)
{
return null;
}
for (ushort i = 0; i < args.Count; i++)
{
object x = args.ElementAt(i);
switch (x)
{
case int _:
arguments.Add((int)x);
break;
case bool _:
arguments.Add((bool)x);
break;
case float _:
arguments.Add((float)x);
break;
case string _:
arguments.Add((string)x);
break;
case LVector3 _:
LVector3 vector = (LVector3)x;
arguments.Add((float)vector.X);
arguments.Add((float)vector.Y);
arguments.Add((float)vector.Z);
break;
default:
GTA.UI.Notification.Show("[DecodeNativeCall][" + hash + "]: Type of argument not found!");
return null;
}
}
if (!returnValue)
{
Function.Call((Hash)hash, arguments.ToArray());
return null;
}
switch (returnType.Value)
{
case 0x00: // int
return Function.Call<int>((Hash)hash, arguments.ToArray());
case 0x01: // bool
return Function.Call<bool>((Hash)hash, arguments.ToArray());
case 0x02: // float
return Function.Call<float>((Hash)hash, arguments.ToArray());
case 0x03: // string
return Function.Call<string>((Hash)hash, arguments.ToArray());
case 0x04: // vector3
return Function.Call<GTA.Math.Vector3>((Hash)hash, arguments.ToArray()).ToLVector();
default:
GTA.UI.Notification.Show("[DecodeNativeCall][" + hash + "]: Type of return not found!");
return null;
}
}
private static void DecodeNativeResponse(Packets.NativeResponse packet)
{
object result = DecodeNativeCall(packet.Hash, packet.Args, true, packet.ResultType);
if (Main.CheckNativeHash.ContainsKey(packet.Hash))
{
foreach (KeyValuePair<ulong, byte> hash in Main.CheckNativeHash)
{
if (hash.Key == packet.Hash)
{
lock (Main.ServerItems)
{
Main.ServerItems.Add((int)result, hash.Value);
}
break;
}
}
}
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
new Packets.NativeResponse()
{
Hash = 0,
Args = new List<object>() { result },
ID = packet.ID
}.Pack(outgoingMessage);
Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
Client.FlushSendQueue();
}
#endregion // -- PLAYER --
#endregion
public static void Tick()
{
// Sync
EntityPool.DoSync();
}
}
}

View File

@ -1,434 +0,0 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Lidgren.Network;
using RageCoop.Core;
using GTA;
using GTA.Math;
using GTA.Native;
namespace RageCoop.Client
{
internal static partial class Networking
{
public static void ReceiveMessages()
{
if (Client == null)
{
return;
}
NetIncomingMessage message;
while ((message = Client.ReadMessage()) != null)
{
BytesReceived += message.LengthBytes;
switch (message.MessageType)
{
case NetIncomingMessageType.StatusChanged:
NetConnectionStatus status = (NetConnectionStatus)message.ReadByte();
string reason = message.ReadString();
switch (status)
{
case NetConnectionStatus.InitiatedConnect:
#if !NON_INTERACTIVE
Main.MainMenu.InitiateConnectionMenuSetting();
#endif
Main.QueueAction(() => { GTA.UI.Notification.Show("~y~Trying to connect..."); return true; });
break;
case NetConnectionStatus.Connected:
if (message.SenderConnection.RemoteHailMessage.ReadByte() != (byte)PacketTypes.Handshake)
{
Client.Disconnect("Wrong packet!");
}
else
{
int len = message.SenderConnection.RemoteHailMessage.ReadInt32();
byte[] data = message.SenderConnection.RemoteHailMessage.ReadBytes(len);
Packets.Handshake handshakePacket = new Packets.Handshake();
handshakePacket.Unpack(data);
// Main.LocalNetHandle = handshakePacket.NetHandle;
Main.NPCsAllowed = handshakePacket.NPCsAllowed;
#if !NON_INTERACTIVE
#endif
COOPAPI.Connected();
Main.QueueAction(() => {
Main.MainMenu.ConnectedMenuSetting();
Main.MainChat.Init();
PlayerList.Cleanup();
GTA.UI.Notification.Show("~g~Connected!");
});
Main.Logger.Info(">> Connected <<");
}
break;
case NetConnectionStatus.Disconnected:
DownloadManager.Cleanup(true);
// Reset all values
Latency = 0;
Main.QueueAction(() => { Main.CleanUpWorld();});
Main.NPCsAllowed = false;
if (Main.MainChat.Focused)
{
Main.MainChat.Focused = false;
}
Main.QueueAction(() => Main.CleanUp());
#if !NON_INTERACTIVE
Main.MainMenu.DisconnectedMenuSetting();
#endif
COOPAPI.Disconnected(reason);
Main.QueueAction(() =>
GTA.UI.Notification.Show("~r~Disconnected: " + reason));
MapLoader.DeleteAll();
Main.Logger.Info($">> Disconnected << reason: {reason}");
break;
}
break;
case NetIncomingMessageType.Data:
if (message.LengthBytes==0) { continue; }
var packetType = (PacketTypes)message.ReadByte();
try
{
int len = message.ReadInt32();
byte[] data = message.ReadBytes(len);
switch (packetType)
{
case PacketTypes.CleanUpWorld:
{
Main.QueueAction(() => { Main.CleanUpWorld(); return true; });
}
break;
case PacketTypes.PlayerConnect:
{
Packets.PlayerConnect packet = new Packets.PlayerConnect();
packet.Unpack(data);
Main.QueueAction(() =>PlayerConnect(packet));
}
break;
case PacketTypes.PlayerDisconnect:
{
Packets.PlayerDisconnect packet = new Packets.PlayerDisconnect();
packet.Unpack(data);
Main.QueueAction(() =>PlayerDisconnect(packet));
}
break;
case PacketTypes.PlayerInfoUpdate:
{
var packet = new Packets.PlayerInfoUpdate();
packet.Unpack(data);
PlayerList.SetPlayer(packet.PedID,packet.Username,packet.Latency);
break;
}
#region ENTITY SYNC
case PacketTypes.VehicleSync:
{
Packets.VehicleSync packet = new Packets.VehicleSync();
packet.Unpack(data);
VehicleSync(packet);
}
break;
case PacketTypes.PedSync:
{
Packets.PedSync packet = new Packets.PedSync();
packet.Unpack(data);
PedSync(packet);
}
break;
case PacketTypes.VehicleStateSync:
{
Packets.VehicleStateSync packet = new Packets.VehicleStateSync();
packet.Unpack(data);
VehicleStateSync(packet);
}
break;
case PacketTypes.PedStateSync:
{
Packets.PedStateSync packet = new Packets.PedStateSync();
packet.Unpack(data);
PedStateSync(packet);
}
break;
case PacketTypes.ProjectileSync:
{
Packets.ProjectileSync packet = new Packets.ProjectileSync();
packet.Unpack(data);
ProjectileSync(packet);
break;
}
#endregion
case PacketTypes.ChatMessage:
{
Packets.ChatMessage packet = new Packets.ChatMessage();
packet.Unpack(data);
if (!COOPAPI.ChatMessageReceived(packet.Username, packet.Message))
{
Main.QueueAction(() => { Main.MainChat.AddMessage(packet.Username, packet.Message);return true; });
}
}
break;
case PacketTypes.NativeCall:
{
Packets.NativeCall packet = new Packets.NativeCall();
packet.Unpack(data);
DecodeNativeCall(packet.Hash, packet.Args, false);
}
break;
case PacketTypes.NativeResponse:
{
Packets.NativeResponse packet = new Packets.NativeResponse();
packet.Unpack(data);
DecodeNativeResponse(packet);
}
break;
case PacketTypes.Mod:
{
Packets.Mod packet = new Packets.Mod();
packet.Unpack(data);
COOPAPI.ModPacketReceived(packet.NetHandle, packet.Name, packet.CustomPacketID, packet.Bytes);
}
break;
case PacketTypes.FileTransferTick:
{
Packets.FileTransferTick packet = new Packets.FileTransferTick();
packet.Unpack(data);
DownloadManager.Write(packet.ID, packet.FileChunk);
}
break;
case PacketTypes.FileTransferRequest:
{
Packets.FileTransferRequest packet = new Packets.FileTransferRequest();
packet.Unpack(data);
DownloadManager.AddFile(packet.ID, packet.FileName, packet.FileLength);
}
break;
case PacketTypes.FileTransferComplete:
{
Packets.FileTransferComplete packet = new Packets.FileTransferComplete();
packet.Unpack(data);
DownloadManager.Cleanup(false);
DownloadManager.DownloadComplete = true;
}
break;
case PacketTypes.ServerClientEvent:
{
Packets.ServerClientEvent packet = new Packets.ServerClientEvent();
packet.Unpack(data);
}
break;
default:
if (packetType.IsSyncEvent())
{
// Dispatch to main thread
Main.QueueAction(() => { SyncEvents.HandleEvent(packetType, data); return true; });
}
break;
}
}
catch (Exception ex)
{
Main.QueueAction(() => {
GTA.UI.Notification.Show("~r~~h~Packet Error");
return true;
});
Main.Logger.Error($"[{packetType}] {ex.Message}");
Main.Logger.Error(ex);
Client.Disconnect($"Packet Error [{packetType}]");
}
break;
case NetIncomingMessageType.ConnectionLatencyUpdated:
Latency = message.ReadFloat();
break;
case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.ErrorMessage:
case NetIncomingMessageType.WarningMessage:
case NetIncomingMessageType.VerboseDebugMessage:
#if DEBUG
// TODO?
#endif
break;
default:
break;
}
Client.Recycle(message);
}
}
private static void PedSync(Packets.PedSync packet)
{
if (!EntityPool.PedExists(packet.ID))
{
Main.Logger.Debug($"Creating character for incoming sync:{packet.ID}");
EntityPool.ThreadSafe.Add(new SyncedPed(packet.ID));
}
PedDataFlags flags = packet.Flag;
SyncedPed c = EntityPool.GetPedByID(packet.ID);
c.ID=packet.ID;
//c.OwnerID=packet.OwnerID;
c.Health = packet.Health;
c.Position = packet.Position.ToVector();
c.Rotation = packet.Rotation.ToVector();
c.Velocity = packet.Velocity.ToVector();
c.Speed = packet.Speed;
c.CurrentWeaponHash = packet.CurrentWeaponHash;
c.IsAiming = flags.HasFlag(PedDataFlags.IsAiming);
c.IsReloading = flags.HasFlag(PedDataFlags.IsReloading);
c.IsJumping = flags.HasFlag(PedDataFlags.IsJumping);
c.IsRagdoll = flags.HasFlag(PedDataFlags.IsRagdoll);
c.IsOnFire = flags.HasFlag(PedDataFlags.IsOnFire);
c.IsInParachuteFreeFall = flags.HasFlag(PedDataFlags.IsInParachuteFreeFall);
c.IsParachuteOpen = flags.HasFlag(PedDataFlags.IsParachuteOpen);
c.IsOnLadder = flags.HasFlag(PedDataFlags.IsOnLadder);
c.IsVaulting = flags.HasFlag(PedDataFlags.IsVaulting);
c.IsInCover = flags.HasFlag(PedDataFlags.IsInCover);
c.Heading=packet.Heading;
c.LastSynced = Main.Ticked;
if (c.IsAiming)
{
c.AimCoords = packet.AimCoords.ToVector();
}
if (c.IsRagdoll)
{
c.RotationVelocity=packet.RotationVelocity.ToVector();
}
}
private static void PedStateSync(Packets.PedStateSync packet)
{
if (!EntityPool.PedExists(packet.ID))
{
Main.Logger.Debug($"Creating character for incoming sync:{packet.ID}");
EntityPool.ThreadSafe.Add(new SyncedPed(packet.ID));
}
SyncedPed c = EntityPool.GetPedByID(packet.ID);
c.ID=packet.ID;
c.OwnerID=packet.OwnerID;
c.Clothes=packet.Clothes;
c.WeaponComponents=packet.WeaponComponents;
c.ModelHash=packet.ModelHash;
c.LastSynced=c.LastStateSynced = Main.Ticked;
}
private static void VehicleSync(Packets.VehicleSync packet)
{
if (!EntityPool.VehicleExists(packet.ID))
{
EntityPool.ThreadSafe.Add(new SyncedVehicle(packet.ID));
}
SyncedVehicle v = EntityPool.GetVehicleByID(packet.ID);
v.ID= packet.ID;
v.Position=packet.Position.ToVector();
v.Rotation=packet.Rotation.ToVector();
v.SteeringAngle=packet.SteeringAngle;
v.ThrottlePower=packet.ThrottlePower;
v.BrakePower=packet.BrakePower;
v.Velocity=packet.Velocity.ToVector();
v.RotationVelocity=packet.RotationVelocity.ToVector();
v.LastSynced=Main.Ticked;
}
private static void VehicleStateSync(Packets.VehicleStateSync packet)
{
if (!EntityPool.VehicleExists(packet.ID))
{
EntityPool.ThreadSafe.Add(new SyncedVehicle(packet.ID));
}
SyncedVehicle v = EntityPool.GetVehicleByID(packet.ID);
v.ID= packet.ID;
v.OwnerID= packet.OwnerID;
v.DamageModel=packet.DamageModel;
v.EngineHealth=packet.EngineHealth;
v.OwnerID=packet.OwnerID;
v.Mods=packet.Mods;
v.ModelHash=packet.ModelHash;
v.Colors=packet.Colors;
v.LandingGear=packet.LandingGear;
v.EngineRunning = packet.Flag.HasFlag(VehicleDataFlags.IsEngineRunning);
v.LightsOn = packet.Flag.HasFlag(VehicleDataFlags.AreLightsOn);
v.BrakeLightsOn = packet.Flag.HasFlag(VehicleDataFlags.AreBrakeLightsOn);
v.HighBeamsOn = packet.Flag.HasFlag(VehicleDataFlags.AreHighBeamsOn);
v.SireneActive = packet.Flag.HasFlag(VehicleDataFlags.IsSirenActive);
v.IsDead = packet.Flag.HasFlag(VehicleDataFlags.IsDead);
v.HornActive = packet.Flag.HasFlag(VehicleDataFlags.IsHornActive);
v.Transformed = packet.Flag.HasFlag(VehicleDataFlags.IsTransformed);
v.Passengers=new Dictionary<VehicleSeat, SyncedPed>();
v.LockStatus=packet.LockStatus;
foreach (KeyValuePair<int, int> pair in packet.Passengers)
{
if (EntityPool.PedExists(pair.Value))
{
v.Passengers.Add((VehicleSeat)pair.Key, EntityPool.GetPedByID(pair.Value));
}
}
v.LastStateSynced=v.LastSynced= Main.Ticked;
}
private static void ProjectileSync(Packets.ProjectileSync packet)
{
var p = EntityPool.GetProjectileByID(packet.ID);
if (p==null)
{
if (packet.Exploded) { return; }
Main.Logger.Debug($"Creating new projectile: {(WeaponHash)packet.WeaponHash}");
EntityPool.ThreadSafe.Add(p=new SyncedProjectile(packet.ID));
}
p.Position=packet.Position.ToVector();
p.Rotation=packet.Rotation.ToVector();
p.Velocity=packet.Velocity.ToVector();
p.Hash=(WeaponHash)packet.WeaponHash;
p.ShooterID=packet.ShooterID;
p.Exploded=packet.Exploded;
p.LastSynced=Main.Ticked;
}
}
}

View File

@ -1,36 +0,0 @@
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("RageCoop.Client")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RageCoop.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("0.2.0.0")]
[assembly: AssemblyFileVersion("0.2.0.0")]

View File

@ -1,162 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>RageCoop.Client</RootNamespace>
<AssemblyName>RageCoop.Client</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>D:\Games\Grand Theft Auto V\Scripts\RageCoop\</OutputPath>
<DefineConstants>TRACE;DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<DocumentationFile>
</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
<DocumentationFile>
</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="ClearScript.Core, Version=7.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\scripts\ClearScript.Core.dll</HintPath>
</Reference>
<Reference Include="ClearScript.V8, Version=7.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\scripts\ClearScript.V8.dll</HintPath>
</Reference>
<Reference Include="LemonUI.SHVDN3, Version=1.5.1.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\scripts\LemonUI.SHVDN3.dll</HintPath>
</Reference>
<Reference Include="Lidgren.Network, Version=2012.1.7.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\scripts\Lidgren.Network.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.DependencyInjection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Options, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.Options.6.0.0\lib\net461\Microsoft.Extensions.Options.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.Primitives.6.0.0\lib\net461\Microsoft.Extensions.Primitives.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\scripts\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="ScriptHookVDotNet3, Version=3.3.2.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\Release\ScriptHookVDotNet3.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Diagnostics.DiagnosticSource, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Diagnostics.DiagnosticSource.6.0.0\lib\net461\System.Diagnostics.DiagnosticSource.dll</HintPath>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Security" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="DevTools\DevTool.cs" />
<Compile Include="Menus\Sub\DevToolMenu.cs" />
<Compile Include="Misc\WeaponUtil.cs" />
<Compile Include="Networking\Chat.cs" />
<Compile Include="COOPAPI.cs" />
<Compile Include="Debug.cs" />
<Compile Include="Networking\DownloadManager.cs" />
<Compile Include="Misc\TaskType.cs" />
<Compile Include="Menus\Sub\DebugMenu.cs" />
<Compile Include="Networking\Receive.cs" />
<Compile Include="Networking\Send.cs" />
<Compile Include="Sync\Entities\SyncedPed.cs" />
<None Include="app.config" />
<None Include="packages.config" />
<Compile Include="Sync\Entities\SyncedProjectile.cs" />
<Compile Include="Sync\EntityPool.cs" />
<Compile Include="Sync\SyncEvents.cs" />
<Compile Include="Sync\Entities\SyncedEntity.cs" />
<Compile Include="Sync\Entities\SyncedVehicle.cs" />
<Compile Include="Main.cs" />
<Compile Include="Networking\MapLoader.cs" />
<Compile Include="Menus\RageCoopMenu.cs" />
<Compile Include="Menus\Sub\SettingsMenu.cs" />
<Compile Include="Networking\Networking.cs" />
<Compile Include="PlayerList.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Sync\SyncParameters.cs" />
<Compile Include="Misc\Util.cs" />
<Compile Include="WorldThread.cs" />
<Compile Include="Settings.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\RageCoop.Core.csproj">
<Project>{cc2e8102-e568-4524-aa1f-f8e0f1cfe58a}</Project>
<Name>RageCoop.Core</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Bcl.AsyncInterfaces" version="6.0.0" targetFramework="net48" />
<package id="Microsoft.Extensions.DependencyInjection" version="6.0.0" targetFramework="net48" />
<package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="6.0.0" targetFramework="net48" />
<package id="Microsoft.Extensions.Options" version="6.0.0" targetFramework="net48" />
<package id="Microsoft.Extensions.Primitives" version="6.0.0" targetFramework="net48" />
<package id="System.Buffers" version="4.5.1" targetFramework="net48" />
<package id="System.Diagnostics.DiagnosticSource" version="6.0.0" targetFramework="net48" />
<package id="System.Memory" version="4.5.4" targetFramework="net48" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net48" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net48" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net48" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net48" />
</packages>

View File

@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Core
{
public class CoreUtils
{
public static (byte, byte[]) GetBytesFromObject(object obj)
{
switch (obj)
{
case byte _:
return (0x01, BitConverter.GetBytes((byte)obj));
case short _:
return (0x02, BitConverter.GetBytes((short)obj));
case ushort _:
return (0x03, BitConverter.GetBytes((ushort)obj));
case int _:
return (0x04, BitConverter.GetBytes((int)obj));
case uint _:
return (0x05, BitConverter.GetBytes((uint)obj));
case long _:
return (0x06, BitConverter.GetBytes((long)obj));
case ulong _:
return (0x07, BitConverter.GetBytes((ulong)obj));
case float _:
return (0x08, BitConverter.GetBytes((float)obj));
case bool _:
return (0x09, BitConverter.GetBytes((bool)obj));
default:
return (0x0, null);
}
}
}
public static class Extensions
{
public static void AddLVector3(this List<byte> bytes, LVector3 vec3)
{
bytes.AddRange(BitConverter.GetBytes(vec3.X));
bytes.AddRange(BitConverter.GetBytes(vec3.Y));
bytes.AddRange(BitConverter.GetBytes(vec3.Z));
}
public static void AddLQuaternion(this List<byte> bytes, LQuaternion quat)
{
bytes.AddRange(BitConverter.GetBytes(quat.X));
bytes.AddRange(BitConverter.GetBytes(quat.Y));
bytes.AddRange(BitConverter.GetBytes(quat.Z));
bytes.AddRange(BitConverter.GetBytes(quat.W));
}
public static void AddInt(this List<byte> bytes,int i)
{
bytes.AddRange(BitConverter.GetBytes(i));
}
public static void AddUint(this List<byte> bytes, uint i)
{
bytes.AddRange(BitConverter.GetBytes(i));
}
public static void AddLong(this List<byte> bytes, long i)
{
bytes.AddRange(BitConverter.GetBytes(i));
}
public static void AddUlong(this List<byte> bytes, ulong i)
{
bytes.AddRange(BitConverter.GetBytes(i));
}
public static void AddFloat(this List<byte> bytes, float i)
{
bytes.AddRange(BitConverter.GetBytes(i));
}
public static byte ToByte(this bool[] source)
{
byte result = 0;
// This assumes the array never contains more than 8 elements!
int index = 8 - source.Length;
// Loop through the array
foreach (bool b in source)
{
// if the element is 'true' set the bit at that position
if (b)
result |= (byte)(1 << (7 - index));
index++;
}
return result;
}
public static bool[] ToBoolArray(this byte b)
{
bool[] result = new bool[8];
// check each bit in the byte. if 1 set to true, if 0 set to false
for (int i = 0; i < 8; i++)
result[i] = (b & (1 << i)) != 0;
// reverse the array
Array.Reverse(result);
return result;
}
}
}

View File

@ -1,34 +0,0 @@
using System.Linq;
namespace RageCoop.Core
{
public class PlayerData
{
public string Username { get; set; }
/// <summary>
/// Universal character ID.
/// </summary>
public int PedID
{
get; set;
}
/// <summary>
/// Universal vehicle ID.
/// </summary>
public int VehicleID { get; set; }
public bool IsInVehicle { get; internal set; }
public LVector3 Position { get; set; }
/// <summary>
/// Player Latency in second.
/// </summary>
public float Latency { get; set; }
public int Health { get; set; }
public bool IsInRangeOf(LVector3 position, float distance)
{
return LVector3.Subtract(Position, position).Length() < distance;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

View File

@ -1,2 +0,0 @@
ReloadKey=None
ConsoleKey=F4

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31919.166
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Server", "Server\RageCoop.Server.csproj", "{84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Server", "RageCoop.Server\RageCoop.Server.csproj", "{84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RageCoop.Client", "Client\RageCoop.Client.csproj", "{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Core", "RageCoop.Core\RageCoop.Core.csproj", "{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Core", "Core\RageCoop.Core.csproj", "{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RageCoop.Client", "RageCoop.Client\RageCoop.Client.csproj", "{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -25,14 +25,6 @@ Global
{84AB99D9-5E00-4CA2-B1DD-56B8419AAD24}.Release|Any CPU.Build.0 = Release|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|Any CPU.ActiveCfg = Debug|Any CPU
{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Debug|Any CPU.Build.0 = Debug|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|Any CPU.ActiveCfg = Release|Any CPU
{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Release|Any CPU.Build.0 = Release|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
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -41,6 +33,14 @@ Global
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|Any CPU.Build.0 = Release|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|x64.ActiveCfg = Release|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|x64.Build.0 = Release|Any CPU
{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Debug|Any CPU.Build.0 = Debug|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|Any CPU.ActiveCfg = Release|Any CPU
{EF56D109-1F22-43E0-9DFF-CFCFB94E0681}.Release|Any CPU.Build.0 = Release|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

View File

@ -1,57 +1,92 @@
# 🌐 RAGECOOP
The original author of this project is @EntenKoeniq.
The project has been reworked and is currently maintained by @Sardelka9515.
# **Help to survive the project**
#### The project is in active development as for now but may get discontinued if user's support is not present!
## if you are a developer...
Please **CONTRIBUTE** to the project and make RAGECOOP better together!
## otherwise...
**[Become a patreon](https://www.patreon.com/Sardelka)** to help keep the project alive and get exclusive support to individual issues.
<br><br><br>
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
# ⚠ Notice
The original author of this project is [EntenKoeniq](https://github.com/EntenKoeniq).
The project has been reworked and is currently maintained by [Sardelka9515](https://github.com/Sardelka9515).
To download the legacy versions, go to [this repository](https://github.com/RAGECOOP/RAGECOOP-V.OLD)
# 🧠 That's it
RAGECOOP is a multiplayer mod to play story mode or some mods made for RAGECOOP or just drive around with your buddy.
_Old name: GTACOOP:R_
# 📋 Requirements
- Visual Studio 2022
- .NET 6.0
- .NET Framework 4.8
# 📚 Libraries
- [ScriptHookVDotNet3](https://github.com/crosire/scripthookvdotnet/releases/tag/v3.4.0)
- [LemonUI.SHVDN3](https://github.com/justalemon/LemonUI/releases/tag/v1.6)
- Lidgren Network Custom (***PRIVATE***)
- - No new features (only improvements)
- [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/13.0.1)
- [ClearScript](https://github.com/microsoft/ClearScript)
- [SharpZipLib](https://github.com/icsharpcode/SharpZipLib)
# Features
1. Synchronized bullets
2. Synchronized vehicle/player/NPC
3. Synchronized Projectile
4. Basic ragdoll sync
3. Synchronized projectiles
4. Simple ragdoll sync
5. Smoother vehicle/ped movement.
6. Ownership based sync logic, carjacking is now working properly.
6. Ownership based sync logic, carjacking is now working (sort of).
7. Introduced SyncEvents.
8. Code refactoring and namespace cleanup
9. Synchronized vehicle doors, brake and throttle.
10. Other improvements
10. Weaponized vehicle sync(WIP).
11. Other improvements
# Known issues
1. Weapon sounds are missing.
2. Cover sync is still buggy.
3. Weaponized vehicle doen't work currently
4. Framerate drop with high number of synchronized entities.
3. Framerate drop with high number of synchronized entities.
5. Scripting API is screwed.(will be rewritten in the future)
# Installation
## Client
Download latest release, remove old version of the mod. Extract the zip and put `RageCoop` in `Grand Theft Auto V/Scripts`.
## Server
Download latest release for your OS, then extract and run.
## Installation
Refer to the [wiki](https://github.com/RAGECOOP/RAGECOOP-V/wiki)
# Downloads
Download latest release [here](https://github.com/RAGECOOP/RAGECOOP-V/releases/latest)
Old release can be downloaded [here](https://gitlab.com/justasausage/RageCOOP-V/-/tree/main/Release)
You can also download nightly builds [here](https://github.com/RAGECOOP/RAGECOOP-V/actions), which includes latest features and bug-fixes, but has not been thoroughly tested.
Select latest workflow run, scroll down to bottom then download the artifacts.
Please note that this is incompatible with all previous versions of ragecoop, remove old files before installing.
# Support us
<a href="https://patreon.com/Sardelka"><img src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3DSardelka%26type%3Dpatrons&style=for-the-badge" /></a>
# 🦆 Special thanks to
- [Makinolo](https://github.com/Makinolo), [oldnapalm](https://github.com/oldnapalm)
- - For testing, ideas, contributions and the first modification with the API
- [crosire](https://github.com/crosire)
- - For the extensive work in ScriptHookVDotNet
- [justalemon](https://github.com/justalemon)
- - For the extensive work in LemonUI
# 📝 License
This project is licensed under [MIT license](https://github.com/RAGECOOP/RAGECOOP-V/blob/main/LICENSE)
[contributors-shield]: https://img.shields.io/github/contributors/RAGECOOP/RAGECOOP-V.svg?style=for-the-badge
[contributors-url]: https://github.com/RAGECOOP/RAGECOOP-V/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/RAGECOOP/RAGECOOP-V.svg?style=for-the-badge
[forks-url]: https://github.com/RAGECOOP/RAGECOOP-V/network/members
[stars-shield]: https://img.shields.io/github/stars/RAGECOOP/RAGECOOP-V.svg?style=for-the-badge
[stars-url]: https://github.com/RAGECOOP/RAGECOOP-V/stargazers
[issues-shield]: https://img.shields.io/github/issues/RAGECOOP/RAGECOOP-V.svg?style=for-the-badge
[issues-url]: https://github.com/RAGECOOP/RAGECOOP-V/issues

View File

@ -130,7 +130,7 @@ namespace RageCoop.Client
s=$@"
// {ToMark.DisplayName}
case {ToMark.Model.Hash}:
i=Main.Ticked%2==0 ? {Current} : {Secondary};
i=BulletsShot%2==0 ? {Current} : {Secondary};
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].{dir}Vector);
";
}
@ -139,7 +139,7 @@ namespace RageCoop.Client
s=$@"
// {ToMark.DisplayName}
case {ToMark.Model.Hash}:
i=Main.Ticked%2==0 ? {Current} : {Secondary};
i=BulletsShot%2==0 ? {Current} : {Secondary};
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].{((MuzzleDir)(dir-3)).ToString()}Vector*-1);
";
}

View File

@ -0,0 +1,41 @@
// NO MERGE
// Taken from the .NET Runtime Repository
// https://github.com/dotnet/runtime
// Copyright (c) .NET Foundation and Contributors
// Under the MIT License
#if FIVEM
using System;
namespace LemonUI // Previously System.ComponentModel
{
/// <summary>
/// Represents the method that will handle the event raised when canceling an event.
/// </summary>
public delegate void CancelEventHandler(object sender, CancelEventArgs e);
/// <summary>
/// EventArgs used to describe a cancel event.
/// </summary>
public class CancelEventArgs : EventArgs
{
/// <summary>
/// Gets or sets a value indicating whether we should cancel the operation or not
/// </summary>
public bool Cancel { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public CancelEventArgs()
{
}
/// <summary>
/// Helper constructor
/// </summary>
/// <param name="cancel"></param>
public CancelEventArgs(bool cancel) => Cancel = cancel;
}
}
#endif

View File

@ -0,0 +1,138 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.Native;
#endif
using System.Collections.Generic;
namespace LemonUI
{
/// <summary>
/// Tools for dealing with controls.
/// </summary>
internal static class Controls
{
/// <summary>
/// Gets if the player used a controller for the last input.
/// </summary>
public static bool IsUsingController
{
get
{
#if FIVEM
return !API.IsInputDisabled(2);
#elif RAGEMP
return !Invoker.Invoke<bool>(Natives.IsInputDisabled, 2);
#elif RPH
return !NativeFunction.CallByHash<bool>(0xA571D46727E2B718, 2);
#elif SHVDN3
return !Function.Call<bool>(Hash._IS_USING_KEYBOARD, 2);
#endif
}
}
/// <summary>
/// Checks if a control was pressed during the last frame.
/// </summary>
/// <param name="control">The control to check.</param>
/// <returns>true if the control was pressed, false otherwise.</returns>
public static bool IsJustPressed(Control control)
{
#if FIVEM
return API.IsDisabledControlJustPressed(0, (int)control);
#elif RAGEMP
return Invoker.Invoke<bool>(Natives.IsDisabledControlJustPressed, 0, (int)control);
#elif RPH
return NativeFunction.CallByHash<bool>(0x91AEF906BCA88877, 0, (int)control);
#elif SHVDN3
return Function.Call<bool>(Hash.IS_DISABLED_CONTROL_JUST_PRESSED, 0, (int)control);
#endif
}
/// <summary>
/// Checks if a control is currently pressed.
/// </summary>
/// <param name="control">The control to check.</param>
/// <returns>true if the control is pressed, false otherwise.</returns>
public static bool IsPressed(Control control)
{
#if FIVEM
return API.IsDisabledControlPressed(0, (int)control);
#elif RAGEMP
return Invoker.Invoke<bool>(Natives.IsDisabledControlJustPressed, 0, (int)control);
#elif RPH
return NativeFunction.CallByHash<bool>(0xE2587F8CBBD87B1D, 0, (int)control);
#elif SHVDN3
return Function.Call<bool>(Hash.IS_DISABLED_CONTROL_PRESSED, 0, (int)control);
#endif
}
/// <summary>
/// Disables all of the controls during the next frame.
/// </summary>
public static void DisableAll(int inputGroup = 0)
{
#if FIVEM
API.DisableAllControlActions(inputGroup);
#elif RAGEMP
Invoker.Invoke(Natives.DisableAllControlActions, inputGroup);
#elif RPH
NativeFunction.CallByHash<int>(0x5F4B6931816E599B, inputGroup);
#elif SHVDN3
Function.Call(Hash.DISABLE_ALL_CONTROL_ACTIONS, inputGroup);
#endif
}
/// <summary>
/// Enables a control during the next frame.
/// </summary>
/// <param name="control">The control to enable.</param>
public static void EnableThisFrame(Control control)
{
#if FIVEM
API.EnableControlAction(0, (int)control, true);
#elif RAGEMP
Invoker.Invoke(Natives.EnableControlAction, 0, (int)control, true);
#elif RPH
NativeFunction.CallByHash<int>(0x351220255D64C155, 0, (int)control);
#elif SHVDN3
Function.Call(Hash.ENABLE_CONTROL_ACTION, 0, (int)control);
#endif
}
/// <summary>
/// Enables a specific set of controls during the next frame.
/// </summary>
/// <param name="controls">The controls to enable.</param>
public static void EnableThisFrame(IEnumerable<Control> controls)
{
foreach (Control control in controls)
{
EnableThisFrame(control);
}
}
/// <summary>
/// Disables a control during the next frame.
/// </summary>
/// <param name="control">The control to disable.</param>
public static void DisableThisFrame(Control control)
{
#if FIVEM
API.DisableControlAction(0, (int)control, true);
#elif RAGEMP
Invoker.Invoke(Natives.DisableControlAction, 0, (int)control, true);
#elif RPH
NativeFunction.CallByHash<int>(0xFE99B66D079CF6BC, 0, (int)control, true);
#elif SHVDN3
Function.Call(Hash.DISABLE_CONTROL_ACTION, 0, (int)control, true);
#endif
}
}
}

View File

@ -0,0 +1,23 @@
#if RPH
namespace LemonUI
{
/// <summary>
/// The alignment of the element to draw.
/// </summary>
public enum Alignment
{
/// <summary>
/// Aligns the element to the Center.
/// </summary>
Center = 0,
/// <summary>
/// Aligns the element to the Left.
/// </summary>
Left = 1,
/// <summary>
/// Aligns the element to the RIght.
/// </summary>
Right = 2,
}
}
#endif

View File

@ -0,0 +1,113 @@
using LemonUI.Extensions;
using System.Drawing;
namespace LemonUI.Elements
{
/// <summary>
/// Base class for all of the 2D elements.
/// </summary>
public abstract class BaseElement : I2Dimensional
{
#region Private Fields
/// <summary>
/// The 1080 scaled position.
/// </summary>
protected internal PointF literalPosition = PointF.Empty;
/// <summary>
/// The relative position between 0 and 1.
/// </summary>
protected internal PointF relativePosition = PointF.Empty;
/// <summary>
/// The 1080 scaled size.
/// </summary>
protected internal SizeF literalSize = SizeF.Empty;
/// <summary>
/// The relative size between 0 and 1.
/// </summary>
protected internal SizeF relativeSize = SizeF.Empty;
#endregion
#region Public Properties
/// <summary>
/// The Position of the drawable.
/// </summary>
public PointF Position
{
get
{
return literalPosition;
}
set
{
literalPosition = value;
Recalculate();
}
}
/// <summary>
/// The Size of the drawable.
/// </summary>
public SizeF Size
{
get
{
return literalSize;
}
set
{
literalSize = value;
Recalculate();
}
}
/// <summary>
/// The Color of the drawable.
/// </summary>
public Color Color { get; set; } = Color.FromArgb(255, 255, 255, 255);
/// <summary>
/// The rotation of the drawable.
/// </summary>
public float Heading { get; set; } = 0;
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="BaseElement"/> with the specified Position and Size.
/// </summary>
/// <param name="pos">The position of the Element.</param>
/// <param name="size">The size of the Element.</param>
public BaseElement(PointF pos, SizeF size)
{
literalPosition = pos;
literalSize = size;
Recalculate();
}
#endregion
#region Private Functions
/// <summary>
/// Recalculates the size and position of this item.
/// </summary>
public virtual void Recalculate()
{
relativePosition = literalPosition.ToRelative();
relativeSize = literalSize.ToRelative();
}
#endregion
#region Public Functions
/// <summary>
/// Draws the item on the screen.
/// </summary>
public abstract void Draw();
#endregion
}
}

View File

@ -0,0 +1,33 @@
// NO MERGE
#if RPH
namespace LemonUI.Elements
{
/// <summary>
/// An enum representing the fonts available in game.
/// </summary>
public enum Font
{
/// <summary>
/// Chalet London Nineteen Sixty.
/// </summary>
ChaletLondon = 0,
/// <summary>
/// SignPainter HouseScript Regular.
/// </summary>
HouseScript = 1,
/// <summary>
/// Unknown Monospaced Font.
/// </summary>
Monospace = 2,
/// <summary>
/// Chalet Comprime Cologne.
/// </summary>
ChaletComprimeCologne = 4,
/// <summary>
/// Pricedown.
/// </summary>
Pricedown = 7
}
}
#endif

View File

@ -0,0 +1,23 @@
using System.Drawing;
namespace LemonUI.Elements
{
/// <summary>
/// A 2D item that can be drawn on the screen.
/// </summary>
public interface I2Dimensional : IRecalculable, IDrawable
{
/// <summary>
/// The Position of the drawable.
/// </summary>
PointF Position { get; set; }
/// <summary>
/// The Size of the drawable.
/// </summary>
SizeF Size { get; set; }
/// <summary>
/// The Color of the drawable.
/// </summary>
Color Color { get; set; }
}
}

View File

@ -0,0 +1,68 @@
#if FIVEM
using Alignment = CitizenFX.Core.UI.Alignment;
using Font = CitizenFX.Core.UI.Font;
#elif RAGEMP
using RAGE.Game;
#elif SHVDN3
using Alignment = GTA.UI.Alignment;
using Font = GTA.UI.Font;
#endif
using System.Drawing;
namespace LemonUI.Elements
{
/// <summary>
/// A Drawable screen text.
/// </summary>
public interface IText : IRecalculable, IDrawable
{
/// <summary>
/// The position of the text.
/// </summary>
PointF Position { get; set; }
/// <summary>
/// The text to draw.
/// </summary>
string Text { get; set; }
/// <summary>
/// The color of the text.
/// </summary>
Color Color { get; set; }
/// <summary>
/// The game font to use.
/// </summary>
Font Font { get; set; }
/// <summary>
/// The scale of the text.
/// </summary>
float Scale { get; set; }
/// <summary>
/// If the text should have a drop down shadow.
/// </summary>
bool Shadow { get; set; }
/// <summary>
/// If the text should have an outline.
/// </summary>
bool Outline { get; set; }
/// <summary>
/// The alignment of the text.
/// </summary>
Alignment Alignment { get; set; }
/// <summary>
/// The maximum distance from X where the text would wrap into a new line.
/// </summary>
float WordWrap { get; set; }
/// <summary>
/// The width that the text takes from the screen.
/// </summary>
float Width { get; }
/// <summary>
/// The number of lines used by this text.
/// </summary>
int LineCount { get; }
/// <summary>
/// The height of each line of text.
/// </summary>
float LineHeight { get; }
}
}

View File

@ -0,0 +1,65 @@
#if FIVEM
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
#elif SHVDN3
using GTA.Native;
#endif
using System.Drawing;
namespace LemonUI.Elements
{
/// <summary>
/// A 2D rectangle.
/// </summary>
public class ScaledRectangle : BaseElement
{
#region Constructor
/// <summary>
/// Creates a new <see cref="ScaledRectangle"/> with the specified Position and Size.
/// </summary>
/// <param name="pos">The position of the Rectangle.</param>
/// <param name="size">The size of the Rectangle.</param>
public ScaledRectangle(PointF pos, SizeF size) : base(pos, size)
{
}
#endregion
#region Public Functions
/// <summary>
/// Draws the rectangle on the screen.
/// </summary>
public override void Draw()
{
if (Size == SizeF.Empty)
{
return;
}
#if FIVEM
API.DrawRect(relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Color.R, Color.G, Color.B, Color.A);
#elif RAGEMP
Invoker.Invoke(Natives.DrawRect, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Color.R, Color.G, Color.B, Color.A);
#elif RPH
NativeFunction.CallByHash<int>(0x3A618A217E5154F0, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Color.R, Color.G, Color.B, Color.A);
#elif SHVDN3
Function.Call(Hash.DRAW_RECT, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Color.R, Color.G, Color.B, Color.A);
#endif
}
/// <summary>
/// Recalculates the position based on the size.
/// </summary>
public override void Recalculate()
{
base.Recalculate();
relativePosition.X += relativeSize.Width * 0.5f;
relativePosition.Y += relativeSize.Height * 0.5f;
}
#endregion
}
}

View File

@ -0,0 +1,506 @@
#if FIVEM
using CitizenFX.Core.Native;
using CitizenFX.Core.UI;
using Font = CitizenFX.Core.UI.Font;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
#elif SHVDN3
using GTA.UI;
using GTA.Native;
using Font = GTA.UI.Font;
#endif
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using LemonUI.Extensions;
namespace LemonUI.Elements
{
/// <summary>
/// A text string.
/// </summary>
public class ScaledText : IText
{
#region Consistent Values
/// <summary>
/// The size of every chunk of text.
/// </summary>
private const int chunkSize = 90;
#endregion
#region Private Fields
/// <summary>
/// The absolute 1080p based screen position.
/// </summary>
private PointF absolutePosition = PointF.Empty;
/// <summary>
/// The relative 0-1 relative position.
/// </summary>
private PointF relativePosition = PointF.Empty;
/// <summary>
/// The raw string of text.
/// </summary>
private string text = string.Empty;
/// <summary>
/// The raw string split into equally sized strings.
/// </summary>
private List<string> chunks = new List<string>();
/// <summary>
/// The alignment of the item.
/// </summary>
private Alignment alignment = Alignment.Left;
/// <summary>
/// The word wrap value passed by the user.
/// </summary>
private float internalWrap = 0f;
/// <summary>
/// The real word wrap value based on the position of the text.
/// </summary>
private float realWrap = 0f;
#endregion
#region Public Properties
/// <summary>
/// The position of the text.
/// </summary>
public PointF Position
{
get => absolutePosition;
set
{
absolutePosition = value;
relativePosition = value.ToRelative();
}
}
/// <summary>
/// The text to draw.
/// </summary>
public string Text
{
get => text;
set
{
text = value;
Slice();
}
}
/// <summary>
/// The color of the text.
/// </summary>
public Color Color { get; set; } = Color.FromArgb(255, 255, 255, 255);
/// <summary>
/// The game font to use.
/// </summary>
public Font Font { get; set; } = Font.ChaletLondon;
/// <summary>
/// The scale of the text.
/// </summary>
public float Scale { get; set; } = 1f;
/// <summary>
/// If the text should have a drop down shadow.
/// </summary>
public bool Shadow { get; set; } = false;
/// <summary>
/// If the test should have an outline.
/// </summary>
public bool Outline { get; set; } = false;
/// <summary>
/// The alignment of the text.
/// </summary>
public Alignment Alignment
{
get => alignment;
set
{
alignment = value;
Recalculate();
}
}
/// <summary>
/// The distance from the start position where the text will be wrapped into new lines.
/// </summary>
public float WordWrap
{
get
{
return internalWrap;
}
set
{
internalWrap = value;
Recalculate();
}
}
/// <summary>
/// The width that the text takes from the screen.
/// </summary>
public float Width
{
get
{
#if FIVEM
API.BeginTextCommandWidth("CELL_EMAIL_BCON");
Add();
return API.EndTextCommandGetWidth(true) * 1f.ToXAbsolute();
#elif RAGEMP
Invoker.Invoke(Natives.BeginTextCommandWidth, "CELL_EMAIL_BCON");
Add();
return Invoker.Invoke<float>(Natives.EndTextCommandGetWidth) * 1f.ToXAbsolute();
#elif RPH
NativeFunction.CallByHash<int>(0x54CE8AC98E120CAB, "CELL_EMAIL_BCON");
Add();
return NativeFunction.CallByHash<float>(0x85F061DA64ED2F67, true) * 1f.ToXAbsolute();
#elif SHVDN3
Function.Call(Hash._BEGIN_TEXT_COMMAND_GET_WIDTH, "CELL_EMAIL_BCON");
Add();
return Function.Call<float>(Hash._END_TEXT_COMMAND_GET_WIDTH, true) * 1f.ToXAbsolute();
#endif
}
}
/// <summary>
/// The number of lines used by this text.
/// </summary>
public int LineCount
{
get
{
#if FIVEM
API.BeginTextCommandLineCount("CELL_EMAIL_BCON");
#elif RAGEMP
Invoker.Invoke(Natives.BeginTextCommandLineCount, "CELL_EMAIL_BCON");
#elif RPH
NativeFunction.CallByHash<int>(0x521FB041D93DD0E4, "CELL_EMAIL_BCON");
#elif SHVDN3
Function.Call(Hash._BEGIN_TEXT_COMMAND_LINE_COUNT, "CELL_EMAIL_BCON");
#endif
Add();
#if FIVEM
return API.EndTextCommandGetLineCount(relativePosition.X, relativePosition.Y);
#elif RAGEMP
return Invoker.Invoke<int>(Natives.EndTextCommandGetLineCount, relativePosition.X, relativePosition.Y);
#elif RPH
return NativeFunction.CallByHash<int>(0x9040DFB09BE75706, relativePosition.X, relativePosition.Y);
#elif SHVDN3
return Function.Call<int>(Hash._END_TEXT_COMMAND_LINE_COUNT, relativePosition.X, relativePosition.Y);
#endif
}
}
/// <summary>
/// The relative height of each line in the text.
/// </summary>
public float LineHeight
{
get
{
// Height will always be 1080
#if FIVEM
return 1080 * API.GetTextScaleHeight(Scale, (int)Font);
#elif RAGEMP
return 1080 * Invoker.Invoke<float>(Natives.GetTextScaleHeight, Scale, (int)Font);
#elif RPH
return 1080 * NativeFunction.CallByHash<float>(0xDB88A37483346780, Scale, (int)Font);
#elif SHVDN3
return 1080 * Function.Call<float>(Hash.GET_RENDERED_CHARACTER_HEIGHT, Scale, (int)Font);
#endif
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a text with the specified options.
/// </summary>
/// <param name="pos">The position where the text should be located.</param>
/// <param name="text">The text to show.</param>
public ScaledText(PointF pos, string text) : this(pos, text, 1f, Font.ChaletLondon)
{
}
/// <summary>
/// Creates a text with the specified options.
/// </summary>
/// <param name="pos">The position where the text should be located.</param>
/// <param name="text">The text to show.</param>
/// <param name="scale">The scale of the text.</param>
public ScaledText(PointF pos, string text, float scale) : this(pos, text, scale, Font.ChaletLondon)
{
}
/// <summary>
/// Creates a text with the specified options
/// </summary>
/// <param name="pos">The position where the text should be located.</param>
/// <param name="text">The text to show.</param>
/// <param name="scale">The scale of the text.</param>
/// <param name="font">The font to use.</param>
public ScaledText(PointF pos, string text, float scale, Font font)
{
Position = pos;
Text = text;
Scale = scale;
Font = font;
}
#endregion
#region Tools
/// <summary>
/// Adds the text information for measurement.
/// </summary>
private void Add()
{
if (Scale == 0)
{
return;
}
#if FIVEM
foreach (string chunk in chunks)
{
API.AddTextComponentString(chunk);
}
API.SetTextFont((int)Font);
API.SetTextScale(1f, Scale);
API.SetTextColour(Color.R, Color.G, Color.B, Color.A);
API.SetTextJustification((int)Alignment);
if (Shadow)
{
API.SetTextDropShadow();
}
if (Outline)
{
API.SetTextOutline();
}
if (WordWrap > 0)
{
switch (Alignment)
{
case Alignment.Center:
API.SetTextWrap(relativePosition.X - (realWrap * 0.5f), relativePosition.X + (realWrap * 0.5f));
break;
case Alignment.Left:
API.SetTextWrap(relativePosition.X, relativePosition.X + realWrap);
break;
case Alignment.Right:
API.SetTextWrap(relativePosition.X - realWrap, relativePosition.X);
break;
}
}
else if (Alignment == Alignment.Right)
{
API.SetTextWrap(0f, relativePosition.X);
}
#elif RAGEMP
foreach (string chunk in chunks)
{
Invoker.Invoke(Natives.AddTextComponentSubstringPlayerName, chunk);
}
Invoker.Invoke(Natives.SetTextFont, (int)Font);
Invoker.Invoke(Natives.SetTextScale, 1f, Scale);
Invoker.Invoke(Natives.SetTextColour, Color.R, Color.G, Color.B, Color.A);
Invoker.Invoke(Natives.SetTextJustification, (int)Alignment);
if (Shadow)
{
Invoker.Invoke(Natives.SetTextDropShadow);
}
if (Outline)
{
Invoker.Invoke(Natives.SetTextOutline);
}
if (WordWrap > 0)
{
switch (Alignment)
{
case Alignment.Center:
Invoker.Invoke(Natives.SetTextWrap, relativePosition.X - (realWrap * 0.5f), relativePosition.X + (realWrap * 0.5f));
break;
case Alignment.Left:
Invoker.Invoke(Natives.SetTextWrap, relativePosition.X, relativePosition.X + realWrap);
break;
case Alignment.Right:
Invoker.Invoke(Natives.SetTextWrap, relativePosition.X - realWrap, relativePosition.X);
break;
}
}
else if (Alignment == Alignment.Right)
{
Invoker.Invoke(0x63145D9C883A1A70, 0f, relativePosition.X);
}
#elif RPH
foreach (string chunk in chunks)
{
NativeFunction.CallByHash<int>(0x6C188BE134E074AA, chunk);
}
NativeFunction.CallByHash<int>(0x66E0276CC5F6B9DA, (int)Font);
NativeFunction.CallByHash<int>(0x07C837F9A01C34C9, 1f, Scale);
NativeFunction.CallByHash<int>(0xBE6B23FFA53FB442, Color.R, Color.G, Color.B, Color.A);
NativeFunction.CallByHash<int>(0x4E096588B13FFECA, (int)Alignment);
if (Shadow)
{
NativeFunction.CallByHash<int>(0x1CA3E9EAC9D93E5E);
}
if (Outline)
{
NativeFunction.CallByHash<int>(0x2513DFB0FB8400FE);
}
if (WordWrap > 0)
{
switch (Alignment)
{
case Alignment.Center:
NativeFunction.CallByHash<int>(0x63145D9C883A1A70, relativePosition.X - (realWrap * 0.5f), relativePosition.X + (realWrap * 0.5f));
break;
case Alignment.Left:
NativeFunction.CallByHash<int>(0x63145D9C883A1A70, relativePosition.X, relativePosition.X + realWrap);
break;
case Alignment.Right:
NativeFunction.CallByHash<int>(0x63145D9C883A1A70, relativePosition.X - realWrap, relativePosition.X);
break;
}
}
else if (Alignment == Alignment.Right)
{
NativeFunction.CallByHash<int>(0x63145D9C883A1A70, 0f, relativePosition.X);
}
#elif SHVDN3
foreach (string chunk in chunks)
{
Function.Call((Hash)0x6C188BE134E074AA, chunk); // _ADD_TEXT_COMPONENT_STRING on v2, ADD_TEXT_COMPONENT_SUBSTRING_PLAYER_NAME on v3
}
Function.Call(Hash.SET_TEXT_FONT, (int)Font);
Function.Call(Hash.SET_TEXT_SCALE, 1f, Scale);
Function.Call(Hash.SET_TEXT_COLOUR, Color.R, Color.G, Color.B, Color.A);
Function.Call(Hash.SET_TEXT_JUSTIFICATION, (int)Alignment);
if (Shadow)
{
Function.Call(Hash.SET_TEXT_DROP_SHADOW);
}
if (Outline)
{
Function.Call(Hash.SET_TEXT_OUTLINE);
}
if (WordWrap > 0)
{
switch (Alignment)
{
case Alignment.Center:
Function.Call(Hash.SET_TEXT_WRAP, relativePosition.X - (realWrap * 0.5f), relativePosition.X + (realWrap * 0.5f));
break;
case Alignment.Left:
Function.Call(Hash.SET_TEXT_WRAP, relativePosition.X, relativePosition.X + realWrap);
break;
case Alignment.Right:
Function.Call(Hash.SET_TEXT_WRAP, relativePosition.X - realWrap, relativePosition.X);
break;
}
}
else if (Alignment == Alignment.Right)
{
Function.Call(Hash.SET_TEXT_WRAP, 0f, relativePosition.X);
}
#endif
}
/// <summary>
/// Slices the string of text into appropiately saved chunks.
/// </summary>
private void Slice()
{
// If the entire text is under 90 bytes, save it as is and return
if (Encoding.UTF8.GetByteCount(text) <= chunkSize)
{
chunks.Clear();
chunks.Add(text);
return;
}
// Create a new list of chunks and a temporary string
List<string> newChunks = new List<string>();
string temp = string.Empty;
// Iterate over the characters in the string
foreach (char character in text)
{
// Create a temporary string with the character
string with = string.Concat(temp, character);
// If this string is higher than 90 bytes, add the existing string onto the list
if (Encoding.UTF8.GetByteCount(with) > chunkSize)
{
newChunks.Add(temp);
temp = character.ToString();
continue;
}
// And save the new string generated
temp = with;
}
// If after finishing we still have a piece, save it
if (temp != string.Empty)
{
newChunks.Add(temp);
}
// Once we have finished, replace the old chunks
chunks = newChunks;
}
/// <summary>
/// Recalculates the size, position and word wrap of this item.
/// </summary>
public void Recalculate()
{
// Do the normal Size and Position recalculation
relativePosition = absolutePosition.ToRelative();
// And recalculate the word wrap if necessary
if (internalWrap <= 0)
{
realWrap = 0;
}
else
{
realWrap = internalWrap.ToXRelative();
}
}
#endregion
#region Public Functions
/// <summary>
/// Draws the text on the screen.
/// </summary>
public void Draw()
{
#if FIVEM
API.SetTextEntry("CELL_EMAIL_BCON");
#elif RAGEMP
Invoker.Invoke(Natives.BeginTextCommandDisplayText, "CELL_EMAIL_BCON");
#elif RPH
NativeFunction.CallByHash<int>(0x25FBB336DF1804CB, "CELL_EMAIL_BCON");
#elif SHVDN3
Function.Call((Hash)0x25FBB336DF1804CB, "CELL_EMAIL_BCON"); // _SET_TEXT_ENTRY on v2, BEGIN_TEXT_COMMAND_DISPLAY_TEXT on v3
#endif
Add();
#if FIVEM
API.DrawText(relativePosition.X, relativePosition.Y);
#elif RAGEMP
Invoker.Invoke(Natives.DrawDebugText, relativePosition.X, relativePosition.Y);
#elif RPH
NativeFunction.CallByHash<int>(0xCD015E5BB0D96A57, relativePosition.X, relativePosition.Y);
#elif SHVDN3
Function.Call((Hash)0xCD015E5BB0D96A57, relativePosition.X, relativePosition.Y); // _DRAW_TEXT on v2, END_TEXT_COMMAND_DISPLAY_TEXT on v3
#endif
}
#endregion
}
}

View File

@ -0,0 +1,144 @@
#if FIVEM
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
#elif SHVDN3
using GTA.Native;
#endif
using System.Drawing;
namespace LemonUI.Elements
{
/// <summary>
/// A 2D game texture.
/// </summary>
public class ScaledTexture : BaseElement
{
#region Public Properties
/// <summary>
/// The dictionary where the texture is loaded.
/// </summary>
public string Dictionary { get; set; }
/// <summary>
/// The texture to draw from the dictionary.
/// </summary>
public string Texture { get; set; }
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="ScaledTexture"/> with a Position and Size of Zero.
/// </summary>
/// <param name="dictionary">The dictionary where the texture is located.</param>
/// <param name="texture">The texture to draw.</param>
public ScaledTexture(string dictionary, string texture) : this(PointF.Empty, SizeF.Empty, dictionary, texture)
{
}
/// <summary>
/// Creates a new <see cref="ScaledTexture"/> with a Position and Size of zero.
/// </summary>
/// <param name="pos">The position of the Texture.</param>
/// <param name="size">The size of the Texture.</param>
/// <param name="dictionary">The dictionary where the texture is located.</param>
/// <param name="texture">The texture to draw.</param>
public ScaledTexture(PointF pos, SizeF size, string dictionary, string texture) : base(pos, size)
{
Dictionary = dictionary;
Texture = texture;
Request();
}
#endregion
#region Private Functions
/// <summary>
/// Requests the texture dictionary for this class.
/// </summary>
private void Request()
{
#if FIVEM
if (!API.HasStreamedTextureDictLoaded(Dictionary))
{
API.RequestStreamedTextureDict(Dictionary, true);
}
#elif RAGEMP
if (!Invoker.Invoke<bool>(Natives.HasStreamedTextureDictLoaded, Dictionary))
{
Invoker.Invoke(Natives.RequestStreamedTextureDict, Dictionary, true);
}
#elif RPH
if (!NativeFunction.CallByHash<bool>(0x0145F696AAAAD2E4, Dictionary))
{
NativeFunction.CallByHash<int>(0xDFA2EF8E04127DD5, Dictionary, true);
}
#elif SHVDN3
if (!Function.Call<bool>(Hash.HAS_STREAMED_TEXTURE_DICT_LOADED, Dictionary))
{
Function.Call(Hash.REQUEST_STREAMED_TEXTURE_DICT, Dictionary, true);
}
#endif
}
#endregion
#region Public Functions
/// <summary>
/// Draws the texture on the screen.
/// </summary>
public override void Draw()
{
if (Size == SizeF.Empty)
{
return;
}
Request();
#if FIVEM
API.DrawSprite(Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Heading, Color.R, Color.G, Color.B, Color.A);
#elif RAGEMP
Invoker.Invoke(Natives.DrawSprite, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Heading, Color.R, Color.G, Color.B, Color.A);
#elif RPH
NativeFunction.CallByHash<int>(0xE7FFAE5EBF23D890, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Heading, Color.R, Color.G, Color.B, Color.A);
#elif SHVDN3
Function.Call(Hash.DRAW_SPRITE, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, Heading, Color.R, Color.G, Color.B, Color.A);
#endif
}
/// <summary>
/// Draws a specific part of the texture on the screen.
/// </summary>
public void DrawSpecific(PointF topLeft, PointF bottomRight)
{
if (Size == SizeF.Empty)
{
return;
}
Request();
#if FIVEM
API.DrawSpriteUv(Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y, Heading, Color.R, Color.G, Color.B, Color.A);
#elif RAGEMP
Invoker.Invoke(0x95812F9B26074726, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y, Heading, Color.R, Color.G, Color.B, Color.A);
#elif RPH
NativeFunction.CallByHash<int>(0x95812F9B26074726, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y, Heading, Color.R, Color.G, Color.B, Color.A);
#elif SHVDN3
Function.Call((Hash)0x95812F9B26074726, Dictionary, Texture, relativePosition.X, relativePosition.Y, relativeSize.Width, relativeSize.Height, topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y, Heading, Color.R, Color.G, Color.B, Color.A);
#endif
}
/// <summary>
/// Recalculates the position based on the size.
/// </summary>
public override void Recalculate()
{
base.Recalculate();
relativePosition.X += relativeSize.Width * 0.5f;
relativePosition.Y += relativeSize.Height * 0.5f;
}
#endregion
}
}

View File

@ -0,0 +1,49 @@
namespace LemonUI.Extensions
{
/// <summary>
/// Extensions for the float class.
/// </summary>
public static class FloatExtensions
{
/// <summary>
/// Converts an absolute X or Width float to a relative one.
/// </summary>
/// <param name="fin">The float to convert.</param>
/// <returns>A relative float between 0 and 1.</returns>
public static float ToXRelative(this float fin)
{
Screen.ToRelative(fin, 0, out float fout, out _);
return fout;
}
/// <summary>
/// Converts an absolute Y or Height float to a relative one.
/// </summary>
/// <param name="fin">The float to convert.</param>
/// <returns>A relative float between 0 and 1.</returns>
public static float ToYRelative(this float fin)
{
Screen.ToRelative(0, fin, out _, out float fout);
return fout;
}
/// <summary>
/// Converts an relative X or Width float to an absolute one.
/// </summary>
/// <param name="fin">The float to convert.</param>
/// <returns>An absolute float.</returns>
public static float ToXAbsolute(this float fin)
{
Screen.ToAbsolute(fin, 0, out float fout, out _);
return fout;
}
/// <summary>
/// Converts an relative Y or Height float to an absolute one.
/// </summary>
/// <param name="fin">The float to convert.</param>
/// <returns>An absolute float.</returns>
public static float ToYAbsolute(this float fin)
{
Screen.ToAbsolute(0, fin, out _, out float fout);
return fout;
}
}
}

View File

@ -0,0 +1,31 @@
using System.Drawing;
namespace LemonUI.Extensions
{
/// <summary>
/// Extensions for the Point and PointF classes.
/// </summary>
public static class PointExtensions
{
/// <summary>
/// Converts an absolute 1080-based position into a relative one.
/// </summary>
/// <param name="point">The absolute PointF.</param>
/// <returns>A new PointF with relative values.</returns>
public static PointF ToRelative(this PointF point)
{
Screen.ToRelative(point.X, point.Y, out float x, out float y);
return new PointF(x, y);
}
/// <summary>
/// Converts a normalized 0-1 position into an absolute one.
/// </summary>
/// <param name="point">The relative PointF.</param>
/// <returns>A new PointF with absolute values.</returns>
public static PointF ToAbsolute(this PointF point)
{
Screen.ToAbsolute(point.X, point.Y, out float x, out float y);
return new PointF(x, y);
}
}
}

View File

@ -0,0 +1,31 @@
using System.Drawing;
namespace LemonUI.Extensions
{
/// <summary>
/// Extensions for the Size and SizeF classes.
/// </summary>
public static class SizeExtensions
{
/// <summary>
/// Converts an absolute 1080-based size into a relative one.
/// </summary>
/// <param name="size">The absolute SizeF.</param>
/// <returns>A new SizeF with relative values.</returns>
public static SizeF ToRelative(this SizeF size)
{
Screen.ToRelative(size.Width, size.Height, out float width, out float height);
return new SizeF(width, height);
}
/// <summary>
/// Converts a normalized 0-1 size into an absolute one.
/// </summary>
/// <param name="size">The relative SizeF.</param>
/// <returns>A new SizeF with absolute values.</returns>
public static SizeF ToAbsolute(this SizeF size)
{
Screen.ToAbsolute(size.Width, size.Height, out float width, out float height);
return new SizeF(width, height);
}
}
}

View File

@ -0,0 +1,36 @@
using System;
namespace LemonUI
{
/// <summary>
/// Represents a container that can hold other UI Elements.
/// </summary>
public interface IContainer<T> : IRecalculable, IProcessable
{
/// <summary>
/// Adds the specified item into the Container.
/// </summary>
/// <param name="item">The item to add.</param>
void Add(T item);
/// <summary>
/// Removes the item from the container.
/// </summary>
/// <param name="item">The item to remove.</param>
void Remove(T item);
/// <summary>
/// Removes all of the items that match the function.
/// </summary>
/// <param name="func">The function to check items.</param>
void Remove(Func<T, bool> func);
/// <summary>
/// Clears all of the items in the container.
/// </summary>
void Clear();
/// <summary>
/// Checks if the item is part of the container.
/// </summary>
/// <param name="item">The item to check.</param>
/// <returns><see langword="true"/> if the item is in this container, <see langword="false"/> otherwise.</returns>
bool Contains(T item);
}
}

View File

@ -0,0 +1,13 @@
namespace LemonUI
{
/// <summary>
/// Represents an item that can be drawn.
/// </summary>
public interface IDrawable
{
/// <summary>
/// Draws the item on the screen.
/// </summary>
void Draw();
}
}

View File

@ -0,0 +1,17 @@
namespace LemonUI
{
/// <summary>
/// Interface for items that can be processed in an Object Pool.
/// </summary>
public interface IProcessable
{
/// <summary>
/// If this processable item is visible on the screen.
/// </summary>
bool Visible { get; set; }
/// <summary>
/// Processes the object.
/// </summary>
void Process();
}
}

View File

@ -0,0 +1,13 @@
namespace LemonUI
{
/// <summary>
/// Interface for classes that have values that need to be recalculated on resolution changes.
/// </summary>
public interface IRecalculable
{
/// <summary>
/// Recalculates the values.
/// </summary>
void Recalculate();
}
}

View File

@ -0,0 +1,67 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents a badge that can be applied to a <see cref="NativeItem"/>.
/// </summary>
public class BadgeSet
{
#region Properties
/// <summary>
/// The texture dictionary where the normal texture is located.
/// </summary>
public string NormalDictionary { get; set; } = string.Empty;
/// <summary>
/// The texture to use when the item is not hovered.
/// </summary>
public string NormalTexture { get; set; } = string.Empty;
/// <summary>
/// The texture dictionary where the normal texture is located.
/// </summary>
public string HoveredDictionary { get; set; } = string.Empty;
/// <summary>
/// The texture to use when the item is hovered.
/// </summary>
public string HoveredTexture { get; set; } = string.Empty;
#endregion
#region Constructor
/// <summary>
/// Creates a new empty <see cref="BadgeSet"/>.
/// </summary>
public BadgeSet()
{
}
/// <summary>
/// Creates a new <see cref="BadgeSet"/> where both textures are in the same dictionary.
/// </summary>
/// <param name="dict">The dictionary where the textures are located.</param>
/// <param name="normal">The normal texture name.</param>
/// <param name="hovered">The hovered texture name.</param>
public BadgeSet(string dict, string normal, string hovered)
{
NormalDictionary = dict;
NormalTexture = normal;
HoveredDictionary = dict;
HoveredTexture = hovered;
}
/// <summary>
/// Creates a new <see cref="BadgeSet"/> where both textures are in different dictionaries.
/// </summary>
/// <param name="normalDict">The dictionary where the normal texture is located.</param>
/// <param name="normalTexture">The normal texture name.</param>
/// <param name="hoveredDict">The dictionary where the hovered texture is located.</param>
/// <param name="hoveredTexture">The hovered texture name.</param>
public BadgeSet(string normalDict, string normalTexture, string hoveredDict, string hoveredTexture)
{
NormalDictionary = normalDict;
NormalTexture = normalTexture;
HoveredDictionary = hoveredDict;
HoveredTexture = hoveredTexture;
}
#endregion
}
}

View File

@ -0,0 +1,101 @@
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Stores the different colors required to make the colors of a <see cref="NativeItem"/> dynamic.
/// </summary>
public class ColorSet
{
#region Fields
private static readonly Color colorWhite = Color.FromArgb(255, 255, 255, 255);
private static readonly Color colorWhiteSmoke = Color.FromArgb(255, 245, 245, 245);
private static readonly Color colorBlack = Color.FromArgb(255, 0, 0, 0);
private static readonly Color colorDisabled = Color.FromArgb(255, 163, 159, 148);
#endregion
#region Properties
/// <summary>
/// The color of the <see cref="NativeItem.Title"/> when the <see cref="NativeItem"/> is not hovered and enabled.
/// </summary>
public Color TitleNormal { get; set; } = colorWhiteSmoke;
/// <summary>
/// The color of the <see cref="NativeItem.Title"/> when the <see cref="NativeItem"/> is hovered.
/// </summary>
public Color TitleHovered { get; set; } = colorBlack;
/// <summary>
/// The color of the <see cref="NativeItem.Title"/> when the <see cref="NativeItem"/> is disabled.
/// </summary>
public Color TitleDisabled { get; set; } = colorDisabled;
/// <summary>
/// The color of the <see cref="NativeItem.AltTitle"/> when the <see cref="NativeItem"/> is not hovered and enabled.
/// </summary>
public Color AltTitleNormal { get; set; } = colorWhiteSmoke;
/// <summary>
/// The color of the <see cref="NativeItem.AltTitle"/> when the <see cref="NativeItem"/> is hovered.
/// </summary>
public Color AltTitleHovered { get; set; } = colorBlack;
/// <summary>
/// The color of the <see cref="NativeItem.AltTitle"/> when the <see cref="NativeItem"/> is disabled.
/// </summary>
public Color AltTitleDisabled { get; set; } = colorDisabled;
/// <summary>
/// The color of the <see cref="NativeSlidableItem"/> arrows when the item is not hovered and enabled.
/// </summary>
public Color ArrowsNormal { get; set; } = colorWhiteSmoke;
/// <summary>
/// The color of the <see cref="NativeSlidableItem"/> arrows when the item is hovered.
/// </summary>
public Color ArrowsHovered { get; set; } = colorBlack;
/// <summary>
/// The color of the <see cref="NativeSlidableItem"/> arrows when the item is disabled.
/// </summary>
public Color ArrowsDisabled { get; set; } = colorDisabled;
/// <summary>
/// The color of the <see cref="NativeItem.LeftBadge"/> when the <see cref="NativeItem"/> is not hovered and enabled.
/// </summary>
public Color BadgeLeftNormal { get; set; } = colorWhite;
/// <summary>
/// The color of the <see cref="NativeItem.LeftBadge"/> when the <see cref="NativeItem"/> is hovered.
/// </summary>
public Color BadgeLeftHovered { get; set; } = colorWhite;
/// <summary>
/// The color of the <see cref="NativeItem.LeftBadge"/> when the <see cref="NativeItem"/> is disabled.
/// </summary>
public Color BadgeLeftDisabled { get; set; } = colorWhite;
/// <summary>
/// The color of the <see cref="NativeItem.RightBadge"/> or <see cref="NativeCheckboxItem"/> checkbox when the <see cref="NativeItem"/> is not hovered and enabled.
/// </summary>
public Color BadgeRightNormal { get; set; } = colorWhite;
/// <summary>
/// The color of the <see cref="NativeItem.RightBadge"/> or <see cref="NativeCheckboxItem"/> checkbox when the <see cref="NativeItem"/> is hovered.
/// </summary>
public Color BadgeRightHovered { get; set; } = colorWhite;
/// <summary>
/// The color of the <see cref="NativeItem.RightBadge"/> or <see cref="NativeCheckboxItem"/> checkbox when the <see cref="NativeItem"/> is disabled.
/// </summary>
public Color BadgeRightDisabled { get; set; } = colorWhite;
/// <summary>
/// The normal color of the custom background if <see cref="NativeItem.UseCustomBackground"/> is set to <see langword="true"/>.
/// </summary>
public Color BackgroundNormal { get; set; } = colorWhite;
/// <summary>
/// The hovered color of the custom background if <see cref="NativeItem.UseCustomBackground"/> is set to <see langword="true"/>.
/// </summary>
public Color BackgroundHovered { get; set; } = colorWhite;
/// <summary>
/// The disabled color of the custom background if <see cref="NativeItem.UseCustomBackground"/> is set to <see langword="true"/>.
/// </summary>
public Color BackgroundDisabled { get; set; } = colorWhite;
#endregion
}
}

View File

@ -0,0 +1,22 @@
namespace LemonUI.Menus
{
/// <summary>
/// The Style of title for the Color Panel.
/// </summary>
public enum ColorTitleStyle
{
/// <summary>
/// Does not shows any Title.
/// The count will still be shown if <see cref="NativeColorPanel.ShowCount"/> is set to <see langword="true"/>.
/// </summary>
None = -1,
/// <summary>
/// Shows a Simple Title for all of the Colors.
/// </summary>
Simple = 0,
/// <summary>
/// Shows the Color Name as the Title.
/// </summary>
ColorName = 1
}
}

View File

@ -0,0 +1,21 @@
namespace LemonUI.Menus
{
/// <summary>
/// The visibility setting for the Item Count of the Menu.
/// </summary>
public enum CountVisibility
{
/// <summary>
/// The Item Count is never shown.
/// </summary>
Never = -1,
/// <summary>
/// The Item Count is shown when is not possible to show all of the items in the screen.
/// </summary>
Auto = 0,
/// <summary>
/// The Item Count is always shown.
/// </summary>
Always = 1
}
}

View File

@ -0,0 +1,21 @@
namespace LemonUI.Menus
{
/// <summary>
/// The movement direction of the item change.
/// </summary>
public enum Direction
{
/// <summary>
/// The Direction is Unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The item was moved to the Left.
/// </summary>
Left = 1,
/// <summary>
/// The item was moved to the Right.
/// </summary>
Right = 2,
}
}

View File

@ -0,0 +1,21 @@
namespace LemonUI.Menus
{
/// <summary>
/// The style of the Grid Panel.
/// </summary>
public enum GridStyle
{
/// <summary>
/// The full grid with X and Y values.
/// </summary>
Full = 0,
/// <summary>
/// A single row on the center with the X value only.
/// </summary>
Row = 1,
/// <summary>
/// A single column on the center with the Y value only.
/// </summary>
Column = 2,
}
}

View File

@ -0,0 +1,25 @@
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Represents the Previous and Current X and Y values when changing the position on a grid.
/// </summary>
public class GridValueChangedArgs
{
/// <summary>
/// The values present before they were changed.
/// </summary>
public PointF Before { get; }
/// <summary>
/// The values present after they were changed.
/// </summary>
public PointF After { get; }
internal GridValueChangedArgs(PointF before, PointF after)
{
Before = before;
After = after;
}
}
}

View File

@ -0,0 +1,9 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the method that is called when the value on a grid is changed.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An <see cref="ItemActivatedArgs"/> with the item information.</param>
public delegate void GridValueChangedEventHandler(object sender, GridValueChangedArgs e);
}

View File

@ -0,0 +1,18 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the arguments of an item activation.
/// </summary>
public class ItemActivatedArgs
{
/// <summary>
/// The item that was just activated.
/// </summary>
public NativeItem Item { get; }
internal ItemActivatedArgs(NativeItem item)
{
Item = item;
}
}
}

View File

@ -0,0 +1,9 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the method that is called when an item is activated on a menu.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An <see cref="ItemActivatedArgs"/> with the item information.</param>
public delegate void ItemActivatedEventHandler(object sender, ItemActivatedArgs e);
}

View File

@ -0,0 +1,29 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the change of the selection of an item.
/// </summary>
/// <typeparam name="T">The type of object that got changed.</typeparam>
public class ItemChangedEventArgs<T>
{
/// <summary>
/// The new object.
/// </summary>
public T Object { get; set; }
/// <summary>
/// The index of the object.
/// </summary>
public int Index { get; }
/// <summary>
/// The direction of the Item Changed event.
/// </summary>
public Direction Direction { get; }
internal ItemChangedEventArgs(T obj, int index, Direction direction)
{
Object = obj;
Index = index;
Direction = direction;
}
}
}

View File

@ -0,0 +1,10 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the method that is called when the selected item is changed on a List Item.
/// </summary>
/// <typeparam name="T">The type of item that was changed.</typeparam>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="ItemChangedEventArgs{T}"/> with the information of the selected item.</param>
public delegate void ItemChangedEventHandler<T>(object sender, ItemChangedEventArgs<T> e);
}

View File

@ -0,0 +1,17 @@
namespace LemonUI.Menus
{
/// <summary>
/// The operation performed when the menu items are modified.
/// </summary>
public enum ItemOperation
{
/// <summary>
/// The item has been removed.
/// </summary>
Removed = -1,
/// <summary>
/// The item has been added.
/// </summary>
Added = 0,
}
}

View File

@ -0,0 +1,36 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the different
/// </summary>
public class MenuModifiedEventArgs
{
#region Properties
/// <summary>
/// The item that was modified.
/// </summary>
public NativeItem Item { get; }
/// <summary>
/// The operation that was performed in the item.
/// </summary>
public ItemOperation Operation { get; }
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="MenuModifiedEventArgs"/>.
/// </summary>
/// <param name="item">The item that was modified.</param>
/// <param name="operation">The operation that was performed in the item.</param>
public MenuModifiedEventArgs(NativeItem item, ItemOperation operation)
{
Item = item;
Operation = operation;
}
#endregion
}
}

View File

@ -0,0 +1,9 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the method that is called when the items on a menu are changed (added or removed).
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An <see cref="MenuModifiedEventArgs"/> with the menu operation.</param>
public delegate void MenuModifiedEventHandler(object sender, MenuModifiedEventArgs e);
}

View File

@ -0,0 +1,174 @@
using LemonUI.Elements;
using System;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Rockstar-like checkbox item.
/// </summary>
public class NativeCheckboxItem : NativeItem
{
#region Fields
/// <summary>
/// The image shown on the checkbox.
/// </summary>
internal protected ScaledTexture check = new ScaledTexture(PointF.Empty, SizeF.Empty, "commonmenu", string.Empty);
/// <summary>
/// If this item is checked or not.
/// </summary>
private bool checked_ = false;
#endregion
#region Properties
/// <summary>
/// If this item is checked or not.
/// </summary>
public bool Checked
{
get => checked_;
set
{
if (checked_ == value)
{
return;
}
checked_ = value;
UpdateTexture(lastSelected);
CheckboxChanged?.Invoke(this, EventArgs.Empty);
}
}
#endregion
#region Events
/// <summary>
/// Event triggered when the checkbox changes.
/// </summary>
public event EventHandler CheckboxChanged;
#endregion
#region Constructor
/// <summary>
/// Creates a new <see cref="NativeCheckboxItem"/>.
/// </summary>
/// <param name="title">The title used for the Item.</param>
public NativeCheckboxItem(string title) : this(title, string.Empty, false)
{
}
/// <summary>
/// Creates a new <see cref="NativeCheckboxItem"/>.
/// </summary>
/// <param name="title">The title used for the Item.</param>
/// <param name="check">If the checkbox should be enabled or not.</param>
public NativeCheckboxItem(string title, bool check) : this(title, string.Empty, check)
{
}
/// <summary>
/// Creates a new <see cref="NativeCheckboxItem"/>.
/// </summary>
/// <param name="title">The title used for the Item.</param>
/// <param name="description">The description of the Item.</param>
public NativeCheckboxItem(string title, string description) : this(title, description, false)
{
}
/// <summary>
/// Creates a new <see cref="NativeCheckboxItem"/>.
/// </summary>
/// <param name="title">The title used for the Item.</param>
/// <param name="description">The description of the Item.</param>
/// <param name="check">If the checkbox should be enabled or not.</param>
public NativeCheckboxItem(string title, string description, bool check) : base(title, description)
{
Checked = check;
Activated += Toggle;
EnabledChanged += NativeCheckboxItem_EnabledChanged;
}
#endregion
#region Local Events
private void NativeCheckboxItem_EnabledChanged(object sender, EventArgs e) => UpdateTexture(lastSelected);
#endregion
#region Internal Functions
/// <summary>
/// Inverts the checkbox activation.
/// </summary>
private void Toggle(object sender, EventArgs e) => Checked = !Checked;
/// <summary>
/// Updates the texture of the sprite.
/// </summary>
internal protected void UpdateTexture(bool selected)
{
// If the item is not selected or is not enabled, use the white pictures
if (!selected || !Enabled)
{
check.Texture = Checked ? "shop_box_tick" : "shop_box_blank";
}
// Otherwise, use the black ones
else
{
check.Texture = Checked ? "shop_box_tickb" : "shop_box_blankb";
}
}
#endregion
#region Public Functions
/// <summary>
/// Recalculates the item positions and sizes with the specified values.
/// </summary>
/// <param name="pos">The position of the item.</param>
/// <param name="size">The size of the item.</param>
/// <param name="selected">If this item has been selected.</param>
public override void Recalculate(PointF pos, SizeF size, bool selected)
{
base.Recalculate(pos, size, selected);
// Set the correct texture
UpdateTexture(selected);
// And set the checkbox positions
check.Position = new PointF(pos.X + size.Width - 50, pos.Y - 6);
check.Size = new SizeF(50, 50);
}
/// <summary>
/// Draws the Checkbox on the screen.
/// </summary>
public override void Draw()
{
title.Draw();
badgeLeft?.Draw();
check.Draw();
}
/// <inheritdoc/>
public override void UpdateColors()
{
base.UpdateColors();
if (!Enabled)
{
check.Color = Colors.BadgeRightDisabled;
}
else if (lastSelected)
{
check.Color = Colors.BadgeRightHovered;
}
else
{
check.Color = Colors.BadgeRightNormal;
}
}
#endregion
}
}

View File

@ -0,0 +1,49 @@
using System.Drawing;
using LemonUI.Elements;
namespace LemonUI.Menus
{
/// <summary>
/// Represents the Color Information shown on the Panel.
/// </summary>
public class NativeColorData
{
#region Internal Fields
internal readonly ScaledRectangle rectangle = new ScaledRectangle(PointF.Empty, SizeF.Empty);
#endregion
#region Public Properties
/// <summary>
/// The name of the color.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The RGBA values of the color.
/// </summary>
public Color Color
{
get => rectangle.Color;
set => rectangle.Color = value;
}
#endregion
#region Constructor
/// <summary>
/// Creates a new Color Panel information.
/// </summary>
/// <param name="name">The name of the color.</param>
/// <param name="color">The RGBA values of the color.</param>
public NativeColorData(string name, Color color)
{
Name = name;
rectangle.Color = color;
}
#endregion
}
}

View File

@ -0,0 +1,678 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.UI;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.UI;
#endif
using LemonUI.Elements;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// A Panel that allows you to select a Color.
/// </summary>
public class NativeColorPanel : NativePanel
{
#region Constants
/// <summary>
/// The space difference for the colors and opacity bar on the left.
/// </summary>
private const float leftDifference = 16;
/// <summary>
/// The space difference for the colors and opacity bar on the left.
/// </summary>
private const float rightDifference = 12;
#endregion
#region Private Fields
/// <summary>
/// The position reported after the last Recalculation.
/// </summary>
private PointF lastPosition = PointF.Empty;
/// <summary>
/// The Width reported after the last Recalculation.
/// </summary>
private float lastWidth = 0;
/// <summary>
/// The title of the Color Panel.
/// </summary>
private ScaledText title = new ScaledText(PointF.Empty, string.Empty, 0.325f)
{
Alignment = Alignment.Center
};
/// <summary>
/// The rectangle used for marking the item selection on the screen.
/// </summary>
private ScaledRectangle selectionRectangle = new ScaledRectangle(PointF.Empty, SizeF.Empty);
/// <summary>
/// The "Opacity" text when the opacity bar is enabled
/// </summary>
private ScaledText opacityText = new ScaledText(PointF.Empty, "Opacity", 0.325f)
{
Alignment = Alignment.Center
};
/// <summary>
/// The zero percent text when the opacity bar is enabled.
/// </summary>
private ScaledText percentMin = new ScaledText(PointF.Empty, "0%", 0.325f);
/// <summary>
/// The 100 percent text when the opacity bar is enabled.
/// </summary>
private ScaledText percentMax = new ScaledText(PointF.Empty, "100%", 0.325f);
/// <summary>
/// The top section of the opacity bar.
/// </summary>
private ScaledRectangle opacityForeground = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 240, 240, 240)
};
/// <summary>
/// The background of the opacity bar.
/// </summary>
private ScaledRectangle opacityBackground = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(150, 88, 88, 88)
};
/// <summary>
/// If the opacity bar is available to the user.
/// </summary>
private bool showOpacity = false;
/// <summary>
/// The current value of the opacity slider.
/// </summary>
private int opacity = 0;
/// <summary>
/// The current index of the Colors.
/// </summary>
private int index = 0;
/// <summary>
/// The position of the first item.
/// </summary>
private int firstItem = 0;
/// <summary>
/// The maximum number of items shown at once.
/// </summary>
private int maxItems = 9;
/// <summary>
/// The generic title for this color.
/// </summary>
private string simpleTitle = "Color";
/// <summary>
/// The style of the title.
/// </summary>
private ColorTitleStyle titleStyle = ColorTitleStyle.Simple;
/// <summary>
/// If the number of colors should be shown.
/// </summary>
private bool showCount = true;
/// <summary>
/// The items that are currently visible on the screen.
/// </summary>
private List<NativeColorData> visibleItems = new List<NativeColorData>();
#endregion
#region Public Fields
/// <summary>
/// The default sound used for the Color Navigation.
/// </summary>
public static readonly Sound DefaultSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "NAV_LEFT_RIGHT");
#endregion
#region Public Properties
/// <inheritdoc/>
public override bool Clickable => true;
/// <summary>
/// If the Opacity selector should be shown.
/// </summary>
public bool ShowOpacity
{
get => showOpacity;
set
{
showOpacity = value;
Recalculate();
}
}
/// <summary>
/// The opacity value of the color.
/// </summary>
/// <remarks>
/// The value needs to be set between 100 and 0.
/// It will return -1 if <see cref="ShowOpacity"/> is set to <see langword="false"/>.
/// </remarks>
public int Opacity
{
get
{
if (!ShowOpacity)
{
return -1;
}
return opacity;
}
set
{
if (!ShowOpacity)
{
return;
}
if (value > 100 || value < 0)
{
throw new IndexOutOfRangeException("The value needs to be over 0 and under 100.");
}
opacity = value;
UpdateOpacityBar();
}
}
/// <summary>
/// The currently selected color.
/// </summary>
/// <remarks>
/// If <see cref="ShowOpacity"/> is set to <see langword="true"/>.
/// </remarks>
public Color SelectedColor
{
get
{
// If there is no selected color information, return
NativeColorData data = SelectedItem;
if (data == null)
{
return default;
}
// Otherwise, return the color
return Color.FromArgb(ShowOpacity ? (int)(255 * (Opacity * 0.01f)) : 255, data.Color.R, data.Color.G, data.Color.B);
}
}
/// <summary>
/// Returns the currently selected <see cref="NativeColorData"/>.
/// </summary>
public NativeColorData SelectedItem
{
get
{
if (Colors.Count == 0 || index >= Colors.Count)
{
return null;
}
return Colors[SelectedIndex];
}
}
/// <summary>
/// The index of the currently selected Color.
/// </summary>
public int SelectedIndex
{
get
{
if (Colors.Count == 0 || index >= Colors.Count)
{
return -1;
}
return index;
}
set
{
// If the list of items is empty, don't allow the user to set the index
if (Colors == null || Colors.Count == 0)
{
throw new InvalidOperationException("There are no items in this menu.");
}
// If the value is over or equal than the number of items, raise an exception
else if (value >= Colors.Count)
{
throw new InvalidOperationException($"The index is over {Colors.Count - 1}");
}
// Calculate the bounds of the menu
int lower = firstItem;
int upper = firstItem + maxItems;
// Time to set the first item based on the total number of items
// If the item is between the allowed values, do nothing because we are on the correct first item
if (value >= lower && value < upper - 1)
{
}
// If the upper bound + 1 equals the new index, increase it by one
else if (upper == value)
{
firstItem += 1;
}
// If the first item minus one equals the value, decrease it by one
else if (lower - 1 == value)
{
firstItem -= 1;
}
// Otherwise, set it somewhere
else
{
// If the value is under the max items, set it to zero
if (value < maxItems)
{
firstItem = 0;
}
// Otherwise, set it at the bottom
else
{
firstItem = value - maxItems + 1;
}
}
// Save the index and update the items
index = value;
UpdateItems();
// Finally, play the switch change sound
Sound?.PlayFrontend();
}
}
/// <summary>
/// The Title used for the Panel when <see cref="TitleStyle"/> is set to <see cref="ColorTitleStyle.Simple"/>.
/// </summary>
public string Title
{
get => simpleTitle;
set
{
simpleTitle = value;
UpdateTitle();
}
}
/// <summary>
/// The style of the Panel Title.
/// </summary>
public ColorTitleStyle TitleStyle
{
get => titleStyle;
set
{
titleStyle = value;
UpdateTitle();
}
}
/// <summary>
/// If the count of items should be shown as part of the title.
/// </summary>
public bool ShowCount
{
get => showCount;
set
{
showCount = value;
UpdateTitle();
}
}
/// <summary>
/// THe maximum number of items shown on the screen.
/// </summary>
public int MaxItems
{
get => maxItems;
set
{
if (value == maxItems)
{
return;
}
maxItems = value;
UpdateItems();
UpdateTitle();
}
}
/// <summary>
/// The colors shown on this Panel.
/// </summary>
public List<NativeColorData> Colors { get; } = new List<NativeColorData>();
/// <summary>
/// The sound played when the item is changed.
/// </summary>
public Sound Sound { get; set; } = DefaultSound;
#endregion
#region Constructors
/// <summary>
/// Creates a color panel with no Items or Title.
/// </summary>
public NativeColorPanel() : this(string.Empty)
{
}
/// <summary>
/// Creates a Panel with a specific Title and set of Colors.
/// </summary>
/// <param name="title">The title of the panel.</param>
/// <param name="colors">The colors of the panel.</param>
public NativeColorPanel(string title, params NativeColorData[] colors)
{
// Set the title of the Panel
Title = title;
// Add the colors that we got
Colors.AddRange(colors);
}
#endregion
#region Private Functions
/// <summary>
/// Updates the Text of the Title.
/// </summary>
private void UpdateTitle()
{
string newTitle = string.Empty;
// Add the title based on the correct style
switch (titleStyle)
{
case ColorTitleStyle.Simple:
newTitle = Title;
break;
case ColorTitleStyle.ColorName:
newTitle = SelectedItem == null ? string.Empty : SelectedItem.Name;
break;
}
// If we need to add the count of colors
if (ShowCount)
{
// Add a space at the end if required
if (!newTitle.EndsWith(" "))
{
newTitle += " ";
}
// And add the item count
newTitle += $"({SelectedIndex + 1} of {Colors.Count})";
}
// And finally set the new title
title.Text = newTitle;
}
/// <summary>
/// Updates the position of the Items.
/// </summary>
private void UpdateItems()
{
// See UpdateItemList() on LemonUI.Menus.NativeMenu to understand this section
List<NativeColorData> list = new List<NativeColorData>();
for (int i = 0; i < MaxItems; i++)
{
int start = firstItem + i;
if (start >= Colors.Count)
{
break;
}
list.Add(Colors[start]);
}
visibleItems = list;
// Get the width based on the maximum number of items
float width = (lastWidth - leftDifference - rightDifference) / maxItems;
// And store the number of items already completed
int count = 0;
// Select the correct extra distance based on the prescence of the Opacity toggle
float extra = ShowOpacity ? 78 : 0;
// Then, start iterating over the colors visible on the screen
foreach (NativeColorData color in visibleItems)
{
// Set the position based on the number of items completed
color.rectangle.Position = new PointF(lastPosition.X + leftDifference + (width * count), lastPosition.Y + extra + 54);
// And set the size of it based on the number of items
color.rectangle.Size = new SizeF(width, 45);
// Finally, increase the count by one
count++;
}
// If there is a selected color item
if (SelectedItem != null)
{
// Set the position and size of the selection rectangle based on the currently selected color
ScaledRectangle colorRect = SelectedItem.rectangle;
const float height = 8;
selectionRectangle.Position = new PointF(colorRect.Position.X, colorRect.Position.Y - height);
selectionRectangle.Size = new SizeF(colorRect.Size.Width, height);
}
// Finally, update the text of the title
UpdateTitle();
}
/// <summary>
/// Updates the size of the opacity bar.
/// </summary>
private void UpdateOpacityBar()
{
// If the opacity bar is disabled, return
if (!ShowOpacity)
{
return;
}
// Otherwise, set the size based in the last known position
float x = lastPosition.X + 13;
float y = lastPosition.Y + 48;
float width = lastWidth - leftDifference - rightDifference;
const float height = 9;
opacityBackground.Position = new PointF(x, y);
opacityBackground.Size = new SizeF(width, height);
opacityForeground.Position = new PointF(x, y);
opacityForeground.Size = new SizeF(width * (Opacity * 0.01f), height);
}
/// <summary>
/// Recalculates the Color panel with the last known Position and Width.
/// </summary>
private void Recalculate() => Recalculate(lastPosition, lastWidth);
#endregion
#region Public Functions
/// <summary>
/// Moves to the Previous Color.
/// </summary>
public void Previous()
{
// If there are no items, return
if (Colors.Count == 0)
{
return;
}
// If we are on the first item, go back to the last one
if (SelectedIndex == 0)
{
SelectedIndex = Colors.Count - 1;
}
// Otherwise, reduce it by one
else
{
SelectedIndex -= 1;
}
}
/// <summary>
/// Moves to the Next Color.
/// </summary>
public void Next()
{
// If there are no items, return
if (Colors.Count == 0)
{
return;
}
// If we are on the last item, go back to the first one
if (Colors.Count - 1 == SelectedIndex)
{
SelectedIndex = 0;
}
// Otherwise, increase it by one
else
{
SelectedIndex += 1;
}
}
/// <summary>
/// Adds a color to the Panel.
/// </summary>
/// <param name="color">The color to add.</param>
public void Add(NativeColorData color)
{
if (Colors.Contains(color))
{
throw new ArgumentException("Color is already part of the Panel.", nameof(color));
}
Colors.Add(color);
Recalculate();
}
/// <summary>
/// Removes a color from the panel.
/// </summary>
/// <param name="color">The color to remove.</param>
public void Remove(NativeColorData color)
{
// Remove it if there
// If not, ignore it
Colors.Remove(color);
// If the index is higher or equal than the max number of items
// Set the max - 1
if (SelectedIndex >= Colors.Count)
{
SelectedIndex = Colors.Count - 1;
}
else
{
UpdateItems();
}
}
/// <summary>
/// Removes all of the
/// </summary>
/// <param name="func"></param>
public void Remove(Func<NativeColorData, bool> func)
{
foreach (NativeColorData color in new List<NativeColorData>(Colors))
{
if (func(color))
{
Colors.Remove(color);
}
}
Recalculate();
}
/// <summary>
/// Removes all of the colors from the Panel.
/// </summary>
public void Clear()
{
Colors.Clear();
Recalculate();
}
/// <summary>
/// Checks if the Color Data is present on this Panel.
/// </summary>
/// <param name="color">The Color Data to check.</param>
public void Contains(NativeColorData color) => Colors.Contains(color);
/// <summary>
/// Recalculates the position of the Color Panel.
/// </summary>
/// <param name="position">The position of the panel.</param>
/// <param name="width">The width of the menu.</param>
public override void Recalculate(PointF position, float width)
{
// Save the last position and width
lastPosition = position;
lastWidth = width;
// Select the correct extra distance based on the prescence of the Opacity toggle
float extra = ShowOpacity ? 78 : 0;
// Set the position and size of the Background
Background.Position = position;
Background.Size = new SizeF(width, ShowOpacity ? 188 : 111);
// And then set the position of the text
title.Position = new PointF(position.X + (width * 0.5f), position.Y + extra + 10f);
// Then, set the position of the opacity bar and texts
UpdateOpacityBar();
opacityText.Position = new PointF(position.X + (width * 0.5f), position.Y + 10f);
percentMin.Position = new PointF(position.X + 9, position.Y + 11);
percentMax.Position = new PointF(position.X + width - 60, position.Y + 11);
// Finally, update the list of items
UpdateItems();
}
/// <summary>
/// Draws the Color Panel.
/// </summary>
public override void Process()
{
// If the user pressed one of the keys, move to the left or right
if (Controls.IsJustPressed(Control.FrontendLt))
{
Previous();
}
else if (Controls.IsJustPressed(Control.FrontendRt))
{
Next();
}
// If the user pressed one of the bumpers with the Opacity bar enabled, increase or decrease it
else if (ShowOpacity && Controls.IsJustPressed(Control.FrontendLb))
{
if (Opacity > 0)
{
Opacity--;
Sound?.PlayFrontend();
}
}
else if (ShowOpacity && Controls.IsJustPressed(Control.FrontendRb))
{
if (Opacity < 100)
{
Opacity++;
Sound?.PlayFrontend();
}
}
// Draw the items
base.Process();
title.Draw();
foreach (NativeColorData color in visibleItems)
{
color.rectangle.Draw();
}
if (Colors.Count != 0)
{
selectionRectangle.Draw();
}
if (ShowOpacity)
{
opacityText.Draw();
percentMin.Draw();
percentMax.Draw();
opacityBackground.Draw();
opacityForeground.Draw();
}
}
#endregion
}
}

View File

@ -0,0 +1,163 @@
using LemonUI.Elements;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Dynamic Items allow you to dynamically change the item shown to the user.
/// </summary>
/// <typeparam name="T">The type of item.</typeparam>
public class NativeDynamicItem<T> : NativeSlidableItem
{
#region Fields
private readonly ScaledText text = new ScaledText(PointF.Empty, string.Empty, 0.35f);
private T item = default;
#endregion
#region Properties
/// <summary>
/// The currently selected item.
/// </summary>
public T SelectedItem
{
get => item;
set
{
item = value;
}
}
#endregion
#region Events
/// <summary>
/// Event triggered when the user has changed the item.
/// </summary>
public event ItemChangedEventHandler<T> ItemChanged;
#endregion
#region Constructor
/// <summary>
/// Creates a new Dynamic List Item.
/// </summary>
/// <param name="title">The Title of the item.</param>
public NativeDynamicItem(string title) : this(title, string.Empty, default)
{
}
/// <summary>
/// Creates a new Dynamic List Item.
/// </summary>
/// <param name="title">The Title of the item.</param>
/// <param name="item">The Item to set.</param>
public NativeDynamicItem(string title, T item) : this(title, string.Empty, item)
{
}
/// <summary>
/// Creates a new Dynamic List Item.
/// </summary>
/// <param name="title">The Title of the item.</param>
/// <param name="description">The Description of the item.</param>
public NativeDynamicItem(string title, string description) : this(title, description, default)
{
}
/// <summary>
/// Creates a new Dynamic List Item.
/// </summary>
/// <param name="title">The Title of the item.</param>
/// <param name="description">The Description of the item.</param>
/// <param name="item">The Item to set.</param>
public NativeDynamicItem(string title, string description, T item) : base(title, description)
{
this.item = item;
}
#endregion
#region Functions
/// <summary>
/// Updates the currently selected item based on the index.
/// </summary>
private void UpdateItemName()
{
// This is the SAME as the normal NativeListItem
text.Text = !SelectedItem.Equals(default) ? SelectedItem.ToString() : string.Empty;
text.Position = new PointF(RightArrow.Position.X - text.Width + 3, text.Position.Y);
LeftArrow.Position = new PointF(text.Position.X - LeftArrow.Size.Width, LeftArrow.Position.Y);
}
/// <summary>
/// Gets the previous item.
/// </summary>
public override void GoLeft()
{
ItemChangedEventArgs<T> arguments = new ItemChangedEventArgs<T>(item, -1, Direction.Left);
ItemChanged?.Invoke(this, arguments);
SelectedItem = arguments.Object;
UpdateItemName();
}
/// <summary>
/// Gets the next item.
/// </summary>
public override void GoRight()
{
ItemChangedEventArgs<T> arguments = new ItemChangedEventArgs<T>(item, -1, Direction.Right);
ItemChanged?.Invoke(this, arguments);
SelectedItem = arguments.Object;
UpdateItemName();
}
/// <summary>
/// Recalculates the position of the current List Item.
/// </summary>
/// <param name="pos">The new position of the item.</param>
/// <param name="size">The Size of the item.</param>
/// <param name="selected">If the item is selected or not.</param>
public override void Recalculate(PointF pos, SizeF size, bool selected)
{
// This is the SAME as the normal NativeListItem
base.Recalculate(pos, size, selected);
float textWidth = RightArrow.Size.Width;
text.Position = new PointF(pos.X + size.Width - textWidth - 1 - text.Width, pos.Y + 3);
LeftArrow.Position = new PointF(text.Position.X - LeftArrow.Size.Width, pos.Y + 4);
UpdateItemName();
}
/// <summary>
/// Draws the List on the screen.
/// </summary>
public override void Draw()
{
base.Draw(); // Arrows, Title and Left Badge
text.Draw();
}
/// <inheritdoc/>
public override void UpdateColors()
{
base.UpdateColors();
if (!Enabled)
{
text.Color = Colors.TitleDisabled;
}
else if (lastSelected)
{
text.Color = Colors.TitleHovered;
}
else
{
text.Color = Colors.TitleNormal;
}
}
#endregion
}
}

View File

@ -0,0 +1,371 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
using CitizenFX.Core.UI;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
using Rage.Native;
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.Native;
using GTA.UI;
#endif
using LemonUI.Elements;
using LemonUI.Extensions;
using System;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Represents a grid where you can select X and Y values.
/// </summary>
public class NativeGridPanel : NativePanel
{
#region Fields
private PointF position = PointF.Empty;
private float width = 0;
private readonly ScaledText labelTop = new ScaledText(PointF.Empty, "Y+", 0.33f)
{
Alignment = Alignment.Center
};
private readonly ScaledText labelBottom = new ScaledText(PointF.Empty, "Y-", 0.33f)
{
Alignment = Alignment.Center
};
private readonly ScaledText labelLeft = new ScaledText(PointF.Empty, "X-", 0.33f)
{
Alignment = Alignment.Right
};
private readonly ScaledText labelRight = new ScaledText(PointF.Empty, "X+", 0.33f);
private readonly ScaledTexture grid = new ScaledTexture("pause_menu_pages_char_mom_dad", "nose_grid")
{
Color = Color.FromArgb(205, 105, 105, 102)
};
private readonly ScaledTexture dot = new ScaledTexture("commonmenu", "common_medal")
{
Color = Color.FromArgb(255, 255, 255, 255)
};
private PointF innerPosition = PointF.Empty;
private SizeF innerSize = SizeF.Empty;
private GridStyle style = GridStyle.Full;
private float x;
private float y;
#endregion
#region Properties
/// <inheritdoc/>
public override bool Clickable => true;
/// <summary>
/// The X value between 0 and 1.
/// </summary>
public float X
{
get
{
switch (style)
{
case GridStyle.Full:
case GridStyle.Row:
return x;
default:
return 0.5f;
}
}
set
{
if (value > 1 || value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
if (style == GridStyle.Column)
{
return;
}
PointF before = new PointF(X, Y);
x = value;
UpdateDot(before);
}
}
/// <summary>
/// The X value between 0 and 1.
/// </summary>
public float Y
{
get
{
switch (style)
{
case GridStyle.Full:
case GridStyle.Column:
return y;
default:
return 0.5f;
}
}
set
{
if (value > 1 || value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
if (style == GridStyle.Row)
{
return;
}
PointF before = new PointF(X, Y);
y = value;
UpdateDot(before);
}
}
/// <summary>
/// The text label shown on the top.
/// </summary>
public string LabelTop
{
get => labelTop.Text;
set => labelTop.Text = value;
}
/// <summary>
/// The text label shown on the bottom.
/// </summary>
public string LabelBottom
{
get => labelBottom.Text;
set => labelBottom.Text = value;
}
/// <summary>
/// The text label shown on the left.
/// </summary>
public string LabelLeft
{
get => labelLeft.Text;
set => labelLeft.Text = value;
}
/// <summary>
/// The text label shown on the right.
/// </summary>
public string LabelRight
{
get => labelRight.Text;
set => labelRight.Text = value;
}
/// <summary>
/// The style of this grid.
/// </summary>
public GridStyle Style
{
get => style;
set
{
if (!Enum.IsDefined(typeof(GridStyle), value))
{
throw new ArgumentOutOfRangeException(nameof(value), "The Grid style is not valid! Expected Full, Row or Column.");
}
style = value;
Recalculate();
}
}
#endregion
#region Events
/// <summary>
/// Event triggered when X and/or Y values are changed.
/// </summary>
public event GridValueChangedEventHandler ValuesChanged;
#endregion
#region Constructor
/// <summary>
/// Creates a new <see cref="NativeGridPanel"/>.
/// </summary>
public NativeGridPanel() : base()
{
}
#endregion
#region Functions
private void Recalculate() => Recalculate(position, width);
private void UpdateDot(PointF before, bool trigger = true)
{
float posX = innerSize.Width * (style == GridStyle.Full || style == GridStyle.Row ? x : 0.5f);
float posY = innerSize.Height * (style == GridStyle.Full || style == GridStyle.Column ? y : 0.5f);
dot.Size = new SizeF(45, 45);
dot.Position = new PointF(innerPosition.X + posX - (dot.Size.Width * 0.5f),
innerPosition.Y + posY - (dot.Size.Height * 0.5f));
if (trigger)
{
ValuesChanged?.Invoke(this, new GridValueChangedArgs(before, new PointF(X, Y)));
}
}
/// <inheritdoc/>
public override void Recalculate(PointF position, float width)
{
this.position = position;
this.width = width;
const float height = 270;
const int offsetX = 20;
const int offsetY = 20;
base.Recalculate(position, width);
Background.Size = new SizeF(width, height);
switch (style)
{
case GridStyle.Full:
grid.Position = new PointF(position.X + (width * 0.5f) - 95, position.Y + (height * 0.5f) - 94);
grid.Size = new SizeF(192, 192);
break;
case GridStyle.Row:
grid.Position = new PointF(position.X + (width * 0.5f) - 95, position.Y + (height * 0.5f) - 15);
grid.Size = new SizeF(192, 36);
break;
case GridStyle.Column:
grid.Position = new PointF(position.X + (width * 0.5f) - 17, position.Y + (height * 0.5f) - 94);
grid.Size = new SizeF(36, 192);
break;
}
labelTop.Position = new PointF(position.X + (width * 0.5f), position.Y + 10);
labelBottom.Position = new PointF(position.X + (width * 0.5f), position.Y + height - 34);
labelLeft.Position = new PointF(position.X + (width * 0.5f) - 102, position.Y + (height * 0.5f) - (labelLeft.LineHeight * 0.5f));
labelRight.Position = new PointF(position.X + (width * 0.5f) + 102, position.Y + (height * 0.5f) - (labelLeft.LineHeight * 0.5f));
innerPosition = new PointF(grid.Position.X + offsetX, grid.Position.Y + offsetY);
innerSize = new SizeF(grid.Size.Width - (offsetX * 2), grid.Size.Height - (offsetY * 2));
UpdateDot(PointF.Empty, false);
}
/// <inheritdoc/>
public override void Process()
{
float previousX = X;
float previousY = Y;
Background.Draw();
switch (style)
{
case GridStyle.Full:
labelTop.Draw();
labelBottom.Draw();
labelLeft.Draw();
labelRight.Draw();
grid.Draw();
break;
case GridStyle.Row:
labelLeft.Draw();
labelRight.Draw();
grid.DrawSpecific(new PointF(0, 0.4f), new PointF(1, 0.6f));
break;
case GridStyle.Column:
labelTop.Draw();
labelBottom.Draw();
grid.DrawSpecific(new PointF(0.4f, 0), new PointF(0.6f, 1));
break;
}
dot.Draw();
#if FIVEM
bool usingKeyboard = API.IsInputDisabled(2);
#elif RAGEMP
bool usingKeyboard = Invoker.Invoke<bool>(0xA571D46727E2B718, 2);
#elif RPH
bool usingKeyboard = NativeFunction.CallByHash<bool>(0xA571D46727E2B718, 2);
#elif SHVDN3
bool usingKeyboard = Function.Call<bool>(Hash._IS_USING_KEYBOARD, 2);
#endif
if (usingKeyboard)
{
if (Screen.IsCursorInArea(grid.Position, grid.Size) && Controls.IsPressed(Control.CursorAccept))
{
PointF cursor = Screen.CursorPositionRelative;
PointF pos = innerPosition.ToRelative();
PointF start = new PointF(cursor.X - pos.X, cursor.Y - pos.Y);
SizeF size = innerSize.ToRelative();
x = start.X / size.Width;
y = start.Y / size.Height;
}
else
{
return;
}
}
else
{
Controls.DisableThisFrame(Control.LookUpDown);
Controls.DisableThisFrame(Control.LookLeftRight);
Controls.EnableThisFrame(Control.ScriptRightAxisX);
Controls.EnableThisFrame(Control.ScriptRightAxisY);
#if FIVEM
float rX = Game.GetControlNormal(0, Control.ScriptRightAxisX);
float rY = Game.GetControlNormal(0, Control.ScriptRightAxisY);
float frameTime = Game.LastFrameTime;
#elif RAGEMP
float rX = Invoker.Invoke<float>(0xEC3C9B8D5327B563, 0, (int)Control.ScriptRightAxisX);
float rY = Invoker.Invoke<float>(0xEC3C9B8D5327B563, 0, (int)Control.ScriptRightAxisY);
float frameTime = Invoker.Invoke<float>(Natives.GetFrameTime);
#elif RPH
float rX = NativeFunction.CallByHash<float>(0xEC3C9B8D5327B563, 0, (int)Control.ScriptRightAxisX);
float rY = NativeFunction.CallByHash<float>(0xEC3C9B8D5327B563, 0, (int)Control.ScriptRightAxisY);
float frameTime = Game.FrameTime;
#elif SHVDN3
float rX = Game.GetControlValueNormalized(Control.ScriptRightAxisX);
float rY = Game.GetControlValueNormalized(Control.ScriptRightAxisY);
float frameTime = Game.LastFrameTime;
#endif
x += rX * frameTime;
y += rY * frameTime;
}
// Make sure that the values are not under zero or over one
if (x < 0)
{
x = 0;
}
else if (x > 1)
{
x = 1;
}
if (y < 0)
{
y = 0;
}
else if (y > 1)
{
y = 1;
}
if (previousX != x || previousY != y)
{
UpdateDot(new PointF(previousX, previousX));
}
}
#endregion
}
}

View File

@ -0,0 +1,398 @@
#if FIVEM
using Font = CitizenFX.Core.UI.Font;
#elif RAGEMP
using Font = RAGE.Game.Font;
#elif RPH
using Font = LemonUI.Elements.Font;
#elif SHVDN3
using Font = GTA.UI.Font;
#endif
using LemonUI.Elements;
using System;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Basic Rockstar-like item.
/// </summary>
public class NativeItem : IDrawable
{
#region Protected Internal Fields
/// <summary>
/// The title of the object.
/// </summary>
protected internal ScaledText title = null;
/// <summary>
/// The last known Item Position.
/// </summary>
protected internal PointF lastPosition = PointF.Empty;
/// <summary>
/// The last known Item Size.
/// </summary>
protected internal SizeF lastSize = SizeF.Empty;
/// <summary>
/// The last known Item Selection.
/// </summary>
protected internal bool lastSelected = false;
/// <summary>
/// The left badge of the Item.
/// </summary>
protected internal I2Dimensional badgeLeft = null;
/// <summary>
/// The left badge of the Item.
/// </summary>
protected internal I2Dimensional badgeRight = null;
/// <summary>
/// The alternate title of the menu.
/// </summary>
protected internal ScaledText altTitle = null;
#endregion
#region Private Fields
private bool enabled = true;
private BadgeSet badgeSetLeft = null;
private BadgeSet badgeSetRight = null;
private ColorSet colors = new ColorSet();
private ScaledRectangle background = new ScaledRectangle(PointF.Empty, SizeF.Empty);
#endregion
#region Public Properties
/// <summary>
/// If this item can be used or not.
/// </summary>
public bool Enabled
{
get => enabled;
set
{
if (enabled == value)
{
return;
}
enabled = value;
EnabledChanged?.Invoke(this, EventArgs.Empty);
UpdateColors();
}
}
/// <summary>
/// Object that contains data about this Item.
/// </summary>
public virtual object Tag { get; set; }
/// <summary>
/// The title of the item.
/// </summary>
public string Title
{
get => title.Text;
set => title.Text = value;
}
/// <summary>
/// The alternative title of the item shown on the right.
/// </summary>
public string AltTitle
{
get => altTitle.Text;
set
{
altTitle.Text = value;
Recalculate();
}
}
/// <summary>
/// The font of title item.
/// </summary>
public Font TitleFont
{
get => title.Font;
set => title.Font = value;
}
/// <summary>
/// The font of alternative title item shown on the right.
/// </summary>
public Font AltTitleFont
{
get => altTitle.Font;
set => altTitle.Font = value;
}
/// <summary>
/// The description of the item.
/// </summary>
public string Description { get; set; }
/// <summary>
/// The Left badge of the Item.
/// </summary>
public I2Dimensional LeftBadge
{
get => badgeLeft;
set
{
badgeLeft = value;
Recalculate();
UpdateColors();
}
}
/// <summary>
/// The Left badge set of the Item.
/// </summary>
public BadgeSet LeftBadgeSet
{
get => badgeSetLeft;
set
{
badgeSetLeft = value;
Recalculate();
UpdateColors();
}
}
/// <summary>
/// The Right badge of the Item.
/// </summary>
public I2Dimensional RightBadge
{
get => badgeRight;
set
{
badgeRight = value;
Recalculate();
UpdateColors();
}
}
/// <summary>
/// The Right badge set of the Item.
/// </summary>
public BadgeSet RightBadgeSet
{
get => badgeSetRight;
set
{
badgeSetRight = value;
Recalculate();
UpdateColors();
}
}
/// <summary>
/// The different colors that change dynamically when the item is used.
/// </summary>
public ColorSet Colors
{
get => colors;
set
{
colors = value;
UpdateColors();
}
}
/// <summary>
/// The Panel asociated to this <see cref="NativeItem"/>.
/// </summary>
public NativePanel Panel { get; set; } = null;
/// <summary>
/// If a custom colored background should be used.
/// </summary>
public bool UseCustomBackground { get; set; }
/// <summary>
/// If this item is being hovered.
/// </summary>
public bool IsHovered => Screen.IsCursorInArea(background.Position, background.Size);
#endregion
#region Events
/// <summary>
/// Event triggered when the item is selected.
/// </summary>
public event SelectedEventHandler Selected;
/// <summary>
/// Event triggered when the item is activated.
/// </summary>
public event EventHandler Activated;
/// <summary>
/// Event triggered when the <see cref="Enabled"/> property is changed.
/// </summary>
public event EventHandler EnabledChanged;
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="NativeItem"/>.
/// </summary>
/// <param name="title">The title of the item.</param>
public NativeItem(string title) : this(title, string.Empty, string.Empty)
{
}
/// <summary>
/// Creates a new <see cref="NativeItem"/>.
/// </summary>
/// <param name="title">The title of the item.</param>
/// <param name="description">The description of the item.</param>
public NativeItem(string title, string description) : this(title, description, string.Empty)
{
}
/// <summary>
/// Creates a new <see cref="NativeItem"/>.
/// </summary>
/// <param name="title">The title of the item.</param>
/// <param name="description">The description of the item.</param>
/// <param name="altTitle">The alternative title of the item, shown on the right.</param>
public NativeItem(string title, string description, string altTitle)
{
this.title = new ScaledText(PointF.Empty, title, 0.345f);
Description = description;
this.altTitle = new ScaledText(PointF.Empty, altTitle, 0.345f);
}
#endregion
#region Event Triggers
/// <summary>
/// Triggers the Selected event.
/// </summary>
protected internal void OnSelected(object sender, SelectedEventArgs e) => Selected?.Invoke(sender, e);
/// <summary>
/// Triggers the Activated event.
/// </summary>
protected internal void OnActivated(object sender) => Activated?.Invoke(sender, EventArgs.Empty);
#endregion
#region Private Functions
/// <summary>
/// Recalculates the item with the last known values.
/// </summary>
protected void Recalculate() => Recalculate(lastPosition, lastSize, lastSelected);
#endregion
#region Public Functions
/// <summary>
/// Recalculates the item positions and sizes with the specified values.
/// </summary>
/// <param name="pos">The position of the item.</param>
/// <param name="size">The size of the item.</param>
/// <param name="selected">If this item has been selected.</param>
public virtual void Recalculate(PointF pos, SizeF size, bool selected)
{
lastPosition = pos;
lastSize = size;
lastSelected = selected;
background.Position = pos;
background.Size = size;
if (badgeSetLeft != null)
{
if (!(badgeLeft is ScaledTexture))
{
badgeLeft = new ScaledTexture(string.Empty, string.Empty);
}
ScaledTexture left = (ScaledTexture)badgeLeft;
left.Dictionary = selected ? badgeSetLeft.HoveredDictionary : badgeSetLeft.NormalDictionary;
left.Texture = selected ? badgeSetLeft.HoveredTexture : badgeSetLeft.NormalTexture;
}
if (badgeSetRight != null)
{
if (!(badgeRight is ScaledTexture))
{
badgeRight = new ScaledTexture(string.Empty, string.Empty);
}
ScaledTexture right = (ScaledTexture)badgeRight;
right.Dictionary = selected ? badgeSetRight.HoveredDictionary : badgeSetRight.NormalDictionary;
right.Texture = selected ? badgeSetRight.HoveredTexture : badgeSetRight.NormalTexture;
}
if (badgeLeft != null)
{
badgeLeft.Position = new PointF(pos.X + 2, pos.Y - 3);
badgeLeft.Size = new SizeF(45, 45);
}
if (badgeRight != null)
{
badgeRight.Position = new PointF(pos.X + size.Width - 47, pos.Y - 3);
badgeRight.Size = new SizeF(45, 45);
}
title.Position = new PointF(pos.X + (badgeLeft == null ? 0 : 34) + 6, pos.Y + 3);
altTitle.Position = new PointF(pos.X + size.Width - (badgeRight == null ? 0 : 34) - altTitle.Width - 6, pos.Y + 3);
UpdateColors();
}
/// <summary>
/// Draws the item.
/// </summary>
public virtual void Draw()
{
if (UseCustomBackground)
{
background.Draw();
}
title.Draw();
altTitle.Draw();
badgeLeft?.Draw();
badgeRight?.Draw();
}
/// <summary>
/// Updates the colors of the <see cref="Elements"/> from the <see cref="Colors"/> <see cref="ColorSet"/>.
/// </summary>
public virtual void UpdateColors()
{
if (!Enabled)
{
background.Color = Colors.BackgroundDisabled;
title.Color = Colors.TitleDisabled;
altTitle.Color = Colors.AltTitleDisabled;
if (badgeLeft != null)
{
badgeLeft.Color = Colors.BadgeLeftDisabled;
}
if (badgeRight != null)
{
badgeRight.Color = Colors.BadgeRightDisabled;
}
}
else if (lastSelected)
{
background.Color = Colors.BackgroundHovered;
title.Color = Colors.TitleHovered;
altTitle.Color = Colors.AltTitleHovered;
if (badgeLeft != null)
{
badgeLeft.Color = Colors.BadgeLeftHovered;
}
if (badgeRight != null)
{
badgeRight.Color = Colors.BadgeRightHovered;
}
}
else
{
background.Color = Colors.BackgroundNormal;
title.Color = Colors.TitleNormal;
altTitle.Color = Colors.AltTitleNormal;
if (badgeLeft != null)
{
badgeLeft.Color = Colors.BadgeLeftNormal;
}
if (badgeRight != null)
{
badgeRight.Color = Colors.BadgeRightNormal;
}
}
}
#endregion
}
}

View File

@ -0,0 +1,366 @@
using LemonUI.Elements;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Base class for list items.
/// </summary>
public abstract class NativeListItem : NativeSlidableItem
{
/// <summary>
/// The text of the current item.
/// </summary>
internal protected ScaledText text = null;
/// <summary>
/// Creates a new list item with a title and subtitle.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="subtitle">The subtitle of the Item.</param>
public NativeListItem(string title, string subtitle) : base(title, subtitle)
{
text = new ScaledText(PointF.Empty, string.Empty, 0.35f);
}
}
/// <summary>
/// An item that allows you to scroll between a set of objects.
/// </summary>
public class NativeListItem<T> : NativeListItem
{
#region Fields
private int index = 0;
private List<T> items = new List<T>();
#endregion
#region Properties
/// <summary>
/// The index of the currently selected index.
/// </summary>
public int SelectedIndex
{
get
{
if (Items.Count == 0)
{
return -1;
}
return index;
}
set
{
if (Items.Count == 0)
{
throw new InvalidOperationException("There are no available items.");
}
if (value < 0)
{
throw new InvalidOperationException("The index is under zero.");
}
if (value >= Items.Count)
{
throw new InvalidOperationException($"The index is over the limit of {Items.Count - 1}");
}
if (index == value)
{
return;
}
index = value;
TriggerEvent(value, Direction.Unknown);
UpdateIndex();
}
}
/// <summary>
/// The currently selected item.
/// </summary>
public T SelectedItem
{
get
{
if (Items.Count == 0)
{
return default;
}
return Items[SelectedIndex];
}
set
{
if (Items.Count == 0)
{
throw new InvalidOperationException("There are no available items.");
}
int newIndex = Items.IndexOf(value);
if (newIndex == -1)
{
throw new InvalidOperationException("The object is not the list of Items.");
}
SelectedIndex = newIndex;
}
}
/// <summary>
/// The objects used by this item.
/// </summary>
public List<T> Items
{
get => items;
set
{
items = value;
UpdateIndex();
}
}
#endregion
#region Events
/// <summary>
/// Event triggered when the selected item is changed.
/// </summary>
public event ItemChangedEventHandler<T> ItemChanged;
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="NativeListItem"/>.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="objs">The objects that are available on the Item.</param>
public NativeListItem(string title, params T[] objs) : this(title, string.Empty, objs)
{
}
/// <summary>
/// Creates a new <see cref="NativeListItem"/>.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="subtitle">The subtitle of the Item.</param>
/// <param name="objs">The objects that are available on the Item.</param>
public NativeListItem(string title, string subtitle, params T[] objs) : base(title, subtitle)
{
items = new List<T>();
items.AddRange(objs);
UpdateIndex();
}
#endregion
#region Tools
/// <summary>
/// Triggers the <seealso cref="ItemChangedEventHandler{T}"/> event.
/// </summary>
private void TriggerEvent(int index, Direction direction)
{
ItemChanged?.Invoke(this, new ItemChangedEventArgs<T>(items[index], index, direction));
}
private void FixIndexIfRequired()
{
if (index >= items.Count)
{
index = items.Count - 1;
UpdateIndex();
}
}
/// <summary>
/// Updates the currently selected item based on the index.
/// </summary>
private void UpdateIndex()
{
text.Text = SelectedIndex != -1 ? SelectedItem.ToString() : string.Empty;
text.Position = new PointF(RightArrow.Position.X - text.Width + 3, text.Position.Y);
LeftArrow.Position = new PointF(text.Position.X - LeftArrow.Size.Width, LeftArrow.Position.Y);
}
#endregion
#region Functions
/// <summary>
/// Adds a <typeparamref name="T" /> into this item.
/// </summary>
/// <param name="item">The <typeparamref name="T" /> to add.</param>
public void Add(T item)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
if (items.Contains(item))
{
throw new InvalidOperationException("Item is already part of this NativeListItem.");
}
items.Add(item);
if (items.Count == 1)
{
UpdateIndex();
}
}
/// <summary>
/// Adds a <typeparamref name="T" /> in a specific location.
/// </summary>
/// <param name="position">The position where the item should be added.</param>
/// <param name="item">The <typeparamref name="T" /> to add.</param>
public void Add(int position, T item)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
if (position < 0 || position > Items.Count)
{
throw new ArgumentOutOfRangeException(nameof(position), "The position is out of the range of items.");
}
Items.Insert(position, item);
FixIndexIfRequired();
}
/// <summary>
/// Removes a specific <typeparamref name="T" />.
/// </summary>
/// <param name="item">The <typeparamref name="T" /> to remove.</param>
public void Remove(T item)
{
if (items.Remove(item))
{
FixIndexIfRequired();
}
}
/// <summary>
/// Removes a <typeparamref name="T" /> at a specific location.
/// </summary>
/// <param name="position">The position of the <typeparamref name="T" />.</param>
public void RemoveAt(int position)
{
if (position >= items.Count)
{
return;
}
items.RemoveAt(position);
FixIndexIfRequired();
}
/// <summary>
/// Removes all of the items that match the <paramref name="pred"/>.
/// </summary>
/// <param name="pred">The function to use as a check.</param>
public void Remove(Func<T, bool> pred)
{
if (items.RemoveAll(pred.Invoke) > 0)
{
FixIndexIfRequired();
}
}
/// <summary>
/// Removes all of the <typeparamref name="T" /> from this item.
/// </summary>
public void Clear()
{
items.Clear();
UpdateIndex();
}
/// <summary>
/// Recalculates the item positions and sizes with the specified values.
/// </summary>
/// <param name="pos">The position of the item.</param>
/// <param name="size">The size of the item.</param>
/// <param name="selected">If this item has been selected.</param>
public override void Recalculate(PointF pos, SizeF size, bool selected)
{
base.Recalculate(pos, size, selected);
text.Position = new PointF(pos.X + size.Width - RightArrow.Size.Width - 1 - text.Width, pos.Y + 3);
LeftArrow.Position = new PointF(text.Position.X - LeftArrow.Size.Width, pos.Y + 4);
}
/// <summary>
/// Moves to the previous item.
/// </summary>
public override void GoLeft()
{
if (Items.Count == 0)
{
return;
}
if (index == 0)
{
index = Items.Count - 1;
}
else
{
index--;
}
TriggerEvent(index, Direction.Left);
UpdateIndex();
}
/// <summary>
/// Moves to the next item.
/// </summary>
public override void GoRight()
{
if (Items.Count == 0)
{
return;
}
if (index == Items.Count - 1)
{
index = 0;
}
else
{
index++;
}
TriggerEvent(index, Direction.Right);
UpdateIndex();
}
/// <summary>
/// Draws the List on the screen.
/// </summary>
public override void Draw()
{
base.Draw(); // Arrows, Title and Left Badge
text.Draw();
}
/// <inheritdoc/>
public override void UpdateColors()
{
base.UpdateColors();
if (!Enabled)
{
text.Color = Colors.TitleDisabled;
}
else if (lastSelected)
{
text.Color = Colors.TitleHovered;
}
else
{
text.Color = Colors.TitleNormal;
}
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
using LemonUI.Elements;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Represents a panel shown under the description of the item description.
/// </summary>
public abstract class NativePanel
{
#region Public Properties
/// <summary>
/// If this panel is visible to the user.
/// </summary>
public virtual bool Visible { get; set; } = true;
/// <summary>
/// If the item has controls that can be clicked.
/// </summary>
public virtual bool Clickable { get; } = false;
/// <summary>
/// The Background of the panel itself.
/// </summary>
public ScaledTexture Background { get; } = new ScaledTexture("commonmenu", "gradient_bgd");
#endregion
#region Public Functions
/// <summary>
/// Recalculates the menu contents.
/// </summary>
/// <param name="position">The position of the panel.</param>
/// <param name="width">The width of the menu.</param>
public virtual void Recalculate(PointF position, float width)
{
Background.Position = position;
}
/// <summary>
/// Processes and Draws the panel.
/// </summary>
public virtual void Process()
{
Background.Draw();
}
#endregion
}
}

View File

@ -0,0 +1,30 @@
namespace LemonUI.Menus
{
/// <summary>
/// A Blank Separator Item for creating empty spaces between menu items.
/// </summary>
public class NativeSeparatorItem : NativeItem
{
#region Constructor
/// <summary>
/// Creates a new Menu Separator.
/// </summary>
public NativeSeparatorItem() : base(string.Empty, string.Empty, string.Empty)
{
}
#endregion
#region Public Functions
/// <summary>
/// Draws nothing.
/// </summary>
public override void Draw()
{
}
#endregion
}
}

View File

@ -0,0 +1,143 @@
using LemonUI.Elements;
using System;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Basic elements for a slidable item.
/// </summary>
public abstract class NativeSlidableItem : NativeItem
{
#region Private Fields
private bool alwaysVisible = false;
#endregion
#region Internal Fields
/// <summary>
/// The arrow pointing to the Left.
/// </summary>
[Obsolete("arrowLeft is Obsolete, use LeftArrow instead.")]
internal protected ScaledTexture arrowLeft = null;
/// <summary>
/// The arrow pointing to the Right.
/// </summary>
[Obsolete("arrowRight is Obsolete, use RightArrow instead.")]
internal protected ScaledTexture arrowRight = null;
#endregion
#region Public Properties
/// <summary>
/// The arrow pointing to the Left.
/// </summary>
public ScaledTexture LeftArrow { get; }
/// <summary>
/// The arrow pointing to the Right.
/// </summary>
public ScaledTexture RightArrow { get; }
/// <summary>
/// Whether the arrows should always be shown regardless of the visibility of the Item.
/// </summary>
public bool ArrowsAlwaysVisible
{
get => alwaysVisible;
set
{
alwaysVisible = value;
Recalculate();
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a new item that can be sliden.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="description">The description of the Item.</param>
public NativeSlidableItem(string title, string description) : base(title, description)
{
LeftArrow = new ScaledTexture(PointF.Empty, SizeF.Empty, "commonmenu", "arrowleft");
RightArrow = new ScaledTexture(PointF.Empty, SizeF.Empty, "commonmenu", "arrowright");
#pragma warning disable CS0618
arrowLeft = LeftArrow;
arrowRight = RightArrow;
#pragma warning restore CS0618
EnabledChanged += NativeSlidableItem_EnabledChanged;
}
#endregion
#region Local Events
private void NativeSlidableItem_EnabledChanged(object sender, EventArgs e) => Recalculate();
#endregion
#region Public Functions
/// <summary>
/// Recalculates the item positions and sizes with the specified values.
/// </summary>
/// <param name="pos">The position of the item.</param>
/// <param name="size">The size of the item.</param>
/// <param name="selected">If this item has been selected.</param>
public override void Recalculate(PointF pos, SizeF size, bool selected)
{
base.Recalculate(pos, size, selected);
LeftArrow.Size = (selected && Enabled) || ArrowsAlwaysVisible ? new SizeF(30, 30) : SizeF.Empty;
RightArrow.Size = (selected && Enabled) || ArrowsAlwaysVisible ? new SizeF(30, 30) : SizeF.Empty;
RightArrow.Position = new PointF(pos.X + size.Width - RightArrow.Size.Width - 5, pos.Y + 4);
}
/// <summary>
/// Moves to the previous item.
/// </summary>
public abstract void GoLeft();
/// <summary>
/// Moves to the next item.
/// </summary>
public abstract void GoRight();
/// <summary>
/// Draws the left and right arrow.
/// </summary>
public override void Draw()
{
title.Draw();
badgeLeft?.Draw();
LeftArrow.Draw();
RightArrow.Draw();
}
/// <inheritdoc/>
public override void UpdateColors()
{
base.UpdateColors();
if (!Enabled)
{
LeftArrow.Color = Colors.ArrowsDisabled;
RightArrow.Color = Colors.ArrowsDisabled;
}
else if (lastSelected)
{
LeftArrow.Color = Colors.ArrowsHovered;
RightArrow.Color = Colors.ArrowsHovered;
}
else
{
LeftArrow.Color = Colors.ArrowsNormal;
RightArrow.Color = Colors.ArrowsNormal;
}
}
#endregion
}
}

View File

@ -0,0 +1,241 @@
using LemonUI.Elements;
using System;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// A slider item for changing integer values.
/// </summary>
public class NativeSliderItem : NativeSlidableItem
{
#region Internal Fields
/// <summary>
/// The background of the slider.
/// </summary>
internal protected ScaledRectangle background = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 4, 32, 57)
};
/// <summary>
/// THe front of the slider.
/// </summary>
internal protected ScaledRectangle slider = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 57, 116, 200)
};
#endregion
#region Private Fields
/// <summary>
/// The maximum value of the slider.
/// </summary>
private int maximum = 0;
/// <summary>
/// The current value of the slider.
/// </summary>
private int _value = 100;
#endregion
#region Public Properties
/// <summary>
/// The color of the Slider.
/// </summary>
public Color SliderColor
{
get => slider.Color;
set => slider.Color = value;
}
/// <summary>
/// The maximum value of the slider.
/// </summary>
public int Maximum
{
get => maximum;
set
{
// If the value was not changed, return
if (maximum == value)
{
return;
}
// Otherwise, save the new value
maximum = value;
// If the current value is higher than the max, set the max
if (_value > maximum)
{
_value = maximum;
ValueChanged?.Invoke(this, EventArgs.Empty);
}
// Finally, update the location of the slider
UpdatePosition();
}
}
/// <summary>
/// The current value of the slider.
/// </summary>
public int Value
{
get => _value;
set
{
// If the value is over the limit, raise an exception
if (value > maximum)
{
throw new ArgumentOutOfRangeException(nameof(value), $"The value is over the maximum of {maximum - 1}");
}
// Otherwise, save it
_value = value;
// Trigger the respective event
ValueChanged?.Invoke(this, EventArgs.Empty);
// And update the location of the slider
UpdatePosition();
}
}
/// <summary>
/// The multiplier for increasing and decreasing the value.
/// </summary>
public int Multiplier { get; set; } = 1;
#endregion
#region Event
/// <summary>
/// Event triggered when the value of the menu changes.
/// </summary>
public event EventHandler ValueChanged;
#endregion
#region Constructors
/// <summary>
/// Creates a <see cref="NativeSliderItem"/> with a maximum of 100.
/// </summary>
/// <param name="title">The title of the Item.</param>
public NativeSliderItem(string title) : this(title, string.Empty, 100, 0)
{
}
/// <summary>
/// Creates a <see cref="NativeSliderItem"/> with a maximum of 100.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="description">The description of the Item.</param>
public NativeSliderItem(string title, string description) : this(title, description, 100, 0)
{
}
/// <summary>
/// Creates a <see cref="NativeSliderItem"/> with a specific current and maximum value.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="max">The maximum value of the Slider.</param>
/// <param name="value">The current value of the Slider.</param>
public NativeSliderItem(string title, int max, int value) : this(title, string.Empty, max, value)
{
}
/// <summary>
/// Creates a <see cref="NativeSliderItem"/> with a specific maximum.
/// </summary>
/// <param name="title">The title of the Item.</param>
/// <param name="description">The description of the Item.</param>
/// <param name="max">The maximum value of the Slider.</param>
/// <param name="value">The current value of the Slider.</param>
public NativeSliderItem(string title, string description, int max, int value) : base(title, description)
{
maximum = max;
_value = value;
}
#endregion
#region Internal Functions
/// <summary>
/// Updates the position of the bar based on the value.
/// </summary>
internal protected void UpdatePosition()
{
// Calculate the increment, and then the value of X
float increment = _value / (float)maximum;
float x = (background.Size.Width - slider.Size.Width) * increment;
// Then, add the X to the slider position
slider.Position = new PointF(background.Position.X + x, background.Position.Y);
}
#endregion
#region Public Functions
/// <summary>
/// Recalculates the item positions and sizes with the specified values.
/// </summary>
/// <param name="pos">The position of the item.</param>
/// <param name="size">The size of the item.</param>
/// <param name="selected">If this item has been selected.</param>
public override void Recalculate(PointF pos, SizeF size, bool selected)
{
base.Recalculate(pos, size, selected);
// Set the position and size of the background
background.Size = new SizeF(150, 9);
background.Position = new PointF(pos.X + size.Width - background.Size.Width - 7 - LeftArrow.Size.Width, pos.Y + 14);
// And do the same for the left arrow
LeftArrow.Position = new PointF(background.Position.X - LeftArrow.Size.Width, pos.Y + 4);
// Finally, set the correct position of the slider
slider.Size = new SizeF(75, 9);
UpdatePosition();
}
/// <summary>
/// Reduces the value of the slider.
/// </summary>
public override void GoLeft()
{
// Calculate the new value
int newValue = _value - Multiplier;
// If is under zero, set it to zero
if (newValue < 0)
{
Value = 0;
}
// Otherwise, set it to the new value
else
{
Value = newValue;
}
}
/// <summary>
/// Increases the value of the slider.
/// </summary>
public override void GoRight()
{
// Calculate the new value
int newValue = _value + Multiplier;
// If the value is over the maximum, set the max
if (newValue > maximum)
{
Value = maximum;
}
// Otherwise, set the calculated value
else
{
Value = newValue;
}
}
/// <summary>
/// Draws the slider.
/// </summary>
public override void Draw()
{
base.Draw(); // Arrows, Title and Left Badge
background.Draw();
slider.Draw();
}
#endregion
}
}

View File

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using LemonUI.Elements;
namespace LemonUI.Menus
{
/// <summary>
/// Represents the Information of a specific field in a <see cref="NativeStatsPanel"/>.
/// </summary>
public class NativeStatsInfo
{
#region Fields
private readonly ScaledText text = new ScaledText(PointF.Empty, string.Empty, 0.35f);
private float value = 100;
private readonly List<ScaledRectangle> backgrounds = new List<ScaledRectangle>();
private readonly List<ScaledRectangle> foregrounds = new List<ScaledRectangle>();
#endregion
#region Properties
/// <summary>
/// The name of the Stats Field.
/// </summary>
public string Name
{
get => text.Text;
set => text.Text = value;
}
/// <summary>
/// The value of the Stats bar.
/// </summary>
public float Value
{
get => value;
set
{
if (value > 100 || value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "The Value of the Stat can't be over 100 or under 0.");
}
this.value = value;
UpdateBars();
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new Stat Info with the specified name and value set to zero.
/// </summary>
/// <param name="name">The name of the Stat.</param>
public NativeStatsInfo(string name) : this(name, 0)
{
}
/// <summary>
/// Creates a new Stat Info with the specified name and value.
/// </summary>
/// <param name="name">The name of the Stat.</param>
/// <param name="value"></param>
public NativeStatsInfo(string name, int value)
{
Name = name;
this.value = value;
for (int i = 0; i < 5; i++)
{
backgrounds.Add(new ScaledRectangle(PointF.Empty, SizeF.Empty));
foregrounds.Add(new ScaledRectangle(PointF.Empty, SizeF.Empty));
}
}
#endregion
#region Functions
internal void SetColor(Color background, Color foreground)
{
foreach (ScaledRectangle rectangle in backgrounds)
{
rectangle.Color = background;
}
foreach (ScaledRectangle rectangle in foregrounds)
{
rectangle.Color = foreground;
}
}
/// <summary>
/// Updates the values of the bars.
/// </summary>
private void UpdateBars()
{
SizeF @default = new SizeF(35, 9);
// FIRST BAR
if (value > 0 && value < 20)
{
foregrounds[0].Size = new SizeF(@default.Width * (value / 20), @default.Height);
}
else
{
foregrounds[0].Size = value > 20 ? @default : SizeF.Empty;
}
// SECOND BAR
if (value > 20 && value < 40)
{
foregrounds[1].Size = new SizeF(@default.Width * ((value - 20) / 20), @default.Height);
}
else
{
foregrounds[1].Size = value > 40 ? @default : SizeF.Empty;
}
// THIRD BAR
if (value > 40 && value < 60)
{
foregrounds[2].Size = new SizeF(@default.Width * ((value - 40) / 20), @default.Height);
}
else
{
foregrounds[2].Size = value > 60 ? @default : SizeF.Empty;
}
// FOURTH BAR
if (value > 60 && value < 80)
{
foregrounds[3].Size = new SizeF(@default.Width * ((value - 60) / 20), @default.Height);
}
else
{
foregrounds[3].Size = value > 80 ? @default : SizeF.Empty;
}
// FIFTH BAR
if (value > 80 && value < 100)
{
foregrounds[4].Size = new SizeF(@default.Width * ((value - 80) / 20), @default.Height);
}
else
{
foregrounds[4].Size = value == 100 ? @default : SizeF.Empty;
}
}
/// <summary>
/// Recalculates the position of the stat Text and Bar.
/// </summary>
/// <param name="position">The new position fot the Stat.</param>
/// <param name="width">The Width of the parent Stats Panel.</param>
public void Recalculate(PointF position, float width)
{
text.Position = new PointF(position.X, position.Y);
for (int i = 0; i < 5; i++)
{
PointF pos = new PointF(position.X + width - 234 + ((35 + 3) * i), position.Y + 10);
backgrounds[i].Position = pos;
backgrounds[i].Size = new SizeF(35, 9);
foregrounds[i].Position = pos;
}
UpdateBars();
}
/// <summary>
/// Draws the stat information.
/// </summary>
public void Draw()
{
foreach (ScaledRectangle background in backgrounds)
{
background.Draw();
}
foreach (ScaledRectangle foreground in foregrounds)
{
foreground.Draw();
}
text.Draw();
}
#endregion
}
}

View File

@ -0,0 +1,167 @@
using LemonUI.Elements;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace LemonUI.Menus
{
/// <summary>
/// Represents a Statistics panel.
/// </summary>
public class NativeStatsPanel : NativePanel, IContainer<NativeStatsInfo>
{
#region Fields
private readonly List<NativeStatsInfo> fields = new List<NativeStatsInfo>();
private PointF lastPosition = PointF.Empty;
private float lastWidth = 0;
private Color backgroundColor = Color.FromArgb(255, 169, 169, 169);
private Color foregroundColor = Color.FromArgb(255, 255, 255, 255);
#endregion
#region Properties
/// <summary>
/// The color of the background of the bars.
/// </summary>
public Color BackgroundColor
{
get => backgroundColor;
set
{
backgroundColor = value;
foreach (NativeStatsInfo field in fields)
{
field.SetColor(value, foregroundColor);
}
}
}
/// <summary>
/// The color of the foreground of the bars.
/// </summary>
public Color ForegroundColor
{
get => foregroundColor;
set
{
foregroundColor = value;
foreach (NativeStatsInfo field in fields)
{
field.SetColor(backgroundColor, value);
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new Stats Panel.
/// </summary>
/// <param name="stats">The Statistics to add.</param>
public NativeStatsPanel(params NativeStatsInfo[] stats)
{
foreach (NativeStatsInfo field in stats)
{
Add(field);
}
}
#endregion
#region Functions
/// <summary>
/// Adds a stat to the player field.
/// </summary>
/// <param name="field">The Field to add.</param>
public void Add(NativeStatsInfo field)
{
if (fields.Contains(field))
{
throw new InvalidOperationException("Stat is already part of the panel.");
}
fields.Add(field);
field.SetColor(backgroundColor, foregroundColor);
}
/// <summary>
/// Removes a field from the panel.
/// </summary>
/// <param name="field">The field to remove.</param>
public void Remove(NativeStatsInfo field)
{
if (!fields.Contains(field))
{
return;
}
fields.Remove(field);
Recalculate();
}
/// <summary>
/// Removes the items that match the function.
/// </summary>
/// <param name="func">The function used to match items.</param>
public void Remove(Func<NativeStatsInfo, bool> func)
{
foreach (NativeStatsInfo item in new List<NativeStatsInfo>(fields))
{
if (func(item))
{
Remove(item);
}
}
}
/// <summary>
/// Removes all of the Stats fields.
/// </summary>
public void Clear()
{
fields.Clear();
Recalculate();
}
/// <summary>
/// Checks if the field is part of the Stats Panel.
/// </summary>
/// <param name="field">The field to check.</param>
/// <returns><see langword="true"/> if the item is part of the Panel, <see langword="false"/> otherwise.</returns>
public bool Contains(NativeStatsInfo field) => fields.Contains(field);
/// <summary>
/// Recalculates the Stats panel with the last known Position and Width.
/// </summary>
public void Recalculate() => Recalculate(lastPosition, lastWidth);
/// <summary>
/// Recalculates the position of the Stats panel.
/// </summary>
/// <param name="position">The new position of the Stats Panel.</param>
/// <param name="width">The width of the menu.</param>
public override void Recalculate(PointF position, float width)
{
base.Recalculate(position, width);
Background.Size = new SizeF(width, (fields.Count * 38) + 9);
for (int i = 0; i < fields.Count; i++)
{
fields[i].Recalculate(new PointF(position.X + 9, position.Y + 9 + (38 * i)), width);
}
}
/// <summary>
/// Processes the Stats Panel.
/// </summary>
public override void Process()
{
base.Process();
foreach (NativeStatsInfo info in fields)
{
info.Draw();
}
}
#endregion
}
}

View File

@ -0,0 +1,75 @@
using System;
namespace LemonUI.Menus
{
/// <summary>
/// Item used for opening submenus.
/// </summary>
public class NativeSubmenuItem : NativeItem
{
#region Public Properties
/// <summary>
/// The menu opened by this item.
/// </summary>
public NativeMenu Menu { get; }
#endregion
#region Constructors
/// <summary>
/// Creates a new Item that opens a Submenu.
/// </summary>
/// <param name="menu">The menu that this item will open.</param>
/// <param name="parent">The parent menu where this item will be located.</param>
public NativeSubmenuItem(NativeMenu menu, NativeMenu parent) : this(menu, parent, ">>>")
{
}
/// <summary>
/// Creates a new Item that opens a Submenu.
/// </summary>
/// <param name="menu">The menu that this item will open.</param>
/// <param name="parent">The parent menu where this item will be located.</param>
/// <param name="endlabel">The alternative title of the item, shown on the right.</param>
public NativeSubmenuItem(NativeMenu menu, NativeMenu parent, string endlabel) : base(menu.Subtitle, menu.Description, endlabel)
{
Menu = menu ?? throw new ArgumentNullException(nameof(menu));
Menu.Parent = parent ?? throw new ArgumentNullException(nameof(parent));
Activated += NativeSubmenuItem_Activated;
}
#endregion
#region Functions
/// <inheritdoc/>
public override void Draw()
{
// There is no Process(), so let's use draw to update the description
if (Description != Menu.Description)
{
Description = Menu.Description;
}
base.Draw();
}
#endregion
#region Local Events
private void NativeSubmenuItem_Activated(object sender, EventArgs e)
{
Menu.Parent.Visible = false;
if (!Menu.Parent.Visible)
{
Menu.Visible = true;
}
}
#endregion
}
}

View File

@ -0,0 +1,28 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the selection of an item in the screen.
/// </summary>
public class SelectedEventArgs
{
/// <summary>
/// The index of the item in the full list of items.
/// </summary>
public int Index { get; }
/// <summary>
/// The index of the item in the screen.
/// </summary>
public int OnScreen { get; }
/// <summary>
/// Creates a new <see cref="SelectedEventArgs"/>.
/// </summary>
/// <param name="index">The index of the item in the menu.</param>
/// <param name="screen">The index of the item based on the number of items shown on screen,</param>
public SelectedEventArgs(int index, int screen)
{
Index = index;
OnScreen = screen;
}
}
}

View File

@ -0,0 +1,9 @@
namespace LemonUI.Menus
{
/// <summary>
/// Represents the method that is called when a new item is selected in the Menu.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="SelectedEventArgs"/> with the index information.</param>
public delegate void SelectedEventHandler(object sender, SelectedEventArgs e);
}

View File

@ -0,0 +1,21 @@
namespace LemonUI.Menus
{
/// <summary>
/// The behavior of the <see cref="NativeMenu"/>'s subtitle.
/// </summary>
public enum SubtitleBehavior
{
/// <summary>
/// The subtitle will always be shown.
/// </summary>
AlwaysShow = 0,
/// <summary>
/// The subtitle will always be shown, except when is empty.
/// </summary>
ShowIfRequired = 1,
/// <summary>
/// The subtitle will never be shown.
/// </summary>
AlwaysHide = 2
}
}

View File

@ -0,0 +1,297 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.UI;
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
using RAGE.NUI;
#elif RPH
using Rage;
using Rage.Native;
#elif SHVDN3
using GTA.Native;
using GTA.UI;
#endif
using System;
using System.Collections.Concurrent;
using System.Drawing;
namespace LemonUI
{
/// <summary>
/// Represents the method that reports a Resolution change in the Game Settings.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="ResolutionChangedEventArgs"/> containing the Previous and Current resolution.</param>
public delegate void ResolutionChangedEventHandler(object sender, ResolutionChangedEventArgs e);
/// <summary>
/// Represents the method that reports a Safe Zone change in the Game Settings.
/// </summary>
/// <param name="sender">The source of the event event.</param>
/// <param name="e">A <see cref="ResolutionChangedEventArgs"/> containing the Previous and Current Safe Zone.</param>
public delegate void SafeZoneChangedEventHandler(object sender, SafeZoneChangedEventArgs e);
/// <summary>
/// Represents the information after a Resolution Change in the game.
/// </summary>
public class ResolutionChangedEventArgs
{
/// <summary>
/// The Game Resolution before it was changed.
/// </summary>
public SizeF Before { get; }
/// <summary>
/// The Game Resolution after it was changed.
/// </summary>
public SizeF After { get; }
internal ResolutionChangedEventArgs(SizeF before, SizeF after)
{
Before = before;
After = after;
}
}
/// <summary>
/// Represents the information after a Safe Zone Change in the game.
/// </summary>
public class SafeZoneChangedEventArgs
{
/// <summary>
/// The raw Safezone size before the change.
/// </summary>
public float Before { get; }
/// <summary>
/// The Safezone size after the change.
/// </summary>
public float After { get; }
internal SafeZoneChangedEventArgs(float before, float after)
{
Before = before;
After = after;
}
}
/// <summary>
/// Manager for Menus and Items.
/// </summary>
public class ObjectPool
{
#region Private Fields
/// <summary>
/// The last known resolution by the object pool.
/// </summary>
#if FIVEM
private SizeF lastKnownResolution = CitizenFX.Core.UI.Screen.Resolution;
#elif RAGEMP
private SizeF lastKnownResolution = new SizeF(Game.ScreenResolution.Width, Game.ScreenResolution.Height);
#elif RPH
private SizeF lastKnownResolution = Game.Resolution;
#elif SHVDN3
private SizeF lastKnownResolution = GTA.UI.Screen.Resolution;
#endif
/// <summary>
/// The last know Safezone size.
/// </summary>
#if FIVEM
private float lastKnownSafezone = API.GetSafeZoneSize();
#elif RAGEMP
private float lastKnownSafezone = Invoker.Invoke<float>(Natives.GetSafeZoneSize);
#elif RPH
private float lastKnownSafezone = NativeFunction.CallByHash<float>(0xBAF107B6BB2C97F0);
#elif SHVDN3
private float lastKnownSafezone = Function.Call<float>(Hash.GET_SAFE_ZONE_SIZE);
#endif
/// <summary>
/// The list of processable objects.
/// </summary>
private readonly ConcurrentDictionary<int, IProcessable> objects = new ConcurrentDictionary<int, IProcessable>();
#endregion
#region Public Properties
/// <summary>
/// Checks if there are objects visible on the screen.
/// </summary>
public bool AreAnyVisible
{
get
{
// Iterate over the objects
foreach (IProcessable obj in objects.Values)
{
// If is visible return true
if (obj.Visible)
{
return true;
}
}
// If none were visible return false
return false;
}
}
#endregion
#region Events
/// <summary>
/// Event triggered when the game resolution is changed.
/// </summary>
public event ResolutionChangedEventHandler ResolutionChanged;
/// <summary>
/// Event triggered when the Safezone size option in the Display settings is changed.
/// </summary>
public event SafeZoneChangedEventHandler SafezoneChanged;
#endregion
#region Tools
/// <summary>
/// Detects resolution changes by comparing the last known resolution and the current one.
/// </summary>
private void DetectResolutionChanges()
{
// Get the current resolution
#if FIVEM
SizeF resolution = CitizenFX.Core.UI.Screen.Resolution;
#elif RAGEMP
ScreenResolutionType raw = Game.ScreenResolution;
SizeF resolution = new SizeF(raw.Width, raw.Height);
#elif RPH
SizeF resolution = Game.Resolution;
#elif SHVDN3
SizeF resolution = GTA.UI.Screen.Resolution;
#endif
// If the old res does not matches the current one
if (lastKnownResolution != resolution)
{
// Trigger the event
ResolutionChanged?.Invoke(this, new ResolutionChangedEventArgs(lastKnownResolution, resolution));
// Refresh everything
RefreshAll();
// And save the new resolution
lastKnownResolution = resolution;
}
}
/// <summary>
/// Detects Safezone changes by comparing the last known value to the current one.
/// </summary>
private void DetectSafezoneChanges()
{
// Get the current Safezone size
#if FIVEM
float safezone = API.GetSafeZoneSize();
#elif RAGEMP
float safezone = Invoker.Invoke<float>(Natives.GetSafeZoneSize);
#elif RPH
float safezone = NativeFunction.CallByHash<float>(0xBAF107B6BB2C97F0);
#elif SHVDN3
float safezone = Function.Call<float>(Hash.GET_SAFE_ZONE_SIZE);
#endif
// If is not the same as the last one
if (lastKnownSafezone != safezone)
{
// Trigger the event
SafezoneChanged?.Invoke(this, new SafeZoneChangedEventArgs(lastKnownSafezone, safezone));
// Refresh everything
RefreshAll();
// And save the new safezone
lastKnownSafezone = safezone;
}
}
#endregion
#region Public Function
/// <summary>
/// Adds the object into the pool.
/// </summary>
/// <param name="obj">The object to add.</param>
public void Add(IProcessable obj)
{
// Make sure that the object is not null
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
int key = obj.GetHashCode();
// Otherwise, add it to the general pool
if (objects.ContainsKey(key))
{
throw new InvalidOperationException("The object is already part of this pool.");
}
objects.TryAdd(key, obj);
}
/// <summary>
/// Removes the object from the pool.
/// </summary>
/// <param name="obj">The object to remove.</param>
public void Remove(IProcessable obj)
{
objects.TryRemove(obj.GetHashCode(), out _);
}
/// <summary>
/// Performs the specified action on each element that matches T.
/// </summary>
/// <typeparam name="T">The type to match.</typeparam>
/// <param name="action">The action delegate to perform on each T.</param>
public void ForEach<T>(Action<T> action)
{
foreach (IProcessable obj in objects.Values)
{
if (obj is T conv)
{
action(conv);
}
}
}
/// <summary>
/// Refreshes all of the items.
/// </summary>
public void RefreshAll()
{
// Iterate over the objects and recalculate those possible
foreach (IProcessable obj in objects.Values)
{
if (obj is IRecalculable recal)
{
recal.Recalculate();
}
}
}
/// <summary>
/// Hides all of the objects.
/// </summary>
public void HideAll()
{
foreach (IProcessable obj in objects.Values)
{
obj.Visible = false;
}
}
/// <summary>
/// Processes the objects and features in this pool.
/// This needs to be called every tick.
/// </summary>
public void Process()
{
// See if there are resolution or safezone changes
DetectResolutionChanges();
DetectSafezoneChanges();
// And process the objects in the pool
foreach (IProcessable obj in objects.Values)
{
obj.Process();
}
}
#endregion
}
}

View File

@ -0,0 +1,342 @@
#if FIVEM
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
#elif SHVDN3
using GTA.Native;
#endif
using System;
namespace LemonUI.Scaleform
{
/// <summary>
/// Represents a generic Scaleform object.
/// </summary>
public abstract class BaseScaleform : IScaleform
{
#region Private Fields
#if FIVEM || SHVDN3
/// <summary>
/// The ID of the scaleform.
/// </summary>
[Obsolete("Please use the Handle or Name properties and call the methods manually.", true)]
#endif
#if FIVEM
protected CitizenFX.Core.Scaleform scaleform = new CitizenFX.Core.Scaleform(string.Empty);
#elif SHVDN3
protected GTA.Scaleform scaleform = new GTA.Scaleform(string.Empty);
#endif
#endregion
#region Public Properties
/// <summary>
/// The ID or Handle of the Scaleform.
/// </summary>
public int Handle { get; private set; }
/// <summary>
/// The Name of the Scaleform.
/// </summary>
public string Name { get; }
/// <summary>
/// If the Scaleform should be visible or not.
/// </summary>
public bool Visible { get; set; }
/// <summary>
/// If the Scaleform is loaded or not.
/// </summary>
public bool IsLoaded
{
get
{
#if FIVEM
return API.HasScaleformMovieLoaded(Handle);
#elif RAGEMP
return Invoker.Invoke<bool>(Natives.HasScaleformMovieLoaded, Handle);
#elif RPH
return NativeFunction.CallByHash<bool>(0x85F01B8D5B90570E, Handle);
#elif SHVDN3
return Function.Call<bool>(Hash.HAS_SCALEFORM_MOVIE_LOADED, Handle);
#endif
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a new Scaleform class with the specified Scaleform object name.
/// </summary>
/// <param name="sc">The Scalform object.</param>
public BaseScaleform(string sc)
{
Name = sc;
#if FIVEM
Handle = API.RequestScaleformMovie(Name);
#elif RAGEMP
Handle = Invoker.Invoke<int>(Natives.RequestScaleformMovie, Name);
#elif RPH
Handle = NativeFunction.CallByHash<int>(0x11FE353CF9733E6F, Name);
#elif SHVDN3
Handle = Function.Call<int>(Hash.REQUEST_SCALEFORM_MOVIE, Name);
#endif
}
#endregion
#region Private Functions
private void CallFunctionBase(string function, params object[] parameters)
{
#if FIVEM
API.BeginScaleformMovieMethod(Handle, function);
#elif RAGEMP
Invoker.Invoke(0xF6E48914C7A8694E, Handle, function);
#elif RPH
NativeFunction.CallByHash<int>(0xF6E48914C7A8694E, Handle, function);
#elif SHVDN3
Function.Call((Hash)0xF6E48914C7A8694E, Handle, function);
#endif
foreach (object obj in parameters)
{
if (obj is int objInt)
{
#if FIVEM
API.ScaleformMovieMethodAddParamInt(objInt);
#elif RAGEMP
Invoker.Invoke(0xC3D0841A0CC546A6, objInt);
#elif RPH
NativeFunction.CallByHash<int>(0xC3D0841A0CC546A6, objInt);
#elif SHVDN3
Function.Call((Hash)0xC3D0841A0CC546A6, objInt);
#endif
}
else if (obj is string objString)
{
#if FIVEM
API.BeginTextCommandScaleformString("STRING");
API.AddTextComponentSubstringPlayerName(objString);
API.EndTextCommandScaleformString();
#elif RAGEMP
Invoker.Invoke(Natives.BeginTextCommandScaleformString, "STRING");
Invoker.Invoke(Natives.AddTextComponentSubstringPlayerName, objString);
Invoker.Invoke(Natives.EndTextCommandScaleformString);
#elif RPH
NativeFunction.CallByHash<int>(0x80338406F3475E55, "STRING");
NativeFunction.CallByHash<int>(0x6C188BE134E074AA, objString);
NativeFunction.CallByHash<int>(0x362E2D3FE93A9959);
#elif SHVDN3
Function.Call((Hash)0x80338406F3475E55, "STRING");
Function.Call((Hash)0x6C188BE134E074AA, objString);
Function.Call((Hash)0x362E2D3FE93A9959);
#endif
}
else if (obj is float objFloat)
{
#if FIVEM
API.ScaleformMovieMethodAddParamFloat(objFloat);
#elif RAGEMP
Invoker.Invoke(0xD69736AAE04DB51A, objFloat);
#elif RPH
NativeFunction.CallByHash<int>(0xD69736AAE04DB51A, objFloat);
#elif SHVDN3
Function.Call((Hash)0xD69736AAE04DB51A, objFloat);
#endif
}
else if (obj is double objDouble)
{
#if FIVEM
API.ScaleformMovieMethodAddParamFloat((float)objDouble);
#elif RAGEMP
Invoker.Invoke(0xD69736AAE04DB51A, (float)objDouble);
#elif RPH
NativeFunction.CallByHash<int>(0xD69736AAE04DB51A, (float)objDouble);
#elif SHVDN3
Function.Call((Hash)0xD69736AAE04DB51A, (float)objDouble);
#endif
}
else if (obj is bool objBool)
{
#if FIVEM
API.ScaleformMovieMethodAddParamBool(objBool);
#elif RAGEMP
Invoker.Invoke(0xC58424BA936EB458, objBool);
#elif RPH
NativeFunction.CallByHash<int>(0xC58424BA936EB458, objBool);
#elif SHVDN3
Function.Call((Hash)0xC58424BA936EB458, objBool);
#endif
}
else
{
throw new ArgumentException($"Unexpected argument type {obj.GetType().Name}.", nameof(parameters));
}
}
}
#endregion
#region Public Functions
/// <summary>
/// Checks if the specified Scaleform Return Value is ready to be fetched.
/// </summary>
/// <param name="id">The Identifier of the Value.</param>
/// <returns><see langword="true"/> if the value is ready, <see langword="false"/> otherwise.</returns>
public bool IsValueReady(int id)
{
#if FIVEM
return API.IsScaleformMovieMethodReturnValueReady(id);
#elif RAGEMP
return Invoker.Invoke<bool>(Natives._0x768FF8961BA904D6, id);
#elif RPH
return NativeFunction.CallByHash<bool>(0x768FF8961BA904D6, id);
#elif SHVDN3
return Function.Call<bool>(Hash.IS_SCALEFORM_MOVIE_METHOD_RETURN_VALUE_READY, id);
#endif
}
/// <summary>
/// Gets a specific value.
/// </summary>
/// <typeparam name="T">The type of value.</typeparam>
/// <param name="id">The Identifier of the value.</param>
/// <returns>The value returned by the native.</returns>
public T GetValue<T>(int id)
{
if (typeof(T) == typeof(string))
{
#if FIVEM
return (T)(object)API.GetScaleformMovieMethodReturnValueString(id);
#elif RAGEMP
return Invoker.Invoke<T>(0xE1E258829A885245, id);
#elif RPH
return (T)NativeFunction.CallByHash(0xE1E258829A885245, typeof(string), id);
#elif SHVDN3
return (T)(object)Function.Call<string>(Hash.GET_SCALEFORM_MOVIE_METHOD_RETURN_VALUE_STRING, id);
#endif
}
else if (typeof(T) == typeof(int))
{
#if FIVEM
return (T)(object)API.GetScaleformMovieMethodReturnValueInt(id);
#elif RAGEMP
return Invoker.Invoke<T>(0x2DE7EFA66B906036, id);
#elif RPH
return (T)(object)NativeFunction.CallByHash<int>(0x2DE7EFA66B906036, id);
#elif SHVDN3
return (T)(object)Function.Call<int>(Hash.GET_SCALEFORM_MOVIE_METHOD_RETURN_VALUE_INT, id);
#endif
}
else if (typeof(T) == typeof(bool))
{
#if FIVEM
return (T)(object)API.GetScaleformMovieMethodReturnValueBool(id);
#elif RAGEMP
return Invoker.Invoke<T>(0xD80A80346A45D761, id);
#elif RPH
return (T)(object)NativeFunction.CallByHash<bool>(0xD80A80346A45D761, id);
#elif SHVDN3
return (T)(object)Function.Call<bool>(Hash._GET_SCALEFORM_MOVIE_METHOD_RETURN_VALUE_BOOL, id);
#endif
}
else
{
throw new InvalidOperationException($"Expected string, int or bool, got {typeof(T).Name}.");
}
}
/// <summary>
/// Calls a Scaleform function.
/// </summary>
/// <param name="function">The name of the function to call.</param>
/// <param name="parameters">The parameters to pass.</param>
public void CallFunction(string function, params object[] parameters)
{
CallFunctionBase(function, parameters);
#if FIVEM
API.EndScaleformMovieMethod();
#elif RAGEMP
Invoker.Invoke(0xC6796A8FFA375E53);
#elif RPH
NativeFunction.CallByHash<int>(0xC6796A8FFA375E53);
#elif SHVDN3
Function.Call((Hash)0xC6796A8FFA375E53);
#endif
}
/// <summary>
/// Calls a Scaleform function with a return value.
/// </summary>
/// <param name="function">The name of the function to call.</param>
/// <param name="parameters">The parameters to pass.</param>
public int CallFunctionReturn(string function, params object[] parameters)
{
CallFunctionBase(function, parameters);
#if FIVEM
return API.EndScaleformMovieMethodReturnValue();
#elif RAGEMP
return Invoker.Invoke<int>(0xC50AA39A577AF886);
#elif RPH
return NativeFunction.CallByHash<int>(0xC50AA39A577AF886);
#elif SHVDN3
return Function.Call<int>((Hash)0xC50AA39A577AF886);
#endif
}
/// <summary>
/// Updates the parameters of the Scaleform.
/// </summary>
public abstract void Update();
/// <summary>
/// Draws the scaleform full screen.
/// </summary>
public virtual void DrawFullScreen()
{
if (!Visible)
{
return;
}
Update();
#if FIVEM
API.DrawScaleformMovieFullscreen(Handle, 255, 255, 255, 255, 0);
#elif RAGEMP
Invoker.Invoke(Natives.DrawScaleformMovieFullscreen, 255, 255, 255, 255, 0);
#elif RPH
NativeFunction.CallByHash<int>(0x0DF606929C105BE1, Handle, 255, 255, 255, 255, 0);
#elif SHVDN3
Function.Call(Hash.DRAW_SCALEFORM_MOVIE_FULLSCREEN, Handle, 255, 255, 255, 255, 0);
#endif
}
/// <summary>
/// Draws the scaleform full screen.
/// </summary>
public virtual void Draw() => DrawFullScreen();
/// <summary>
/// Draws the scaleform full screen.
/// </summary>
public virtual void Process() => DrawFullScreen();
/// <summary>
/// Marks the scaleform as no longer needed.
/// </summary>
public void Dispose()
{
int id = Handle;
#if FIVEM
API.SetScaleformMovieAsNoLongerNeeded(ref id);
#elif RAGEMP
Invoker.Invoke(Natives.SetScaleformMovieAsNoLongerNeeded, id);
#elif RPH
NativeFunction.CallByHash<int>(0x1D132D614DD86811, new NativeArgument(id));
#elif SHVDN3
Function.Call(Hash.SET_SCALEFORM_MOVIE_AS_NO_LONGER_NEEDED, new OutputArgument(id));
#endif
}
#endregion
}
}

View File

@ -0,0 +1,357 @@
#if FIVEM
using CitizenFX.Core;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
#elif SHVDN3
using GTA;
#endif
using System;
namespace LemonUI.Scaleform
{
/// <summary>
/// The type for a big message.
/// </summary>
public enum MessageType
{
/// <summary>
/// A centered message with customizable text an d background colors.
/// Internally called SHOW_SHARD_CENTERED_MP_MESSAGE.
/// </summary>
Customizable = 0,
/// <summary>
/// Used when you rank up on GTA Online.
/// Internally called SHOW_SHARD_CREW_RANKUP_MP_MESSAGE.
/// </summary>
RankUp = 1,
/// <summary>
/// The Mission Passed screen on PS3 and Xbox 360.
/// Internally called SHOW_MISSION_PASSED_MESSAGE.
/// </summary>
MissionPassedOldGen = 2,
/// <summary>
/// The Message Type shown on the Wasted screen.
/// Internally called SHOW_SHARD_WASTED_MP_MESSAGE.
/// </summary>
Wasted = 3,
/// <summary>
/// Used on the GTA Online Freemode event announcements.
/// Internally called SHOW_PLANE_MESSAGE.
/// </summary>
Plane = 4,
/// <summary>
/// Development leftover from when GTA Online was Cops and Crooks.
/// Internally called SHOW_BIG_MP_MESSAGE.
/// </summary>
CopsAndCrooks = 5,
/// <summary>
/// Message shown when the player purchases a weapon.
/// Internally called SHOW_WEAPON_PURCHASED.
/// </summary>
Weapon = 6,
/// <summary>
/// Unknown where this one is used.
/// Internally called SHOW_CENTERED_MP_MESSAGE_LARGE.
/// </summary>
CenteredLarge = 7,
}
/// <summary>
/// A customizable big message.
/// </summary>
public class BigMessage : BaseScaleform
{
#region Constant Fields
private const uint unarmed = 0xA2719263;
#endregion
#region Private Fields
private MessageType type;
private uint weaponHash;
private uint hideAfter;
#endregion
#region Public Properties
/// <summary>
/// The title of the message.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The subtitle or description of the message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// The color of the text.
/// Only used on the Customizable message type.
/// </summary>
public int TextColor { get; set; }
/// <summary>
/// The color of the background in the Customizable message type.
/// </summary>
public int BackgroundColor { get; set; }
/// <summary>
/// The Rank when the mode is set to Cops and Crooks.
/// </summary>
public string Rank { get; set; }
#if !RAGEMP
/// <summary>
/// The hash of the Weapon as an enum.
/// </summary>
public WeaponHash Weapon
{
get => (WeaponHash)weaponHash;
set => weaponHash = (uint)value;
}
#endif
/// <summary>
/// The hash of the Weapon as it's native value.
/// </summary>
public uint WeaponHash
{
get => weaponHash;
set => weaponHash = value;
}
/// <summary>
/// The type of message to show.
/// </summary>
public MessageType Type
{
get => type;
set
{
if (!Enum.IsDefined(typeof(MessageType), value))
{
throw new InvalidOperationException($"{value} is not a valid message type.");
}
type = value;
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a standard customizable message with just a title.
/// </summary>
/// <param name="title">The title to use.</param>
public BigMessage(string title) : this(title, string.Empty, string.Empty, unarmed, 0, 0, MessageType.Customizable)
{
}
/// <summary>
/// Creates a custom message with the specified title.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="type">The type of message.</param>
public BigMessage(string title, MessageType type) : this(title, string.Empty, string.Empty, unarmed, 0, 0, type)
{
}
/// <summary>
/// Creates a standard customizable message with a title and message.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
public BigMessage(string title, string message) : this(title, message, string.Empty, unarmed, 0, 0, MessageType.Customizable)
{
}
/// <summary>
/// Creates a Cops and Crooks message type.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
/// <param name="rank">Text to show in the Rank space.</param>
public BigMessage(string title, string message, string rank) : this(title, message, rank, unarmed, 0, 0, MessageType.CopsAndCrooks)
{
}
/// <summary>
/// Creates a message with the specified type, title and message.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
/// <param name="type">The type of message.</param>
public BigMessage(string title, string message, MessageType type) : this(title, message, string.Empty, unarmed, 0, 0, type)
{
}
/// <summary>
/// Creates a standard customizable message with a title and a custom text color.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="colorText">The color of the text.</param>
public BigMessage(string title, int colorText) : this(title, string.Empty, string.Empty, unarmed, colorText, 0, MessageType.Customizable)
{
}
/// <summary>
/// Creates a standard customizable message with a specific title and custom colors.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="colorText">The color of the text.</param>
/// <param name="colorBackground">The color of the background.</param>
public BigMessage(string title, int colorText, int colorBackground) : this(title, string.Empty, string.Empty, unarmed, colorText, colorBackground, MessageType.Customizable)
{
}
#if !RAGEMP
/// <summary>
/// Creates a Weapon Purchase message with a custom text and weapons.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="weapon">The name of the Weapon.</param>
/// <param name="hash">The hash of the Weapon image.</param>
public BigMessage(string title, string weapon, WeaponHash hash) : this(title, string.Empty, weapon, hash, 0, 0, MessageType.Weapon)
{
}
/// <summary>
/// Creates a Weapon Purchase message with a custom text and weapons.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
/// <param name="weapon">The name of the Weapon.</param>
/// <param name="hash">The hash of the Weapon image.</param>
public BigMessage(string title, string message, string weapon, WeaponHash hash) : this(title, message, weapon, hash, 0, 0, MessageType.Weapon)
{
}
/// <summary>
/// Creates a message with all of the selected information.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
/// <param name="rank">The Rank on Cops and Crooks.</param>
/// <param name="weapon">The hash of the Weapon image.</param>
/// <param name="colorText">The color of the text.</param>
/// <param name="colorBackground">The color of the background.</param>
/// <param name="type">The type of message.</param>
public BigMessage(string title, string message, string rank, WeaponHash weapon, int colorText, int colorBackground, MessageType type) : this(title, message, rank, (uint)weapon, colorText, colorBackground, type)
{
}
#endif
/// <summary>
/// Creates a message with all of the selected information.
/// </summary>
/// <param name="title">The title to use.</param>
/// <param name="message">The message to show.</param>
/// <param name="rank">The Rank on Cops and Crooks.</param>
/// <param name="weapon">The hash of the Weapon image.</param>
/// <param name="colorText">The color of the text.</param>
/// <param name="colorBackground">The color of the background.</param>
/// <param name="type">The type of message.</param>
public BigMessage(string title, string message, string rank, uint weapon, int colorText, int colorBackground, MessageType type) : base("MP_BIG_MESSAGE_FREEMODE")
{
Title = title;
Message = message;
Rank = rank;
WeaponHash = weapon;
TextColor = colorText;
BackgroundColor = colorBackground;
Type = type;
Update();
}
#endregion
#region Public Functions
/// <summary>
/// Updates the Message information in the Scaleform.
/// </summary>
public override void Update()
{
// Select the correct function to call
string function;
switch (type)
{
case MessageType.Customizable:
function = "SHOW_SHARD_CENTERED_MP_MESSAGE";
break;
case MessageType.RankUp:
function = "SHOW_SHARD_CREW_RANKUP_MP_MESSAGE";
break;
case MessageType.MissionPassedOldGen:
function = "SHOW_MISSION_PASSED_MESSAGE";
break;
case MessageType.Wasted:
function = "SHOW_SHARD_WASTED_MP_MESSAGE";
break;
case MessageType.Plane:
function = "SHOW_PLANE_MESSAGE";
break;
case MessageType.CopsAndCrooks:
function = "SHOW_BIG_MP_MESSAGE";
break;
case MessageType.Weapon:
function = "SHOW_WEAPON_PURCHASED";
break;
case MessageType.CenteredLarge:
function = "SHOW_CENTERED_MP_MESSAGE_LARGE";
break;
default:
throw new InvalidOperationException($"{type} is not a valid message type.");
}
// And add the parameters
switch (type)
{
case MessageType.Customizable:
CallFunction(function, Title, Message, TextColor, BackgroundColor);
break;
case MessageType.CopsAndCrooks:
CallFunction(function, Title, Message, Rank);
break;
case MessageType.Weapon:
CallFunction(function, Title, Message, (int)weaponHash);
break;
default:
CallFunction(function, Title, Message);
break;
}
}
/// <summary>
/// Fades the big message out.
/// </summary>
/// <param name="time">The time it will take to do the fade.</param>
public void FadeOut(int time)
{
if (time < 0)
{
throw new ArgumentOutOfRangeException(nameof(time), "Time can't be under zero.");
}
CallFunction("SHARD_ANIM_OUT", 0, time);
#if RAGEMP
uint currentTime = (uint)Misc.GetGameTimer();
#elif RPH
uint currentTime = Game.GameTime;
#else
uint currentTime = (uint)Game.GameTime;
#endif
hideAfter = currentTime + (uint)time;
}
/// <inheritdoc/>
public override void DrawFullScreen()
{
#if RAGEMP
uint time = (uint)Misc.GetGameTimer();
#elif RPH
uint time = Game.GameTime;
#else
uint time = (uint)Game.GameTime;
#endif
if (hideAfter > 0 && time > hideAfter)
{
Visible = false;
hideAfter = 0;
}
base.DrawFullScreen();
}
#endregion
}
}

View File

@ -0,0 +1,425 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
using Rage.Native;
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.Native;
#endif
using System;
using System.Collections.Generic;
namespace LemonUI.Scaleform
{
/// <summary>
/// The Background of the BruteForce Hack Minigame.
/// </summary>
public enum BruteForceBackground
{
/// <summary>
/// A simple Black background.
/// </summary>
Black = 0,
/// <summary>
/// A simple Purple background.
/// </summary>
Purple = 1,
/// <summary>
/// A simple Gray background.
/// </summary>
Gray = 2,
/// <summary>
/// A simple Light Blue background.
/// </summary>
LightBlue = 3,
/// <summary>
/// A Light Blue Wallpaper.
/// </summary>
Wallpaper1 = 4,
/// <summary>
/// A Fade from Gray in the center to Black in the corners.
/// </summary>
DarkFade = 5,
}
/// <summary>
/// The status of the BruteForce Hack after finishing.
/// </summary>
public enum BruteForceStatus
{
/// <summary>
/// The user completed the hack successfully.
/// </summary>
Completed = 0,
/// <summary>
/// The user ran out of time.
/// </summary>
OutOfTime = 1,
/// <summary>
/// The player ran out of lives.
/// </summary>
OutOfLives = 2,
}
/// <summary>
/// Event information after an the BruteForce hack has finished.
/// </summary>
public class BruteForceFinishedEventArgs
{
/// <summary>
/// The final status of the Hack.
/// </summary>
public BruteForceStatus Status { get; }
internal BruteForceFinishedEventArgs(BruteForceStatus status)
{
Status = status;
}
}
/// <summary>
/// Represents the method that is called when the end user finishes the BruteForce hack.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An <see cref="BruteForceFinishedEventArgs"/> with the hack status.</param>
public delegate void BruteForceFinishedEventHandler(object sender, BruteForceFinishedEventArgs e);
/// <summary>
/// The BruteForce Hacking Minigame shown in multiple missions.
/// </summary>
public class BruteForce : BaseScaleform
{
#region Fields
private static readonly Random random = new Random();
private static readonly Sound soundRowSwitch = new Sound(string.Empty, "HACKING_MOVE_CURSOR");
private static readonly Sound soundRowCompleted = new Sound(string.Empty, "HACKING_CLICK");
private static readonly Sound soundRowFailed = new Sound(string.Empty, "HACKING_CLICK_BAD");
private static readonly Sound soundSuccess = new Sound(string.Empty, "HACKING_SUCCESS");
private int hideTime = -1;
private int output = 0;
private bool firstRun = true;
private bool inProgress = false;
private BruteForceBackground background = BruteForceBackground.Black;
private string word = "LEMONADE";
private int livesTotal = 5;
private int livesCurrent = 5;
private int closeAfter = -1;
private TimeSpan end = TimeSpan.Zero;
private TimeSpan countdown = TimeSpan.Zero;
private bool showLives = true;
#endregion
#region Properties
/// <summary>
/// The Word shown to select in the menu.
/// </summary>
public string Word
{
get => word;
set
{
if (value.Length != 8)
{
throw new ArgumentOutOfRangeException("The word needs to be exactly 8 characters long.", nameof(value));
}
word = value;
CallFunction("SET_ROULETTE_WORD", value);
}
}
/// <summary>
/// The background of the Hacking minigame.
/// </summary>
public BruteForceBackground Background
{
get => background;
set
{
background = value;
CallFunction("SET_BACKGROUND", (int)value);
}
}
/// <summary>
/// The number of Lives of the minigame.
/// </summary>
public int TotalLives
{
get => livesTotal;
set
{
livesTotal = value;
if (livesCurrent > value)
{
livesCurrent = value;
}
CallFunction("SET_LIVES", livesCurrent, value);
}
}
/// <summary>
/// The current number of lives that the player has.
/// </summary>
public int CurrentLives => livesCurrent;
/// <summary>
/// The messages that might appear on success.
/// </summary>
public List<string> SuccessMessages { get; } = new List<string>();
/// <summary>
/// The messages that will appear when the player fails.
/// </summary>
public List<string> FailMessages { get; } = new List<string>();
/// <summary>
/// The time in milliseconds to wait before closing the Hack window automatically.
/// </summary>
/// <remarks>
/// This can be set to -1 to keep the Hack window open.
/// </remarks>
public int CloseAfter
{
get => closeAfter;
set
{
if (value < -1)
{
throw new ArgumentOutOfRangeException("The Closure time can't be under -1.", nameof(value));
}
closeAfter = value;
}
}
/// <summary>
/// If the player can retry the hack after failing.
/// </summary>
public bool CanRetry { get; set; } = false;
/// <summary>
/// The countdown of the Hack minigame.
/// </summary>
public TimeSpan Countdown
{
get => countdown;
set => countdown = value;
}
/// <summary>
/// If the lives of the player should be shown on the top right.
/// </summary>
public bool ShowLives
{
get => showLives;
set
{
showLives = value;
CallFunction("SHOW_LIVES", value);
}
}
/// <summary>
/// If all of the rows should be restarted after the player fails one.
/// </summary>
public bool ResetOnRowFail { get; set; } = true;
#endregion
#region Events
/// <summary>
/// Event triggered when the player finishes a hack.
/// </summary>
public event BruteForceFinishedEventHandler HackFinished;
#endregion
#region Constructors
/// <summary>
/// Creates a new Hacking Scaleform.
/// </summary>
public BruteForce() : base("HACKING_PC")
{
Visible = false;
for (int i = 0; i < 8; i++)
{
SetColumnSpeed(i, 100);
}
}
#endregion
#region Functions
/// <summary>
/// Resets the entire Hacking minigame.
/// </summary>
public void Reset()
{
inProgress = true;
Background = background;
RunProgram(4);
RunProgram(83);
TotalLives = livesTotal;
Word = word;
ShowLives = showLives;
#if RAGEMP
int time = Misc.GetGameTimer();
#elif RPH
uint time = Game.GameTime;
#else
int time = Game.GameTime;
#endif
end = TimeSpan.FromMilliseconds(time) + countdown;
}
/// <summary>
/// Sets the speed of one of the 8 columns.
/// </summary>
/// <param name="index">The index of the column.</param>
/// <param name="speed">The speed of the column.</param>
public void SetColumnSpeed(int index, float speed)
{
if (index >= 8 || index < 0)
{
throw new ArgumentOutOfRangeException("The index needs to be between 0 and 7.", nameof(index));
}
CallFunction("SET_COLUMN_SPEED", index, speed);
}
/// <summary>
/// Runs the specified Hacking program.
/// </summary>
/// <param name="program">The program to open.</param>
public void RunProgram(int program)
{
CallFunction("RUN_PROGRAM", program);
}
/// <summary>
/// Updates the information of the Hacking window.
/// </summary>
public override void Update()
{
#if RAGEMP
int time = Misc.GetGameTimer();
#elif RPH
uint time = Game.GameTime;
#else
int time = Game.GameTime;
#endif
// If there is a time set to hide the Hack window
if (hideTime != -1)
{
// If that time has already passed, go ahead and hide the window
if (hideTime <= time)
{
Visible = false;
hideTime = -1;
return;
}
}
// If this is the first run and is not in progress, reset it
if (firstRun && !inProgress)
{
firstRun = false;
Reset();
}
// If the hack minigame is not in progress but the player can retry and he pressed enter, reset it
if (!inProgress && CanRetry && Controls.IsJustPressed(Control.FrontendAccept))
{
Reset();
hideTime = -1;
}
// If the Hack minigame is in progress
if (inProgress)
{
// If there is a countdown set
if (countdown > TimeSpan.Zero)
{
// Calculate the time left
TimeSpan span = countdown - (TimeSpan.FromMilliseconds(time) - end);
// If is lower or equal than zero, the player failed
if (span <= TimeSpan.Zero)
{
CallFunction("SET_COUNTDOWN", 0, 0, 0);
string err = FailMessages.Count == 0 ? string.Empty : FailMessages[random.Next(FailMessages.Count)];
CallFunction("SET_ROULETTE_OUTCOME", false, err);
hideTime = closeAfter == -1 ? -1 : (int)time + CloseAfter;
inProgress = false;
HackFinished?.Invoke(this, new BruteForceFinishedEventArgs(BruteForceStatus.OutOfTime));
return;
}
// Otherwise, update the visible time
else
{
CallFunction("SET_COUNTDOWN", span.Minutes, span.Seconds, span.Milliseconds);
}
}
// If the user pressed left, go to the left
if (Controls.IsJustPressed(Control.MoveLeftOnly) || Controls.IsJustPressed(Control.FrontendLeft))
{
soundRowSwitch.PlayFrontend();
CallFunction("SET_INPUT_EVENT", 10);
}
// If the user pressed right, go to the right
else if (Controls.IsJustPressed(Control.MoveRightOnly) || Controls.IsJustPressed(Control.FrontendRight))
{
soundRowSwitch.PlayFrontend();
CallFunction("SET_INPUT_EVENT", 11);
}
// If the user pressed accept, send the selection event
else if (Controls.IsJustPressed(Control.FrontendAccept))
{
output = CallFunctionReturn("SET_INPUT_EVENT_SELECT");
}
// If there is some output to receive
if (output != 0)
{
// If the value is ready, go ahead and check it
if (IsValueReady(output))
{
switch (GetValue<int>(output))
{
case 86: // Hack Completed
string ok = SuccessMessages.Count == 0 ? string.Empty : SuccessMessages[random.Next(SuccessMessages.Count)];
CallFunction("SET_ROULETTE_OUTCOME", true, ok);
soundSuccess.PlayFrontend();
HackFinished?.Invoke(this, new BruteForceFinishedEventArgs(BruteForceStatus.Completed));
hideTime = closeAfter == -1 ? -1 : (int)time + CloseAfter;
inProgress = false;
break;
case 87: // Row Failed (or lives failed)
livesCurrent--;
CallFunction("SET_LIVES", livesCurrent, livesTotal);
soundRowFailed.PlayFrontend();
if (livesCurrent <= 0)
{
string err = FailMessages.Count == 0 ? string.Empty : FailMessages[random.Next(FailMessages.Count)];
CallFunction("SET_ROULETTE_OUTCOME", false, err);
hideTime = closeAfter == -1 ? -1 : (int)time + CloseAfter;
inProgress = false;
HackFinished?.Invoke(this, new BruteForceFinishedEventArgs(BruteForceStatus.OutOfLives));
}
break;
case 92: // Row Completed
soundRowCompleted.PlayFrontend();
break;
}
output = 0;
}
}
}
}
#endregion
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace LemonUI.Scaleform
{
/// <summary>
/// Scaleforms are 2D Adobe Flash-like objects.
/// </summary>
public interface IScaleform : IDrawable, IProcessable, IDisposable
{
/// <summary>
/// Draws the Scaleform in full screen.
/// </summary>
void DrawFullScreen();
}
}

View File

@ -0,0 +1,196 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage.Native;
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.Native;
#endif
using System;
using System.Collections.Generic;
namespace LemonUI.Scaleform
{
/// <summary>
/// An individual instructional button.
/// </summary>
public struct InstructionalButton
{
#region Private Fields
private Control control;
private string raw;
#endregion
#region Public Properties
/// <summary>
/// The description of this button.
/// </summary>
public string Description { get; set; }
/// <summary>
/// The Control used by this button.
/// </summary>
public Control Control
{
get => control;
set
{
control = value;
#if FIVEM
raw = API.GetControlInstructionalButton(2, (int)value, 1);
#elif RAGEMP
raw = Invoker.Invoke<string>(Natives.GetControlInstructionalButton, 2, (int)value, 1);
#elif RPH
raw = (string)NativeFunction.CallByHash(0x0499D7B09FC9B407, typeof(string), 2, (int)control, 1);
#elif SHVDN3
raw = Function.Call<string>(Hash.GET_CONTROL_INSTRUCTIONAL_BUTTON, 2, (int)value, 1);
#endif
}
}
/// <summary>
/// The Raw Control sent to the Scaleform.
/// </summary>
public string Raw
{
get => raw;
set
{
raw = value;
control = (Control)(-1);
}
}
#endregion
#region Constructor
/// <summary>
/// Creates an instructional button for a Control.
/// </summary>
/// <param name="description">The text for the description.</param>
/// <param name="control">The control to use.</param>
public InstructionalButton(string description, Control control)
{
Description = description;
this.control = control;
#if FIVEM
raw = API.GetControlInstructionalButton(2, (int)control, 1);
#elif RAGEMP
raw = Invoker.Invoke<string>(Natives.GetControlInstructionalButton, 2, (int)control, 1);
#elif RPH
raw = (string)NativeFunction.CallByHash(0x0499D7B09FC9B407, typeof(string), 2, (int)control, 1);
#elif SHVDN3
raw = Function.Call<string>(Hash.GET_CONTROL_INSTRUCTIONAL_BUTTON, 2, (int)control, 1);
#endif
}
/// <summary>
/// Creates an instructional button for a raw control.
/// </summary>
/// <param name="description">The text for the description.</param>
/// <param name="raw">The raw value of the control.</param>
public InstructionalButton(string description, string raw)
{
Description = description;
control = (Control)(-1);
this.raw = raw;
}
#endregion
}
/// <summary>
/// Buttons shown on the bottom right of the screen.
/// </summary>
public class InstructionalButtons : BaseScaleform
{
#region Public Fields
/// <summary>
/// The buttons used in this Scaleform.
/// </summary>
private readonly List<InstructionalButton> buttons = new List<InstructionalButton>();
#endregion
#region Constructors
/// <summary>
/// Creates a new set of Instructional Buttons.
/// </summary>
/// <param name="buttons">The buttons to add into this menu.</param>
public InstructionalButtons(params InstructionalButton[] buttons) : base("INSTRUCTIONAL_BUTTONS")
{
this.buttons.AddRange(buttons);
}
#endregion
#region Public Functions
/// <summary>
/// Adds an Instructional Button.
/// </summary>
/// <param name="button">The button to add.</param>
public void Add(InstructionalButton button)
{
// If the item is already in the list, raise an exception
if (buttons.Contains(button))
{
throw new InvalidOperationException("The button is already in the Scaleform.");
}
// Otherwise, add it to the list of items
buttons.Add(button);
}
/// <summary>
/// Removes an Instructional Button.
/// </summary>
/// <param name="button">The button to remove.</param>
public void Remove(InstructionalButton button)
{
// If the button is not in the list, return
if (!buttons.Contains(button))
{
return;
}
// Otherwise, remove it
buttons.Remove(button);
}
/// <summary>
/// Removes all of the instructional buttons.
/// </summary>
public void Clear()
{
buttons.Clear();
}
/// <summary>
/// Refreshes the items shown in the Instructional buttons.
/// </summary>
public override void Update()
{
// Clear all of the existing items
CallFunction("CLEAR_ALL");
CallFunction("TOGGLE_MOUSE_BUTTONS", 0);
CallFunction("CREATE_CONTAINER");
// And add them again
for (int i = 0; i < buttons.Count; i++)
{
InstructionalButton button = buttons[i];
CallFunction("SET_DATA_SLOT", i, button.Raw, button.Description);
}
CallFunction("DRAW_INSTRUCTIONAL_BUTTONS", -1);
}
#endregion
}
}

View File

@ -0,0 +1,92 @@
namespace LemonUI.Scaleform
{
/// <summary>
/// Loading screen like the transition between story mode and online.
/// </summary>
public class LoadingScreen : BaseScaleform
{
#region Public Properties
/// <summary>
/// The title of the loading screen.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The subtitle of the loading screen.
/// </summary>
public string Subtitle { get; set; }
/// <summary>
/// The description of the loading screen.
/// </summary>
public string Description { get; set; }
/// <summary>
/// The Texture Dictionary (TXD) where the texture is loaded.
/// </summary>
public string Dictionary { get; private set; }
/// <summary>
/// The texture in the dictionary.
/// </summary>
public string Texture { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Creates a new GTA Online like loading screen with no image.
/// </summary>
/// <param name="title">The title of the screen.</param>
/// <param name="subtitle">The subtitle of the screen.</param>
/// <param name="description">The description of the screen.</param>
public LoadingScreen(string title, string subtitle, string description) : this(title, subtitle, description, string.Empty, string.Empty)
{
}
/// <summary>
/// Creates a new GTA Online like loading screen with a custom texture.
/// </summary>
/// <param name="title">The title of the screen.</param>
/// <param name="subtitle">The subtitle of the screen.</param>
/// <param name="description">The description of the screen.</param>
/// <param name="dictionary">The dictionary where the texture is located.</param>
/// <param name="texture">The texture to use on the right.</param>
public LoadingScreen(string title, string subtitle, string description, string dictionary, string texture) : base("GTAV_ONLINE")
{
// Tell the Scaleform to use the online loading screen
CallFunction("HIDE_ONLINE_LOGO");
CallFunction("SETUP_BIGFEED", false);
// Save the values
Title = title;
Subtitle = subtitle;
Description = description;
Dictionary = dictionary;
Texture = texture;
// And send them back to the scaleform
Update();
}
#endregion
#region Public Functions
/// <summary>
/// Changes the texture shown on the loading screen.
/// </summary>
/// <param name="dictionary">The Texture Dictionary or TXD.</param>
/// <param name="texture">The Texture name.</param>
public void ChangeTexture(string dictionary, string texture)
{
Dictionary = dictionary;
Texture = texture;
Update();
}
/// <summary>
/// Updates the Title, Description and Image of the loading screen.
/// </summary>
public override void Update()
{
CallFunction("SET_BIGFEED_INFO", "footerStr", Description, 0, Dictionary, Texture, Subtitle, "urlDeprecated", Title);
}
#endregion
}
}

View File

@ -0,0 +1,57 @@
namespace LemonUI.Scaleform
{
/// <summary>
/// A warning pop-up.
/// </summary>
public class PopUp : BaseScaleform
{
#region Properties
/// <summary>
/// The title of the Pop-up.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The subtitle of the Pop-up.
/// </summary>
public string Subtitle { get; set; }
/// <summary>
/// The prompt of the Pop-up.
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// If the black background should be shown.
/// </summary>
public bool ShowBackground { get; set; } = true;
/// <summary>
/// The error message to show.
/// </summary>
public string Error { get; set; }
#endregion
#region Constructors
/// <summary>
/// Creates a new Pop-up instance.
/// </summary>
public PopUp() : base("POPUP_WARNING")
{
}
#endregion
#region Functions
/// <summary>
/// Updates the texts of the Pop-up.
/// </summary>
public override void Update()
{
// first parameter "msecs" is unused
CallFunction("SHOW_POPUP_WARNING", 0, Title, Subtitle, Prompt, ShowBackground, 0, Error);
}
#endregion
}
}

View File

@ -0,0 +1,284 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
using CitizenFX.Core.UI;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
using Rage.Native;
using Control = Rage.GameControl;
#elif SHVDN3
using GTA;
using GTA.Native;
using GTA.UI;
#endif
using LemonUI.Extensions;
using System;
using System.Drawing;
namespace LemonUI
{
/// <summary>
/// Represents the internal alignment of screen elements.
/// </summary>
public enum GFXAlignment
{
/// <summary>
/// Vertical Alignment to the Bottom.
/// </summary>
Bottom = 66,
/// <summary>
/// Vertical Alignment to the Top.
/// </summary>
Top = 84,
/// <summary>
/// Centered Vertically or Horizontally.
/// </summary>
Center = 67,
/// <summary>
/// Horizontal Alignment to the Left.
/// </summary>
Left = 76,
/// <summary>
/// Horizontal Alignment to the Right.
/// </summary>
Right = 82,
}
/// <summary>
/// Contains a set of tools to work with the screen information.
/// </summary>
public static class Screen
{
/// <summary>
/// The Aspect Ratio of the screen resolution.
/// </summary>
public static float AspectRatio
{
get
{
#if FIVEM
return API.GetAspectRatio(false);
#elif RAGEMP
return Invoker.Invoke<float>(Natives.GetAspectRatio);
#elif RPH
return NativeFunction.CallByHash<float>(0xF1307EF624A80D87, false);
#elif SHVDN3
return Function.Call<float>(Hash._GET_ASPECT_RATIO, false);
#endif
}
}
/// <summary>
/// The location of the cursor on screen between 0 and 1.
/// </summary>
public static PointF CursorPositionRelative
{
get
{
#if FIVEM
float cursorX = API.GetControlNormal(0, (int)Control.CursorX);
float cursorY = API.GetControlNormal(0, (int)Control.CursorY);
#elif RAGEMP
float cursorX = Invoker.Invoke<float>(Natives.GetControlNormal, 0, (int)Control.CursorX);
float cursorY = Invoker.Invoke<float>(Natives.GetControlNormal, 0, (int)Control.CursorY);
#elif RPH
float cursorX = NativeFunction.CallByHash<float>(0xEC3C9B8D5327B563, 0, (int)Control.CursorX);
float cursorY = NativeFunction.CallByHash<float>(0xEC3C9B8D5327B563, 0, (int)Control.CursorY);
#elif SHVDN3
float cursorX = Function.Call<float>(Hash.GET_CONTROL_NORMAL, 0, (int)Control.CursorX);
float cursorY = Function.Call<float>(Hash.GET_CONTROL_NORMAL, 0, (int)Control.CursorY);
#endif
return new PointF(cursorX, cursorY);
}
}
/// <summary>
/// Converts a relative resolution into one scaled to 1080p.
/// </summary>
/// <param name="relativeX">The relative value of X.</param>
/// <param name="relativeY">The relative value of Y.</param>
/// <param name="absoluteX">The value of X scaled to 1080p.</param>
/// <param name="absoluteY">The value of Y scaled to 1080p.</param>
public static void ToAbsolute(float relativeX, float relativeY, out float absoluteX, out float absoluteY)
{
// Get the real width based on the aspect ratio
float width = 1080f * AspectRatio;
// And save the correct values
absoluteX = width * relativeX;
absoluteY = 1080f * relativeY;
}
/// <summary>
/// Converts a 1080p-based resolution into relative values.
/// </summary>
/// <param name="absoluteX">The 1080p based X coord.</param>
/// <param name="absoluteY">The 1080p based Y coord.</param>
/// <param name="relativeX">The value of X converted to relative.</param>
/// <param name="relativeY">The value of Y converted to relative.</param>
public static void ToRelative(float absoluteX, float absoluteY, out float relativeX, out float relativeY)
{
// Get the real width based on the aspect ratio
float width = 1080f * AspectRatio;
// And save the correct values
relativeX = absoluteX / width;
relativeY = absoluteY / 1080f;
}
/// <summary>
/// Checks if the cursor is inside of the specified area.
/// </summary>
/// <remarks>
/// This function takes values scaled to 1080p and is aware of the alignment set via SET_SCRIPT_GFX_ALIGN.
/// </remarks>
/// <param name="pos">The start of the area.</param>
/// <param name="size">The size of the area to check.</param>
/// <returns><see langword="true"/> if the cursor is in the specified bounds, <see langword="false"/> otherwise.</returns>
public static bool IsCursorInArea(PointF pos, SizeF size) => IsCursorInArea(pos.X, pos.Y, size.Width, size.Height);
/// <summary>
/// Checks if the cursor is inside of the specified area.
/// </summary>
/// <remarks>
/// This function takes values scaled to 1080p and is aware of the alignment set via SET_SCRIPT_GFX_ALIGN.
/// </remarks>
/// <param name="x">The start X position.</param>
/// <param name="y">The start Y position.</param>
/// <param name="width">The height of the search area from X.</param>
/// <param name="height">The height of the search area from Y.</param>
/// <returns><see langword="true"/> if the cursor is in the specified bounds, <see langword="false"/> otherwise.</returns>
public static bool IsCursorInArea(float x, float y, float width, float height)
{
PointF cursor = CursorPositionRelative;
ToRelative(width, height, out float realWidth, out float realHeight);
PointF realPos = GetRealPosition(x, y).ToRelative();
bool isX = cursor.X >= realPos.X && cursor.X <= realPos.X + realWidth;
bool isY = cursor.Y > realPos.Y && cursor.Y < realPos.Y + realHeight;
return isX && isY;
}
/// <summary>
/// Converts the specified position into one that is aware of <see cref="SetElementAlignment(GFXAlignment, GFXAlignment)"/>.
/// </summary>
/// <param name="og">The original 1080p based position.</param>
/// <returns>A new 1080p based position that is aware of the the Alignment.</returns>
public static PointF GetRealPosition(PointF og) => GetRealPosition(og.X, og.Y);
/// <summary>
/// Converts the specified position into one that is aware of <see cref="SetElementAlignment(GFXAlignment, GFXAlignment)"/>.
/// </summary>
/// <param name="x">The 1080p based X position.</param>
/// <param name="y">The 1080p based Y position.</param>
/// <returns>A new 1080p based position that is aware of the the Alignment.</returns>
public static PointF GetRealPosition(float x, float y)
{
// Convert the resolution to relative
ToRelative(x, y, out float relativeX, out float relativeY);
// Request the real location of the position
float realX = 0, realY = 0;
#if FIVEM
API.GetScriptGfxPosition(relativeX, relativeY, ref realX, ref realY);
#elif RAGEMP
FloatReference argX = new FloatReference();
FloatReference argY = new FloatReference();
Invoker.Invoke<int>(0x6DD8F5AA635EB4B2, relativeX, relativeY, argX, argY);
realX = argX.Value;
realY = argY.Value;
#elif RPH
using (NativePointer argX = new NativePointer())
using (NativePointer argY = new NativePointer())
{
NativeFunction.CallByHash<int>(0x6DD8F5AA635EB4B2, relativeX, relativeY, argX, argY);
realX = argX.GetValue<float>();
realY = argY.GetValue<float>();
}
#elif SHVDN3
OutputArgument argX = new OutputArgument();
OutputArgument argY = new OutputArgument();
Function.Call((Hash)0x6DD8F5AA635EB4B2, relativeX, relativeY, argX, argY); // _GET_SCRIPT_GFX_POSITION
realX = argX.GetResult<float>();
realY = argY.GetResult<float>();
#endif
// And return it converted to absolute
ToAbsolute(realX, realY, out float absoluteX, out float absoluteY);
return new PointF(absoluteX, absoluteY);
}
/// <summary>
/// Shows the cursor during the current game frame.
/// </summary>
public static void ShowCursorThisFrame()
{
#if FIVEM
API.SetMouseCursorActiveThisFrame();
#elif RAGEMP
Invoker.Invoke(0xAAE7CE1D63167423);
#elif RPH
NativeFunction.CallByHash<int>(0xAAE7CE1D63167423);
#elif SHVDN3
Function.Call(Hash._SET_MOUSE_CURSOR_ACTIVE_THIS_FRAME);
#endif
}
/// <summary>
/// Sets the alignment of game elements like <see cref="Elements.ScaledRectangle"/>, <see cref="Elements.ScaledText"/> and <see cref="Elements.ScaledTexture"/>.
/// </summary>
/// <param name="horizontal">The Horizontal alignment of the items.</param>
/// <param name="vertical">The vertical alignment of the items.</param>
public static void SetElementAlignment(Alignment horizontal, GFXAlignment vertical)
{
// If the enum value is not correct, raise an exception
if (!Enum.IsDefined(typeof(Alignment), horizontal))
{
throw new ArgumentException("Alignment is not one of the allowed values (Left, Right, Center).", nameof(horizontal));
}
// Otherwise, just call the correct function
switch (horizontal)
{
case Alignment.Left:
SetElementAlignment(GFXAlignment.Left, vertical);
break;
case Alignment.Right:
SetElementAlignment(GFXAlignment.Right, vertical);
break;
case Alignment.Center:
SetElementAlignment(GFXAlignment.Right, vertical);
break;
}
}
/// <summary>
/// Sets the alignment of game elements like <see cref="Elements.ScaledRectangle"/>, <see cref="Elements.ScaledText"/> and <see cref="Elements.ScaledTexture"/>.
/// </summary>
/// <param name="horizontal">The Horizontal alignment of the items.</param>
/// <param name="vertical">The vertical alignment of the items.</param>
public static void SetElementAlignment(GFXAlignment horizontal, GFXAlignment vertical)
{
#if FIVEM
API.SetScriptGfxAlign((int)horizontal, (int)vertical);
API.SetScriptGfxAlignParams(0, 0, 0, 0);
#elif RAGEMP
Invoker.Invoke(0xB8A850F20A067EB6, (int)horizontal, (int)vertical);
Invoker.Invoke(0xF5A2C681787E579D, 0, 0, 0, 0);
#elif RPH
NativeFunction.CallByHash<int>(0xB8A850F20A067EB6, (int)horizontal, (int)vertical);
NativeFunction.CallByHash<int>(0xF5A2C681787E579D, 0, 0, 0, 0);
#elif SHVDN3
Function.Call(Hash.SET_SCRIPT_GFX_ALIGN, (int)horizontal, (int)vertical);
Function.Call(Hash.SET_SCRIPT_GFX_ALIGN_PARAMS, 0, 0, 0, 0);
#endif
}
/// <summary>
/// Resets the alignment of the game elements.
/// </summary>
public static void ResetElementAlignment()
{
#if FIVEM
API.ResetScriptGfxAlign();
#elif RAGEMP
Invoker.Invoke(0xE3A3DB414A373DAB);
#elif RPH
NativeFunction.CallByHash<int>(0xE3A3DB414A373DAB);
#elif SHVDN3
Function.Call(Hash.RESET_SCRIPT_GFX_ALIGN);
#endif
}
}
}

View File

@ -0,0 +1,65 @@
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
using Rage.Native;
#elif SHVDN3
using GTA;
using GTA.Native;
#endif
namespace LemonUI
{
/// <summary>
/// Contains information for a Game Sound that is played at specific times.
/// </summary>
public class Sound
{
/// <summary>
/// The Set where the sound is located.
/// </summary>
public string Set { get; set; }
/// <summary>
/// The name of the sound file.
/// </summary>
public string File { get; set; }
/// <summary>
/// Creates a new <see cref="Sound"/> class with the specified Sound Set and File.
/// </summary>
/// <param name="set">The Set where the sound is located.</param>
/// <param name="file">The name of the sound file.</param>
public Sound(string set, string file)
{
Set = set;
File = file;
}
/// <summary>
/// Plays the sound for the local <see cref="Player"/>.
/// </summary>
public void PlayFrontend()
{
#if FIVEM
API.PlaySoundFrontend(-1, File, Set, false);
int id = API.GetSoundId();
API.ReleaseSoundId(id);
#elif RAGEMP
Invoker.Invoke(Natives.PlaySoundFrontend, -1, File, Set, false);
int id = Invoker.Invoke<int>(Natives.GetSoundId);
Invoker.Invoke(Natives.ReleaseSoundId, id);
#elif RPH
NativeFunction.CallByHash<int>(0x67C540AA08E4A6F5, -1, File, Set, false);
int id = NativeFunction.CallByHash<int>(0x430386FE9BF80B45);
NativeFunction.CallByHash<int>(0x353FC880830B88FA, id);
#elif SHVDN3
Function.Call(Hash.PLAY_SOUND_FRONTEND, -1, File, Set, false);
int id = Function.Call<int>(Hash.GET_SOUND_ID);
Function.Call(Hash.RELEASE_SOUND_ID, id);
#endif
}
}
}

View File

@ -0,0 +1,23 @@
namespace LemonUI.TimerBars
{
/// <summary>
/// The spacing of the objectives in the timer bar.
/// </summary>
public enum ObjectiveSpacing
{
/// <summary>
/// The objectives will be equally spaced.
/// </summary>
/// <remarks>
/// If you have way too many objectives and not enough width, you might end up with objectives overlapping each other.
/// </remarks>
Equal = 0,
/// <summary>
/// The items will all have the same spacing.
/// </summary>
/// <remarks>
/// If you have way too many objectives and not enough width, the objectives might end up outside of the timer bar.
/// </remarks>
Fixed = 1
}
}

View File

@ -0,0 +1,151 @@
#if FIVEM
using CitizenFX.Core.UI;
#elif RAGEMP
using RAGE.Game;
#elif SHVDN3
using GTA.UI;
#endif
using LemonUI.Elements;
using System.Drawing;
namespace LemonUI.TimerBars
{
/// <summary>
/// Represents a Bar with text information shown in the bottom right.
/// </summary>
public class TimerBar : IDrawable
{
#region Constant Fields
/// <summary>
/// The separation between the different timer bars.
/// </summary>
internal const float separation = 6.25f;
/// <summary>
/// The width of the background.
/// </summary>
internal const float backgroundWidth = 220;
/// <summary>
/// The height of the background.
/// </summary>
internal const float backgroundHeight = 37;
#endregion
#region Private Fields
private string rawTitle = string.Empty;
private string rawInfo = string.Empty;
#endregion
#region Internal Fields
/// <summary>
/// The background of the timer bar.
/// </summary>
internal protected readonly ScaledTexture background = new ScaledTexture("timerbars", "all_black_bg")
{
Color = Color.FromArgb(160, 255, 255, 255)
};
/// <summary>
/// The title of the timer bar.
/// </summary>
internal protected readonly ScaledText title = new ScaledText(PointF.Empty, string.Empty, 0.29f)
{
Alignment = Alignment.Right,
WordWrap = 1000
};
/// <summary>
/// The information of the Timer Bar.
/// </summary>
internal protected readonly ScaledText info = new ScaledText(PointF.Empty, string.Empty, 0.5f)
{
Alignment = Alignment.Right,
WordWrap = 1000
};
#endregion
#region Public Properties
/// <summary>
/// The title of the bar, shown on the left.
/// </summary>
public string Title
{
get => rawTitle;
set
{
rawTitle = value;
title.Text = value.ToUpperInvariant();
}
}
/// <summary>
/// The information shown on the right.
/// </summary>
public string Info
{
get => rawInfo;
set
{
rawInfo = value;
info.Text = value.ToUpperInvariant();
}
}
/// <summary>
/// The Width of the information text.
/// </summary>
public float InfoWidth => info.Width;
/// <summary>
/// The color of the information text.
/// </summary>
public Color Color
{
get => info.Color;
set => info.Color = value;
}
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="TimerBar"/> with the specified Title and Value.
/// </summary>
/// <param name="title">The title of the bar.</param>
/// <param name="info">The information shown on the bar.</param>
public TimerBar(string title, string info)
{
Title = title;
Info = info;
}
#endregion
#region Public Functions
/// <summary>
/// Recalculates the position of the timer bar elements based on the location of it on the screen.
/// </summary>
/// <param name="pos">The Top Left position of the Timer Bar.</param>
public virtual void Recalculate(PointF pos)
{
background.Position = pos;
background.Size = new SizeF(backgroundWidth, backgroundHeight);
title.Position = new PointF(pos.X + 91, pos.Y + 8);
info.Position = new PointF(pos.X + 218, pos.Y - 3);
}
/// <summary>
/// Draws the timer bar information.
/// </summary>
public virtual void Draw()
{
background.Draw();
title.Draw();
info.Draw();
}
#endregion
}
}

View File

@ -0,0 +1,174 @@
#if FIVEM
using CitizenFX.Core.UI;
#elif RAGEMP
using RAGE.Game;
#elif RPH
using Rage;
using Rage.Native;
#elif SHVDN3
using GTA.UI;
#endif
using System;
using System.Collections.Generic;
using System.Drawing;
namespace LemonUI.TimerBars
{
/// <summary>
/// A collection or Set of <see cref="TimerBar"/>.
/// </summary>
public class TimerBarCollection : IContainer<TimerBar>
{
#region Public Properties
/// <summary>
/// If this collection of Timer Bars is visible to the user.
/// </summary>
public bool Visible { get; set; } = true;
/// <summary>
/// The <see cref="TimerBar"/>s that are part of this collection.
/// </summary>
public List<TimerBar> TimerBars { get; } = new List<TimerBar>();
#endregion
#region Constructors
/// <summary>
/// Creates a new collection of Timer Bars.
/// </summary>
/// <param name="bars"></param>
public TimerBarCollection(params TimerBar[] bars)
{
TimerBars.AddRange(bars);
Recalculate();
}
#endregion
#region Public Functions
/// <summary>
/// Adds a <see cref="TimerBar"/> onto this collection.
/// </summary>
/// <param name="bar">The <see cref="TimerBar"/> to add.</param>
public void Add(TimerBar bar)
{
// If the item is already on the list, raise an exception
if (TimerBars.Contains(bar))
{
throw new InvalidOperationException("The item is already part of the menu.");
}
// Also raise an exception if is null
if (bar == null)
{
throw new ArgumentNullException(nameof(bar));
}
// If we got here, add it
TimerBars.Add(bar);
// And recalculate the positions of the existing items
Recalculate();
}
/// <summary>
/// Removes a <see cref="TimerBar"/> from the Collection.
/// </summary>
/// <param name="bar">The <see cref="TimerBar"/> to remove.</param>
public void Remove(TimerBar bar)
{
// If the bar is not present, return
if (!TimerBars.Contains(bar))
{
return;
}
// Otherwise, remove it
TimerBars.Remove(bar);
// And recalculate the positions
Recalculate();
}
/// <summary>
/// Removes all of the <see cref="TimerBar"/> that match the function.
/// </summary>
/// <param name="func">The function to check the <see cref="TimerBar"/>.</param>
public void Remove(Func<TimerBar, bool> func)
{
// Iterate over the timer bars
foreach (TimerBar bar in new List<TimerBar>(TimerBars))
{
// If it matches the function, remove it
if (func(bar))
{
TimerBars.Remove(bar);
}
}
// Finally, recalculate the positions
Recalculate();
}
/// <summary>
/// Removes all of the <see cref="TimerBar"/> in this collection.
/// </summary>
public void Clear() => TimerBars.Clear();
/// <summary>
/// Checks if the <see cref="TimerBar"/> is part of this collection.
/// </summary>
/// <param name="bar">The <see cref="TimerBar"/> to check.</param>
public bool Contains(TimerBar bar) => TimerBars.Contains(bar);
/// <summary>
/// Recalculates the positions and sizes of the <see cref="TimerBar"/>.
/// </summary>
public void Recalculate()
{
// Get the position of 0,0 while staying safe zone aware
Screen.SetElementAlignment(GFXAlignment.Right, GFXAlignment.Bottom);
PointF pos = Screen.GetRealPosition(PointF.Empty);
Screen.ResetElementAlignment();
// Iterate over the existing timer bars and save the count
int count = 0;
foreach (TimerBar timerBar in TimerBars)
{
// And send them to the timer bar
timerBar.Recalculate(new PointF(pos.X - TimerBar.backgroundWidth, pos.Y - (TimerBar.backgroundHeight * (TimerBars.Count - count)) - (TimerBar.separation * (TimerBars.Count - count - 1))));
// Finish by increasing the total count of items
count++;
}
}
/// <summary>
/// Draws the known timer bars.
/// </summary>
public void Process()
{
// If there are no timer bars or the collection is disabled, return
if (TimerBars.Count == 0 || !Visible)
{
return;
}
// Hide the texts in the bottom right corner of the screen
#if FIVEM
CitizenFX.Core.UI.Screen.Hud.HideComponentThisFrame(HudComponent.AreaName);
CitizenFX.Core.UI.Screen.Hud.HideComponentThisFrame(HudComponent.StreetName);
CitizenFX.Core.UI.Screen.Hud.HideComponentThisFrame(HudComponent.VehicleName);
#elif RAGEMP
Invoker.Invoke(Natives.HideHudComponentThisFrame, HudComponent.AreaName);
Invoker.Invoke(Natives.HideHudComponentThisFrame, HudComponent.StreetName);
Invoker.Invoke(Natives.HideHudComponentThisFrame, HudComponent.VehicleName);
#elif RPH
NativeFunction.CallByHash<int>(0x6806C51AD12B83B8, 7);
NativeFunction.CallByHash<int>(0x6806C51AD12B83B8, 9);
NativeFunction.CallByHash<int>(0x6806C51AD12B83B8, 6);
#elif SHVDN3
Hud.HideComponentThisFrame(HudComponent.AreaName);
Hud.HideComponentThisFrame(HudComponent.StreetName);
Hud.HideComponentThisFrame(HudComponent.VehicleName);
#endif
// Draw the existing timer bars
foreach (TimerBar timerBar in TimerBars)
{
timerBar.Draw();
}
}
#endregion
}
}

View File

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using LemonUI.Elements;
namespace LemonUI.TimerBars
{
/// <summary>
/// A timer bar for a specific amount of objectives.
/// </summary>
public class TimerBarObjective : TimerBar
{
#region Fields
private const float width = 20;
private const float height = 20;
private static readonly Color colorWhite = Color.FromArgb(255, 255, 255);
private static readonly Color colorCompleted = Color.FromArgb(101, 180, 212);
private readonly List<ScaledTexture> objectives = new List<ScaledTexture>();
private PointF lastPosition = default;
private int count = 1;
private int completed = 0;
private Color colorSet = colorCompleted;
private ObjectiveSpacing objectiveSpacing = ObjectiveSpacing.Equal;
#endregion
#region Properties
/// <summary>
/// The number of objectives shown in the timer bar.
/// </summary>
public int Count
{
get => count;
set
{
if (count == value)
{
return;
}
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "The number of objectives can't be under or equal to zero.");
}
count = value;
UpdateObjectiveCount();
}
}
/// <summary>
/// The number of completed objectives.
/// </summary>
public int Completed
{
get => completed;
set
{
if (completed == value)
{
return;
}
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "The number of completed objectives can't be under zero.");
}
if (value > Count)
{
throw new ArgumentOutOfRangeException(nameof(value), "The number of completed objectives can't be over the total number of objectives.");
}
completed = value;
UpdateObjectiveColors();
}
}
/// <summary>
/// The color used for completed objectives.
/// </summary>
public Color CompletedColor
{
get => colorSet;
set
{
if (colorSet == value)
{
return;
}
colorSet = value;
UpdateObjectiveColors();
}
}
public ObjectiveSpacing Spacing
{
get => objectiveSpacing;
set
{
if (objectiveSpacing == value)
{
return;
}
objectiveSpacing = value;
Recalculate(lastPosition);
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a new timer bar used to show objectives.
/// </summary>
public TimerBarObjective(string title) : base(title, string.Empty)
{
UpdateObjectiveCount();
}
#endregion
#region Tools
private void UpdateObjectiveCount()
{
// just to make sure
if (completed > count)
{
completed = count;
}
objectives.Clear();
for (int i = 0; i < count; i++)
{
objectives.Add(new ScaledTexture("timerbars", "circle_checkpoints"));
}
UpdateObjectiveColors();
}
private void UpdateObjectiveColors()
{
for (int i = 0; i < objectives.Count; i++)
{
ScaledTexture texture = objectives[i];
texture.Color = i < completed ? colorSet : colorWhite;
}
}
#endregion
#region Functions
/// <summary>
/// Draws the objective timer bar.
/// </summary>
public override void Draw()
{
background.Draw();
title.Draw();
foreach (ScaledTexture texture in objectives)
{
texture.Draw();
}
}
/// <inheritdoc/>
public override void Recalculate(PointF pos)
{
lastPosition = pos;
base.Recalculate(pos);
const float safe = width + 5;
float startY = pos.Y + (backgroundHeight * 0.5f) - (height * 0.5f);
switch (objectiveSpacing)
{
case ObjectiveSpacing.Equal:
{
const float half = backgroundWidth * 0.5f;
float startX = pos.X + half;
float spacingWidth = (half - safe) / (objectives.Count - 1);
for (int i = 0; i < objectives.Count; i++)
{
ScaledTexture texture = objectives[i];
texture.Size = new SizeF(width, height);
texture.Position = new PointF(startX + (spacingWidth * i), startY);
}
break;
}
case ObjectiveSpacing.Fixed:
{
float startX = pos.X + backgroundWidth - safe - (width * (count - 1));
for (int i = 0; i < objectives.Count; i++)
{
ScaledTexture texture = objectives[i];
texture.Size = new SizeF(width, height);
texture.Position = new PointF(startX + (i * width), startY);
}
break;
}
}
}
#endregion
}
}

View File

@ -0,0 +1,123 @@
using LemonUI.Elements;
using System;
using System.Drawing;
namespace LemonUI.TimerBars
{
/// <summary>
/// Represents a Timer Bar that shows the progress of something.
/// </summary>
public class TimerBarProgress : TimerBar
{
#region Constant Fields
private const float barWidth = 108;
private const float barHeight = 15;
#endregion
#region Private Fields
private float progress = 100;
#endregion
#region Internal Fields
/// <summary>
/// The background of the Progress Bar.
/// </summary>
internal protected readonly ScaledRectangle barBackground = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 139, 0, 0)
};
/// <summary>
/// The foreground of the Progress Bar.
/// </summary>
internal protected readonly ScaledRectangle barForeground = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 255, 0, 0)
};
#endregion
#region Public Properties
/// <summary>
/// The progress of the bar.
/// </summary>
public float Progress
{
get => progress;
set
{
if (value < 0 || value > 100)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
progress = value;
barForeground.Size = new SizeF(barWidth * (value * 0.01f), barHeight);
}
}
/// <summary>
/// The Foreground color of the Progress bar.
/// </summary>
public Color ForegroundColor
{
get => barForeground.Color;
set => barForeground.Color = value;
}
/// <summary>
/// The Background color of the Progress bar.
/// </summary>
public Color BackgroundColor
{
get => barBackground.Color;
set => barBackground.Color = value;
}
#endregion
#region Constructors
/// <summary>
/// Creates a new <see cref="TimerBarProgress"/> with the specified title.
/// </summary>
/// <param name="title">The title of the bar.</param>
public TimerBarProgress(string title) : base(title, string.Empty)
{
}
#endregion
#region Public Functions
/// <summary>
/// Recalculates the position of the timer bar elements based on the location of it on the screen.
/// </summary>
/// <param name="pos">The Top Left position of the Timer Bar.</param>
public override void Recalculate(PointF pos)
{
// Recalculate the base elements
base.Recalculate(pos);
// And set the size and position of the progress bar
PointF barPos = new PointF(pos.X + 103, pos.Y + 12);
barBackground.Position = barPos;
barBackground.Size = new SizeF(barWidth, barHeight);
barForeground.Position = barPos;
barForeground.Size = new SizeF(barWidth * (progress * 0.01f), barHeight);
}
/// <summary>
/// Draws the TimerBar.
/// </summary>
public override void Draw()
{
background.Draw();
title.Draw();
barBackground.Draw();
barForeground.Draw();
}
#endregion
}
}

View File

@ -21,35 +21,42 @@ namespace RageCoop.Client
{
private bool _gameLoaded = false;
private static bool _isGoingToCar = false;
internal static readonly string CurrentVersion = "V0_5_0";
public static readonly string CurrentVersion = "V0_2";
internal static int LocalPlayerID=0;
public static int LocalPlayerID=0;
public static bool NPCsAllowed = false;
internal static RelationshipGroup SyncedPedsGroup;
public static new Settings Settings = null;
internal static new Settings Settings = null;
internal static Scripting.BaseScript BaseScript=new Scripting.BaseScript();
#if !NON_INTERACTIVE
public static RageCoopMenu MainMenu = null;
#endif
public static Chat MainChat = null;
public static Stopwatch Counter = new Stopwatch();
public static ulong Ticked = 0;
public static Loggger Logger=new Loggger("Scripts\\RageCoop\\RageCoop.Client.log");
internal static Chat MainChat = null;
internal static Stopwatch Counter = new Stopwatch();
internal static Logger Logger = null;
internal static ulong Ticked = 0;
internal static Scripting.Resources Resources=null;
private static List<Func<bool>> QueuedActions = new List<Func<bool>>();
/// <summary>
/// Don't use it!
/// </summary>
public Main()
{
// Required for some synchronization!
/*if (Game.Version < GameVersion.v1_0_1290_1_Steam)
Settings = Util.ReadSettings();
Logger=new Logger()
{
LogPath=$"RageCoop\\RageCoop.Client.log",
UseConsole=false,
#if DEBUG
LogLevel = 0,
#else
LogLevel=Settings.LogLevel,
#endif
};
Resources = new Scripting.Resources();
if (Game.Version < GameVersion.v1_0_1290_1_Steam)
{
Tick += (object sender, EventArgs e) =>
{
@ -65,22 +72,15 @@ namespace RageCoop.Client
}
};
return;
}*/
}
BaseScript.OnStart();
SyncedPedsGroup=World.AddRelationshipGroup("SYNCPED");
Game.Player.Character.RelationshipGroup.SetRelationshipBetweenGroups(SyncedPedsGroup, Relationship.Neutral, true);
Settings = Util.ReadSettings();
Networking.Start();
#if !NON_INTERACTIVE
MainMenu = new RageCoopMenu();
#endif
MainChat = new Chat();
#if DEBUG
Logger.LogLevel = 0;
#else
Logger.LogLevel=Settings.LogLevel;
#endif
Tick += OnTick;
Tick += (s,e) => { Scripting.API.Events.InvokeTick(); };
KeyDown += OnKeyDown;
Aborted += (object sender, EventArgs e) => CleanUp();
@ -107,14 +107,9 @@ namespace RageCoop.Client
}
#if !NON_INTERACTIVE
MainMenu.MenuPool.Process();
CoopMenu.MenuPool.Process();
#endif
if (_isGoingToCar && Game.Player.Character.IsInVehicle())
{
_isGoingToCar = false;
}
DoQueuedActions();
if (!Networking.IsOnServer)
{
@ -126,17 +121,13 @@ namespace RageCoop.Client
}
try
{
Networking.Tick();
EntityPool.DoSync();
}
catch (Exception ex)
{
Logger.Error(ex);
Main.Logger.Error(ex);
}
if (!DownloadManager.DownloadComplete)
{
DownloadManager.RenderProgress();
}
MapLoader.LoadAll();
@ -148,10 +139,9 @@ namespace RageCoop.Client
{
_lastDebugData = time;
_debugBytesReceived = Networking.BytesReceived;
Networking.BytesReceived = 0;
_debugBytesSend = Networking.BytesSend;
Networking.BytesSend = 0;
_debugBytesReceived = Networking.Client.Statistics.ReceivedBytes;
_debugBytesSend = Networking.Client.Statistics.SentBytes;
Networking.Client.Statistics.Reset();
}
new LemonUI.Elements.ScaledText(new PointF(Screen.PrimaryScreen.Bounds.Width / 2, 0), $"L: {Networking.Latency * 1000:N0}ms", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
@ -164,9 +154,33 @@ namespace RageCoop.Client
MainChat.Tick();
PlayerList.Tick();
if (!Scripting.API.Config.EnableAutoRespawn)
{
Function.Call(Hash.PAUSE_DEATH_ARREST_RESTART, true);
Function.Call(Hash.IGNORE_NEXT_RESTART, true);
Function.Call(Hash.FORCE_GAME_STATE_PLAYING);
Function.Call(Hash.TERMINATE_ALL_SCRIPTS_WITH_THIS_NAME, "respawn_controller");
var P = Game.Player.Character;
if (P.IsDead)
{
Function.Call(Hash.SET_FADE_OUT_AFTER_DEATH, false);
if (P.Health!=1)
{
P.Health=1;
Game.Player.WantedLevel=0;
Main.Logger.Debug("Player died.");
}
GTA.UI.Screen.StopEffects();
}
else
{
Function.Call(Hash.DISPLAY_HUD, true);
}
}
Ticked++;
}
@ -193,14 +207,20 @@ namespace RageCoop.Client
}
if (e.KeyCode == Settings.MenuKey)
{
if (MainMenu.MenuPool.AreAnyVisible)
if (CoopMenu.MenuPool.AreAnyVisible)
{
MainMenu.MainMenu.Visible = false;
MainMenu.SubSettings.Menu.Visible = false;
CoopMenu.MenuPool.ForEach<LemonUI.Menus.NativeMenu>(x =>
{
if (x.Visible)
{
CoopMenu.LastMenu=x;
x.Visible=false;
}
});
}
else
{
MainMenu.MainMenu.Visible = true;
CoopMenu.LastMenu.Visible = true;
}
}
else if (Game.IsControlJustPressed(GTA.Control.MultiplayerInfo))
@ -234,7 +254,7 @@ namespace RageCoop.Client
if (V!=null)
{
var seat = Util.GetNearestSeat(P, V);
var seat = P.GetNearestSeat(V);
P.Task.EnterVehicle(V, seat);
}
}
@ -250,7 +270,7 @@ namespace RageCoop.Client
}
public static readonly Dictionary<ulong, byte> CheckNativeHash = new Dictionary<ulong, byte>()
internal static readonly Dictionary<ulong, byte> CheckNativeHash = new Dictionary<ulong, byte>()
{
{ 0xD49F9B0955C367DE, 1 }, // Entities
{ 0xEF29A16337FACADB, 1 }, //
@ -267,8 +287,8 @@ namespace RageCoop.Client
{ 0x5A039BB0BCA604B6, 4 }, //
{ 0x0134F0835AB6BFCB, 5 } // Checkpoints
};
public static Dictionary<int, byte> ServerItems = new Dictionary<int, byte>();
public static void CleanUpWorld()
internal static Dictionary<int, byte> ServerItems = new Dictionary<int, byte>();
internal static void CleanUpWorld()
{
if (ServerItems.Count == 0)
{
@ -311,10 +331,9 @@ namespace RageCoop.Client
catch
{
GTA.UI.Notification.Show("~r~~h~CleanUpWorld() Error");
Logger.Error($"CleanUpWorld(): ~r~Item {item.Value} cannot be deleted!");
Main.Logger.Error($"CleanUpWorld(): ~r~Item {item.Value} cannot be deleted!");
}
}
ServerItems.Clear();
}
}
@ -334,7 +353,7 @@ namespace RageCoop.Client
}
catch(Exception ex)
{
GTA.UI.Screen.ShowSubtitle(ex.ToString());
Logger.Error(ex);
QueuedActions.Remove(action);
}
}
@ -345,14 +364,14 @@ namespace RageCoop.Client
/// Queue an action to be executed on next tick, allowing you to call scripting API from another thread.
/// </summary>
/// <param name="a"> The action to be executed, must return a bool indicating whether the action cane be removed after execution.</param>
public static void QueueAction(Func<bool> a)
internal static void QueueAction(Func<bool> a)
{
lock (QueuedActions)
{
QueuedActions.Add(a);
}
}
public static void QueueAction(Action a)
internal static void QueueAction(Action a)
{
lock (QueuedActions)
{
@ -362,37 +381,11 @@ namespace RageCoop.Client
/// <summary>
/// Clears all queued actions
/// </summary>
public static void ClearQueuedActions()
internal static void ClearQueuedActions()
{
lock (QueuedActions) { QueuedActions.Clear(); }
}
public static string DumpCharacters()
{
string s = "Characters:";
lock (EntityPool.PedsLock)
{
foreach (int id in EntityPool.GetPedIDs())
{
var c = EntityPool.GetPedByID(id);
s+=$"\r\nID:{c.ID} Owner:{c.OwnerID} LastUpdated:{c.LastUpdated} LastSynced:{c.LastSynced} LastStateSynced:{c.LastStateSynced}";
// s+=$"\r\n{c.IsAiming} {c.IsJumping} {c.IsOnFire} {c.IsOnLadder} {c.IsRagdoll} {c.IsReloading} {c.IsShooting} {c.Speed}";
}
}
Logger.Trace(s);
return s;
}
public static string DumpPlayers()
{
string s = "Players:";
foreach (PlayerData p in PlayerList.Players)
{
s+=$"\r\nID:{p.PedID} Username:{p.Username}";
}
Logger.Trace(s);
return s;
}
}
}

View File

@ -0,0 +1,166 @@
using GTA;
using System.Drawing;
using LemonUI;
using LemonUI.Menus;
using LemonUI.Scaleform;
namespace RageCoop.Client.Menus
{
/// <summary>
/// Don't use it!
/// </summary>
internal static class CoopMenu
{
public static ObjectPool MenuPool = new ObjectPool();
public static NativeMenu Menu = new NativeMenu("RAGECOOP", "MAIN")
{
UseMouse = false,
Alignment = Main.Settings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left
};
public static PopUp PopUp=new PopUp()
{
Title="",
Prompt="",
Subtitle = "",
Error="",
ShowBackground = true,
Visible=false,
};
public static NativeMenu LastMenu { get; set; } = Menu;
#region ITEMS
private static readonly NativeItem _usernameItem = new NativeItem("Username") { AltTitle = Main.Settings.Username };
private static readonly NativeItem _passwordItem = new NativeItem("Password") { AltTitle = new string('*',Main.Settings.Password.Length) };
public static readonly NativeItem ServerIpItem = new NativeItem("Server IP") { AltTitle = Main.Settings.LastServerAddress };
private static readonly NativeItem _serverConnectItem = new NativeItem("Connect");
private static readonly NativeItem _aboutItem = new NativeItem("About", "~y~SOURCE~s~~n~" +
"https://github.com/RAGECOOP~n~" +
"~y~VERSION~s~~n~" +
Main.CurrentVersion.Replace("_", ".")) { LeftBadge = new LemonUI.Elements.ScaledTexture("commonmenu", "shop_new_star") };
#endregion
/// <summary>
/// Don't use it!
/// </summary>
static CoopMenu()
{
Menu.Banner.Color = Color.FromArgb(225, 0, 0, 0);
Menu.Title.Color = Color.FromArgb(255, 165, 0);
_usernameItem.Activated += UsernameActivated;
_passwordItem.Activated+=_passwordActivated;
ServerIpItem.Activated += ServerIpActivated;
_serverConnectItem.Activated += (sender, item) => { Networking.ToggleConnection(Main.Settings.LastServerAddress); };
Menu.AddSubMenu(ServersMenu.Menu);
Menu.Add(_usernameItem);
Menu.Add(_passwordItem);
Menu.Add(ServerIpItem);
Menu.Add(_serverConnectItem);
Menu.AddSubMenu(SettingsMenu.Menu);
Menu.AddSubMenu(DevToolMenu.Menu);
Menu.AddSubMenu(DebugMenu.Menu);
MenuPool.Add(Menu);
MenuPool.Add(SettingsMenu.Menu);
MenuPool.Add(DevToolMenu.Menu);
MenuPool.Add(DebugMenu.Menu);
MenuPool.Add(DebugMenu.DiagnosticMenu);
MenuPool.Add(ServersMenu.Menu);
MenuPool.Add(PopUp);
Menu.Add(_aboutItem);
}
public static bool ShowPopUp(string prompt, string title,string subtitle,string error,bool showbackground)
{
PopUp.Prompt=prompt;
PopUp.Title=title;
PopUp.Subtitle=subtitle;
PopUp.Error=error;
PopUp.ShowBackground=showbackground;
PopUp.Visible=true;
while (true)
{
Game.DisableAllControlsThisFrame();
MenuPool.Process();
if (Game.IsControlJustPressed(Control.FrontendAccept))
{
PopUp.Visible=false;
return true;
}
else if (Game.IsControlJustPressed(Control.FrontendCancel))
{
PopUp.Visible = false;
return false;
}
Script.Yield();
}
}
public static void UsernameActivated(object a, System.EventArgs b)
{
string newUsername = Game.GetUserInput(WindowTitle.EnterMessage20, _usernameItem.AltTitle, 20);
if (!string.IsNullOrWhiteSpace(newUsername))
{
Main.Settings.Username = newUsername;
Util.SaveSettings();
_usernameItem.AltTitle = newUsername;
}
}
private static void _passwordActivated(object sender, System.EventArgs e)
{
string newPass = Game.GetUserInput(WindowTitle.EnterMessage20, "", 20);
if (!string.IsNullOrWhiteSpace(newPass))
{
Main.Settings.Password = newPass;
Util.SaveSettings();
_passwordItem.AltTitle = new string('*', newPass.Length);
}
}
public static void ServerIpActivated(object a, System.EventArgs b)
{
string newServerIp = Game.GetUserInput(WindowTitle.EnterMessage60, ServerIpItem.AltTitle, 60);
if (!string.IsNullOrWhiteSpace(newServerIp) && newServerIp.Contains(":"))
{
Main.Settings.LastServerAddress = newServerIp;
Util.SaveSettings();
ServerIpItem.AltTitle = newServerIp;
}
}
public static void InitiateConnectionMenuSetting()
{
_usernameItem.Enabled = false;
ServerIpItem.Enabled = false;
_serverConnectItem.Enabled = false;
}
public static void ConnectedMenuSetting()
{
_serverConnectItem.Enabled = true;
_serverConnectItem.Title = "Disconnect";
Menu.Visible = false;
}
public static void DisconnectedMenuSetting()
{
_usernameItem.Enabled = true;
ServerIpItem.Enabled = true;
_serverConnectItem.Enabled = true;
_serverConnectItem.Title = "Connect";
}
}
}

View File

@ -29,7 +29,7 @@ namespace RageCoop.Client
d1.Activated+=(sender,e) =>
{
try{ SyncParameters.PositioinPrediction =float.Parse(Game.GetUserInput(WindowTitle.EnterMessage20, SyncParameters.PositioinPrediction.ToString(), 20));}
try{ SyncParameters.PositioinPredictionDefault =float.Parse(Game.GetUserInput(WindowTitle.EnterMessage20, SyncParameters.PositioinPredictionDefault.ToString(), 20));}
catch { }
Update();
};
@ -55,7 +55,7 @@ namespace RageCoop.Client
private static void Update()
{
d1.AltTitle = SyncParameters.PositioinPrediction.ToString();
d1.AltTitle = SyncParameters.PositioinPredictionDefault.ToString();
}
}
}

View File

@ -9,7 +9,7 @@ using System.Drawing;
namespace RageCoop.Client
{
internal class DevToolMenu
internal static class DevToolMenu
{
public static NativeMenu Menu = new NativeMenu("RAGECOOP", "DevTool", "Help with the development")
{

View File

@ -0,0 +1,148 @@
using System;
using System.Net;
using System.Drawing;
using System.Collections.Generic;
using Newtonsoft.Json;
using LemonUI.Menus;
using System.Threading;
namespace RageCoop.Client.Menus
{
internal class ServerListClass
{
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("port")]
public string Port { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("players")]
public int Players { get; set; }
[JsonProperty("maxPlayers")]
public int MaxPlayers { get; set; }
[JsonProperty("country")]
public string Country { get; set; }
}
/// <summary>
/// Don't use it!
/// </summary>
internal static class ServersMenu
{
private static Thread GetServersThread;
internal static NativeMenu Menu = new NativeMenu("RAGECOOP", "Servers", "Go to the server list")
{
UseMouse = false,
Alignment = Main.Settings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left
};
internal static NativeItem ResultItem = null;
/// <summary>
/// Don't use it!
/// </summary>
static ServersMenu()
{
Menu.Banner.Color = Color.FromArgb(225, 0, 0, 0);
Menu.Title.Color = Color.FromArgb(255, 165, 0);
Menu.Opening += (object sender, System.ComponentModel.CancelEventArgs e) =>
{
CleanUpList();
Menu.Add(ResultItem = new NativeItem("Loading..."));
// Prevent freezing
GetServersThread=new Thread(()=> GetAllServers());
GetServersThread.Start();
};
Menu.Closing += (object sender, System.ComponentModel.CancelEventArgs e) =>
{
CleanUpList();
};
}
private static void CleanUpList()
{
Menu.Clear();
ResultItem = null;
}
private static void GetAllServers()
{
List<ServerListClass> serverList = null;
var realUrl = Main.Settings.MasterServer=="[AUTO]" ? DownloadString("https://ragecoop.online/stuff/masterserver") : Main.Settings.MasterServer;
serverList = JsonConvert.DeserializeObject<List<ServerListClass>>(DownloadString(realUrl));
// Need to be processed in main thread
Main.QueueAction(() =>
{
if (serverList == null)
{
ResultItem.Title = "Something went wrong!";
return;
}
if (serverList.Count == 0)
{
ResultItem.Title = "No server was found!";
return;
}
CleanUpList();
foreach (ServerListClass server in serverList)
{
string address = $"{server.Address}:{server.Port}";
NativeItem tmpItem = new NativeItem($"[{server.Country}] {server.Name}", $"~b~{address}~s~~n~~g~Version {server.Version}.x~s~") { AltTitle = $"[{server.Players}/{server.MaxPlayers}]" };
tmpItem.Activated += (object sender, EventArgs e) =>
{
try
{
Menu.Visible = false;
Networking.ToggleConnection(address);
#if !NON_INTERACTIVE
CoopMenu.ServerIpItem.AltTitle = address;
CoopMenu.Menu.Visible = true;
#endif
Main.Settings.LastServerAddress = address;
Util.SaveSettings();
}
catch (Exception ex)
{
GTA.UI.Notification.Show($"~r~{ex.Message}");
}
};
Menu.Add(tmpItem);
}
});
}
private static string DownloadString(string url)
{
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
WebClient client = new WebClient();
return client.DownloadString(url);
}
catch (Exception ex)
{
Main.QueueAction(() =>
{
ResultItem.Title = "Download failed!";
ResultItem.Description = ex.Message;
});
return "";
}
}
}
}

View File

@ -4,24 +4,24 @@ using System.Windows.Forms;
using GTA;
using LemonUI.Menus;
namespace RageCoop.Client.Menus.Sub
namespace RageCoop.Client.Menus
{
/// <summary>
/// Don't use it!
/// </summary>
public class SettingsMenu
internal static class SettingsMenu
{
public NativeMenu Menu = new NativeMenu("RAGECOOP", "Settings", "Go to the settings")
public static NativeMenu Menu = new NativeMenu("RAGECOOP", "Settings", "Go to the settings")
{
UseMouse = false,
Alignment = Main.Settings.FlipMenu ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left
};
private readonly NativeCheckboxItem _disableTrafficItem = new NativeCheckboxItem("Disable Traffic (NPCs/Vehicles)", "Local traffic only", Main.Settings.DisableTraffic);
private readonly NativeCheckboxItem _flipMenuItem = new NativeCheckboxItem("Flip menu", Main.Settings.FlipMenu);
private readonly NativeCheckboxItem _disablePauseAlt = new NativeCheckboxItem("Disable Alternate Pause", "Don't freeze game time when Esc pressed", Main.Settings.DisableTraffic);
private static readonly NativeCheckboxItem _disableTrafficItem = new NativeCheckboxItem("Disable Traffic (NPCs/Vehicles)", "Local traffic only", Main.Settings.DisableTraffic);
private static readonly NativeCheckboxItem _flipMenuItem = new NativeCheckboxItem("Flip menu", Main.Settings.FlipMenu);
private static readonly NativeCheckboxItem _disablePauseAlt = new NativeCheckboxItem("Disable Alternate Pause", "Don't freeze game time when Esc pressed", Main.Settings.DisableTraffic);
private readonly NativeCheckboxItem _showNetworkInfoItem = new NativeCheckboxItem("Show Network Info", Networking.ShowNetworkInfo);
private static readonly NativeCheckboxItem _showNetworkInfoItem = new NativeCheckboxItem("Show Network Info", Networking.ShowNetworkInfo);
private static NativeItem _menuKey = new NativeItem("Menu Key","The key to open menu", Main.Settings.MenuKey.ToString());
private static NativeItem _passengerKey = new NativeItem("Passenger Key", "The key to enter a vehicle as passenger", Main.Settings.PassengerKey.ToString());
@ -30,7 +30,7 @@ namespace RageCoop.Client.Menus.Sub
/// <summary>
/// Don't use it!
/// </summary>
public SettingsMenu()
static SettingsMenu()
{
Menu.Banner.Color = Color.FromArgb(225, 0, 0, 0);
Menu.Title.Color = Color.FromArgb(255, 165, 0);
@ -54,12 +54,12 @@ namespace RageCoop.Client.Menus.Sub
private void _disablePauseAlt_CheckboxChanged(object sender, EventArgs e)
private static void _disablePauseAlt_CheckboxChanged(object sender, EventArgs e)
{
Main.Settings.DisableAlternatePause=_disablePauseAlt.Checked;
Util.SaveSettings();
}
private void vehicleSoftLimit_Activated(object sender, EventArgs e)
private static void vehicleSoftLimit_Activated(object sender, EventArgs e)
{
try
{
@ -71,7 +71,7 @@ namespace RageCoop.Client.Menus.Sub
}
catch { }
}
private void ChaneMenuKey(object sender, EventArgs e)
private static void ChaneMenuKey(object sender, EventArgs e)
{
try
{
@ -85,7 +85,7 @@ namespace RageCoop.Client.Menus.Sub
catch { }
}
private void ChangePassengerKey(object sender, EventArgs e)
private static void ChangePassengerKey(object sender, EventArgs e)
{
try
{
@ -99,30 +99,25 @@ namespace RageCoop.Client.Menus.Sub
catch { }
}
public void DisableTrafficCheckboxChanged(object a, System.EventArgs b)
public static void DisableTrafficCheckboxChanged(object a, System.EventArgs b)
{
Main.Settings.DisableTraffic = _disableTrafficItem.Checked;
Util.SaveSettings() ;
}
public void FlipMenuCheckboxChanged(object a, System.EventArgs b)
public static void FlipMenuCheckboxChanged(object a, System.EventArgs b)
{
Main.MainMenu.MainMenu.Alignment = _flipMenuItem.Checked ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left;
CoopMenu.Menu.Alignment = _flipMenuItem.Checked ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left;
Menu.Alignment = _flipMenuItem.Checked ? GTA.UI.Alignment.Right : GTA.UI.Alignment.Left;
Main.Settings.FlipMenu = _flipMenuItem.Checked;
Util.SaveSettings();
}
public void ShowNetworkInfoCheckboxChanged(object a, System.EventArgs b)
public static void ShowNetworkInfoCheckboxChanged(object a, System.EventArgs b)
{
Networking.ShowNetworkInfo = _showNetworkInfoItem.Checked;
if (!Networking.ShowNetworkInfo)
{
Networking.BytesReceived = 0;
Networking.BytesSend = 0;
}
}
}
}

View File

@ -8,7 +8,7 @@ using GTA.Native;
namespace RageCoop.Client
{
public class Chat
internal class Chat
{
private readonly Scaleform MainScaleForm;

View File

@ -0,0 +1,169 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
namespace RageCoop.Client
{
internal static class DownloadManager
{
static string downloadFolder = $"RageCoop\\Resources\\{Main.Settings.LastServerAddress.Replace(":", ".")}";
private static readonly Dictionary<int, DownloadFile> InProgressDownloads = new Dictionary<int, DownloadFile>();
public static void AddFile(int id, string name, long length)
{
Main.Logger.Debug($"Downloading file to {downloadFolder}\\{name} , id:{id}");
if (!Directory.Exists(downloadFolder))
{
Directory.CreateDirectory(downloadFolder);
}
if (FileAlreadyExists(downloadFolder, name, length))
{
Main.Logger.Debug($"File already exists! canceling download:{name}");
Cancel(id);
if (name=="Resources.zip")
{
Main.Logger.Debug("Loading resources...");
Main.Resources.Load(Path.Combine(downloadFolder));
}
return;
}
if (!name.EndsWith(".zip"))
{
Cancel(id);
GTA.UI.Notification.Show($"The download of a file from the server was blocked! [{name}]", true);
Main.Logger.Error($"The download of a file from the server was blocked! [{name}]");
return;
}
lock (InProgressDownloads)
{
InProgressDownloads.Add(id, new DownloadFile()
{
FileID = id,
FileName = name,
FileLength = length,
Stream = new FileStream($"{downloadFolder}\\{name}", FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite)
});
}
}
/// <summary>
/// Check if the file already exists and if the size correct otherwise delete this file
/// </summary>
/// <param name="folder"></param>
/// <param name="name"></param>
/// <param name="length"></param>
/// <returns></returns>
private static bool FileAlreadyExists(string folder, string name, long length)
{
string filePath = $"{folder}\\{name}";
if (File.Exists(filePath))
{
if (new FileInfo(filePath).Length == length)
{
return true;
}
// Delete the file because the length is wrong (maybe the file was updated)
File.Delete(filePath);
}
return false;
}
public static void Write(int id, byte[] chunk)
{
lock (InProgressDownloads)
{
DownloadFile file;
if (InProgressDownloads.TryGetValue(id, out file))
{
file.Stream.Write(chunk, 0, chunk.Length);
}
else
{
Main.Logger.Trace($"Received unhandled file chunk:{id}");
return;
}
}
}
public static void Cancel(int id)
{
Main.Logger.Debug($"Canceling download:{id}");
// Tell the server to stop sending chunks
Networking.SendDownloadFinish(id);
DownloadFile file;
lock (InProgressDownloads)
{
if (InProgressDownloads.TryGetValue(id, out file))
{
InProgressDownloads.Remove(id);
file.Dispose();
}
}
}
public static void Complete(int id)
{
DownloadFile f;
if (InProgressDownloads.TryGetValue(id, out f))
{
lock (InProgressDownloads)
{
InProgressDownloads.Remove(id);
f.Dispose();
Main.Logger.Info($"Download finished:{f.FileName}");
if (f.FileName=="Resources.zip")
{
Main.Logger.Debug("Loading resources...");
Main.Resources.Load(Path.Combine(downloadFolder));
}
Networking.SendDownloadFinish(id);
}
}
else
{
Main.Logger.Error($"Download not found! {id}");
}
}
public static void Cleanup()
{
lock (InProgressDownloads)
{
foreach (var file in InProgressDownloads.Values)
{
file.Dispose();
}
InProgressDownloads.Clear();
}
}
}
public class DownloadFile: System.IDisposable
{
public int FileID { get; set; } = 0;
public string FileName { get; set; } = string.Empty;
public long FileLength { get; set; } = 0;
public long FileWritten { get; set; } = 0;
public FileStream Stream { get; set; }
public void Dispose()
{
if(Stream!= null)
{
Stream.Flush();
Stream.Close();
Stream.Dispose();
}
}
}
}

View File

@ -59,7 +59,7 @@ namespace RageCoop.Client
public static void LoadAll()
{
string downloadFolder = $"Scripts\\RageCoop\\Resources\\{Main.Settings.LastServerAddress.Replace(":", ".")}";
string downloadFolder = $"RageCoop\\Resources\\{Main.Settings.LastServerAddress.Replace(":", ".")}";
if (!Directory.Exists(downloadFolder))
{

Some files were not shown because too many files have changed in this diff Show More