Skip to content

announcer

Rotating server broadcasts. Each broadcast line is its own wired component, and a module discovers them all, runs the timer, and tears it down again. This is the example for @WeftModule, Weft.all, @ModuleSettings, and @FeatureFlag, including an optional dependency on a flagged component. The full source lives in examples/bukkit/announcer.

The packages follow the module split: broadcast is the engine, written as a library would ship it, announcements is this plugin's content, and neither knows the other's classes beyond the Announcement interface.

The plugin main

AnnouncerPlugin joins the module through @Registry(modules = AnnouncerModule.class). Bundled adapter modules need no such declaration, plugin-local and third-party ones do.

package org.weftkit.examples.announcer;

import org.weftkit.examples.announcer.broadcast.AnnouncerModule;
import org.weftkit.wiring.Registry;
import org.weftkit.wiring.bukkit.WeftPlugin;
import org.weftkit.wiring.registry.ComponentRegistry;

@Registry(modules = AnnouncerModule.class)
public final class AnnouncerPlugin extends WeftPlugin {

    @Override
    protected ComponentRegistry registry() {
        return WeftWiring.INSTANCE;
    }
}

The extension point

Announcement is one method, one broadcast line.

package org.weftkit.examples.announcer.broadcast;

/** One line in the broadcast rotation, discovered through {@code Weft.all}. */
public interface Announcement {

    String text();
}

The module

AnnouncerModule implements ModuleLifecycle. In activate it enumerates every Announcement with weft.all, the one capability constructor injection cannot express, and starts a repeating task; deactivate cancels it. Adding a broadcast line is adding a class, nothing else changes.

package org.weftkit.examples.announcer.broadcast;

import java.util.List;
import org.bukkit.ChatColor;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import org.weftkit.wiring.ModuleLifecycle;
import org.weftkit.wiring.WeftModule;
import org.weftkit.wiring.runtime.Weft;

/** Broadcasts the wired {@link Announcement} components in rotation while active. */
@WeftModule(id = AnnouncerModules.BROADCASTS)
public final class AnnouncerModule implements ModuleLifecycle {

    private final JavaPlugin plugin;

    private final Weft weft;

    private final AnnouncerSettings settings;

    private BukkitTask task;

    private int next;

    public AnnouncerModule(JavaPlugin plugin, Weft weft, AnnouncerSettings settings) {
        this.plugin = plugin;
        this.weft = weft;
        this.settings = settings;
    }

    @Override
    public boolean activate() {
        List<Announcement> rotation = weft.all(Announcement.class);
        if (rotation.isEmpty()) return true;
        task = plugin.getServer().getScheduler().runTaskTimer(plugin,
                () -> broadcast(rotation), settings.intervalTicks(), settings.intervalTicks());
        return true;
    }

    @Override
    public void deactivate() {
        if (task == null) return;
        task.cancel();
        task = null;
    }

    private void broadcast(List<Announcement> rotation) {
        Announcement announcement = rotation.get(next);
        next = (next + 1) % rotation.size();
        plugin.getServer().broadcastMessage(ChatColor.GOLD + announcement.text());
    }
}

AnnouncerModules holds the module id, so @Registry(disable = AnnouncerModules.BROADCASTS) can switch the module off without importing it.

package org.weftkit.examples.announcer.broadcast;

/** Ids of this plugin's modules, for {@code @Registry(disable = ...)} without importing them. */
public final class AnnouncerModules {

    /** The broadcast rotation of {@link AnnouncerModule}. */
    public static final String BROADCASTS = "announcer:broadcasts";

    private AnnouncerModules() {}
}

The settings

AnnouncerSettings is the module's @ModuleSettings interface. No implementation is wired, so the processor generates one that keeps the one minute default. Wire a component implementing it to change the pace.

package org.weftkit.examples.announcer.broadcast;

import org.weftkit.wiring.ModuleSettings;

/** Configuration for {@link AnnouncerModule}. No implementation is wired, the defaults apply. */
@ModuleSettings
public interface AnnouncerSettings {

    /** Ticks between two broadcasts. */
    default long intervalTicks() {
        return 20L * 60;
    }
}

The content

announcements/ holds the broadcast lines, each a package-private component the module only ever sees through enumeration. SeasonalAnnouncement is gated by @FeatureFlag: while the flag is off the component is never constructed and weft.all skips it.

package org.weftkit.examples.announcer.announcements;

import org.weftkit.examples.announcer.broadcast.Announcement;
import org.weftkit.wiring.FeatureFlag;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;

/** Only exists while its flag is on, see ConfigFlags and config.yml. */
@Wired
@Singleton
@FeatureFlag(value = "seasonal-broadcast", enabledByDefault = false)
final class SeasonalAnnouncement implements Announcement {

    @Override
    public String text() {
        return "The harvest event runs all week, bring a hoe.";
    }
}

Depending on a flagged component

HeadlineAnnouncement reaches SeasonalAnnouncement directly, not through enumeration. Nothing may require a flagged component, since it may never be constructed, so the dependency is an Optional<SeasonalAnnouncement>: it holds the component while the flag is on and is empty while it is off. This is how a config-gated feature reaches a consumer that should keep working without it, with the wiring stating the optionality instead of the consumer reading the flag itself.

package org.weftkit.examples.announcer.announcements;

import java.util.Optional;
import org.weftkit.examples.announcer.broadcast.Announcement;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;

/**
 * Depends on the flagged {@link SeasonalAnnouncement} directly, rather than through enumeration. A
 * required dependency on a flagged component is rejected at compile time, so the type is
 * {@code Optional<SeasonalAnnouncement>}: it holds the component while the flag is on and is empty
 * while it is off, letting this line reuse the seasonal text when there is one and fall back when
 * there is not.
 */
@Wired
@Singleton
final class HeadlineAnnouncement implements Announcement {

    private final Optional<SeasonalAnnouncement> seasonal;

    HeadlineAnnouncement(Optional<SeasonalAnnouncement> seasonal) {
        this.seasonal = seasonal;
    }

    @Override
    public String text() {
        return seasonal
                .map(event -> "Headline: " + event.text())
                .orElse("Headline: a quiet week on the server, no events running.");
    }
}

The flags

ConfigFlags resolves flags from config.yml, which it writes out on first load. Without such a FeatureFlags component, each flag's enabledByDefault decides.

package org.weftkit.examples.announcer.flags;

import org.bukkit.plugin.java.JavaPlugin;
import org.weftkit.wiring.Loader;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;
import org.weftkit.wiring.FeatureFlags;

/** Resolves feature flags from config.yml. A flag missing there counts as off. */
@Wired
@Singleton
final class ConfigFlags implements FeatureFlags, Loader {

    private final JavaPlugin plugin;

    ConfigFlags(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public boolean load() {
        plugin.saveDefaultConfig();
        return true;
    }

    @Override
    public boolean isEnabled(String key) {
        return plugin.getConfig().getBoolean("flags." + key);
    }
}

Run it

Build the example as described in the overview, drop the jar into a server's plugins/ folder, and a broadcast appears every minute, cycling through the wired announcements.

Flip flags.seasonal-broadcast in plugins/Announcer/config.yml and restart to watch the flagged component appear and disappear from the rotation, and the headline switch between reusing the seasonal text and its fallback as its Optional dependency fills and empties.