diff --git a/plugins/rust/RustApp.cs b/plugins/rust/RustApp.cs index 2ad543e..f4e3b18 100644 --- a/plugins/rust/RustApp.cs +++ b/plugins/rust/RustApp.cs @@ -250,41 +250,42 @@ public class PluginStatePlayerMetaDto public class PluginStatePlayerDto { - public static PluginStatePlayerDto FromConnection(Network.Connection connection, string status) + public static PluginStatePlayerDto FromConnection(Connection connection, string status) { var userid = connection.userid; if (!players.TryGetValue(userid, out var payload)) { - payload = new PluginStatePlayerDto(); - players[userid] = payload; - payload.steam_id = connection.player is BasePlayer basePlayer ? basePlayer.UserIDString : RustApp.GetSteamIdString(userid); - payload.steam_name = connection.username.Replace("", "blank"); - payload.ip = IPAddressWithoutPort(connection.ipaddress); - payload.no_license = DetectNoLicense(connection); - payload.team = Pool.Get>(); + players[userid] = payload = new PluginStatePlayerDto + { + steam_id = connection.player is BasePlayer basePlayer ? basePlayer.UserIDString : GetSteamIdString(userid), + steam_name = connection.username.Replace("", "blank"), + ip = IPAddressWithoutPort(connection.ipaddress), + no_license = DetectNoLicense(connection) + }; } payload.ping = Network.Net.sv.GetAveragePing(connection); payload.seconds_connected = (int)connection.GetSecondsConnected(); payload.language = _RustApp.lang.GetLanguage(payload.steam_id); payload.status = status; - try { payload.meta = CollectPlayerMeta(payload.steam_id, payload.meta); } catch (Exception ex) { Debug(ex.ToString()); } - - payload.team ??= Pool.Get>(); + payload.team ??= Pool.Get>(); payload.team.Clear(); - RelationshipManager.PlayerTeam? newTeam = RelationshipManager.ServerInstance.FindPlayersTeam(userid); - if (newTeam == null) + var newTeam = RelationshipManager.ServerInstance.FindPlayersTeam(userid); + if (newTeam != null) { - return payload; + var members = newTeam.members; + for (int i = 0; i < members.Count; i++) + { + ulong member = members[i]; + if (member != userid) + { + payload.team.Add(member); + } + } } - for (int index = 0; index < newTeam.members.Count; index++) - { - ulong member = newTeam.members[index]; - if (member != userid) - payload.team.Add(member.ToString()); - } + try { payload.meta = CollectPlayerMeta(payload.steam_id, payload.meta); } catch (Exception ex) { Debug(ex.ToString()); } return payload; } @@ -293,9 +294,9 @@ public static PluginStatePlayerDto FromPlayer(BasePlayer player) { PluginStatePlayerDto payload = FromConnection(player.Connection, "active"); - payload.position = player.transform.position.ToString(); - payload.rotation = player.eyes.rotation.ToString(); - payload.coords = MapHelper.PositionToString(player.transform.position); + payload.position = player.transform.position; + payload.rotation = player.eyes.rotation; + payload.coords = PositionToGridString(player.transform.position); payload.can_build = DetectBuildingAuth(player); payload.is_raiding = DetectIsRaidBlock(player); @@ -311,8 +312,10 @@ public static PluginStatePlayerDto FromPlayer(BasePlayer player) public int seconds_connected; public string language; - [CanBeNull] public string position; - [CanBeNull] public string rotation; + [JsonConverter(typeof(Vector3Converter))] + public Vector3 position; + [JsonConverter(typeof(QuaternionConverter))] + public Quaternion rotation; [CanBeNull] public string coords; public bool can_build = false; @@ -323,7 +326,9 @@ public static PluginStatePlayerDto FromPlayer(BasePlayer player) public string status; public PluginStatePlayerMetaDto meta = new(); - public List team; + + [JsonProperty(ItemConverterType = typeof(UlongConverter))] + public List team; public void FreePooledFields() { @@ -333,24 +338,13 @@ public void FreePooledFields() public class PluginStateUpdatePayload : PluginServerDto { - public PluginServerDto server_info; - public List players; public Dictionary disconnected; public Dictionary team_changes; - public override void LeavePool() - { - base.LeavePool(); - server_info = Pool.Get(); - } - public override void EnterPool() { base.EnterPool(); - - if (server_info != null) Pool.Free(ref server_info); - if (players != null) Pool.FreeUnmanaged(ref players); if (disconnected != null) Pool.FreeUnmanaged(ref disconnected); if (team_changes != null) Pool.FreeUnmanaged(ref team_changes); @@ -647,8 +641,10 @@ public void EnterPool() public class PluginPlayerAlertDugUpStashMeta { public string steam_id; - public string owner_steam_id; - public string position; + [JsonConverter(typeof(UlongConverter))] + public ulong owner_steam_id; + [JsonConverter(typeof(Vector3Converter))] + public Vector3 position; public string square; } @@ -730,10 +726,11 @@ public class PluginSignageCreateDto : Pool.IPooled public ulong net_id; public byte[] base64_image; public string type; - public string position; + [JsonConverter(typeof(Vector3Converter))] + public Vector3 position; public string square; - public static PluginSignageCreateDto Create(string steamId, ulong netId, byte[] base64Image, string type, string position, string square) + public static PluginSignageCreateDto Create(string steamId, ulong netId, byte[] base64Image, string type, Vector3 position, string square) { PluginSignageCreateDto? dto = Pool.Get(); dto.steam_id = steamId; @@ -753,7 +750,7 @@ public void EnterPool() net_id = 0; base64_image = null; type = null; - position = null; + position = default; square = null; } } @@ -1564,19 +1561,19 @@ public void StartPairing(string code) { InvokeRepeating(nameof(WaitPairFinish), 0f, 1f); }, - (err) => + (error) => { - if (Api.ErrorContains(err, "code not exists")) + if (Api.ErrorContains(error, "code not exists")) { Error("Pairing failed: requested code not exists"); } - else if (Api.ErrorContains(err, "pairing prevented from abuse")) + else if (Api.ErrorContains(error, "pairing prevented from abuse")) { Error("Pairing failed: seems this server was already connected to another project, please contact TG: @rustapp_help if you think, that it is wrong"); } else { - Debug($"Pairing failed: unknown exception {err}"); + Debug($"Pairing failed: unknown exception {error}"); } Destroy(this); @@ -1617,15 +1614,15 @@ void SaveData() Destroy(this); } }, - (err) => + (error) => { - if (Api.ErrorContains(err, "code not exists")) + if (Api.ErrorContains(error, "code not exists")) { Error("Pairing failed: seems you closed modal on site"); } else { - Error($"Pairing failed: unknown exception {err}"); + Error($"Pairing failed: unknown exception {error}"); } Destroy(this); @@ -1652,10 +1649,8 @@ public void CycleSendUpdate() public void SendUpdate(Action? onFinished = null) { - CourtApi.PluginStateUpdatePayload? payload = Pool.Get(); payload.FillSnapshot(); - payload.server_info.FillSnapshot(); payload.players = Pool.Get>(); CollectPlayers(payload.players); @@ -1675,10 +1670,10 @@ public void SendUpdate(Action? onFinished = null) Trace("State was sent successfull"); onFinished?.Invoke(); }, - (err) => + (error) => { onFinished?.Invoke(); - Debug($"State sent error: {err}"); + Debug($"State sent error: {error}"); ResurrectDictionary(payload.disconnected, DisconnectReasons); ResurrectDictionary(payload.team_changes, TeamChanges); @@ -1785,8 +1780,7 @@ private class QueueWorker : RustAppWorker private void GetQueueTasks() { - QueueApi.GetQueueTasks().Execute( - CallQueueTasks, + QueueApi.GetQueueTasks().Execute(CallQueueTasks, (error) => { Debug($"Queue retreive failed {error}"); @@ -1841,9 +1835,9 @@ private void ProcessQueueTasks(Dictionary queueResponses) QueueProcessedIds.Clear(); Trace("Ответ по очередям успешно доставлен"); }, - (err) => + (error) => { - Debug($"Failed to process queue: {err}"); + Debug($"Failed to process queue: {error}"); }); } @@ -1999,7 +1993,7 @@ private void CycleBanUpdateWrapper(Action callback) Pool.Free(ref payload); } }, - (_) => + () => { Error($"Failed to process ban checks ({payload.players.Count}), retrying..."); for (int i = 0; i < payload.players.Count; i++) @@ -2100,7 +2094,7 @@ private void SendChatMessages() Pool.Free(ref payload.messages, freeElements: true); Pool.Free(ref payload); }, - (_) => + () => { QueueMessages.AddRange(payload.messages); payload.messages.Clear(); @@ -2155,7 +2149,7 @@ private void CycleReportSend() Pool.Free(ref payload.reports, freeElements: true); Pool.Free(ref payload); }, - (_) => + () => { QueueReportSend.AddRange(payload.reports); payload.reports.Clear(); @@ -2206,7 +2200,7 @@ private void CycleSendPlayerAlerts() Pool.Free(ref payload.alerts, freeElements: true); Pool.Free(ref payload); }, - (_) => + () => { PlayerAlertQueue.AddRange(payload.alerts); payload.alerts.Clear(); @@ -2254,8 +2248,8 @@ public void SignageCreate(BaseImageUpdate update) update.Entity.net.ID.Value, update.GetImage(), update.Entity.ShortPrefabName, - pos.ToString(), - MapHelper.PositionToString(pos)); + pos, + PositionToGridString(pos)); CourtApi.SendSignage(obj).Execute(); @@ -2282,7 +2276,7 @@ private void CycleSendUpdate() { Pool.Free(ref payload); }, - (_) => + () => { DestroyedSignagesQueue.AddRange(payload.net_ids); payload.net_ids.Clear(); @@ -2329,7 +2323,7 @@ private void CycleSendSleepingBags() Pool.Free(ref payload.sleeping_bags, freeElements: true); Pool.Free(ref payload); }, - (_) => + () => { SleepingBags.AddRange(payload.sleeping_bags); payload.sleeping_bags.Clear(); @@ -2384,7 +2378,7 @@ private void CycleSendKills() Pool.Free(ref payload.kills, freeElements: true); Pool.Free(ref payload); }, - (_) => + () => { KillsQueue.AddRange(payload.kills); payload.kills.Clear(); @@ -2425,8 +2419,7 @@ private void CycleUpdateMutes() { PlayerMutes.Clear(); data?.data?.ForEach(AddPlayerMute); - }, - (_) => { }); + }); } public void AddPlayerMute(CourtApi.PlayerMuteDto playerMuteDto) @@ -2460,8 +2453,7 @@ private void CmdSendContact(BasePlayer player, string contact, string[] args) { SendMessage(player, lang.GetMessage("Contact.Sent", this, player.UserIDString) + $" {string.Join(" ", args)}"); SendMessage(player, lang.GetMessage("Contact.SentWait", this, player.UserIDString)); - }, - (_) => { }); + }); } private void CmdChatReportInterface(BasePlayer player) @@ -4008,6 +4000,21 @@ private void DrawNoticeInterface(BasePlayer player) #region Methods + private static readonly Dictionary _gridToStringCache = new(); + + public static string PositionToGridString(Vector3 position) + { + var grid = MapHelper.PositionToGrid(position); + if (_gridToStringCache.TryGetValue(grid, out var result)) + { + return result; + } + + result = MapHelper.GridToString(grid); + _gridToStringCache.Add(grid, result); + return result; + } + private static string ResolveWeaponName(HitInfo? info) { if (info == null) return "unknown"; @@ -4220,9 +4227,14 @@ private void BanCreate(string steamId, CourtApi.PluginBanCreatePayload payload) { string reason = payload.reason; - CourtApi.BanCreate(payload).Execute( - () => Log($"Player {steamId} banned for {reason}"), - (err) => Error($"Failed to ban {steamId}. Reason: {err}")); + CourtApi.BanCreate(payload).Execute(() => + { + Log($"Player {steamId} banned for {reason}"); + }, + (error) => + { + Error($"Failed to ban {steamId}. Reason: {error}"); + }); Pool.Free(ref payload); } @@ -4233,7 +4245,10 @@ private void BanDelete(string steamId) { Log($"Player {steamId} unbanned"); }, - (err) => Error($"Failed to unban {steamId}. Reason: {err}")); + (error) => + { + Error($"Failed to unban {steamId}. Reason: {error}"); + }); } private void CreatePlayerAlertsCustom(Plugin plugin, string message, object? data = null, object? meta = null) @@ -4272,9 +4287,10 @@ private void CreatePlayerAlertsCustom(Plugin plugin, string message, object? dat category: $"{plugin.Name} • {(name ?? "")}", customLinks: customLinks); - CourtApi.CreatePlayerAlertsCustom(payload).Execute( - (error) => Debug($"Failed to send custom alert: {error}") - ); + CourtApi.CreatePlayerAlertsCustom(payload).Execute(onException: (error) => + { + Debug($"Failed to send custom alert: {error}"); + }); Pool.Free(ref payload); } @@ -4283,16 +4299,157 @@ private void CreatePlayerAlertsCustom(Plugin plugin, string message, object? dat #region StableRequest + #region Custom Json Stuff + + public sealed class UlongConverter : JsonConverter + { + [ThreadStatic] + private static char[] _reusableBuffer; + public override bool CanRead => false; + public override bool CanConvert(Type objectType) => objectType == typeof(ulong); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (writer is not CustomJsonTextWriter customWriter) + { + throw new JsonSerializationException("Cannot write ulong using non CustomJsonTextWriter"); + } + + if (value is not ulong number) + { + throw new JsonSerializationException($"Unsupported type: {value?.GetType()}"); + } + + _reusableBuffer ??= new char[256]; + var buffer = _reusableBuffer; + var pos = WriteUlong(buffer, number); + customWriter.WriteRawStringValue(buffer, pos); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Unnecessary because CanRead is false."); + } + } + + public sealed class Vector3Converter : JsonConverter + { + [ThreadStatic] + private static char[] _reusableBuffer; + public override bool CanRead => false; + public override bool CanConvert(Type objectType) => objectType == typeof(Vector3); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (writer is not CustomJsonTextWriter customWriter) + { + throw new JsonSerializationException("Cannot write Vector3 using non CustomJsonTextWriter"); + } + + if (value is not Vector3 vec3) + { + throw new JsonSerializationException($"Unsupported type: {value?.GetType()}"); + } + + var buffer = _reusableBuffer ??= new char[256]; + var pos = WriteTuple(buffer, vec3.x, vec3.y, vec3.z); + customWriter.WriteRawStringValue(buffer, pos); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Unnecessary because CanRead is false."); + } + } + + public sealed class QuaternionConverter : JsonConverter + { + [ThreadStatic] + private static char[] _reusableBuffer; + public override bool CanRead => false; + public override bool CanConvert(Type objectType) => objectType == typeof(Quaternion); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (writer is not CustomJsonTextWriter customWriter) + { + throw new JsonSerializationException("Cannot write Quaternion using non CustomJsonTextWriter"); + } + + if (value is not Quaternion quat) + { + throw new JsonSerializationException($"Unsupported type: {value?.GetType()}"); + } + + var buffer = _reusableBuffer ??= new char[256]; + var pos = WriteTuple(buffer, quat.x, quat.y, quat.z, quat.w); + customWriter.WriteRawStringValue(buffer, pos); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Unnecessary because CanRead is false."); + } + } + + private static int WriteUlong(char[] buffer, ulong value) + { + if (!value.TryFormat(buffer.AsSpan(), out int len, default, CultureInfo.InvariantCulture)) + { + throw new JsonSerializationException("Failed to serialize ulong value"); + } + + return len; + } + + private static int WriteTuple(char[] buffer, float x, float y, float z) + { + int pos = 0; + buffer[pos++] = '('; + pos = WriteFloat(buffer, pos, x); + buffer[pos++] = ','; + pos = WriteFloat(buffer, pos, y); + buffer[pos++] = ','; + pos = WriteFloat(buffer, pos, z); + buffer[pos++] = ')'; + return pos; + } + + private static int WriteTuple(char[] buffer, float x, float y, float z, float w) + { + int pos = 0; + buffer[pos++] = '('; + pos = WriteFloat(buffer, pos, x); + buffer[pos++] = ','; + pos = WriteFloat(buffer, pos, y); + buffer[pos++] = ','; + pos = WriteFloat(buffer, pos, z); + buffer[pos++] = ','; + pos = WriteFloat(buffer, pos, w); + buffer[pos++] = ')'; + return pos; + } + + private static int WriteFloat(char[] buffer, int pos, float value) + { + if (!value.TryFormat(buffer.AsSpan(pos), out int len, "F3", CultureInfo.InvariantCulture)) + { + throw new JsonSerializationException("Failed to serialize float value"); + } + + return pos + len; + } + [ThreadStatic] private static char[] _reusableCharBuffer; private static readonly UTF8Encoding _encoding = new(false); - public sealed class PooledTextReaderUtf8 : TextReader + public sealed class CustomTextReader : TextReader { private readonly int _length; private int _pos; - public PooledTextReaderUtf8(ReadOnlySpan data) + public CustomTextReader(ReadOnlySpan data) { var requiredBufferLen = _encoding.GetMaxCharCount(data.Length); if (_reusableCharBuffer == null || _reusableCharBuffer.Length < requiredBufferLen) @@ -4300,41 +4457,57 @@ public PooledTextReaderUtf8(ReadOnlySpan data) var length = _reusableCharBuffer?.Length ?? 2048; _reusableCharBuffer = new char[Math.Max(length * 2, requiredBufferLen)]; } + _length = _encoding.GetChars(data, _reusableCharBuffer); _pos = 0; - } public override int Read(char[] buffer, int index, int count) { if (buffer == null) + { throw new ArgumentNullException(nameof(buffer), "Buffer cannot be null."); + } + if (index < 0) + { throw new ArgumentOutOfRangeException(nameof(index), "Non-negative number required."); + } + if (count < 0) + { throw new ArgumentOutOfRangeException(nameof(count), "Non-negative number required."); + } + if (buffer.Length - index < count) + { throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + int readCount = _length - _pos; if (readCount > 0) { if (readCount > count) + { readCount = count; + } + _reusableCharBuffer.AsSpan(_pos, readCount).CopyTo(buffer.AsSpan(index)); _pos += readCount; } + return readCount; } } - public sealed class PooledTextWriterUtf8 : TextWriter + public sealed class CustomTextWriter : TextWriter { [ThreadStatic] private static byte[] _reusableByteBuffer; - private int _pos; + public override Encoding Encoding => _encoding; - public PooledTextWriterUtf8() : base(CultureInfo.InvariantCulture) + public CustomTextWriter() : base(CultureInfo.InvariantCulture) { _reusableCharBuffer ??= new char[4096]; _pos = 0; @@ -4348,12 +4521,11 @@ public ArraySegment AsArraySegment() var length = _reusableByteBuffer?.Length ?? 2048; _reusableByteBuffer = new byte[Math.Max(length * 2, requiredBufferLen)]; } + var len = _encoding.GetBytes(_reusableCharBuffer, 0, _pos, _reusableByteBuffer, 0); return new ArraySegment(_reusableByteBuffer, 0, len); } - public override Encoding Encoding => _encoding; - public override void Write(char value) { Grow(1); @@ -4377,7 +4549,10 @@ public override void Write(ReadOnlySpan buffer) public override void Write(string value) { if (value == null) + { return; + } + Grow(value.Length); value.CopyTo(0, _reusableCharBuffer, _pos, value.Length); _pos += value.Length; @@ -4388,22 +4563,42 @@ private void Grow(int requestedSize) var freeSize = _reusableCharBuffer.Length - _pos; if (freeSize < requestedSize) { - var newBuffer = new char[Math.Max(_reusableCharBuffer.Length * 2, _reusableCharBuffer.Length + requestedSize)]; - _reusableCharBuffer.AsSpan().CopyTo(newBuffer); + var newBuffer = new char[Math.Max(_reusableCharBuffer.Length * 2, _pos + requestedSize)]; + _reusableCharBuffer.AsSpan(0, _pos).CopyTo(newBuffer); _reusableCharBuffer = newBuffer; } } } + public class CustomJsonTextWriter : JsonTextWriter + { + private readonly TextWriter _textWriter; + + public CustomJsonTextWriter(TextWriter textWriter) : base(textWriter) + { + _textWriter = textWriter; + } + + public void WriteRawStringValue(char[] buffer, int count) + { + WriteRawValue(string.Empty); + _textWriter.Write(QuoteChar); + _textWriter.Write(buffer, 0, count); + _textWriter.Write(QuoteChar); + } + } + + #endregion + public class StableRequest where T : class { - private Uri url; - private string method; - private object? data; + private readonly Uri _url; + private readonly string _method; + private readonly object _data; - public StableRequest(Uri url, RequestMethod requestMethod, object? data) + public StableRequest(Uri url, RequestMethod requestMethod, object data) { - method = requestMethod switch + _method = requestMethod switch { RequestMethod.GET => UnityWebRequest.kHttpVerbGET, RequestMethod.PUT => UnityWebRequest.kHttpVerbPUT, @@ -4412,80 +4607,76 @@ public StableRequest(Uri url, RequestMethod requestMethod, object? data) _ => throw new ArgumentOutOfRangeException(nameof(requestMethod), requestMethod, null) }; - this.url = url; - this.data = data; + _url = url; + _data = data; } - public StableRequest(string url, RequestMethod requestMethod, object? data) - : this(new Uri(url), requestMethod, data) { } + public StableRequest(string url, RequestMethod requestMethod, object data) : this(new Uri(url), requestMethod, data) { } public void Execute() { - Rust.Global.Runner.StartCoroutine(SendWebRequestDeserialize(onComplete: null, onException: null)); + Rust.Global.Runner.StartCoroutine(SendRequest(null, null, null, null)); } - public void Execute(Action onException) + public void Execute(Action onComplete) { - Rust.Global.Runner.StartCoroutine(SendWebRequestDeserialize(onComplete: null, onException)); + Rust.Global.Runner.StartCoroutine(SendRequest(onComplete, null, null, null)); } - public void Execute(Action onComplete, Action onException) + public void Execute(Action onComplete) { - Rust.Global.Runner.StartCoroutine(SendWebRequestDeserialize(onComplete, onException)); + Rust.Global.Runner.StartCoroutine(SendRequest(null, onComplete, null, null)); } - // Overload for requests when we don't need to deserialize response - public void Execute(Action onComplete, Action onException) + public void Execute(Action onException) { - Rust.Global.Runner.StartCoroutine(SendWebRequest(onComplete, onException)); + Rust.Global.Runner.StartCoroutine(SendRequest(null, null, null, onException)); } - private IEnumerator SendWebRequest(Action onComplete, Action onException) + public void Execute(Action onComplete, Action onException) { - using var request = CreateWebRequest(); + Rust.Global.Runner.StartCoroutine(SendRequest(onComplete, null, onException, null)); + } - yield return request.SendWebRequest(); + public void Execute(Action onComplete, Action onException) + { + Rust.Global.Runner.StartCoroutine(SendRequest(onComplete, null, null, onException)); + } - if (TryGetError(request, out var error)) - { - onException?.Invoke(error); - yield break; - } + public void Execute(Action onComplete, Action onException) + { + Rust.Global.Runner.StartCoroutine(SendRequest(null, onComplete, onException, null)); + } - onComplete?.Invoke(); + public void Execute(Action onComplete, Action onException) + { + Rust.Global.Runner.StartCoroutine(SendRequest(null, onComplete, null, onException)); } - private IEnumerator SendWebRequestDeserialize(Action onComplete, Action onException) + private IEnumerator SendRequest(Action onComplete, Action onCompleteT, Action onException, Action onExceptionText) { using var request = CreateWebRequest(); - yield return request.SendWebRequest(); - if (TryGetError(request, out var error)) - { - onException?.Invoke(error); - yield break; - } - - if (onComplete == null) + if (request.result == UnityWebRequest.Result.Success) { - yield break; - } + onComplete?.Invoke(); - try - { - var obj = DeserializeWebResponse(request); - onComplete.Invoke(obj); + if (onCompleteT != null && TryDeserializeResponse(request, out var deserialized)) + { + onCompleteT.Invoke(deserialized); + } } - catch (Exception parseException) + else { - Error($"Failed to parse response ({request.method.ToUpper()} {request.url}): {parseException} (Response: {request.downloadHandler?.text})"); + onException?.Invoke(); + onExceptionText?.Invoke(GetError(request)); } } private UnityWebRequest CreateWebRequest() { - var request = new UnityWebRequest(url, method) + var request = new UnityWebRequest(_url, _method) { downloadHandler = new DownloadHandlerBuffer(), timeout = 10 @@ -4496,62 +4687,67 @@ private UnityWebRequest CreateWebRequest() request.SetRequestHeader(name, value); } - SetWebRequestPayload(request, data); - return request; - } - - private static void SetWebRequestPayload(UnityWebRequest request, object data) - { - if (data == null) + if (_data != null) { - return; - } + using var stringWriter = new CustomTextWriter(); + using var jsonWriter = new CustomJsonTextWriter(stringWriter); + _jsonSerializer.Serialize(jsonWriter, _data); + var dataArray = stringWriter.AsArraySegment(); + var dataNativeArray = new NativeArray(dataArray.Count, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + NativeArray.Copy(dataArray.Array, dataNativeArray, dataArray.Count); - using var stringWriter = new PooledTextWriterUtf8(); - _jsonSerializer.Serialize(stringWriter, data); - var dataArray = stringWriter.AsArraySegment(); - - var dataNativeArray = new NativeArray(dataArray.Count, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); - NativeArray.Copy(dataArray.Array, dataNativeArray, dataArray.Count); + request.uploadHandler = new UploadHandlerRaw(dataNativeArray, true) + { + contentType = "application/json" + }; + } - request.uploadHandler = new UploadHandlerRaw(dataNativeArray, transferOwnership: true) - { - contentType = "application/json" - }; + return request; } - private static bool TryGetError(UnityWebRequest request, out string error) + private static bool TryDeserializeResponse(UnityWebRequest request, out T deserialized) { - if (request.result == UnityWebRequest.Result.Success) + deserialized = default; + + try { - error = null; - return false; - } + var data = request.downloadHandler.nativeData; + if (data.Length == 0 || data.Length == 2 && data[0] == '[' && data[1] == ']') + { + return true; + } - error = $"Error: {request.result}. Message: {request.downloadHandler?.text.ToLower() ?? "possible network errors, contact @rustapp_help if you see > 5 minutes"}"; - if (error.Contains("502 bad gateway") || error.Contains("cloudflare")) + using var textReader = new CustomTextReader(data.AsReadOnlySpan()); + using var reader = new JsonTextReader(textReader); + deserialized = _jsonSerializer.Deserialize(reader); + return true; + } + catch (Exception ex) { - error = "rustapp is restarting, wait please"; + Error($"Failed to parse response ({request.method.ToUpper()} {request.url}): {ex} (Response: {request.downloadHandler?.text})"); + return false; } - return true; } - private static T DeserializeWebResponse(UnityWebRequest request) + private static string GetError(UnityWebRequest request) { - var data = request.downloadHandler.nativeData; - if (data.Length == 0) + string message; + string downloadHandlerText = request.downloadHandler?.text; + if (string.IsNullOrEmpty(downloadHandlerText)) { - return default; + message = "possible network errors, contact @rustapp_help if you see this for more than 5 minutes"; } - - if (data.Length == 2 && data[0] == '[' && data[1] == ']') + else { - return default; + if (downloadHandlerText.Contains("502 bad gateway", StringComparison.OrdinalIgnoreCase) || downloadHandlerText.Contains("cloudflare", StringComparison.OrdinalIgnoreCase)) + { + return "rustapp is restarting, please wait"; + } + + message = downloadHandlerText; } - using var textReader = new PooledTextReaderUtf8(data.AsReadOnlySpan()); - using var reader = new JsonTextReader(textReader); - return _jsonSerializer.Deserialize(reader); + return $"Error: {request.result}. Message: {message}"; } } @@ -4563,9 +4759,14 @@ private void RustApp_PlayerMuteCreate(string targetSteamId, string reason, strin { CourtApi.PlayerMuteCreateDto? payload = CourtApi.PlayerMuteCreateDto.Create(targetSteamId, reason, duration, broadcast, comment, referenceMessageText); - CourtApi.PlayerMuteCreate(payload).Execute( - () => Puts($"Player ({targetSteamId}) is muted"), - (err) => PrintError($"Failed to mute player: {err}")); + CourtApi.PlayerMuteCreate(payload).Execute(() => + { + Puts($"Player ({targetSteamId}) is muted"); + }, + (error) => + { + PrintError($"Failed to mute player: {error}"); + }); Pool.Free(ref payload); } @@ -4574,9 +4775,14 @@ private void RustApp_PlayerMuteDelete(string targetSteamId) { CourtApi.PlayerMuteDeleteDto? payload = CourtApi.PlayerMuteDeleteDto.Create(targetSteamId); - CourtApi.PlayerMuteDelete(payload).Execute( - () => Puts($"Player ({targetSteamId}) is unmuted"), - (err) => PrintError($"Failed to unmute player: {err}")); + CourtApi.PlayerMuteDelete(payload).Execute(() => + { + Puts($"Player ({targetSteamId}) is unmuted"); + }, + (error) => + { + PrintError($"Failed to unmute player: {error}"); + }); Pool.Free(ref payload); } @@ -4715,32 +4921,82 @@ private void SoundToast(BasePlayer player, string text, SoundToastType type) player.Command("gametip.showtoast", (int)type, text, 1); } - private static readonly BaseEntity[] _buildAuthArr = new BaseEntity[32]; - private static readonly Func _buildAuthFilter = static e => e is BuildingPrivlidge; + private const float FastSearchRadius = 16f; + private const float CheckRadius = FastSearchRadius + 2f; + private const float SqrCheckRadius = CheckRadius * CheckRadius; + + private static readonly Dictionary _buildAuthStates = new(128); + private static readonly BaseEntity[] _detectResult = new BaseEntity[1]; + private static readonly Func _detectFilter = DetectFilter; + + private static Vector3 _detectPos; + private static ulong _detectUserId; - // It is more optimized way to detect building authed instead of default BasePlayer.IsBuildingAuthed() private static bool DetectBuildingAuth(BasePlayer player) { - const float SearchRadius = 22f; - const float SqrRadius = SearchRadius * SearchRadius; - Vector3 pos = player.transform.position; - int count = BaseEntity.Query.Server.GetInSphereFast(pos, SearchRadius, _buildAuthArr, _buildAuthFilter); + _detectPos = pos; + _detectUserId = player.userID; try { + return BaseEntity.Query.Server.GetInSphereFast(pos, FastSearchRadius, _detectResult, _detectFilter) > 0; + } + finally + { + _buildAuthStates.Clear(); + _detectResult[0] = null; + } + } + + private static bool DetectFilter(BaseEntity ent) + { + if (ent is not BuildingBlock block) + { + return false; + } + + uint buildingId = block.buildingID; + if (!_buildAuthStates.TryGetValue(buildingId, out bool isAuthed)) + { + _buildAuthStates[buildingId] = isAuthed = IsAuthed(buildingId, _detectUserId); + } + + if (!isAuthed) + { + return false; + } + + return (block.transform.position - _detectPos).sqrMagnitude <= SqrCheckRadius; + + static bool IsAuthed(uint buildingId, ulong userid) + { + BuildingManager.Building building = BuildingManager.server.GetBuilding(buildingId); + if (building == null) + { + return false; + } + + ListHashSet privileges = building.buildingPrivileges; + if (privileges == null || privileges.Count == 0) + { + return false; + } + + int count = privileges.Count; for (int i = 0; i < count; i++) { - BuildingPrivlidge tc = (BuildingPrivlidge)_buildAuthArr[i]; - if ((tc.transform.position - pos).sqrMagnitude <= SqrRadius) - return tc.IsAuthed(player); + BuildingPrivlidge tc = privileges[i]; + if (tc == null || !tc.IsAuthed(userid)) + { + continue; + } + + return true; } + return false; } - finally - { - Array.Clear(_buildAuthArr, 0, count); - } } private static readonly HashSet _normalizeAllowed = new() @@ -5129,7 +5385,7 @@ public override byte[] GetImage() private void OnEntityKill(BaseNetworkable entity) { - if (entity is not ISignage || entity.net is null) + if (!IsPaintable(entity) || entity.net == null) { return; } @@ -5137,19 +5393,10 @@ private void OnEntityKill(BaseNetworkable entity) _RustAppEngine?.SignageWorker?.AddSignageDestroy(entity.net.ID.Value.ToString()); } - private void OnImagePost(BasePlayer player, string url, bool raw, ISignage signage, uint textureIndex) - { - _RustAppEngine?.SignageWorker?.SignageCreate(new SignageUpdate(player, signage, textureIndex, url)); - } - + // ISignage can only be Signage, PhotoFrame, or CarvablePumpkin here private void OnSignUpdated(ISignage signage, BasePlayer player, int textureIndex = 0) { - if (player == null) - { - return; - } - - if (signage.GetTextureCRCs()[textureIndex] == 0) + if (player == null || signage.GetTextureCRCs()[textureIndex] == 0) { return; } @@ -5157,6 +5404,7 @@ private void OnSignUpdated(ISignage signage, BasePlayer player, int textureIndex _RustAppEngine?.SignageWorker?.SignageCreate(new SignageUpdate(player, signage, (uint)textureIndex)); } + // Egg Suit private void OnItemPainted(PaintedItemStorageEntity entity, Item item, BasePlayer player, byte[] image) { if (entity._currentImageCrc == 0) @@ -5179,7 +5427,7 @@ private void OnFireworkDesignChanged(PatternFirework firework, ProtoBuf.PatternF private void OnEntityBuilt(Planner plan, GameObject go) { - if (go.ToBaseEntity() is not ISignage signage || plan.GetOwnerPlayer() is not BasePlayer player) + if (go.ToBaseEntity() is not ISignage signage || !IsPaintable(signage) || plan.GetOwnerPlayer() is not BasePlayer player) { return; } @@ -5194,5 +5442,16 @@ private void OnEntityBuilt(Planner plan, GameObject go) }); } + // Sil external hook + private void OnImagePost(BasePlayer player, string url, bool raw, ISignage signage, uint textureIndex) + { + _RustAppEngine?.SignageWorker?.SignageCreate(new SignageUpdate(player, signage, textureIndex, url)); + } + + private static bool IsPaintable(object entity) + { + return entity is Signage or PhotoFrame or CarvablePumpkin or PaintedItemStorageEntity; + } + #endregion } \ No newline at end of file