All pages

Architecture

How Vantage is put together - the boot order, the packages and what each owns, the event bus, how the mod knows where you are, where data comes from, which thread does what, and the house rules the code keeps.

This page is a map of the mod's source for someone about to change it. It starts at the one entry point, walks the boot order, then goes package by package: the event bus everything talks through, the state that says where the player is, the data layer, the drawing layers and the threads. It ends with the rules the code holds itself to. Paths are under src/main/java/dev/cherishev/vantage/ unless they say otherwise. As of 2.0.1; the repository's own ARCHITECTURE.md is the maintainer's version of the same map.

The shape

Vantage is a client-only Fabric mod with two entry points, both named in fabric.mod.json: the client entrypoint dev.cherishev.vantage.Vantage, and compat.ModMenuIntegration, which hands ModMenu the Vantage menu as the mod's config screen when ModMenu is installed.

Below that the code splits in three:

PartWhat it is
corethe framework: events, config, the feature base class, SkyBlock state, network and data, the HUD, the UI toolkit, commands, developer tools
featuresone package per gameplay area, each holding the features themselves; a feature is a class that extends core.feature.Feature
mixinthe few places that hook Minecraft directly, grouped by area

Minecraft 26.x ships unobfuscated, so there are no mappings: the code calls the real net.minecraft.* names. One tree builds for 26.1.2 and 26.2; the handful of files that differ per version live in overlays beside the main tree, which Multi-version explains.

Boot order

Vantage.onInitializeClient() boots every subsystem itself, in one readable sequence, rather than letting each register itself. The order is the code's, from Vantage.java:

StepCallWhat it starts
1ConfigMigrations.register(…), then ConfigManager.get().load()config schema migrations, then the active config profile
2CoreHooks.init()Fabric events turned into Vantage events; the scoreboard, tab list and location readers
3WorldRenderer.init()the world render hook and its pipelines
4ApiManager.init()the item repository, prices and the election, all asynchronous
5DataManager.init()the community datasets, asynchronous, with a shutdown hook for its watchdog
6StatsBootstrap.init(), Uploader.hooks()Vantage Stats' catalogue and recorder tick, and its sync triggers; nothing records without consent
7HudManager.get().load()the saved HUD layout
8UiBootstrap.setHudEditorFactory(…)wires the HUD editor screen into the UI layer
9Features.registerAll()every area's shared bootstrap, then every feature, constructed from the generated index
10FeatureRegistry.INSTANCE.initAll()each feature's options registered and onInit() run; HUD elements handed to the HUD manager
11UiBootstrap.init()the two Minecraft key mappings and the notifier
12Commands.register()/vantage and /vt
13BugCommand.register()/vantage bug
14DevTools.init()/vantage dev and its overlays
15EventBus.INSTANCE.post(new Events.ModReady())everything is up

The log brackets it with Vantage <version> booting and Vantage ready in <n> ms.

Three consequences of the order are worth knowing. The config is loaded before any feature exists, so a feature's stored values are applied the moment it registers its options. The HUD layout is loaded before any element is registered, so each element picks up its saved position as it arrives. And features run onInit() before the command tree is built, so a subcommand registered there is part of the tree from the first join.

core, package by package

PackageWhat it owns
core.eventEventBus, @Subscribe, the cancellable base, and every event record in Events
core.config@Option, @Range, @Button, Named, the Color and Keybind types, ConfigManager and its implementation
core.featureFeature, @FeatureInfo, FeatureRegistry, and the taxonomy: FeatureCategory, FeatureGroup, FeatureTag
core.skyblockSkyblockState, CoreHooks, Island, the scoreboard and tab-list readers, TabWidgets, SkyblockText, SkyblockTime, and the item API under item/
core.ravengardRavengardState, the same idea as SkyblockState for Hypixel's Ravengard
core.apithe HTTP client, the NotEnoughUpdates item repository, prices, keyless Hypixel resources, Mojang, profiles through Vantage's service, and bug reports
core.datadatasets fetched and cached on disk, LootTracker, and core.data.ledger, the grind ledger's recorder
core.statsVantage Stats: the recording facade, the wire model, the on-disk store, the sync layer, the profile mirror and the per-area collectors
core.hudHudElement and its three ready-made shapes, HudGroup, the HUD manager, the editor, and the Director's gate
core.uithe Obsidian Glass toolkit: Theme, Draw, widgets, screens, animation, notifications, generated config pages
core.renderworld drawing: boxes, lines, beams, world text, waypoints, entity outlines
core.pathfindA* over collision-shape cells, written without Minecraft types and unit-tested, plus the adapter to the live world
core.commandthe /vantage tree, Deferred, CommandEvents
core.devtools/vantage dev: inspectors, overlays, the performance monitor, the UI and pathfinding smoke tests
core.compatthe seam between Minecraft versions; the common part is GpuTimer, the overlays add Mc, Dyed, Mobs and Gpu
core.utilformatting, scheduling, JSON, colour, maths, input and text helpers, and SelfTest, which /vantage dev selftest runs

core.ravengard and core.compat exist in the tree but are missing from the package map in ARCHITECTURE.md.

