95 Commits

Author SHA1 Message Date
1e1259d0a5 Fix 2022-07-12 17:16:28 +08:00
20b326d396 Check before breaking a door 2022-07-12 17:15:54 +08:00
b7cad3b8f7 Move GetWeaponComponents to WeaponUtil 2022-07-12 17:14:06 +08:00
cf8b54a3b5 Fix WeatherTimeSync option 2022-07-12 17:10:16 +08:00
edca1e2b98 Don't sync invisible vehicles 2022-07-11 14:53:22 +08:00
89410b8b46 Fix master server 2022-07-11 13:26:28 +08:00
a9397690e2 Fix 2022-07-11 12:02:46 +08:00
ea678ece94 Update master server 2022-07-11 12:01:12 +08:00
f28f23f560 Change master server 2022-07-11 12:00:25 +08:00
b2911017d0 Cleanup 2022-07-11 11:59:32 +08:00
cda4f702a1 Small API cleanup 2022-07-11 11:30:00 +08:00
b6a0ae7f4a Remove MapLoader 2022-07-11 11:24:17 +08:00
b6f3508680 Fix 2022-07-11 11:11:48 +08:00
ad0368b4c0 Merge resource loading 2022-07-11 11:10:43 +08:00
b1d67bd1c4 Add OnKeyDown and OnKeyUp event for API 2022-07-11 11:06:57 +08:00
cda3d37f37 Connect fix 2022-07-11 11:06:31 +08:00
abed0f146f Don't load resources previously downloaded 2022-07-11 10:08:21 +08:00
bdeaeee293 Update Resources.cs 2022-07-10 23:30:29 +08:00
b37ce4d0c1 Update 2022-07-10 18:02:53 +08:00
e3ff62bdc7 Remove docs 2022-07-10 16:49:41 +08:00
eab6c79e15 Update docs 2022-07-10 16:39:28 +08:00
260678c1bc Update index.md 2022-07-10 16:34:42 +08:00
b2d300784f Update docs 2022-07-10 16:33:59 +08:00
8d1d4f5eb7 Update nightly-build.yaml 2022-07-10 16:25:44 +08:00
25a03c7e27 Update nightly-build.yaml 2022-07-10 16:22:24 +08:00
27ab63ae7a Add weather and time sync 2022-07-10 16:13:08 +08:00
9567e97b31 Add vehicle livery sync 2022-07-09 22:18:00 +08:00
b10a8b47d5 Small vehicle exit fix 2022-07-09 19:59:17 +08:00
1bc274e2d0 Some tweaks 2022-07-09 19:32:11 +08:00
dca3420071 Use Array.Copy() instead of Linq 2022-07-09 14:04:39 +08:00
bc3fc70a95 Some tweaks 2022-07-09 13:55:06 +08:00
eee983dfd2 Change output path 2022-07-09 13:27:07 +08:00
fbec69cd71 Reload SHVDN on disconnect if there're client resources 2022-07-09 13:16:41 +08:00
615c49e395 Add country 2022-07-09 11:39:37 +08:00
462d68df6b Add addtional server info 2022-07-09 11:36:04 +08:00
0224abc9bd Request model before creating vehicle 2022-07-09 11:21:42 +08:00
16fe2fe1ab Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-09 10:51:26 +08:00
61c4486551 Disable code trimming 2022-07-09 10:44:54 +08:00
17ef088492 Don't show info if no script loaded 2022-07-09 09:59:17 +08:00
1111879c4d Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-09 09:55:22 +08:00
e9d0b73296 Add managed and loaded assembly check 2022-07-09 09:55:12 +08:00
8eadbf6fee Update README.md 2022-07-08 19:51:21 +08:00
8ae491d0d5 Move ServerPbject and ServerEntities to RageCoop.Server.Scripting 2022-07-07 16:57:43 +08:00
3dd125f861 Reference custom SHVDN build 2022-07-07 16:24:03 +08:00
01b4753dc6 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-06 01:03:35 +08:00
5a5d78ac27 Networking statistics, disable resource unload 2022-07-06 01:03:18 +08:00
a05eedd68e Fix NativeResponse 2022-07-05 21:58:37 +08:00
05304f9461 Add SendCustomEventQueued() 2022-07-05 11:18:26 +08:00
fff5ec4534 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-05 10:52:33 +08:00
cf7641cee0 Change object list to array for CustomEvent 2022-07-05 10:52:22 +08:00
a77e0c1fc9 Fix ServerBlip 2022-07-05 00:18:12 +08:00
f374de2064 Update BaseScript.cs 2022-07-05 00:16:15 +08:00
8e0620bedb Fix CreateVehicle() not setting owner properly 2022-07-05 00:02:22 +08:00
8bb39b1bfa Add PedBlip 2022-07-04 23:59:51 +08:00
2450956fda Add ped blip sync 2022-07-04 22:50:47 +08:00
56cd17401b Change parameter order for consistency 2022-07-04 21:35:01 +08:00
a53b514fec Add ServerEntities.CreateVehicle() 2022-07-04 21:29:13 +08:00
de93af73f2 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-04 09:57:13 +08:00
44f106fd0c Add ServerBlip.Handle handle 2022-07-04 09:54:43 +08:00
8ac5cc40c1 Change parameter order for SendCustomEvent and SendNativecall
Use NativeCall to update entity
2022-07-04 09:41:00 +08:00
8d7eebd390 Fix DeleteEntity not being dispatched to main thread 2022-07-03 23:42:00 +08:00
0a5bb67c54 Add Name property for blip, fix blip scale 2022-07-03 21:00:05 +08:00
f866151cc5 Update docs 2022-07-03 15:37:51 +08:00
08e17b1714 Server side blip and Vector2 argument 2022-07-03 15:28:28 +08:00
fa96f4c073 Add Freeze() method for ServerObject 2022-07-03 10:55:49 +08:00
dc5cf2b965 ServerObject Inheritance, getter and setter for Position and Roatation, Fix connect error. 2022-07-03 10:46:24 +08:00
f47f0570f9 Update docs 2022-07-02 18:32:49 +08:00
e0c96cc200 Minor documentation fix 2022-07-02 18:30:16 +08:00
17e1ae9ea9 Update documentation 2022-07-02 18:04:07 +08:00
8401d616e0 Convert object to handle at client side 2022-07-02 17:53:35 +08:00
d8ac486984 Server side prop control 2022-07-02 17:14:56 +08:00
335ea2ca38 Fix data folder 2022-07-02 13:53:14 +08:00
c73ff96690 Add JB7002 weapon (not tested), closes #15 2022-07-02 13:37:44 +08:00
15b23e3a4e Add DOMINATOR5, IMPALER3, IMPERATOR2, SLAMVAN5 (not tested), closes #16 2022-07-02 13:32:26 +08:00
47f86c098d Add Runier2 weapon, closes #18 2022-07-02 13:21:55 +08:00
6c2c7e881f Add TAMPA3 weapon, closes #19 2022-07-02 13:12:59 +08:00
74a2265200 Add DUNE3 weapon sync 2022-07-02 12:53:18 +08:00
0b2ec1d626 Fix player position not correctly set 2022-07-02 12:39:50 +08:00
8ee188cf19 update docs 2022-07-02 11:23:12 +08:00
eb5b23ae17 Server side entities and custom resource location 2022-07-01 19:02:38 +08:00
77999fe8f3 Add docfx documentation 2022-07-01 17:00:42 +08:00
64fda51917 Restore 2022-07-01 14:39:43 +08:00
4165b757a5 XML comments 2022-07-01 13:54:18 +08:00
d31799ab7b Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-07-01 12:25:37 +08:00
8a46bd6b68 Resource loading 2022-07-01 12:22:31 +08:00
58362c2613 Preparing for PluginLoader 2022-06-30 09:28:13 +08:00
7d6c5fe3bd Remove "nightly" suffix 2022-06-27 16:00:19 +08:00
f9b9d78b79 Merge branch 'main' of https://github.com/RAGECOOP/RageCoop-V 2022-06-27 15:35:32 +08:00
7670af88a2 Thread termination fix 2022-06-27 15:35:23 +08:00
0800361b40 Minor fixes 2022-06-27 14:42:57 +08:00
cdf3f41127 Update README.md 2022-06-27 14:13:55 +08:00
552bf8d61c Update README.md 2022-06-27 14:12:53 +08:00
2f58e48f42 Nightly 2022-06-27 14:07:13 +08:00
67035f5d2f test 2022-06-27 13:56:42 +08:00
b48206d942 nightly test 2022-06-27 13:49:16 +08:00
229 changed files with 3821 additions and 27296 deletions

View File

@ -23,28 +23,77 @@ jobs:
- 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
run: dotnet publish RageCoop.Client/RageCoop.Client.csproj --no-restore --configuration Release -o RageCoop.Client/bin/RageCoop -f net48
- 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
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=false -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
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=false -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
run: dotnet publish RageCoop.Server/RageCoop.Server.csproj --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=false -r linux-arm -o RageCoop.Server/bin/linux-arm -c Release
- uses: vimtor/action-zip@v1
with:
name: RageCoop.Client
path: RageCoop.Client/bin
- uses: actions/upload-artifact@v3
files: RageCoop.Client/bin
dest: RageCoop.Client.zip
- uses: vimtor/action-zip@v1
with:
name: RageCoop.Server-win-x64
path: RageCoop.Server/bin/win-x64
- uses: actions/upload-artifact@v3
files: RageCoop.Server/bin/win-x64
dest: RageCoop.Server-win-x64.zip
- uses: vimtor/action-zip@v1
with:
name: RageCoop.Server-linux-x64
path: RageCoop.Server/bin/linux-x64
- uses: actions/upload-artifact@v3
files: RageCoop.Server/bin/linux-x64
dest: RageCoop.Server-linux-x64.zip
- uses: vimtor/action-zip@v1
with:
name: RageCoop.Server-linux-arm
path: RageCoop.Server/bin/linux-arm
files: RageCoop.Server/bin/linux-arm
dest: RageCoop.Server-linux-arm.zip
- uses: WebFreak001/deploy-nightly@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions
with:
upload_url: https://uploads.github.com/repos/RAGECOOP/RAGECOOP-V/releases/70603992/assets{?name,label}
release_id: 70603992
asset_path: RageCoop.Client.zip
asset_name: RageCoop.Client.zip
asset_content_type: application/zip
max_releases: 7
- uses: WebFreak001/deploy-nightly@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions
with:
upload_url: https://uploads.github.com/repos/RAGECOOP/RAGECOOP-V/releases/70603992/assets{?name,label}
release_id: 70603992
asset_path: RageCoop.Server-win-x64.zip
asset_name: RageCoop.Server-win-x64.zip
asset_content_type: application/zip
max_releases: 7
- uses: WebFreak001/deploy-nightly@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions
with:
upload_url: https://uploads.github.com/repos/RAGECOOP/RAGECOOP-V/releases/70603992/assets{?name,label}
release_id: 70603992
asset_path: RageCoop.Server-linux-x64.zip
asset_name: RageCoop.Server-linux-x64.zip
asset_content_type: application/zip
max_releases: 7
- uses: WebFreak001/deploy-nightly@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions
with:
upload_url: https://uploads.github.com/repos/RAGECOOP/RAGECOOP-V/releases/70603992/assets{?name,label}
release_id: 70603992
asset_path: RageCoop.Server-linux-arm.zip
asset_name: RageCoop.Server-linux-arm.zip
asset_content_type: application/zip
max_releases: 7
- uses: actions/checkout@v2

View File

