diff --git a/LemonUI/Phone/CallBehavior.cs b/LemonUI/Phone/CallBehavior.cs
new file mode 100644
index 00000000..4f57187b
--- /dev/null
+++ b/LemonUI/Phone/CallBehavior.cs
@@ -0,0 +1,17 @@
+namespace LemonUI.Phone
+{
+ ///
+ /// The behavior of a call after is initiated by the user.
+ ///
+ public enum CallBehavior
+ {
+ ///
+ /// The contact being called is busy.
+ ///
+ Busy = 0,
+ ///
+ /// The contact is available and will answer.
+ ///
+ Available = 1
+ }
+}
diff --git a/LemonUI/Phone/CallOrigin.cs b/LemonUI/Phone/CallOrigin.cs
new file mode 100644
index 00000000..7c73913b
--- /dev/null
+++ b/LemonUI/Phone/CallOrigin.cs
@@ -0,0 +1,17 @@
+namespace LemonUI.Phone
+{
+ ///
+ /// The origin of the call.
+ ///
+ public enum CallOrigin
+ {
+ ///
+ /// The Contact called the Player.
+ ///
+ Contact = 0,
+ ///
+ /// The Player called the Contact.
+ ///
+ Player = 1
+ }
+}
diff --git a/LemonUI/Phone/CallStatus.cs b/LemonUI/Phone/CallStatus.cs
new file mode 100644
index 00000000..8143fb49
--- /dev/null
+++ b/LemonUI/Phone/CallStatus.cs
@@ -0,0 +1,33 @@
+namespace LemonUI.Phone
+{
+ ///
+ /// The status of the current call.
+ ///
+ internal enum CallStatus
+ {
+ ///
+ /// The contact is not being called.
+ ///
+ Inactive = -1,
+ ///
+ /// The contact has been triggered and is currently idling.
+ ///
+ Idle = 0,
+ ///
+ /// The contact is currently being called.
+ ///
+ Calling = 1,
+ ///
+ /// The contact has been called and has answered as Busy or Connected.
+ ///
+ Called = 2,
+ ///
+ /// The contact is busy.
+ ///
+ Busy = 3,
+ ///
+ /// The contact has been connected.
+ ///
+ Connected = 4
+ }
+}
diff --git a/LemonUI/Phone/CalledEventArgs.cs b/LemonUI/Phone/CalledEventArgs.cs
new file mode 100644
index 00000000..9e5bb4af
--- /dev/null
+++ b/LemonUI/Phone/CalledEventArgs.cs
@@ -0,0 +1,38 @@
+#if SHVDN3
+using System;
+
+namespace LemonUI.Phone
+{
+ ///
+ /// Represents the call process when the player initiates a call
+ ///
+ public class CalledEventArgs : EventArgs
+ {
+ #region Properties
+
+ ///
+ /// The contact that was called by the player.
+ ///
+ public PhoneContact Contact { get; }
+ ///
+ /// The behavior of the call after the wait.
+ ///
+ public CallBehavior Behavior { get; set; } = CallBehavior.Busy;
+ ///
+ /// The time to wait before the is applied.
+ ///
+ public int Wait { get; set; } = 3000;
+
+ #endregion
+
+ #region Constructors
+
+ internal CalledEventArgs(PhoneContact contact)
+ {
+ Contact = contact;
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/LemonUI/Phone/CalledEventHandler.cs b/LemonUI/Phone/CalledEventHandler.cs
new file mode 100644
index 00000000..5e0fcd19
--- /dev/null
+++ b/LemonUI/Phone/CalledEventHandler.cs
@@ -0,0 +1,11 @@
+#if SHVDN3
+namespace LemonUI.Phone
+{
+ ///
+ /// Represents the method that is called when the value on a grid is changed.
+ ///
+ /// The source of the event.
+ /// An with the call information.
+ public delegate void CalledEventHandler(object sender, CalledEventArgs e);
+}
+#endif
diff --git a/LemonUI/Phone/ConnectedEventArgs.cs b/LemonUI/Phone/ConnectedEventArgs.cs
new file mode 100644
index 00000000..343991e4
--- /dev/null
+++ b/LemonUI/Phone/ConnectedEventArgs.cs
@@ -0,0 +1,34 @@
+#if SHVDN3
+using System;
+
+namespace LemonUI.Phone
+{
+ ///
+ /// Represents the call process when the player answers the phone and the call is connected.
+ ///
+ public class ConnectedEventArgs : EventArgs
+ {
+ #region Properties
+
+ ///
+ /// The contact that was called by the player.
+ ///
+ public PhoneContact Contact { get; }
+ ///
+ /// After how much time should the call be disconnected automatically.
+ ///
+ public int DisconnectAfter { get; set; }
+
+ #endregion
+
+ #region Constructors
+
+ internal ConnectedEventArgs(PhoneContact contact)
+ {
+ Contact = contact;
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/LemonUI/Phone/ConnectedEventHandler.cs b/LemonUI/Phone/ConnectedEventHandler.cs
new file mode 100644
index 00000000..0bfcbc84
--- /dev/null
+++ b/LemonUI/Phone/ConnectedEventHandler.cs
@@ -0,0 +1,11 @@
+#if SHVDN3
+namespace LemonUI.Phone
+{
+ ///
+ /// Represents the method that is called when the phone call is connected.
+ ///
+ /// The source of the event.
+ /// An with the connection information.
+ public delegate void ConnectedEventHandler(object sender, ConnectedEventArgs e);
+}
+#endif
diff --git a/LemonUI/Phone/Phone.cs b/LemonUI/Phone/Phone.cs
new file mode 100644
index 00000000..70224a3d
--- /dev/null
+++ b/LemonUI/Phone/Phone.cs
@@ -0,0 +1,135 @@
+#if SHVDN3
+using GTA;
+using GTA.Native;
+using System;
+using System.Collections.Generic;
+using LemonUI.Scaleform;
+
+namespace LemonUI.Phone
+{
+ ///
+ /// Class used to manage the phone.
+ ///
+ public class PhoneManager : Script
+ {
+ #region Fields
+
+ private static readonly List contacts = new List();
+ private static int total = -1;
+ private static Scaleform.Phone badger = new Scaleform.Phone(PhoneType.Badger);
+ private static Scaleform.Phone facade = new Scaleform.Phone(PhoneType.Facade);
+ private static Scaleform.Phone ifruit = new Scaleform.Phone(PhoneType.IFruit);
+
+ #endregion
+
+ #region Properties
+
+ private static Scaleform.Phone CurrentPhone
+ {
+ get
+ {
+ switch ((uint)Game.Player.Character.Model.Hash)
+ {
+ case 0x9B22DBAF: // Franklin
+ return badger;
+ case 0x0D7114C9: // Michael
+ return ifruit;
+ case 0x9B810FA2: // Trevor
+ return facade;
+ default:
+ return ifruit;
+ }
+ }
+ }
+ ///
+ /// If the phone's contact are open on the screen.
+ ///
+ public static bool AreContactsOpen => Function.Call(Hash._GET_NUMBER_OF_REFERENCES_OF_SCRIPT_WITH_NAME_HASH, Game.GenerateHash("appcontacts"));
+ ///
+ /// The contact currently being called.
+ ///
+ public static PhoneContact Current { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ public PhoneManager()
+ {
+ Tick += Process;
+ }
+
+ #endregion
+
+ #region Functions
+
+ private static void Process(object sender, EventArgs e)
+ {
+ Scaleform.Phone currentPhone = CurrentPhone;
+
+ if (Current != null)
+ {
+ Current.Process(currentPhone);
+
+ if (!Current.IsActive)
+ {
+ Current = null;
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ if (AreContactsOpen)
+ {
+ if (total == -1)
+ {
+ total = currentPhone.Total;
+ }
+
+ int index = currentPhone.Index;
+
+ if (Game.IsControlPressed(Control.PhoneSelect) && index >= total)
+ {
+ Function.Call(Hash.TERMINATE_ALL_SCRIPTS_WITH_THIS_NAME, "appcontacts");
+ Current = contacts[index - total];
+ Current.Call(currentPhone);
+ return;
+ }
+
+ for (int i = 0; i < contacts.Count; i++)
+ {
+ PhoneContact contact = contacts[i];
+ currentPhone.AddContactAt(total + i, contact.Name, contact.Icon);
+ }
+ }
+ else
+ {
+ total = -1;
+ }
+ }
+ ///
+ /// Adds a new contact to the phone.
+ ///
+ /// The contact to add.
+ public static void Add(PhoneContact contact)
+ {
+ if (contacts.Contains(contact))
+ {
+ return;
+ }
+
+ contacts.Add(contact);
+ }
+ ///
+ /// Removes a contact from the phone.
+ ///
+ /// The contact to remove.
+ /// if the contact was removed, otherwise.
+ public static bool Remove(PhoneContact contact) => contacts.Remove(contact);
+
+ #endregion
+ }
+}
+#endif
diff --git a/LemonUI/Phone/PhoneContact.cs b/LemonUI/Phone/PhoneContact.cs
new file mode 100644
index 00000000..a510e883
--- /dev/null
+++ b/LemonUI/Phone/PhoneContact.cs
@@ -0,0 +1,192 @@
+#if SHVDN3
+using GTA;
+using System;
+using GTA.Native;
+
+namespace LemonUI.Phone
+{
+ ///
+ /// A phone contact.
+ ///
+ public class PhoneContact
+ {
+ #region Fields
+
+ private const string dialingLabel = "CELL_211";
+ private const string busyLabel = "CELL_220";
+ private const string connectedLabel = "CELL_219";
+ private static readonly Sound busySound = new Sound("Phone_SoundSet_Default", "Remote_Engaged");
+ private static readonly Sound callingSound = new Sound("Phone_SoundSet_Default", "Dial_and_Remote_Ring");
+ private CalledEventArgs called;
+ private string label = dialingLabel;
+ private CallStatus status = CallStatus.Inactive;
+ private int waitUntil = -1;
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// Event triggered when the contact is called by the player.
+ ///
+ public event CalledEventHandler Called;
+ ///
+ /// Event triggered when the call is answered by the player.
+ ///
+ ///
+ /// This event will trigger both, when the player calls the contact and when the contact calls the player.
+ ///
+ public event ConnectedEventHandler Connected;
+ ///
+ /// Event triggered when the call finishes naturally, without player input.
+ ///
+ public event EventHandler Finished;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// The name of the contact.
+ ///
+ public string Name { get; set; }
+ ///
+ /// The icon of the contact.
+ ///
+ public string Icon { get; set; } = "CHAR_DEFAULT";
+ ///
+ /// If this phone contact is currently active.
+ ///
+ public bool IsActive => status != CallStatus.Inactive;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new contact.
+ ///
+ /// The name of the contact.
+ public PhoneContact(string name)
+ {
+ Name = name;
+ }
+
+ #endregion
+
+ #region Tools
+
+ private static void RestoreScript(string script, int stack)
+ {
+ if (Function.Call(Hash._GET_NUMBER_OF_REFERENCES_OF_SCRIPT_WITH_NAME_HASH, Game.GenerateHash("appcontacts") > 0))
+ {
+ return;
+ }
+
+ while (!Function.Call(Hash.HAS_SCRIPT_LOADED, script))
+ {
+ Function.Call(Hash.REQUEST_SCRIPT, script);
+ }
+
+ Function.Call(Hash.START_NEW_SCRIPT, script, stack);
+ }
+
+ #endregion
+
+ #region Functions
+
+ private static void Restart()
+ {
+ Function.Call(Hash.TERMINATE_ALL_SCRIPTS_WITH_THIS_NAME, "cellphone_flashhand");
+ Function.Call(Hash.TERMINATE_ALL_SCRIPTS_WITH_THIS_NAME, "cellphone_controller");
+ RestoreScript("cellphone_flashhand", 1424);
+ RestoreScript("cellphone_controller", 1424);
+ }
+ internal void Call(Scaleform.Phone phone)
+ {
+ status = CallStatus.Idle;
+ Process(phone);
+ }
+ internal void Process(Scaleform.Phone phone)
+ {
+ phone.SetButtonIcon(1, 1);
+ phone.SetButtonIcon(2, 1);
+ phone.SetButtonIcon(3, 6);
+
+ phone.ShowCalling(Name, label, Icon);
+
+ if (status == CallStatus.Idle)
+ {
+ callingSound.PlayFrontend(false);
+
+ Game.Player.Character.Task.UseMobilePhone();
+ called = new CalledEventArgs(this);
+ Called?.Invoke(phone, called);
+ waitUntil = Game.GameTime + called.Wait;
+ status = CallStatus.Calling;
+ }
+
+ if (status == CallStatus.Calling)
+ {
+ if (waitUntil < Game.GameTime)
+ {
+ callingSound.Stop();
+ status = CallStatus.Called;
+ waitUntil = -1;
+ }
+
+ if (Game.IsControlJustPressed(Control.PhoneCancel))
+ {
+ callingSound.Stop();
+ status = CallStatus.Inactive;
+ waitUntil = -1;
+ return;
+ }
+ }
+
+ if (status == CallStatus.Called)
+ {
+ switch (called.Behavior)
+ {
+ case CallBehavior.Busy:
+ busySound.PlayFrontend(false);
+ status = CallStatus.Busy;
+ label = busyLabel;
+ break;
+ case CallBehavior.Available:
+ ConnectedEventArgs connected = new ConnectedEventArgs(this);
+ Connected?.Invoke(phone, connected);
+ waitUntil = Game.GameTime + connected.DisconnectAfter;
+ status = CallStatus.Connected;
+ label = connectedLabel;
+ break;
+ }
+
+ Game.Player.Character.Task.PutAwayMobilePhone();
+ }
+
+ if (status == CallStatus.Busy)
+ {
+ if (Game.IsControlPressed(Control.PhoneCancel))
+ {
+ busySound.Stop();
+ phone.ShowPage(2);
+ RestoreScript("appContacts", 4000);
+ status = CallStatus.Inactive;
+ return;
+ }
+ }
+
+ if (status == CallStatus.Connected &&
+ (waitUntil < Game.GameTime || Game.IsControlPressed(Control.PhoneCancel)))
+ {
+ status = CallStatus.Inactive;
+ waitUntil = -1;
+ Restart();
+ }
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/LemonUI/Scaleform/Phone.cs b/LemonUI/Scaleform/Phone.cs
new file mode 100644
index 00000000..783396d1
--- /dev/null
+++ b/LemonUI/Scaleform/Phone.cs
@@ -0,0 +1,131 @@
+#if SHVDN3
+using GTA;
+
+namespace LemonUI.Scaleform
+{
+ ///
+ /// Class used to manage the phone scaleform.
+ ///
+ public sealed class Phone : BaseScaleform
+ {
+ #region Properties
+
+ ///
+ /// The type of phone.
+ ///
+ public PhoneType Type { get; }
+ ///
+ /// The currently selected index.
+ ///
+ public int Index
+ {
+ get
+ {
+ int value = CallFunctionReturn("GET_CURRENT_SELECTION");
+
+ while (!IsValueReady(value))
+ {
+ Script.Yield();
+ }
+
+ return GetValue(value);
+ }
+ }
+ ///
+ /// The total amount of contacts.
+ ///
+ ///
+ /// This will set the contact position to the first element.
+ ///
+ public int Total
+ {
+ get
+ {
+ while (Index != 0)
+ {
+ Controls.DisableThisFrame(Control.PhoneUp);
+ Controls.DisableThisFrame(Control.PhoneDown);
+ Controls.DisableThisFrame(Control.PhoneLeft);
+ Controls.DisableThisFrame(Control.PhoneRight);
+ Controls.DisableThisFrame(Control.PhoneSelect);
+ Controls.DisableThisFrame(Control.PhoneCancel);
+ Controls.DisableThisFrame(Control.PhoneOption);
+ CallFunction("SET_INPUT_EVENT", 3);
+ }
+
+ CallFunction("SET_INPUT_EVENT", 1);
+ int last = Index;
+ CallFunction("SET_INPUT_EVENT", 3);
+ return last + 1;
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ public Phone(PhoneType type) : base(GetName(type))
+ {
+ Type = type;
+ }
+
+ #endregion
+
+ #region Functions
+
+ private static string GetName(PhoneType type)
+ {
+ switch (type)
+ {
+ case PhoneType.Badger:
+ return "cellphone_badger";
+ case PhoneType.Facade:
+ return "cellphone_facade";
+ case PhoneType.IFruit:
+ return "cellphone_ifruit";
+ default:
+ return "cellphone_ifruit";
+ }
+ }
+ ///
+ public override void Update()
+ {
+ }
+ ///
+ /// Sets the icon of a button.
+ ///
+ /// The button to change.
+ /// The icon to set.
+ public void SetButtonIcon(int button, int icon) => CallFunction("SET_SOFT_KEYS", button, true, icon);
+ ///
+ /// Shows a specific page on the phone.
+ ///
+ /// The page ID to show.
+ public void ShowPage(int page) => CallFunction("DISPLAY_VIEW", page, 0);
+ ///
+ /// Adds a contact at the specified location.
+ ///
+ /// The position to add the contact at.
+ /// The name of the contact.
+ /// The icon to use.
+ public void AddContactAt(int position, string name, string icon)
+ {
+ CallFunction("SET_DATA_SLOT", 2, position, 0, name, string.Empty, icon);
+ }
+ ///
+ /// Shows the calling screen.
+ ///
+ /// The name of the contact.
+ /// The label used to dial up.
+ /// The icon to use.
+ public void ShowCalling(string name, string dialingLabel, string icon)
+ {
+ CallFunction("SET_DATA_SLOT", 4, 0, 3, name, icon, Game.GetLocalizedString(dialingLabel));
+ CallFunction("DISPLAY_VIEW", 4);
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/LemonUI/Scaleform/PhoneType.cs b/LemonUI/Scaleform/PhoneType.cs
new file mode 100644
index 00000000..5a95c292
--- /dev/null
+++ b/LemonUI/Scaleform/PhoneType.cs
@@ -0,0 +1,21 @@
+namespace LemonUI.Scaleform
+{
+ ///
+ /// The different type of phones in GTA V.
+ ///
+ public enum PhoneType
+ {
+ ///
+ /// Badger, the phone used by Franklin.
+ ///
+ Badger = 0,
+ ///
+ /// Facade, the phone used by Trevor.
+ ///
+ Facade = 1,
+ ///
+ /// iFruit, the phone used by Michael and the GTA Online protagonist.
+ ///
+ IFruit = 2
+ }
+}