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;
    }
}