Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Source/RIMAPI/RIMAPI_Mod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public class RIMAPI_Mod : Mod
/// </summary>
public RIMAPI_Mod(ModContentPack content) : base(content)
{
GameObject dispatcherObject = new GameObject("RIMAPI_ThreadDispatcher");
Object.DontDestroyOnLoad(dispatcherObject);
dispatcherObject.AddComponent<GameThreadDispatcher>();

Settings = GetSettings<RIMAPI_Settings>();
InitializeHarmony();
}
Expand Down
27 changes: 23 additions & 4 deletions Source/RIMAPI/RimworldRestApi/BaseControllers/CameraController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using RIMAPI.Core;
using RIMAPI.Http;
using RIMAPI.Models;
using RIMAPI.Models.Camera;
using RIMAPI.Services;
using Verse;

Expand Down Expand Up @@ -43,23 +44,41 @@ public async Task MoveToPosition(HttpListenerContext context)
await context.SendJsonResponse(result);
}

[Post("/api/v1/stream/start")]
[Post("/api/v1/camera/screenshot")]
[EndpointMetadata("Capture a screenshot of the game view. Supports resizing and format changes.")]
public async Task MakeScreenshot(HttpListenerContext context)
{
var request = await context.Request.ReadBodyAsync<CameraScreenshotRequestDto>();
var result = await _cameraService.MakeScreenshotAsync(request);
await context.SendJsonResponse(result);
}

[Post("/api/v1/camera/screenshot/native")]
[EndpointMetadata("Moves the camera and saves a high-quality screenshot directly to the host machine's hard drive.")]
public async Task TriggerNativeScreenshot(HttpListenerContext context)
{
var request = await context.Request.ReadBodyAsync<NativeScreenshotRequestDto>();
var result = _cameraService.TakeNativeScreenshot(request);
await context.SendJsonResponse(result);
}

[Post("/api/v1/camera/stream/start")]
[EndpointMetadata("Start game camera stream")]
public async Task PostStreamStart(HttpListenerContext context)
{
var result = _cameraService.StartStream(_cameraStream);
await context.SendJsonResponse(result);
}

[Post("/api/v1/stream/stop")]
[Post("/api/v1/camera/stream/stop")]
[EndpointMetadata("Stop game camera stream")]
public async Task PostStreamStop(HttpListenerContext context)
{
var result = _cameraService.StopStream(_cameraStream);
await context.SendJsonResponse(result);
}

[Post("/api/v1/stream/setup")]
[Post("/api/v1/camera/stream/setup")]
[EndpointMetadata("Set game camera stream configuration")]
public async Task PostStreamSetup(HttpListenerContext context)
{
Expand All @@ -68,7 +87,7 @@ public async Task PostStreamSetup(HttpListenerContext context)
await context.SendJsonResponse(result);
}

[Get("/api/v1/stream/status")]
[Get("/api/v1/camera/stream/status")]
[EndpointMetadata("Get game camera stream status")]
public async Task GetStreamStatus(HttpListenerContext context)
{
Expand Down
85 changes: 85 additions & 0 deletions Source/RIMAPI/RimworldRestApi/Core/GameThreadUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace RIMAPI.Core
{
// 1. The hidden Unity object that runs on the main game thread
public class GameThreadDispatcher : MonoBehaviour
{
// Thread-safe queue to hold actions coming from the HTTP server
private static readonly ConcurrentQueue<Action> _executionQueue = new ConcurrentQueue<Action>();

public void Update()
{
// Drain the queue every frame.
// We limit to 50 per frame so a massive API spam doesn't drop the game's FPS to zero.
int processed = 0;
while (processed < 50 && _executionQueue.TryDequeue(out var action))
{
try
{
action();
}
catch (Exception ex)
{
Log.Error($"[RIMAPI] Error executing API action on main thread: {ex}");
}
processed++;
}
}

public static void Enqueue(Action action)
{
_executionQueue.Enqueue(action);
}
}

// 2. The utility wrapper that gives us beautiful async/await syntax in our Controllers
public static class GameThreadUtility
{
// For methods that return a value (e.g., getting a screenshot or pawn data)
public static Task<T> InvokeAsync<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();

GameThreadDispatcher.Enqueue(() =>
{
try
{
T result = func();
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});

return tcs.Task;
}

// For methods that just execute an action (e.g., forcing a job, dropping an item)
public static Task InvokeAsync(Action action)
{
var tcs = new TaskCompletionSource<bool>();

GameThreadDispatcher.Enqueue(() =>
{
try
{
action();
tcs.TrySetResult(true);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});

return tcs.Task;
}
}
}
57 changes: 57 additions & 0 deletions Source/RIMAPI/RimworldRestApi/Models/Camera/CameraDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Newtonsoft.Json;

namespace RIMAPI.Models.Camera
{
public class CameraScreenshotRequestDto
{
[JsonProperty("format")]
public string Format { get; set; } = "jpeg";

[JsonProperty("quality")]
public int Quality { get; set; } = 75;

[JsonProperty("width")]
public int? Width { get; set; }

[JsonProperty("height")]
public int? Height { get; set; }

[JsonProperty("hide_ui")]
public bool HideUI { get; set; } = true;
}


public class NativeScreenshotRequestDto
{
public string FileName { get; set; } // Optional: Game auto-generates if empty
public float? CenterX { get; set; }
public float? CenterZ { get; set; }
public float? ZoomLevel { get; set; } // Typically 10 to 60
public bool HideUI { get; set; } = true;
}

public class CameraScreenshotResponseDto
{
public ImageData Image { get; set; }
public ImageMetadata Metadata { get; set; }
public GameContext GameContext { get; set; }
}

public class ImageData
{
public string DataUri { get; set; }
}

public class ImageMetadata
{
public string Format { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int SizeBytes { get; set; }
}

public class GameContext
{
public int CurrentTick { get; set; }
}
}
Loading