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.