The example client is provided to show the usage of ClientAPI and is included in the CDK (Client Development Kit). For those who would like to start from stratch, below is an overview of the usage of the basic features of ClientAPI.
The JSON is loaded using Gson, mapping the JSON file
to an instance of ClientConfiguration
. An example of a properly defined client.json
file is below. Please note that comments are not official supported in JSON, and if you choose
to copy the config below, they must be removed. Not all fields are required, see the
ClientConfiguration
class for additional details.
{
"name": "Example Client", // The name of the Client
"authors": [ "Your Name" ], // An array of the Client authors
"id": "example", // A unique ID for the Client
"version": "1.0", // The build/version
"mainClass": "com.example.ExampleClient", // The classpath of the main class extending clientapi.Client
"mixins": [ // A list of mixin configuration files
"mixins.example.json" // The name of the Mixin configuration file
],
"transformers": [ // A list of transformer class names
"com.example.transform.Transformer" // The class name of a transformer
]
}
The JSON file should be named client.json
(Case Sensitive) and be located in the root
directory of the compiled client jar file. When using the CDK, it should be located in
src/main/java/resources
to be recognized when compiling in a testing environment.
All Clients/Utility mods extend clientapi.Client
and must inherit the init
method and the default (ClientConfiguration)
constructor. Even though there should only be one active
instance of the Client
class per game instance, ClientAPI does not provide a static method to
access the instance of the Client, therefore it is recommended that one is added.
public final class ExampleClient extends Client {
private static ExampleClient instance;
public ExampleClient(ClientConfiguration config) {
super(config);
instance = this;
}
@Override
public final void init() {
// Your init code here
}
public static ExampleClient getInstance() {
return instance;
}
}
Modules provide an easy way to have toggleable
listeners contained within a class. For more information on how to contain listeners, view the Alpine repository.
All Modules extend clientapi.module.Module
and are required to have a @Mod()
annotation
(clientapi.module.Mod
) or an implementation of one of the multi-parameter constructors. In
both cases, the bind parameter is optional. An example of both are below.
@Mod(name = "Example Module", description = "An example module", bind = Keyboard.KEY_R)
public final class ExampleModule extends Module {
}
public final class ExampleModule extends Module {
public ExampleModule() {
super("Example Module", "An example module", Keyboard.KEY_R);
}
}
ClientAPI provides a class called Manager
, packaged under clientapi.manage.Manager
. This
class is used to organize the instances of a class, defined by the generic type. Manager<T>
extends ArrayList<T>
, allowing full access and manipulation of the contents. Manager also
provides a method which returns entries in the Manager based on class. (get(Class)
).
public final class ManagerExample extends Manager<String> {
public ManagerExample(String name) {
super(name);
String entry = "Test";
// Add an entry to this manager
this.add(entry);
// Remove the entry from this manager
this.add(entry);
// Clear all entries from this manager
this.clear();
// Remove duplicate entries from this manager
Set<String> nonDuplicateSet = new LinkedHashSet<>(this);
this.clear();
this.addAll(nonDuplicateSet);
}
}
Create a new class that extends Manager<Module>
and implement the default methods. In order
to register a new module, an instance of it must be created and added in the load()
method
of the Module Manager.
public final class ExampleModManager extends Manager<Module> {
public ExampleModManager() {
super("Module");
}
@Override
public final void load() {
this.addAll(
new SomeModule()
);
}
@Override
public final void save() {
}
}
In your main class, create an instance of the new Module Manager class and call the load()
method.
public final class ExampleClient extends Client {
private static ExampleClient instance;
private ExampleModManager moduleManager;
public ExampleClient(ClientConfiguration config) {
super(config);
instance = this;
}
@Override
public final void init() {
moduleManager = new ExampleModManager();
moduleManager.load();
}
public final ExampleModManager getModuleManager() {
return this.moduleManager;
}
public static ExampleClient getInstance() {
return instance;
}
}
Event listeners are provided by Alpine and allow for any
class with valid listener fields to receive events when they are posted to the ClientAPI event
manager. (clientapi.ClientAPI#EVENT_BUS
). I will create a basic flight mod to demonstrate.
@Mod(name = "Fly", description = "Allows you to fly", bind = Keyboard.KEY_F)
public final class Fly extends Module implements IMovement {
@EventHandler
private Listener<UpdateEvent> updateListener = new Listener<>(event ->
mc.player.motionY = Boolean.compare(mc.gameSettings.keyBindJump.isKeyDown(), mc.gameSettings.keyBindSneak.isKeyDown()) * 0.2
);
}
This sets the local player's Y-axis motion to the comparator result of the jump key bind state and the sneak key bind state multiplied by 0.2.
If the jump key bind state is true, and the sneak
key bind state is false, compare(boolean, boolean)
will return 1, If the jump key bind
state is false, and the sneak key bind state is true, compare(boolean, boolean)
will
return -1, and if both of the key states are true or both are false,
compare(boolean, boolean)
will return 0.
There are many more events than just UpdateEvent
, all of which are packaged under
clientapi.event
.
Commands are a way of executing some task from in-game. For example, commands may be used to set the keybinding for a module, list all of the modules, execute a script, etc. Commands can have arguments that are used to instruct the task that is being executed by the command.
All Commands extend clientapi.command.Command
and are required to have a @Cmd()
annotation
(clientapi.command.Cmd
) or an implementation of one of the multi-parameter constructors.
In both cases, the bind parameter is optional. An example of both are below.
@Cmd(handles = { "example", "ex" }, description = "An example command")
public final class ExampleCommand extends Command {
}
public final class ExampleCommand extends Command {
public ExampleCommand() {
super(new String[] { "example", "ex", }, "An example command");
}
}
To create a new command handle, create a new method with a single ExecutionContext
parameter
(clientapi.command.execute.ExecutionContext
) and the @Sub
annotation (clientapi.command.Sub
).
Sub
is used to define the properties of the sub command, which are execution handles, argument
names, and a description.
The handles parameter is used to identify the handle from the first execution argument. If no handles are defined by a handle, it is assumed that the first given argument for execution directly correlates with the first argument of the method, excluding the Execution Context.
The arguments paremeter is used to give a name to each argument parameter. It is not a requirement if the environment that it is running in does not have obfuscated method parameters.
java.util.Optional
can be used to define optional parameters. As of the current revision, there
can only be a single optional parameter per handle, and it must be the last parameter.
Here is an example of a command with a single default handle...
@Cmd(handles = { "example", "ex" }, description = "An example command")
public final class ExampleCommand extends Command {
@Sub(arguments = { "arg1", "arg2" }, description = "An example command handle")
private void execute(ExecutionContext context, String required, Optional<String> optional) {
// Print out the executor of the command, and both arguments. If the
// optional parameter contains no value, interpret it as "undefined".
mc.ingameGUI.getChatGUI().printChatMessage(
new ChatBuilder()
.append(String.format("%s executed the ", context.sender().getName()), TextFormatting.GRAY)
.append("\"Test\"", TextFormatting.WHITE)
.append(" command with argument(s) ", TextFormatting.GRAY)
.append(required, TextFormatting.WHITE)
.append(" and ", TextFormatting.GRAY)
.append(optional.orElse("undefined"), TextFormatting.WHITE)
.build());
}
}
The syntax would be as follows
<arg1> [arg2]
A command with multiple handles may look like...
@Cmd(handles = { "example", "ex" }, description = "An example command")
public final class ExampleCommand extends Command {
@Sub(handles = { "add" }, arguments = { "element" }, description = "Adds an element")
private void add(ExecutionContext context, String elementName) {
// Add the element
}
@Sub(handles = { "remove" }, arguments = { "element" }, description = "Removes an element")
private void remove(ExecutionContext context, String elementName) {
// Remove the element
}
}
The syntax would be as follows
add <element>
remove <element>
Create a new class that extends Manager<Command>
and implement the default methods. In order
to register a new command, an instance of it must be created and added in the load()
method
of the Module Manager.
An instance of clientapi.command.CommandHandler
is required for the execution of commands, it
is responsible for parsing command arguments and carrying out execution. In order for the
command handler to understand how to parse certain parameter types, they must be specified.
ClientAPI provides a handful of parsers that cover all primitive types and additional ones (Such as
java.util.Optional
) that are used for features in the command system.
public final class ExampleCommandManager extends Manager<Command> {
private final CommandHandler handler = new CommandHandler(this);
public ExampleCommandManager() {
super("Command");
}
@Override
public final void load() {
// Register all of the default parsers
handler.registerParser(new BlockParser());
handler.registerParser(new BooleanParser());
handler.registerParser(new CharParser());
handler.registerParser(new CommandParser(ExampleClient.getInstance().getCommandManager()));
handler.registerParser(new ModuleParser(ExampleClient.getInstance().getModuleManager()));
handler.registerParser(new NumberParser());
handler.registerParser(new OptionalParser());
handler.registerParser(new StringParser());
// Subscribe the handler and a chat command listener to the event bus
ClientAPI.EVENT_BUS.subscribe(
handler,
new ChatCommandListener(handler)
);
}
@Override
public final void save() {
}
}
In your main class, create an instance of the new Command Manager class and call the load()
method.
public final class ExampleClient extends Client {
private static ExampleClient instance;
private Manager<Module> moduleManager;
private Manager<Command> commandManager;
public ExampleClient(ClientConfiguration config) {
super(config);
instance = this;
}
@Override
public final void init() {
moduleManager = new ExampleModManager();
moduleManager.load();
commandManager = new ExampleCommandManager();
commandManager.load();
}
public final Manager<Module> getModuleManager() {
return this.moduleManager;
}
public final Manager<Command> getCommandManager() {
return this.commandManager;
}
public static ExampleClient getInstance() {
return instance;
}
}
For parsing parameter types that aren't covered by the ClientAPI default parsers, or you want to effectively replace one of the ClientAPI parsers, it is very simple to do so.
All argument parsers implement clientapi.command.executor.argument.ArgumentParser
. There are 2
methods that must be implemented, parse(ExecutionContext, Type, String)
and isTarget(Type)
.
The parse
method is invoked only if the isTarget
method returns true for a specified type.
Let's create a parser for hex colors as an example.
public final class ColorParser implements ArgumentParser<Color> {
@Override
public final Color parse(ExecutionContext context, Type type, String raw) {
// Remove all non-hexadecimal characters
raw = raw.replaceAll("([^0-9a-fA-F])", "");
// If there aren't either 24 or 32 bits, return null
if (raw.length() != 6 && raw.length() != 8) {
return null;
}
// If there are only 24 bits, set the first 8 (alpha) to equal 255.
if (raw.length() == 6) {
raw = "FF" + raw;
}
return new Color(Integer.parseInt(raw, 16), true);
}
@Override
public final boolean isTarget(Type type) {
return type instanceof Class && Color.class.isAssignableFrom((Class) type);
}
}