Skip to content

Version / API version / macro matrix

This library exposes three numbering schemes; consumers use whichever they need:

Number Where Purpose
Plugin release version version in manifest.json For deployment; has the same name as the git tag (e.g. 26.40.2), shaped <BDS major>.<minor>.<patch>
API version the "API version" line of README.md The human-readable capability version (e.g. 1.21.0)
HOLOGLIB_API_VERSION include/hologramlib/HologramLib.h The only version source in code: compile-time static assertion + IHologramLib::version() runtime handshake

IHologramLib::version() returns HOLOGLIB_API_VERSION itself, so the runtime handshake and the compile-time assertion read the same value.

Encoding rule

The format is 0x01 + the minor version (the middle byte, incremented in hexadecimal) + the patch digit:

1.15.0 -> 0x011500
1.19.0 -> 0x011900
1.20.0 -> 0x011A00   ← after 0x19 it advances in hexadecimal to 0x1A, i.e. 1.20
1.21.0 -> 0x011B00

In other words, each minor version adds +0x1 to the middle byte (it shows up as 0x19 → 0x1A → 0x1B); do not read those two hexadecimal characters as decimal.

The patch digit is usually 00, so 1.19.1 and 1.19.0 share 0x011900 — you can only gate on the minor version. Early releases used the last digit to distinguish patches (0x010701 = 1.7.1).

This macro is only pushed up when a new version is actually released. Continuing to add capability domains on a line that has not been released does not change it: the "lowest version I can use" the consumer sees has not changed, and raising the macro would only make old consumers misjudge it as incompatible. 0x012300 shipped with the official release of 26.40.10 (view override gained the AddItemActor spawn packet: retyping/hiding a dropped item is done in one step from then on), and on the 26.51 line it ships together with 26.51.3 (the same batch of capability domains, only the protocol line differs); before that 0x012100 shipped with 26.40.9 (view override EntityView::skinId same-id model swap + real entity interaction multicast + ABI layout stamp); 0x012000 shipped with 26.40.8 (view override debris cleanup + five methods appended to IPlayerNpc + holographic-text/text domain shrink and completion + view* per-field exports); 0x011F00 shipped with 26.40.7 (client view override IViewOverride) — before that it was 26.40.5's 0x011D00 (fake inventory + sulfur-cube display).

One exception (on the record): 26.40.3 released the 1.21.0 trade-menu click callbacks, and they were retracted on request right after publication — the trade menu became display-only, and TradeClickEvent / TradeActionCallback / TradeRawAction with ITradeMenu's six listener methods were removed wholesale. This is a shrink, not an addition, and by this document's convention it should have been a major version (2.0.0); it was bumped to the minor version 1.22.0 and written out in the table instead, because that API's public window was a single release and no consumer adopted it. If you already wrote trade-menu click listeners against 1.21.0, upgrading to 26.40.4 requires deleting those calls (it is a compile-time error, not a silent behaviour change).

The second shrink (on the record): 26.40.8 removed IHologramText's per-line fake functionality wholesale (8 functions + the in-library self-driven holoTick; see the 26.40.8 row in the table below). These functions had been exported since the early domains of 26.10.4, but there was never a corresponding protocol capability underneath: protocol 2168's text payload is one whole block with a single colour/scale, there is no line-spacing field, and rainbow mode only painted the lines white; nothing inside or outside the library consumed the animation offset holoTick drove. Given this library's positioning of "basic wheels, no fake functionality", keeping them was worse than removing them — consumers would think they had per-line capability. As with 26.40.3: the shrink does not get a major version of its own; it ships with the minor version 1.26.0 (> the previous release 1.25.0) and is written out in the table. ABI handling (the key difference from 26.40.3): the six listener methods deleted back then were at the tail of the interface and did not move existing slots; this time the per-line methods sit in the middle of IHologramText, and deleting them outright would shift setColor / setLocation / draw and everything after them forward — consumer binaries compiled before 1.26.0 and not recompiled would silently misalign. So these 8 methods are kept as deprecated empty slots in the C++ interface (non-pure + inline empty body + [[deprecated]]; calls do nothing / return false), and the vtable slots are exactly the same as in 1.25.0; a recompiled consumer calling them the old way gets a compile-time deprecation warning (not a silent behaviour change); on the LSE side the exports are deleted as before. The real slot cleanup waits for the next major version (2.0.0).

