Skip to content

Extension API for Modders

siralusian edited this page Aug 4, 2026 · 1 revision

Extension API for Modders

CobbleCompanion: Basis exposes a small, stable, client-only API that lets another mod add its own tab to the companion window — this is exactly the mechanism CobbleCompanion's own official extension modules (CobbleDollars' Wallet tab, etc.) use, so it's a real, exercised code path, not a theoretical hook.

Stability promise: everything in package com.cobblecompanion.api is the only supported way to extend CompanionScreen. Internal classes under com.cobblecompanion.client.screens (and elsewhere) are not part of the API contract and can change at any time without notice — don't reflect into them.

Package overview

Class Role
CompanionExtensions Static registry. Call once at startup to plug your tab in.
CompanionTabExtension Interface you implement — your tab's content and input handling.
CompanionTabContext Facade passed into every callback — window geometry + the shared mini-GUI toolkit.

Source: src/main/java/com/cobblecompanion/api/ in the CobbleCompanion repo.

How it fits together

  1. CobbleCompanion reserves a fixed set of tab slots (CompanionScreen.TAB_* constants). Most are used by Basis itself; TAB_WALLET (index 6) is the one currently free for extensions — it's what CobbleCompanion: CobbleDollars' Wallet tab uses.
  2. If nothing is registered for a tab index, CompanionScreen just renders its existing "not installed" placeholder there — no crash, no compile-time dependency in either direction.
  3. Your mod depends on CobbleCompanion compileOnly (see Building against it below) — at runtime, if a player doesn't have CobbleCompanion installed at all, your extension mod simply never calls registerTab, because CompanionExtensions itself never gets classloaded on the server / non-companion clients. If a player does have both installed, your tab lights up automatically.
Constant Index Used by
TAB_POKEDEX 0 Basis
TAB_LIVINGDEX 1 Basis
TAB_TODO 2 Basis
TAB_WHONEEDS 3 Basis
TAB_TYPES 4 Basis
TAB_TEAMBUILDER 5 Basis
TAB_WALLET 6 Free — CobbleDollars' Wallet tab uses it today, the intended extension slot
TAB_SEARCH 7 Basis
TAB_HOME 8 Basis
TAB_FRIENDS 9 Basis
TAB_PROFESSOR 10 Basis (admin-only)
TAB_SETTINGS 11 Basis

If you need an additional slot beyond TAB_WALLET, open an issue on the CobbleCompanion repo — new tab indices are added on request.

CompanionExtensions

public final class CompanionExtensions {
    public static void registerTab(int tabIndex, CompanionTabExtension extension);
    public static CompanionTabExtension getTab(int tabIndex);
    public static boolean hasTab(int tabIndex);
}

Call registerTab once, in your mod's client-side constructor/init (same place CobbleCompanion's own extensions do it) — see the worked example below.

CompanionTabExtension

The interface you implement. Only isAvailable, render, and mouseClicked are required — everything else has a sensible default (no-op / false):

public interface CompanionTabExtension {
    boolean isAvailable(CompanionTabContext ctx);
    void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, CompanionTabContext ctx);
    boolean mouseClicked(double mouseX, double mouseY, int button, CompanionTabContext ctx);

    default void renderTopLayerContent(GuiGraphics graphics, int mouseX, int mouseY, CompanionTabContext ctx) {}
    default void mouseReleased(double mouseX, double mouseY, int button, CompanionTabContext ctx) {}
    default void mouseDragged(double mouseX, double mouseY, CompanionTabContext ctx) {}
    default boolean isDraggingScrollbar() { return false; }
    default boolean mouseScrolled(double mouseX, double mouseY, double scrollY, CompanionTabContext ctx) { return false; }
    default boolean keyPressed(int keyCode, int scanCode, int modifiers, CompanionTabContext ctx) { return false; }
    default boolean charTyped(char chr, int modifiers, CompanionTabContext ctx) { return false; }
    default boolean isCapturingTextInput() { return false; }
    default void onTabOpened(CompanionTabContext ctx) {}

    // Optional blocking yes/no confirmation overlay (used by the Wallet tab's transfer confirm dialog)
    default boolean hasBlockingOverlay() { return false; }
    default void renderBlockingOverlay(GuiGraphics graphics, int mouseX, int mouseY, CompanionTabContext ctx) {}
    default boolean blockingOverlayMouseClicked(double mouseX, double mouseY, CompanionTabContext ctx) { return false; }
}

