Skip to content

Commit

Permalink
rework RustPlus socket
Browse files Browse the repository at this point in the history
  • Loading branch information
HandyS11 committed May 30, 2024
1 parent 102321c commit a660f76
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 243 deletions.
52 changes: 40 additions & 12 deletions RustPlusApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,39 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FcmRegister", "RustPlusApi\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FcmListener", "RustPlusApi\Examples\Fcm\FcmListener\FcmListener.csproj", "{5B175E6B-4118-422D-BF08-F94B7337D229}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetTeamChat", "RustPlusApi\Examples\GetTeamChat\GetTeamChat.csproj", "{1A186504-8461-4F19-BDC5-C3DB90A554D6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetTeamChat", "RustPlusApi\Examples\GetTeamChat\GetTeamChat.csproj", "{1A186504-8461-4F19-BDC5-C3DB90A554D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PromoteToLeader", "RustPlusApi\Examples\PromoteToLeader\PromoteToLeader.csproj", "{3DE93E0D-DAF5-48E7-A353-BD99ABB942DE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PromoteToLeader", "RustPlusApi\Examples\PromoteToLeader\PromoteToLeader.csproj", "{3DE93E0D-DAF5-48E7-A353-BD99ABB942DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetTeamChatChanges", "RustPlusApi\Examples\GetTeamChatChanges\GetTeamChatChanges.csproj", "{D39626D0-079C-40C6-A260-09852E11C70A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetTeamChatChanges", "RustPlusApi\Examples\GetTeamChatChanges\GetTeamChatChanges.csproj", "{D39626D0-079C-40C6-A260-09852E11C70A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetSmartSwitchInfo", "RustPlusApi\Examples\GetSmartSwitchInfo\GetSmartSwitchInfo.csproj", "{CCC95529-EA3A-4FF3-945A-97151848F6BB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetSmartSwitchInfo", "RustPlusApi\Examples\GetSmartSwitchInfo\GetSmartSwitchInfo.csproj", "{CCC95529-EA3A-4FF3-945A-97151848F6BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetAlarmInfo", "RustPlusApi\Examples\GetAlarmInfo\GetAlarmInfo.csproj", "{B9E7E802-867B-40E4-BEEE-807735F091E3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetAlarmInfo", "RustPlusApi\Examples\GetAlarmInfo\GetAlarmInfo.csproj", "{B9E7E802-867B-40E4-BEEE-807735F091E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetStorageMonitorInfo", "RustPlusApi\Examples\GetStorageMonitorInfo\GetStorageMonitorInfo.csproj", "{51CD85C0-0240-4C7C-BBDC-0DCF5234299E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetStorageMonitorInfo", "RustPlusApi\Examples\GetStorageMonitorInfo\GetStorageMonitorInfo.csproj", "{51CD85C0-0240-4C7C-BBDC-0DCF5234299E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Legacy", "Legacy", "{EBB661D0-93FF-4742-AF91-9036027CD136}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetEntityInfoLegacy", "RustPlusApi\Examples\Legacy\GetEntityInfoLegacy\GetEntityInfoLegacy.csproj", "{39EC2C27-0351-4099-A07B-237DD8C73FA6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetEntityInfoLegacy", "RustPlusApi\Examples\Legacy\GetEntityInfoLegacy\GetEntityInfoLegacy.csproj", "{39EC2C27-0351-4099-A07B-237DD8C73FA6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetEntityChangesLegacy", "RustPlusApi\Examples\Legacy\GetEntityChangesLegacy\GetEntityChangesLegacy.csproj", "{443F0C99-2BC7-44CB-9DE8-A292B10B9CE3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetEntityChangesLegacy", "RustPlusApi\Examples\Legacy\GetEntityChangesLegacy\GetEntityChangesLegacy.csproj", "{443F0C99-2BC7-44CB-9DE8-A292B10B9CE3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetInfoLegacy", "RustPlusApi\Examples\Legacy\GetInfoLegacy\GetInfoLegacy.csproj", "{17F33FF0-AFE2-4DE4-81E9-6804A2B210A7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetInfoLegacy", "RustPlusApi\Examples\Legacy\GetInfoLegacy\GetInfoLegacy.csproj", "{17F33FF0-AFE2-4DE4-81E9-6804A2B210A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetMapLegacy", "RustPlusApi\Examples\Legacy\GetMapLegacy\GetMapLegacy.csproj", "{4C4D800C-1DF9-4139-BD7C-2FE0D78FF30C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetMapLegacy", "RustPlusApi\Examples\Legacy\GetMapLegacy\GetMapLegacy.csproj", "{4C4D800C-1DF9-4139-BD7C-2FE0D78FF30C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetSmartSwitchChanges", "RustPlusApi\Examples\GetSmartSwitchChanges\GetSmartSwitchChanges.csproj", "{A7852D6E-9371-4026-B3BE-1BE264718D1D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetSmartSwitchChanges", "RustPlusApi\Examples\GetSmartSwitchChanges\GetSmartSwitchChanges.csproj", "{A7852D6E-9371-4026-B3BE-1BE264718D1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetStorageMonitorChanges", "RustPlusApi\Examples\GetStorageMonitorChanges\GetStorageMonitorChanges.csproj", "{2E65E71F-1C4E-4813-86C9-C5F5D3E60187}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetStorageMonitorChanges", "RustPlusApi\Examples\GetStorageMonitorChanges\GetStorageMonitorChanges.csproj", "{2E65E71F-1C4E-4813-86C9-C5F5D3E60187}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetMapMarkersLegacy", "RustPlusApi\Examples\Legacy\GetMapMarkersLegacy\GetMapMarkersLegacy.csproj", "{70F7555D-DEC9-45E7-BB64-CB31704A2C0A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetTeamChatLegacy", "RustPlusApi\Examples\Legacy\GetTeamChatLegacy\GetTeamChatLegacy.csproj", "{2223E2FE-9CAA-45DF-9F47-E52371018077}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetTeamInfoLegacy", "RustPlusApi\Examples\Legacy\GetTeamInfoLegacy\GetTeamInfoLegacy.csproj", "{4E63E17B-7FD4-4B58-9F22-265B0EE92672}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetTimeLegacy", "RustPlusApi\Examples\Legacy\GetTimeLegacy\GetTimeLegacy.csproj", "{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -165,6 +173,22 @@ Global
{2E65E71F-1C4E-4813-86C9-C5F5D3E60187}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E65E71F-1C4E-4813-86C9-C5F5D3E60187}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E65E71F-1C4E-4813-86C9-C5F5D3E60187}.Release|Any CPU.Build.0 = Release|Any CPU
{70F7555D-DEC9-45E7-BB64-CB31704A2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70F7555D-DEC9-45E7-BB64-CB31704A2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70F7555D-DEC9-45E7-BB64-CB31704A2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70F7555D-DEC9-45E7-BB64-CB31704A2C0A}.Release|Any CPU.Build.0 = Release|Any CPU
{2223E2FE-9CAA-45DF-9F47-E52371018077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2223E2FE-9CAA-45DF-9F47-E52371018077}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2223E2FE-9CAA-45DF-9F47-E52371018077}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2223E2FE-9CAA-45DF-9F47-E52371018077}.Release|Any CPU.Build.0 = Release|Any CPU
{4E63E17B-7FD4-4B58-9F22-265B0EE92672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E63E17B-7FD4-4B58-9F22-265B0EE92672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E63E17B-7FD4-4B58-9F22-265B0EE92672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E63E17B-7FD4-4B58-9F22-265B0EE92672}.Release|Any CPU.Build.0 = Release|Any CPU
{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -195,6 +219,10 @@ Global
{4C4D800C-1DF9-4139-BD7C-2FE0D78FF30C} = {EBB661D0-93FF-4742-AF91-9036027CD136}
{A7852D6E-9371-4026-B3BE-1BE264718D1D} = {BC948ADE-1674-4955-B27C-F0E96100978E}
{2E65E71F-1C4E-4813-86C9-C5F5D3E60187} = {BC948ADE-1674-4955-B27C-F0E96100978E}
{70F7555D-DEC9-45E7-BB64-CB31704A2C0A} = {EBB661D0-93FF-4742-AF91-9036027CD136}
{2223E2FE-9CAA-45DF-9F47-E52371018077} = {EBB661D0-93FF-4742-AF91-9036027CD136}
{4E63E17B-7FD4-4B58-9F22-265B0EE92672} = {EBB661D0-93FF-4742-AF91-9036027CD136}
{4255A1A9-C7D8-45F5-81C3-BBCA96886DAA} = {EBB661D0-93FF-4742-AF91-9036027CD136}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A4B4251F-ADA4-418D-95B5-27BA99A307A3}
Expand Down
81 changes: 79 additions & 2 deletions RustPlusApi/RustPlusApi/RustPlus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected override void ParseNotification(AppBroadcast? broadcast)

if (broadcast.EntityChanged is not null)
{
// There is no physical difference between aSmartSwitch and an Alarm
// There is no physical difference between a SmartSwitch and an Alarm
// If you check the status of an alarm, it will return the same as a smart switch
if (broadcast.EntityChanged.Payload.Capacity is 0)
OnSmartSwitchTriggered?.Invoke(this, broadcast.EntityChanged.ToSmartSwitchEvent());
Expand All @@ -59,7 +59,7 @@ protected override void ParseNotification(AppBroadcast? broadcast)

return IsError(response)
? ResponseHelper.BuildGenericOutput<T>(false, default!, response.Response.Error.Error)
: ResponseHelper.BuildGenericOutput<T>(true, successSelector(response));
: ResponseHelper.BuildGenericOutput(true, successSelector(response));
}

/// <summary>
Expand Down Expand Up @@ -132,5 +132,82 @@ protected override void ParseNotification(AppBroadcast? broadcast)
};
return await ProcessRequestAsync<ServerMap?>(request, r => r.Response.Map.ToServerMap());
}

/*
public async Task GetMapMarkersAsync(Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
GetMapMarkers = new AppEmpty()
};
await SendRequestAsync(request, callback);
}
public async Task GetTeamChatAsync(Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
GetTeamChat = new AppEmpty(),
};
await SendRequestAsync(request, callback);
}
public async Task GetTeamInfoAsync(Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
GetTeamInfo = new AppEmpty()
};
await SendRequestAsync(request, callback);
}
public async Task GetTimeAsync(Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
GetTime = new AppEmpty()
};
await SendRequestAsync(request, callback);
}
public async Task PromoteToLeaderAsync(ulong steamId, Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
PromoteToLeader = new AppPromoteToLeader
{
SteamId = steamId
}
};
await SendRequestAsync(request, callback);
}
public async Task SendTeamMessageAsync(string message, Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
SendTeamMessage = new AppSendMessage
{
Message = message
}
};
await SendRequestAsync(request, callback);
}
public async Task SetEntityValueAsync(int entityId, bool value, Func<AppMessage, bool>? callback = null)
{
var request = new AppRequest
{
EntityId = (uint)entityId,
SetEntityValue = new AppSetEntityValue
{
Value = value
}
};
await SendRequestAsync(request, callback);
}
*/
}
}
180 changes: 180 additions & 0 deletions RustPlusApi/RustPlusApi/RustPlusBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System.Diagnostics;
using System.Net.WebSockets;

using Google.Protobuf;

using RustPlusContracts;

using static System.GC;
// ReSharper disable MemberCanBeProtected.Global
// ReSharper disable MemberCanBePrivate.Global

namespace RustPlusApi
{
/// <summary>
/// A Rust+ API client made in C#.
/// </summary>
/// <param name="server">The IP address of the Rust+ server.</param>
/// <param name="port">The port dedicated for the Rust+ companion app (not the one used to connect in-game).</param>
/// <param name="playerId">Your Steam ID.</param>
/// <param name="playerToken">Your player token acquired with FCM.</param>
/// <param name="useFacepunchProxy">Specifies whether to use the Facepunch proxy.</param>
public abstract class RustPlusBase(string server, int port, ulong playerId, int playerToken, bool useFacepunchProxy = false) : IDisposable
{
private ClientWebSocket? _webSocket;
private uint _seq;
private readonly Dictionary<int, TaskCompletionSource<AppMessage>> _seqCallbacks = [];

public event EventHandler? Connecting;
public event EventHandler? Connected;
public event EventHandler<AppMessage>? MessageReceived;
public event EventHandler<AppRequest>? RequestSent;
public event EventHandler? Disconnected;
public event EventHandler<Exception>? ErrorOccurred;

/// <summary>
/// Connects to the Rust+ server asynchronously.
/// </summary>
public async Task ConnectAsync()
{
_webSocket = new ClientWebSocket();
_webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
var address = useFacepunchProxy
? new Uri($"wss://companion-rust.facepunch.com/game/{server}/{port}")
: new Uri($"ws://{server}:{port}");

Connecting?.Invoke(this, EventArgs.Empty);

try
{
await _webSocket.ConnectAsync(address, CancellationToken.None);
Connected?.Invoke(this, EventArgs.Empty);
await ReceiveMessagesAsync();
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Dispose();
}
}

/// <summary>
/// Receives messages from the Rust+ server asynchronously.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
protected async Task ReceiveMessagesAsync()
{
const int bufferSize = 1024;
var buffer = new byte[bufferSize];

try
{
while (_webSocket!.State == WebSocketState.Open)
{
var receiveBuffer = new List<byte>();
WebSocketReceiveResult result;

do
{
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
receiveBuffer.AddRange(buffer.Take(result.Count));
} while (!result.EndOfMessage);

var messageData = receiveBuffer.ToArray();
var message = AppMessage.Parser.ParseFrom(messageData);
HandleResponse(message);
}
}
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();
}
}

/// <summary>
/// Handles the response received from the Rust+ server.
/// </summary>
/// <param name="message">The AppMessage received from the server.</param>
protected void HandleResponse(AppMessage message)
{
if (message.Response != null
&& message.Response.Seq != 0
&& _seqCallbacks.ContainsKey((int)message.Response.Seq))
{
var tcs = _seqCallbacks[(int)message.Response.Seq];
tcs.SetResult(message);
_seqCallbacks.Remove((int)message.Response.Seq);
return;
}
MessageReceived?.Invoke(this, message);
ParseNotification(message.Broadcast);
}

/// <summary>
/// Parses the notification received from the Rust+ server.
/// </summary>
/// <param name="broadcast">The AppBroadcast received from the server.</param>
protected virtual void ParseNotification(AppBroadcast? broadcast) { }

/// <summary>
/// Sends a request to the Rust+ server asynchronously.
/// </summary>
/// <param name="request">The request to send.</param>
/// <return>A task representing the asynchronous operation.</return>
public async Task<AppMessage> SendRequestAsync(AppRequest request)
{
var seq = ++_seq;
var tcs = new TaskCompletionSource<AppMessage>();
_seqCallbacks[(int)seq] = tcs;

request.Seq = seq;
request.PlayerId = playerId;
request.PlayerToken = playerToken;

var requestData = request.ToByteArray();
var buffer = new ArraySegment<byte>(requestData);
await _webSocket!.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None);
RequestSent?.Invoke(this, request);

return await tcs.Task;
}

/// <summary>
/// Disposes the Rust+ API client and disconnects from the Rust+ server.
/// </summary>
public void Dispose()
{
if (_webSocket is not { State: WebSocketState.Open }) return;

_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by client.", CancellationToken.None).Wait();
_webSocket.Dispose();

Disconnected?.Invoke(this, EventArgs.Empty);

SuppressFinalize(this);
}

/// <summary>
/// Checks if the client is connected to the Rust+ socket.
/// </summary>
/// <returns>True if the client is connected; otherwise, false.</returns>
public bool IsConnected() => _webSocket is { State: WebSocketState.Open };

/// <summary>
/// Checks if the given response is an error.
/// </summary>
/// <param name="response">The AppMessage response to check.</param>
/// <returns>True if the response is an error; otherwise, false.</returns>
protected static bool IsError(AppMessage response) => response.Response.Error is not null;
}
}
Loading

0 comments on commit a660f76

Please sign in to comment.