Files
oniagent/mod/ONIAgentBridge.cs
root 6a7ac87c1b feat: comprehensive AI-oriented data model with cell-level map API and knowledge base
- 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
2026-05-22 08:54:03 +08:00

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; } }
}