Skip to content

For developers EN

Иван edited this page Sep 8, 2026 · 3 revisions

Gradle

repositories {
	mavenCentral()
	...
}

dependencies {
	...
	implementation 'io.github.sawfowl:synapse-api:1.0.0'
}

The main class for accessing API services is sawfowl.synapse.api.Synapse

Creating localizations for your plugin.

Use the LocaleService. First, you need to create your collection of localizations. Please note that this will check for the presence of previously created localization files and attempt to load them if they exist. Once the server is loaded, Synapse will start monitoring changes in the localization files, so it is recommended to create them as early as possible.

@Plugin(
    id = "pluginid",
    authors = {"You"},
    dependencies = {
        @Dependency(id = "synapse")
    },
    version = "1.0.0"
)
public class Main {

    private LocalesList<T> locales; // T - Your serializable localization class. If you don’t want to use serializable classes, you can specify `Translation` or `?`. You can specify your interface or abstract class for your localization. Your class must extend the Translation interface.

    @Inject
    public Main(ProxyServer server, @DataDirectory Path dataDirectory, PluginContainer container) {
        // Some code of yours.
        locales = LocaleService.get().createLocales(container, LocaleConfig.class); // `LocaleConfig.class` - Your serializable localization config. If you are not using it, specify `null`. It is not allowed to specify an interface or an abstract class here.
        // The option with a serializable configuration class.
        // You can specify a class or create an object of that class.
        // Be sure to create a default localization — Locales.DEFAULT.
        // You can also choose one of seven configuration loader options. Please note that not all of them support comments. The server administrator can override your choice via the Synapse configuration.
        // When using serializable classes, there will be no difference in performance between different configuration formats.
        if(!locales.contains(Locales.DEFAULT)) locales.createReferencedTranslation(ConfigTypes.GEYSER_YAML, Locales.DEFAULT, LocaleConfig.class);
        // Getting the config. You will receive an instance of your serializable class.
        locales.getAsReferenced(Locales.DEFAULT);
        // A variant without serializable configuration classes
        if(!locales.contains(Locales.DEFAULT)) {
            PluginLocale locale = locales.createSimpleTranslation(ConfigTypes.GEYSER_YAML, Locales.DEFAULT);
            // You will need to fill in your configuration.
            // The `addIfNotExist` method will add an object if the section does not exist.
            // The section is specified last; in this case, it is an array of strings: `"Config", "Path"`.
            locale.addIfNotExist(Component.text("Какой-то текст"), "Комментарий", "Config", "Path");
            // Getting an object. Use the method that is more convenient for you.
            // Please note that when using this approach to working with the configuration, you will perform object deserialization each time, which may slightly reduce the performance of your code.
            Component var1 = locale.getComponent("Config", "Path");
            try {
                Component var2 = locale.getRootNode().node("Config", "Path").get(Component.class);
            } catch (SerializationException e) {
                e.printStackTrace();
            }
        }
        // Getting the localization config and necessary objects without serializable classes.
        locales.getSimple(Locales.DEFAULT);
        Component var1 = locale.getComponent("Config", "Path");
        // Some code of yours.
    }

}

Creating a configuration for your plugin.

If you use the @LocalizedComment annotation for comments in the serializable class of your configuration, you must first create the localizations; otherwise, the comments will not be generated.
The configuration service also allows you to create virtual configs, which can then be converted to a string. They are usually used for writing to a database or something similar.

@Plugin(
    id = "pluginid",
    authors = {"You"},
    dependencies = {
        @Dependency(id = "synapse")
    },
    version = "1.0.0"
)
public class Main {

    private ReferencedConfig<SerializableConfig> referencedConfig; // Config - Your serializable class.
    private Config config; // Simple config.

    @Inject
    public Main(ProxyServer server, @DataDirectory Path dataDirectory, PluginContainer container) {
        // Some code of yours.
        // Creating a configuration with a serializable class.
        referencedConfig = ConfigurationService.get()
                               .createReferencedConfig(container, SerializableConfig.class)
                               .setPath(dataDirectory)
                               .setName("Config")
                               // Here you must choose one of the seven configuration loader options. Please note that not all of them support comments. The server administrator can override your choice via the Synapse configuration.
                               .setType(ConfigTypes.GEYSER_YAML)
                               .build();
        // Getting an instance of your serializable configuration class.
        referencedConfig.get();
        // Creating a simple config without serializable classes. In some cases, this may be more convenient.
        // Filling in a simple config is done in the same way as in the example with localization without serializable classes.
        config = ConfigurationService.get()
                     .createSimpleConfig(container)
                     .setName("Config")
                     .setPath(configDir)
                     .setType(ConfigTypes.GEYSER_YAML)
                     .build();
        // Some code of yours.
    }


}

