Volver al índice
src/main/java/ar/com/companeros/tools/ToolExecution.java
package ar.com.companeros.tools;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
/** Ejecución incremental: inicia como máximo una primitiva por tick y admite cancelación. */
public final class ToolExecution {
public enum Status { RUNNING, SUCCEEDED, FAILED, CANCELLED }
private final UUID id = UUID.randomUUID();
private final ToolManifest manifest;
private final ToolRegistry.ExecutionContext context;
private final List<ToolRegistry.CompiledStep> steps;
private final List<ToolRegistry.ExecutionBudget> budgets;
private final PrimitiveExecutor executor;
private final Consumer<Status> completed;
private final Map<Integer, Long> budgetStarts = new HashMap<>();
private PrimitiveExecutor.PrimitiveTask current;
private long lastTick = Long.MIN_VALUE;
private long stepStart;
private int stepIndex;
private double primitiveProgress;
private Status status = Status.RUNNING;
private String failureReason = "";
ToolExecution(ToolManifest manifest, ToolRegistry.ExecutionContext context, List<ToolRegistry.CompiledStep> steps,
List<ToolRegistry.ExecutionBudget> budgets, PrimitiveExecutor executor, Consumer<Status> completed) {
this.manifest = manifest; this.context = context; this.steps = steps;
this.budgets = budgets; this.executor = executor; this.completed = completed;
}
// Las acciones físicas ocurren en el hilo del servidor que llama tick.
public Status tick(long gameTick) {
if (status != Status.RUNNING || gameTick == lastTick) return status;
if (gameTick < context.startTick() || gameTick < lastTick) return finish(Status.FAILED, "El reloj de ejecución retrocedió");
lastTick = gameTick;
if (gameTick - context.startTick() >= manifest.maxTicks()) return finish(Status.FAILED, "Tiempo máximo de herramienta alcanzado");
for (int index = 0; index < budgets.size(); index++) {
ToolRegistry.ExecutionBudget budget = budgets.get(index);
if (stepIndex >= budget.startStep() && stepIndex < budget.endStep()) {
long start = budgetStarts.computeIfAbsent(index, ignored -> gameTick);
if (gameTick - start >= budget.maxTicks()) return finish(Status.FAILED, "Tiempo máximo de una composición alcanzado");
}
}
ToolRegistry.CompiledStep step = steps.get(stepIndex);
if (current != null && gameTick - stepStart >= step.timeoutTicks()) return finish(Status.FAILED, "La acción excedió su tiempo máximo");
try {
if (current == null) {
UUID invocation = UUID.nameUUIDFromBytes((id + ":" + stepIndex).getBytes(java.nio.charset.StandardCharsets.UTF_8));
current = executor.begin(context.actorId(), invocation, step.primitive(), step.arguments().deepCopy());
if (current == null) return finish(Status.FAILED, "La primitiva no pudo iniciarse");
stepStart = gameTick;
}
PrimitiveExecutor.Result result = current.tick(gameTick);
if (result == null) return finish(Status.FAILED, "La primitiva no devolvió resultado");
primitiveProgress = result.progress();
if (result.state() == PrimitiveExecutor.State.FAILED) return finish(Status.FAILED, result.message());
if (result.state() == PrimitiveExecutor.State.SUCCEEDED) {
current = null;
stepIndex++;
primitiveProgress = 0;
if (stepIndex == steps.size()) return finish(Status.SUCCEEDED, "");
}
return status;
} catch (RuntimeException exception) { return finish(Status.FAILED, "La acción no pudo completarse"); }
}
public Status cancel() { return finish(Status.CANCELLED, "Ejecución cancelada"); }
public UUID id() { return id; }
public String toolId() { return manifest.id(); }
public int version() { return manifest.version(); }
public UUID actorId() { return context.actorId(); }
public Status status() { return status; }
public String failureReason() { return failureReason; }
public int completedSteps() { return stepIndex; }
public double progress() {
return status == Status.SUCCEEDED ? 1 : Math.min(1, (stepIndex + primitiveProgress) / steps.size());
}
// El resultado se contabiliza una sola vez, incluso ante ticks y cancelaciones repetidos.
private Status finish(Status target, String reason) {
if (status != Status.RUNNING) return status;
if (current != null) {
try { current.cancel(); } catch (RuntimeException ignored) { }
current = null;
}
status = target;
failureReason = reason == null ? "" : reason;
completed.accept(target);
return status;
}
}