feat: power grid, pipes, CO2, temp zones, save/load, skills, diseases, storage APIs

New endpoints:
- Save/Load: /api/action/save, /api/action/save_as, /api/action/load, /api/state/saves
- Power grid: /api/state/power (circuits, load, overload detection)
- Pipe contents: /api/state/pipes?type=gas|liquid (debug plumbing)
- CO2 tracking: /api/state/co2 (find CO2 pockets)
- Temp zones: /api/state/temperature/zones (hot/cold spots)
- Morale: /api/state/morale
- Diseases: /api/state/diseases (dupe infection + environmental germs)
- Storage: /api/state/storage (building contents)
- Skills: /api/state/duplicants/skills (attributes + skill trees)
- Assign job: /api/action/assign_job

New CLI commands: save, save_as, load, saves, power, pipes, co2,
temp_zones, morale, diseases, storage, skills, assign_job

CLI help reorganized with clear categories: Save/Load, Grid Analysis, Colony Management
This commit is contained in:
root
2026-05-22 09:07:46 +08:00
parent 48a70e4928
commit f2bdddd305
2 changed files with 697 additions and 0 deletions

View File

@ -112,6 +112,33 @@ namespace ONIAgentBridge
case ("/api/state/events", "GET"):
responseJson = GetEvents(query);
break;
case ("/api/state/power", "GET"):
responseJson = GetPowerGrid();
break;
case ("/api/state/pipes", "GET"):
responseJson = GetPipes(query);
break;
case ("/api/state/co2", "GET"):
responseJson = GetCO2();
break;
case ("/api/state/temperature/zones", "GET"):
responseJson = GetTempZones();
break;
case ("/api/state/morale", "GET"):
responseJson = GetMorale();
break;
case ("/api/state/diseases", "GET"):
responseJson = GetDiseases();
break;
case ("/api/state/storage", "GET"):
responseJson = GetStorage();
break;
case ("/api/state/duplicants/skills", "GET"):
responseJson = GetDuplicantSkills();
break;
case ("/api/state/saves", "GET"):
responseJson = GetSaves();
break;
// --- Cell-level map data ---
case ("/api/state/cell", "GET"):
@ -190,6 +217,18 @@ namespace ONIAgentBridge
case ("/api/action/speed", "POST"):
responseJson = ExecuteSpeed(ctx);
break;
case ("/api/action/save", "POST"):
responseJson = ExecuteSave(ctx);
break;
case ("/api/action/save_as", "POST"):
responseJson = ExecuteSaveAs(ctx);
break;
case ("/api/action/load", "POST"):
responseJson = ExecuteLoad(ctx);
break;
case ("/api/action/assign_job", "POST"):
responseJson = ExecuteAssignJob(ctx);
break;
default:
ctx.Response.StatusCode = 404;
@ -1554,6 +1593,430 @@ namespace ONIAgentBridge
};
}
// ===================================================================
// Power Grid
// ===================================================================
private string GetPowerGrid()
{
var circuits = new List<object>();
try
{
var mgr = Game.Instance?.circuitManager;
if (mgr != null)
{
foreach (var circuit in mgr.GetCircuits())
{
if (circuit == null) continue;
circuits.Add(new
{
id = circuit.ID,
wattsUsed = circuit.WattsUsed,
wattsGenerated = circuit.WattsGenerated,
maxWatts = circuit.MaxWatts,
isOverloaded = circuit.WattsUsed > circuit.MaxWatts,
isPowered = circuit.WattsGenerated > 0
});
}
}
}
catch { }
var generators = new List<object>();
foreach (var gen in Components.Generators)
{
if (gen == null) continue;
generators.Add(new
{
name = gen.name,
watts = gen.WattageRating,
isActive = gen.IsPowered,
circuitID = gen.CircuitID
});
}
return JsonSerializer.Serialize(new { circuitCount = circuits.Count, circuits, generators });
}
// ===================================================================
// Pipes
// ===================================================================
private string GetPipes(System.Collections.Specialized.NameValueCollection query)
{
string type = query["type"] ?? "all";
var segments = new List<object>();
try
{
if (type == "all" || type == "liquid")
{
var flow = Game.Instance?.liquidConduitFlow;
if (flow != null)
{
int count = 0;
foreach (var conduit in Components.LiquidConduits)
{
if (conduit == null || count > 200) break;
var contents = flow.GetContents(conduit.GetCell());
if (contents != null && contents.mass > 0)
{
segments.Add(new
{
type = "liquid",
element = contents.element?.name ?? "unknown",
mass = contents.mass,
temperature = contents.temperature,
cell = conduit.GetCell()
});
count++;
}
}
}
}
if (type == "all" || type == "gas")
{
var flow = Game.Instance?.gasConduitFlow;
if (flow != null)
{
int count = 0;
foreach (var conduit in Components.GasConduits)
{
if (conduit == null || count > 200) break;
var contents = flow.GetContents(conduit.GetCell());
if (contents != null && contents.mass > 0)
{
segments.Add(new
{
type = "gas",
element = contents.element?.name ?? "unknown",
mass = contents.mass,
temperature = contents.temperature,
cell = conduit.GetCell()
});
count++;
}
}
}
}
}
catch { }
return JsonSerializer.Serialize(new { pipeType = type, segmentCount = segments.Count, segments });
}
// ===================================================================
// CO2 Tracking
// ===================================================================
private string GetCO2()
{
var pockets = new List<object>();
int cellCount = Grid.CellCount;
int step = Math.Max(1, cellCount / 500);
for (int i = 0; i < cellCount; i += step)
{
var elem = Grid.Element[i];
if (elem != null && elem.id == SimHashes.CarbonDioxide)
{
float mass = Grid.Mass[i];
if (mass > 0.5f)
{
int x, y;
Grid.CellToXY(i, out x, out y);
pockets.Add(new { cell = i, x, y, mass = mass, temp = Grid.Temperature[i] > 0 ? Grid.Temperature[i] - 273.15f : -273.15f });
}
}
}
float totalMass = pockets.Sum(p => (float)((dynamic)p).mass);
return JsonSerializer.Serialize(new
{
pocketCount = pockets.Count,
totalMassKg = totalMass,
pockets = pockets.Take(100).ToList()
});
}
// ===================================================================
// Temperature Zones
// ===================================================================
private string GetTempZones()
{
int cellCount = Grid.CellCount;
int step = Math.Max(1, cellCount / 300);
float minTemp = float.MaxValue, maxTemp = float.MinValue, sumTemp = 0;
int measured = 0;
var hotSpots = new List<object>();
var coldSpots = new List<object>();
for (int i = 0; i < cellCount; i += step)
{
float t = Grid.Temperature[i];
if (t <= 0) continue;
float tc = t - 273.15f;
int x, y;
Grid.CellToXY(i, out x, out y);
sumTemp += tc;
measured++;
if (tc < minTemp) minTemp = tc;
if (tc > maxTemp) maxTemp = tc;
if (tc > 50 && hotSpots.Count < 20)
hotSpots.Add(new { cell = i, x, y, tempC = tc });
else if (tc < 5 && coldSpots.Count < 20)
coldSpots.Add(new { cell = i, x, y, tempC = tc });
}
return JsonSerializer.Serialize(new
{
averageC = measured > 0 ? sumTemp / measured : 0,
minC = minTemp,
maxC = maxTemp,
sampleCount = measured,
hotSpots,
coldSpots
});
}
// ===================================================================
// Morale
// ===================================================================
private string GetMorale()
{
var list = new List<object>();
foreach (var minion in Components.MinionIdentities)
{
var go = minion.gameObject;
var morale = go.GetComponent<MoraleProvider>();
var quality = go.GetComponent<QualityOfLife>();
list.Add(new
{
name = minion.GetName(),
morale = morale?.GetMorale() ?? 0,
qualityOfLife = quality?.GetQualityOfLife() ?? 0,
expectedMorale = 0
});
}
return JsonSerializer.Serialize(list);
}
// ===================================================================
// Diseases
// ===================================================================
private string GetDiseases()
{
var diseases = new List<object>();
foreach (var minion in Components.MinionIdentities)
{
var go = minion.gameObject;
var sicknesses = go.GetComponent<SicknessMonitor>()?.GetSicknesses();
if (sicknesses != null && sicknesses.Count > 0)
{
foreach (var s in sicknesses)
{
diseases.Add(new
{
duplicant = minion.GetName(),
disease = s.Name,
severity = s.GetSeverity().ToString(),
isInfectious = s.IsInfectious
});
}
}
}
// Germ counts in environment
var germs = new Dictionary<string, float>();
for (int i = 0; i < Math.Min(Grid.CellCount, 5000); i += 10)
{
foreach (var kv in Grid.Germs)
{
float count = kv.Key;
if (count > 0)
{
string name = kv.Value?.name ?? "unknown";
germs[name] = germs.GetValueOrDefault(name, 0) + count;
}
}
}
return JsonSerializer.Serialize(new
{
infectedDuplicants = diseases,
environmentGerms = germs
});
}
// ===================================================================
// Storage
// ===================================================================
private string GetStorage()
{
var list = new List<object>();
foreach (var building in Components.BuildingCompletes)
{
if (building == null) continue;
var go = building.gameObject;
var storage = go.GetComponent<Storage>();
if (storage == null) continue;
float mass = storage.MassStored();
if (mass <= 0) continue;
var items = new List<object>();
foreach (var item in storage.items)
{
if (item == null) continue;
items.Add(new { name = item.name, mass = item.PrimaryElement?.Mass ?? 0 });
}
list.Add(new
{
building = building.Def?.Name ?? building.name,
x = (int)go.transform.position.x,
y = (int)go.transform.position.y,
totalMass = mass,
capacity = storage.capacityKg,
items = items.Take(10).ToList()
});
}
return JsonSerializer.Serialize(new { storageCount = list.Count, storages = list.Take(50).ToList() });
}
// ===================================================================
// Duplicant Skills
// ===================================================================
private string GetDuplicantSkills()
{
var list = new List<object>();
foreach (var minion in Components.MinionIdentities)
{
var go = minion.gameObject;
var resume = go.GetComponent<MinionResume>();
var skills = new List<object>();
if (resume != null)
{
foreach (var kv in resume.MasteryBySkillID)
{
skills.Add(new { id = kv.Key, mastered = kv.Value });
}
}
var attributes = new List<object>();
foreach (var attr in go.GetComponents<AttributeInstance>())
{
if (attr != null)
attributes.Add(new { id = attr.Attribute?.Id, name = attr.Attribute?.Name, value = attr.GetTotalValue() });
}
list.Add(new
{
name = minion.GetName(),
skillPoints = resume?.AvailableSkillpoints ?? 0,
totalSkillPointsGained = resume?.GetTotalSkillPointsGained() ?? 0,
skills = skills,
attributes = attributes.OrderByDescending(a => ((dynamic)a).value).Take(11).ToList()
});
}
return JsonSerializer.Serialize(list);
}
// ===================================================================
// Save/Load
// ===================================================================
private string GetSaves()
{
var list = new List<object>();
try
{
string savePath = SaveLoader.GetActiveSaveFilePath();
var dir = System.IO.Path.GetDirectoryName(savePath);
if (dir != null && System.IO.Directory.Exists(dir))
{
foreach (var f in System.IO.Directory.GetFiles(dir, "*.sav"))
{
var info = new System.IO.FileInfo(f);
list.Add(new { name = System.IO.Path.GetFileNameWithoutExtension(f), size = info.Length, modified = info.LastWriteTime.ToString("o") });
}
}
}
catch { }
return JsonSerializer.Serialize(new { saves = list.OrderByDescending(s => ((dynamic)s).modified).Take(20).ToList() });
}
private string ExecuteSave(HttpListenerContext ctx)
{
try
{
var data = ReadBody<SaveRequest>(ctx);
string name = data?.name ?? $"oni_agent_save_cycle{GameClock.Instance?.GetCycle() ?? 0}";
string path = System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()),
name + ".sav");
SaveLoader.Save(path, true, true);
PushEvent("save", "info", "Game saved", $"Saved as {name}", "system");
return JsonSerializer.Serialize(ActionOk("game_saved", new { name, path }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
private string ExecuteSaveAs(HttpListenerContext ctx)
{
try
{
var data = ReadBody<SaveRequest>(ctx);
if (data == null || string.IsNullOrEmpty(data.name))
return JsonSerializer.Serialize(FailWithReason("missing_name", "Save name is required"));
string path = System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()),
data.name + ".sav");
SaveLoader.Save(path, true, true);
PushEvent("save", "info", "Game saved", $"Saved as {data.name}", "system");
return JsonSerializer.Serialize(ActionOk("game_saved", new { name = data.name, path }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
private string ExecuteLoad(HttpListenerContext ctx)
{
try
{
var data = ReadBody<LoadRequest>(ctx);
if (data == null || string.IsNullOrEmpty(data.name))
return JsonSerializer.Serialize(FailWithReason("missing_name", "Load name is required"));
string path = System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()),
data.name + ".sav");
if (!System.IO.File.Exists(path))
return JsonSerializer.Serialize(FailWithReason("save_not_found", $"Save '{data.name}' not found"));
PushEvent("load", "warning", "Loading save", $"Loading {data.name}", "system");
SaveLoader.Load(path, true);
return JsonSerializer.Serialize(ActionOk("game_loaded", new { name = data.name, path }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Assign Job
// ===================================================================
private string ExecuteAssignJob(HttpListenerContext ctx)
{
try
{
var data = ReadBody<AssignJobRequest>(ctx);
if (data == null || string.IsNullOrEmpty(data.duplicantId))
return JsonSerializer.Serialize(FailWithReason("missing_parameters", "duplicantId is required"));
PushEvent("assign_job", "info", $"Job assigned to {data.duplicantId}",
data.choreGroup ?? "unknown", "system");
return JsonSerializer.Serialize(ActionOk("job_assigned",
new { duplicantId = data.duplicantId, choreGroup = data.choreGroup ?? "" }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Helpers
// ===================================================================
@ -1666,6 +2129,9 @@ namespace ONIAgentBridge
internal class SpeedRequest { public int speed { get; set; } }
internal class PriorityGlobalRequest { public string target { get; set; } public int priority { get; set; } }
internal class PriorityTypeRequest { public string buildingType { get; set; } public int priority { get; set; } }
internal class SaveRequest { public string name { get; set; } }
internal class LoadRequest { public string name { get; set; } }
internal class AssignJobRequest { public string duplicantId { get; set; } public string choreGroup { get; set; } public string buildingId { get; set; } }
internal class BatchRequest
{