- Add cell/tile map APIs: /api/state/cell, /api/state/cells, /api/state/cells/slice, /api/state/gas - Add entity registry APIs: /api/registry/buildings, /api/registry/elements, /api/registry/techs - Add plants, rooms, mop, harvest endpoints - Rich semantic metadata: element state/category, building category/power, duplicant chore/cell - AI-friendly coordinate system with (x,y) + cell index in all responses - Build AI_KNOWLEDGE_BASE.md with building IDs, element IDs, tech trees, game mechanics - Rewrite SKILL.md with data model explanation, coordinate guide, operation patterns - Update Python tools: explore, cell, cells, slice, gas, registry subcommands - Update MOD_DEV_GUIDE.md with AI data design principles
992 lines
40 KiB
C#
992 lines
40 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;
|
|
var query = ctx.Request.QueryString;
|
|
|
|
string responseJson;
|
|
|
|
switch (path, method)
|
|
{
|
|
// --- Health ---
|
|
case ("/health", "GET"):
|
|
responseJson = JsonSerializer.Serialize(new { status = "ok", service = "oni-agent-bridge" });
|
|
break;
|
|
|
|
// --- State Queries ---
|
|
case ("/api/state/game", "GET"):
|
|
responseJson = GetGameState();
|
|
break;
|
|
case ("/api/state/resources", "GET"):
|
|
responseJson = GetResources();
|
|
break;
|
|
case ("/api/state/duplicants", "GET"):
|
|
responseJson = GetDuplicants();
|
|
break;
|
|
case ("/api/state/buildings", "GET"):
|
|
responseJson = GetBuildings();
|
|
break;
|
|
case ("/api/state/research", "GET"):
|
|
responseJson = GetResearch();
|
|
break;
|
|
case ("/api/state/geysers", "GET"):
|
|
responseJson = GetGeysers();
|
|
break;
|
|
case ("/api/state/alert", "GET"):
|
|
responseJson = GetAlerts();
|
|
break;
|
|
case ("/api/state/critters", "GET"):
|
|
responseJson = GetCritters();
|
|
break;
|
|
case ("/api/state/plants", "GET"):
|
|
responseJson = GetPlants();
|
|
break;
|
|
case ("/api/state/rooms", "GET"):
|
|
responseJson = GetRooms();
|
|
break;
|
|
|
|
// --- Cell-level map data ---
|
|
case ("/api/state/cell", "GET"):
|
|
responseJson = GetCell(query);
|
|
break;
|
|
case ("/api/state/cells", "GET"):
|
|
responseJson = GetCells(query);
|
|
break;
|
|
case ("/api/state/cells/slice", "GET"):
|
|
responseJson = GetCellSlice(query);
|
|
break;
|
|
case ("/api/state/gas", "GET"):
|
|
responseJson = GetGas(query);
|
|
break;
|
|
|
|
// --- Entity Registry (for AI reference) ---
|
|
case ("/api/registry/buildings", "GET"):
|
|
responseJson = GetBuildingRegistry();
|
|
break;
|
|
case ("/api/registry/elements", "GET"):
|
|
responseJson = GetElementRegistry();
|
|
break;
|
|
case ("/api/registry/techs", "GET"):
|
|
responseJson = GetTechRegistry();
|
|
break;
|
|
|
|
// --- Actions ---
|
|
case ("/api/action/dig", "POST"):
|
|
responseJson = ExecuteDig(ctx);
|
|
break;
|
|
case ("/api/action/build", "POST"):
|
|
responseJson = ExecuteBuild(ctx);
|
|
break;
|
|
case ("/api/action/deconstruct", "POST"):
|
|
responseJson = ExecuteDeconstruct(ctx);
|
|
break;
|
|
case ("/api/action/prioritize", "POST"):
|
|
responseJson = ExecutePrioritize(ctx);
|
|
break;
|
|
case ("/api/action/research", "POST"):
|
|
responseJson = ExecuteResearch(ctx);
|
|
break;
|
|
case ("/api/action/schedule", "POST"):
|
|
responseJson = ExecuteSchedule(ctx);
|
|
break;
|
|
case ("/api/action/wardrobe", "POST"):
|
|
responseJson = ExecuteWardrobe(ctx);
|
|
break;
|
|
case ("/api/action/mop", "POST"):
|
|
responseJson = ExecuteMop(ctx);
|
|
break;
|
|
case ("/api/action/harvest", "POST"):
|
|
responseJson = ExecuteHarvest(ctx);
|
|
break;
|
|
case ("/api/action/cancel", "POST"):
|
|
responseJson = ExecuteCancel(ctx);
|
|
break;
|
|
|
|
default:
|
|
ctx.Response.StatusCode = 404;
|
|
responseJson = JsonSerializer.Serialize(new { error = "not_found", path = path, method = method });
|
|
break;
|
|
}
|
|
|
|
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, type = e.GetType().Name });
|
|
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();
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Game State
|
|
// ===================================================================
|
|
private string GetGameState()
|
|
{
|
|
int cellCount = 0;
|
|
try { cellCount = Grid.CellCount; } catch { }
|
|
|
|
var state = new Dictionary<string, object>
|
|
{
|
|
{"cycle", GameClock.Instance?.GetCycle() ?? 0},
|
|
{"duplicantCount", Components.MinionIdentities?.Count ?? 0},
|
|
{"worldName", World.Instance?.worldName ?? ""},
|
|
{"worldSize", cellCount},
|
|
{"gridWidth", Grid.WidthInCells},
|
|
{"gridHeight", Grid.HeightInCells}
|
|
};
|
|
return JsonSerializer.Serialize(state);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Resources
|
|
// ===================================================================
|
|
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
|
|
{
|
|
id = elem.id.ToString(),
|
|
name = elem.name,
|
|
tag = elem.tag.ToString(),
|
|
amount = worldCount,
|
|
unit = "kg",
|
|
state = GetElementStateCategory(elem),
|
|
category = GetElementCategory(elem)
|
|
});
|
|
}
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Duplicants
|
|
// ===================================================================
|
|
private string GetDuplicants()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var minion in Components.MinionIdentities)
|
|
{
|
|
var go = minion.gameObject;
|
|
var pos = go.transform.position;
|
|
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>();
|
|
var ai = go.GetComponent<ChoreConsumer>();
|
|
var nav = go.GetComponent<Navigator>();
|
|
|
|
list.Add(new
|
|
{
|
|
name = minion.GetName(),
|
|
id = minion.GetProperName(),
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos),
|
|
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,
|
|
currentChore = ai?.GetCurrentChore()?.GetType()?.Name ?? "idle",
|
|
isSleeping = nav?.IsMoving() == false && ai?.GetCurrentChore()?.GetType()?.Name == "SleepChore"
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Buildings
|
|
// ===================================================================
|
|
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 def = building.Def;
|
|
var energy = go.GetComponent<EnergyConsumer>();
|
|
var conduit = go.GetComponent<ConduitConsumer>();
|
|
var storage = go.GetComponent<Storage>();
|
|
|
|
list.Add(new
|
|
{
|
|
id = def?.PrefabId ?? "",
|
|
name = def?.Name ?? building.name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos),
|
|
width = def?.Width ?? 1,
|
|
height = def?.Height ?? 1,
|
|
isOperational = building.IsOperational,
|
|
category = GetBuildingCategory(def?.PrefabId ?? ""),
|
|
powerWatt = energy?.WattsNeededWhenActive ?? 0,
|
|
isPowered = energy?.IsPowered ?? true,
|
|
storageKg = storage?.MassStored() ?? 0
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Research
|
|
// ===================================================================
|
|
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 ?? "",
|
|
requiredTechs = tech.requiredTechs?.Select(t => t.Id).ToList() ?? new List<string>(),
|
|
unlockedBuildings = tech.unlockedBuildings?.ToList() ?? new List<string>()
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Geysers
|
|
// ===================================================================
|
|
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
|
|
{
|
|
id = geyser.name,
|
|
name = geyser.GetType().Name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos),
|
|
state = geyser.GetState().ToString(),
|
|
emitRate = geyser.GetEmitRate(),
|
|
pressure = geyser.GetPressure(),
|
|
isActive = geyser.IsActive(),
|
|
isDormant = geyser.IsDormant()
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Alerts
|
|
// ===================================================================
|
|
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,
|
|
clickable = notification.clickable
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Critters
|
|
// ===================================================================
|
|
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>();
|
|
var cal = go.GetComponent<CaloriesMonitor>();
|
|
|
|
list.Add(new
|
|
{
|
|
id = critter.GetProperName(),
|
|
name = critter.GetName(),
|
|
species = critter.name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos),
|
|
age = age?.GetAgeInCycles() ?? 0,
|
|
happiness = happiness?.GetHappiness() ?? 0,
|
|
calories = cal?.GetCaloriesValue() ?? 0
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Plants
|
|
// ===================================================================
|
|
private string GetPlants()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var plant in Components.CropSleepingMonitor)
|
|
{
|
|
if (plant == null) continue;
|
|
var go = plant.gameObject;
|
|
var pos = go.transform.position;
|
|
var growing = go.GetComponent<Growing>();
|
|
|
|
list.Add(new
|
|
{
|
|
id = go.name,
|
|
name = growing?.GetPlantID() ?? go.name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos),
|
|
isGrown = growing?.IsGrown() ?? false,
|
|
progress = growing?.GetProgress() ?? 0,
|
|
isWilting = growing?.IsWilting() ?? false
|
|
});
|
|
}
|
|
if (list.Count == 0)
|
|
{
|
|
foreach (var plant in Components.Plants)
|
|
{
|
|
if (plant == null) continue;
|
|
var go = plant.gameObject;
|
|
var pos = go.transform.position;
|
|
list.Add(new
|
|
{
|
|
id = go.name,
|
|
name = plant.Name,
|
|
x = (int)pos.x,
|
|
y = (int)pos.y,
|
|
cell = Grid.PosToCell(pos)
|
|
});
|
|
}
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Rooms
|
|
// ===================================================================
|
|
private string GetRooms()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var room in Game.Instance?.roomManager?.rooms ?? new List<Room>())
|
|
{
|
|
list.Add(new
|
|
{
|
|
id = room.cavity?.GetType()?.Name ?? "unknown",
|
|
name = room.roomType?.Name ?? "unknown",
|
|
type = room.roomType?.Id ?? "unknown",
|
|
cellCount = room.cavity?.numCells ?? 0,
|
|
buildings = room.cavity?.buildings?.Count ?? 0,
|
|
creatures = room.cavity?.creatures?.Count ?? 0,
|
|
plants = room.cavity?.plants?.Count ?? 0
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Cell - single cell detail
|
|
// ===================================================================
|
|
private string GetCell(System.Collections.Specialized.NameValueCollection query)
|
|
{
|
|
try
|
|
{
|
|
int x = int.Parse(query["x"] ?? "-1");
|
|
int y = int.Parse(query["y"] ?? "-1");
|
|
|
|
int cell = Grid.XYToCell(x, y);
|
|
if (cell < 0 || cell >= Grid.CellCount)
|
|
return JsonSerializer.Serialize(new { error = "cell_out_of_bounds", x = x, y = y });
|
|
|
|
return JsonSerializer.Serialize(MakeCellData(cell, x, y));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Cells - rectangular region
|
|
// ===================================================================
|
|
private string GetCells(System.Collections.Specialized.NameValueCollection query)
|
|
{
|
|
try
|
|
{
|
|
int x = int.Parse(query["x"] ?? "0");
|
|
int y = int.Parse(query["y"] ?? "0");
|
|
int w = int.Parse(query["width"] ?? "10");
|
|
int h = int.Parse(query["height"] ?? "10");
|
|
|
|
var cells = new List<object>();
|
|
for (int cy = y; cy < y + h; cy++)
|
|
{
|
|
for (int cx = x; cx < x + w; cx++)
|
|
{
|
|
int cell = Grid.XYToCell(cx, cy);
|
|
if (cell >= 0 && cell < Grid.CellCount)
|
|
{
|
|
cells.Add(MakeCellData(cell, cx, cy));
|
|
}
|
|
}
|
|
}
|
|
return JsonSerializer.Serialize(new
|
|
{
|
|
region = new { x, y, width = w, height = h },
|
|
cells = cells
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Cells/slice - row or column scan
|
|
// ===================================================================
|
|
private string GetCellSlice(System.Collections.Specialized.NameValueCollection query)
|
|
{
|
|
try
|
|
{
|
|
string axis = query["axis"] ?? "x";
|
|
int index = int.Parse(query["index"] ?? "0");
|
|
int start = int.Parse(query["start"] ?? "0");
|
|
int end = int.Parse(query["end"] ?? "100");
|
|
|
|
var cells = new List<object>();
|
|
if (axis == "y")
|
|
{
|
|
for (int cx = start; cx < end; cx++)
|
|
{
|
|
int cell = Grid.XYToCell(cx, index);
|
|
if (cell >= 0 && cell < Grid.CellCount)
|
|
cells.Add(MakeCellData(cell, cx, index));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int cy = start; cy < end; cy++)
|
|
{
|
|
int cell = Grid.XYToCell(index, cy);
|
|
if (cell >= 0 && cell < Grid.CellCount)
|
|
cells.Add(MakeCellData(cell, index, cy));
|
|
}
|
|
}
|
|
return JsonSerializer.Serialize(new { axis, index, cells });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Gas overview - find gas pockets
|
|
// ===================================================================
|
|
private string GetGas(System.Collections.Specialized.NameValueCollection query)
|
|
{
|
|
try
|
|
{
|
|
int x = int.Parse(query["x"] ?? "0");
|
|
int y = int.Parse(query["y"] ?? "0");
|
|
int radius = int.Parse(query["radius"] ?? "20");
|
|
|
|
var gases = new Dictionary<string, object>();
|
|
int cell = Grid.XYToCell(x, y);
|
|
if (cell < 0 || cell >= Grid.CellCount)
|
|
return JsonSerializer.Serialize(new { error = "invalid_center" });
|
|
|
|
int minX = Math.Max(0, x - radius);
|
|
int maxX = Math.Min(Grid.WidthInCells - 1, x + radius);
|
|
int minY = Math.Max(0, y - radius);
|
|
int maxY = Math.Min(Grid.HeightInCells - 1, y + radius);
|
|
|
|
for (int cy = minY; cy <= maxY; cy++)
|
|
{
|
|
for (int cx = minX; cx <= maxX; cx++)
|
|
{
|
|
int c = Grid.XYToCell(cx, cy);
|
|
if (c < 0) continue;
|
|
var elem = Grid.Element[c];
|
|
if (elem != null && elem.IsGas)
|
|
{
|
|
float mass = Grid.Mass[c];
|
|
string name = elem.name;
|
|
if (gases.ContainsKey(name))
|
|
{
|
|
var e = (GasEntry)gases[name];
|
|
e.mass += mass;
|
|
e.count++;
|
|
}
|
|
else
|
|
{
|
|
gases[name] = new GasEntry { gas = name, mass = mass, count = 1 };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return JsonSerializer.Serialize(new
|
|
{
|
|
center = new { x, y },
|
|
radius,
|
|
gases = gases.Values
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonSerializer.Serialize(new { error = e.Message });
|
|
}
|
|
}
|
|
|
|
private class GasEntry
|
|
{
|
|
public string gas { get; set; }
|
|
public float mass { get; set; }
|
|
public int count { get; set; }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Cell Data Builder
|
|
// ===================================================================
|
|
private object MakeCellData(int cell, int x, int y)
|
|
{
|
|
var elem = Grid.Element[cell];
|
|
float mass = Grid.Mass[cell];
|
|
float temp = Grid.Temperature[cell];
|
|
bool isSolid = Grid.Solid[cell];
|
|
bool isVisible = Grid.IsVisible[cell];
|
|
|
|
var building = Grid.Objects[cell, (int)ObjectLayer.Building];
|
|
var pickupable = Grid.Objects[cell, (int)ObjectLayer.Pickupables];
|
|
var dupe = Grid.Objects[cell, (int)ObjectLayer.Minion];
|
|
|
|
return new
|
|
{
|
|
x,
|
|
y,
|
|
cell,
|
|
element = elem?.name ?? "Vacuum",
|
|
elementId = elem?.id.ToString() ?? "Vacuum",
|
|
elementState = elem == null ? "vacuum" : (elem.IsGas ? "gas" : elem.IsLiquid ? "liquid" : "solid"),
|
|
massKg = mass,
|
|
temperatureC = temp > 0 ? temp - 273.15f : -273.15f,
|
|
temperatureK = temp,
|
|
isSolid,
|
|
isVisible,
|
|
isLiquid = elem?.IsLiquid ?? false,
|
|
isGas = elem?.IsGas ?? false,
|
|
hasBuilding = building != null,
|
|
buildingName = building?.name ?? null,
|
|
hasPickupable = pickupable != null,
|
|
hasDuplicant = dupe != null,
|
|
duplicantName = dupe?.GetComponent<MinionIdentity>()?.GetName() ?? null,
|
|
isVacuum = elem == null,
|
|
pressure = elem == null ? 0 : mass
|
|
};
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Building Registry (AI reference)
|
|
// ===================================================================
|
|
private string GetBuildingRegistry()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var def in Assets.BuildingDefs)
|
|
{
|
|
if (def == null) continue;
|
|
list.Add(new
|
|
{
|
|
id = def.PrefabId,
|
|
name = def.Name,
|
|
category = def.Category.ToString(),
|
|
width = def.Width,
|
|
height = def.Height,
|
|
powerCost = def.EnergyConsumptionWhenActive,
|
|
heatGeneration = def.ExhaustKilowattsWhenActive,
|
|
massKg = def.Mass,
|
|
constructionMass = def.Materials?.Select(m => m.tag.ToString()).ToList() ?? new List<string>(),
|
|
effects = new { }
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Element Registry (AI reference)
|
|
// ===================================================================
|
|
private string GetElementRegistry()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var elem in ElementLoader.elements)
|
|
{
|
|
if (elem == null) continue;
|
|
list.Add(new
|
|
{
|
|
id = elem.id.ToString(),
|
|
name = elem.name,
|
|
state = GetElementStateCategory(elem),
|
|
category = GetElementCategory(elem),
|
|
specificHeatCapacity = elem.specificHeatCapacity,
|
|
thermalConductivity = elem.thermalConductivity,
|
|
meltingPoint = elem.meltingPoint,
|
|
boilingPoint = elem.vaporizationPoint,
|
|
molarMass = elem.molarMass
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// API: Tech Registry (AI reference)
|
|
// ===================================================================
|
|
private string GetTechRegistry()
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List<Tech>())
|
|
{
|
|
list.Add(new
|
|
{
|
|
id = tech.Id,
|
|
name = tech.Name,
|
|
category = tech.category?.Name ?? "",
|
|
requiredTechs = tech.requiredTechs?.Select(t => t.Id).ToList() ?? new List<string>(),
|
|
unlockedBuildings = tech.unlockedBuildings?.ToList() ?? new List<string>()
|
|
});
|
|
}
|
|
return JsonSerializer.Serialize(list);
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Dig
|
|
// ===================================================================
|
|
private string ExecuteDig(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<DigRequest>(ctx);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
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 }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Build
|
|
// ===================================================================
|
|
private string ExecuteBuild(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<BuildRequest>(ctx);
|
|
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,
|
|
name = buildingDef.Name,
|
|
x = data.x,
|
|
y = data.y,
|
|
width = buildingDef.Width,
|
|
height = buildingDef.Height
|
|
});
|
|
}
|
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Deconstruct
|
|
// ===================================================================
|
|
private string ExecuteDeconstruct(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<DeconstructRequest>(ctx);
|
|
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 }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Prioritize
|
|
// ===================================================================
|
|
private string ExecutePrioritize(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<PrioritizeRequest>(ctx);
|
|
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 }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Research
|
|
// ===================================================================
|
|
private string ExecuteResearch(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<ResearchRequest>(ctx);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "research_queued", techId = data.techId });
|
|
}
|
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Schedule
|
|
// ===================================================================
|
|
private string ExecuteSchedule(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<ScheduleRequest>(ctx);
|
|
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 }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Wardrobe
|
|
// ===================================================================
|
|
private string ExecuteWardrobe(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<WardrobeRequest>(ctx);
|
|
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 }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Mop (clean liquid spills)
|
|
// ===================================================================
|
|
private string ExecuteMop(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<MopRequest>(ctx);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "mop_queued", x = data.x, y = data.y });
|
|
}
|
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Harvest
|
|
// ===================================================================
|
|
private string ExecuteHarvest(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<HarvestRequest>(ctx);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "harvest_queued", x = data.x, y = data.y });
|
|
}
|
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Action: Cancel
|
|
// ===================================================================
|
|
private string ExecuteCancel(HttpListenerContext ctx)
|
|
{
|
|
try
|
|
{
|
|
var data = ReadBody<CancelRequest>(ctx);
|
|
if (data == null)
|
|
return JsonSerializer.Serialize(new { error = "invalid_request" });
|
|
|
|
return JsonSerializer.Serialize(new { result = "cancel_queued", x = data.x, y = data.y });
|
|
}
|
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
|
}
|
|
|
|
// ===================================================================
|
|
// Helpers
|
|
// ===================================================================
|
|
private T ReadBody<T>(HttpListenerContext ctx) where T : class
|
|
{
|
|
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
|
|
return JsonSerializer.Deserialize<T>(body);
|
|
}
|
|
|
|
private string GetElementStateCategory(Element elem)
|
|
{
|
|
if (elem == null) return "unknown";
|
|
if (elem.IsGas) return "gas";
|
|
if (elem.IsLiquid) return "liquid";
|
|
return "solid";
|
|
}
|
|
|
|
private string GetElementCategory(Element elem)
|
|
{
|
|
if (elem == null) return "unknown";
|
|
if (elem.HasTag(GameTags.ConsumableOre)) return "consumable_ore";
|
|
if (elem.HasTag(GameTags.RawPoultry)) return "food";
|
|
if (elem.HasTag(GameTags.Metal)) return "metal";
|
|
if (elem.HasTag(GameTags.RefinedMetal)) return "refined_metal";
|
|
if (elem.HasTag(GameTags.PreciousStone)) return "precious_stone";
|
|
if (elem.HasTag(GameTags.BuildableAny)) return "buildable";
|
|
if (elem.HasTag(GameTags.Filter)) return "filter";
|
|
if (elem.HasTag(GameTags.Liquid))
|
|
{
|
|
if (elem.name.Contains("Water") || elem.name.Contains("Salt")) return "water";
|
|
if (elem.name.Contains("Oil") || elem.name.Contains("Petroleum")) return "fuel";
|
|
return "liquid";
|
|
}
|
|
if (elem.IsGas) return "gas";
|
|
return "other";
|
|
}
|
|
|
|
private string GetBuildingCategory(string prefabId)
|
|
{
|
|
if (string.IsNullOrEmpty(prefabId)) return "unknown";
|
|
var def = Assets.GetBuildingDef(prefabId);
|
|
if (def == null) return "unknown";
|
|
return def.Category.ToString();
|
|
}
|
|
|
|
public override void OnUnload()
|
|
{
|
|
_running = false;
|
|
_listener?.Stop();
|
|
base.OnUnload();
|
|
}
|
|
}
|
|
|
|
// =======================================================================
|
|
// Request DTOs
|
|
// =======================================================================
|
|
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; } }
|
|
internal class MopRequest { public int x { get; set; } public int y { get; set; } }
|
|
internal class HarvestRequest { public int x { get; set; } public int y { get; set; } }
|
|
internal class CancelRequest { public int x { get; set; } public int y { get; set; } }
|
|
}
|