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:
@ -112,6 +112,33 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/state/events", "GET"):
|
case ("/api/state/events", "GET"):
|
||||||
responseJson = GetEvents(query);
|
responseJson = GetEvents(query);
|
||||||
break;
|
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 ---
|
// --- Cell-level map data ---
|
||||||
case ("/api/state/cell", "GET"):
|
case ("/api/state/cell", "GET"):
|
||||||
@ -190,6 +217,18 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/action/speed", "POST"):
|
case ("/api/action/speed", "POST"):
|
||||||
responseJson = ExecuteSpeed(ctx);
|
responseJson = ExecuteSpeed(ctx);
|
||||||
break;
|
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:
|
default:
|
||||||
ctx.Response.StatusCode = 404;
|
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
|
// Helpers
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@ -1666,6 +2129,9 @@ namespace ONIAgentBridge
|
|||||||
internal class SpeedRequest { public int speed { get; set; } }
|
internal class SpeedRequest { public int speed { get; set; } }
|
||||||
internal class PriorityGlobalRequest { public string target { get; set; } public int priority { 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 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
|
internal class BatchRequest
|
||||||
{
|
{
|
||||||
|
|||||||
231
tools/oni_api.py
231
tools/oni_api.py
@ -498,6 +498,205 @@ def cmd_speed(args):
|
|||||||
result = api_post('/api/action/speed', {"speed": speed})
|
result = api_post('/api/action/speed', {"speed": speed})
|
||||||
_print_feedback(result)
|
_print_feedback(result)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Save / Load
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_saves(args):
|
||||||
|
"""List saves. Usage: saves"""
|
||||||
|
data = api_get('/api/state/saves')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
for s in data.get('saves', []):
|
||||||
|
print(f" {s.get('name', '?'):40s} {s.get('size', 0)/1024:.0f} KB")
|
||||||
|
|
||||||
|
def cmd_save(args):
|
||||||
|
"""Save game. Usage: save [name]"""
|
||||||
|
name = ' '.join(args) if args else None
|
||||||
|
result = api_post('/api/action/save', {"name": name} if name else {})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
def cmd_save_as(args):
|
||||||
|
"""Save with specific name. Usage: save_as <name>"""
|
||||||
|
if not args:
|
||||||
|
print("Usage: save_as <name>")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/save_as', {"name": ' '.join(args)})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
def cmd_load(args):
|
||||||
|
"""Load a save. Usage: load <name>"""
|
||||||
|
if not args:
|
||||||
|
print("Usage: load <name>")
|
||||||
|
return
|
||||||
|
name = ' '.join(args)
|
||||||
|
print(f"[!] Loading save '{name}' — game will restart!")
|
||||||
|
result = api_post('/api/action/load', {"name": name})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Power Grid
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_power(args):
|
||||||
|
"""Analyze power grid. Usage: power"""
|
||||||
|
data = api_get('/api/state/power')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
print(f"Power Grid — {data.get('circuitCount', 0)} circuits")
|
||||||
|
print()
|
||||||
|
for c in data.get('circuits', []):
|
||||||
|
overload = " *** OVERLOAD ***" if c.get('isOverloaded') else ""
|
||||||
|
print(f" Circuit {c.get('id', '?')}: {c.get('wattsUsed', 0):.0f}W / {c.get('maxWatts', 0):.0f}W used{overload}")
|
||||||
|
print()
|
||||||
|
print(f"Generators ({len(data.get('generators', []))}):")
|
||||||
|
for g in data.get('generators', []):
|
||||||
|
status = 'ON' if g.get('isActive') else 'OFF'
|
||||||
|
print(f" {g.get('name', '?'):25s} {g.get('watts', 0):.0f}W [{status}]")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pipes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_pipes(args):
|
||||||
|
"""Inspect pipe contents. Usage: pipes [gas|liquid]"""
|
||||||
|
ptype = args[0] if args else 'all'
|
||||||
|
if ptype not in ('gas', 'liquid', 'all'):
|
||||||
|
print("Usage: pipes [gas|liquid|all]")
|
||||||
|
return
|
||||||
|
data = api_get(f"/api/state/pipes?type={ptype}")
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
print(f"Pipe Contents ({data.get('pipeType', '?')}): {data.get('segmentCount', 0)} segments")
|
||||||
|
for s in data.get('segments', [])[:20]:
|
||||||
|
t = s.get('type', '?')
|
||||||
|
el = s.get('element', '?')
|
||||||
|
m = s.get('mass', 0)
|
||||||
|
temp = s.get('temperature', 0)
|
||||||
|
print(f" [{t}] {el:20s} {m:8.1f} kg {temp:6.1f} K")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CO2
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_co2(args):
|
||||||
|
"""Find CO2 pockets. Usage: co2"""
|
||||||
|
data = api_get('/api/state/co2')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
print(f"CO2 Pockets: {data.get('pocketCount', 0)} Total: {data.get('totalMassKg', 0):.0f} kg")
|
||||||
|
for p in data.get('pockets', [])[:10]:
|
||||||
|
print(f" ({p.get('x', '?')},{p.get('y', '?')}) {p.get('mass', 0):.1f} kg {p.get('temp', 0):.0f}°C")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Temperature
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_temp_zones(args):
|
||||||
|
"""Analyze temperature zones. Usage: temp_zones"""
|
||||||
|
data = api_get('/api/state/temperature/zones')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
print(f"Temperature Analysis:")
|
||||||
|
print(f" Average: {data.get('averageC', 0):.0f}°C")
|
||||||
|
print(f" Min: {data.get('minC', 0):.0f}°C")
|
||||||
|
print(f" Max: {data.get('maxC', 0):.0f}°C")
|
||||||
|
print(f" Samples: {data.get('sampleCount', 0)}")
|
||||||
|
print()
|
||||||
|
hotspots = data.get('hotSpots', [])
|
||||||
|
if hotspots:
|
||||||
|
print(f"Hot spots (>50°C):")
|
||||||
|
for h in hotspots:
|
||||||
|
print(f" ({h.get('x', '?')},{h.get('y', '?')}) {h.get('tempC', 0):.0f}°C")
|
||||||
|
coldspots = data.get('coldSpots', [])
|
||||||
|
if coldspots:
|
||||||
|
print(f"Cold spots (<5°C):")
|
||||||
|
for c in coldspots:
|
||||||
|
print(f" ({c.get('x', '?')},{c.get('y', '?')}) {c.get('tempC', 0):.0f}°C")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Morale
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_morale(args):
|
||||||
|
"""Check morale. Usage: morale"""
|
||||||
|
data = api_get('/api/state/morale')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
for m in data if isinstance(data, list) else []:
|
||||||
|
print(f" {m.get('name', '?'):12s} morale={m.get('morale', 0):.0f} qol={m.get('qualityOfLife', 0):.0f}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Diseases
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_diseases(args):
|
||||||
|
"""Check disease status. Usage: diseases"""
|
||||||
|
data = api_get('/api/state/diseases')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
infected = data.get('infectedDuplicants', [])
|
||||||
|
if infected:
|
||||||
|
print(f"Infected dupes: {len(infected)}")
|
||||||
|
for d in infected:
|
||||||
|
print(f" {d.get('duplicant', '?')} - {d.get('disease', '?')} ({d.get('severity', '?')})")
|
||||||
|
else:
|
||||||
|
print("No infected duplicants.")
|
||||||
|
germs = data.get('environmentGerms', {})
|
||||||
|
if germs:
|
||||||
|
print(f"Environmental germs:")
|
||||||
|
for name, count in sorted(germs.items(), key=lambda x: -x[1])[:5]:
|
||||||
|
print(f" {name}: {count:.0f}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Storage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_storage(args):
|
||||||
|
"""Check storage. Usage: storage"""
|
||||||
|
data = api_get('/api/state/storage')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
print(f"Storage buildings: {data.get('storageCount', 0)}")
|
||||||
|
for s in data.get('storages', [])[:10]:
|
||||||
|
print(f" {s.get('building', '?'):25s} ({s.get('x', '?')},{s.get('y', '?')}) "
|
||||||
|
f"{s.get('totalMass', 0):.0f}/{s.get('capacity', 0):.0f} kg")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Skills / Job
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_skills(args):
|
||||||
|
"""Show duplicant skills. Usage: skills"""
|
||||||
|
data = api_get('/api/state/duplicants/skills')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
for d in data if isinstance(data, list) else []:
|
||||||
|
print(f" {d.get('name', '?'):12s} skill_pts={d.get('skillPoints', 0)} total_pts={d.get('totalSkillPointsGained', 0)}")
|
||||||
|
for a in d.get('attributes', [])[:5]:
|
||||||
|
print(f" {a.get('name', '?'):15s} = {a.get('value', 0)}")
|
||||||
|
|
||||||
|
def cmd_assign_job(args):
|
||||||
|
"""Assign dupe to a job. Usage: assign_job <dupe_name> <chore_group>"""
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: assign_job <dupe_name> <chore_group>")
|
||||||
|
print(" chore groups: Build, Dig, Cook, Farm, Ranch, Operate, Research, Store, Tidy, LifeSupport")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/assign_job', {
|
||||||
|
"duplicantId": args[0],
|
||||||
|
"choreGroup": args[1]
|
||||||
|
})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
def _print_feedback(result):
|
def _print_feedback(result):
|
||||||
"""Pretty-print action feedback."""
|
"""Pretty-print action feedback."""
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
@ -611,6 +810,19 @@ COMMANDS = {
|
|||||||
'pause': cmd_pause,
|
'pause': cmd_pause,
|
||||||
'unpause': cmd_unpause,
|
'unpause': cmd_unpause,
|
||||||
'speed': cmd_speed,
|
'speed': cmd_speed,
|
||||||
|
'save': cmd_save,
|
||||||
|
'save_as': cmd_save_as,
|
||||||
|
'load': cmd_load,
|
||||||
|
'saves': cmd_saves,
|
||||||
|
'power': cmd_power,
|
||||||
|
'pipes': cmd_pipes,
|
||||||
|
'co2': cmd_co2,
|
||||||
|
'temp_zones': cmd_temp_zones,
|
||||||
|
'morale': cmd_morale,
|
||||||
|
'diseases': cmd_diseases,
|
||||||
|
'storage': cmd_storage,
|
||||||
|
'skills': cmd_skills,
|
||||||
|
'assign_job': cmd_assign_job,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -663,6 +875,25 @@ if __name__ == '__main__':
|
|||||||
print(" unpause [speed] Unpause the game at 1x/2x/3x speed")
|
print(" unpause [speed] Unpause the game at 1x/2x/3x speed")
|
||||||
print(" speed <1|2|3> Set game speed while running")
|
print(" speed <1|2|3> Set game speed while running")
|
||||||
print("")
|
print("")
|
||||||
|
print("=== Save / Load ===")
|
||||||
|
print(" save [name] Save game (AI undo button)")
|
||||||
|
print(" save_as <name> Save with custom name")
|
||||||
|
print(" saves List save files")
|
||||||
|
print(" load <name> Load a save (rollback)")
|
||||||
|
print("")
|
||||||
|
print("=== Grid Analysis ===")
|
||||||
|
print(" power Power grid (circuits, load, overloads)")
|
||||||
|
print(" pipes [gas|liquid|all] Pipe contents (debug plumbing)")
|
||||||
|
print(" co2 Find CO2 pockets (colony killer!)")
|
||||||
|
print(" temp_zones Temperature hot/cold spots")
|
||||||
|
print("")
|
||||||
|
print("=== Colony Management ===")
|
||||||
|
print(" storages Storage building contents")
|
||||||
|
print(" diseases Disease/infection overview")
|
||||||
|
print(" skills Duplicant skills and attributes")
|
||||||
|
print(" assign_job <name> <job> Assign dupe to a chore group")
|
||||||
|
print(" morale Duplicant morale levels")
|
||||||
|
print("")
|
||||||
print("=== Batch / Priority (Advanced) ===")
|
print("=== Batch / Priority (Advanced) ===")
|
||||||
print(" batch <json_file> Execute batch plan")
|
print(" batch <json_file> Execute batch plan")
|
||||||
print(" priority_global <t> <p> Set global default priority")
|
print(" priority_global <t> <p> Set global default priority")
|
||||||
|
|||||||
Reference in New Issue
Block a user