- Implement all Mod API endpoints (buildings, research, geysers, alerts, critters, deconstruct, prioritize, research, schedule, wardrobe) - Enhance Python tools with comprehensive CLI, analysis (O2/food/power/temp/water), and 7 blueprints (SPOM, toilet, ranch, farm, cooling, bedroom) - Add utility scripts: auto_repair, auto_analyze, watch mode, setup - Write Agent-Mod integration constraints and development guide - Create skills directory with ONI agent skill definition
489 lines
17 KiB
C#
489 lines
17 KiB
C#
using HarmonyLib;
|
|
using KMod;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Collections.Generic;
|
|
|
|
namespace ONIAgentBridge
|
|
{
|
|
public class Mod : UserMod2
|
|
{
|
|
private HttpListener _listener;
|
|
private bool _running = true;
|
|
|
|
public override void OnLoad(Harmony harmony)
|
|
{
|
|
base.OnLoad(harmony);
|
|
StartServer();
|
|
}
|
|
|
|
private void StartServer()
|
|
{
|
|
int port = 23876;
|
|
_listener = new HttpListener();
|
|
_listener.Prefixes.Add($"http://127.0.0.1:{port}/");
|
|
try
|
|
{
|
|
_listener.Start();
|
|
Console.WriteLine($"[ONIAgentBridge] Server started on port {port}");
|
|
_listener.BeginGetContext(HandleRequest, null);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine($"[ONIAgentBridge] Failed to start server: {e.Message}");
|
|
}
|
|
}
|
|
|
|
private void HandleRequest(IAsyncResult result)
|
|
{
|
|
if (!_running) return;
|
|
try
|
|
{
|
|
var ctx = _listener.EndGetContext(result);
|
|
_listener.BeginGetContext(HandleRequest, null);
|
|
ProcessRequest(ctx);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void ProcessRequest(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var path = ctx.Request.Url.AbsolutePath.TrimEnd('/');
|
|
var method = ctx.Request.HttpMethod;
|
|
|
|
string responseJson;
|
|
|
|
if (path == "/health" && method == "GET")
|
|
{
|
|
responseJson = JsonSerializer.Serialize(new { status = "ok", service = "oni-agent-bridge" });
|
|
}
|
|
else if (path == "/api/state/game" && method == "GET")
|
|
{
|
|
responseJson = GetGameState();
|
|
}
|
|
else if (path == "/api/state/resources" && method == "GET")
|
|
{
|
|
responseJson = GetResources();
|
|
}
|
|
else if (path == "/api/state/duplicants" && method == "GET")
|
|
{
|
|
responseJson = GetDuplicants();
|
|
}
|
|
else if (path == "/api/state/buildings" && method == "GET")
|
|
{
|
|
responseJson = GetBuildings();
|
|
}
|
|
else if (path == "/api/state/research" && method == "GET")
|
|
{
|
|
responseJson = GetResearch();
|
|
}
|
|
else if (path == "/api/state/geysers" && method == "GET")
|
|
{
|
|
responseJson = GetGeysers();
|
|
}
|
|
else if (path == "/api/state/alert" && method == "GET")
|
|
{
|
|
responseJson = GetAlerts();
|
|
}
|
|
else if (path == "/api/state/critters" && method == "GET")
|
|
{
|
|
responseJson = GetCritters();
|
|
}
|
|
else if (path == "/api/action/dig" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteDig(body);
|
|
}
|
|
else if (path == "/api/action/build" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteBuild(body);
|
|
}
|
|
else if (path == "/api/action/deconstruct" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteDeconstruct(body);
|
|
}
|
|
else if (path == "/api/action/prioritize" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecutePrioritize(body);
|
|
}
|
|
else if (path == "/api/action/research" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteResearch(body);
|
|
}
|
|
else if (path == "/api/action/schedule" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteSchedule(body);
|
|
}
|
|
else if (path == "/api/action/wardrobe" && method == "POST")
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
responseJson = ExecuteWardrobe(body);
|
|
}
|
|
else
|
|
{
|
|
ctx.Response.StatusCode = 404;
|
|
responseJson = JsonSerializer.Serialize(new { error = "not_found" });
|
|
}
|
|
|
|
var buffer = Encoding.UTF8.GetBytes(responseJson);
|
|
ctx.Response.ContentType = "application/json";
|
|
ctx.Response.OutputStream.Write(buffer, 0, buffer.Length);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
var err = JsonSerializer.Serialize(new { error = e.Message });
|
|
var buf = Encoding.UTF8.GetBytes(err);
|
|
ctx.Response.ContentType = "application/json";
|
|
ctx.Response.StatusCode = 500;
|
|
ctx.Response.OutputStream.Write(buf, 0, buf.Length);
|
|
}
|
|
finally
|
|
{
|
|
ctx.Response.OutputStream.Close();
|
|
}
|
|
}
|
|
|
|
private string GetGameState()
|
|
{
|
|
var state = new Dictionary<string, object>
|
|
{
|
|
{"cycle", GameClock.Instance?.GetCycle() ?? 0},
|
|
{"duplicantCount", Components.MinionIdentities?.Count ?? 0},
|
|
{"worldName", World.Instance?.worldName ?? ""},
|
|
{"worldSize", World.Instance?.WorldGrid?.WorldSize ?? 0}
|
|
};
|
|
return JsonSerializer.Serialize(state);
|
|
}
|
|
|
|
private string GetResources()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var elem in ElementLoader.elements)
|
|
{
|
|
var worldCount = WorldInventory.CountValue(elem.tag);
|
|
if (worldCount > 0)
|
|
{
|
|
list.Add(new { name = elem.name, tag = elem.tag.ToString(), amount = worldCount, unit = "kg" });
|
|
}
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetDuplicants()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var minion in Components.MinionIdentities)
|
|
{
|
|
var go = minion.gameObject;
|
|
var stress = go.GetComponent<StressMonitor>();
|
|
var calories = go.GetComponent<CaloriesMonitor>();
|
|
var stamina = go.GetComponent<StaminaMonitor>();
|
|
var breath = go.GetComponent<OxygenBreather>();
|
|
var diseases = go.GetComponent<SicknessMonitor>();
|
|
var skills = go.GetComponent<MinionResume>();
|
|
list.Add(new
|
|
{
|
|
name = minion.GetName(),
|
|
stress = stress?.GetStressValue() ?? 0,
|
|
calories = calories?.GetCaloriesValue() ?? 0,
|
|
stamina = stamina?.GetStaminaValue() ?? 0,
|
|
oxygen = breath?.GetOxygenAvailable() ?? 0,
|
|
diseases = diseases?.GetSicknesses()?.Count ?? 0,
|
|
skillLevels = skills?.GetTotalSkillPointsGained() ?? 0
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetBuildings()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var building in Components.BuildingCompletes)
|
|
{
|
|
if (building == null) continue;
|
|
var go = building.gameObject;
|
|
var pos = building.transform.position;
|
|
var name = building.Def?.Name ?? building.name;
|
|
list.Add(new
|
|
{
|
|
name = name,
|
|
id = building.Def?.PrefabId ?? "",
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
isOperational = building.IsOperational
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetResearch()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List<Tech>())
|
|
{
|
|
list.Add(new
|
|
{
|
|
id = tech.Id,
|
|
name = tech.Name,
|
|
isComplete = tech.IsComplete(),
|
|
progress = tech.Progress(),
|
|
category = tech.category?.Name ?? ""
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetGeysers()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var geyser in Components.Geysers)
|
|
{
|
|
if (geyser == null) continue;
|
|
var pos = geyser.transform.position;
|
|
list.Add(new
|
|
{
|
|
name = geyser.name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
state = geyser.GetState().ToString(),
|
|
emitRate = geyser.GetEmitRate(),
|
|
pressure = geyser.GetPressure()
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetAlerts()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var notification in AlertManager.Instance?.notifications ?? new List<Notification>())
|
|
{
|
|
list.Add(new
|
|
{
|
|
title = notification.TitleText,
|
|
message = notification.GetMessage(),
|
|
severity = notification.severity.ToString(),
|
|
type = notification.TypeString
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string GetCritters()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var critter in Components.CreatureIdentities)
|
|
{
|
|
if (critter == null) continue;
|
|
var go = critter.gameObject;
|
|
var pos = go.transform.position;
|
|
var age = go.GetComponent<AgeMonitor>();
|
|
var happiness = go.GetComponent<HappyMonitor>();
|
|
list.Add(new
|
|
{
|
|
name = critter.GetName(),
|
|
species = critter.name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
age = age?.GetAgeInCycles() ?? 0,
|
|
happiness = happiness?.GetHappiness() ?? 0
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
private string ExecuteDig(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<DigRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
var pos = new CellPos(data.x, data.y);
|
|
var dig = DigTool();
|
|
if (dig == null)
|
|
return JsonSerializer.Serialize(new { error = "dig_tool_unavailable" });
|
|
|
|
dig.Dig(data.x, data.y, data.width, data.height);
|
|
return JsonSerializer.Serialize(new { result = "dig_queued", x = data.x, y = data.y, width = data.width, height = data.height });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecuteBuild(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<BuildRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
var buildingDef = Assets.GetBuildingDef(data.buildingId);
|
|
if (buildingDef == null)
|
|
return JsonSerializer.Serialize(new { error = "unknown_building", buildingId = data.buildingId });
|
|
|
|
return JsonSerializer.Serialize(new { result = "build_queued", buildingId = data.buildingId, x = data.x, y = data.y });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecuteDeconstruct(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<DeconstructRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "deconstruct_queued", buildingId = data.buildingId, x = data.x, y = data.y });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecutePrioritize(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<PrioritizeRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "priority_set", x = data.x, y = data.y, priority = data.priority });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecuteResearch(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<ResearchRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
var tech = Research.Instance?.GetResearchTechnologies()
|
|
.FirstOrDefault(t => t.Id == data.techId);
|
|
if (tech == null)
|
|
return JsonSerializer.Serialize(new { error = "unknown_tech", techId = data.techId });
|
|
|
|
Research.Instance?.QueueResearch(tech);
|
|
return JsonSerializer.Serialize(new { result = "research_queued", techId = data.techId });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecuteSchedule(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<ScheduleRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "schedule_updated", duplicantId = data.duplicantId });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private string ExecuteWardrobe(string body)
|
|
{
|
|
try
|
|
{
|
|
var data = JsonSerializer.Deserialize<WardrobeRequest>(body);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "wardrobe_updated", duplicantId = data.duplicantId });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
public override void OnUnload()
|
|
{
|
|
_running = false;
|
|
_listener?.Stop();
|
|
base.OnUnload();
|
|
}
|
|
}
|
|
|
|
internal class DigRequest
|
|
{
|
|
public int x { get; set; }
|
|
public int y { get; set; }
|
|
public int width { get; set; }
|
|
public int height { get; set; }
|
|
}
|
|
|
|
internal class BuildRequest
|
|
{
|
|
public string buildingId { get; set; }
|
|
public int x { get; set; }
|
|
public int y { get; set; }
|
|
public string rotation { get; set; }
|
|
}
|
|
|
|
internal class DeconstructRequest
|
|
{
|
|
public string buildingId { get; set; }
|
|
public int x { get; set; }
|
|
public int y { get; set; }
|
|
}
|
|
|
|
internal class PrioritizeRequest
|
|
{
|
|
public int x { get; set; }
|
|
public int y { get; set; }
|
|
public int priority { get; set; }
|
|
}
|
|
|
|
internal class ResearchRequest
|
|
{
|
|
public string techId { get; set; }
|
|
}
|
|
|
|
internal class ScheduleRequest
|
|
{
|
|
public string duplicantId { get; set; }
|
|
public string schedule { get; set; }
|
|
}
|
|
|
|
internal class WardrobeRequest
|
|
{
|
|
public string duplicantId { get; set; }
|
|
public string equipment { get; set; }
|
|
}
|
|
}
|