feat: printing pod API + research completion events
Printing Pod:
- GET /api/state/printing_pod: check if ready, options available, cycles remaining
- POST /api/action/printing_pod_select {index}: select option 0/1/2
Research completion tracking:
- Automatic detection of newly completed techs on each state poll
- Events pushed to stream: 'research_complete' with unlocked buildings list
- Events pushed when Printing Pod becomes ready
Event daemon enhanced:
- Classifies 'research complete' events with actionable next steps
- Classifies 'printing pod' events with option viewing instructions
CLI: printing_pod, printing_pod_select <0|1|2>
This commit is contained in:
@ -20,6 +20,10 @@ namespace ONIAgentBridge
|
||||
private static int _eventSeq = 0;
|
||||
private static object _eventLock = new object();
|
||||
|
||||
// Research completion tracking
|
||||
private static HashSet<string> _lastCompletedTechs = new HashSet<string>();
|
||||
private static int _lastPrintingPodCycle = -1;
|
||||
|
||||
public override void OnLoad(Harmony harmony)
|
||||
{
|
||||
base.OnLoad(harmony);
|
||||
@ -155,6 +159,9 @@ namespace ONIAgentBridge
|
||||
case ("/api/state/buildable", "GET"):
|
||||
responseJson = GetBuildable();
|
||||
break;
|
||||
case ("/api/state/printing_pod", "GET"):
|
||||
responseJson = GetPrintingPod();
|
||||
break;
|
||||
|
||||
// --- Cell-level map data ---
|
||||
case ("/api/state/cell", "GET"):
|
||||
@ -278,6 +285,9 @@ namespace ONIAgentBridge
|
||||
case ("/api/action/set_automation", "POST"):
|
||||
responseJson = ExecuteSetAutomation(ctx);
|
||||
break;
|
||||
case ("/api/action/printing_pod_select", "POST"):
|
||||
responseJson = ExecutePrintingPodSelect(ctx);
|
||||
break;
|
||||
|
||||
default:
|
||||
ctx.Response.StatusCode = 404;
|
||||
@ -390,6 +400,50 @@ namespace ONIAgentBridge
|
||||
|
||||
PushEvent("state_poll", "info", "Game state polled", $"Cycle {GameClock.Instance?.GetCycle() ?? 0}");
|
||||
|
||||
// Track research completion
|
||||
try
|
||||
{
|
||||
var currentCompleted = new HashSet<string>();
|
||||
foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List<Tech>())
|
||||
{
|
||||
if (tech.IsComplete()) currentCompleted.Add(tech.Id);
|
||||
}
|
||||
foreach (var completed in currentCompleted)
|
||||
{
|
||||
if (!_lastCompletedTechs.Contains(completed))
|
||||
{
|
||||
var t = Research.Instance?.GetResearchTechnologies()
|
||||
.FirstOrDefault(tech => tech.Id == completed);
|
||||
string name = t?.Name ?? completed;
|
||||
string unlocks = "";
|
||||
if (t?.unlockedBuildings != null)
|
||||
unlocks = string.Join(", ", t.unlockedBuildings.Take(5));
|
||||
PushEvent("research_complete", "info", $"Research completed: {name}",
|
||||
$"Unlocked: {unlocks}", "research", entity: completed);
|
||||
}
|
||||
}
|
||||
_lastCompletedTechs = currentCompleted;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Track printing pod readiness
|
||||
try
|
||||
{
|
||||
int curCycle = GameClock.Instance?.GetCycle() ?? 0;
|
||||
if (curCycle != _lastPrintingPodCycle)
|
||||
{
|
||||
_lastPrintingPodCycle = curCycle;
|
||||
var imm = ImmuneSystemMonitor.Instance;
|
||||
if (imm != null && imm.IsReadyToPrint())
|
||||
{
|
||||
PushEvent("printing_pod", "info", "Printing Pod ready",
|
||||
"New duplicants or supplies available — select one!",
|
||||
"game_event");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false;
|
||||
int gameSpeed = isPaused ? 0 : (SpeedControlScreen.Instance?.GetSpeed() ?? 1);
|
||||
|
||||
@ -2358,6 +2412,109 @@ namespace ONIAgentBridge
|
||||
});
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Printing Pod
|
||||
// ===================================================================
|
||||
private string GetPrintingPod()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isReady = false;
|
||||
float cyclesUntilNext = 0;
|
||||
var options = new List<object>();
|
||||
|
||||
try
|
||||
{
|
||||
var pod = Game.Instance?.printingPod;
|
||||
if (pod != null)
|
||||
{
|
||||
isReady = pod.IsReady();
|
||||
cyclesUntilNext = pod.CyclesUntilReady();
|
||||
var offers = pod.GetCurrentOffers();
|
||||
if (offers != null)
|
||||
{
|
||||
int idx = 0;
|
||||
foreach (var offer in offers)
|
||||
{
|
||||
string desc = "";
|
||||
string type = "unknown";
|
||||
try { desc = offer.GetName(); } catch { }
|
||||
try { type = offer.GetType().Name; } catch { }
|
||||
|
||||
options.Add(new
|
||||
{
|
||||
index = idx,
|
||||
type,
|
||||
description = desc,
|
||||
duplicant = desc
|
||||
});
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: check ImmuneSystemMonitor
|
||||
var imm = ImmuneSystemMonitor.Instance;
|
||||
if (imm != null)
|
||||
{
|
||||
isReady = imm.IsReadyToPrint();
|
||||
cyclesUntilNext = imm.GetCyclesUntilNextPrint();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (isReady)
|
||||
{
|
||||
PushEvent("printing_pod", "info", "Printing Pod ready",
|
||||
$"{options.Count} options available — select one!",
|
||||
"game_event");
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
isReady,
|
||||
cyclesUntilNext,
|
||||
options
|
||||
});
|
||||
}
|
||||
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
||||
}
|
||||
|
||||
private string ExecutePrintingPodSelect(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = ReadBody<PrintingPodSelectRequest>(ctx);
|
||||
if (data == null)
|
||||
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
||||
|
||||
int index = data.index;
|
||||
if (index < 0 || index > 2)
|
||||
return JsonSerializer.Serialize(FailWithReason("invalid_index",
|
||||
"Index must be 0, 1, or 2"));
|
||||
|
||||
try
|
||||
{
|
||||
var pod = Game.Instance?.printingPod;
|
||||
if (pod != null && pod.IsReady())
|
||||
{
|
||||
pod.SelectOffer(index);
|
||||
PushEvent("printing_pod", "info", $"Selected printing pod option {index}",
|
||||
$"Selected option {index}", "action");
|
||||
return JsonSerializer.Serialize(ActionOk("printing_pod_selected",
|
||||
new { index }));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return JsonSerializer.Serialize(ActionOk("printing_pod_selected",
|
||||
new { index }));
|
||||
}
|
||||
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand
|
||||
// ===================================================================
|
||||
@ -2876,6 +3033,7 @@ namespace ONIAgentBridge
|
||||
internal class RecipeRequest { public int x { get; set; } public int y { get; set; } public string recipeId { get; set; } }
|
||||
internal class BuildingPriorityRequest { public int x { get; set; } public int y { get; set; } public int priority { get; set; } }
|
||||
internal class AutomationRequest { public int x { get; set; } public int y { get; set; } public bool enabled { get; set; } }
|
||||
internal class PrintingPodSelectRequest { public int index { get; set; } }
|
||||
|
||||
internal class BatchRequest
|
||||
{
|
||||
|
||||
@ -126,6 +126,10 @@ def classify_event(e):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['duplicant', 'stress', 'break']):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['research complete', 'research completed']):
|
||||
return 'info_research'
|
||||
if any(w in combined for w in ['printing pod']):
|
||||
return 'info_printing_pod'
|
||||
|
||||
return 'info'
|
||||
|
||||
@ -198,6 +202,20 @@ def poll_loop(event_history):
|
||||
elif cls == 'warning':
|
||||
print(format_event_for_ai(e))
|
||||
print()
|
||||
elif cls == 'info_research':
|
||||
# Research completed — show with unlocks
|
||||
print("=" * 40)
|
||||
print(format_event_for_ai(e))
|
||||
print(" -> Check what's new: python3 tools/oni_api.py buildable")
|
||||
print("=" * 40)
|
||||
print()
|
||||
elif cls == 'info_printing_pod':
|
||||
# Printing pod ready
|
||||
print("=" * 40)
|
||||
print(format_event_for_ai(e))
|
||||
print(" -> View options: python3 tools/oni_api.py printing_pod")
|
||||
print("=" * 40)
|
||||
print()
|
||||
else:
|
||||
# Only print non-info events or batch feedback
|
||||
cat = e.get('category', '')
|
||||
|
||||
@ -931,6 +931,42 @@ def cmd_set_automation(args):
|
||||
})
|
||||
_print_feedback(result)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Printing Pod
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_printing_pod(args):
|
||||
"""Check Printing Pod status. Usage: printing_pod"""
|
||||
data = api_get('/api/state/printing_pod')
|
||||
if 'error' in data:
|
||||
print(f"Error: {data['error']}")
|
||||
return
|
||||
if data.get('isReady'):
|
||||
print("=== Printing Pod: READY ===")
|
||||
print(f" Options available:")
|
||||
for opt in data.get('options', []):
|
||||
print(f" [{opt.get('index')}] {opt.get('description', '?')} ({opt.get('type', '?')})")
|
||||
print()
|
||||
print(" Select with: python3 tools/oni_api.py printing_pod_select <0|1|2>")
|
||||
else:
|
||||
cycles = data.get('cyclesUntilNext', 0)
|
||||
if cycles > 0:
|
||||
print(f"Printing Pod: not ready ({cycles:.1f} cycles remaining)")
|
||||
else:
|
||||
print("Printing Pod: checking...")
|
||||
|
||||
def cmd_printing_pod_select(args):
|
||||
"""Select a Printing Pod option. Usage: printing_pod_select <0|1|2>"""
|
||||
if not args:
|
||||
print("Usage: printing_pod_select <0|1|2>")
|
||||
return
|
||||
index = int(args[0])
|
||||
if index < 0 or index > 2:
|
||||
print("Index must be 0, 1, or 2")
|
||||
return
|
||||
result = api_post('/api/action/printing_pod_select', {"index": index})
|
||||
_print_feedback(result)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Screenshot / Camera
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -1105,6 +1141,8 @@ COMMANDS = {
|
||||
'building_detail': cmd_building_detail,
|
||||
'set_building_priority': cmd_set_building_priority,
|
||||
'set_automation': cmd_set_automation,
|
||||
'printing_pod': cmd_printing_pod,
|
||||
'printing_pod_select': cmd_printing_pod_select,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -1133,9 +1171,10 @@ if __name__ == '__main__':
|
||||
print(" gas <x> <y> [r] Gas analysis in radius r")
|
||||
print(" explore <x> <y> <w> <h> AI-friendly region summary")
|
||||
print("")
|
||||
print("=== Event / Queue ===")
|
||||
print("=== Events / Printing Pod ===")
|
||||
print(" events [since] [limit] Poll new game events")
|
||||
print(" queue [batch_id] View pending task queue")
|
||||
print(" printing_pod Check Printing Pod status (ready/options)")
|
||||
print(" printing_pod_select <0|1|2> Select Printing Pod option")
|
||||
print("")
|
||||
print("=== Registries (AI Reference) ===")
|
||||
print(" registry buildings [f] List all building IDs with metadata")
|
||||
|
||||
Reference in New Issue
Block a user