Skip to content

Writing Plugins

Fed43 edited this page Oct 10, 2026 · 2 revisions

Writing Plugins

This page explains how to write your own plugin for MineAlphaProxy using the MineAlpha Plugin API.

Overview

MineAlphaProxy loads plugins from two folders:

Folder API
MineAlphaPlugins/ Own MineAlpha API (com.minealphaproxy.api)
VelocityPlugins/ Compatibility layer for Velocity plugins

This page covers the MineAlpha API. If you have an existing Velocity plugin, just drop it into VelocityPlugins/ and it will load.

Project Setup

Step 1. Create the project structure

MyPlugin/
+-- build.gradle.kts
+-- settings.gradle.kts
+-- src/main/
    +-- java/com/example/myplugin/
    |   +-- MyPlugin.java
    +-- resources/
        +-- minealpha-plugin.yml

Step 2. settings.gradle.kts

rootProject.name = "MyPlugin"

Step 3. build.gradle.kts

plugins {
    java
}

group = "com.example"
version = "1.0.0"

java {
    toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}

repositories {
    mavenCentral()
}

dependencies {
    // Path to your MineAlphaProxy JAR (built from source, or downloaded from Releases).
    // Replace the path below with the actual location on your machine.
    compileOnly(files("PATH_TO_MINEALPHAPROXY_JAR"))

    compileOnly("net.kyori:adventure-api:4.24.0")
    compileOnly("org.slf4j:slf4j-api:2.0.13")
}

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
    options.release.set(17)
}

tasks.jar {
    archiveFileName.set("MyPlugin-1.0.0.jar")
}

Replace PATH_TO_MINEALPHAPROXY_JAR with the full path to your MineAlphaProxy JAR.

Common locations:

OS Example path
Windows D:/MineAlphaProxy/build/libs/MineAlphaProxy-1.0.1-ALPHA.jar
Linux /home/user/MineAlphaProxy/build/libs/MineAlphaProxy-1.0.1-ALPHA.jar
macOS /Users/user/MineAlphaProxy/build/libs/MineAlphaProxy-1.0.1-ALPHA.jar

Use forward slashes / even on Windows - Gradle accepts them and they work cross-platform.

Recommended for distribution: place the MineAlphaProxy JAR inside your plugin project's libs/ folder and use a relative path:

compileOnly(files("libs/MineAlphaProxy-1.0.1-ALPHA.jar"))

This way, anyone who clones your plugin repository can build it without editing paths.

What is compileOnly?

compileOnly tells Gradle: "Use this JAR to compile my code, but do not bundle it into my plugin's output JAR."

This is important because:

  • MineAlphaProxy's API classes (MineAlphaPlugin, MineAlphaPlayer, etc.) are already present in the running proxy
  • Bundling them into your plugin would create duplicate classes and cause LinkageError at runtime
  • It keeps your plugin JAR small (a few KB instead of tens of MB)

Use compileOnly for anything the proxy already provides:

  • The MineAlphaProxy JAR itself
  • Adventure API (chat components)
  • SLF4J (logging)

Use implementation only for libraries that are not part of the proxy and must be bundled with your plugin.

Step 4. minealpha-plugin.yml

Place this file in src/main/resources/:

id: myplugin
name: MyPlugin
version: 1.0.0
author: YourName
description: A short description of what this plugin does
main: com.example.myplugin.MyPlugin
Key Required Description
id yes Unique plugin id - used as folder name for data
name no Display name
version no Plugin version
author no Author
description no Description
main yes Fully qualified main class

Writing the Main Class

The main class extends MineAlphaPlugin and overrides lifecycle methods.

package com.example.myplugin;

import com.minealphaproxy.api.MineAlphaPlugin;
import com.minealphaproxy.api.annotation.MineAlphaPluginInfo;

@MineAlphaPluginInfo(
        id = "myplugin",
        name = "MyPlugin",
        version = "1.0.0",
        author = "YourName"
)
public class MyPlugin extends MineAlphaPlugin {

    @Override
    public void onLoad() {
        getLogger().info("Loading...");
    }

    @Override
    public void onEnable() {
        getLogger().info("Enabled!");
    }

    @Override
    public void onDisable() {
        getLogger().info("Disabled.");
    }
}

Lifecycle

Method When Use for
onLoad() Before onEnable, before events fire Reading config, early setup
onEnable() After all plugins loaded Registering commands and events
onDisable() On proxy shutdown Saving data, closing resources