Notes:

  • No lifecycle state is handed to you. Your implementation holds its own state (text fields, scroll position, ...) exactly like CobbleCompanion's own built-in tabs do — a fresh CompanionScreen instance is created every time the player opens the window.
  • isAvailable is checked by CompanionScreen to decide whether to show your tab as active or as the "not installed" placeholder — typically wired to a server-sent flag ("does the server have my companion mod loaded too?").
  • renderTopLayerContent runs after everything else in CompanionScreen.render() (same as Basis' own search-suggestion rendering) — use it for anything that must draw above other widgets, e.g. a suggestion dropdown under a search field.

CompanionTabContext

Passed into every callback — gives you window geometry plus the shared mini-GUI toolkit (CompanionScreen implements this interface itself and passes this):

public interface CompanionTabContext {
    int guiLeft(); int guiTop(); int guiWidth(); int guiHeight();
    int screenWidth(); int screenHeight();

    void drawSmallLabel(GuiGraphics graphics, String text, int x, int y, float scale, int color, boolean bold, boolean uniformFont);
    void drawScaledBoldText(GuiGraphics graphics, String text, int x, int y, float scale, int color);
    int smallLabelWidth(String text, float scale, boolean bold, boolean uniformFont);
    List<String> wrapText(String text, float scale, boolean bold, boolean uniformFont, int maxWidth);

    void renderConfirmButton(GuiGraphics graphics, int x, int y, int w, int h, String label, int color, int mouseX, int mouseY);
    void renderScrollbar(GuiGraphics graphics, int barX, int barWidth, int trackTop, int trackHeight, double scrollAmount, int maxScroll);
    boolean isMouseOverScrollbar(double mouseX, double mouseY, int barX, int barWidth, int trackTop, int trackHeight);
    double scrollAmountFromMouseY(double mouseY, int trackTop, int trackHeight, int maxScroll);
    boolean isInRect(double mouseX, double mouseY, int x, int y, int w, int h);

    void renderSearchSuggestions(GuiGraphics graphics, int mouseX, int mouseY, CobblemonSearchBox box, List<String> suggestions);
    boolean handleSearchSuggestionClick(double mouseX, double mouseY, CobblemonSearchBox box, List<String> suggestions);

    default void sendToServer(CustomPacketPayload payload) { /* channel-guarded, see below */ }
    default String tr(String key) { /* I18n.get */ }
    default String tr(String key, Object... args) { /* I18n.get */ }
}

Use ctx.sendToServer(...), not PacketDistributor.sendToServer(...) directly. It's guarded against sending a payload the connected server never negotiated (mixed-version / vanilla-only servers) — a raw PacketDistributor call there throws UnsupportedOperationException and crashes the client. This is the same guard CobbleCompanion's own code uses everywhere internally.

Layout constants (padding, colors, etc.) are deliberately not part of this interface — bring your own, exactly like every built-in tab already does for its own content.

Building against it

Add CobbleCompanion as a compileOnly dependency (you don't ship its classes, you only compile against its public API — the same pattern CobbleCompanion's own split repos use against each other, see their READMEs' "Building" sections):

dependencies {
    compileOnly fileTree(dir: 'libs', include: ['CobbleCompanion-Basis*.jar'])
    // ... your other dependencies
}

Build CobbleCompanion-Basis-*.jar yourself from the CobbleCompanion repo (./gradlew jar) and drop the result into your own libs/ folder.

Worked example: a minimal extension tab

This mirrors exactly how CobbleCompanion: CobbleDollars registers its real Wallet tab.

package com.example.mycompanionaddon.client;

import com.cobblecompanion.api.CompanionTabContext;
import com.cobblecompanion.api.CompanionTabExtension;
import net.minecraft.client.gui.GuiGraphics;

public class MyTabExtension implements CompanionTabExtension {

    @Override
    public boolean isAvailable(CompanionTabContext ctx) {
        return true; // or: some server-synced "my mod is loaded server-side too" flag
    }

    @Override
    public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, CompanionTabContext ctx) {
        ctx.drawScaledBoldText(graphics, "Hello from my addon!", ctx.guiLeft() + 10, ctx.guiTop() + 10, 1f, 0xFFFFFFFF);
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button, CompanionTabContext ctx) {
        return false; // return true if you consumed the click
    }
}

Registration, in your mod's client constructor (guard it so a server-only install of your mod never touches this class — same as CobbleCompanion's own extensions):

// client-side init only, e.g. inside FMLClientSetupEvent or your mod's client constructor
CompanionExtensions.registerTab(CompanionScreen.TAB_WALLET, new MyTabExtension());

That's it — no mixins, no reflection, no compile-time dependency from CobbleCompanion back to your mod.

Questions / requesting a new tab slot

Open an issue on the CobbleCompanion repo.