@ -7,11 +7,6 @@
[![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.
@ -30,6 +25,7 @@ _Old name: GTACOOP:R_
- [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/13.0.1)
- [ClearScript](https://github.com/microsoft/ClearScript)
- [SharpZipLib](https://github.com/icsharpcode/SharpZipLib)
- [DotNetCorePlugins](https://github.com/natemcmaster/DotNetCorePlugins)
# Features
@ -60,8 +56,7 @@ Refer to the [wiki](https://github.com/RAGECOOP/RAGECOOP-V/wiki)
Download latest release [here](https://github.com/RAGECOOP/RAGECOOP-V/releases/latest)
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.
You can also download nightly builds [here](https://github.com/RAGECOOP/RAGECOOP-V/releases/nightly), which includes latest features and bug-fixes, but has not been thoroughly tested.
Please note that this is incompatible with all previous versions of ragecoop, remove old files before installing.

View File

@ -172,7 +172,7 @@ namespace RageCoop.Client
}
}
public enum MuzzleDir:byte
internal enum MuzzleDir:byte
{
Forward=0,
Right = 1,

View File

@ -1,41 +0,0 @@
// 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

@ -1,138 +0,0 @@
#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

@ -1,23 +0,0 @@
#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

@ -1,113 +0,0 @@
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

@ -1,33 +0,0 @@
// 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

@ -1,23 +0,0 @@
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

@ -1,68 +0,0 @@
#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

@ -1,65 +0,0 @@
#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

@ -1,506 +0,0 @@
#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

@ -1,144 +0,0 @@
#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

@ -1,49 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,67 +0,0 @@
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

@ -1,101 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,174 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,678 +0,0 @@
#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

@ -1,163 +0,0 @@
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

@ -1,371 +0,0 @@
#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

@ -1,398 +0,0 @@
#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

@ -1,366 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,30 +0,0 @@
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

@ -1,143 +0,0 @@
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

@ -1,241 +0,0 @@
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

@ -1,187 +0,0 @@
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

@ -1,167 +0,0 @@
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

@ -1,75 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,297 +0,0 @@
#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

@ -1,342 +0,0 @@
#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

@ -1,357 +0,0 @@
#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

@ -1,425 +0,0 @@
#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

@ -1,15 +0,0 @@
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

@ -1,196 +0,0 @@
#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

@ -1,92 +0,0 @@
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

@ -1,57 +0,0 @@
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

@ -1,284 +0,0 @@
#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

@ -1,65 +0,0 @@
#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

@ -1,23 +0,0 @@
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

@ -1,151 +0,0 @@
#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

@ -1,174 +0,0 @@
#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

@ -1,220 +0,0 @@
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

@ -1,123 +0,0 @@
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

@ -45,9 +45,10 @@ namespace RageCoop.Client
public Main()
{
Settings = Util.ReadSettings();
Directory.CreateDirectory(Settings.DataDirectory);
Logger=new Logger()
{
LogPath=$"RageCoop\\RageCoop.Client.log",
LogPath=$"{Settings.DataDirectory}\\RageCoop.Client.log",
UseConsole=false,
#if DEBUG
LogLevel = 0,
@ -64,7 +65,6 @@ namespace RageCoop.Client
{
return;
}
if (!_gameLoaded)
{
GTA.UI.Notification.Show("~r~Please update your GTA5 to v1.0.1290 or newer!", true);
@ -82,6 +82,8 @@ namespace RageCoop.Client
Tick += OnTick;
Tick += (s,e) => { Scripting.API.Events.InvokeTick(); };
KeyDown += OnKeyDown;
KeyDown+=(s, e) => { Scripting.API.Events.InvokeKeyDown(s, e); };
KeyUp+=(s, e) => { Scripting.API.Events.InvokeKeyUp(s, e); };
Aborted += (object sender, EventArgs e) => CleanUp();
Util.NativeMemory();
@ -89,9 +91,6 @@ namespace RageCoop.Client
}
#if DEBUG
private ulong _lastDebugData;
private int _debugBytesSend;
private int _debugBytesReceived;
#endif
private void OnTick(object sender, EventArgs e)
{
@ -129,24 +128,14 @@ namespace RageCoop.Client
}
MapLoader.LoadAll();
#if DEBUG
if (Networking.ShowNetworkInfo)
{
ulong time = Util.GetTickCount64();
if (time - _lastDebugData > 1000)
{
_lastDebugData = time;
_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();
new LemonUI.Elements.ScaledText(new PointF(Screen.PrimaryScreen.Bounds.Width / 2, 30), $"R: {Lidgren.Network.NetUtility.ToHumanReadable(_debugBytesReceived)}/s", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
new LemonUI.Elements.ScaledText(new PointF(Screen.PrimaryScreen.Bounds.Width / 2, 60), $"S: {Lidgren.Network.NetUtility.ToHumanReadable(_debugBytesSend)}/s", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
new LemonUI.Elements.ScaledText(new PointF(Screen.PrimaryScreen.Bounds.Width / 2, 30), $"R: {Lidgren.Network.NetUtility.ToHumanReadable(Statistics.BytesDownPerSecond)}/s", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
new LemonUI.Elements.ScaledText(new PointF(Screen.PrimaryScreen.Bounds.Width / 2, 60), $"S: {Lidgren.Network.NetUtility.ToHumanReadable(Statistics.BytesUpPerSecond)}/s", 0.5f) { Alignment = GTA.UI.Alignment.Center }.Draw();
}
#endif

View File

@ -34,7 +34,7 @@ namespace RageCoop.Client.Menus
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");
internal 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~" +

View File

@ -77,7 +77,7 @@ namespace RageCoop.Client.Menus
private static void GetAllServers()
{
List<ServerListClass> serverList = null;
var realUrl = Main.Settings.MasterServer=="[AUTO]" ? DownloadString("https://ragecoop.online/stuff/masterserver") : Main.Settings.MasterServer;
var realUrl = Main.Settings.MasterServer;
serverList = JsonConvert.DeserializeObject<List<ServerListClass>>(DownloadString(realUrl));
// Need to be processed in main thread

View File

@ -1,41 +1,85 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
using RageCoop.Core;
using System;
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)
static DownloadManager()
{
Main.Logger.Debug($"Downloading file to {downloadFolder}\\{name} , id:{id}");
if (!Directory.Exists(downloadFolder))
Networking.RequestHandlers.Add(PacketType.FileTransferRequest, (data) =>
{
Directory.CreateDirectory(downloadFolder);
var fr = new Packets.FileTransferRequest();
fr.Unpack(data);
if (fr.Name.EndsWith(".zip"))
{
_zips.Add(fr.Name);
}
return new Packets.FileTransferResponse()
{
ID= fr.ID,
Response=AddFile(fr.ID,fr.Name,fr.FileLength) ? FileResponse.NeedToDownload : FileResponse.AlreadyExists
};
});
Networking.RequestHandlers.Add(PacketType.FileTransferComplete, (data) =>
{
Packets.FileTransferComplete packet = new Packets.FileTransferComplete();
packet.Unpack(data);
Main.Logger.Debug($"Finalizing download:{packet.ID}");
Complete(packet.ID);
// Inform the server that the download is completed
return new Packets.FileTransferResponse()
{
ID= packet.ID,
Response=FileResponse.Completed
};
});
Networking.RequestHandlers.Add(PacketType.AllResourcesSent, (data) =>
{
try
{
Main.Resources.Load(ResourceFolder,_zips.ToArray());
return new Packets.FileTransferResponse() { ID=0, Response=FileResponse.Loaded };
}
catch(Exception ex)
{
Main.Logger.Error("Error occurred when loading server resource:");
Main.Logger.Error(ex);
return new Packets.FileTransferResponse() { ID=0, Response=FileResponse.LoadFailed };
}
});
}
public static string ResourceFolder {
get {
return Path.Combine(Main.Settings.DataDirectory,"Resources", Main.Settings.LastServerAddress.Replace(":", "."));
}
}
private static readonly Dictionary<int, DownloadFile> InProgressDownloads = new Dictionary<int, DownloadFile>();
private static readonly List<string> _zips = new List<string>();
public static bool AddFile(int id, string name, long length)
{
Main.Logger.Debug($"Downloading file to {ResourceFolder}\\{name} , id:{id}");
if (!Directory.Exists(ResourceFolder))
{
Directory.CreateDirectory(ResourceFolder);
}
if (FileAlreadyExists(downloadFolder, name, length))
if (FileAlreadyExists(ResourceFolder, 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;
return false;
}
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;
Main.Logger.Error($"File download blocked! [{name}]");
return false;
}
lock (InProgressDownloads)
{
@ -44,9 +88,10 @@ namespace RageCoop.Client
FileID = id,
FileName = name,
FileLength = length,
Stream = new FileStream($"{downloadFolder}\\{name}", FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite)
Stream = new FileStream($"{ResourceFolder}\\{name}", FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite)
});
}
return true;
}
/// <summary>
@ -87,47 +132,24 @@ namespace RageCoop.Client
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();
if (f.FileName.EndsWith(".zip"))
{
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);
_zips.Add(f.FileName);
}
Main.Logger.Info($"Download finished:{f.FileName}");
}
else
{
@ -145,11 +167,20 @@ namespace RageCoop.Client
}
InProgressDownloads.Clear();
}
if (Directory.Exists(ResourceFolder))
{
foreach (var zip in Directory.GetDirectories(ResourceFolder, "*.zip"))
{
File.Delete(zip);
}
}
_zips.Clear();
}
}
public class DownloadFile: System.IDisposable
internal class DownloadFile: System.IDisposable
{
public int FileID { get; set; } = 0;
public string FileName { get; set; } = string.Empty;

View File

@ -1,180 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using System.Collections.Generic;
using RageCoop.Core;
using GTA;
using GTA.Math;
using GTA.Native;
namespace RageCoop.Client
{
/// <summary>
///
/// </summary>
[XmlRoot(ElementName = "Map")]
public class CoopMap
{
/// <summary>
///
/// </summary>
[XmlArray("Props")]
[XmlArrayItem("Prop")]
public List<CoopProp> Props { get; set; } = new List<CoopProp>();
}
/// <summary>
///
/// </summary>
public struct CoopProp
{
/// <summary>
///
/// </summary>
public Vector3 Position { get; set; }
/// <summary>
///
/// </summary>
public Vector3 Rotation { get; set; }
/// <summary>
///
/// </summary>
public int Hash { get; set; }
/// <summary>
///
/// </summary>
public bool Dynamic { get; set; }
/// <summary>
///
/// </summary>
public int Texture { get; set; }
}
public static class MapLoader
{
// string = file name
private static readonly Dictionary<string, CoopMap> _maps = new Dictionary<string, CoopMap>();
private static readonly List<int> _createdObjects = new List<int>();
public static void LoadAll()
{
string downloadFolder = $"RageCoop\\Resources\\{Main.Settings.LastServerAddress.Replace(":", ".")}";
if (!Directory.Exists(downloadFolder))
{
try
{
Directory.CreateDirectory(downloadFolder);
}
catch (Exception ex)
{
Main.Logger.Error(ex.Message);
// Without the directory we can't do the other stuff
return;
}
}
string[] files = Directory.GetFiles(downloadFolder, "*.xml");
lock (_maps)
{
for (int i = 0; i < files.Length; i++)
{
string filePath = files[i];
string fileName = Path.GetFileName(filePath);
XmlSerializer serializer = new XmlSerializer(typeof(CoopMap));
CoopMap map;
using (var stream = new FileStream(filePath, FileMode.Open))
{
try
{
map = (CoopMap)serializer.Deserialize(stream);
}
catch (Exception ex)
{
Main.Logger.Error($"The map with the name \"{fileName}\" couldn't be added!");
Main.Logger.Error($"{ex.Message}");
continue;
}
}
_maps.Add(fileName, map);
}
}
}
public static void LoadMap(string name)
{
lock (_maps) lock (_createdObjects)
{
if (!_maps.ContainsKey(name) || _createdObjects.Count != 0)
{
GTA.UI.Notification.Show($"The map with the name \"{name}\" couldn't be loaded!");
Main.Logger.Error($"The map with the name \"{name}\" couldn't be loaded!");
return;
}
CoopMap map = _maps[name];
foreach (CoopProp prop in map.Props)
{
Model model = prop.Hash.ModelRequest();
if (model == null)
{
Main.Logger.Error($"Model for object \"{model.Hash}\" couldn't be loaded!");
continue;
}
int handle = Function.Call<int>(Hash.CREATE_OBJECT, model.Hash, prop.Position.X, prop.Position.Y, prop.Position.Z, 1, 1, prop.Dynamic);
model.MarkAsNoLongerNeeded();
if (handle == 0)
{
Main.Logger.Error($"Object \"{prop.Hash}\" couldn't be created!");
continue;
}
_createdObjects.Add(handle);
if (prop.Texture > 0 && prop.Texture < 16)
{
Function.Call(Hash._SET_OBJECT_TEXTURE_VARIATION, handle, prop.Texture);
}
}
}
}
public static bool AnyMapLoaded()
{
lock (_createdObjects) return _createdObjects.Any();
}
public static void UnloadMap()
{
lock (_createdObjects)
{
foreach (int handle in _createdObjects)
{
unsafe
{
int tmpHandle = handle;
Function.Call(Hash.DELETE_OBJECT, &tmpHandle);
}
}
_createdObjects.Clear();
}
}
public static void DeleteAll()
{
UnloadMap();
lock (_maps)
{
_maps.Clear();
}
}
}
}

View File

@ -1,15 +1,9 @@
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 System.Security.Cryptography;
using GTA.Math;
using GTA.Native;
using System.Net;
using System.IO;
namespace RageCoop.Client
{
@ -76,26 +70,40 @@ namespace RageCoop.Client
EntityPool.AddPlayer();
Task.Run(() =>
{
Client = new NetClient(config);
Client.Start();
Security.Regen();
GetServerPublicKey(address);
// Send HandshakePacket
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
var handshake=new Packets.Handshake()
try
{
PedID = Main.LocalPlayerID,
Username =username,
ModVersion = Main.CurrentVersion,
PassHashEncrypted=Security.Encrypt(password.GetHash())
};
Security.GetSymmetricKeysCrypted(out handshake.AesKeyCrypted,out handshake.AesIVCrypted);
handshake.Pack(outgoingMessage);
Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage);
Main.QueueAction(() => { GTA.UI.Notification.Show($"~y~Trying to connect..."); });
DownloadManager.Cleanup();
Client = new NetClient(config);
Client.Start();
Main.QueueAction(() => { GTA.UI.Notification.Show($"~y~Trying to connect..."); });
Menus.CoopMenu._serverConnectItem.Enabled=false;
Security.Regen();
if(!GetServerPublicKey(address))
{
Menus.CoopMenu._serverConnectItem.Enabled=true;
throw new TimeoutException("Failed to retrive server's public key");
}
// Send HandshakePacket
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
var handshake = new Packets.Handshake()
{
PedID = Main.LocalPlayerID,
Username =username,
ModVersion = Main.CurrentVersion,
PassHashEncrypted=Security.Encrypt(password.GetHash())
};
Security.GetSymmetricKeysCrypted(out handshake.AesKeyCrypted, out handshake.AesIVCrypted);
handshake.Pack(outgoingMessage);
Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage);
}
catch(Exception ex)
{
Main.Logger.Error("Cannot connect to server: ", ex);
Main.QueueAction(() => GTA.UI.Notification.Show("Cannot connect to server: "+ex.Message));
}
});
}
@ -131,13 +139,13 @@ namespace RageCoop.Client
#endregion // -- PLAYER --
private static void GetServerPublicKey(string address,int timeout=10000)
private static bool GetServerPublicKey(string address,int timeout=10000)
{
var msg=Client.CreateMessage();
new Packets.PublicKeyRequest().Pack(msg);
var adds =address.Split(':');
Client.SendUnconnectedMessage(msg,adds[0],int.Parse(adds[1]));
PublicKeyReceived.WaitOne(timeout);
return PublicKeyReceived.WaitOne(timeout);
}
#endregion

View File

@ -1,21 +1,36 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Lidgren.Network;
using RageCoop.Core;
using GTA;
using RageCoop.Client.Menus;
using System.Threading;
using GTA.Math;
using GTA.Native;
using System.Net;
namespace RageCoop.Client
{
internal static partial class Networking
{
private static Func<byte, BitReader, object> _resolveHandle = (t, reader) =>
{
switch (t)
{
case 50:
return EntityPool.ServerProps[reader.ReadInt()].MainProp?.Handle;
case 51:
return EntityPool.GetPedByID(reader.ReadInt())?.MainPed?.Handle;
case 52:
return EntityPool.GetVehicleByID(reader.ReadInt())?.MainVehicle?.Handle;
case 60:
return EntityPool.ServerBlips[reader.ReadInt()].Handle;
default:
throw new ArgumentException("Cannot resolve server side argument: "+t);
}
};
private static AutoResetEvent PublicKeyReceived=new AutoResetEvent(false);
private static Dictionary<int, Action<PacketType, byte[]>> PendingResponses = new Dictionary<int, Action<PacketType, byte[]>>();
internal static Dictionary<PacketType, Func< byte[], Packet>> RequestHandlers = new Dictionary<PacketType, Func< byte[], Packet>>();
public static void ProcessMessage(NetIncomingMessage message)
{
if(message == null) { return; }
@ -66,10 +81,10 @@ namespace RageCoop.Client
Main.QueueAction(() =>
GTA.UI.Notification.Show("~r~Disconnected: " + reason));
MapLoader.DeleteAll();
Main.Resources.Unload();
Main.Resources.Unload();
Main.Logger.Info($">> Disconnected << reason: {reason}");
break;
}
break;
@ -77,138 +92,47 @@ namespace RageCoop.Client
{
if (message.LengthBytes==0) { break; }
var packetType = PacketTypes.Unknown;
var packetType = PacketType.Unknown;
try
{
packetType = (PacketTypes)message.ReadByte();
int len = message.ReadInt32();
byte[] data = message.ReadBytes(len);
// Get packet type
packetType = (PacketType)message.ReadByte();
switch (packetType)
{
case PacketTypes.PlayerConnect:
case PacketType.Response:
{
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.UpdatePlayer(packet);
int id = message.ReadInt32();
if (PendingResponses.TryGetValue(id, out var callback))
{
callback((PacketType)message.ReadByte(), message.ReadBytes(message.ReadInt32()));
PendingResponses.Remove(id);
}
break;
}
#region ENTITY SYNC
case PacketTypes.VehicleSync:
case PacketType.Request:
{
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);
int id = message.ReadInt32();
var realType = (PacketType)message.ReadByte();
int len = message.ReadInt32();
Main.Logger.Debug($"{id},{realType},{len}");
if (RequestHandlers.TryGetValue(realType, out var handler))
{
var response = Client.CreateMessage();
response.Write((byte)PacketType.Response);
response.Write(id);
handler(message.ReadBytes(len)).Pack(response);
Client.SendMessage(response, NetDeliveryMethod.ReliableOrdered);
}
break;
}
#endregion
case PacketTypes.ChatMessage:
{
Packets.ChatMessage packet = new Packets.ChatMessage();
packet.Unpack(data);
Main.QueueAction(() => { Main.MainChat.AddMessage(packet.Username, packet.Message); return true; });
}
break;
case PacketTypes.CustomEvent:
{
Packets.CustomEvent packet = new Packets.CustomEvent();
packet.Unpack(data);
Scripting.API.Events.InvokeCustomEventReceived(packet);
}
break;
case PacketTypes.FileTransferChunk:
{
Packets.FileTransferChunk packet = new Packets.FileTransferChunk();
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.Name, packet.FileLength);
}
break;
case PacketTypes.FileTransferComplete:
{
Packets.FileTransferComplete packet = new Packets.FileTransferComplete();
packet.Unpack(data);
Main.Logger.Debug($"Finalizing download:{packet.ID}");
DownloadManager.Complete(packet.ID);
}
break;
default:
if (packetType.IsSyncEvent())
{
// Dispatch to main thread
Main.QueueAction(() => { SyncEvents.HandleEvent(packetType, data); return true; });
byte[] data = message.ReadBytes(message.ReadInt32());
HandlePacket(packetType, data);
break;
}
break;
}
}
catch (Exception ex)
@ -228,10 +152,10 @@ namespace RageCoop.Client
break;
case NetIncomingMessageType.UnconnectedData:
{
var packetType = (PacketTypes)message.ReadByte();
var packetType = (PacketType)message.ReadByte();
int len = message.ReadInt32();
byte[] data = message.ReadBytes(len);
if (packetType==PacketTypes.PublicKeyResponse)
if (packetType==PacketType.PublicKeyResponse)
{
var packet=new Packets.PublicKeyResponse();
packet.Unpack(data);
@ -252,14 +176,133 @@ namespace RageCoop.Client
Client.Recycle(message);
}
private static void HandlePacket(PacketType packetType, byte[] data)
{
switch (packetType)
{
case PacketType.PlayerConnect:
{
Packets.PlayerConnect packet = new Packets.PlayerConnect();
packet.Unpack(data);
Main.QueueAction(() => PlayerConnect(packet));
}
break;
case PacketType.PlayerDisconnect:
{
Packets.PlayerDisconnect packet = new Packets.PlayerDisconnect();
packet.Unpack(data);
Main.QueueAction(() => PlayerDisconnect(packet));
}
break;
case PacketType.PlayerInfoUpdate:
{
var packet = new Packets.PlayerInfoUpdate();
packet.Unpack(data);
PlayerList.UpdatePlayer(packet);
break;
}
#region ENTITY SYNC
case PacketType.VehicleSync:
{
Packets.VehicleSync packet = new Packets.VehicleSync();
packet.Unpack(data);
VehicleSync(packet);
}
break;
case PacketType.PedSync:
{
Packets.PedSync packet = new Packets.PedSync();
packet.Unpack(data);
PedSync(packet);
}
break;
case PacketType.VehicleStateSync:
{
Packets.VehicleStateSync packet = new Packets.VehicleStateSync();
packet.Unpack(data);
VehicleStateSync(packet);
}
break;
case PacketType.PedStateSync:
{
Packets.PedStateSync packet = new Packets.PedStateSync();
packet.Unpack(data);
PedStateSync(packet);
}
break;
case PacketType.ProjectileSync:
{
Packets.ProjectileSync packet = new Packets.ProjectileSync();
packet.Unpack(data);
ProjectileSync(packet);
break;
}
#endregion
case PacketType.ChatMessage:
{
Packets.ChatMessage packet = new Packets.ChatMessage();
packet.Unpack(data);
Main.QueueAction(() => { Main.MainChat.AddMessage(packet.Username, packet.Message); return true; });
}
break;
case PacketType.CustomEvent:
{
Packets.CustomEvent packet = new Packets.CustomEvent(_resolveHandle);
packet.Unpack(data);
Scripting.API.Events.InvokeCustomEventReceived(packet);
}
break;
case PacketType.CustomEventQueued:
{
Packets.CustomEvent packet = new Packets.CustomEvent(_resolveHandle);
Main.QueueAction(() =>
{
packet.Unpack(data);
Scripting.API.Events.InvokeCustomEventReceived(packet);
});
}
break;
case PacketType.FileTransferChunk:
{
Packets.FileTransferChunk packet = new Packets.FileTransferChunk();
packet.Unpack(data);
DownloadManager.Write(packet.ID, packet.FileChunk);
}
break;
default:
if (packetType.IsSyncEvent())
{
// Dispatch to main thread
Main.QueueAction(() => { SyncEvents.HandleEvent(packetType, data); return true; });
}
break;
}
}
private static void PedSync(Packets.PedSync packet)
{
SyncedPed c = EntityPool.GetPedByID(packet.ID);
if (c==null)
{
Main.Logger.Debug($"Creating character for incoming sync:{packet.ID}");
// Main.Logger.Debug($"Creating character for incoming sync:{packet.ID}");
EntityPool.ThreadSafe.Add(c=new SyncedPed(packet.ID));
}
PedDataFlags flags = packet.Flag;
@ -304,6 +347,9 @@ namespace RageCoop.Client
c.WeaponTint=packet.WeaponTint;
c.ModelHash=packet.ModelHash;
c.LastStateSynced = Main.Ticked;
c.BlipColor=packet.BlipColor;
c.BlipSprite=packet.BlipSprite;
c.BlipScale=packet.BlipScale;
}
private static void VehicleSync(Packets.VehicleSync packet)
{
@ -312,7 +358,7 @@ namespace RageCoop.Client
{
EntityPool.ThreadSafe.Add(v=new SyncedVehicle(packet.ID));
}
if (v.IsMine) { return; }
if (v.IsLocal) { return; }
v.ID= packet.ID;
v.Position=packet.Position;
v.Quaternion=packet.Quaternion;
@ -327,7 +373,7 @@ namespace RageCoop.Client
private static void VehicleStateSync(Packets.VehicleStateSync packet)
{
SyncedVehicle v = EntityPool.GetVehicleByID(packet.ID);
if (v==null||v.IsMine) { return; }
if (v==null||v.IsLocal) { return; }
v.ID= packet.ID;
v.OwnerID= packet.OwnerID;
v.DamageModel=packet.DamageModel;
@ -350,6 +396,7 @@ namespace RageCoop.Client
v.LockStatus=packet.LockStatus;
v.RadioStation=packet.RadioStation;
v.LicensePlate=packet.LicensePlate;
v.Livery=packet.Livery;
v.Flags=packet.Flag;
foreach (KeyValuePair<int, int> pair in packet.Passengers)
{
@ -367,7 +414,7 @@ namespace RageCoop.Client
if (p==null)
{
if (packet.Exploded) { return; }
Main.Logger.Debug($"Creating new projectile: {(WeaponHash)packet.WeaponHash}");
// Main.Logger.Debug($"Creating new projectile: {(WeaponHash)packet.WeaponHash}");
EntityPool.ThreadSafe.Add(p=new SyncedProjectile(packet.ID));
}
p.Position=packet.Position;

View File

@ -1,9 +1,4 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Lidgren.Network;
using Lidgren.Network;
using RageCoop.Core;
using GTA;
using GTA.Native;
@ -58,16 +53,32 @@ namespace RageCoop.Client
{
Ped p = c.MainPed;
var packet=new Packets.PedStateSync()
var packet = new Packets.PedStateSync()
{
ID = c.ID,
OwnerID=c.OwnerID,
Clothes=p.GetPedClothes(),
ModelHash=p.Model.Hash,
WeaponComponents=p.Weapons.Current.GetWeaponComponents(),
WeaponTint=(byte)Function.Call<int>(Hash.GET_PED_WEAPON_TINT_INDEX, p, p.Weapons.Current.Hash)
WeaponTint=(byte)Function.Call<int>(Hash.GET_PED_WEAPON_TINT_INDEX, p, p.Weapons.Current.Hash),
};
Blip b;
if (c.IsPlayer)
{
packet.BlipColor=Scripting.API.Config.BlipColor;
packet.BlipSprite=Scripting.API.Config.BlipSprite;
packet.BlipScale=Scripting.API.Config.BlipScale;
}
else if ((b = p.AttachedBlip) !=null)
{
packet.BlipColor=b.Color;
packet.BlipSprite=b.Sprite;
if (packet.BlipSprite==BlipSprite.PoliceOfficer || packet.BlipSprite==BlipSprite.PoliceOfficer2)
{
packet.BlipScale=0.5f;
}
}
Send(packet, ConnectionChannel.PedSync);
}
public static void SendVehicle(SyncedVehicle v)
@ -97,7 +108,7 @@ namespace RageCoop.Client
{
Function.Call<byte>(Hash.GET_VEHICLE_COLOURS, veh, &primaryColor, &secondaryColor);
}
var packet=new Packets.VehicleStateSync()
var packet = new Packets.VehicleStateSync()
{
ID =v.ID,
OwnerID = v.OwnerID,
@ -111,7 +122,8 @@ namespace RageCoop.Client
EngineHealth=veh.EngineHealth,
Passengers=veh.GetPassengers(),
LockStatus=veh.LockStatus,
LicensePlate=Function.Call<string>(Hash.GET_VEHICLE_NUMBER_PLATE_TEXT, veh)
LicensePlate=Function.Call<string>(Hash.GET_VEHICLE_NUMBER_PLATE_TEXT, veh),
Livery=Function.Call<int>(Hash.GET_VEHICLE_LIVERY, veh)
};
if (v.MainVehicle==Game.Player.LastVehicle)
{
@ -158,18 +170,6 @@ namespace RageCoop.Client
Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Chat);
Client.FlushSendQueue();
#if DEBUG
#endif
}
public static void SendDownloadFinish(int id)
{
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
new Packets.FileTransferComplete() { ID = id }.Pack(outgoingMessage);
Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.File);
Client.FlushSendQueue();
#if DEBUG
#endif
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RageCoop.Client
{
internal static class Statistics
{
public static int BytesDownPerSecond{ get; private set; }
public static int BytesUpPerSecond { get; private set; }
static Statistics()
{
Task.Run(() =>
{
while (true)
{
var bu=Networking.Client.Statistics.SentBytes;
var bd = Networking.Client.Statistics.ReceivedBytes;
Thread.Sleep(1000);
BytesUpPerSecond=Networking.Client.Statistics.SentBytes-bu;
BytesDownPerSecond=Networking.Client.Statistics.ReceivedBytes-bd;
}
});
}
}
}

View File

@ -80,17 +80,7 @@ namespace RageCoop.Client
var p = GetPlayer(packet.PedID);
if(p?.Character != null)
{
p.Character.DisplayNameTag=packet.Flags.HasConfigFlag(PlayerConfigFlags.ShowNameTag);
p.Character.DisplayBlip=packet.Flags.HasConfigFlag(PlayerConfigFlags.ShowBlip);
Main.Logger.Info($"blip color:{packet.BlipColor}");
// Need to be dispatched to script thread.
Main.QueueAction(() => {
if (p.Character.PedBlip!=null && p.Character.PedBlip.Exists() && p.Character.PedBlip.Color!=packet.BlipColor)
{
p.Character.PedBlip.Color=packet.BlipColor;
}
});
p.Latency= packet.Latency;
}
}
public static PlayerData GetPlayer(int id)
@ -102,7 +92,10 @@ namespace RageCoop.Client
public static PlayerData GetPlayer(SyncedPed p)
{
var player = GetPlayer(p.ID);
player.Character=p;
if (player!=null)
{
player.Character=p;
}
return player;
}
public static void RemovePlayer(int id)
@ -134,7 +127,8 @@ namespace RageCoop.Client
/// <summary>
/// Player Latency in second.
/// </summary>
public float Latency { get; internal set; }
public float Latency { get; set; }
public bool DisplayNameTag { get; set; } = true;
}
}

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\publish\win-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>True</PublishSingleFile>
<PublishReadyToRun>True</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<History>True|2022-06-27T04:41:08.2707244Z;True|2022-06-27T12:39:50.9236964+08:00;True|2022-06-27T12:37:12.3619963+08:00;True|2022-06-27T12:36:59.5077744+08:00;True|2022-06-27T12:35:49.2538484+08:00;</History>
</PropertyGroup>
</Project>

View File

@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<UseWindowsForms>True</UseWindowsForms>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<DocumentationFile></DocumentationFile>
<DebugType>portable</DebugType>
<AssemblyVersion>0.5.0</AssemblyVersion>
@ -22,11 +21,15 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<DefineConstants>SHVDN3</DefineConstants>
<TargetFrameworks>net48</TargetFrameworks>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<OutDir>..\bin\Debug\Client</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<OutDir>..\bin\Release\Client</OutDir>
</PropertyGroup>
<ItemGroup>
<Content Include="icon.ico" />
@ -40,11 +43,17 @@
</ItemGroup>
<ItemGroup>
<Reference Include="LemonUI.SHVDN3">
<HintPath>..\libs\LemonUI.SHVDN3.dll</HintPath>
</Reference>
<Reference Include="Lidgren.Network">
<HintPath>..\libs\Lidgren.Network.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>..\libs\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="ScriptHookVDotNet">
<HintPath>..\..\RageCoop.SHVDN\bin\Release\ScriptHookVDotNet.dll</HintPath>
<HintPath>..\libs\ScriptHookVDotNet.dll</HintPath>
</Reference>
<Reference Include="ScriptHookVDotNet3">
<HintPath>..\libs\ScriptHookVDotNet3.dll</HintPath>

View File

@ -3,13 +3,24 @@ using System.Collections.Generic;
using System;
using System.Linq;
using RageCoop.Core;
using System.Windows.Forms;
using GTA;
namespace RageCoop.Client.Scripting
{
/// <summary>
///
/// </summary>
public class CustomEventReceivedArgs : EventArgs
{
/// <summary>
/// The event hash
/// </summary>
public int Hash { get; set; }
public List<object> Args { get; set; }
/// <summary>
/// Supported types: byte, short, ushort, int, uint, long, ulong, float, bool, string, Vector3, Quaternion
/// </summary>
public object[] Args { get; set; }
}
/// <summary>
/// Provides vital functionality to interact with RAGECOOP
@ -32,7 +43,7 @@ namespace RageCoop.Client.Scripting
get { return Main.Settings.Username; }
set
{
if (IsOnServer || string.IsNullOrEmpty(value))
if (Networking.IsOnServer || string.IsNullOrEmpty(value))
{
return;
}
@ -43,6 +54,22 @@ namespace RageCoop.Client.Scripting
/// Enable automatic respawn for this player.
/// </summary>
public static bool EnableAutoRespawn { get; set; } = true;
/// <summary>
/// Get or set player's blip color
/// </summary>
public static BlipColor BlipColor { get; set; } = BlipColor.White;
/// <summary>
/// Get or set player's blip sprite
/// </summary>
public static BlipSprite BlipSprite { get; set; } = BlipSprite.Standard;
/// <summary>
/// Get or set scale of player's blip
/// </summary>
public static float BlipScale { get; set; } = 1;
}
/// <summary>
/// Base events for RageCoop
@ -50,7 +77,15 @@ namespace RageCoop.Client.Scripting
public static class Events
{
#region DELEGATES
/// <summary>
///
/// </summary>
public delegate void EmptyEvent();
/// <summary>
///
/// </summary>
/// <param name="hash"></param>
/// <param name="args"></param>
public delegate void CustomEvent(int hash, List<object> args);
#endregion
/// <summary>
@ -83,6 +118,15 @@ namespace RageCoop.Client.Scripting
/// </summary>
public static event EmptyEvent OnTick;
/// <summary>
/// This is equivalent of <see cref="Script.KeyDown"/>
/// </summary>
public static KeyEventHandler OnKeyDown;
/// <summary>
/// This is equivalent of <see cref="Script.KeyUp"/>
/// </summary>
public static KeyEventHandler OnKeyUp;
#region INVOKE
internal static void InvokeVehicleSpawned(SyncedVehicle v) { OnVehicleSpawned?.Invoke(null, v); }
@ -92,6 +136,10 @@ namespace RageCoop.Client.Scripting
internal static void InvokePlayerDied() { OnPlayerDied?.Invoke(); }
internal static void InvokeTick() { OnTick?.Invoke(); }
internal static void InvokeKeyDown(object s,KeyEventArgs e) { OnKeyDown?.Invoke(s,e); }
internal static void InvokeKeyUp(object s, KeyEventArgs e) { OnKeyUp?.Invoke(s, e); }
internal static void InvokeCustomEventReceived(Packets.CustomEvent p)
{
var args = new CustomEventReceivedArgs() { Hash=p.Hash, Args=p.Args};
@ -105,59 +153,9 @@ namespace RageCoop.Client.Scripting
}
}
#endregion
internal static void ClearHandlers()
{
OnPlayerDied=null;
OnTick=null;
OnPedDeleted=null;
OnPedSpawned=null;
OnVehicleDeleted=null;
OnVehicleSpawned=null;
}
}
#region FUNCTIONS
/// <summary>
/// Send a local chat message to this player
/// </summary>
/// <param name="from">Name of the sender</param>
/// <param name="message">The player's message</param>
public static void LocalChatMessage(string from, string message)
{
Main.MainChat.AddMessage(from, message);
}
/// <summary>
/// Get a <see cref="Core.Logger"/> that RAGECOOP is currently using.
/// </summary>
/// <returns></returns>
public static Logger GetLogger()
{
return Main.Logger;
}
/// <summary>
/// Queue an action to be executed on next tick.
/// </summary>
/// <param name="a"></param>
public static void QueueAction(Action a)
{
Main.QueueAction(a);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public static void Disconnect()
{
Networking.ToggleConnection(null);
}
/// <summary>
/// Check if the player is already on a server
/// </summary>
public static bool IsOnServer
{
get { return Networking.IsOnServer; }
}
#region PROPERTIES
/// <summary>
/// Get the local player's ID
@ -199,13 +197,54 @@ namespace RageCoop.Client.Scripting
{
get { return Main.CurrentVersion; }
}
/// <summary>
/// Send an event and data to the specified clients.
/// Get a <see cref="Core.Logger"/> that RAGECOOP is currently using.
/// </summary>
/// <returns></returns>
public static Logger Logger
{
get
{
return Main.Logger;
}
}
#endregion
#region FUNCTIONS
/// <summary>
/// Send a local chat message to this player
/// </summary>
/// <param name="from">Name of the sender</param>
/// <param name="message">The player's message</param>
public static void LocalChatMessage(string from, string message)
{
Main.MainChat.AddMessage(from, message);
}
/// <summary>
/// Queue an action to be executed on next tick.
/// </summary>
/// <param name="a"></param>
public static void QueueAction(Action a)
{
Main.QueueAction(a);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public static void Disconnect()
{
Networking.ToggleConnection(null);
}
/// <summary>
/// Send an event and data to the server.
/// </summary>
/// <param name="eventHash">An unique identifier of the event</param>
/// <param name="args">The objects conataing your data, supported types:
/// byte, short, ushort, int, uint, long, ulong, float, bool, string.</param>
public static void SendCustomEvent(int eventHash, List<object> args)
/// <param name="args">The objects conataing your data, see <see cref="CustomEventReceivedArgs"/> for a list of supported types</param>
public static void SendCustomEvent(int eventHash, params object[] args)
{
var p = new Packets.CustomEvent()
{
@ -222,10 +261,9 @@ namespace RageCoop.Client.Scripting
/// <param name="handler">An handler to be invoked when the event is received from the server. </param>
public static void RegisterCustomEventHandler(int hash, Action<CustomEventReceivedArgs> handler)
{
List<Action<CustomEventReceivedArgs>> handlers;
lock (CustomEventHandlers)
{
if (!CustomEventHandlers.TryGetValue(hash, out handlers))
if (!CustomEventHandlers.TryGetValue(hash, out List<Action<CustomEventReceivedArgs>> handlers))
{
CustomEventHandlers.Add(hash, handlers = new List<Action<CustomEventReceivedArgs>>());
}

View File

@ -1,31 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GTA.Native;
using GTA.Math;
using GTA;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace RageCoop.Client.Scripting
{
internal class BaseScript : ClientScript
{
private bool _isHost=false;
public override void OnStart()
{
API.Events.OnPedDeleted+=(s, p) => { API.SendCustomEvent(CustomEvents.OnPedDeleted, p.ID); };
API.Events.OnVehicleDeleted+=(s, p) => { API.SendCustomEvent(CustomEvents.OnVehicleDeleted, p.ID); };
API.RegisterCustomEventHandler(CustomEvents.SetAutoRespawn,SetAutoRespawn);
API.RegisterCustomEventHandler(CustomEvents.SetDisplayNameTag,SetDisplayNameTag);
API.RegisterCustomEventHandler(CustomEvents.NativeCall,NativeCall);
API.RegisterCustomEventHandler(CustomEvents.CleanUpWorld, (s) => Main.QueueAction(() => Main.CleanUpWorld()));
API.RegisterCustomEventHandler(CustomEvents.ServerPropSync, ServerObjectSync);
API.RegisterCustomEventHandler(CustomEvents.DeleteServerProp, DeleteServerProp);
API.RegisterCustomEventHandler(CustomEvents.DeleteEntity, DeleteEntity);
API.RegisterCustomEventHandler(CustomEvents.SetDisplayNameTag, SetNameTag);
API.RegisterCustomEventHandler(CustomEvents.ServerBlipSync, ServerBlipSync);
API.RegisterCustomEventHandler(CustomEvents.DeleteServerBlip, DeleteServerBlip);
API.RegisterCustomEventHandler(CustomEvents.CreateVehicle, CreateVehicle);
API.RegisterCustomEventHandler(CustomEvents.UpdatePedBlip, UpdatePedBlip);
API.RegisterCustomEventHandler(CustomEvents.IsHost, (e) => { _isHost=(bool)e.Args[0]; });
API.RegisterCustomEventHandler(CustomEvents.WeatherTimeSync, WeatherTimeSync);
Task.Run(() =>
{
while (true)
{
if (_isHost)
{
API.QueueAction(() =>
{
unsafe
{
var time = World.CurrentTimeOfDay;
int weather1 = default(int);
int weather2 = default(int);
float percent2 = default(float);
Function.Call(Hash._GET_WEATHER_TYPE_TRANSITION, &weather1, &weather2, &percent2);
API.SendCustomEvent(CustomEvents.WeatherTimeSync, time.Hours, time.Minutes, time.Seconds, weather1, weather2, percent2);
}
});
}
Thread.Sleep(1000);
}
});
}
private void WeatherTimeSync(CustomEventReceivedArgs e)
{
World.CurrentTimeOfDay=new TimeSpan((int)e.Args[0], (int)e.Args[1], (int)e.Args[2]);
Function.Call(Hash._SET_WEATHER_TYPE_TRANSITION, (int)e.Args[3], (int)e.Args[4], (float)e.Args[5]);
}
private void SetDisplayNameTag(CustomEventReceivedArgs e)
{
var p = PlayerList.GetPlayer((int)e.Args[0]);
if(p != null) { p.DisplayNameTag=(bool)e.Args[1]; }
}
private void UpdatePedBlip(CustomEventReceivedArgs e)
{
var p = Entity.FromHandle((int)e.Args[0]);
if (p == null) { return; }
if (p.Handle==Game.Player.Character.Handle)
{
API.Config.BlipColor=(BlipColor)(byte)e.Args[1];
API.Config.BlipSprite=(BlipSprite)(ushort)e.Args[2];
API.Config.BlipScale=(float)e.Args[3];
}
else
{
var b = p.AttachedBlip;
if (b == null) { b=p.AddBlip(); }
b.Color=(BlipColor)(byte)e.Args[1];
b.Sprite=(BlipSprite)(ushort)e.Args[2];
b.Scale=(float)e.Args[3];
}
}
private void CreateVehicle(CustomEventReceivedArgs e)
{
var vehicleModel = (Model)e.Args[1];
vehicleModel.Request(1000);
Vehicle veh= World.CreateVehicle(vehicleModel, (Vector3)e.Args[2], (float)e.Args[3]);
while (veh==null)
{
veh = World.CreateVehicle(vehicleModel, (Vector3)e.Args[2], (float)e.Args[3]);
Thread.Sleep(10);
}
veh.CanPretendOccupants=false;
var v = new SyncedVehicle()
{
ID=(int)e.Args[0],
MainVehicle=veh,
OwnerID=Main.LocalPlayerID,
};
EntityPool.Add(v);
}
private void DeleteServerBlip(CustomEventReceivedArgs e)
{
if (EntityPool.ServerBlips.TryGetValue((int)e.Args[0], out var blip))
{
EntityPool.ServerBlips.Remove((int)e.Args[0]);
blip?.Delete();
}
}
private void ServerBlipSync(CustomEventReceivedArgs obj)
{
int id= (int)obj.Args[0];
var sprite=(BlipSprite)(ushort)obj.Args[1];
var color = (BlipColor)(byte)obj.Args[2];
var scale=(float)obj.Args[3];
var pos=(Vector3)obj.Args[4];
int rot= (int)obj.Args[5];
var name=(string)obj.Args[6];
Blip blip;
if (!EntityPool.ServerBlips.TryGetValue(id, out blip))
{
EntityPool.ServerBlips.Add(id, blip=World.CreateBlip(pos));
}
blip.Sprite = sprite;
blip.Color = color;
blip.Scale = scale;
blip.Position = pos;
blip.Rotation = rot;
blip.Name = name;
}
private void DeleteEntity(CustomEventReceivedArgs e)
{
Entity.FromHandle((int)e.Args[0])?.Delete();
}
public override void OnStop()
{
}
private void SetNameTag(CustomEventReceivedArgs e)
{
var p =PlayerList.GetPlayer((int)e.Args[0]);
if(p!= null)
{
p.DisplayNameTag=(bool)e.Args[1];
}
}
private void SetAutoRespawn(CustomEventReceivedArgs args)
{
API.Config.EnableAutoRespawn=(bool)args.Args[0];
}
private void DeleteServerProp(CustomEventReceivedArgs e)
{
var id = (int)e.Args[0];
if (EntityPool.ServerProps.TryGetValue(id, out var prop))
{
EntityPool.ServerProps.Remove(id);
prop?.MainProp?.Delete();
}
}
private void ServerObjectSync(CustomEventReceivedArgs e)
{
SyncedProp prop;
var id = (int)e.Args[0];
lock (EntityPool.PropsLock)
{
if (!EntityPool.ServerProps.TryGetValue(id, out prop))
{
EntityPool.ServerProps.Add(id, prop=new SyncedProp(id));
}
}
prop.LastSynced=Main.Ticked+1;
prop.ModelHash= (Model)e.Args[1];
prop.Position=(Vector3)e.Args[2];
prop.Rotation=(Vector3)e.Args[3];
prop.Update();
}
private void NativeCall(CustomEventReceivedArgs e)
{
List<InputArgument> arguments = new List<InputArgument>();
@ -34,83 +196,80 @@ namespace RageCoop.Client.Scripting
TypeCode returnType=(TypeCode)ty;
i = returnType==TypeCode.Empty ? 1 : 2;
var hash = (Hash)e.Args[i++];
for(; i<e.Args.Count;i++)
for(; i<e.Args.Length;i++)
{
arguments.Add(GetInputArgument(e.Args[i]));
}
Main.QueueAction(() =>
if (returnType==TypeCode.Empty)
{
if (returnType==TypeCode.Empty)
{
Function.Call(hash, arguments.ToArray());
return;
}
var t = returnType;
int id = (int)e.Args[1];
Function.Call(hash, arguments.ToArray());
return;
}
var t = returnType;
int id = (int)e.Args[1];
switch (returnType)
{
case TypeCode.Boolean:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<bool>(hash, arguments.ToArray()) });
break;
case TypeCode.Byte:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<byte>(hash, arguments.ToArray()) });
break;
case TypeCode.Char:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<char>(hash, arguments.ToArray()) });
break;
switch (returnType)
{
case TypeCode.Boolean:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<bool>(hash, arguments.ToArray()) });
break;
case TypeCode.Byte:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<byte>(hash, arguments.ToArray()) });
break;
case TypeCode.Char:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<char>(hash, arguments.ToArray()) });
break;
case TypeCode.Single:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<float>(hash, arguments.ToArray()) });
break;
case TypeCode.Single:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<float>(hash, arguments.ToArray()) });
break;
case TypeCode.Double:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<double>(hash, arguments.ToArray()) });
break;
case TypeCode.Double:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<double>(hash, arguments.ToArray()) });
break;
case TypeCode.Int16:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<short>(hash, arguments.ToArray()) });
break;
case TypeCode.Int16:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<short>(hash, arguments.ToArray()) });
break;
case TypeCode.Int32:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<int>(hash, arguments.ToArray()) });
break;
case TypeCode.Int32:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<int>(hash, arguments.ToArray()) });
break;
case TypeCode.Int64:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<long>(hash, arguments.ToArray()) });
break;
case TypeCode.Int64:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<long>(hash, arguments.ToArray()) });
break;
case TypeCode.String:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<string>(hash, arguments.ToArray()) });
break;
case TypeCode.String:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<string>(hash, arguments.ToArray()) });
break;
case TypeCode.UInt16:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<ushort>(hash, arguments.ToArray()) });
break;
case TypeCode.UInt16:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<ushort>(hash, arguments.ToArray()) });
break;
case TypeCode.UInt32:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<uint>(hash, arguments.ToArray()) });
break;
case TypeCode.UInt32:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<uint>(hash, arguments.ToArray()) });
break;
case TypeCode.UInt64:
API.SendCustomEvent(CustomEvents.NativeResponse,
new List<object> { id, Function.Call<ulong>(hash, arguments.ToArray()) });
break;
}
});
case TypeCode.UInt64:
API.SendCustomEvent(CustomEvents.NativeResponse,
new object[] { id, Function.Call<ulong>(hash, arguments.ToArray()) });
break;
}
}
private InputArgument GetInputArgument(object obj)

