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
{