Using BuilderService

This service allows you to register your object constructors. You can look at an example of registering your builder in the Synapse code. Please note that Supplier must always return a new instance of your builder.


CallbackSevice

You don’t need to access this service directly. All you need is to use the Callback and Pagination interfaces. The Callback interface will allow you to make your message clickable, executing your arbitrary code on the proxy server. This can be useful if you want to request confirmation from the player for some action or simply add some buttons to the chat. You can make the action one‑time or set conditions for its availability.
The Pagination interface allows you to create entire multi‑page menus or reference messages. You can combine it with Callback so that the lines perform certain specific actions. The menu can also be single‑page, without displaying page numbers and scrolling arrows, if the number of provided lines is less than the limit you set for a specific menu. Do not use a limit of 0 lines or a negative value!
Absolutely all clickable text loses its functionality 10 minutes after this functionality is added to it.

Examples of usage:

void exampleCallback() {
    Player player = ...;
    Component component = Component.text("Some text").clickEvent(Callback.of(source -> {
        // Your code that will be executed if the player clicks on the message.
    }));
    player.sendMessage(component);
}

void examplePagination() {
    Player player = ...;
    Component component1 = Component.text("Some text").clickEvent(Callback.of(source -> {
        // Your code that will be executed if the player clicks on the message.
    }));
    Component component2 = Component.text("Какой-то текст").clickEvent(Callback.of(source -> {
        // Your code that will be executed if the player clicks on the message.
    }));
    Pagination.builder(10)
        .header(Component.text("Heading"))
        .padding('=', TextColor.color(120, 120, 120))
        .content(Arrays.asList(component1, component2))
        .build()
        .sendTo(player);
}

LoggerService

This service contains only 2 methods for creating loggers with a small number of methods and support for converting colors into console variants. There’s no point in providing examples for it.


PlaceholderService

This service allows you to modify text in messages without constantly listing what needs to be replaced. Unlike the classic PlaceholderAPI, when using this service, you need to specify each time the objects from which the data for text replacement will be obtained. This can be useful if the object from which data needs to be obtained is not the one that PlaceholderAPI will use by default. Essentially, this is an addition to the Text interface, which exists to simplify working with text, including performing multiple replacements. An example of registering placeholders can be seen in the Synapse code. You can also see the default placeholder options there to apply them in your text. Example of use:

void exampleUsePlaceholders() {
    Player player1 = ...;
    Player player2 = ...;
    YourObject yourObject = ...;
    Component component = ...;
    Component altReplace = Component.text("n/a"); // Alternative text for the replay, if the placeholder supports its use and the condition for its application is met. You can use an empty `Component`.
    // In this example, the data for text substitution will be taken from the player2 and yourObject objects. For yourObject, a placeholder must be registered. For player2, the placeholder is registered by default.
    player1.sendMessage(PlaceholderService.get().apply(component, altReplace, player2, yourObject).get());
}

Using ServiceProvider.

This interface allows you to access services registered in Synapse. By default, the following services are registered: BuilderService, CallbackSevice, CommandService, ConfigurationService, LocaleService, LoggerService, PlaceholderService. ProxyServer is also registered as a service, so you don’t need to manually save a reference to it when loading your plugin.
Synapse also has an interface for the economy service — EconomyService. But it has no implementation. It is assumed that another plugin will create an implementation for synchronizing the economy between servers and the proxy server. EconomyService is partially similar to the economy API in SpongeAPI. To access the ServiceProvider, use the main API class — Synapse. An example of service registration can be seen in the Synapse code. An example of obtaining a service can be seen in any of the default service interfaces or in the main Synapse class. Example of a service registration listener registration:

