From 5cad42517b87bf5f0b40561fdf95429386df29ed Mon Sep 17 00:00:00 2001 From: HandyS11 Date: Wed, 29 May 2024 21:34:14 +0200 Subject: [PATCH] totaly rework the app logic by removing the callback principe implement entityinfo --- .../Examples/GetEntityChanges/Program.cs | 31 ++++-- RustPlusApi/Examples/GetEntityInfo/Program.cs | 15 +-- RustPlusApi/RustPlusApi/Data/AlarmInfo.cs | 7 ++ .../Data/Events/SmartSwitchEventArg.cs | 7 ++ .../Data/Events/StorageMonitorEventArg.cs | 7 ++ .../RustPlusApi/Data/SmartSwitchInfo.cs | 7 ++ .../RustPlusApi/Data/StorageMonitorInfo.cs | 10 ++ .../Data/StorageMonitorItemInfo.cs | 9 ++ .../Extensions/AppEntityInfoToModel.cs | 62 ++++++++++++ .../Extensions/EntityChangedToModel.cs | 30 ++++++ RustPlusApi/RustPlusApi/RustPlus.cs | 99 +++++++++++++++---- 11 files changed, 248 insertions(+), 36 deletions(-) create mode 100644 RustPlusApi/RustPlusApi/Data/AlarmInfo.cs create mode 100644 RustPlusApi/RustPlusApi/Data/Events/SmartSwitchEventArg.cs create mode 100644 RustPlusApi/RustPlusApi/Data/Events/StorageMonitorEventArg.cs create mode 100644 RustPlusApi/RustPlusApi/Data/SmartSwitchInfo.cs create mode 100644 RustPlusApi/RustPlusApi/Data/StorageMonitorInfo.cs create mode 100644 RustPlusApi/RustPlusApi/Data/StorageMonitorItemInfo.cs create mode 100644 RustPlusApi/RustPlusApi/Extensions/AppEntityInfoToModel.cs create mode 100644 RustPlusApi/RustPlusApi/Extensions/EntityChangedToModel.cs diff --git a/RustPlusApi/Examples/GetEntityChanges/Program.cs b/RustPlusApi/Examples/GetEntityChanges/Program.cs index 3942816..1a5f1fa 100644 --- a/RustPlusApi/Examples/GetEntityChanges/Program.cs +++ b/RustPlusApi/Examples/GetEntityChanges/Program.cs @@ -5,23 +5,34 @@ using static __Constants.ExamplesConst; var rustPlus = new RustPlus(Ip, Port, PlayerId, PlayerToken); -var entityId = 0; +uint entityId = 0; rustPlus.Connected += async (_, _) => { - await rustPlus.GetEntityInfoAsync(entityId, message => - { - Console.WriteLine($"Infos:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); - return true; - }); + // The message would be SmartSwitchInfo, AlarmInfo, or StorageMonitorInfo + // If you want to do your own parsing you can set the useRawObject parameter to true + var message = await rustPlus.GetEntityInfoAsync(entityId); + + Console.WriteLine($"Infos:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); }; -rustPlus.MessageReceived += (_, message) => +rustPlus.OnSmartSwitchTriggered += (_, message) => { - if (message.Broadcast is not { EntityChanged: not null }) return; + Console.WriteLine($"SmartSwitch:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); +}; - var entityChanged = message.Broadcast.EntityChanged; - Console.WriteLine($"Message:\n{JsonConvert.SerializeObject(entityChanged, JsonSettings)}"); +rustPlus.OnStorageMonitorTriggered += (_, message) => +{ + Console.WriteLine($"StorageMonitor:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); }; +// If you want to get the raw message for the notification you can use the MessageReceived event +//rustPlus.MessageReceived += (_, message) => +//{ +// if (message.Broadcast is not { EntityChanged: not null }) return; + +// var entityChanged = message.Broadcast.EntityChanged; +// Console.WriteLine($"Message:\n{JsonConvert.SerializeObject(entityChanged, JsonSettings)}"); +//}; + await rustPlus.ConnectAsync(); \ No newline at end of file diff --git a/RustPlusApi/Examples/GetEntityInfo/Program.cs b/RustPlusApi/Examples/GetEntityInfo/Program.cs index b11b284..36c6ab4 100644 --- a/RustPlusApi/Examples/GetEntityInfo/Program.cs +++ b/RustPlusApi/Examples/GetEntityInfo/Program.cs @@ -5,16 +5,17 @@ using static __Constants.ExamplesConst; var rustPlus = new RustPlus(Ip, Port, PlayerId, PlayerToken); -var entityId = 0; +uint entityId = 0; rustPlus.Connected += async (_, _) => { - await rustPlus.GetEntityInfoAsync(entityId, message => - { - Console.WriteLine($"Infos:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); - rustPlus.Dispose(); - return true; - }); + // The message would be SmartSwitchInfo, AlarmInfo, or StorageMonitorInfo + // If you want to do your own parsing you can set the useRawObject parameter to true + var message = await rustPlus.GetEntityInfoAsync(entityId); + + Console.WriteLine($"Infos:\n{JsonConvert.SerializeObject(message, JsonSettings)}"); + + rustPlus.Dispose(); }; await rustPlus.ConnectAsync(); \ No newline at end of file diff --git a/RustPlusApi/RustPlusApi/Data/AlarmInfo.cs b/RustPlusApi/RustPlusApi/Data/AlarmInfo.cs new file mode 100644 index 0000000..0b02ee9 --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/AlarmInfo.cs @@ -0,0 +1,7 @@ +namespace RustPlusApi.Data +{ + public class AlarmInfo + { + public bool Value { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Data/Events/SmartSwitchEventArg.cs b/RustPlusApi/RustPlusApi/Data/Events/SmartSwitchEventArg.cs new file mode 100644 index 0000000..4934c5c --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/Events/SmartSwitchEventArg.cs @@ -0,0 +1,7 @@ +namespace RustPlusApi.Data.Events +{ + public class SmartSwitchEventArg : SmartSwitchInfo + { + public uint Id { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Data/Events/StorageMonitorEventArg.cs b/RustPlusApi/RustPlusApi/Data/Events/StorageMonitorEventArg.cs new file mode 100644 index 0000000..10e47c8 --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/Events/StorageMonitorEventArg.cs @@ -0,0 +1,7 @@ +namespace RustPlusApi.Data.Events +{ + public class StorageMonitorEventArg : StorageMonitorInfo + { + public uint Id { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Data/SmartSwitchInfo.cs b/RustPlusApi/RustPlusApi/Data/SmartSwitchInfo.cs new file mode 100644 index 0000000..f7b9385 --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/SmartSwitchInfo.cs @@ -0,0 +1,7 @@ +namespace RustPlusApi.Data +{ + public class SmartSwitchInfo + { + public bool Value { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Data/StorageMonitorInfo.cs b/RustPlusApi/RustPlusApi/Data/StorageMonitorInfo.cs new file mode 100644 index 0000000..78331b4 --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/StorageMonitorInfo.cs @@ -0,0 +1,10 @@ +namespace RustPlusApi.Data +{ + public class StorageMonitorInfo + { + public int? Capacity { get; set; } + public bool? HasProtection { get; set; } + public uint? ProtectionExpiry { get; set; } + public List? Items { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Data/StorageMonitorItemInfo.cs b/RustPlusApi/RustPlusApi/Data/StorageMonitorItemInfo.cs new file mode 100644 index 0000000..6a3a75c --- /dev/null +++ b/RustPlusApi/RustPlusApi/Data/StorageMonitorItemInfo.cs @@ -0,0 +1,9 @@ +namespace RustPlusApi.Data +{ + public class StorageMonitorItemInfo + { + public int Id { get; set; } + public int? Quantity { get; set; } + public bool? IsItemBlueprint { get; set; } + } +} diff --git a/RustPlusApi/RustPlusApi/Extensions/AppEntityInfoToModel.cs b/RustPlusApi/RustPlusApi/Extensions/AppEntityInfoToModel.cs new file mode 100644 index 0000000..d182b97 --- /dev/null +++ b/RustPlusApi/RustPlusApi/Extensions/AppEntityInfoToModel.cs @@ -0,0 +1,62 @@ +using RustPlusApi.Data; + +using RustPlusContracts; + +namespace RustPlusApi.Extensions +{ + public static class AppEntityInfoToModel + { + public static object ToEntityInfo(this AppEntityInfo entity) + { + return entity.Type switch + { + AppEntityType.Switch => entity.ToSmartSwitchInfo(), + AppEntityType.Alarm => entity.ToAlarmInfo(), + AppEntityType.StorageMonitor => entity.ToStorageMonitorInfo(), + _ => throw new ArgumentException($"The given type is not possible: {entity.Type}") + }; + } + + public static SmartSwitchInfo ToSmartSwitchInfo(this AppEntityInfo entity) + { + return new SmartSwitchInfo + { + Value = entity.Payload.Value + }; + } + + public static AlarmInfo ToAlarmInfo(this AppEntityInfo entity) + { + return new AlarmInfo + { + Value = entity.Payload.Value + }; + } + + public static StorageMonitorInfo ToStorageMonitorInfo(this AppEntityInfo entity) + { + return new StorageMonitorInfo + { + Capacity = entity.Payload.Capacity, + HasProtection = entity.Payload.HasProtection, + ProtectionExpiry = entity.Payload.ProtectionExpiry, + Items = entity.Payload.Items.ToStorageMonitorItemsInfo().ToList() + }; + } + + public static StorageMonitorItemInfo ToStorageMonitorItemInfo(this AppEntityPayload.Types.Item item) + { + return new StorageMonitorItemInfo + { + Id = item.ItemId, + Quantity = item.Quantity, + IsItemBlueprint = item.ItemIsBlueprint, + }; + } + + public static IEnumerable ToStorageMonitorItemsInfo(this IEnumerable items) + { + return items.Select(ToStorageMonitorItemInfo); + } + } +} diff --git a/RustPlusApi/RustPlusApi/Extensions/EntityChangedToModel.cs b/RustPlusApi/RustPlusApi/Extensions/EntityChangedToModel.cs new file mode 100644 index 0000000..e0d505d --- /dev/null +++ b/RustPlusApi/RustPlusApi/Extensions/EntityChangedToModel.cs @@ -0,0 +1,30 @@ +using RustPlusApi.Data.Events; + +using RustPlusContracts; + +namespace RustPlusApi.Extensions +{ + public static class EntityChangedToModel + { + public static SmartSwitchEventArg ToSmartSwitchEvent(this AppEntityChanged entityChanged) + { + return new SmartSwitchEventArg + { + Id = entityChanged.EntityId, + Value = entityChanged.Payload.Value + }; + } + + public static StorageMonitorEventArg ToStorageMonitorEvent(this AppEntityChanged entityChanged) + { + return new StorageMonitorEventArg + { + Id = entityChanged.EntityId, + Capacity = entityChanged.Payload.Capacity, + HasProtection = entityChanged.Payload.HasProtection, + ProtectionExpiry = entityChanged.Payload.ProtectionExpiry, + Items = entityChanged.Payload.Items.ToStorageMonitorItemsInfo().ToList() + }; + } + } +} diff --git a/RustPlusApi/RustPlusApi/RustPlus.cs b/RustPlusApi/RustPlusApi/RustPlus.cs index 159420d..c05f253 100644 --- a/RustPlusApi/RustPlusApi/RustPlus.cs +++ b/RustPlusApi/RustPlusApi/RustPlus.cs @@ -1,7 +1,11 @@ -using System.Net.WebSockets; +using System.Diagnostics; +using System.Net.WebSockets; using Google.Protobuf; +using RustPlusApi.Data.Events; +using RustPlusApi.Extensions; + using RustPlusContracts; using static System.GC; @@ -20,7 +24,7 @@ public class RustPlus(string server, int port, ulong playerId, int playerToken, { private ClientWebSocket? _webSocket; private uint _seq; - private readonly Dictionary> _seqCallbacks = []; + private readonly Dictionary> _seqCallbacks = []; public event EventHandler? Connecting; public event EventHandler? Connected; @@ -29,6 +33,9 @@ public class RustPlus(string server, int port, ulong playerId, int playerToken, public event EventHandler? Disconnected; public event EventHandler? ErrorOccurred; + public event EventHandler? OnSmartSwitchTriggered; // The Alarm behave exactly like the SmartSwitch so if you get the status of the alarm, this will be triggered + public event EventHandler? OnStorageMonitorTriggered; + /// /// Connects to the Rust+ server asynchronously. /// @@ -58,7 +65,8 @@ public async Task ConnectAsync() /// /// Receives messages from the Rust+ server asynchronously. /// - private async Task ReceiveMessagesAsync() + /// A task representing the asynchronous operation. + protected async Task ReceiveMessagesAsync() { const int bufferSize = 1024; var buffer = new byte[bufferSize]; @@ -83,40 +91,49 @@ private async Task ReceiveMessagesAsync() } catch (WebSocketException ex) { + Debug.WriteLine($"Disconnected from the Rust+ socket due to a WebSocketException: {ex}"); ErrorOccurred?.Invoke(this, ex); } catch (Exception ex) { + Debug.WriteLine($"Disconnected from the Rust+ socket due to an Exception: {ex}"); ErrorOccurred?.Invoke(this, ex); } + finally + { + Dispose(); + } } /// /// Handles the response received from the Rust+ server. /// /// The AppMessage received from the server. - private void HandleResponse(AppMessage message) + protected void HandleResponse(AppMessage message) { - if (message.Response != null && message.Response.Seq != 0 && _seqCallbacks.ContainsKey((int)message.Response.Seq)) + if (message.Response != null + && message.Response.Seq != 0 + && _seqCallbacks.ContainsKey((int)message.Response.Seq)) { - var callback = _seqCallbacks[(int)message.Response.Seq]; - var result = callback.Invoke(message); + var tcs = _seqCallbacks[(int)message.Response.Seq]; + tcs.SetResult(message); _seqCallbacks.Remove((int)message.Response.Seq); - if (result) return; + return; } MessageReceived?.Invoke(this, message); + ParseNotification(message.Broadcast); } /// /// Sends a request to the Rust+ server asynchronously. /// /// The request to send. - /// An optional callback function to handle the response. - /// A task representing the asynchronous operation. - public async Task SendRequestAsync(AppRequest request, Func? callback = null) + /// A task representing the asynchronous operation. + public async Task SendRequestAsync(AppRequest request) { var seq = ++_seq; - if (callback != null) _seqCallbacks[(int)seq] = callback; + var tcs = new TaskCompletionSource(); + _seqCallbacks[(int)seq] = tcs; request.Seq = seq; request.PlayerId = playerId; @@ -126,6 +143,28 @@ public async Task SendRequestAsync(AppRequest request, Func? c var buffer = new ArraySegment(requestData); await _webSocket!.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); RequestSent?.Invoke(this, request); + + return await tcs.Task; + } + + /// + /// Parses the notification received from the Rust+ server. + /// + /// The AppBroadcast received from the server. + protected void ParseNotification(AppBroadcast? broadcast) + { + if (broadcast is null) return; + + if (broadcast.EntityChanged is not null) + { + // It is physically impossible to differentiate between a SmartSwitch and an Alarm + // This is a limitation of the Rust+ API + if (broadcast.EntityChanged.Payload.Capacity is 0) + OnSmartSwitchTriggered?.Invoke(this, broadcast.EntityChanged.ToSmartSwitchEvent()); + else + OnStorageMonitorTriggered?.Invoke(this, broadcast.EntityChanged.ToStorageMonitorEvent()); + } + else Debug.WriteLine($"Unknown broadcast:\n{broadcast}"); } /// @@ -144,27 +183,49 @@ public void Dispose() } /// - /// Checks if the client is connected to the Rust+ server. + /// Checks if the client is connected to the Rust+ socket. /// /// True if the client is connected; otherwise, false. public bool IsConnected() => _webSocket is { State: WebSocketState.Open }; + /// + /// Checks if the given response is an error. + /// + /// The AppMessage response to check. + /// True if the response is an error; otherwise, false. + private static bool IsError(AppMessage response) => response.Response.Error is not null; + /// /// Retrieves information about an entity from the Rust+ server asynchronously. /// /// The ID of the entity to retrieve information for. - /// An optional callback function to handle the response. - /// A task representing the asynchronous operation. - public async Task GetEntityInfoAsync(int entityId, Func? callback = null) + /// Specifies whether to use the raw object or convert it to a custom entity info object. + /// + /// The entity information. + /// If useRawObject is true, it returns an instance of the AppMessage class. + /// If useRawObject is false, it returns a custom entity info object such as SmartSwitchInfo, AlarmInfo, or StorageMonitorInfo. + /// + public async Task GetEntityInfoAsync(uint entityId, bool useRawObject = false) { var request = new AppRequest { - EntityId = (uint)entityId, + EntityId = entityId, GetEntityInfo = new AppEmpty() }; - await SendRequestAsync(request, callback); + var response = await SendRequestAsync(request); + + if (IsError(response)) + return useRawObject + ? response + : response.Response.Error; + + return useRawObject + ? response + : response.Response.EntityInfo.ToEntityInfo(); } + + /* /// /// Retrieves general information from the Rust+ server asynchronously. /// @@ -335,6 +396,6 @@ public async Task GetClanInfoAsync(Func? callback = null) GetClanInfo = new AppEmpty() }; await SendRequestAsync(request, callback); - } + }*/ } } \ No newline at end of file