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:
root
2026-05-22 09:41:00 +08:00
parent bf24e95240
commit 1608357040
3 changed files with 217 additions and 2 deletions

View File

@ -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
{

View File

@ -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', '')

View File

@ -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")