features

features/ has one package per area, plus a few for systems big enough to be areas of their own: audiovis, blackbox, chat, combat, containers, crimson, dungeons, events, farming, fishing, foraging, general, hunting, inventory, ledger, mining, onboarding, profileviewer, qol, ravengard, rift, slayers, stats, vet and visuals. The package a feature lives in is not what decides where it shows up in the menu: its @FeatureInfo group does. Writing a feature covers that.

features/Features.java boots the feature layer in two phases. First every area's XFeatures.register() runs, for that area's shared state, event wiring and read models. Then every feature in GeneratedFeatureIndex is constructed and registered. A bootstrap that throws costs its area's shared state; a feature that throws costs that one feature.

mixin

Mixins live in mixin.<area>: chat, containers, core, dungeons, farming, inventory, qol, render and visuals. Their configs sit in src/main/resources/. vantage.mixins.json has the package dev.cherishev.vantage.mixin and lists the core mixins along with the farming, inventory and render ones; vantage.chat, vantage.containers, vantage.dungeons, vantage.qol and vantage.visuals each cover their own area.

Every config sets "required": false, and the core one sets defaultRequire to 0. A mixin whose target has moved in a new Minecraft version therefore degrades to a feature that does nothing instead of a crash. That is quiet by design, so python tools/mixin_targets.py checks every target before a release; Multi-version explains it.

Events

Almost everything in the mod talks through one bus, EventBus.INSTANCE. Fabric callbacks and mixins post events; features listen. The bus dispatches by the event's exact class, with no inheritance, so posting costs one list walk.

A feature listens by annotating a one-parameter method with @Subscribe. The method may be private.

@Subscribe
private void onSecond(Events.SecondTick event) {
    // runs once a second, only while this feature is active
}

The registry subscribes a feature's annotated methods when it becomes active and removes them when it stops being active, so a feature never checks its own switch. Code that is not a feature can subscribe a lambda directly:

EventBus.INSTANCE.subscribe(Events.ConfigReloaded.class, event -> refresh(), EventBus.PRIORITY_LOW);
Priority constantValueRuns
PRIORITY_HIGHEST-200first
PRIORITY_HIGH-100
PRIORITY_NORMAL0the default
PRIORITY_LOW100
PRIORITY_LOWEST200last

Some events are cancellable. Cancelling one stops it reaching later listeners, except those registered with receiveCancelled = true, and post returns true so the poster can act on it: a cancelled ChatMessage is not shown, a cancelled SlotClick is not sent, a cancelled KeyPress never reaches Minecraft's key mappings. A listener that throws is logged and the next listener still runs.

What is posted

Every event is a small final class or record in core/event/Events.java:

FamilyEvents
Ticks and lifecycleClientTick (20 a second while a world is loaded), SecondTick, WorldJoin, WorldLeave, ModReady, ConfigReloaded
SkyBlock stateSkyblockStateChange, IslandChange, AreaChange, ServerChange, ProfileChange, PurseChange, ScoreboardUpdate, TabListUpdate
The playerPlayerDeath (once per death, from whichever of three signals fires first), PlayerRespawn
Chat and textChatMessage, ActionBar, TitleShown, ChatSend
RenderingHudRender, LevelRender
Screens and containersScreenOpen, ScreenClose, ContainerOpen, ContainerUpdate, SlotClick, SlotRenderPre, SlotRenderPost, ItemTooltip
InputKeyPress
WorldEntityAdded, EntityRemoved, BlockChange, SoundPlay, ParticleSpawn
NetworkPacketReceive, PacketSend

Unless its Javadoc says otherwise, an event is posted on the client thread. PacketReceive is the exception that matters: it arrives on the network thread, so its handlers must be trivial and must not touch game state.

ChatMessage and ActionBar can also be rewritten with setReplacement(…). Every chat rewrite in the mod passes through CoreHooks, which runs it through TextUtils.keepActions: if the original line could be clicked and the rewrite cannot, the original's click action is put back on the whole line. Commands and keybinds has more on that rule.

Where am I?

SkyblockState.get() answers the question every feature asks. Its getters are cheap enough to call every frame.

GetterAnswers
onHypixel(), inSkyblock(), inLobby()which network, which game, and whether this is a lobby rather than a game
island(), area()the Island constant, and the area line under it ("Forge", "Village")
serverId(), profileName()the Hypixel server id, and the SkyBlock profile's name
ironman(), stranded(), bingo()the profile's mode
purse(), bits(), motes(), copper()the balances Hypixel shows you
time()the SkyBlock calendar
scoreboardLines(), tabListEntries()the raw lines, colour-stripped
dungeon()floor, class, timer, boss phase, party, secrets, score inputs

The state is fed from three sources: the Hypixel Mod API's location packet first, the scoreboard as a fallback and for the area, purse and time, and the tab list for the profile and server. When a value changes, the matching event (IslandChange, PurseChange and the rest) is posted after the getter already returns the new value.

