Skip to content

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 itstopped, exactly as a block would stop you
flying or jumping up into it from belowstopped at its underside
falling onto it from aboveyou drop straight through — it is not there
standing beside it, trying to step upno step-up; it has no top to stand on
looking at itthe crosshair passes through and reports a miss
clicking itthe 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. Returns false when one was already there. Takes int or double coordinates; doubles are floored to the block.
  • remove(x, y, z) removes one. Returns false when 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 as int[]{x, y, z}, in the order it was added.
  • isEnabled() / setEnabled(boolean) — whether they collide and are drawn. Saved, as the PHANTOM_BLOCKS setting, 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 viewAdded per framePer block
1000 real blocksno 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);
}
}
}