View File

@ -1,19 +1,31 @@
namespace RageCoop.Client.Scripting
using RageCoop.Core.Scripting;
namespace RageCoop.Client.Scripting
{
/// <summary>
/// Inherit from this class, constructor will be called automatically, but other scripts might have yet been loaded, you should use <see cref="OnStart"/>. to initiate your script.
/// </summary>
public abstract class ClientScript:Core.Scripting.Scriptable
public abstract class ClientScript
{
/// <summary>
/// This method would be called from main thread shortly after all scripts have been loaded.
/// This method would be called from background thread, call <see cref="API.QueueAction(System.Action)"/> to dispatch it to main thread.
/// </summary>
public override abstract void OnStart();
public abstract void OnStart();
/// <summary>
/// This method would be called from main thread when the client disconnected from the server, you MUST terminate all background jobs/threads in this method.
/// This method would be called from background thread when the client disconnected from the server, you MUST terminate all background jobs/threads in this method.
/// </summary>
public override abstract void OnStop();
public abstract void OnStop();
/// <summary>
/// Get the <see cref="ResourceFile"/> instance where this script is loaded from.
/// </summary>
public ResourceFile CurrentFile { get; internal set; }
/// <summary>
/// Get the <see cref="ClientResource"/> that this script belongs to.
/// </summary>
public ClientResource CurrentResource { get; internal set; }
}
}

