Releases: matthewperiut/RetroAPI
Release list
0.4.1
StationAPI mods now get the same automatic treatment RetroAPI mods do.
Creative tabs
- Auto tabs now include mods that register through StationAPI, not just RetroAPI. A mod that has never heard of RetroAPI gets a tab named after itself.
- Spawn eggs are no longer duplicated into the owning mod's tab, since they already sit in the Spawn Eggs tab. Eggs from other mods are left where they are.
Spawn eggs
- Automatic eggs now work under StationAPI. The sweep reads vanilla's EntityRegistry, which both toolchains populate. Living entities only.
- The fallback egg is tinted from a saturation weighted average of the mob's own render texture, so eggs are distinguishable before anyone draws one. Drop a sprite at assets//textures/item/_spawn_egg.png to replace it.
Opting out without a dependency
- com.periut.retroapi.compat.RetroOptional takes only Strings, so a mod can call excludeItemGroup, excludeSpawnEggs and excludeSpawnEgg reflectively with no hard dependency on RetroAPI.
- templates/RetroCompat.java.txt is a copy in helper that wraps the mod check and the reflection, and does nothing when RetroAPI is absent.
Fixes
- The handshake length widener uses ModifyArg instead of ModifyConstant. Only one injector may claim a constant, so RetroTweaks and Glass Networking previously fought over it and whichever lost crashed on startup. Both now apply.
0.4.0
Commands (Brigadier)
- Full Brigadier port bundled in-tree, with argument types, suggestions and command-tree sync to clients
- Built-in commands: /give, /setblock, /gamemode, /gamerule, /fly, /help and others
- Preloaded suggestions so completion is instant on servers rather than a round trip
- Text component system (retroapi.text) with translation resolution
- Command blocks: all three types, full editor with text editing and suggestions, chunk-loaded execution
Game modes
- Creative, Survival, Adventure, Spectator, with per-life persistent flight
- Modern creative screen: tabs, search, player render, page buttons, click semantics (1 / shift-64 / 1–9 hotbar), bin slot
- Creative instant break, break cadence, no durability loss, no item consumption, mobs ignore you
- Middle-click block picker returning obtainable item forms
- /fly independent of game mode, with fall-damage immunity
Config API (com.periut.retroapi.config)
- Annotation-driven configs any mod registers in one call; per-mod JSON, screen, defaults
- Options screen button renamed Configs..., with a Mods page listing every registered config
- Server sync for Scope.WORLD options, operator editing with red asterisks and a footer note
Content / registration
- Spawn eggs for all vanilla mobs, plus automatic spawn eggs for modded entities
- Automatic creative tab for mods that register content but no tab
- Achievement pages, with hidden pages and a viewer; StationAPI page mirroring
- RetroData.chunk(...) for arbitrary per-chunk sidecar data
- Tags resolving through both RetroAPI's and StationAPI's registries
RetroTweaks merged in (com.periut.retrotweaks, ~290 classes)
- The whole tweaks/fixes/QoL set HUD, inventory, blocks, entities, rendering, auth/skins, scoring
- Now breaks the standalone mod, and its config is the config API's first consumer
Packaging
- OSL modules bundled in the Ornithe jar too (previously babric only)
- StationAPI compat nested and loaded only when StationAPI is present
RetroAPI 0.3.9
Two StationAPI fixes.
- Breaking a disguised block no longer overflows the stack.
- Blocks that register their own BlockItem subclass keep it, so their placement rules work again.
RetroAPI 0.3.8
Minor bug fixes.
- Fixed a server-side crash.
- Networking patches.
- Custom world height now reaches the game's own block API, and blocks can have water flow around them.
- Block items can carry an icon of their own, and the break sound the widened block id was eating is back.
- Break sounds and debris now read the position before the record, so they play at the right place.
- Breaking speed works under StationAPI through its own hook, and the disguise path no longer relies on optional injections.
- Dropped item scale works under StationAPI.
0.3.7
Four releases' worth of work going out as one, because none of it shipped: 0.3.6 was the last tag and everything here has been sitting on main since.
World generation
world.noise is the modern noise stack ported whole, taking a firstOctave and an amplitude per octave rather than beta's fixed halving spectrum, and summing two Perlins at incommensurable scales so the lattice artifacts cancel. RetroCubicBiomes stores biomes on a sparse 16x16x16 grid with the world, which is what beta's getBiome(temperature, rainfall) cannot express: a cavern under a desert is not a property of the desert, and one a player has walked through has an identity that has to survive being unloaded. RetroCarvers makes carving registrable and vanilla carving switchable, running against the chunk's raw byte[] before lighting or decoration exists. RetroWorldHeight extends a dimension vertically, data layer only.
A launcher that works
starac was declared modRuntimeOnly, and maven-publish exports that into the POM at runtime scope, so every mod depending on RetroAPI silently got it on the runtime classpath. Its MinecraftMixin redirects a noCanvas target that does not exist on Fabric's applet path, and a redirect that finds nothing is a hard failure, so runClient in a consumer's project died before drawing a frame in a mixin belonging to a mod they never asked for. It is replaced by retrodragon 0.1.10.
Blocks that are not cubes
Beta decides a lot of things once, for a whole block type, and then applies the answer to a piece of one.
Smooth lighting is four brightness values, one per corner of the whole block face, handed to face methods that draw at whatever bounding box is current. A full cube occupies those same corners. A stair's step spans half the block and gets the whole gradient squeezed into half the distance, and since a stair is two boxes in two passes, the halves disagree where they meet. Those four numbers describe a bilinear field, so it is now sampled at the corners the quad actually covers, which is what modern Minecraft does.
Texture coordinates had the same problem with a twist. Two of the six faces emit their horizontal coordinate reversed, which on a full cube only mirrors the tile and is old enough to count as intended, but on a partial box also takes the wrong half. The mirroring is kept and the sub-rectangle it implies is used. Both corrections reproduce vanilla's own numbers for a full face, epsilon included, so ordinary blocks do not move.
droppedItemScale covers beta doubling the dropped size of anything that is not a full cube, a rule about torches and flowers that lands a stair at twice the size of everything else in the pile. setFlatItem covers the other end: beta draws its own door and pane as a flat sprite because a three pixel panel is unreadable at inventory size, and custom render types had no way to say so.
A block can present as another block
RetroBlockDisguise answers, per position, three questions beta only answers per block type: which tool works (mineableTools takes a block, no coordinates), what the particles look like (BlockParticle reads the block's static sprite), and what it sounds like (soundGroup is a field).
public class FramedBlock extends Block implements RetroBlockDisguise {
public Block disguisedBlock(BlockView world, int x, int y, int z) { return whatItWears(world, x, y, z); }
}Tools are additive on purpose: a wooden frame wearing stone answers to an axe and a pickaxe, because subtracting would mean a block got harder to break the more it had been decorated. Sounds needed two hooks, not one: beta plays a tapping sound every four ticks while you mine, from the interaction managers, and a separate crunch when the block gives way, from world event 2001. The tapping is what you hear for nearly the whole interaction. The crunch and the debris cloud also needed the disguise written down before the block leaves, since both are produced by an event that arrives after the position is already empty.
The StationAPI smoke suite could not run
smokeTest -Pstationapi had stopped launching anything, and two of the three causes came in with the toolchain move: :stationapi and :test:stapi never got the org.lwjgl claim the root project took, and :test:stapi still asked for a starac_version property that no longer exists. A suite that cannot start looks exactly like a suite with nothing to report.
Running again, it found a real bug on the first try: DroppedItemScaleMixin crashed the StationAPI client on load, because StationAPI's arsenic renderer merges ItemRenderer.render and an injector cannot target a method another mixin has merged at the same priority. That is a hard error at class load, not a hook quietly not applying. It is disabled under StationAPI, so the dropped item scale is a no-StationAPI feature.
All four suites pass: client 60/60, server 46/47 (one not on that side's classpath), stapiClient 63/63, stapiServer 47/48.
0.3.6
Auxiliary per-position block data.
RetroAPI had both ends of block storage and nothing between them. Block state is 12 bits, total, for every property a block has. A block entity holds anything, at the price of being walked and range-checked every single tick, forever, by a game that expects a world to contain dozens of them and not tens of thousands. Data that needs more than a nibble and changes when a player right-clicks fell down the middle: give up on it, or pay per block per tick for it.
RetroBlockData, a 32-bit value per block position, per registered type, saved and synced. Sparse maps on the chunk, one entry per position that actually carries data, and no per-tick cost at all.
public static final RetroBlockDataType CAMO = RetroBlockData.registerBlockRef(id("camo"));
RetroBlockData.set(world, x, y, z, CAMO, RetroBlockData.encodeBlockRef(Block.GLASS.id, 0));
int worn = RetroBlockData.get(world, x, y, z, CAMO);It persists in the region sidecar as a new v4 section, omitted entirely from chunks that carry none so those files stay byte-identical to v3. It rides the chunk packet on chunk send, and single-position changes are pushed to the players who can actually see the position. Reads work from the chunk-render thread's WorldRegion view, because a block's own renderer is exactly the caller that wants this.
registerBlockRef, for the case that makes a raw int wrong. A runtime block id is a property of the installed mod set, not of the world, so storing one verbatim means the day a mod is added or removed, every stored reference quietly points at a different block. A whole build re-skins itself and nothing errors. Block reference types go through a per-chunk string palette instead, the same machinery RetroAPI already uses for the modded blocks themselves, and a reference whose mod is missing this session is parked and written back out on save rather than erased. Vanilla ids are fixed for all time and are written numerically.
A position's data is dropped when the block there changes, so a value can never be inherited by whatever is placed there next. Metadata and state changes, a door opening or a crop growing, go through setBlockMeta and keep theirs.
Nothing else changed. Both launch smoke suites pass, client and server, with every mixin applying cleanly.
0.3.5
Right-click behavior, block entity sync, freeform multiblocks.
Everything here comes from one modder hitting the same wall three ways: RetroAPI gave you a place to put your block, and nothing to put in it.
BlockUseCallback, right-click behavior on any block, safely. Beta's CropBlock never overrides onUse, which tempts you into mixing a fresh onUse INTO CropBlock to add right-click harvest. That method then shadows Block.onUse, and every other mod's @Inject into Block.onUse silently stops running for crops. Their mod breaks, from three dependencies away, with no error anywhere. Listeners here compose: they run in order until one returns SUCCESS or FAIL, and the event fires for every block before its own onUse, so it can also replace or veto vanilla behavior.
BlockUseCallback.EVENT.register((player, world, held, x, y, z, face) -> {
if (world.getBlockId(x, y, z) != Block.WHEAT.id) return Result.PASS;
if (!world.isRemote) harvestAndReplant(world, x, y, z);
return Result.SUCCESS;
});Hooked on the side that actually decides the interaction, the client in singleplayer and the dedicated server in multiplayer, so a listener runs exactly once per click.
RetroSyncedBlockEntity, block entity data on the client without an inventory. b1.7.3's protocol has no generic block entity packet. The only one that carries block entity data is the sign packet, so the one vanilla-shaped way for a modded block entity to reach the client was to masquerade as a container and push its state through the inventory and window packets. Anything that is not an inventory, a tank's fluid level, a machine's progress bar, a barrel's displayed stack, had no answer at all. Implement the interface and RetroAPI carries the NBT over its own channel, automatically on chunk send and on every setBlockDirty; RetroBlockEntities.sync(be) is the explicit push. It rides vanilla's own per-chunk player tracking, so only players who can see the block get the packet.
RetroMultiblock.matchAnywhere(...) and RetroBlockRegion. match assumed the position IS the anchor, the dedicated-controller shape: walk to the core block and click that. matchAnywhere tries the position as every cell in every rotation, so right-clicking any part of the structure works, and Match.anchor() says where the controller landed. RetroBlockRegion is the other half: a pattern is the wrong tool for a structure whose size and shape are the player's choice, so it floods outward from any block through whatever rule you give it, with a visit limit so an unbounded build comes back marked incomplete rather than freezing the tick.
Registrable tool tiers. RetroToolTier was an enum, which made its five tiers the only tiers that could ever exist. It is a registry now, and the built-ins are ordinary entries in it: RetroToolTier.register("bronze", 1, 5.0F). Levels need not be unique, so two tiers can harvest the same blocks and still differ in speed and in which needs_<name>_tool tag they answer to. Being a class rather than an enum, it can no longer be used in a switch or an EnumSet; compare with isAtLeast or read getLevel().
RetroToolTier.Positional, a tool tier that can see the block's position and state. Contextual gets the Block, which is the block TYPE, and every state of a block is the same Block object, so "diamond-tier on lit ore, wood-tier on unlit" was not expressible. Beta's harvest hooks carry no coordinates at all, so RetroAPI records what a player is breaking and hands the position over. Reads are validated against the world, so a stale record from an abandoned break can never answer for the wrong block.
RetroBlockAccess.AUTO_ID, which the item side has had since 0.3.0, plus block() and item() for reaching a vanilla method mid-chain.
Fixed: hoes no longer mine leaves faster. Material inference gave undeclared blocks a sensible default tool, and it was reaching vanilla blocks too. Leaves are the LEAVES material, modern Minecraft files leaves under mineable/hoe, and so merely installing RetroAPI handed every beta hoe a leaf-cutting bonus. Vanilla membership is spelled out block by block in VanillaToolTags, transcribed from beta's own tool code, and that is now the whole truth about vanilla blocks. A library has no business changing how vanilla plays.
Fixed: allocateId() reserves what it hands out. It scanned for a null slot and returned it, so two allocations before either constructor ran picked the same id and the second store silently won, the race RetroItemIds already closed on the item side.
Fixed: a javadoc that caused a bug. RetroModInitializer.initRetro() listed recipes among the things to register there. Every mod's initRetro() runs before any callback fires, but they run in mod load order relative to each other, so a recipe built there that names another mod's item works or fails depending on which mod loaded first. Register those in RecipeRegistrationCallback.EVENT instead, which is what it is for.
Built and validated with and without -Pstationapi on Ornithe (b1.7.3): all four launch smoke suites plus the four-stage conversion pipeline. The StationAPI run earned its keep this time, catching that the new use hook had been added to a mixin class RetroAPI disables under StationAPI, which would have shipped the event dead for every StationAPI user.
0.3.4
Tinted item layers, on any item.
Blocks could already tint an overlay. Items could not, and the reason was structural: RetroItemAccess.overlay(id) flattens its sprite into the atlas at stitch time, so by the time anything could apply a color the layers are one image. The only tinted path was implementing RetroLayeredTexture on the item class, which is impossible when wrapping a subclass the mod does not own - which is exactly what RetroItemAccess.of(id -> new MyItem(id, ...)) is for.
RetroItemAccess.of(id -> new MyOreItem(id, material))
.texture(id("ore_reg/raw"))
.overlay(id("ore_reg/raw_overlay"), material.color)
.register(id("raw_" + material.id));overlay(textureId, tint)draws as a separate render-time pass, so the0xRRGGBBmultiply survives. It is declared, not implemented, so it needs no interface and no subclass of yours.- The overlay texture no longer has to be registered separately. It goes through
getOrAddItemTexture, so the same overlay declared across twenty items costs one atlas slot, and the handle resolves its index at draw time - naming a texture before the atlas is stitched still points at the right sprite. layer(RetroTextureLayer)appends a fully specified layer (a tinted base, or one built from a sprite index you already hold), andgetDeclaredLayers()reads them back.- An item that implements
RetroLayeredTexturestill wins, so a component-driven per-stack look keeps overriding the declared one.
Layer 0 is seeded from the item's own texture, preferring the tracked RetroTexture handle so the base resolves at draw time too.
Built and validated with -Pstationapi on Ornithe (b1.7.3): all four launch smoke suites, plus the four-stage conversion pipeline (populate, forward and reverse convert, runtime verify). The client suite asks the renderer the same question it asks every frame, so a declaration that never reaches the screen fails the build rather than rendering untinted in game; it passes under StationAPI too, which has its own atlas.
Full feature set: https://matthewperiut.github.io/retroapi/features-0.3.4.html
0.3.3
Closing the gaps around block state.
A sweep for one shape of bug: an API that quietly does less than it looks like it does. Everything below compiled, ran, and either lost data or had no way to express the thing its own documentation described.
Truncation a state index is 12 bits, not 4
RetroBlockState.getIndex() puts its low nibble in vanilla metadata and bits 4–11 in the sidecar. Three APIs took a bare 4-bit meta, so a block with more than 16 states silently lost everything above the nibble:
RetroFeatures.setBlock(world, x, y, z, state)— a world feature could not place one.RetroWorldGen.setStateInChunk(...)— a custom chunk generator could not place one at all.RetroMultiblock.Match.fill(world, state)— a multiblock could only be formed out of the low nibble.
If you store block state as an int anywhere, this is the trap: it looks correct up to state 15, then wraps instead of failing.
A tier that can refuse
RetroToolTier.NONE sits below every tier, so it satisfies no needs_<tier>_tool requirement. A Dynamic/Contextual tier previously had no way to say "this tool cannot harvest this block" — null means "no opinion, fall through", and falling through lands on WOOD. A drill bit that only bites certain ores is now .tier((stack, block, player) -> isOre(block) ? DIAMOND : NONE).
Placement that does not notify
RetroStates.set(...) always notified neighbors, which during generation can cascade a block update back into the chunk still being built, with no way to opt out. Added RetroStates.setWithoutNotifyingNeighbors(...) and placeWithoutNotifyingNeighbors(...) (block + state in one call). The position is still marked dirty and a dedicated server still syncs the index — those are not neighbor updates, and skipping them would leave the block invisible rather than merely un-notified.
Textures
RetroTextures.getOrAddItemTexture(...) / getOrAddBlockTexture(...). addItemTexture allocates a new atlas slot on every call, so two callers wanting the same sprite quietly burned a slot and got two handles to one image. The get-or-add form is safe on either side and at any time — which is what code that tints a sprite someone else registered needs. Also RetroToolTier.getTagName(), for building needs_<tier>_tool ids from code.
The test harness had the same disease
The conversion pipeline documented four stages and ran three. Stage 4 — load the reverse-converted world on a plain, non-StationAPI server and prove the modded content is runtime-valid, not merely intact on disk — existed as a written, documented scenario and was wired into no task. Disk verification proves the bytes survived; this proves the world is usable, which is the actual claim. It runs now, and it passes. A failing populate stage also used to sail through silently; it is gated where it is written.
Full feature set: https://matthewperiut.github.io/retroapi/features-0.3.0.html
Built and validated with -Pstationapi on Ornithe (b1.7.3): all four launch smoke suites, plus the now-complete four-stage conversion pipeline (populate → forward/reverse convert → runtime verify).
0.3.2
Shared component state, and the IDE storm.
Two bugs that only showed up once mods started leaning on 0.3.x. Update if you use data components, or if your IDE has started demanding you implement methods you never wrote.
- Fixed: every item shared one set of components.
RetroComponentType.getDefault()returned the exact instance passed at registration, soRetroComponents.get(stack, TYPE)on any stack that had not set a value handed back the same object every time. Mutating it —get(stack, LIST).add(x)— wrote into the value every other stack reads, so the data appeared on every item in the game at once, and deleting one item's value looked like it worked only sometimes. Only mutable components could show it.List/Set/Mapdefaults are now copied per read, so existing mods are fixed with no code change, andRetroComponents.registerSupplied(id, supplier, serializer)covers a mutable default of a type RetroAPI cannot copy for you. - Fixed: mods inherited 56 unimplemented methods.
RetroItemAccessandRetroBlockAccessdeclared their methods abstract, and 0.3.0 made interface injection actually work — soItemandBlockgenuinely implement them, and from an IDE's point of view every class extending either had to implement all 56.javacnever complained, because it does not re-verify a binary superclass, which is why it looked like an IDE-only hallucination and only surfaced once the class implemented some interface of its own. Every method is now adefaultthat throws; the mixin's implementation is a method on the class, and a class method always wins over an interface default, so runtime behavior is unchanged. - Fixed: the client no longer tries to load dedicated-server classes. Thirteen mixins targeting
net.minecraft.server.*sat in the mixin config's common list, so every client launch attempted each one and loggedCannot load class ... in environment type CLIENT. b1.7.3 has no integrated server — singleplayer is the client — so none could ever apply there. A client launch now logs no mixin warnings at all.
Reminder for anyone storing block state as an integer: RetroBlockState.getIndex() is 12 bits, not 4. The low nibble rides vanilla metadata and bits 4–11 live in the sidecar, so world.setBlockMeta(x, y, z, state.getIndex()) silently truncates for any block with more than 16 states. Use RetroStates.set(...).
Full feature set: https://matthewperiut.github.io/retroapi/features-0.3.0.html
Built and validated with -Pstationapi on Ornithe (b1.7.3): all four launch smoke suites pass (client, server, and both with StationAPI), plus the headless populate self-check and the vanilla↔StationAPI conversion round-trip.