Phantom Block Manager
com.originmint.managers.PhantomBlockManager places phantom blocks: collision boxes at
coordinates Mint keeps itself, with no block behind them. Nothing is placed in the world and nothing
is sent to the server — the block at those coordinates is still air, and every other player sees air.
import com.originmint.managers.PhantomBlockManager;
PhantomBlockManager phantom = PhantomBlockManager.getInstance();phantom.setEnabled(true);phantom.add(120, 70, -32);phantom.add(121, 70, -32);
if (phantom.contains(120, 70, -32)) phantom.remove(120, 70, -32);phantom.clear();Available on all five client families.
What a phantom block does
A phantom block is a wall, never a floor:
| You are… | Result |
|---|---|
| walking into it | stopped, exactly as a block would stop you |
| flying or jumping up into it from below | stopped at its underside |
| falling onto it from above | you drop straight through — it is not there |
| standing beside it, trying to step up | no step-up; it has no top to stand on |
| looking at it | the crosshair passes through and reports a miss |
| clicking it | the click lands on whatever is really behind it |
The crosshair behaviour is not a special case that had to be written: targeting raycasts block states, and a phantom block has none, so it is invisible to targeting by construction.
Once you are partly inside one it stops pushing you altogether, so a fall through is a single clean drop rather than a fight with the collision resolver.
Collision applies to you and whatever you are riding only. These boxes exist on your client alone, so applying them to server-driven entities would only fight the positions the server sends.
They are drawn as a 70%-transparent gold block with the Mint logo on every face.
Methods
getInstance()returns the manager.add(x, y, z)adds a phantom block. Returnsfalsewhen one was already there. Takesintordoublecoordinates; doubles are floored to the block.remove(x, y, z)removes one. Returnsfalsewhen there was none.contains(x, y, z)reports whether one is there.clear()removes every phantom block.count()returns how many there are.positions()returns every phantom block asint[]{x, y, z}, in the order it was added.isEnabled()/setEnabled(boolean)— whether they collide and are drawn. Saved, as thePHANTOM_BLOCKSsetting, and off by default.isOverride()/setOverride(boolean)— draw one that landed on a real block as that block. See below. Saved, and off by default.isPlacing()/setPlacing(boolean)— the GUI’s placement mode. Session only, never saved.
Use the real block’s look
By default every phantom block is drawn as a see-through gold block. With setOverride(true), one
that landed on a real block is drawn as that block instead: its own model, its own textures and
its own tint, at the same transparency, with the same logo on every face. A phantom block on stone
becomes see-through stone; on oak planks, see-through oak planks; on leaves it even keeps its biome
tint.
Blocks with no block model of their own — chests, water, anything drawn as a block entity — have nothing to borrow, so those keep the gold block rather than drawing nothing at all.
The logo always sits on the faces of the full cube, whatever shape the borrowed model is, so on a slab it marks out the collision volume rather than the slab.
Because collision is additive, a phantom block inside a solid block adds nothing to how you move: the solid block already stops you from every direction. Where a phantom block still earns its keep on top of a real one is a block with no collision of its own — tall grass, a torch, water — where the phantom supplies the wall and override makes it look like the plant it is standing in.
Override is a single switch for all phantom blocks, not a per-block flag.
Lifetime
The coordinates last until the game closes and are never written to disk. They are kept across a world change, so the same coordinates are the same phantom blocks wherever you are — out of render distance they simply stop drawing. A plugin that wants a set to survive a restart should keep its own list and re-add it, ideally from a world-join handler.
They are shared client-wide rather than owned per plugin: two plugins adding the same coordinate are
adding the same block, and clear() clears everyone’s. Prefer remove over clear when you only
mean to tidy up after yourself.
Placement mode
Settings → Phantom blocks carries the same switches plus Placement mode, which is how you put them down by hand. With it on, right-click places a phantom block where you are pointing — against the face of whatever you are looking at, or in mid-air at arm’s length if you are looking at nothing — and right-clicking an existing one takes it away. A coloured outline shows where the next click lands: green to place, red to remove.
While placement mode is on, right-click and attack do nothing else: they are swallowed before they reach the world, so nothing is placed or mined for real and nothing is sent to the server.
Placement mode is session state and is never saved — it is off again at the next launch.
setPlacing(true) turns it on from a plugin, but it needs setEnabled(true) as well; the GUI toggle
switches the feature on for you.
Performance
A phantom block is not free the way a real block is. Real blocks are baked into a chunk mesh once and redrawn as part of one buffer, so a thousand of them cost nothing per frame. Phantom blocks are drawn like block entities — chests, signs — which means re-tessellated every frame.
Measured in an empty test arena at a ~2.2 ms/frame baseline, all of them in view:
| In view | Added per frame | Per block |
|---|---|---|
| 1000 real blocks | no measurable change | ~0 |
| 100 phantom blocks | +0.6 – 1.0 ms | — |
| 500 phantom blocks | +1.3 – 2.1 ms | ~0.003 ms |
| 1000 phantom blocks | +2.4 – 2.9 ms | ~0.003 ms |
| 4000 phantom blocks | +9.9 – 11.8 ms | ~0.003 ms |
So roughly three microseconds per phantom block per frame, scaling linearly, with a small fixed overhead on top for the two draw calls. A few hundred is imperceptible. A thousand costs a couple of milliseconds a frame — real but affordable. Several thousand in view at once will be felt.
Anything past 128 blocks from the camera is skipped entirely, so the number that matters is how many are near you, not how many exist. Override does not change the cost: it adds a block-state lookup and a tint lookup per block, both negligible, and every block model shares the same texture atlas so it is still one draw call however many different blocks are involved.
Everything that would cost on the server or the network costs nothing: no chunk update, no block entity, no lighting propagation, no packet.
Collision is a bounding-box test per phantom block per movement step over a flat array with no allocation, which is nothing next to the block lookups Minecraft is already doing.
Example: a bridge you cannot fall off
import com.originmint.managers.PhantomBlockManager;import com.originmint.plugin.IPlugin;
public class GuardRail implements IPlugin { private final PhantomBlockManager phantom = PhantomBlockManager.getInstance();
@Override public void onEnable() { phantom.setEnabled(true); // A waist-high rail along both sides of a walkway. You cannot walk off the edge, but you // can still drop down onto the walkway from above, because a phantom block is never a floor. for (int z = -40; z <= -20; z++) { phantom.add(119, 70, z); phantom.add(123, 70, z); } }
@Override public void onDisable() { for (int z = -40; z <= -20; z++) { phantom.remove(119, 70, z); phantom.remove(123, 70, z); } }}