View File

@ -1,12 +1,42 @@
using System.IO;
using RageCoop.Core.Scripting;
using RageCoop.Core;
using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
namespace RageCoop.Client.Scripting
{
internal class Resources:ResourceLoader
/// <summary>
///
/// </summary>
public class ClientResource
{
public Resources() : base("RageCoop.Client.Scripting.ClientScript", Main.Logger) { }
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
public string DataFolder { get; internal set; }
/// <summary>
/// Get all <see cref="ClientScript"/> instance in this resource.
/// </summary>
public List<ClientScript> Scripts { get; internal set; } = new List<ClientScript>();
/// <summary>
/// Get the <see cref="ResourceFile"/> where this script is loaded from.
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new Dictionary<string, ResourceFile>();
}
internal class Resources
{
public Resources(){
BaseScriptType = "RageCoop.Client.Scripting.ClientScript";
Logger = Main.Logger;
}
private void StartAll()
{
lock (LoadedResources)
@ -15,7 +45,15 @@ namespace RageCoop.Client.Scripting
{
foreach (var s in d.Scripts)
{
Main.QueueAction(() => s.OnStart());
try
{
s.OnStart();
}
catch(Exception ex)
{
Logger.Error("Error occurred when starting script:"+s.GetType().FullName);
Logger?.Error(ex);
}
}
}
}
@ -28,39 +66,221 @@ namespace RageCoop.Client.Scripting
{
foreach (var s in d.Scripts)
{
Main.QueueAction(() => s.OnStop());
try
{
s.OnStop();
}
catch (Exception ex)
{
Logger.Error("Error occurred when stopping script:"+s.GetType().FullName);
Logger?.Error(ex);
}
}
}
}
}
/// <summary>
/// Load all resources from the server
/// </summary>
/// <param name="path">The path to the directory containing all resources to load.</param>
public void Load(string path)
public void Load(string path,string[] zips)
{
Unload();
foreach (var d in Directory.GetDirectories(path))
foreach (var zip in zips)
{
if(Path.GetFileName(d).ToLower() != "data")
{
Directory.Delete(d, true);
}
}
Directory.CreateDirectory(path);
foreach (var resource in Directory.GetDirectories(path))
{
if (Path.GetFileName(resource).ToLower()!="data") { continue; }
Logger?.Info($"Loading resource: {Path.GetFileName(resource)}");
LoadResource(resource,Path.Combine(path,"data"));
var zipPath=Path.Combine(path, zip);
Logger?.Info($"Loading resource: {Path.GetFileNameWithoutExtension(zip)}");
LoadResource(new ZipFile(zipPath),Path.Combine(path,"data"));
}
StartAll();
}
public void Unload()
{
StopAll();
if (LoadedResources.Count > 0)
{
API.QueueAction(()=>Util.Reload());
}
LoadedResources.Clear();
}
private List<string> ToIgnore = new List<string>
{
"RageCoop.Client.dll",
"RageCoop.Core.dll",
"RageCoop.Server.dll",
"ScriptHookVDotNet3.dll"
};
private List<ClientResource> LoadedResources = new List<ClientResource>();
private string BaseScriptType;
public Logger Logger { get; set; }
private void LoadResource(ZipFile file, string dataFolderRoot)
{
var r = new ClientResource()
{
Scripts = new List<ClientScript>(),
Name=Path.GetFileNameWithoutExtension(file.Name),
DataFolder=Path.Combine(dataFolderRoot, Path.GetFileNameWithoutExtension(file.Name))
};
Directory.CreateDirectory(r.DataFolder);
foreach (ZipEntry entry in file)
{
ResourceFile rFile;
r.Files.Add(entry.Name, rFile=new ResourceFile()
{
Name=entry.Name,
IsDirectory=entry.IsDirectory,
});
if (!entry.IsDirectory)
{
rFile.GetStream=() => { return file.GetInputStream(entry); };
if (entry.Name.EndsWith(".dll"))
{
var tmp = Path.GetTempFileName();
var f = File.OpenWrite(tmp);
rFile.GetStream().CopyTo(f);
f.Close();
LoadScriptsFromAssembly(rFile, tmp, r, false);
}
}
}
LoadedResources.Add(r);
file.Close();
}
private bool LoadScriptsFromAssembly(ResourceFile file, string path, ClientResource resource, bool shadowCopy = true)
{
lock (LoadedResources)
{
if (!IsManagedAssembly(path)) { return false; }
if (ToIgnore.Contains(file.Name)) { try { File.Delete(path); } catch { }; return false; }
Logger?.Debug($"Loading assembly {file.Name} ...");
Assembly assembly;
try
{
if (shadowCopy)
{
var temp = Path.GetTempFileName();
File.Copy(path, temp, true);
assembly = Assembly.LoadFrom(temp);
}
else
{
assembly = Assembly.LoadFrom(path);
}
}
catch (Exception ex)
{
Logger?.Error("Unable to load "+file.Name);
Logger?.Error(ex);
return false;
}
return LoadScriptsFromAssembly(file, assembly, path, resource);
}
}
private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly, string filename, ClientResource toload)
{
int count = 0;
try
{
// Find all script types in the assembly
foreach (var type in assembly.GetTypes().Where(x => IsSubclassOf(x, BaseScriptType)))
{
ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
{
try
{
// Invoke script constructor
var script = constructor.Invoke(null) as ClientScript;
// script.CurrentResource = toload;
script.CurrentFile=rfile;
script.CurrentResource=toload;
toload.Scripts.Add(script);
count++;
}
catch (Exception ex)
{
Logger?.Error($"Error occurred when loading script: {type.FullName}.");
Logger?.Error(ex);
}
}
else
{
Logger?.Error($"Script {type.FullName} has an invalid contructor.");
}
}
}
catch (ReflectionTypeLoadException ex)
{
Logger?.Error($"Failed to load assembly {rfile.Name}: ");
Logger?.Error(ex);
foreach (var e in ex.LoaderExceptions)
{
Logger?.Error(e);
}
return false;
}
Logger?.Info($"Loaded {count} script(s) in {rfile.Name}");
return count != 0;
}
private bool IsManagedAssembly(string filename)
{
try
{
using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
if (file.Length < 64)
return false;
using (BinaryReader bin = new BinaryReader(file))
{
// PE header starts at offset 0x3C (60). Its a 4 byte header.
file.Position = 0x3C;
uint offset = bin.ReadUInt32();
if (offset == 0)
offset = 0x80;
// Ensure there is at least enough room for the following structures:
// 24 byte PE Signature & Header
// 28 byte Standard Fields (24 bytes for PE32+)
// 68 byte NT Fields (88 bytes for PE32+)
// >= 128 byte Data Dictionary Table
if (offset > file.Length - 256)
return false;
// Check the PE signature. Should equal 'PE\0\0'.
file.Position = offset;
if (bin.ReadUInt32() != 0x00004550)
return false;
// Read PE magic number from Standard Fields to determine format.
file.Position += 20;
var peFormat = bin.ReadUInt16();
if (peFormat != 0x10b /* PE32 */ && peFormat != 0x20b /* PE32Plus */)
return false;
// Read the 15th Data Dictionary RVA field which contains the CLI header RVA.
// When this is non-zero then the file contains CLI data otherwise not.
file.Position = offset + (peFormat == 0x10b ? 232 : 248);
return bin.ReadUInt32() != 0;
}
}
}
catch
{
// This is likely not a valid assembly if any IO exceptions occur during reading
return false;
}
}
private bool IsSubclassOf(Type type, string baseTypeName)
{
for (Type t = type.BaseType; t != null; t = t.BaseType)
if (t.FullName == baseTypeName)
return true;
return false;
}
}
}