Accessing the Proxy

From onEnable() onward, getProxy() returns a MineAlphaProxy instance.

MineAlphaProxy proxy = getProxy();

// Players
Collection<MineAlphaPlayer> all = proxy.getPlayers();
Optional<MineAlphaPlayer> steve = proxy.getPlayer("Steve");
int count = proxy.getPlayerCount();

// Servers
Collection<MineAlphaServer> servers = proxy.getServers();
Optional<MineAlphaServer> lobby = proxy.getServer("lobby");

// Broadcast
proxy.broadcastMessage("§aServer restart in 5 minutes!");
proxy.broadcastMessage(Component.text("Hello"));

Player API

MineAlphaPlayer player = ...;

String name = player.getName();
UUID uuid = player.getUniqueId();
InetSocketAddress address = player.getAddress();
boolean online = player.isOnline();

// Messages
player.sendMessage("§aHello!");
player.sendMessage(Component.text("Hello!", NamedTextColor.GREEN));
player.sendActionBar("§eNow playing: lobby");

// Kick
player.kick("§cYou have been kicked.");

// Permissions (always returns true in 1.0.1-ALPHA)
boolean has = player.hasPermission("myplugin.admin");

// Server (returns "unknown" in 1.0.1-ALPHA)
String server = player.getCurrentServer();

Registering Commands

Commands extend MineAlphaCommand and are registered through the command manager.

import com.minealphaproxy.api.command.CommandSource;
import com.minealphaproxy.api.command.MineAlphaCommand;

public final class HelloCommand extends MineAlphaCommand {

    public HelloCommand() {
        super(
                "hello",                      // name
                "Say hello",                  // description
                "/hello [name]",              // usage
                "myplugin.hello",             // permission (null = none)
                "hi", "hey"                   // aliases
        );
    }

    @Override
    public void execute(CommandSource sender, String[] args) {
        String target = args.length > 0 ? args[0] : sender.getName();
        sender.sendMessage("§aHello, §e" + target + "§a!");
    }
}

Register in onEnable():

@Override
public void onEnable() {
    getProxy().getCommandManager().register(new HelloCommand());
    getLogger().info("Registered /hello");
}

Unregister by name:

getProxy().getCommandManager().unregister("hello");

Command Sources

CommandSource source = ...;

source.sendMessage("§aMessage");
source.getName();                    // "CONSOLE" or player name
source.hasPermission("perm.node");   // always true in 1.0.1-ALPHA
source.isPlayer();                   // true if a player ran the command
source.asPlayer();                   // Optional<MineAlphaPlayer>

Example using asPlayer():

@Override
public void execute(CommandSource sender, String[] args) {
    if (!sender.isPlayer()) {
        sender.sendMessage("§cOnly players can use this.");
        return;
    }
    MineAlphaPlayer player = sender.asPlayer().get();
    player.sendActionBar("§eYou did it!");
}

Registering Events

Annotate methods with @Subscribe and the single argument is the event type.

import com.minealphaproxy.api.annotation.Subscribe;
import com.minealphaproxy.api.event.PlayerJoinEvent;
import com.minealphaproxy.api.event.PlayerLeaveEvent;
import com.minealphaproxy.api.event.ProxyStartEvent;
import com.minealphaproxy.api.event.ProxyStopEvent;

@Subscribe
public void onProxyStart(ProxyStartEvent event) {
    getLogger().info("Proxy started");
}

@Subscribe
public void onJoin(PlayerJoinEvent event) {
    MineAlphaPlayer player = event.getPlayer();
    player.sendMessage("§aWelcome, " + player.getName() + "!");
}

@Subscribe
public void onLeave(PlayerLeaveEvent event) {
    getLogger().info("{} left", event.getPlayer().getName());
}

@Subscribe
public void onStop(ProxyStopEvent event) {
    getLogger().info("Goodbye");
}
Event When
ProxyStartEvent After all plugins loaded, proxy is listening
ProxyStopEvent Before shutdown
PlayerJoinEvent After a player successfully logs in
PlayerLeaveEvent When a player disconnects
PlayerChatEvent (planned, not fired yet in 1.0.1-ALPHA)
ServerSwitchEvent (planned, not fired yet in 1.0.1-ALPHA)

Event priority (via @Subscribe(priority = N), higher runs first) is supported by the API but not yet used by most events.

Scheduler

Run tasks in the background:

