Repository navigation
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 Rust example is
examples/hello-pier-rs, besidehello-pier-go,
hello-pier-zigandhello-pier-cpp. Its manifest name ishello-pier-rsand its entry
hello_pier_rs.dll; a server with the old example installed keeps ahello-pierfolder
that the new build does not replace. - The Rust mod template is
pier-mod-template, formerlypier-rs-mod-template. It holds
one Rust mod, to start from with GitHub's Use this template orcargo generate; the
documentation of the other languages starts from their examples. - Rust:
sys::PierMoneyEventis a transparenti32newtype withPIER_MONEY_*
constants. It was a Rustenumreceiving whatever the economy backend sent, which is
undefined behavior for an unlisted value, so it is the soundness exception of contract
§2.5 and changed at once.PierMoneyEvent::Set,Add,ReduceandTransstill compile,
as deprecated constants, andMoneyEvent::kindkeeps its type; the new
MoneyEvent::kind_checked()returns aMoneyEventKindwith anUnknown(i32)arm. What
still breaks: amatchon the old enum with no wildcard arm, which is now non-exhaustive,
andas i32on a value, which is.0now. - An event payload carries
"partial":1when a field could not be read, in
ExplosionEvent,FarmlandDecayEvent,SpawnItemActorEvent,ChestPairEventand
PistonPushEvent. The fields kept their defaults before, such assourceIsPlayer:0or an
emptyattachedlist, which read as answers. The field is absent when every read
succeeded, so a payload that was complete is unchanged. - Rust:
World::sleep_statusis anErrwhen a field is missing, instead of reporting
nobody asleep.EventPayload::check_completeis false for a payload that did not
parse, which it called complete, anddim()says that the payload could not be parsed. kvdb_is_emptyanswers false for a handle that is not open, like every other
kvdb_*slot, instead of reporting a closed database as empty.PlayerAttackTargetEventandPlayerInteractEntityEventcarrytargetKnown. When
the target cannot be read it is 0 andtargetIsPlayeris 1, so a PvP rule refuses. It
wastargetIsPlayer:0, which let the attack through.- Four slots that always failed are NULL:
edit_spawn_entity_nbt,level_find_path,
item_set_enchantsandclient_get_screen_name. A binding now reports them as not
provided instead of as a failed call.
Fixed
-
The documentation put Pier mods in
mods/. They live inplugins/, beside Pier
itself, where LeviLamina looks for every mod; a mod placed inmods/was never loaded.
/pier loadnow names the real path when a manifest is missing. -
The
typed-storagesurrogate runs clean against real LeviLamina headers. It reported
seven correct sites, through two faults of its own: a type writtenX constwas judged
by the wordconst, and any name held by several classes was skipped even when every
spelling got the same verdict, which left members such asmCauseunchecked. Names that a
class template or Pier itself declares as plain members are now ambiguous instead of
misattributed. It judges 10446 members, up from 9559, and Pier passes. -
Kill, hurt and death events name the attacker.
MobDieEvent,ActorHurtEventand
PlayerDieEventcarried only the damage cause. The source now hasattackerUidand
attacker, and for a projectileprojectileUidandprojectiletoo; the attacker of a
projectile is the actor that fired it, as LegacyScriptEngine and iListenAttentively
resolve it. Every described actor carriesuid, and a player alsoxuidand
realName. -
SpawnItemActorEventfires again. It hookedSpawner::spawnItem, while the level's
spawner is aBedrockSpawnerthat overrides that function, so the detour installed and
never ran.spawnMobandspawnProjectileare inherited, not overridden, so the hooks on
them were right and stay onSpawner. -
PlayerChangeGameModeEventhooksServerPlayer::setPlayerGameType, the override every
server-side player goes through, as iListenAttentively does. -
player_set_numworks. Level, experience, hunger, saturation and exhaustion always
failed; they are written through the attribute map, and a level throughaddLevels, the
way LegacyScriptEngine does on this engine version. -
A failed detour install is reported. The profiler, tick control and TPS statistics
recorded a failed install as done, soprofile_beginandtick_freezereturned true
while nothing was measured or frozen. -
An economy event of a type Pier does not know reaches no listener and is logged once,
and an exception from a listener counts as a veto instead of unwinding into LegacyMoney. -
Registering a supplied-terrain dimension no longer races chunk generation: the
terrain table is locked. -
A wrongly typed field in a dimension spec is reported.
"seed":"123"became seed 0
silently; it now leaves a warning naming the field. -
A volume pack whose palette passes 65536 entries no longer hangs the server in a loop
whose counter wrapped. -
Rust: deeply nested SNBT is an error and not a stack overflow. The parser stops at 512
levels; running out of stack aborted the process, past every panic fence. -
Rust:
Invocation::origin()keeps the name on aregistercommand. A plain name is
itself valid SNBT, so it parsed and came back empty. -
Rust:
Player::last_death_posreports an unreadable dimension instead of answering
the overworld. -
Rust: eight slot reads check the table length first.
hostanddimensionsread
fields such asgaming_statusandmd_pack_inspectstraight from the table, past its end
on a host older than the SDK. They go throughopt_slot!, which applies the gate. -
A dimension whose definition shape cannot be aligned is refused, as the line it logs
says entering it is not safe; it used to be registered anyway. -
/pier unload a bis refused instead of unloadingaand reporting success. -
The chunk trace summary counts transitions and loads; both counters were never
incremented and always read 0. -
send_message_typedlogs a type outside 0..11 once, before sending it as raw text. -
Documentation that disagreed with the code: the protocol-version and
md_is_available
comments ofabi.h, the key unregister and money listener slots, the MoreDimensions note of
native_dimensions.h, the file names and counts inMIGRATION.md, the mirror comments of
pier-sys-rs, and the README and installation pages, which still named LeviLamina 26.40.0. -
A bridge call with a reply over 2 KiB no longer runs the service twice on a host that
has the sink form. The buffer form fetched a long reply by calling again, and a service
that changes state did so twice. -
A service provider can no longer be unloaded in the middle of a call from another
thread. Service calls were not counted as callbacks in flight, so an unload on the
server thread could unmap a provider still running on a bridge caller's or a worker's
thread. Bus, packet and connection dispatch from other threads now recheck after being
counted, and the counter and the unloading flag use sequentially consistent ordering:
with release and acquire, x64 may reorder each side's store with its following load and
both sides miss each other. -
An exception out of
pier_main,on_enable,on_disableoron_unloadis caught and
reported as a refusal instead of unwinding through the loader. -
The vtable handshake keeps the append-only promise. The host required
struct_size
to be at least its ownsizeof(PierModVTable), so appending a lifecycle callback would
have refused every mod already built, and a mod built after such an append would have
written past the host's struct.pier_mainnow writes into 512 zeroed bytes, the host
copies the prefix it knows, and anystruct_sizethat coverson_unloadis accepted. -
Nine
PierApislots declared with()now say(void). In C,()declares no
prototype, which-Wstrict-prototypeswarns about and which a C translator such as
Zig's turns into a variadic function. The calling convention is unchanged. -
examples/hello-bridgebuilds against LeviLamina 26.51.5, the version the host is
built against, instead of 26.40.0. -
The C++ example and pages stop exceptions at every callback, check the host's ABI
version and target before filling the vtable, and include<cstddef>foroffsetof. -
A block container query no longer logs "Access violation - no RTTI data!" for a block
entity that carries no RTTI.resolveContainerreached a block's container through
dynamic_cast<VanillaBlockActor*>, which throwsstd::__non_rtti_objecton such an object
instead of yielding null; the API guard caught it and logged it on every call. The cast now
answers "no container" for it, as it does for any block entity that is not a container. -
The server no longer crashes the first time anyone uses a nether or end portal. The
detour onDimensionRegistry::registerDimensiondeclared its id asDimensionIdType. The
SDK givesNewTypea hand-written copy constructor, so the detour expected the id behind a
pointer, while the engine passes its trivially copyableNewType<ushort>as the value
itself. Building the nether or the end on first use handed the detour 1 or 2, which it read
as an address. The detour now takes theushortthe engine passes and is installed by
address; the host's own call that registers a custom dimension passes the value the same way. -
The money guard no longer asks LeviLamina's symbol resolver for
LLMoney_Get. With
LeviLamina 26.51 and LegacyMoney 0.22 installed and enabled, that lookup came back empty,
everymoney::*entry point stayed inert for the session and the log said the export
could not be resolved. The guard now asks the Windows loader aboutLegacyMoney.dll, the
module the delay-load helper binds to, and checks everyLLMoneyfunction Pier imports
by the name it imports it under. The answer is the one the first real call would get.
When something is missing the warning names it and lists theLLMoneyexports the DLL
does have, so a version mismatch can be read off one line. The export table is read by
pier/api/pe_exports.h, which has a test intools/pe-exports/.