View File

@ -38,6 +38,7 @@ namespace RageCoop.Client
}
public void Regen()
{
ClientAes=Aes.Create();
ClientAes.GenerateKey();
ClientAes.GenerateIV();
}

View File

@ -11,7 +11,9 @@ namespace RageCoop.Client
/// Don't use it!
/// </summary>
public string Username { get; set; } = "Player";
/// <summary>
/// The password used to authenticate when connecting to a server.
/// </summary>
public string Password { get; set; } = "";
/// <summary>
/// Don't use it!
@ -20,7 +22,7 @@ namespace RageCoop.Client
/// <summary>
/// Don't use it!
/// </summary>
public string MasterServer { get; set; } = "[AUTO]";
public string MasterServer { get; set; } = "https://masterserver.ragecoop.online/";
/// <summary>
/// Don't use it!
/// </summary>
@ -56,5 +58,14 @@ namespace RageCoop.Client
/// The game won't spawn more NPC traffic if the limit is exceeded. -1 for unlimited (not recommended).
/// </summary>
public int WorldVehicleSoftLimit { get; set; } = 35;
/// <summary>
/// The game won't spawn more NPC traffic if the limit is exceeded. -1 for unlimited (not recommended).
/// </summary>
public int WorldPedSoftLimit { get; set; } = 50;
/// <summary>
/// The directory where log and resources downloaded from server will be placed.
/// </summary>
public string DataDirectory { get; set; } = "Scripts\\RageCoop\\Data";
}
}

