From 2dbd39f7416ee9f513fbc5371c216173c7ba3bdd Mon Sep 17 00:00:00 2001 From: AenclaveGames <55506834+IlyaChichkov@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:01:07 +0300 Subject: [PATCH 1/2] feat(client): implement in-game alerts list retrieval - Added new REST endpoint and DTO models to extract active game alerts and notifications. - Wired alerts data retrieval through the Client/UI domain service. --- .../Controllers/Client/UIController.cs | 9 +++ .../RimworldRestApi/Models/UI/AlertsDto.cs | 16 ++++ .../Services/Client/UIService/IUIService.cs | 1 + .../Services/Client/UIService/UIService.cs | 80 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 Source/RIMAPI/RimworldRestApi/Models/UI/AlertsDto.cs diff --git a/Source/RIMAPI/RimworldRestApi/Controllers/Client/UIController.cs b/Source/RIMAPI/RimworldRestApi/Controllers/Client/UIController.cs index 3cd7790..7acdad3 100644 --- a/Source/RIMAPI/RimworldRestApi/Controllers/Client/UIController.cs +++ b/Source/RIMAPI/RimworldRestApi/Controllers/Client/UIController.cs @@ -18,6 +18,15 @@ public UIController(IUIService uiService, IWindowService windowService) _windowService = windowService; } + [Get("/api/v1/ui/alerts")] + [EndpointMetadata("Retrieve a list of all currently active right-hand screen alerts (e.g., 'Need Defenses', 'Major Break Risk').")] + public async Task GetActiveAlerts(HttpListenerContext context) + { + var result = await GameThreadUtility.InvokeAsync(() => _uiService.GetActiveAlerts()); + + await context.SendJsonResponse(result); + } + [Post("/api/v1/ui/message")] public async Task ShowMessage(HttpListenerContext context) { diff --git a/Source/RIMAPI/RimworldRestApi/Models/UI/AlertsDto.cs b/Source/RIMAPI/RimworldRestApi/Models/UI/AlertsDto.cs new file mode 100644 index 0000000..3bda05c --- /dev/null +++ b/Source/RIMAPI/RimworldRestApi/Models/UI/AlertsDto.cs @@ -0,0 +1,16 @@ + +using System.Collections.Generic; + +namespace RIMAPI.Models.UI +{ + public class AlertDto + { + public string Label { get; set; } + public string Explanation { get; set; } + + // e.g., "Critical", "High", "Medium" + public string Priority { get; set; } + public List Targets { get; set; } + public List Cells { get; set; } + } +} \ No newline at end of file diff --git a/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/IUIService.cs b/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/IUIService.cs index 8395a7e..9d7a73a 100644 --- a/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/IUIService.cs +++ b/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/IUIService.cs @@ -7,6 +7,7 @@ namespace RIMAPI.Services.Interfaces { public interface IUIService { + ApiResult> GetActiveAlerts(); ApiResult OpenTab(string tabName); ApiResult SendLetterSimple(SendLetterRequestDto body); } diff --git a/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/UIService.cs b/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/UIService.cs index a96fcb2..eae82f9 100644 --- a/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/UIService.cs +++ b/Source/RIMAPI/RimworldRestApi/Services/Client/UIService/UIService.cs @@ -4,10 +4,12 @@ using RimWorld; using Verse; using RIMAPI.Core; +using RIMAPI.Models.UI; using RIMAPI.Services.Interfaces; using RIMAPI.Helpers; using System.Linq; using RIMAPI.Models; +using RimWorld.Planet; namespace RIMAPI.Services { @@ -18,6 +20,84 @@ public class UIService : IUIService BindingFlags.Instance | BindingFlags.NonPublic ); + public ApiResult> GetActiveAlerts() + { + if (Current.ProgramState != ProgramState.Playing) + { + return ApiResult>.Fail("Cannot fetch alerts: Game is not currently playing."); + } + + if (Find.Alerts == null) + { + return ApiResult>.Fail("Alerts system is currently unavailable."); + } + + if (ActiveAlertsField == null) + { + return ApiResult>.Fail("Reflection failed: Could not find the activeAlerts field."); + } + + var alertDtos = new List(); + + try + { + var activeAlerts = (List)ActiveAlertsField.GetValue(Find.Alerts); + + if (activeAlerts != null) + { + foreach (Alert alert in activeAlerts) + { + try + { + var alertDto = new AlertDto + { + Label = alert.GetLabel(), + Explanation = alert.GetExplanation(), + Priority = alert.Priority.ToString(), + Targets = new List(), + Cells = new List() + }; + + AlertReport report = alert.GetReport(); + if (report.AllCulprits != null) + { + foreach (GlobalTargetInfo target in report.AllCulprits) + { + if (!target.IsValid) continue; + + if (target.HasThing) + { + alertDto.Targets.Add(target.Thing.thingIDNumber); + } + else if (target.HasWorldObject) + { + alertDto.Targets.Add(target.WorldObject.ID); + } + else if (target.Cell.IsValid) + { + alertDto.Cells.Add($"Cell_{target.Cell.x}_{target.Cell.z}"); + } + } + } + + alertDtos.Add(alertDto); + } + catch (Exception ex) + { + Log.Warning($"[RIMAPI] Failed to parse alert of type {alert.GetType().Name}: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + Log.Error($"[RIMAPI] Critical reflection error while reading alerts: {ex}"); + return ApiResult>.Fail("Internal server error reading memory."); + } + + return ApiResult>.Ok(alertDtos); + } + public ApiResult OpenTab(string tabName) { try From d257af8cb213314b24392ea702c6f00a3c4a74a2 Mon Sep 17 00:00:00 2001 From: AenclaveGames <55506834+IlyaChichkov@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:56:59 +0300 Subject: [PATCH 2/2] test(client): add bruno yml configs for alerts endpoint - Created Bruno .yml test configurations to validate and interact with the newly created in-game alerts endpoint. --- tests/bruno_api_collection/Camera/Screenshot.yml | 2 +- tests/bruno_api_collection/UI/Alerts List.yml | 15 +++++++++++++++ tests/bruno_api_collection/UI/folder.yml | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/bruno_api_collection/UI/Alerts List.yml create mode 100644 tests/bruno_api_collection/UI/folder.yml diff --git a/tests/bruno_api_collection/Camera/Screenshot.yml b/tests/bruno_api_collection/Camera/Screenshot.yml index 120152f..3954be7 100644 --- a/tests/bruno_api_collection/Camera/Screenshot.yml +++ b/tests/bruno_api_collection/Camera/Screenshot.yml @@ -11,7 +11,7 @@ http: data: |- { "format": "png", - "hide_ui": false + "hide_ui": true } auth: inherit diff --git a/tests/bruno_api_collection/UI/Alerts List.yml b/tests/bruno_api_collection/UI/Alerts List.yml new file mode 100644 index 0000000..8e901ac --- /dev/null +++ b/tests/bruno_api_collection/UI/Alerts List.yml @@ -0,0 +1,15 @@ +info: + name: Alerts List + type: http + seq: 1 + +http: + method: GET + url: "{{baseURL}}/api/v1/ui/alerts" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 \ No newline at end of file diff --git a/tests/bruno_api_collection/UI/folder.yml b/tests/bruno_api_collection/UI/folder.yml new file mode 100644 index 0000000..dbde1c5 --- /dev/null +++ b/tests/bruno_api_collection/UI/folder.yml @@ -0,0 +1,6 @@ +info: + name: UI + type: folder + seq: 22 + +request: {}