Class DrawFrame

java.lang.Object
com.botwithus.bot.api.draw.DrawFrame
All Implemented Interfaces:
DrawTarget, AutoCloseable

public final class DrawFrame extends Object implements DrawTarget, AutoCloseable
A tick's worth of drawing, collapsed into as few round-trips as the wire allows.
try (DrawFrame frame = draw.frame()) {
    frame.rect("box", x, y, w, h).color(Colors.GREEN).submit();
    frame.text("label", x, y - 12, npc.name()).submit();
    frame.npc(npc).color(Colors.RED).submit();
}   // one debug_draw_set_batch for the primitives, one highlight_entity, here

Commands submitted to a frame are held, not sent. close() sends them and is what try-with-resources calls; flush() is the same send with the result handed back. Both are safe to call repeatedly — a frame with nothing pending sends nothing.

A frame is one round-trip for primitives and one more per highlight, and that is the wire's shape rather than a shortcoming here. debug_draw_set_batch applies debug_draw_set items, whose kind field cannot name a semantic highlight, so there is no batch a DrawCommand.Highlight could ride. Both families are accepted here anyway, because the alternative — refusing frame.npc(...) at compile time — would break a call site to tell the caller something they cannot act on, and would scatter a script's drawing across two mechanisms for a reason that is not the script's business. pendingHighlightCount() is how many extra calls a flush will make, if it matters.

The cost is bounded by what highlights are for: a script marks a handful of tracked things a tick, while the batch exists for the dozens of primitives it draws around them. Twenty rects and three highlights is four round-trips, not twenty-three.

Not thread-safe, and not meant to be: a frame is a tick-scoped local.

  • Method Details

    • submit

      public String submit(DrawCommand command)
      Route a fully-built command, answering the key it is stored under.

      The plumbing seam the fluent builders call, and the escape hatch for a caller holding a DrawCommand it built or stored itself.

      Queues rather than sends, into whichever of the two pending lists the command's family belongs to. The key is answered immediately because it is the caller's own — nothing about it depends on the producer.

      Specified by:
      submit in interface DrawTarget
    • pendingCount

      public int pendingCount()
      How many commands are waiting to be sent, of both families.
    • pendingHighlightCount

      public int pendingHighlightCount()
      How many of the pending commands are highlights — which is how many round-trips beyond the batch a flush will cost, since none of them can be batched.
    • flush

      public DrawBatchResult flush()
      Send everything queued and answer what the producer did with it.

      The primitives go first, as one debug_draw_set_batch per DrawLimits.MAX_BATCH_ITEMS of them — so exactly one for any frame of that size or smaller, which is every realistic frame. Beyond it the frame is split rather than truncated or refused, because the producer rejects an over-size batch outright and would lose the whole frame including the part that would have fit. Then each highlight, as its own highlight_* call, because no batch can carry one.

      The returned result is the aggregate over all of it. Check DrawBatchResult.dropped(): the wire reports per-item refusals inside a successful reply, so a frame that drew nothing does not throw. A refused highlight is folded into the same count rather than thrown, so one contract covers the whole frame. Anything refused is also logged at WARN, so a caller that ignores this return is still told.

      If this throws, the store is in a partly-known state — not a clean one.

      What is applied when it does, in the order the flush attempts them:

      1. Sub-batches that completed are entirely drawn. The producer applies each item to its store as it walks the array, so a batch that fails at the envelope level may still have drawn part of what it carried, and the error reply says nothing about how far it got. Splitting sharpens that: when a later sub-batch fails, every command in an earlier one is definitely drawn.
      2. The failing sub-batch is the unknown. Somewhere between none and all of it reached the store.
      3. Highlights are not half-applied. Each is a single store operation behind a single call, so every highlight before a failure is drawn and the failing one is not. That is strictly more determinate than the batch half — the one thing sending them separately buys.
      4. Nothing after the failure was sent at all.

      Two things make it recoverable, and both are deliberate:

      • The pending commands are kept rather than cleared, so the keys are still in hand and calling flush() again is safe — setting a key replaces rather than appends, so a redelivered command is a no-op rather than a duplicate.
      • Draw.list() reports what the producer actually retained, and Draw.clearAll() removes all of it without needing any keys at all.

      In practice a content-level rejection is not reachable from this API — see DrawCodecTest for why the encoder cannot emit an item the producer would refuse structurally — so a throw here is a transport or script-lifecycle failure. When it is severe enough to close the pipe, the producer drops everything this connection drew, which leaves the store clean rather than half-full; a revoked script is the case where earlier calls stay drawn until their TTL expires.

    • abandon

      public int abandon()
      Discard everything queued without sending it. Answers how many were dropped on the floor, of both families.
    • close

      public void close()
      Flush. Never throws for refused commands — a debug overlay must not be able to take a script's tick down, and that holds for a refused highlight as much as for a refused rect — but every refusal is logged at WARN. Call flush() instead when the script wants to act on the result.

      A transport failure does still propagate, because it is not the overlay failing: swallowing it would hide a dead pipe. Note that a throw from here inside try-with-resources is suppressed when the block's own body already threw, so the WARN flush() logs is the reliable record that the store may hold part of this frame.

      Specified by:
      close in interface AutoCloseable