View File

@ -8,13 +8,16 @@ using GTA.Math;
namespace RageCoop.Client
{
/// <summary>
///
/// </summary>
public abstract class SyncedEntity
{
/// <summary>
/// Indicates whether the current player is responsible for syncing this entity.
/// </summary>
public bool IsMine
public bool IsLocal
{
get
{
@ -22,19 +25,29 @@ namespace RageCoop.Client
}
}
/// <summary>
/// Network ID for this entity
/// </summary>
public int ID { get;internal set; }
/// <summary>
///
/// </summary>
public int OwnerID { get; internal set; }
/// <summary>
///
/// </summary>
public bool IsOutOfSync
{
get
{
return Main.Ticked-LastSynced>200;
return Main.Ticked-LastSynced>200 && ID!=0;
}
}
internal bool IsReady
{
get {return !(LastSynced==0||LastStateSynced==0);}
get {return (LastSynced>0||LastStateSynced==0);}
}
internal bool IsInvincible { get; set; } = false;
internal bool NeedUpdate
{
get { return LastSynced>LastUpdated; }
@ -54,6 +67,11 @@ namespace RageCoop.Client
public ulong LastUpdated { get; set; } = 0;
#endregion
/// <summary>
///
/// </summary>
internal protected bool _lastFrozen=false;
internal int ModelHash { get; set; }
internal Vector3 Position { get; set; }
internal Vector3 Rotation { get; set; }
internal Quaternion Quaternion { get; set; }

View File

@ -32,7 +32,7 @@ namespace RageCoop.Client
Function.Call(Hash._SET_PED_CAN_PLAY_INJURED_ANIMS, false);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableHurt, true);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableMelee, true);
// MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableMelee, true);
}
@ -46,10 +46,11 @@ namespace RageCoop.Client
}
#endregion
#region PLAYER -- ONLY
public string Username = "N/A";
internal Blip PedBlip = null;
internal bool DisplayBlip { get; set; } = true;
internal bool DisplayNameTag { get; set; } = true;
internal BlipColor BlipColor = (BlipColor)255;
internal BlipSprite BlipSprite = (BlipSprite)0;
internal float BlipScale=1;
internal PlayerData Player;
#endregion
/// <summary>
@ -68,12 +69,8 @@ namespace RageCoop.Client
private bool _lastRagdoll=false;
private ulong _lastRagdollTime=0;
private bool _lastInCover = false;
internal int ModelHash
{
get;set;
}
private byte[] _lastClothes = null;
public byte[] Clothes { get; set; }
internal byte[] Clothes { get; set; }
internal float Heading { get; set; }
internal Vector3 RotationVelocity { get; set; }
@ -86,23 +83,10 @@ namespace RageCoop.Client
{
if (IsPlayer)
{
if (Username=="N/A")
if (Player==null)
{
var p = PlayerList.GetPlayer(this);
if (p!=null)
{
Username=p.Username;
if (PedBlip!=null)
{
PedBlip.Name=Username;
}
}
}
if((!DisplayBlip) && (PedBlip!=null))
{
PedBlip.Delete();
PedBlip=null;
Player = PlayerList.GetPlayer(this);
return;
}
RenderNameTag();
}
@ -124,7 +108,27 @@ namespace RageCoop.Client
CreateCharacter();
return;
}
if (((byte)BlipColor==255) && (PedBlip!=null))
{
PedBlip.Delete();
PedBlip=null;
}
else if (((byte)BlipColor != 255) && PedBlip==null)
{
PedBlip=MainPed.AddBlip();
if (IsPlayer)
{
Main.Logger.Debug("blip:"+Player.Username);
PedBlip.Name=Player.Username;
}
PedBlip.Color=BlipColor;
PedBlip.Sprite=BlipSprite;
PedBlip.Scale=BlipScale;
}
// Need to update state
@ -141,6 +145,19 @@ namespace RageCoop.Client
{
SetClothes();
}
var b = MainPed.AttachedBlip;
if (b==null || b.Color!=BlipColor || b.Sprite!=BlipSprite)
{
PedBlip?.Delete();
PedBlip=MainPed.AddBlip();
PedBlip.Color=BlipColor;
PedBlip.Sprite =BlipSprite;
if (IsPlayer)
{
Main.Logger.Debug("blip:"+Player.Username);
b.Name=Player.Username;
}
}
CheckCurrentWeapon();
}
@ -188,12 +205,12 @@ namespace RageCoop.Client
private void RenderNameTag()
{
if (!DisplayNameTag || (MainPed==null) || !MainPed.IsVisible || !MainPed.IsInRange(Game.Player.Character.Position, 20f))
if (!Player.DisplayNameTag || (MainPed==null) || !MainPed.IsVisible || !MainPed.IsInRange(Game.Player.Character.Position, 20f))
{
return;
}
string renderText = IsOutOfSync ? "~r~AFK" : Username;
string renderText = IsOutOfSync ? "~r~AFK" : Player.Username;
Vector3 targetPos = MainPed.Bones[Bone.IKHead].Position + new Vector3(0, 0, 0.35f);
Function.Call(Hash.SET_DRAW_ORIGIN, targetPos.X, targetPos.Y, targetPos.Z, 0);
@ -245,14 +262,7 @@ namespace RageCoop.Client
}
Function.Call(Hash.SET_PED_CAN_EVASIVE_DIVE, MainPed.Handle, false);
Function.Call(Hash.SET_PED_DROPS_WEAPONS_WHEN_DEAD, MainPed.Handle, false);
Function.Call(Hash.SET_PED_CAN_BE_TARGETTED, MainPed.Handle, true);
Function.Call(Hash.SET_PED_CAN_BE_TARGETTED_BY_PLAYER, MainPed.Handle, Game.Player, true);
Function.Call(Hash.SET_PED_GET_OUT_UPSIDE_DOWN_VEHICLE, MainPed.Handle, false);
Function.Call(Hash.SET_CAN_ATTACK_FRIENDLY, MainPed.Handle, true, true);
Function.Call(Hash._SET_PED_CAN_PLAY_INJURED_ANIMS, false);
MainPed.BlockPermanentEvents = true;
MainPed.CanWrithe=false;
MainPed.CanBeDraggedOutOfVehicle = true;
@ -261,8 +271,16 @@ namespace RageCoop.Client
MainPed.IsFireProof=false;
MainPed.IsExplosionProof=false;
Function.Call(Hash.SET_PED_DROPS_WEAPONS_WHEN_DEAD, MainPed.Handle, false);
Function.Call(Hash.SET_PED_CAN_BE_TARGETTED, MainPed.Handle, true);
Function.Call(Hash.SET_PED_CAN_BE_TARGETTED_BY_PLAYER, MainPed.Handle, Game.Player, true);
Function.Call(Hash.SET_PED_GET_OUT_UPSIDE_DOWN_VEHICLE, MainPed.Handle, false);
Function.Call(Hash.SET_CAN_ATTACK_FRIENDLY, MainPed.Handle, true, true);
Function.Call(Hash._SET_PED_CAN_PLAY_INJURED_ANIMS, false);
Function.Call(Hash.SET_PED_CAN_EVASIVE_DIVE, MainPed.Handle, false);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DrownsInWater,false);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableMelee, true);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableHurt, true);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_DisableExplosionReactions, true);
MainPed.SetConfigFlag((int)PedConfigFlags.CPED_CONFIG_FLAG_AvoidTearGas, false);
@ -273,17 +291,9 @@ namespace RageCoop.Client
if (IsPlayer)
{
if (DisplayBlip)
{
// Add a new blip for the ped
PedBlip=MainPed.AddBlip();
MainPed.AttachedBlip.Color = BlipColor.White;
MainPed.AttachedBlip.Scale = 0.8f;
MainPed.AttachedBlip.Name =Username;
}
MainPed.IsInvincible=true;
}
if (IsInvincible) { MainPed.IsInvincible=true; }
// Add to EntityPool so this Character can be accessed by handle.
EntityPool.Add(this);

