OpBase
Client
Server
Base class for the Vox operation builders (OpAdd, OpRemove, OpPaint, OpCopy, …). You don't use it directly — its chain methods (Force, Ignore, OnFinished, Run, …) are inherited by all of them. See Vox for the usage pattern.
Methods
OpBase Target(opTarget target)
Set the target shape/object(s) for this operation. Updates the cached VoxelEdit shape in-place if already built.
self Batch(boolean? on)
Batch this edit instead of applying it the moment you call Run(). It joins a shared queue and
lands within Vox.editQueue.flushFrames frames (default 8), in the order it was issued.
Why it is worth it. A voxel edit costs far more for the FRAME it lands on than for itself:
applying it invalidates blocks, which makes the renderer reload them and the server re-stream
them to every client. Pay that once for 40 edits instead of once each and most of it disappears.
Measured in a Cortex Command firefight (2 troops, ~200 edits/s, wall clock, 6 interleaved pairs):
| server frame | immediate | batched (every 8) |
| median | 123ms | 65ms |
| average | 127ms | 85ms |
| fps | 7.9 | 12.1 |
Cutting the effects instead is far weaker: dropping blood entirely won ~20%, spark scorch ~17%,
bullet craters ~4% - because whatever you leave in still edits every frame and still pays the
per-frame toll. Batching everything got 89% of the win available from deleting every edit.
Use it for cosmetic scarring - blood splatter, scorch marks, bullet craters, rubble, shell
casings. Anything the player only looks at.
Do NOT use it for anything read back synchronously - digging progress, volume/volumePerc
health checks, build placement validation, or an edit another edit depends on this frame. A
queued edit is invisible to those reads until it flushes and the failure mode is a silent
desync, not an error. Leave those alone; un-batched is the default.
What it costs. Up to flushFrames frames of latency before the mark appears (~0.5s at 15fps).
The flush frame itself is heavier, so batching trades tail for median: measured p95 183 -> 199ms
and max 215 -> 270ms while the median halved. Lower flushFrames to flatten that, raise it to
push the median lower.
When it silently declines (the edit just runs immediately, which is always correct):
- the edit is forced onto a dynamic object - it would land where the object was, and troop
damage has to register on the frame the bullet lands. One splatter op alternating between
terrain and body hits is decided per
Run(), so this is normal, not a mistake. - the shape is a
PolyhedronorPolygon(no cheap way to snapshot it). Vox.editQueue.enabledis false - the global off switch, for A/B profiling.
Vox:Paint(Sphere(hit.pos, 0.04)):Color(0, 0, 0, 0.5):Batch():Run() -- scorch mark
nil AcceptStall()
Opt-in for ONE oversized edit (soft limit only — the hard limit stays refused). Confirm with the user first: the edit executes as a single multi-second frame stall for every connected client.
nil OnFinished(fun onFinished)
Callback function. OnFinished is called after OnProgress if it was last part
nil OnProgress(fun onProgress)
Callback function. May not be called every frame. Is called after script updates