getProxy().getScheduler().runAsync(() -> {
    getLogger().info("Hello from a task");
});

getProxy().getScheduler().runAsyncLater(() -> {
    getLogger().info("Hello 5 seconds later");
}, 5, TimeUnit.SECONDS);

getProxy().getScheduler().runAsyncRepeating(() -> {
    getLogger().info("Tick - {} players online",
            getProxy().getPlayerCount());
}, 0, 60, TimeUnit.SECONDS);

Cancel a task:

var task = getProxy().getScheduler().runAsyncRepeating(...);
task.cancel();

Configuration and Data Folder

Each plugin gets its own folder under MineAlphaPlugins/<id>/.

File folder = getDataFolder();
File configFile = new File(folder, "config.yml");

Save a bundled resource:

// Copies src/main/resources/config.yml to your data folder
saveResource("config.yml", false);

Read the YAML with SnakeYAML (already available):

try (InputStream in = new FileInputStream(
        new File(getDataFolder(), "config.yml"))) {
    Map<String, Object> map = new Yaml().load(in);
    String greeting = (String) map.getOrDefault("greeting", "Hello");
}

Complete Example Plugin

A small plugin that greets players, adds a /hello command, and logs player count every minute.

package com.example.myplugin;

import com.minealphaproxy.api.MineAlphaPlugin;
import com.minealphaproxy.api.MineAlphaPlayer;
import com.minealphaproxy.api.annotation.MineAlphaPluginInfo;
import com.minealphaproxy.api.annotation.Subscribe;
import com.minealphaproxy.api.command.CommandSource;
import com.minealphaproxy.api.command.MineAlphaCommand;
import com.minealphaproxy.api.event.PlayerJoinEvent;
import com.minealphaproxy.api.event.ProxyStartEvent;

import java.util.concurrent.TimeUnit;

@MineAlphaPluginInfo(
        id = "myplugin",
        name = "MyPlugin",
        version = "1.0.0",
        author = "YourName"
)
public class MyPlugin extends MineAlphaPlugin {

    @Override
    public void onEnable() {
        getLogger().info("MyPlugin enabled!");

        getProxy().getCommandManager().register(new MineAlphaCommand(
                "hello", "Say hello", "/hello [name]", null, "hi") {
            @Override
            public void execute(CommandSource sender, String[] args) {
                String target = args.length > 0 ? args[0] : sender.getName();
                sender.sendMessage("§aHello, §e" + target + "§a!");
            }
        });

        getProxy().getScheduler().runAsyncRepeating(() -> {
            getLogger().info("Online: {} players",
                    getProxy().getPlayerCount());
        }, 0, 60, TimeUnit.SECONDS);
    }

    @Override
    public void onDisable() {
        getLogger().info("MyPlugin disabled");
    }

    @Subscribe
    public void onStart(ProxyStartEvent event) {
        getLogger().info("Proxy is up!");
    }

    @Subscribe
    public void onJoin(PlayerJoinEvent event) {
        MineAlphaPlayer player = event.getPlayer();
        player.sendMessage("§aWelcome, " + player.getName() + "!");
    }
}

Building and Installing

cd MyPlugin
./gradlew clean build

Output: build/libs/MyPlugin-1.0.0.jar

Copy to the proxy:

MineAlphaPlugins/MyPlugin-1.0.0.jar

Restart MineAlphaProxy. Expected log:

[..] INFO - Loaded MineAlpha plugin: myplugin v1.0.0
[..] INFO - Registered command: /hello
[..] INFO - MyPlugin enabled!

Full API Reference

Interface Purpose
MineAlphaProxy Main API - players, servers, commands, scheduler, events
MineAlphaPlayer A connected player
MineAlphaServer A backend server
MineAlphaLogger Plugin logger (info, warn, error)
MineAlphaScheduler Async task scheduler
MineAlphaEventBus Event dispatch (used internally)
MineAlphaCommandManager Command registry
CommandSource Source of a command (console or player)

See the Installation page for the proxy JAR location used for compileOnly.

Limitations (1.0.1-ALPHA)

  • Permission checks always return true - no Permissions API yet
  • getCurrentServer() returns "unknown"
  • PlayerChatEvent and ServerSwitchEvent exist but are not fired
  • Player.sendMessage(Component) works reliably only on 1.20.2 and older; on newer versions NBT-based chat is required (planned)
  • Velocity plugin support is partial - complex plugins may fail on missing API classes

Next Steps

Clone this wiki locally