Also noted: 0x012200 (= API 1.28.0) never appeared as a released version — the number was skipped, no release ever used it, and there is no 1.28.0 row in the table below.

26.51.2 was retracted: that release was retracted after publication; deploy 26.51.3 directly (the tag 26.51.2 is kept as a historical anchor).

Full matrix

Counting basis (the conclusion of a check during the 2026-10 documentation pass): the "N in total" at the end of each row in the table is the record as of that release, and it contains one accounting drift — the 26.40.8 row records view* as 6→16, whereas the real value in code is 6→17; that undercount of 1 also made the total recorded then 268 (it should actually be 269). 26.40.10 then added viewItemSpawnHooked, bringing the 26.51.3 snapshot to 270 exportAs calls; 26.51.4 adds 8 more (itemDisplay* 33→40, view* 18→19). Current count: 273 (26.51.4) (the two ghost* entries are now wired into the registration flow). The historical numbers in the table are kept as in the original and not corrected.

Three further known historical inconsistencies (likewise kept as in the original and only registered here): ① 26.10.6's API version does not match — this table records 1.15.0 (0x011500), while that entry in the changelog writes 1.17.1; and 1.17.1 does not appear in any row of this table. There is no way to establish which is right (the tag from back then is missing), so consumers should not gate on the release number 26.10.6. ② The "cannot do" in the 26.40.7 row has been partially overturned by later releases — that row asserted "26.40 cannot do per-player scale / glowing / invisibility (the metadata table has no scale)", but the 26.40.10 row then added EntityView::metaScale through Reserved38 + Reserved53·54. The old entry carries no annotation, so when reading that 26.40.7 passage, read the 26.40.10 one with it. ③ Suspected double-counting — the changelog entry for 26.40.1 (1.20.0) says "new ghost interaction multicast listeners (1.19.1)", while the 26.10.7 entry on the same page already attributes that feature to 1.19.0.

