-
Notifications
You must be signed in to change notification settings - Fork 0
API
Listeners for modules are registered in the Module#enable(EnableContext) method:
@Override
public void enable(EnableContext context) {
context.listeners().register(
new ExampleListener()
);
}Commands for modules work through the PaperMC's Command API and are also registered in the Module#enable(EnableContext) method. An example of good registration process:
package docs.example;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
public final class ExampleCommand {
private ExampleCommand() {}
public static LiteralCommandNode<CommandSourceStack> bootstrap() {
return Commands.literal("example") // Name of the command
.requires(source -> source.getSender() instanceof Player) // Requires sender of the command to be a player
.then(Commands.argument("maximum", IntegerArgumentType.integer(1)) // Adds an integer argument to the command
.executes(context -> { // Executes only when argument is passed
int maximum = context.getArgument("maximum", int.class);
int randomValue = (int) (Math.random() * maximum);
context.getSource().getSender().sendMessage(Component.text(String.format(
"Random value from 0 to %s: %s",
maximum,
randomValue
)));
return 1;
}))
.build();
}
}@Override
public void enable(EnableContext context) {
context.commands().register(
ExampleCommand.bootstrap()
);
}Hitori provides a configuration API based on SectionScheme and Field. A module defines its configuration as a Java object hierarchy, where SectionScheme represents configuration sections and Field<T> represents individual configurable values.
A module's root configuration should extend SectionScheme. Nested SectionScheme classes can be used to group related settings:
package docs.example;
import su.hitori.api.configuration.Field;
import su.hitori.api.configuration.SectionScheme;
public final class ExampleConfiguration extends SectionScheme {
public final Chat chat = new Chat();
public final Storage storage = new Storage();
public static final class Chat extends SectionScheme {
public final Field<Boolean> enabled = Field.create(true);
public final Field<String> format = Field.create("<green>%message%</green>");
}
public static final class Storage extends SectionScheme {
public final Field<String>
address = Field.create("ws://localhost:80"),
user = Field.create("root"),
password = Field.create("root");
}
}Each Field is initialized with a default value using Field.create(...). The Java field structure determines the structure of the resulting configuration.
For example, the configuration above can be represented as:
chat:
enabled: true
format: "<green>%message%</green>"
storage:
address: "ws://localhost:80"
user: "root"
password: "root"Configuration values can be grouped into arbitrarily deep sections by nesting additional SectionScheme classes.
A configuration is registered through EnableContext#configurations() in the module's enable method:
@Override
public void enable(EnableContext context) {
context.configurations().register(HitoriConfiguration.create(
Key.key("example", "main"),
configuration,
ConfigurationSource.file(
YAMLSerializer.INSTANCE,
defaultConfig()
)
));
}A typical module keeps the configuration object as a field:
private final ExampleConfiguration configuration = new ExampleConfiguration();The same instance is then passed to HitoriConfiguration.create(...) and to the parts of the module that need to access configuration values.
The first argument to HitoriConfiguration.create(...) is the configuration's Key:
Key.key("example", "main")The key identifies the configuration within Hitori. Use the module's namespace for the first component and a descriptive name for the second.
ConfigurationSource.file(...) configures the configuration to be loaded from a file:
ConfigurationSource.file(
YAMLSerializer.INSTANCE,
defaultConfig()
)YAMLSerializer.INSTANCE specifies YAML serialization, while defaultConfig() supplies the path to the default configuration.
A complete example based on a real module:
public final class MainModule extends Module {
private final MainConfiguration configuration = new MainConfiguration();
@Override
public void enable(EnableContext context) {
context.configurations().register(HitoriConfiguration.create(
Key.key("modoru", "main"),
configuration,
ConfigurationSource.file(
YAMLSerializer.INSTANCE,
defaultConfig()
)
));
// The same configuration instance can now be passed
// to other components of the module.
packRemote = new PackRemote(configuration, executorService, packProcessor, folder().toFile());
}
}Field<T> is generic, so its type corresponds to the value stored in the field:
public final Field<Boolean> enabled = Field.create(true);
public final Field<String> address = Field.create("ws://localhost:80");Configuration sections and fields can then be accessed through the normal Java object hierarchy:
configuration.chat.enabled
configuration.chat.format
configuration.storage.addressThis makes configuration access type-safe and avoids using string paths throughout the module code.
The following is a more complete example showing nested sections and multiple field types:
public final class MainConfiguration extends SectionScheme {
public final Chat chat = new Chat();
public final StorageClient storageClient = new StorageClient();
public final PackRemote packRemote = new PackRemote();
public static final class Chat extends SectionScheme {
public final PrivateMessages privateMessages = new PrivateMessages();
public static final class PrivateMessages extends SectionScheme {
public final Field<String>
remoteReceiverFormat = Field.create(
"<color:#479dff><hover:show_text:\"<lang:modoru.main.remote_message_hover:'<aqua>%original_client%':'%delay%'>\">ℹ</hover> [%sender_name% » I]:</color> <white><click:suggest_command:'/tell %sender_name% '>%message%</white>"
),
receiverFormat = Field.create(
"<color:#479dff>[%sender_name% » I]:</color> <white><click:suggest_command:'/tell %sender_name% '>%message%</white>"
),
senderFormat = Field.create(
"<color:#47ff8e>[I » %receiver_name%]:</color> <white><click:suggest_command:'/tell %receiver_name% '>%message%</white>"
),
noRecentMessage = Field.create(
"<lang:modoru.main.no_recent_message>"
);
}
}
public static final class StorageClient extends SectionScheme {
public final Field<String>
address = Field.create("ws://localhost:80"),
user = Field.create("root"),
password = Field.create("root");
}
public static final class PackRemote extends SectionScheme {
public final Field<Boolean>
enabled = Field.create(true),
checkOnStart = Field.create(true),
deleteOldRevisions = Field.create(true);
public final Field<String>
token = Field.create(""),
repo = Field.create("modoruru/assets"),
branch = Field.create("release");
}
}This structure produces logical configuration sections corresponding to the Java hierarchy:
chat:
privateMessages:
remoteReceiverFormat: "..."
receiverFormat: "..."
senderFormat: "..."
noRecentMessage: "..."
storageClient:
address: "ws://localhost:80"
user: "root"
password: "root"
packRemote:
enabled: true
checkOnStart: true
deleteOldRevisions: true
token: ""
repo: "modoruru/assets"
branch: "release"The important pattern is that sections are represented by SectionScheme instances, while values are represented by Field<T> instances. This allows the configuration to mirror the module's domain model directly while remaining serializable by the selected configuration serializer.
Caution
This section is outdated! It's only valid for hitori versions 1.x.x.
- Hard dependency: Module A depends on module B. If module B is not present, module A will not start. Module A will only enable after module B is enabled. However, accessing module B classes inside module A's enable method is not safe.
-
Optional dependency: Module A can work without module B, but when B is present, module A wants to execute additional logic. In this case, module A can register an
enable hook— logic that will execute once module B is enabled. -
Handling enable hooks: Module B can handle other modules' enable hooks using
CompletableFuturepresent inEnableContextby the nameenableHooksFuture. This future is completed once all enable hooks from other modules has finished executing. It is also completed when no enable hooks is present.
@Override
public void setupCompatibility(CompatibilityLayer compatibilityLayer) {
compatibilityLayer.require(Key.key("hitori", "resourcepack"));
}@Override
public void setupCompatibility(CompatibilityLayer compatibilityLayer) {
Key resourcepackModuleKey = Key.key("hitori", "resourcepack");
compatibilityLayer.addEnableHook(resourcepackModuleKey, () -> {
PackModule packModule = Hitori.instance().moduleRepository()
.<PackModule>getUnsafe(resourcepackModuleKey)
.orElse(null);
if(packModule == null);
// use API of resourcepack module...
});
}@Override
public void enable(EnableContext context) {
context.enableHooksFuture().thenAccept(_ -> {
// Executed when all enable hooks for this module has finished executing
});
}