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 int _eventSeq = 0;
|
||||||
private static object _eventLock = new object();
|
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)
|
public override void OnLoad(Harmony harmony)
|
||||||
{
|
{
|
||||||
base.OnLoad(harmony);
|
base.OnLoad(harmony);
|
||||||
@ -155,6 +159,9 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/state/buildable", "GET"):
|
case ("/api/state/buildable", "GET"):
|
||||||
responseJson = GetBuildable();
|
responseJson = GetBuildable();
|
||||||
break;
|
break;
|
||||||
|
case ("/api/state/printing_pod", "GET"):
|
||||||
|
responseJson = GetPrintingPod();
|
||||||
|
break;
|
||||||
|
|
||||||
// --- Cell-level map data ---
|
// --- Cell-level map data ---
|
||||||
case ("/api/state/cell", "GET"):
|
case ("/api/state/cell", "GET"):
|
||||||
@ -278,6 +285,9 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/action/set_automation", "POST"):
|
case ("/api/action/set_automation", "POST"):
|
||||||
responseJson = ExecuteSetAutomation(ctx);
|
responseJson = ExecuteSetAutomation(ctx);
|
||||||
break;
|
break;
|
||||||
|
case ("/api/action/printing_pod_select", "POST"):
|
||||||
|
responseJson = ExecutePrintingPodSelect(ctx);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
@ -390,6 +400,50 @@ namespace ONIAgentBridge
|
|||||||
|
|
||||||
PushEvent("state_poll", "info", "Game state polled", $"Cycle {GameClock.Instance?.GetCycle() ?? 0}");
|
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;
|
bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false;
|
||||||
int gameSpeed = isPaused ? 0 : (SpeedControlScreen.Instance?.GetSpeed() ?? 1);
|
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
|
// 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 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 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 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
|
internal class BatchRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@ -126,6 +126,10 @@ def classify_event(e):
|
|||||||
return 'warning'
|
return 'warning'
|
||||||
if any(w in combined for w in ['duplicant', 'stress', 'break']):
|
if any(w in combined for w in ['duplicant', 'stress', 'break']):
|
||||||
return 'warning'
|
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'
|
return 'info'
|
||||||
|
|
||||||
@ -198,6 +202,20 @@ def poll_loop(event_history):
|
|||||||
elif cls == 'warning':
|
elif cls == 'warning':
|
||||||
print(format_event_for_ai(e))
|
print(format_event_for_ai(e))
|
||||||
print()
|
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:
|
else:
|
||||||
# Only print non-info events or batch feedback
|
# Only print non-info events or batch feedback
|
||||||
cat = e.get('category', '')
|
cat = e.get('category', '')
|
||||||
|
|||||||
@ -931,6 +931,42 @@ def cmd_set_automation(args):
|
|||||||
})
|
})
|
||||||
_print_feedback(result)
|
_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
|
# Screenshot / Camera
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -1105,6 +1141,8 @@ COMMANDS = {
|
|||||||
'building_detail': cmd_building_detail,
|
'building_detail': cmd_building_detail,
|
||||||
'set_building_priority': cmd_set_building_priority,
|
'set_building_priority': cmd_set_building_priority,
|
||||||
'set_automation': cmd_set_automation,
|
'set_automation': cmd_set_automation,
|
||||||
|
'printing_pod': cmd_printing_pod,
|
||||||
|
'printing_pod_select': cmd_printing_pod_select,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -1133,9 +1171,10 @@ if __name__ == '__main__':
|
|||||||
print(" gas <x> <y> [r] Gas analysis in radius r")
|
print(" gas <x> <y> [r] Gas analysis in radius r")
|
||||||
print(" explore <x> <y> <w> <h> AI-friendly region summary")
|
print(" explore <x> <y> <w> <h> AI-friendly region summary")
|
||||||
print("")
|
print("")
|
||||||
print("=== Event / Queue ===")
|
print("=== Events / Printing Pod ===")
|
||||||
print(" events [since] [limit] Poll new game events")
|
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("")
|
||||||
print("=== Registries (AI Reference) ===")
|
print("=== Registries (AI Reference) ===")
|
||||||
print(" registry buildings [f] List all building IDs with metadata")
|
print(" registry buildings [f] List all building IDs with metadata")
|
||||||
|
|||||||
Reference in New Issue
Block a user