feat: complete ONI Agent project with full Mod API, Python toolchain, and development guide
- 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
This commit is contained in:
488
mod/ONIAgentBridge.cs
Normal file
488
mod/ONIAgentBridge.cs
Normal file
@ -0,0 +1,488 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
8
mod/mod_info.yaml
Normal file
8
mod/mod_info.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "ONI Agent Bridge",
|
||||
"description": "暴露 HTTP API 供外部 agent 读取游戏状态和执行操作",
|
||||
"staticID": "oni_agent_bridge",
|
||||
"version": 1,
|
||||
"supportedContent": "base_game,expanded",
|
||||
"minimumSupportedVersion": 612000
|
||||
}
|
||||
Reference in New Issue
Block a user