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 + } +}