Skip to content

Commit 55dbacc

Browse files
committed
Fix task terminal events with quiescence-based lifecycle resolution
1 parent 8de944a commit 55dbacc

6 files changed

Lines changed: 448 additions & 16 deletions

File tree

mod/src/main/java/com/pyritone/bridge/PyritoneBridgeClientMod.java

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.pyritone.bridge;
22

3-
import com.google.gson.JsonArray;
43
import com.google.gson.JsonElement;
5-
import com.google.gson.JsonNull;
64
import com.google.gson.JsonObject;
75
import com.pyritone.bridge.command.PyritoneCommand;
86
import com.pyritone.bridge.config.BridgeConfig;
@@ -14,6 +12,7 @@
1412
import com.pyritone.bridge.runtime.TaskRegistry;
1513
import com.pyritone.bridge.runtime.TaskSnapshot;
1614
import com.pyritone.bridge.runtime.TaskState;
15+
import com.pyritone.bridge.runtime.TaskLifecycleResolver;
1716
import com.pyritone.bridge.runtime.WatchPatternRegistry;
1817
import net.fabricmc.api.ClientModInitializer;
1918
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@@ -43,6 +42,7 @@ public final class PyritoneBridgeClientMod implements ClientModInitializer {
4342
private String serverVersion;
4443

4544
private final TaskRegistry taskRegistry = new TaskRegistry();
45+
private final TaskLifecycleResolver taskLifecycleResolver = new TaskLifecycleResolver();
4646
private final WatchPatternRegistry watchPatternRegistry = new WatchPatternRegistry();
4747

4848
private BaritoneGateway baritoneGateway;
@@ -65,6 +65,7 @@ public void onInitializeClient() {
6565
ClientTickEvents.END_CLIENT_TICK.register(client -> {
6666
baritoneGateway.tickRegisterPathListener();
6767
baritoneGateway.tickApplyPyritoneChatBranding();
68+
tickTaskLifecycle();
6869
});
6970
ClientSendMessageEvents.ALLOW_CHAT.register(this::handleOutgoingChat);
7071
ClientSendMessageEvents.ALLOW_COMMAND.register(this::handleOutgoingCommand);
@@ -277,13 +278,17 @@ private JsonObject handleBaritoneExecute(String id, JsonObject params) {
277278
emitTaskEvent("task.canceled", startResult.replacedTask(), "replaced");
278279
}
279280

281+
taskLifecycleResolver.start(startResult.startedTask().taskId());
280282
emitTaskEvent("task.started", startResult.startedTask(), "dispatched");
281283

282284
BaritoneGateway.Outcome outcome = baritoneGateway.executeRaw(command);
283285
if (!outcome.ok()) {
284286
emitPyritoneNotice("Python execute failed: " + compactCommand(outcome.message()));
285287
Optional<TaskSnapshot> failed = taskRegistry.transitionActive(TaskState.FAILED, outcome.message());
286-
failed.ifPresent(snapshot -> emitTaskEvent("task.failed", snapshot, "execute_failed"));
288+
failed.ifPresent(snapshot -> {
289+
taskLifecycleResolver.clearForTask(snapshot.taskId());
290+
emitTaskEvent("task.failed", snapshot, "execute_failed");
291+
});
287292
return ProtocolCodec.errorResponse(id, "EXECUTION_FAILED", outcome.message());
288293
}
289294

@@ -317,13 +322,14 @@ private JsonObject handleTaskCancel(String id) {
317322
return ProtocolCodec.errorResponse(id, "EXECUTION_FAILED", outcome.message());
318323
}
319324

320-
Optional<TaskSnapshot> canceled = taskRegistry.transitionActive(TaskState.CANCELED, outcome.message());
321-
canceled.ifPresent(snapshot -> emitTaskEvent("task.canceled", snapshot, "cancel_requested"));
325+
taskLifecycleResolver.markApiCancelRequested(active.orElseThrow().taskId());
326+
Optional<TaskSnapshot> updated = taskRegistry.updateActiveDetail("Cancel requested by API");
327+
updated.ifPresent(snapshot -> emitTaskEvent("task.progress", snapshot, "cancel_requested"));
322328
emitPyritoneNotice("Python cancel accepted");
323329

324330
JsonObject result = new JsonObject();
325331
result.addProperty("canceled", true);
326-
result.add("task", canceled.<JsonElement>map(TaskSnapshot::toJson).orElse(JsonNull.INSTANCE));
332+
result.add("task", updated.<JsonElement>map(TaskSnapshot::toJson).orElse(active.orElseThrow().toJson()));
327333
return ProtocolCodec.successResponse(id, result);
328334
}
329335

@@ -340,16 +346,41 @@ private void onPathEvent(String pathEventName) {
340346
return;
341347
}
342348

343-
switch (pathEventName) {
344-
case "AT_GOAL" -> taskRegistry.transitionActive(TaskState.COMPLETED, "Reached goal")
345-
.ifPresent(snapshot -> emitTaskEvent("task.completed", snapshot, pathEventName));
346-
case "CANCELED" -> taskRegistry.transitionActive(TaskState.CANCELED, "Baritone canceled")
347-
.ifPresent(snapshot -> emitTaskEvent("task.canceled", snapshot, pathEventName));
348-
case "CALC_FAILED", "NEXT_CALC_FAILED" -> taskRegistry.transitionActive(TaskState.FAILED, pathEventName)
349-
.ifPresent(snapshot -> emitTaskEvent("task.failed", snapshot, pathEventName));
350-
default -> taskRegistry.updateActiveDetail("Path event: " + pathEventName)
351-
.ifPresent(snapshot -> emitTaskEvent("task.progress", snapshot, pathEventName));
349+
TaskSnapshot current = active.orElseThrow();
350+
taskLifecycleResolver.recordPathEvent(current.taskId(), pathEventName);
351+
taskRegistry.updateActiveDetail(pathEventDetail(pathEventName))
352+
.ifPresent(snapshot -> emitTaskEvent("task.progress", snapshot, pathEventName));
353+
}
354+
355+
private void tickTaskLifecycle() {
356+
Optional<TaskSnapshot> active = taskRegistry.active();
357+
if (active.isEmpty()) {
358+
taskLifecycleResolver.clear();
359+
return;
352360
}
361+
362+
TaskSnapshot current = active.orElseThrow();
363+
Optional<TaskLifecycleResolver.TerminalDecision> terminal = taskLifecycleResolver.evaluate(
364+
current.taskId(),
365+
baritoneGateway.activitySnapshot()
366+
);
367+
368+
if (terminal.isEmpty()) {
369+
return;
370+
}
371+
372+
TaskLifecycleResolver.TerminalDecision decision = terminal.orElseThrow();
373+
taskRegistry.transitionActive(decision.state(), decision.detail())
374+
.ifPresent(snapshot -> emitTaskEvent(decision.eventName(), snapshot, decision.stage()));
375+
}
376+
377+
private static String pathEventDetail(String pathEventName) {
378+
return switch (pathEventName) {
379+
case "AT_GOAL" -> "Reached goal (awaiting stable idle)";
380+
case "CANCELED" -> "Baritone canceled (awaiting stable idle)";
381+
case "CALC_FAILED", "NEXT_CALC_FAILED" -> "Path calculation failed (awaiting stable idle)";
382+
default -> "Path event: " + pathEventName;
383+
};
353384
}
354385

355386
private void emitTaskEvent(String eventName, TaskSnapshot taskSnapshot, String stage) {

mod/src/main/java/com/pyritone/bridge/runtime/BaritoneGateway.java

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.lang.reflect.InvocationHandler;
77
import java.lang.reflect.Method;
88
import java.lang.reflect.Proxy;
9+
import java.util.Optional;
910
import java.util.concurrent.Callable;
1011
import java.util.concurrent.CompletableFuture;
1112
import java.util.concurrent.CompletionException;
@@ -60,6 +61,10 @@ public void tickApplyPyritoneChatBranding() {
6061
// Py-Ritone branding is applied only to bridge-originated notices.
6162
}
6263

64+
public ActivitySnapshot activitySnapshot() {
65+
return onClientThread(this::activitySnapshotOnClientThread, ActivitySnapshot.idle());
66+
}
67+
6368
private Outcome executeRawOnClientThread(String command) {
6469
if (!isClientReady()) {
6570
return new Outcome(false, "Client is not in a world");
@@ -131,6 +136,102 @@ private void ensurePathListenerOnClientThread() {
131136
}
132137
}
133138

139+
private ActivitySnapshot activitySnapshotOnClientThread() {
140+
if (!isClientReady()) {
141+
return ActivitySnapshot.idle();
142+
}
143+
144+
try {
145+
Object baritone = getPrimaryBaritone();
146+
Object pathingBehavior = tryInvokeNoArgs(baritone, "getPathingBehavior");
147+
148+
boolean isPathing = tryInvokeBooleanNoArgs(pathingBehavior, "isPathing");
149+
boolean hasPath = detectHasPath(pathingBehavior);
150+
boolean calcInProgress = detectCalcInProgress(pathingBehavior);
151+
boolean processInControlActive = detectProcessInControl(baritone);
152+
153+
return new ActivitySnapshot(isPathing, hasPath, calcInProgress, processInControlActive);
154+
} catch (ReflectiveOperationException exception) {
155+
logger.debug("Unable to gather Baritone runtime snapshot", exception);
156+
return ActivitySnapshot.idle();
157+
}
158+
}
159+
160+
private static boolean detectHasPath(Object pathingBehavior) {
161+
if (pathingBehavior == null) {
162+
return false;
163+
}
164+
if (tryInvokeBooleanNoArgs(pathingBehavior, "hasPath")) {
165+
return true;
166+
}
167+
if (tryInvokeNoArgs(pathingBehavior, "getCurrent") != null) {
168+
return true;
169+
}
170+
return tryInvokeNoArgs(pathingBehavior, "getPath") != null;
171+
}
172+
173+
private static boolean detectCalcInProgress(Object pathingBehavior) {
174+
if (pathingBehavior == null) {
175+
return false;
176+
}
177+
if (tryInvokeBooleanNoArgs(pathingBehavior, "isPathCalcInProgress")) {
178+
return true;
179+
}
180+
if (tryInvokeBooleanNoArgs(pathingBehavior, "isCalcInProgress")) {
181+
return true;
182+
}
183+
if (tryInvokeBooleanNoArgs(pathingBehavior, "isCalculating")) {
184+
return true;
185+
}
186+
if (tryInvokeBooleanNoArgs(pathingBehavior, "isPlanning")) {
187+
return true;
188+
}
189+
if (tryInvokeNoArgs(pathingBehavior, "getInProgress") != null) {
190+
return true;
191+
}
192+
return tryInvokeNoArgs(pathingBehavior, "getCurrentCalculation") != null;
193+
}
194+
195+
private static boolean detectProcessInControl(Object baritone) {
196+
if (baritone == null) {
197+
return false;
198+
}
199+
200+
Object pathingControlManager = tryInvokeNoArgs(baritone, "getPathingControlManager");
201+
if (pathingControlManager == null) {
202+
return false;
203+
}
204+
205+
Object inControl = tryInvokeNoArgs(pathingControlManager, "mostRecentInControl");
206+
if (inControl == null) {
207+
inControl = tryInvokeNoArgs(pathingControlManager, "inControlThisTick");
208+
}
209+
if (inControl == null) {
210+
inControl = tryInvokeNoArgs(pathingControlManager, "getInControl");
211+
}
212+
213+
if (inControl instanceof Optional<?> optional) {
214+
inControl = optional.orElse(null);
215+
}
216+
if (inControl == null) {
217+
return false;
218+
}
219+
if (inControl instanceof Boolean value) {
220+
return value;
221+
}
222+
if (tryInvokeBooleanNoArgs(inControl, "isActive")) {
223+
return true;
224+
}
225+
if (tryInvokeBooleanNoArgs(inControl, "isPathing")) {
226+
return true;
227+
}
228+
Object pausedValue = tryInvokeNoArgs(inControl, "isPaused");
229+
if (pausedValue instanceof Boolean paused) {
230+
return !paused;
231+
}
232+
return false;
233+
}
234+
134235
private Object handleEventListenerCall(Object proxy, Method method, Object[] args) {
135236
String methodName = method.getName();
136237
if ("onPathEvent".equals(methodName) && args != null && args.length == 1 && args[0] != null) {
@@ -195,6 +296,22 @@ private static Object invokeNoArgs(Object target, String methodName) throws Refl
195296
return target.getClass().getMethod(methodName).invoke(target);
196297
}
197298

299+
private static Object tryInvokeNoArgs(Object target, String methodName) {
300+
if (target == null) {
301+
return null;
302+
}
303+
try {
304+
return invokeNoArgs(target, methodName);
305+
} catch (ReflectiveOperationException exception) {
306+
return null;
307+
}
308+
}
309+
310+
private static boolean tryInvokeBooleanNoArgs(Object target, String methodName) {
311+
Object value = tryInvokeNoArgs(target, methodName);
312+
return value instanceof Boolean bool && bool;
313+
}
314+
198315
private static Object invoke(Object target, String methodName, Class<?>[] parameterTypes, Object[] values) throws ReflectiveOperationException {
199316
return target.getClass().getMethod(methodName, parameterTypes).invoke(target, values);
200317
}
@@ -236,4 +353,19 @@ private <T> T onClientThread(Callable<T> callable, T fallback) {
236353

237354
public record Outcome(boolean ok, String message) {
238355
}
356+
357+
public record ActivitySnapshot(
358+
boolean isPathing,
359+
boolean hasPath,
360+
boolean calcInProgress,
361+
boolean processInControlActive
362+
) {
363+
public boolean isBusy() {
364+
return isPathing || hasPath || calcInProgress || processInControlActive;
365+
}
366+
367+
public static ActivitySnapshot idle() {
368+
return new ActivitySnapshot(false, false, false, false);
369+
}
370+
}
239371
}

0 commit comments

Comments
 (0)