A capability battery for Minecraft Bedrock: what the game can actually do, measured, per version, written down where a build can read it.
Bedrock's documentation does not cover most of what an add-on actually runs into, and the
surrounding web covers a lot of it wrongly. The usual response is to assume — and an assumed
capability is the most expensive kind of mistake here, because a rendering failure on Bedrock
is silent: an unresolvable Molang query does not error, it resolves to 0 and draws
something. The rig renders perfectly and means nothing.
So this repository asks the questions instead, and keeps the answers somewhere a build can check them.
npm install
npm run build # -> dist/bedshock-*.mcaddon, a probe-only add-on
npx bedshock run --version 1.21.120 # boots a real Bedrock server, records what it says
npx bedshock amend --version 1.21.120 # asks you the rows only eyes can settle
npx bedshock report # regenerates docs/ from the ledgerThree files, and the boundaries between them are the whole design.
content/capabilities/*.yaml |
The questions. Hand-written, and they never contain an answer. A question is the same on every Minecraft version — that is what makes it worth naming |
ledger/observations.jsonl |
The answers. Append-only, one line per (capability, version, run). Nothing ever edits one |
docs/CAPABILITIES.md |
Generated. Do not edit it. Every fact in it is a projection of the ledger |
There is no status: SETTLED field anywhere. A status is a function of observations, so the
only way to change one is to record a measurement — and the old measurement stays visible
beside the new one. That is the structural version of a rule that used to be prose, and prose
could not stop anybody: do not edit an expectation to make a probe quiet.
A capability is one assertable proposition with one verdict. A probe is the apparatus that
reports several of them at once. One container probe settles seven distinct things — whether
storage works, whether a horse container opens, whether a chest one does, whether the slots
are typed, whether the screen draws what was declared — and those have seven independent
lifetimes. Giving each its own row is what lets six of them go quiet while the seventh stays
open.
A capability is not true or false. It is true or false on a version, and the interesting moment is the one where that changes.
1.21.120 1.26.30
item.name.glyph_renders_in_colour yes yes
render.attachable.reads_held_item_durability no ·
Which makes narrowing the point of the whole thing. There are three reasons to ask a question, and they are different questions:
bedshock run --open --version <new> # nobody has answered this here
bedshock run --negative --version <new> # the answer was NO — the watchlist
bedshock run --regress --version <new> # the answer was YES — a drift check--negative is the one that matters most, and treating it as an afterthought was a real bug
in the first version of this: --open excluded measured-no rows, so a negative was never
re-asked by any narrowed sweep. That is exactly backwards. A battery for a platform that changes
under you is not mainly there to confirm what already works — it is there so that the day
something becomes possible, the test that proves it was written months ago.
So a negative is a question with a pending answer, not a closed file:
$ bedshock watchlist
4 capability(s) measured NO on Bedrock 1.21.120.
These are questions with a pending answer, not closed files.
render.attachable.reads_held_item_durability
Can a render controller on an attachable read the durability of the item it draws?
would unblock: The largest single consequence in this battery. If yes, ONE item type
renders every configuration in the hand and hundreds of baked sprites go away.
A sweep on a new Bedrock normally wants --open --negative: everything that is not already a
confirmed yes. And bedshock diff between two manifests prints became_possible first, because
that is the single most valuable line this project can produce.
What kind of answer it is — automated, observed, derived, solved.
What would have to change for the answer to change — and this one is required, because a consumer asking "will this still hold if I raise my script pin" gets a different answer for each:
| Surface | Moves when | Example |
|---|---|---|
engine |
Mojang changes the game | how far a thrown item travels; whether an entity blocks a player |
script |
the @minecraft/server version moves |
whether setEquipment sticks; what one knockback unit means |
content |
the pack format or the client's handling of it moves | max_durability as an int16; whether a flipbook animates an item tile |
A sweep that finds an engine row moved has found a gameplay change. A moved script row has
usually found an API change. Those are different news, and a row guessed into the wrong bucket
tells someone their pin does not matter when it does.
This pack always pins the newest script API, which is the opposite of what a shipping add-on
does and for the opposite reason. A mod pins low because every version it raises is a player
whose game is now too old. bedshock ships to nobody — so a low pin buys no compatibility and
costs the only thing that matters: a battery cannot ask about an API it did not declare. An
old pin does not make a newer capability OPEN; it makes it invisible.
The distinction is not cosmetic, and the runtime cannot blur it.
automated — the script API decides it with no interpretation. Whether a component reports
the number it was given; whether a dynamic property survives a round trip. These run headless
against a real Bedrock Dedicated Server, so they cost nothing and can sweep versions unattended.
observed — only a person looking at a screen can answer. Which pixel rows a bar covers;
whether a glyph is coloured; whether four flags render. The battery's job is to set the scene
exactly and enumerate the outcomes, never to guess the result.
derived — established by reading what the engine or our own emitted files do. Recorded so
nobody re-derives them, and so a future contradiction is visible as one.
solved — the answer is a number, not a yes. How close can a moving block pass beneath
a falling anvil without interrupting it? has an answer, and a probe for one of these does not
check a condition — it searches for the boundary.
These are the rows most worth having and the easiest to overlook. A yes/no capability only reports when something appears or disappears. A solved one reports when the game's constants shift underneath a design that was tuned to them — a reference implementation that still runs, still passes, and is now subtly wrong. Nothing else in this battery would notice that.
So a solved row drifts on its value, not its verdict: two runs both converging is not agreement
if they converged somewhere else. Each declares a tolerance taken from its own apparatus's
resolution, because a tolerance of zero reports drift on noise and a drift people learn to ignore
is worse than none.
Values are rendered as one aligned row of text rather than a chart — logarithmic, so a tenfold change is always the same visible jump and a sixteenth of a block is still a readable length:
physics.falling_block.min_clearance_under_a_falling_anvil ├────●─────────────── 0.063 blocks
physics.falling_block.gravity_curve ├───────●──────────── 0.33 b/tick
physics.knockback.blocks_per_unit ├──────────●───────── 1.4 b/unit
physics.throw.item_travel_distance ├────────────────●─── 28.5 blocks
├──────────────────── log scale, 0.01 to 100
The bar is for the eye; the number beside it is the record. bedshock diff reports a moved value
separately from a status change, because nothing appeared or disappeared — the ground shifted.
A LOOK is not a result. The runtime gives it a different verb, and bedshock run refuses to
record one. The only path from an eyes-only row to the ledger runs through a person.
Half this battery needs a person. Which pixel rows a bar covers, whether a glyph is coloured, whether four flags render — no API decides those. There are two ways in, and the first one is the one that matters.
Import the .mcworld from a release, join it, and run one command:
/scriptevent bedshock:session
It moves you into place for each question, locks movement so a rig cannot be walked away from, points the camera at what is being measured, hands you exactly the items that question needs, and shows the question with its outcomes as buttons. You look, and you tap.
None of that is a new idea — it is the same look_at and outcomes every observed capability
has always had to declare, because a probe that cannot say what to look for and how to tell two
results apart is not readable. That requirement turned out to be a UI specification.
At the end you get one line:
--- answer code ---
1C3C-2000-G101-G202-G303-G40S-V
9 answer(s). Hand this over; nothing else needs to leave the game.
bedshock redeem 1C3C-2000-G101-G202-G303-G40S-V --version 1.26.36.1That code is the entire channel out of the game, and it has to be. @minecraft/server-net
says so itself — "This module can only be used on Bedrock Dedicated Server" — so a client
add-on has no HTTP, no socket, no egress whatsoever. On a tablet or a console there is no
terminal beside it and nothing to write to. So: one short string per session, not one
sentence per question, in Crockford base32 with the confusable glyphs removed, grouped in fours
to be read off a screenshot.
It is checksummed, and that part is not decoration. A code that will not decode costs one re-read. A code that decoded to different valid answers would put measurements nobody made into an append-only ledger that other repositories build on, and nothing downstream could ever tell. A single mistyped character, or two swapped, is refused:
! checksum SV does not match QE. Something was mistyped or misread — re-read the code
rather than adjusting it.
The code also carries the catalog revision, because outcome indices are positional — a question edited since the session moves every index after it, so a code from another revision is refused rather than filed against the wrong questions. And the verdict recorded is the one the catalog declares for that outcome, never one carried in the code: the code says which button, the catalog says what it meant.
bedshock amend asks the same questions with the same answer space, for when you do have a
keyboard next to the game.
[3/9] item.icon.animates_from_flipbook (P10)
Does an item atlas tile animate from a flipbook_textures entry?
Look at:
The two flipbook probe items in your hotbar. They carry the SAME animation under two
deliberately different spellings — one with explicit frames and blend off, one omitting
frames with blend on. Different in every field that could plausibly be the mistake.
1) Both icons cycle [YES]
2) One cycles, one is frozen [YES]
Item flipbooks work and the frozen spelling was simply wrong. Note which one cycled.
3) Both frozen on frame 0 [NO]
Still short of proof. The definitive control is a flipbook on a BLOCK texture.
4) Cannot tell — the frames may be too similar, or nothing rendered [INCONCLUSIVE]
s) skip q) stop here
Either way, every observed row must offer an "I could not read it" answer — bedshock validate refuses one that cannot. A rig that did not render is not a negative result, and a
person forced to pick a real verdict for a broken apparatus will pick one.
bedshock is a dev-time dependency. Nothing from it ships in your add-on.
Cite a capability where your code rests on it:
/** @requires bedshock:item.max_durability.int16_ceiling */
const PACKED_MAX = 32767;and check the citations against your pack's own floor:
npx bedshock check --requires-from 'packs/**/*.ts' --version 1.21.120FAIL packs/behavior/scripts/gun.ts:212
@requires bedshock:item.creative.order_follows_emission
never measured at or below 1.21.120. This is a guess, however confident the code around
it looks. Run the battery, or carry a fallback and stop citing it.
Evidence inherits downward and never upward. A capability measured on 1.26.30 says nothing about a pack declaring it runs on 1.21.120 — the newer engine is exactly where a thing that did not used to work starts working. A pack that wants a capability its floor cannot prove has to raise the floor or carry a fallback.
Half the decisions downstream of a capability battery are made from absences. Hundreds of sprites get baked because a render controller cannot read the item it draws; a codec refuses shulker boxes because nothing can carry one opaquely. Those rest on a capability too — on it being measured no — so they get their own form:
/** @requires-not bedshock:render.attachable.reads_held_item_durability */
const BAKED_SPRITES = everyCombination();That passes while the row is CLOSED-NEGATIVE, fails while it is unmeasured — an absence
nobody confirmed is as much a guess as a presence nobody confirmed — and warns the day the
row turns positive, naming the workaround that has become unnecessary.
Which is the version axis paying off. A question stays in the catalog after its answer is no;
--open re-asks it on each new Bedrock; and when one finally comes back yes, the build tells
you what you can now delete. Nobody has to remember to go and re-read a document.
There is a programmatic form too, for a build that prefers a list to comments:
import { requireCapabilities } from 'bedshock';
requireCapabilities({ version: '1.21.120', ids: [...] }); // throws, naming every failureThe Markdown is for people. bedshock export writes the thing a machine reads:
{
"schema": 1,
"minecraft": "1.21.120",
"catalog_revision": "rc3c24c08",
"release": "mc-1.21.120-rc3c24c08",
"counts": { "SETTLED": 22, "CLOSED-NEGATIVE": 4, "OPEN": 18, "INCONCLUSIVE": 1, "derived": 15 },
"watchlist": ["render.attachable.reads_held_item_durability", "..."],
"capabilities": [ { "id": "...", "status": "...", "inherited": false, "outcomes": [...] } ]
}A manifest has two halves to its identity, and both are in the tag. The Minecraft version is one; the other is the catalog of questions that were asked, because adding a question or widening an answer space makes the same game yield a different manifest.
| Tag | |
|---|---|
mc-1.21.120-rc3c24c08 |
Immutable. This catalog, measured against this game, forever |
mc-1.21.120 |
Moves. Always the newest revision for that game |
Pin the moving tag for the best answers available on the game you run, or the full id for a byte-identical artifact. The revision is a content hash rather than a counter, because a counter is a thing somebody has to remember to bump.
bedshock is meant to be submoduled. action.yml is a composite action, so a repository that
vendors it at vendor/bedshock runs it as a local action with no registry and no credential:
- uses: ./vendor/bedshock
with:
mode: run
server-url: https://.../bedrock-server-1.26.36.1.zip # any version, any server build
record: trueAnd it can measure into its own ledger. catalog and ledger inputs — or the
BEDSHOCK_CATALOG / BEDSHOCK_LEDGER environment variables locally — point the apparatus at
files in your repository:
- uses: ./vendor/bedshock
with:
catalog: capabilities/ # your questions
ledger: capabilities/observations.jsonl
manifest-out: dist/my-capabilities.jsonThat is deliberately a first-class path rather than a hack. A project that measures its own answers has a better proof than one citing ours, and the code that enforces the discipline — append-only answers, an enumerated answer space, an absent run never recorded as a negative — is the part worth sharing. The questions in this repository are one instance of it.
Vendoring gets you the apparatus, and the apparatus includes the search. bedshock init writes a
catalog, a probe and an empty ledger that already validate:
bedshock init capabilities --domain portals --bedshock ../vendor/bedshock
The probe it writes imports from pack/scripts/harness.ts, which is
the contract for somebody else's pack:
solve |
A boundary. Your trial answers yes/no at a value; this bisects it and refuses the three ways a search lies about itself. |
measure |
A measurement. Your apparatus hands back a number; this takes several, reports the median, and refuses when they disagree. |
arena |
A box to measure in — flat, empty, carved, and verified, so a short throw is never confused with a wall. |
bisect / summarise |
Both again with no Minecraft in them, for measuring in memory. |
Write a trial, declare a range and a tolerance, and your numbers come out in the same shape ours do: same collector, same drift detection, same log-scale column, your own manifest with its own release tag.
This matters because a capability battery has two halves and only one of them generalises. Nobody else can ask how much upward boost your horizontal portal needs to clear the floor on exit, or the speed above which something crossing your trigger volume is never seen inside it. Those are questions about your code — but they are the same kind of question, and a boundary that moves between Minecraft versions is worth knowing about whoever owns it.
docs/SOLVING.md is the walk through, including the three ways a search
converges on a number that is not a measurement, and what this does about each.
The server is a seam too: server-url takes any Bedrock Dedicated Server zip, and log skips
the server entirely so a harness this action does not know how to start can feed a battery log in
and get the same collection rules applied to it.
docs/CAPABILITIES.md |
The matrix: every capability × every version. Generated |
docs/versions/ |
One report per Minecraft version. Generated |
docs/DESIGN.md |
Why it is shaped this way, and which failures each rule is for |
docs/PORTING.md |
Moving a messy in-repo test battery onto this |
docs/TECHNIQUES.md |
Proposal. Reference implementations that carry the version they last stood up on |
docs/SOLVING.md |
Measuring a number instead of a yes — and doing it with your own questions |
docs/WANTED.md |
Questions with no row yet, and the decision each blocks |
docs/HARNESS.md |
Design. Measuring a client on real hardware, by mounting this engine on a station that has one attached |
content/capabilities/ |
The questions |
content/pack.yaml |
The probe pack's identity and every apparatus parameter |
ledger/ |
The answers |
pack/scripts/ |
The in-game runtime |
pack/scripts/harness.ts |
The entry point for somebody else's pack |
tools/ |
Everything else, and its tests |
advocate.yml |
The seats. What each advocate was told to want |
.advocate-engine/ |
The caretaking framework, mounted. Work lives on advocate/<name> branches, never in main |
bedshock validate |
The questions and the ledger, checked against each other |
bedshock build |
Emit the probe pack |
bedshock run [--open] [--negative] [--regress] |
Boot a real server, record the automated answers |
bedshock collect <log> --version v |
Record from a log captured elsewhere |
bedshock amend [--probe p] [--all] [--negative] |
Answer the eyes-only rows from a terminal |
bedshock redeem <code> --version v |
Record a guided session's answer code |
bedshock report |
Regenerate docs/ |
bedshock check --requires-from <glob> --version v |
Fail a build resting on an unsettled capability |
bedshock watchlist [--version v] |
Every row measured NO, and what flipping it unblocks |
bedshock export [--version v] [--out f] |
The machine-readable manifest |
bedshock diff <a.json> <b.json> |
What moved between two manifests |
bedshock status [prefix] |
What the ledger says right now |
bedshock init [dir] [--domain d] |
A catalog, probe and ledger of your own questions |
bedshock tidy |
Sort the ledger file. Never alters a line |
The pack ships no gameplay. It is nothing but instruments, several of them deliberately in the creative menu because whether they can be kept out of it is one of the questions. Install it, ask, record, remove.