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 steps; private final List budgets; private final PrimitiveExecutor executor; private final Consumer completed; private final Map 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 steps, List budgets, PrimitiveExecutor executor, Consumer 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; } }