Plugin release version API version HOLOGLIB_API_VERSION Capability domains added in that release
26.51.6 1.31.0 0x012500 Internal refactor and fixes; API and vtable unchanged: packet path (new core/PacketSink.h), tick driver (core/TickDriver.h, five Level::$tick detours → one), domain lifecycle (core/ServiceHost.h, which also wires up the shutdown() implementations that ContainerMenu / NpcDialogue / PlayerSensing / FakeInventory never had called), ABI contract tooling and baseline (abi/v1.31.0.json, abi/slots.md, tools/abi_tool.py; CI blocks mid-class slot insertion). Fix: trade planConsumption consumed one balance twice for two same-type costs (buyA / buyB of the same item) — now merged before consuming, with 64-bit demand accumulation. Build uses a src/**.cpp glob and GhostInteractRouter.cpp is a standalone translation unit again. Engineering: tests/ under version control, build workflow (static checks always, /W4+/WX compile optional), .clang-format / .clang-tidy / .editorconfig, exception semantics fixed (/EHa next to "exceptions disabled" produced C4530 undefined behaviour), the holographic-text manager uses a recursive mutex (the packet hook calls back on the same thread).
26.51.5 1.31.0 0x012500 The 26.51 line merges 26.40.12: non-uniform stretch, live transforms, animation preset extensions and single-channel presets, stop recipe, NPC pose/equipment/ride, FMBE spawn-twitch fix, image plane with transparent mode, container session isolation (uiOpen / ownerEpoch / id reuse guard), trusted PlayerSkin and skin Id rebuild. Protocol 2193; shape text carries LineGapHeight. Tail-only vtable appends; HOLOGLIB_ABI_STAMP unchanged.
26.51.4 1.30.0 0x012400 Three primitives/notifications only, no feature logic: viewPollSpawnEvents (spawn notification) · itemDisplayCreateFull (create with NBT/glint) · itemDisplayEmitForEntity (render an existing entity as an FMBE model for one player). All decisions live in the consumer (ItemPhys). LSE exports only, vtable untouched → HOLOGLIB_ABI_STAMP unchanged.
26.51.3 1.29.0 0x012300 The merged release of the 26.51 line: the three batches 26.40.8 / 26.40.9 / 26.40.10 from the 26.40 line moved to the 26.51 line wholesale (the API goes straight from 26.51.2's 1.25.0 to 1.29.0; 1.28.0 is skipped per the 26.40 line's convention). The capability-domain content matches the 26.40.10 row below; the only difference is the protocol line. The only newly written adaptation on the 26.51 side this time is one: in 26.51, PlayerAuthInputPacketPayload::mInputData changed from std::bitset<66> to Bedrock::EnumSet<InputData, 66> (no operator[]), so view override's input-bit mask uses contains() bit by bit; the four adaptations of 26.51.0 (text LineGapHeight, ItemInstance::EMPTY_ITEM(), container response presence=false, shape NetworkId seed 0x00FFFFFF) were re-checked item by item after the merge and are all still there. Protocol constant check: sculk/protocol/codec/actor/ActorDataIDs.hpp and sculk/protocol/codec/MinecraftPacketIds.hpp are byte-identical across the two protocol libraries 26.40 / 26.51, and PrimitiveShapes.hpp only adds mLineGapHeight (already sent by kTextLineGapHeight). Build has 0 warnings; offline checks all green (view* 59 items / trade settlement 43 items / container 42 items / NPC carrier 32 items / PlayerList and trade-offers byte-compared). Not retested on a 26.51 client: the on-device effect of entity metadata and AddItemActor (those conclusions come from a 26.40 packet capture).
26.51.2 1.25.0 0x011F00 New client view override IViewOverride (protocol-layer interception). Two disciplines: suppress-only — the outbound hook sits on NetworkSystem::send / sendToMultiple and only decides "pass / drop", never modifying engine packet fields; wherever a change is needed it always drops the original packet + the library hand-writes its own protocol packet to resend (sculk construction → read-back check → raw bytes sent). The only data source is protocol packets (inbound PlayerAuthInput's position/rotation/input bits, spawn-packet payloads, Animate·ActorEvent); server-side entity state is not read (the exception: identity resolution). What it can do: change a mob's type, turn a player into a mob = replacement (eats AddPlayer and sends an entity of that type with the same runtimeId/uniqueId; MovePlayer is eaten for that viewer and the position is pushed by MoveActorAbsolute → the server still sees them as a real player, and hitting them hits the player themselves), reskin a player (resend PlayerList, revocable immediately), swap the nametag, hide, change a block's appearance; plus a per-tick heartbeat (re-pushing overrides when an entity re-enters view / a player changes chunk). Not in the library: "features that read engine state" such as the inventory mirror (consumers compose those themselves from the container domain's protocol capabilities). LSE exports view* (6 functions). The decision logic is isolated in src/view/ViewOverrideLogic.h, testable offline (tests/check-view-override.bat, 54 items). Not done: per-player scale/glowing/invisibility (those items are not in the metadata table measured on the 26.40 line; the 26.51 line was not retested).
26.51.1 1.24.0 0x011E00 Trade menu gains "protocol-layer-only real settlement": TradeMenuSpec::settleLocally + ITradeMenu::setSettleLocally / isSettleLocally / settlement callbacks (TradeSettlementEvent listener + tradePollSettlements). The interface is still a self-built UpdateTrade (the server sends no trade table), but the library catches the client's payment placement / retrieval / completion requests and really deducts the payment from the player's inventory and writes the product into it (closing the interface refunds the escrowed payment); the cursor ("click once to pick up / click again to place in the slot") is managed by the library. The decision logic is isolated in src/trade/TradeSettlementLogic.h, testable offline (tests/check-trade-settlement.bat, 43 items). Known limitation: the "trade slot slot correction" the client asks for is inconsistent between this protocol version and the protocol library's wire format (hand-writing it crashes the client), so the client may retract the payment it placed — for reliable settlement use usePacketOffers=false (the real trade table) or a container UI. (This feature was originally developed on the 26.40 line and shipped together with the 26.51.0 port)
26.51.0 1.23.0 0x011D00 Version port only, no new capability domain: adapt to LeviLamina 26.51 / BDS 1.26.51 (protocol 2193) — shape text sends the newly added LineGapHeight (not sending it misaligns), the trade menu's empty-slot entry uses ItemInstance::EMPTY_ITEM() instead (in this version the default constructor is only exported on the client platform), and the virtual container's success response switches to presence=false encoding (containers became optional)
26.40.10 1.29.0 0x012300 View override caught up on two things at once (the C++ interface only does tail appends, vtable slots unchanged; but EntityView changed size → consumers that construct that struct directly must recompile). ① AddItemActor(id 15) is now intercepted too: a dropped item's (item entity's) spawn packet goes through AddItemActor rather than AddActor, and previously only the latter was intercepted → "change a dropped item's type / hide it" was only half done — at the moment the override was set it could swap out the players visible then, but the engine's own AddItemActor right afterwards pushed the item icon back into the client (2026-10-16 capture: after RemoveActor + AddActor(minecraft:cod) an AddItemActor with the same uniqueId arrives 13 ms later). Now hidden drops the packet, and identifier drops the original packet + hand-writes an AddActor with the same runtimeId/uniqueId (attributes use the verified recipe of health 20/scale 1, not copying ItemActor's maxHealth = 0); the decision was extracted into ViewOverrideLogic::planItemSpawn (Pass/Drop/Rewrite), offline checks 54 → 59 items; new LSE export viewItemSpawnHooked(). ② EntityView gained pose / scale / variant / colour (tail append): pose · metaScale · variant · markVariant · colorIndex · color2 (each with hasXxx, false = leave that item alone); the LSE viewEntity spec gained pose= / scale= / variant= / mark= / color= / color2=. Protocol items and types: PoseIndex(79) · Reserved38 + Reserved53·54 (scale and the client hitbox) · Variant(2)=Int · MarkVariant(43)=Int · ColorIndex(3)=Byte · Color2Index(82)=Byte — the types come from the decode engine's own real spawn packets (a real sheep: id2(type2) id43(type2) id3(type0) id82(type0)). Consumers: FishDropEntity (a standalone LSE plugin, moved out of Meow's container on 2026-10-16) (every tropical-fish drop gets a random vanilla look, pufferfish always fully puffed). Second fix in the same release: SetActorDataPacket / UpdateAttributesPacket both carry mTick and the view override domain never filled it (=0) → the client drops them as stale packets (the metadata in the spawn packet is all correct, yet nothing changes on screen; the attribute second confirmation fails the same way). Now these two packets uniformly fill currentViewTick(), a SetActorData follows immediately after the spawn packet, and a +2 tick confirmation is scheduled (scheduleMetaPush/flushMetaPushes).
26.40.9 1.27.0 0x012100 EntityView::skinId (view override: swap a non-player entity for "our model") — the intercepted spawn packet + the library's own packets as the replacement, the same runtimeId/uniqueId: the library eats that entity's spawn packet and sends PlayerList(Add)+AddPlayer (player model + registered skin, optionally with scale). The client sees our model, while the id is unchanged — the server still knows it is that mob, and clicking it / hitting it / aiming at it all hit the mob itself (damage, drops and what other plugins see are all the real thing). Supporting work: that entity's MoveActor*(18/111) is eaten for those viewers, and position/rotation is pushed every tick by the heartbeat with MoveActorAbsolute (PlayerNpc's light-push channel); the PlayerList entry is removed after ~20 ticks (it does not enter the Tab list, the entity is kept); revoke = clearEntity resending the real spawn packet. Skins come from the IPlayerNpc registry (registerSkin / captureSkin / registerSkinFromBlob). This is the proper route for "give a mob a custom model" (the PlayerNpc carrier approach needs an invisible real entity + per-tick following + ghost-forwarded attacks; this one needs none of that). Consumer: MSkinventory's "same-id reskin" (config.mobRenderMode=replace, the default). Real entity interaction events: addActorInteractListener / removeActorInteractListener (tail appends; multicast; reusing the existing inbound-packet hook, no new detour layer); ABI layout stamp (vtable layout descriptor + consumer-side compile-time self-check; the library intercepts layout-mismatched consumers at runtime — a mechanical guard against the two mid-class slot-move startup crashes); the LSE viewEntity spec gained skin=<skin id>. Appended inside the batch (unreleased) · shrink (on the record): the dialogue's avatarViewSpec skin= branch is removed — it swapped the carrier for a "player model" (PlayerList(Add)+AddPlayer), and in testing the client closed the interface when clicking into the menu on that carrier (in the same test a type= carrier: zombie/chicken, normal); following the "the library only does mechanisms" positioning, that route is left for consumers to build themselves (playerNpcs() + npcUniqueIdOverride). Kept: type= / name= and avatarSkinVariant; the two fields themselves do not change (ABI unchanged), and a piece of dead code that never took effect, allocating player-model carrier ids, was removed along the way.

ABI incident (this time, on the record): the first version of addActorInteractListener / removeActorInteractListener was inserted into the middle of IHologramLib (after the ghost section) — every later vtable slot shifted forward, and a consumer compiled against the old header (MeowHolographicRenderer fetched the wrong slot for playerNpcs()) crashed immediately with 0xC0000005. They were moved to the end of the class and a ⚠ note was left in the header. The lesson: "append only at the tail" is not politeness, it is ABI — a mid-class insert = every pre-compiled consumer silently misaligns. | 26.40.8 | 1.26.0 | 0x012000 | View override old-world debris cleanup (behaviour unchanged; "suppress-only" has been so since 26.40.7 — rewrote the stale "rewrite the original packet" header comment, deleted the in-place-rewrite dead code (dataListOf / setItemById / cloneItems / restoreItems), made the whole decision path const; a type change/nametag/hide/block override always drops the original packet + the library hand-writes its own protocol packet); the view* LSE exports were completed field by field (6→16 functions; a unique-id decision fix — a player's id is itself negative). Five methods appended at the tail of IPlayerNpc (setPositionLight lightweight position update, the same channel as setRotationLight; injectSkin / injectSkinAll player skin injection, sending PlayerList(Add) with the target's own UUID, including "see yourself"; playAnimation / playAnimationTo triggering animations by name + setEntitySpawnCallback; the existing ABI is unchanged). Holographic-text domain shrink (see "the second shrink" above): removes the 8 functions for per-line scale/colour/gradient/rainbow/scroll/vertical animation/line spacing and the in-library self-driven holoTick. Text capability completion: holo* gained setScale / setBackgroundColor / clearBackgroundColor / setDepthTest / setRotation / clearRotation (background-box colour / see-through toggle / three-axis Euler degree rotation); shape* gained shapeSetBackgroundColor / shapeClearBackgroundColor / shapeSetDepthTest (text shapes only). Fixes: setRotation now sends UseRotation + the double-sided rendering bit (previously text rotation was silently ineffective); destroyBatch / destroyAll send the removal packet (previously they only cleared memory); colour packing fix: int32 colours are packed as v2168's ARGB (previously the v944 byte order was used → R/B swapped, filling in red showed blue; shapes/text colour/background box are all affected, packing and unpacking are corrected together); view override (two fixes proven by Disguise + one piece of wiring): asPlayer missing from the null check ("disguise as some player" was treated as a revocation and reported "无法伪装" ("cannot disguise")); the override no longer applies to "self" (the player themselves is neither sent nor eats packets about their own entity — previously turning a player into a mob froze their own client; the reskin family is the exception: it includes the player themselves, so the self-view changes too); the replaced player's movement push was added (MoveActorAbsolute; previously dead code, so the stand-in stood still). Dynamic lines return (refactored from Phantom (github.com/GroupMountain/Phantom), LGPL-3.0 — the origin is credited in code/README as its licence requires; that portion is distributed under LGPL-3.0, the rest of the library remains MIT): IHologramText gained tail appends setLinePool (content pool rotated by time modulo, stateless) / setLineParseVariables (per-line variable toggle), with a 0.5 s throttle refresh inside the library and a resend only when the content changed; and fixed that a per-viewer (with {var}) text refresh only changed memory without sending a packet (previously an update only reached the client with the 15 s safety-net resend); the variable set gained {dimension} / {x} / {y} / {z}. LSE exports holo* 26→24→26 (after the shrink the dynamic-line setLinePool / setLineParseVariables were added back) / shape* 36→39 / playerNpc* 25→29 (added playerNpcInjectSkin(All) skin injection and the playerNpcGetSkinBlobB64 / playerNpcRegisterSkinFromBlobB64 skin blob persistence pair — the script-side proper route for "player sees their own reskin"); NPC dialogue avatar customisation: two parameters appended at the tail of NpcDialogSpec — avatarSkinVariant (the NPC's built-in skin variant 0..59 → the carrier's ActorData SkinId(104); -1 = do not add the item) / avatarViewSpec (layering IViewOverride semantics on the carrier: change the type/change the title), LSE gained npcDialogSetAvatar (in-place reopen, 0 flicker) — npcDialog* 8→9, 268 in total | | 26.40.7 | 1.25.0 | 0x011F00 | New client view override IViewOverride (protocol-layer interception). Two disciplines: suppress-only — the outbound hook sits on NetworkSystem::send / sendToMultiple and only decides "pass / drop", never modifying engine packet fields; wherever a change is needed it always drops the original packet + the library hand-writes its own protocol packet to resend (sculk construction → read-back check → raw bytes sent). The only data source is protocol packets (inbound PlayerAuthInput's position/rotation/input bits, spawn-packet payloads, Animate·ActorEvent); server-side entity state is not read (the exception: identity resolution). What it can do: change a mob's type, turn a player into a mob = replacement (eats AddPlayer and sends an entity of that type with the same runtimeId/uniqueId; MovePlayer is eaten for that viewer and the position is pushed by MoveActorAbsolute → the server still sees them as a real player, and hitting them hits the player themselves), reskin a player (resend PlayerList, revocable immediately), swap the nametag, hide, change a block's appearance; plus a per-tick heartbeat (re-pushing overrides when an entity re-enters view / a player changes chunk). Not in the library: "features that read engine state" such as the inventory mirror (consumers compose those themselves from the container domain's protocol capabilities). LSE exports view* (6 functions). The decision logic is isolated in src/view/ViewOverrideLogic.h, testable offline (tests/check-view-override.bat, 54 items). Not done: per-player scale/glowing/invisibility (those items are not in the 26.40 metadata table).| | 26.40.6 | 1.24.0 | 0x011E00 | Trade menu gains "protocol-layer-only real settlement": TradeMenuSpec::settleLocally + ITradeMenu::setSettleLocally / isSettleLocally / settlement callbacks (TradeSettlementEvent listener + tradePollSettlements). The interface is still a self-built UpdateTrade (the server sends no trade table), but the library catches the client's payment placement / retrieval / completion requests and really deducts the payment from the player's inventory and writes the product into it (closing the interface refunds the escrowed payment); the cursor ("click once to pick up / click again to place in the slot") is managed by the library. The decision logic is isolated in src/trade/TradeSettlementLogic.h, testable offline (tests/check-trade-settlement.bat, 43 items). Known limitation: the "trade slot slot correction" the client asks for is inconsistent between this protocol version and the protocol library's wire format (hand-writing it crashes the client), so the client may retract the payment it placed — for reliable settlement use usePacketOffers=false (the real trade table) or a container UI | | 26.40.5 | 1.23.0 | 0x011D00 | New IFakeInventory (fake inventory: protocol-layer rewrite of the player inventory content the client sees, clicking a forged item reports back the slot number, coexists with the trade menu/virtual container) and ISulfurDisplay (sulfur-cube display: the block "swallowed" in the main hand + invisible on by default + appearance-tier property), both with LSE exports; the virtual container gained an interactive mode (dragging/swapping items inside the container is caught and kept by the library); ICustomEntity::setMobProperty opened up for entity property sync | | 26.40.4 | 1.22.0 | 0x011C00 | The trade menu became display-only (no effective listener methods for now), new ITradeMenu::addOffer / setTier (in-place trade-table resend); four new domains completed with LSE exports (trade* / container* / npcDialog* / sensing*; container and dialogue clicks go through a polling queue), entity* gained per-client rendering exports (entitySetPlayerNametag / entitySetPlayerScale / entitySetPlayerEquipmentSlot / entityClearPlayerAppearance)| | 26.40.3 | 1.21.0 | 0x011B00 | New ITradeMenu (villager trade menu: protocol-layer trade interface), INpcDialogue (NPC dialogue interface: scene/body/button JSON, click and close callbacks, synthetic NPC carrier), IContainerMenu (virtual container/list: replicating GMLIB ChestUI — a client-side chest block + block entity NBT + a ContainerOpen bound to the block coordinates; a small container with 27 slots, a large one with 54 paired; click reports the slot number) and IPlayerSensing (sensing: per-packet capture of the client device from AuthInput InputMode); ICustomEntity / IHologramText gained per-client rendering (per-viewer overrides of nametag/scale/equipment; text containing {var} resolved per viewer); per-client sounds and short-lived floating text removed | | 26.40.2 | 1.20.0 | 0x011A00 | Fix NPC skins not rendering (Id/FullId completion, PlayerList 2168 frame format)| | 26.40.1 | 1.20.0 | 0x011A00 | Adapt to LeviLamina 26.40 / BDS 1.26.40 (protocol 2168); per-client rotation (setPlayerRotation and the like)| | 26.10.7 | 1.19.0 | 0x011900 | Skin export from memory / directory import, geometryData custom models, NPC scaling; 1.19.1 adds ghost interaction multicast listeners | | 26.10.6 | 1.15.0 | 0x011500 | Fix permanent storage of captured skins | | 26.10.5 | 1.15.0 | 0x011500 | IParticleShape (particle shapes) domain | | 26.10.4 / 26.10.3 | 1.9.0 | 0x010900 | Early domains (IShapeDrawer / IHologramText / IItemDetail etc.)| | 26.10.1 | 1.7.1 | 0x010701 | The first version |

The READMEs of the early tags have no "API version" line; the table above is inferred from the macro and changelog of the time. To pin down an exact tag, use git show <tag>:include/hologramlib/HologramLib.h | grep HOLOGLIB_API_VERSION.

How consumers use it

#include <hologramlib/HologramLib.h>

// 1) Compile time: require a capability domain to exist
static_assert(HOLOGLIB_API_VERSION >= 0x011B00, "requires 1.21.0 (ITradeMenu)");

// 2) Runtime: handshake on plugin enable (the library may have been replaced by an older build)
auto const ver = hologramlib::IHologramLib::getInstance().version();
if (ver < 0x011B00) {
    logger.error("HologramLib too old: 0x{:06X}, need >= 0x011B00", ver);
    return false;
}

Convention for appending a new domain

IHologramLib: new capabilities are always appended as virtual functions at the tail of the class, existing methods are not modified or reordered (so the existing methods' ABI stays unchanged), and HOLOGLIB_API_VERSION is incremented. Each new domain is recorded on one line in the README changelog and registered in this table.