Island is the enum a feature's islands scope uses: HUB, PRIVATE_ISLAND, GARDEN, DUNGEON_HUB, DUNGEON, DWARVEN_MINES, CRYSTAL_HOLLOWS, MINESHAFT, CRIMSON_ISLE, KUUDRA, THE_RIFT, THE_END, and the rest. Each carries the mode value the Hypixel Mod API reports for it and the name the scoreboard prints, which doubles as its display name. For anything the state does not expose, ScoreboardReader, TabListReader and TabWidgets read the raw sidebar and tab list. RavengardState does the same job for Ravengard and follows the same shape.

Data

Apart from the one Mojang handshake, which IdentityProof hands to Minecraft's own session service, nothing in the mod opens its own network connection. Everything else goes through core.api.http.Http:

  • one HttpClient, with every request on a virtual thread and every call returning a CompletableFuture;
  • an ETag and Last-Modified disk cache under config/vantage/cache, optionally served stale when the network is down, and marked stale so a caller cannot mistake it for fresh;
  • per-host rate limiting with backoff on 429, 5xx and network errors, honouring Retry-After;
  • the User-Agent Vantage/<mod version> (Minecraft <game version>) on every request.

On top of it, core.api holds the sources: NeuRepo (the NotEnoughUpdates item repository, downloaded into config/vantage/repo/), Prices (Bazaar and lowest-BIN prices), HypixelApi (keyless resources only), MojangApi, and ProfileService, which reads SkyBlock profiles through Vantage's own service because the mod holds no Hypixel API key. The service says what that service is.

core.data keeps the community datasets under config/vantage/data/<id>/: Skyblocker's dungeon room data, a selection of SkyHanni's constants, and NotEnoughUpdates' constants. Each is parsed from its cached files first, so a launch without a network serves what was fetched last time; a source that cannot be reached logs one warning and its accessors return empty collections. Nothing in this layer runs on the render thread.

core.stats is Vantage Stats, opt-in and off by default. Its model and store are free of Minecraft types and unit-tested; The Stats protocol describes what it sends.

Drawing

Three layers draw things, and each has its own page:

LayerPackageDrawsPage
Screens and widgetscore.uithe menu, popups, every Vantage screen, in Obsidian GlassThe UI toolkit
The HUDcore.hudmovable elements, containers, the editorHUD elements
The worldcore.renderboxes, lines, beams, world text, waypointsbelow

WorldRenderer is frame-scoped: a feature submits shapes in world coordinates from an Events.LevelRender handler, or from any client-thread code during the frame, and resubmits them every frame. They are collected and drawn once per frame during Fabric's COLLECT_SUBMITS stage, which is also where LevelRender is posted. Each primitive has a depth-tested and a through-walls variant.

Threads

The rule is the one Minecraft imposes: game state is touched only on the client thread. Anything that blocks - the network, the disk - runs on a virtual thread and hands its result back.

You want toUse
compute off-thread, then use the result on the client threadScheduler.async(work, onClient)
run something on the client thread from anywhereScheduler.runOnClient(…) or Http.onClientThread(…)
run something after the current tick, for example open a screen from a commandDeferred.nextTick(…)
run something after a number of ticksDeferred.after(ticks, …) or Scheduler.schedule(…, delayTicks)

Config saves are debounced by 500 ms and written by a single background thread, vantage-config-saver; HUD layout saves are debounced by 600 ms. Both are written once more from a JVM shutdown hook.

House rules

These are the ground rules from ARCHITECTURE.md, put for someone outside the original team. They are why the code looks the way it does, and a change that breaks one is likely to be sent back.

  1. Never guess a Minecraft or Fabric signature. Read Minecraft's own sources and Fabric API's before calling anything. Minecraft 26.x ships unobfuscated, so the names you read are the real ones.
  2. Read other mods for facts, never copy them. Skyblocker and SkyHanni are LGPL. They are fine to read for chat message formats, scoreboard and tab-list formats, item data keys, repository layouts and room data. Write your own implementation; Vantage's licence is MIT.
  3. Compile often. ./gradlew compileJava test before you call anything done, and no stub that throws UnsupportedOperationException.
  4. Java 25, and only Java. Records, sealed interfaces, pattern-matching switch, text blocks, virtual threads for blocking work, java.net.http.HttpClient. No Kotlin.
  5. One package per subsystem. Keep a change inside its package, and say so when it cannot be.
  6. Plain English strings. Text the player sees is an English literal in the code, not a translation key, unless it is a vanilla component. The language file holds only the two key mapping names and their category.
  7. Mixins are the last resort. Prefer a Fabric API event. A mixin goes in mixin.<area> and is listed in that area's config; an access widener entry goes in both versions' vantage.accesswidener.
  8. The client thread owns the game. Network and file work on virtual threads, posted back through Minecraft.getInstance().execute(…) or Scheduler.
  9. One place touches the Minecraft session. The access token has exactly one call site, core/stats/sync/IdentityProof.java, which hands it straight to Mojang's joinServer and keeps it nowhere. This is a release gate:
grep -rn "getAccessToken\|getSessionId" src/main/java

It must print exactly one line. Releasing runs it beside the mixin check.

The files under core/event, core/config, core/feature and core/hud are treated as contracts: implement against them, and when one has to change, make the smallest change that works and say so in the merge request.