@Plugin(
    id = "pluginid",
    authors = {"You"},
    dependencies = {
        @Dependency(id = "synapse")
    },
    version = "1.0.0"
)
public class Main {

    private EconomyService economyService;

    @Inject
    public Main(ProxyServer server, @DataDirectory Path dataDirectory, PluginContainer container) {
        // Some code of yours.
        // As an example, an unrealized economic service was chosen.
        Synapse.getInstance().getServiceProvider().registerPendingListener(EconomyService.class, economy -> {
            	economyService = economy;
        });
        // Next, you can use the economy service.
        // It shouldn’t matter to you whether the listener is registered before or after the third‑party service is registered.
        // However, this was not tested at the time the plugin was written. If a problem arises, please let me know about it.
        // You can also try to access the service directly without a listener, but if the registration is completed after your attempt to access it, you won’t get anything.
    }
}

Command registration and information about CommandService

The CommandService is usually not required to be used, but with its help you can attempt to retrieve a command registered in Synapse, as well as cancel a deferred command execution by the player. To cancel a deferred execution, you do not need to attempt to retrieve the command itself. This service also provides access to some pre‑created command arguments. All these arguments are listed in the Argument interface as static objects. You can also see examples of argument creation in it.
Example of command registration:

void exampleCommandRegistration() {
    Currency currency = ...; // The currency from EconomyService.
    SynapseBrigadierCommand.builder("proxybroadcast", container)
        .canUse(source -> source.hasPermission("permission") // In this example, a permission check is performed to execute the command. You can also perform other checks, for example, whether the executor is a player.
        .setAliases("alias1", "alias2") // An array of command aliases.
        .setSettings(
            CommandSettings.builder()
                .setCooldown(5) // Rollback time before reuse.
                .setIgnoreCooldown("permission") // Allows you to ignore the cooldown.
                .setDelay(5) // A delay before executing the command code.
                .setIgnoreDelay("permission") // Allows you to ignore the execution delay.
                .setPrice( // You don’t have to add it if there’s no economy or you don’t plan to use it.
                    CommandPrice.of(
                        currency, // The currency in which the funds will be deducted from the balance.
                        BigDecimal.valueOf(2.17), // The amount of currency being written off.
                        "ignorePermission" // A permission that will allow you to ignore the deduction of money from the balance.
                    )
                )
                .build()
        )
        .setArguments( // Array of arguments. Their input will correspond to the order in the array. However, if you add an optional argument first and then a required one, they will be swapped. Required arguments are always entered first. Optional arguments can be skipped if the input is incorrect, provided that they do not imply the need to enter anything specific.
            Argument.createComponent(
                "Message", // Argument name. Used to identify it. Must always be unique.
                false // Is the argument mandatory?
            )
        )
        .setChilds(...) // An array of child commands, if you need them.
        .setExecutor((command, context) -> { // command is a reference to the object of the command you created and provides you with access to parsing the arguments specifically for this command and some other functions. Using your own command executor implementation gives you access to the default methods of its interface.
            Optional<Component> message = command.<Component>getArgument(context, "Message"); // Getting the entered argument. Before this stage, the availability of required arguments is checked, which gives you the opportunity to get them from Optional immediately.
            if(!message.isPresent()) throw new CommandException("You need to enter a message"); // You can use such exceptions to interrupt the execution of a command and simultaneously send a message to the person who entered the command.
// An interrupted command execution will not trigger a cooldown, unlike a successful execution. This does not output a stack trace to the console.
            // Your code.
            return command.success(); // Always use this or return a number greater than 0 when the command is executed successfully. This affects the operation of cooldowns. 
        })
        .build() // Command assembly. This will be the final method of creating it if you are creating a child command.
        .register(); // Command registration. This will be the final method of creating it if you want to register it.
}

The SynapseBrigadierCommand interface also contains other methods that may be useful to you. For example, you can unregister the command at any time that is convenient for you. If you save a reference to it, you can re‑register it without rebuilding. You cannot create a command unless you add an executor to it or child commands, to which the same requirement applies.
You can create your own arguments with the data types you need, but keep in mind that the data type being entered is always either a primitive or a string. You cannot change this without modifying the game client. To bypass this limitation, you can create a parser, which can also be configured with a condition to perform parsing, although conditions are usually not required. You can view the allowed data types in the Velocity API.