View File

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using GTA;
using GTA.Math;
using RageCoop.Core;
namespace RageCoop.Client
{
@ -37,17 +38,17 @@ namespace RageCoop.Client
IsValid=false;
}
ShooterID=shooter.ID;
IsMine=shooter.IsMine;
IsLocal=shooter.IsLocal;
}
}
public SyncedProjectile(int id)
{
ID= id;
IsMine=false;
IsLocal=false;
}
public bool IsValid { get; private set; } = true;
public new bool IsMine { get; private set; } = false;
public new bool IsLocal { get; private set; } = false;
public bool Exploded { get; set; } = false;
public Projectile MainProjectile { get; set; }
public int ShooterID { get; set; }

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GTA.Native;
using GTA;
namespace RageCoop.Client
{
/// <summary>
/// Synchronized prop, mostly owned by server
/// </summary>
public class SyncedProp : SyncedEntity
{
internal SyncedProp(int id)
{
ID= id;
}
/// <summary>
/// The real entity
/// </summary>
public Prop MainProp { get; set; }
internal new int OwnerID { get
{
// alwayse owned by server
return 0;
} }
internal override void Update()
{
if (!NeedUpdate) { return; }
if (MainProp== null || !MainProp.Exists())
{
MainProp=World.CreateProp(ModelHash,Position,Rotation,false,false);
MainProp.IsInvincible=true;
}
MainProp.Position=Position;
MainProp.Rotation=Rotation;
MainProp.SetFrozen(true);
LastUpdated=Main.Ticked;
}
}
}

View File

