Repository navigation
Releases: Maskviva/Pier
Release list
Pier 26.51.2
Built for BDS 1.26.51 and LeviLamina 26.51.5, like 26.51.1. PIER_ABI_VERSION stays at 2 and
no slot moved: a mod built against 26.51.1 loads unchanged. pier-sys-rs is 26.51.2 and
pier-rs 2.1.0; nothing public was removed from either (contract §2.5). The Go and Zig
bindings are new in this release.
Added
- The documentation site runs on MkDocs Material and lives on GitHub Pages. It is laid
out as the LegacyScriptEngine documentation is: navigation tabs, a section tree on the
left, every entry of the page on the right. The pages under Common tasks, in the
Tutorials tab, document one function per entry, with its call form in Rust, Go and Zig,
its parameters, return value and type, its slot, and an example in each language, in
English and Chinese; their overview page maps the data types between the languages. The
home page follows the layout of levimc.org, in Pier's orange: LeviLamina is the green
leaves and Pier the fruit. The English and Chinese sites build separately,mkdocs build --strictrefusing a broken link, anddocs.ymlpublishes both to GitHub Pages whenever a
release is published. - The API tab lists every public interface of each binding, generated from its source.
One part per binding: thelevilaminacrate, 792 functions, methods and macros, 117
types and 76 constants; the Go package, 628 functions and methods, 173 of them on
Raw, one per slot without a callback, with 62 types and 316 constants; the Zig module,
305 functions and methods; andsdk/abi.hfor C++ mods compiled with the Pier SDK, its
201 slots and 6 macros with every constant table and type. An entry gives the
declaration, the source comment, the parameters and return type, and the slots of abi.h
the interface reaches, found by following its body through the calls whose target the
text names; each slot on the C++ pages lists its callers in Rust, Go and Zig.
tools/gen-api-docs.pywrites the pages and the API section of both navigations, and the
api-docs-currentcheck fails when a binding changes and the pages are not generated
again. Links in both sites are checked down to the heading they name, so a renamed
interface failsmkdocs build --strict. The Chinese site translates every description:
1541 units indocs/i18n/zh/api/, each quoting the English comment it translates and
keyed by that comment's fingerprint, and the same check fails when a comment has no unit
or a unit's English has left the source. Reading the Go comments against abi.h on the
way corrected three that named constants abi.h does not have, and Raw methods that had
taken a section note in place of their slot's own comment. - Documentation and comments describe what happens. Headings, slogans and closing
metaphors that handed over a verdict were rewritten to show the process instead, in the
READMEs, the design and why pages, the API pages and the binding comments.
COMMENTS.md section 9 says why, and theprose-tellscheck fails on the recognizable
constructions in every Markdown file and comment. - The Zig binding, for Zig 0.15.2: the package
pierinbindings/zig, exporting the
modulelevilamina, with the exampleexamples/hello-pier-zig. translate-c readsabi.hitself, andlevilamina.slot("name")is
any slot's function pointer behind both gates of contract §10, so the whole table is
reachable from the start. On it: the lifecycle throughexportMod, logging, mod-owned
tasks, events, commands, services and the bus under the same rules as the Rust and Go
bindings, one method per property and verb onPlayer,Entity,BlockAtandItem,
and an SNBT parser with the same rules and depth cap.zig build testmakes the compiler
analyze every function of the binding against the header and runs the parser's tests; a
zigjob in build.yml runs it, checks the formatting and cross-compiles the example to a
Windows DLL.tools/gen-zig-api.pywrites the property methods and the package's copy of
abi.h, and thezig-bindingcheck fails when either is stale or a slot or constant
the sources name does not exist. Documented in English and Chinese. - The Go binding, imported as
github.com/Maskviva/pier/bindings/go/levilaminaand
writtenlevilamina.…in code, with the example
examples/hello-pier-go. cgo includesabi.hitself, so the table is laid out by the C
compiler and not mirrored by hand, and every slot is reached through a generated C
function that applies both gates of contract §10. Its API is within reach of the Rust
binding's: every slot without a callback is a typed method ofpier.Raw, every property
and verb of the constant tables is a generated method ofPlayer,Entity,BlockAtor
Item, and hand-written functions cover the server, world, events with payload parsing,
commands with typed overloads, containers, key-value stores, the economy, forms, packet
and connection hooks, region scans, bulk block writes, simulated players, custom and
supplied-terrain dimensions and client keys, with an SNBT parser of the same rules as the
Rust one. Every slot is reachable but the two of lanes, which pair only mods of one
toolchain. Its services and bus topics follow the same rules as the Rust binding's, a veto istrueand a provider's error text reaches the caller
unchanged, so it calls and answers mods of any language and native plugins through the
bridge. Built withgo build -buildmode=c-sharedand a MinGW-w64 gcc. Documented in English and Chinese. tools/gen-go-slots.pyand thego-bindingcheck. The generator writes the
per-slot C functions and the Go module's copy ofabi.h; the check fails when either is
stale, when Go calls a C function nothing declares, or when the glue hands the host a
callback no Go file exports. Agojob in build.yml formats, vets and builds the binding
and the example on Windows.- Rust:
try_variants for every call that answered "cannot tell" with a value.
Host::try_pending_tasks,Host::try_is_wine,World::try_villages,
World::try_structures_near,money::try_history,money::try_ranking,
money::try_clear_history,sim::try_list,SimPlayer::try_is_simulatedand
KvDb::try_getreturn aResult, so a host without the slot is anErrand no longer the
same answer as an empty list, a 0 or a false. The old functions stay, deprecated, with the
behavior they had (contract §2.5);KvDb::getis not deprecated, sincetry_getcannot
tell a closed database from a missing key either. - The documentation is organized by reader, the way LeviLamina's is: a user guide, a
developer guide split into tutorials, how-to guides, FAQ, architecture and API reference,
a maintainer guide, and the supported versions. New pages: troubleshooting, FAQ, and an
event payload reference, which documents the attacker fields of death events among the
rest. Every existing page keeps its address. - Contract §2.5: a binding removes nothing while the ABI version stays the same. It
applies to every binding in every language: an item to retire is deprecated through the
language's own mechanism, and removed only in the release whose ABI version changes. The
adding-a-language guide states it for binding authors. - The
abi-valuescheck keeps every releasedPIER_*constant and enum member at its
value, against the baselinetools/abi-v2.values. A renumbering done inabi.hand the
mirror together passed every check before. comment-styleapplies the English rule to Rust, Python, TOML and xmake comments,
where Chinese documentation had been sitting unseen.- The
synthetic-namescheck compares the SDK'sALL_SYNTHETICwith the events the
host registers, in both directions. The list had drifted by one entry with nothing to see
it. - A watchdog over mod code, on by default. Every place Pier runs a mod's code is
watched: loading and unloading its DLL,pier_main, the lifecycle callbacks and every
event, command, task, form, bus, service, packet and hook callback. A mod holding a thread
pastwatchdog.warn_ms(2000) is named in a warning with the entry and the thread; past
watchdog.hang_ms(30000), orwatchdog.shutdown_ms(10000) once shutdown has begun, the
process is ended with exit code 70 so a supervisor can restart it. A server that froze on
a mod, or never finished stopping, now says which mod and comes back.watchdog.enabled
turns it off; a limit of 0 turns that one step off. Nothing is ended while a debugger is
attached. pier_bridge_call_sinkandpier_bridge_list_sink. The bridge hands a reply over
whole, so the provider runs once.pier-bridge.huses them when present and reports
throughClient::runsOnce()whether it can. The bridge ABI stays at 1.registry_list: the engine's block, item and entity registries, one JSON object per
entry. A block carries its creative category, solidity, shape families, block entity,
destroy speed, resistance and light; an item its rarity, stack size, creative category and
whether commands hide it; an entity its spawn egg, summonability, experiment gate and actor
type bits. In Rust:registry::blocks(),registry::items(),registry::entities(). A mod
that hands out random content picks by rules over the game's own content instead of keeping
a list of names.
Changed
- Every binding is written
levilaminain code, while its package names Pier: the Rust
packagepier-rsexposes the cratelevilamina, the Go package
github.com/Maskviva/pier/bindings/go/levilaminaislevilamina, the Zig packagepier
exports the modulelevilamina, andpier-bridge.hputs its classes in
levilamina::bridge.pier::bridgeremains as a deprecated alias of the new namespace
(contract §2.5), so a native plugin written against it still builds, with a warning under
MSVC and clang. - **The...
Pier 26.51.1
Built for BDS 1.26.51 and LeviLamina 26.51.5. A release is built against the one BDS its
number names, so 26.40.3 stays the build for BDS 1.26.40 and does not run on 1.26.51.
None of the engine changes reaches the ABI: PIER_ABI_VERSION stays at 2 and a mod built
against 26.40.3 keeps working without a rebuild. One behaviour does change for mods: an
ability or permission level written before the player has finished joining is now
refused, see below.
Added
- Literal subcommands in
register_command_ex. A parameter of kindtext, with the
word in itstextfield, declares a literal: a node in the command tree, which is what
/scoreboard objectives addis made of, and the only form the client narrows as the
player types. Anenumparameter lists its values but is not a tree node, so the
client had nothing to filter on, and the spec had no way to ask for a literal. The
handler receives the word under the parameter's name like any other argument, so
dispatch does not depend on an overload index that moves when an overload is inserted
above it. Two overloads that differ only by a literal digest differently, so a reload
cannot rebind one onto the other. In pier-rs this isOverloadBuilder::text(name, literal). A host before 26.51.1 rejects the kind as unknown and does not register the
command.
Changed
- Ability and permission-level writes wait until the player has finished joining.
PIER_PACT_SET_ABILITYandPIER_PACT_SET_PERMISSION_LEVELreturn false, with a
warning in the log, untilPlayer::isPlayerInitialized. Written earlier, the change
reaches a client that has not built its ability layers yet: server and client then
disagree, and the player can be left unable to build or attack until they rejoin. The
write is refused rather than queued, so a caller that tried too early finds out at the
call instead of seeing the change land at an unrelated moment. Simulated players have
no client and are exempt. A mod that sets abilities while a player joins has to make
the write once the player has fully joined. PIER_SRV_PROTOCOL_VERSIONknows 1.26.51, which speaks 2193, read from the
startup banner of a 1.26.51 server like the 1.26.40 row before it.- Custom dimension registration passes the whole definition.
DimensionManager::_registerCustomDimensionWithDimensionDefinitionGroupis gone in
26.51. Its replacement,_registerDimensionWithDimensionDefinitionGroup, takes a
completeDimensionDefinition, so the height and the generator go in at registration
instead of being corrected afterwards. The definition carries the 1.26.50
minecraft:dimension_heightshape,mMinYandmHeightRange, in place of a minimum
and a maximum. The top of a dimension ismMinY + mHeightRange, and the
dimension-data trace prints it that way. MobEffectInstancehas its two-argument constructor back. 26.51 exports
MobEffectInstance(id, duration)again and declares the default constructor without
defining it. Adding an effect builds the instance through that constructor and assigns
the amplifier and the visibility; every other field is what the engine's constructor
sets rather than value-initialized.Common::getGameVersionStringis gone on the server.PIER_SRV_BDS_VERSIONreads
Common::StringConstants::mGameVersionStringinstead, built once through
Common::_buildStringConstants.GameMode::useItemOntakes aHandSlot, and thePlayerUseItemOnEventhook
passes it through unchanged. The event payload is unchanged.VoidGeneratortakes the default biome as a constructor argument. A void
dimension passes the biome its spec names, or plains when that one is missing, as
before.ChunkStatelostNeighborAwareUpgradeNeededandNeighborAwareUpgrading, and
the chunk trace no longer names them.- The Rust binding moves to pier-rs 2.1.0 / pier-sys-rs 26.51.1.
Pier 26.40.3
Fixed
-
lip installproduced a mod directory LeviLamina refuses to load. The release
archive kept the packer's output directory as an extra level, so the package installed
asplugins/pier/Pier/. LeviLamina scans one level underplugins/, finds no
manifest.jsoninplugins/pier/, and Pier is never loaded.Two things were wrong and both are fixed.
tooth.jsonplaced the whole archive root
intoplugins/pier/instead of placing the mod directory intoplugins/Pier/. And the
release workflow tried to strip the wrapping directory by testing for a literalpier,
which is silently false on a case-sensitive runner where the packer writesPier.The packaging step now finds the mod directory by the
manifest.jsonit holds, renames
it to the name that manifest states, and refuses to publish an archive whose root is not
that directory with its dll and itslang/beside it. The folder name is not cosmetic:
ModRegistrar.cppcompares it against the manifest name as astd::string, sopier
andPierare two different mods and one of them does not exist. -
manifest.jsonnamedpier.dllwhile the build producesPier.dll. Windows
resolves it either way, which is why it went unnoticed; it now says what the file is
called.
Pier 26.40.2
Added
-
A mod that is not a Pier mod can call a Pier mod's services.
pier_bridge_call,
pier_bridge_listandpier_bridge_abiare exported from the host, and
bindings/bridge/pier-bridge.his a header-only client that binds them. Until now the
registry was reachable only with aPierModHandlethe loader issues, and the only
exported symbol pointed the other way, so a permission manager whose value is being the
backend everyone shares was reachable only by mods that had already adopted Pier.The registry is unchanged:
api_service_callalready accepted a null handle and its
self-call check was already conditional on one. What a bridge caller lacks is identity,
which is a real limit and is documented as one -- a provider that grants anything on the
strength of the caller alone must not be reachable this way. Seedocs/guide/bridge.md. -
PlayerEditSignEventandPlayerOperatedItemFrameEventare raised. Both were
declared inALL_SYNTHETICand listed as names, and no hook in the tree produced
either, so a subscription succeeded and never fired. A protection mod gating on them
reported a refusal its own operator could see while the player edited the sign anyway.
Sign editing goes throughSignBlockActor::_playerCanUpdate, which is the engine's own
gate and is asked before the text is taken from the packet; the item-frame right click
goes throughItemFrameBlock::use. Both are inCANCELLABLEnow. -
Every player event with a position carries
block, and every one with a player
carriesitem. Injected by the enricher rather than by each hook, because the events
that need them are LeviLamina's own and have no hook here to extend. A consumer
subdividing a permission by what was touched had nowhere to get the answer: the payload
named the action and never the object.blockis the block at the event's coordinates
anditemis what the player held. Both are absent rather than empty when they cannot
be read, since a name invented here is indistinguishable from a real one and a rule
keyed on it fires on the wrong block. -
PlayerOpenContainerEventcarriescontainerandblock. The payload had only a
numericcontainerType, so a subscriber wanting to tell a chest from a dropper had to
carry its own copy of a network enum.containeris that enum as a word andblockis
the block's type name, which is empty rather than a placeholder when it cannot be read:
an invented name is indistinguishable from a real one, and a rule keyed on it fires on
the wrong block.
Fixed
PlayerRequestItemActionEventis declared observation-only. Nothing raises it and
there is no point at which it can be cancelled;is_cancellableansweredNone, which
a caller reads as "unknown" rather than "no". It now answersSome(false)with where to
block instead, which is what stops a protection mod from treating a subscription to it
as a working gate.
Changed
-
Translations are files again, not a generated C++ table.
packages/pier-support/src/LangShipped.inc,tools/embed-lang.pyand
tools/checks/lang_embedded.pyare gone. The host readsplugins/Pier/lang/<code>.lang
and nothing else is compiled in beyond the built-in English, so a language is a file an
operator drops in and edits rather than a table only a rebuild can change.The packaging half that made the table necessary is handled where it belongs: the
after_buildstep inxmake.luanamesen_US.langandzh_CN.langexplicitly and
fails the build when either is absent, sobin/Pier/lang/carries both and a release
cannot ship half-translated. A glob alone still succeeds on one file, which is the
shape that let the directory go missing before. -
lang/en_US.langis added and carries every key. It is what a translator copies and
what an operator edits;i18n_keys.pynow fails when it is missing a key, when either
shipped file is absent, or when a placeholder count disagrees with the English.
Fixed
-
The lang directory never reached an installed server, so every log was English.
loadLanguagesreadsgetModDir()/lang, modpacker packages the target and
manifest.jsonand nothing else, solang/sat in the repository and shipped in no
archive. Nothing reported it: a missing lang directory is not an error, and the one
line that would have said so only printed when keys loaded.The startup line now prints on every boot rather than only when a file was found, and
reports how many lines the active locale has rather than how many a file contributed.
Suppressing it when there is no file would leave exactly the gap this closed. -
PIER_SRV_PROTOCOL_VERSIONreported 2169 for BDS 1.26.40, which speaks 2168. The
row came from a published metadata table rather than from a server; the banner on a
real 1.26.40.8 readsProtocolVersion 2168. A consumer mapped the 2169 back through
its own table and announced the server as 1.26.45 — a confident wrong answer on a
clean boot, which is the failure the key was rewritten to stop producing. The 1.21.93
row went with it: it was taken from a save file'sNetworkVersiontag, which is the
same class of unchecked evidence. An unlisted version fails the key, and failing is
the correct outcome. 2168 covers 1.26.40 through 1.26.44 and 2169 is 1.26.45, but only
1.26.40 is listed — a range known second-hand is the evidence that produced the wrong
row in the first place. -
tools/checks/i18n_keys.pytruncated any English value containing{},. It cut
the value at the first},, which also matches a placeholder followed by a comma, so
... as id {}, registry id {}was read as having one placeholder. A correct
translation was then reported as having too many. It now collects the C++ string
literals properly.
Added
- Seven more log keys: the per-dimension boot lines an operator reads to answer "did my
world come up the way I wrote it" (dim.height.set,dim.registered,dim.ready,
dim.terrain.layers,dim.terrain.end), and the host's own ready line
(host.ready). The grid clause of the layers line is its own key rather than a
hardcoded tail, because a translation that cannot move that clause has to reorder the
sentence around it.
Added
-
Three events that used to need iListenAttentively are hooked here.
PlayerEditSignEvent,PlayerOperatedItemFrameEventand
PlayerRequestItemActionEventmoved fromALL_FROM_ILAtoALL_SYNTHETIC.Each closes a hole rather than adding a convenience. Placing a sign is a block place
and was covered; editing the text of one already there was neither a place nor an
interact. Taking an item out of a frame was covered byPlayerAttackItemFrameEvent;
rotating it was not. Opening an enchanting table is an interact and was covered;
everything after that arrives on the item-stack request path and was not.Taking them off another mod is deliberate. A protection feature that only works when a
third-party mod happens to be installed is not a protection feature, and the version
matrix was already BDS × LeviLamina without adding a third pin that this repository
cannot fix when it lags a BDS release.PlayerRequestItemActionEventreports only theCraft*action types, as anaction
word (craft/creative/anvil/grindstone/loom) rather than the engine's
enumerator number, which shifts when Mojang inserts a type. Take, place and swap are
not reported: several arrive per click, and a cross-mod dispatch on that path would
cost more than it protects.
Pier 26.40.1
Built for BDS 1.26.40 and LeviLamina 26.40.0. Two fixes, both of the same shape: a value
that was wrong rather than missing, and therefore acted on.
PIER_ABI_VERSION stays at 2. The two new server_info_str keys are additive — a mod
built against 26.40.0 keeps working without a rebuild, though see the note on
PIER_SRV_PROTOCOL_VERSION below before relying on the number it used to return.
Fixed
-
A per-player sidebar stopped updating after its first send.
PIER_PACT_SIDEBAR_SET
updates a sidebar of unchanged row count in place, sending only the rows whose text
differs. The client stores a row asscoreboard id -> (name, score)and reads a
ChangeFakePlayerScorefor an id it already knows as a score update, not a rename: the
number moved, the label did not. So the first send was correct and every later one
silently did nothing.A changed row now releases its id with a
RemoveScorebefore theChangeFakePlayerScore
re-adds it. The removes go out as their own packet ahead of the changes: nothing here can
verify that a client applies one packet's entries in order, and a remove applied after
the re-add blanks the row instead of updating it. If the second packet cannot be created
after the removes have gone out, the cached state is dropped so the next call rebuilds
from scratch — keeping it would leave those rows blank until the row count changed. -
PIER_SRV_PROTOCOL_VERSIONreported the save file's protocol, not the server's. It
readLevelData::mNetworkVersion, which is a storage tag sitting among
mStorageVersion,mMinCompatibleClientVersionandmLastOpenedWithVersion, and
records the version the level was last written by. A 1.21.93 world opened on a 1.26.40
server answered 819. This looked correct for as long as the test machine's world had
been created by the server running it.It is now derived from
SharedConstants::CurrentGameSemVersion, which unlike
NetworkProtocolVersionis not behind#ifdef LL_PLAT_Cand so is readable on a
dedicated server, mapped through a table of confirmed versions.On a version the table does not list, and on any pre-release build, the key now
fails. That is deliberate. The old behaviour was not a crash, it was a confident wrong
answer, and a caller handed a plausible number acts on it. A pre-release shares its
major.minor.patchwith the retail build but not its protocol — 1.26.40-beta.0 speaks
2168 where retail 1.26.40 speaks 2169 — so a build carrying amPreReleaseis refused
rather than answered from a retail row. The table matches the exact triple: Bedrock
bumps the protocol on patch releases, so matching onmajor.minorwould return a number
from the wrong end of a release line.
Added
-
PIER_SRV_LEVEL_PROTOCOL_VERSION, theLevelData::mNetworkVersionvalue under the
name it deserves. It answers how old the save is, which is a real question and exactly
what the old key was accidentally answering.Host::level_protocol_versionon the Rust
side. -
PIER_SRV_GAME_SEM_VERSION,"major.minor.patch"of the running build, so a mod can
carry its own version-to-protocol table. A Pier release should not be on the critical
path for a mod supporting a BDS that shipped yesterday.Host::game_sem_versionon the
Rust side. -
pier-probecoversserver_info_strkeys 0 through 3. ARefusedon key 1 is recorded
as the correct answer for an unlisted version rather than as a fault; comparing it
against key 2 shows directly when a save is older than the server.
Added
-
A mod can supply its own terrain, and this host stops trying to understand it.
md_add_dimension_generatedregisters a dimension whose spec carries seed, height and
sky and no terrain section at all; the mod hands over two palettes, resolved once here
against the block and biome registries, and a callback the chunk workers call with a
buffer of indices.md_set_dimension_cellsgives the confinement rules the grid the
mod generated, since the hooks are engine hooks but the geometry is not. What crosses
the boundary per chunk is indices and never names: a chunk is 98304 entries and looking
names up per chunk is the difference between a generator and a stall. The threading
contract onPierGenerateChunkFnis the opposite of the rest of this file and is
written out there rather than left to the default.A payload with no terrain section is what says the terrain is a mod's, so
md_add_dimension
refuses one -- taking it there would register a dimension nothing fills and report success --
and a supplied dimension whose mod is gone loads as a void with its chunks intact rather than
falling through to the pack branch, where reading the terrain as a pack would throw on a chunk
thread. -
terrain.kind: layersis served again, and needs no tool and no file. A layer stack,
optionally cut into a grid of cells and gaps, is what a plot world and a superflat world
are, and it had been withdrawn in favour of terrain packs built offline with
tools/pier-pack. A caller now sends the recipe inline, as before, and it is assembled
into a template pack in memory and handed to the pack path: the chunk fill, the layout
expansion and the confinement hook underneath are the ones the pack path uses and the
ones the tests cover, so this adds a front door rather than a second generator.
pierpack/from_layers.pyperforms the identical mapping offline and stays the reference.
Every expression the assembled pack carries is a constant, because the inline recipe
names concrete numbers, so nothing has to be bound and no parameter can fail to bind.
A recipe whose border does not fit inside its cell, or whose stack runs past the top of
the dimension, is refused with the numbers rather than quietly shrunk.terrain.kind: noiseis still not served inline: it is a volume pack and not a layer stack, and the
in-memory assembly does not reach it.
Fixed
- A setting with no implementation behind it is refused, not recorded.
aquifers_enabled
andore_veins_enabledeach had a bit in the volume pack format and nothing anywhere that
read it: the builder wrote the flag, the host loaded the pack, and the terrain came out
without the aquifers or the ore veins its author had asked for, with no line on either
side. The tool refuses the two keys now, the host refuses a pack whose settings carry
either bit, and a bit outside the known set is refused as well, so a pack from a newer
tool cannot be half-honoured. The bit values stay recorded so their meaning is fixed if
either is ever implemented. - A source key the builder does not read is refused. Neither pack builder validated its
top-level keys, so a misspelled name, or a field copied from a Java datapack that this
format has no equivalent for, was dropped without a word and the pack was built as though
the author had not written it. Both builders now carry the list of keys they act on and
refuse anything else. - The shipped terrain packs are built for the world bottom that exists.
fixtures/town.json,
fixtures/plot.json,pierpack/from_layers.pyand two of the tool's tests all declared
a bottom of -512, the valuekWorldMinYheld before it was lowered to the vanilla -64;
nothing updated them at the time. A pack made for -512..320 does not fit a dimension of
-64..320, so mounting one was refused withPIER_PACK_HEIGHTand no world could be built
from it. The declared range is -64..320 now, the empty bottom layer that ran from -512 to
the bedrock is gone, and the two equivalence tests mount at the range the packs actually
carry. - A binary the host cannot open names the path it tried.
binaryin a pack config is
joined onto the config's own directory, and a config written with a path relative to the
server root instead produces a doubled prefix; the refusal said only that a file could
not be opened, which sends the reader to check a path the host never tried. - A mistyped field in a spec is a refusal with a reason, not an exception. The
conversions onCompoundTagVariantare not uniform: a number reached through the wrong
type throwsstd::runtime_error, a string does it throughstd::getand throws
std::bad_variant_access, and.get<CompoundTag>()does the same for a section. Any of
them escaped the reader, was caught by the API guard several frames away, and came back
as a registration that failed naming neither the field, the dimension, nor the recipe.
Every field is now type-checked before it is read, a wrong one reads as absent and is
reported, andmd_add_dimensionnames the dimension and the spec text if anything still
throws. - The host's startup line carries a build stamp, so a report can be told apart from one
taken against the previous binary without asking. md_add_dimensionno longer throws on a layers terrain. The slot refused a pack
terrain and then read the spec as native withstd::get, which throws for the third
alternative rather than falling through. A layers recipe reached that line and the
registration came back as a refusal with no reason attached, since the throw was caught
by the API guard. Both served kinds are read withget_ifnow, and the biome a layers
recipe names is verified at registration for the same reason the pack path verifies its
own:TemplateGeneratorreports a biome it cannot find as the registry having changed
underneath it, which is the wrong thing to tell someone who mistyped a name.- This host owns the dimensions it registers. The instance ledger held a
WeakRef,
and the engine does not keep anOwnerPtrhanded toDimensionRegistry::registerDimension
alive: the reference the call returned locked, the level listed the dimension, and the
weak reference was expired again as soon as the frame that built it returned. Every path
tha...
Pier 26.40.0
Built for BDS 1.26.40 and LeviLamina 26.40.0. A release is built against the one BDS its
number names, so 26.32.2 stays the build for BDS 1.26.32 and does not run on 1.26.40.
The engine changes are small and none of them reaches the ABI: PIER_ABI_VERSION stays
at 2 and a mod built against 26.32.2 keeps working without a rebuild.
Changed
- Custom dimension registration carries a pack id.
DimensionManager::_registerCustomDimensionWithDimensionDefinitionGrouptakes a third
argument in 26.40, the resource pack a definition came from, and
DimensionDefinitionGroup::DimensionDefinitiongained the matchingmPackId. A Pier
dimension comes from no pack and registers with a zero UUID. - The scoreboard packet became a payload packet.
SetScorePacketderives from
ll::PayloadPacket<SetScorePacketPayload>, itsmTypeis gone, and a row is one
alternative ofstd::variant<RemoveScore, ChangePlayerScore, ChangeEntityScore, ChangeFakePlayerScore>instead of aScorePacketInfowith an identity type field. The
sidebar buildsChangeFakePlayerScorerows, which is what the identity field said. Player::startSleepInBedtakessetsRespawnandsleepOffset, and the sleep hook
passes both through unchanged.DedicatedServer::runDedicatedServerLoopreturns::ServerExitCoderather than a
nested one and takes anEditorAllowList; the client-side-generation hook follows.DimensionDataPacket,LevelChunkPacketandSubChunkPacketare payload packets,
so their fields are inherited and$writehas two overloads. The chunk trace names the
one-argument overload explicitly.LevelChunkPacketno longer carries
mClientNeedsToRequestSubchunks: a setmClientRequestSubChunkLimitis the request,
and the trace reports it that way.NetworkChunkPublisher::moveRegionis gone, and every other entry point of that
class carrying a region sits behindLL_PLAT_C, so none of them is declared on a
server build. The send-region trace readsmLastChunkUpdatePositionand
mLastChunkUpdateRadiusfrom inside the queued-chunk hook instead, and reports the
region when it changes rather than on every call.MobEffectInstancehas no constructor of its own. Adding an effect builds the
instance empty and assigns the id, duration, amplifier and visibility, which is what
the removed two-argument constructor did.
Removed
PIER_BPROP_IS_DOORandPIER_BPROP_IS_STAIRare unsupported.
BlockType::isDoorBlockandisStairBlockare gone in 26.40 and nothing replaces
them; a block tag would be a guess at the name the engine files carry.PIER_APROP_IS_IN_LOVEis unsupported.Actor::isInLoveis gone in 26.40 and
nothing replaces it. ThemInLovePartnerfield survives, and reading it would be a
guess at what the accessor tested, so the property is reported as unsupported rather
than answered with a value that may be wrong.
Pier 26.32.2
Terrain packs replace the declarative layers and noise terrains, and the ABI moves
to v2. Nothing in this section had shipped in a tagged release, which is why the
removals below are removals and not deprecations.
Changed
- ABI v2.
PIER_ABI_VERSIONandPIER_ABI_MIN_SUPPORTEDare both 2. The four slots
retired in 26.20.3,md_add_simple_dimension,md_add_plot_dimension,
md_set_plot_gridandmd_clear_plot_grid, are deleted fromPierApi; every slot
after them moves, so a mod built against v1 is refused at load with the version range
in the message.tools/abi-v1.slotswas re-blessed for v2. - Three terrain kinds.
md_add_dimensionservesterrain:{kind:"native"}only, now
withgenerator:"flat"and"void"beside the three vanilla worlds and abiomefor
the void. Alayersornoiseterrain is refused with a pointer to the pack tool;
pier-pack from-layersconverts an old layers spec into a template source. - Dimension spec. The stored form of a pack terrain is
terrain:{kind, pack, sha256, params, roles}: the config path, the binary's hash at
registration, every bound parameter and the role overrides, so the terrain is
regenerable from the save alone and a changed binary is refused rather than mixed in.
Added
-
md_add_dimension_pack(name, config_path, spec_snbt). Registers a dimension from a
terrain pack: a directory with a config file and a binary. The host reads four keys of
the config (pier_terrain,type,binary,sha256), compares the spec's terrain
kind with the config's type and the binary's magic, hashes the binary against the
config, binds the parameters by kind (free, fixed, choice, derived), checks the pack's
constraints, and only then stores the spec. Refusals return one of thePIER_PACK_*
codes, all negative; a dimension id is never negative. -
md_pack_inspect(config_path, ctx, sink). What a pack asks for as one JSON
document, the same shapepier-pack inspectprints, so a mod builds its form from the
pack's own parameter list without a parser of the binary. -
Template packs (PIERTPL). A fixed range with parameters and cells whose data is
the same: zones along each axis with expression lengths, a block stack per combined
zone, constraints, and 3D structures as a CSG graph (box, cylinder, wedge, translate,
rotate, mirror, repeat, union, difference, intersect, shell, paint, choose by parameter
or cell hash, voxel blobs with keep and air cells) picked per cell with weights and
turns. A CONF section registers the cell grid for the confinement rules from the same
mount that produced the terrain. The plot world is the acceptance fixture and is
compared cell by cell with a line-for-line port of the 26.20.2PlotGenerator. -
Volume packs (PIERVOL). Java's density function graph: 31 operators including
splines, shifted noise, the caches, interpolation, end islands, the 1.18 blended
noise and the weird scaled sampler; the multi-noise biome parameter list; and the
surface rule subset (block, sequence, condition, biome, noise_threshold,
vertical_gradient, y_above, water, temperature, steep, not, hole,
above_preliminary_surface, stone_depth). A pack stores noise parameters, never tables:
the host builds the tables from the world seed with Java's Xoroshiro128++ or legacy
random source, so one pack serves any seed.pier-pack from-datapackassembles a
source from a Java datapack directory. -
tools/pier-pack.build,inspect,hash,from-layers,from-datapack, a
Python reference generator for each pack kind, and tests that compile the engine-free
pack layer under g++ and compare it with the reference cell by cell. -
Rust SDK.
dimensions::add_dimension_pack,dimensions::pack_inspect,PackStatus
andPackError;GeneratorType::spec_namefor the spelling a spec uses. The crate is
2.0.0, since the four removed functions are a breaking change. -
tools/migrate_dimension_config.pyno longer writes alayersterrain. A void or a
vanilla generator migrates in place as before; a pre-26.20.3 plot entry is left alone and
the three steps to rebuild it as a template pack are printed, since the terrain of a plot
world is a binary the host verifies by hash and cannot be written into the config file.
Removed
spec::Layers,spec::Noise,LayersGeneratorandNoiseGenerator, superseded by
the two pack kinds; the Rust SDK'sadd_simple,add_plot,set_plot_grid,
clear_plot_gridandPlotLayout, superseded byadd_dimension_packand
pack_inspect.
Known limits
- The liquid layer of a voxel blob is decoded but not yet written into the chunk's
second block layer, so waterlogged blocks in a blob come out dry. - A volume pack has no aquifers, ore veins or bandlands, and the biome of a column is the
one at its surface rather than a 3D field;temperatureuses the biome's base
temperature without the vanilla noise term;sea_levelof the pack is used for the
fluid fill but the dimension's own sea level stays at the sky's default.