@ -11,6 +11,9 @@ using RageCoop.Core;
namespace RageCoop.Client
{
/// <summary>
/// A synchronized vehicle instance
/// </summary>
public class SyncedVehicle : SyncedEntity
{
@ -19,7 +22,7 @@ namespace RageCoop.Client
/// <summary>
/// Create a local entity (outgoing sync)
/// </summary>
/// <param name="p"></param>
/// <param name="v"></param>
internal SyncedVehicle(Vehicle v)
{
@ -54,7 +57,7 @@ namespace RageCoop.Client
private byte[] _lastVehicleColors = new byte[] { 0, 0 };
private Dictionary<int, int> _lastVehicleMods = new Dictionary<int, int>();
private byte _lastRadioIndex=255;
#endregion
#region -- CRITICAL STUFF --
@ -86,7 +89,6 @@ namespace RageCoop.Client
internal VehicleRoofState RoofState { get; set; }
internal bool SireneActive { get; set; }
internal VehicleDamageModel DamageModel { get; set; }
internal int ModelHash { get; set; }
internal byte[] Colors { get; set; }
internal Dictionary<int, int> Mods { get; set; }
internal bool IsDead { get; set; }
@ -98,11 +100,14 @@ namespace RageCoop.Client
internal Dictionary<VehicleSeat, SyncedPed> Passengers { get; set; }
internal byte RadioStation = 255;
internal string LicensePlate { get; set; }
internal int _lastLivery = -1;
internal int Livery { get; set; } = -1;
internal bool _checkSeat { get; set; } = true;
#endregion
internal override void Update()
{
#region -- INITIAL CHECK --
// Check if all data avalible
@ -122,14 +127,9 @@ namespace RageCoop.Client
{
MainVehicle.CustomSteeringAngle((float)(Math.PI / 180) * SteeringAngle);
}
if (MainVehicle.ThrottlePower!=ThrottlePower)
{
MainVehicle.ThrottlePower=ThrottlePower;
}
if (MainVehicle.BrakePower!=BrakePower)
{
MainVehicle.BrakePower=BrakePower;
}
MainVehicle.ThrottlePower=ThrottlePower;
MainVehicle.BrakePower=BrakePower;
if (MainVehicle.Position.DistanceTo(Position)<5)
{
MainVehicle.Velocity = Velocity+5*(Position+Velocity*SyncParameters.PositioinPredictionDefault - MainVehicle.Position);
@ -169,36 +169,33 @@ namespace RageCoop.Client
#region -- PASSENGER SYNC --
// check passengers (and driver).
var currentPassengers = MainVehicle.GetPassengers();
lock (Passengers)
if (_checkSeat)
{
for (int i = -1; i<MainVehicle.PassengerCapacity; i++)
{
VehicleSeat seat = (VehicleSeat)i;
if (Passengers.ContainsKey(seat))
{
SyncedPed c = Passengers[seat];
if (c?.ID==Main.LocalPlayerID && (RadioStation!=Function.Call<int>(Hash.GET_PLAYER_RADIO_STATION_INDEX)))
{
Util.SetPlayerRadioIndex(RadioStation);
}
if (c?.MainPed!=null&&(!currentPassengers.ContainsKey(i))&&(!c.MainPed.IsBeingJacked)&&(!c.MainPed.IsTaskActive(TaskType.CTaskExitVehicleSeat))) {
Passengers[seat].MainPed.SetIntoVehicle(MainVehicle, seat);
}
}
else if (!MainVehicle.IsSeatFree(seat))
var currentPassengers = MainVehicle.GetPassengers();
lock (Passengers)
{
for (int i = -1; i<MainVehicle.PassengerCapacity; i++)
{
if (seat==VehicleSeat.Driver &&MainVehicle.Driver.IsSittingInVehicle())
VehicleSeat seat = (VehicleSeat)i;
if (Passengers.ContainsKey(seat))
{
MainVehicle.Driver.Task.WarpOutOfVehicle(MainVehicle);
SyncedPed c = Passengers[seat];
if (c?.ID==Main.LocalPlayerID && (RadioStation!=Function.Call<int>(Hash.GET_PLAYER_RADIO_STATION_INDEX)))
{
Util.SetPlayerRadioIndex(RadioStation);
}
if (c?.MainPed!=null&&(!currentPassengers.ContainsKey(i))&&(!c.MainPed.IsBeingJacked)&&(!c.MainPed.IsTaskActive(TaskType.CTaskExitVehicleSeat)))
{
Passengers[seat].MainPed.SetIntoVehicle(MainVehicle, seat);
}
}
else
else if (!MainVehicle.IsSeatFree(seat))
{
var p = MainVehicle.Passengers.Where(x => x.SeatIndex==seat).FirstOrDefault();
if ((p!=null)&&p.IsSittingInVehicle())
var p = MainVehicle.Occupants.Where(x => x.SeatIndex==seat).FirstOrDefault();
if ((p!=null)&& !p.IsTaskActive(TaskType.CTaskLeaveAnyCar))
{
p.Task.WarpOutOfVehicle(MainVehicle);
}
@ -335,6 +332,12 @@ namespace RageCoop.Client
{
Function.Call(Hash.SET_VEHICLE_NUMBER_PLATE_TEXT,MainVehicle,LicensePlate);
}
if (_lastLivery!=Livery)
{
Function.Call(Hash.SET_VEHICLE_LIVERY, MainVehicle, Livery);
_lastLivery=Livery;
}
#endregion
}
LastUpdated=Main.Ticked;
@ -375,6 +378,7 @@ namespace RageCoop.Client
{
MainVehicle.RoofState=RoofState;
}
if (IsInvincible) { MainVehicle.IsInvincible=true; }
vehicleModel.MarkAsNoLongerNeeded();
}
#region -- PEDALING --

View File

@ -5,6 +5,7 @@ using GTA.Native;
using RageCoop.Core;
using System.Collections.Generic;
using System.Linq;
using RageCoop.Client.Scripting;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
@ -14,6 +15,7 @@ namespace RageCoop.Client
{
internal class EntityPool
{
private static bool _trafficSpawning=true;
public static object PedsLock = new object();
private static Dictionary<int, SyncedPed> ID_Peds = new Dictionary<int, SyncedPed>();
public static int CharactersCount { get { return ID_Peds.Count; } }
@ -35,6 +37,12 @@ namespace RageCoop.Client
private static Dictionary<int, SyncedProjectile> ID_Projectiles = new Dictionary<int, SyncedProjectile>();
private static Dictionary<int, SyncedProjectile> Handle_Projectiles = new Dictionary<int, SyncedProjectile>();
public static object PropsLock=new object();
public static Dictionary<int,SyncedProp> ServerProps=new Dictionary<int,SyncedProp>();
public static object BlipsLock = new object();
public static Dictionary<int, Blip> ServerBlips = new Dictionary<int, Blip>();
public static void Cleanup(bool keepPlayer=true,bool keepMine=true)
{
@ -64,6 +72,21 @@ namespace RageCoop.Client
}
ID_Projectiles.Clear();
Handle_Projectiles.Clear();
foreach(var p in ServerProps.Values)
{
p?.MainProp?.Delete();
}
ServerProps.Clear();
foreach(var b in ServerBlips.Values)
{
if (b.Exists())
{
b.Delete();
}
}
ServerBlips.Clear();
}
#region PEDS
@ -112,7 +135,7 @@ namespace RageCoop.Client
SyncedPed c = new SyncedPed(p);
Main.LocalPlayerID=c.OwnerID=c.ID;
Add(c);
Main.Logger.Debug($"My player ID is:{c.ID}");
Main.Logger.Debug($"Local player ID is:{c.ID}");
PlayerList.SetPlayer(c.ID, Main.Settings.Username );
return true;
}
@ -137,6 +160,10 @@ namespace RageCoop.Client
{
Handle_Peds.Add(c.MainPed.Handle, c);
}
if (c.IsLocal)
{
API.Events.InvokePedSpawned(c);
}
}
public static void RemovePed(int id,string reason="Cleanup")
{
@ -150,7 +177,7 @@ namespace RageCoop.Client
{
Handle_Peds.Remove(p.Handle);
}
Main.Logger.Debug($"Removing ped {c.ID}. Reason:{reason}");
// Main.Logger.Debug($"Removing ped {c.ID}. Reason:{reason}");
p.AttachedBlip?.Delete();
p.Kill();
p.MarkAsNoLongerNeeded();
@ -159,6 +186,10 @@ namespace RageCoop.Client
c.PedBlip?.Delete();
c.ParachuteProp?.Delete();
ID_Peds.Remove(id);
if (c.IsLocal)
{
API.Events.InvokePedDeleted(c);
}
}
}
#endregion
@ -195,6 +226,10 @@ namespace RageCoop.Client
{
Handle_Vehicles.Add(v.MainVehicle.Handle, v);
}
if (v.IsLocal)
{
API.Events.InvokeVehicleSpawned(v);
}
}
public static void RemoveVehicle(int id,string reason = "Cleanup")
{
@ -208,12 +243,13 @@ namespace RageCoop.Client
{
Handle_Vehicles.Remove(veh.Handle);
}
Main.Logger.Debug($"Removing vehicle {v.ID}. Reason:{reason}");
// Main.Logger.Debug($"Removing vehicle {v.ID}. Reason:{reason}");
veh.AttachedBlip?.Delete();
veh.MarkAsNoLongerNeeded();
veh.Delete();
}
ID_Vehicles.Remove(id);
if (v.IsLocal) { API.Events.InvokeVehicleDeleted(v); }
}
}
@ -295,19 +331,24 @@ namespace RageCoop.Client
allPeds = World.GetAllPeds();
allVehicles=World.GetAllVehicles();
allProjectiles=World.GetAllProjectiles();
vehStatesPerFrame=allVehicles.Length*5/(int)Game.FPS+1;
pedStatesPerFrame=allPeds.Length*5/(int)Game.FPS+1;
vehStatesPerFrame=allVehicles.Length*2/(int)Game.FPS+1;
pedStatesPerFrame=allPeds.Length*2/(int)Game.FPS+1;
if (Main.Settings.WorldVehicleSoftLimit>-1)
if (Main.Ticked%50==0)
{
if (Main.Ticked%100==0) { if (allVehicles.Length>Main.Settings.WorldVehicleSoftLimit) { SetBudget(0); } else { SetBudget(1); } }
bool flag1 = allVehicles.Length>Main.Settings.WorldVehicleSoftLimit && Main.Settings.WorldVehicleSoftLimit>-1;
bool flag2 = allPeds.Length>Main.Settings.WorldPedSoftLimit && Main.Settings.WorldPedSoftLimit>-1;
if ((flag1||flag2) && _trafficSpawning)
{ SetBudget(0); _trafficSpawning=false; }
else if(!_trafficSpawning)
{ SetBudget(1); _trafficSpawning=true; }
}
#if BENCHMARK
Debug.TimeStamps[TimeStamp.GetAllEntities]=PerfCounter.ElapsedTicks;
#endif
#endif
lock (ProjectilesLock)
{
@ -324,12 +365,12 @@ namespace RageCoop.Client
{
// Outgoing sync
if (p.IsMine)
if (p.IsLocal)
{
if (p.MainProjectile.AttachedEntity==null)
{
/// Prevent projectiles from exploding next to vehicle
// Prevent projectiles from exploding next to vehicle
if (WeaponUtil.VehicleProjectileWeapons.Contains((VehicleWeaponHash)p.MainProjectile.WeaponHash))
{
if (p.MainProjectile.WeaponHash!=(WeaponHash)VehicleWeaponHash.Tank && p.Origin.DistanceTo(p.MainProjectile.Position)<2)
@ -369,7 +410,7 @@ namespace RageCoop.Client
SyncedPed c = EntityPool.GetPedByHandle(p.Handle);
if (c==null && (p!=Game.Player.Character))
{
Main.Logger.Trace($"Creating SyncEntity for ped, handle:{p.Handle}");
// Main.Logger.Trace($"Creating SyncEntity for ped, handle:{p.Handle}");
c=new SyncedPed(p);
EntityPool.Add(c);
@ -397,7 +438,7 @@ namespace RageCoop.Client
}
// Outgoing sync
if (c.IsMine)
if (c.IsLocal)
{
#if BENCHMARK
var start = PerfCounter2.ElapsedTicks;
@ -448,7 +489,7 @@ namespace RageCoop.Client
{
if (!Handle_Vehicles.ContainsKey(veh.Handle))
{
Main.Logger.Debug($"Creating SyncEntity for vehicle, handle:{veh.Handle}");
// Main.Logger.Debug($"Creating SyncEntity for vehicle, handle:{veh.Handle}");
EntityPool.Add(new SyncedVehicle(veh));
@ -475,8 +516,9 @@ namespace RageCoop.Client
}
// Outgoing sync
if (v.IsMine)
if (v.IsLocal)
{
if (!v.MainVehicle.IsVisible) { continue; }
SyncEvents.Check(v);
Networking.SendVehicle(v);
@ -501,6 +543,7 @@ namespace RageCoop.Client
}
}
#if BENCHMARK
Debug.TimeStamps[TimeStamp.VehicleTotal]=PerfCounter.ElapsedTicks;
#endif

View File

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GTA;
/*
namespace RageCoop.Client.Sync
{
internal class VehicleStateThread : Script
{
public VehicleStateThread()
{
Tick+=OnTick;
}
int current;
int toSendPerFrame;
int sent;
private void OnTick(object sender, EventArgs e)
{
toSendPerFrame=EntityPool.allVehicles.Length*5/(int)Game.FPS+1;
if (!Networking.IsOnServer) { return; }
for(; sent<toSendPerFrame; sent++)
{
if (current>=EntityPool.allVehicles.Length)
{
current=0;
}
Networking.SendVehicleState(EntityPool.allVehicles[current])
}
}
}
}
*/

View File

@ -58,7 +58,7 @@ namespace RageCoop.Client {
public static void TriggerBulletShot(uint hash,SyncedPed owner,Vector3 impactPosition)
{
Main.Logger.Trace($"bullet shot:{(WeaponHash)hash}");
// Main.Logger.Trace($"bullet shot:{(WeaponHash)hash}");
var start = owner.MainPed.GetMuzzlePosition();
@ -112,12 +112,19 @@ namespace RageCoop.Client {
private static void HandleLeaveVehicle(Packets.LeaveVehicle p)
{
var ped = EntityPool.GetPedByID(p.ID);
var veh = ped.MainPed.CurrentVehicle.GetSyncEntity();
veh._checkSeat=false;
var flag = LeaveVehicleFlags.None;
if (ped.MainPed?.CurrentVehicle==null) { return; }
// Bail out
if (ped.MainPed.CurrentVehicle.Speed>5) { flag|=LeaveVehicleFlags.BailOut;}
ped.PauseUpdate((ulong)Game.FPS*2);
// ped.PauseUpdate((ulong)Game.FPS*2);
ped.MainPed.Task.LeaveVehicle(flag) ;
Task.Run(() =>
{
Thread.Sleep(1000);
veh._checkSeat=true;
});
}
private static void HandlePedKilled(Packets.PedKilled p)
{
@ -169,6 +176,16 @@ namespace RageCoop.Client {
weaponHash=1176362416;
break;
// Tampa3, not working for some reason
case 3670375085:
weaponHash=1176362416;
break;
// Ruiner2, not working for some reason
case 50118905:
weaponHash=1176362416;
break;
// SAVAGE
case 1638077257:
weaponHash=(uint)VehicleWeaponHash.PlayerLazer;
@ -211,18 +228,18 @@ namespace RageCoop.Client {
}
}
}
public static void HandleEvent(PacketTypes type,byte[] data)
public static void HandleEvent(PacketType type,byte[] data)
{
switch (type)
{
case PacketTypes.BulletShot:
case PacketType.BulletShot:
{
Packets.BulletShot p = new Packets.BulletShot();
p.Unpack(data);
HandleBulletShot(p.StartPosition, p.EndPosition, p.WeaponHash, p.OwnerID);
break;
}
case PacketTypes.EnteringVehicle:
case PacketType.EnteringVehicle:
{
Packets.EnteringVehicle p = new Packets.EnteringVehicle();
p.Unpack(data);
@ -231,35 +248,35 @@ namespace RageCoop.Client {
}
break;
case PacketTypes.LeaveVehicle:
case PacketType.LeaveVehicle:
{
Packets.LeaveVehicle packet = new Packets.LeaveVehicle();
packet.Unpack(data);
HandleLeaveVehicle(packet);
}
break;
case PacketTypes.OwnerChanged:
case PacketType.OwnerChanged:
{
Packets.OwnerChanged packet = new Packets.OwnerChanged();
packet.Unpack(data);
HandleOwnerChanged(packet);
}
break;
case PacketTypes.PedKilled:
case PacketType.PedKilled:
{
var packet = new Packets.PedKilled();
packet.Unpack(data);
HandlePedKilled(packet);
}
break;
case PacketTypes.EnteredVehicle:
case PacketType.EnteredVehicle:
{
var packet = new Packets.EnteredVehicle();
packet.Unpack(data);
HandleEnteredVehicle(packet.PedID,packet.VehicleID,(VehicleSeat)packet.VehicleSeat);
break;
}
case PacketTypes.NozzleTransform:
case PacketType.NozzleTransform:
{
var packet = new Packets.NozzleTransform();
packet.Unpack(data);

View File

@ -139,6 +139,7 @@ namespace RageCoop.Client
{
flags |= PedDataFlags.IsInStealthMode;
}
return flags;
}
@ -362,7 +363,8 @@ namespace RageCoop.Client
|| (VehicleHash)veh.Model.Hash == VehicleHash.Cerberus2
|| (VehicleHash)veh.Model.Hash == VehicleHash.Cerberus3;
case 0:
return (VehicleHash)veh.Model.Hash == VehicleHash.Apc;
return (VehicleHash)veh.Model.Hash == VehicleHash.Apc
|| (VehicleHash)veh.Model.Hash==VehicleHash.Dune3;
case 1:
return (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie
|| (VehicleHash)veh.Model.Hash == VehicleHash.Valkyrie2

View File

@ -2,9 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using GTA.Math;
using GTA;
using RageCoop.Core;
using GTA.Native;
using System.IO;
using System.Xml.Serialization;
@ -89,11 +90,12 @@ namespace RageCoop.Client
#endregion
public static string SettingsPath= "Scripts\\RageCoop\\Data\\RageCoop.Client.Settings.xml";
public static Settings ReadSettings()
{
XmlSerializer ser = new XmlSerializer(typeof(Settings));
string path = $"RageCoop\\RageCoop.Client.Settings.xml";
string path = SettingsPath;
Directory.CreateDirectory(Directory.GetParent(path).FullName);
Settings settings = null;
@ -123,7 +125,7 @@ namespace RageCoop.Client
{
try
{
string path = $"RageCoop\\RageCoop.Client.Settings.xml";
string path = SettingsPath ;
Directory.CreateDirectory(Directory.GetParent(path).FullName);
using (FileStream stream = new FileStream(path, File.Exists(path) ? FileMode.Truncate : FileMode.Create, FileAccess.ReadWrite))
@ -138,8 +140,6 @@ namespace RageCoop.Client
}
}
[DllImport("kernel32.dll")]
public static extern ulong GetTickCount64();
public static Vector3 PredictPosition(this Entity e, bool applyDefault = true)
{
return e.Position+e.Velocity*((applyDefault ? SyncParameters.PositioinPredictionDefault : 0)+Networking.Latency);
@ -173,6 +173,10 @@ namespace RageCoop.Client
Function.Call(Hash.STOP_ENTITY_FIRE, e.Handle);
}
}
public static void SetFrozen(this Entity e,bool toggle)
{
Function.Call(Hash.FREEZE_ENTITY_POSITION, e, toggle);
}
public static SyncedPed GetSyncEntity(this Ped p)
{
@ -204,5 +208,44 @@ namespace RageCoop.Client
return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
}
const UInt32 WM_KEYDOWN = 0x0100;
public static void Reload()
{
string reloadKey="None";
var lines = File.ReadAllLines("ScriptHookVDotNet.ini");
foreach (var l in lines)
{
var ss = l.Split('=');
if(ss.Length > 0 && ss[0]=="ReloadKey")
{
reloadKey = ss[1];
}
}
var lineList = lines.ToList();
if (reloadKey=="None")
{
foreach (var l in lines)
{
var ss = l.Split('=');
if (ss.Length > 0 && ss[0]=="ReloadKey")
{
reloadKey = ss[1];
lineList.Remove(l);
}
}
lineList.Add("ReloadKey=Insert");
File.WriteAllLines("ScriptHookVDotNet.ini",lineList.ToArray());
}
Keys key = (Keys)Enum.Parse(typeof(Keys), reloadKey, true);
PostMessage(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle, WM_KEYDOWN, (int)key, 0);
}
[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);
[DllImport("kernel32.dll")]
public static extern ulong GetTickCount64();
}
}

View File

@ -81,24 +81,6 @@ namespace RageCoop.Client
return flags;
}
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>();
@ -157,6 +139,66 @@ namespace RageCoop.Client
RightHeadLightBroken = (byte)(veh.IsRightHeadLightBroken ? 1 : 0)
};
}
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)
{
if (!door.IsBroken)
{
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;
}
public static Dictionary<int, int> GetPassengers(this Vehicle veh)
{
@ -225,62 +267,6 @@ namespace RageCoop.Client
{
Function.Call(Hash.SET_VEHICLE_FLIGHT_NOZZLE_POSITION, plane, ratio);
}
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
}

View File

@ -21,6 +21,23 @@ namespace RageCoop.Client
}
internal static class WeaponUtil
{
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 Vector3 GetMuzzlePosition(this Ped p)
{
var w = p.Weapons.CurrentWeaponObject;
@ -66,6 +83,46 @@ namespace RageCoop.Client
int i;
switch (v.Model.Hash)
{
// JB7002
case 394110044:
i=BulletsShot%2==0 ? 54 : 53;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// DOMINATOR5
case -1375060657:
i=BulletsShot%2==0 ? 35 : 36;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// IMPALER3
case -1924800695:
i=BulletsShot%2==0 ? 75 : 76;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// IMPERATOR2
case 1637620610:
i=BulletsShot%2==0 ? 97 : 99;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// SLAMVAN5
case 373261600:
i=BulletsShot%2==0 ? 51 : 53;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// RUINER2
case 941494461:
i=BulletsShot%2==0 ? 65 : 66;
return new MuzzleInfo(v.Bones[i].Position, v.Bones[i].ForwardVector);
// TAMPA3
case -1210451983:
return new MuzzleInfo(v.Bones[87].Position, v.Bones[87].ForwardVector);
// SCRAMJET
case -638562243:
i=BulletsShot%2==0 ? 44 : 45;

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