Skip to content

player-homes

Per-player homes: /sethome remembers where you stand, /home teleports you back. Where join-leave-messages shows the smallest wiring, this example shows the lifecycle: a component that loads state from disk before anything uses it, saves it on shutdown, and aborts startup when the data is unreadable. The full source lives in examples/bukkit/player-homes.

The plugin main

HomesPlugin is just the registry hook: the commands bind themselves through @CommandHandler, so there is nothing to attach in onWeftEnable, see listeners and commands.

package org.weftkit.examples.homes;

import org.weftkit.wiring.Registry;
import org.weftkit.wiring.bukkit.WeftPlugin;
import org.weftkit.wiring.registry.ComponentRegistry;

@Registry
public final class HomesPlugin extends WeftPlugin {

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

The loader

HomesFile owns homes.yml. As a Loader it reads the file in load, returning false on a corrupt file so the plugin never runs with silently empty homes, and saves it back in unload. Its @Provides getter exposes the parsed YamlConfiguration as an injectable value, captured right after load. The class stays package-private, only the provided value leaves the package.

package org.weftkit.examples.homes.storage;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import org.weftkit.wiring.Loader;
import org.weftkit.wiring.Provides;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;

@Wired
@Singleton
final class HomesFile implements Loader {

    private final JavaPlugin plugin;

    private final File file;

    private YamlConfiguration homes;

    HomesFile(JavaPlugin plugin) {
        this.plugin = plugin;
        this.file = new File(plugin.getDataFolder(), "homes.yml");
    }

    @Override
    public boolean load() {
        YamlConfiguration loaded = new YamlConfiguration();
        try {
            if (file.exists()) loaded.load(file);
        } catch (IOException | InvalidConfigurationException ex) {
            plugin.getLogger().log(Level.SEVERE, "homes.yml is unreadable, refusing to start", ex);
            return false;
        }
        homes = loaded;
        return true;
    }

    @Override
    public void unload() {
        try {
            homes.save(file);
        } catch (IOException ex) {
            plugin.getLogger().log(Level.SEVERE, "Could not save homes.yml", ex);
        }
    }

    @Provides
    YamlConfiguration homes() {
        return homes;
    }
}

The store

HomeStore injects that provided configuration and turns it into a typed set and find API. Because it depends on the provided value, it is guaranteed to load after HomesFile, no ordering annotations needed.

package org.weftkit.examples.homes.storage;

import org.bukkit.Location;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;

@Wired
@Singleton
public final class HomeStore {

    private final YamlConfiguration homes;

    public HomeStore(YamlConfiguration homes) {
        this.homes = homes;
    }

    public void set(Player player) {
        homes.set(player.getUniqueId().toString(), player.getLocation());
    }

    public Location find(Player player) {
        return homes.getLocation(player.getUniqueId().toString());
    }
}

The commands

SetHomeCommand and HomeCommand are CommandExecutor components injecting the store, each @CommandHandlered to its plugin.yml command so the bundled command module binds it during enable.

package org.weftkit.examples.homes.commands;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.weftkit.examples.homes.storage.HomeStore;
import org.weftkit.wiring.Singleton;
import org.weftkit.wiring.Wired;
import org.weftkit.wiring.bukkit.commands.CommandHandler;

@Wired
@Singleton
@CommandHandler("sethome")
public final class SetHomeCommand implements CommandExecutor {

    private final HomeStore homes;

    public SetHomeCommand(HomeStore homes) {
        this.homes = homes;
    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("Only players have homes.");
            return true;
        }
        homes.set(player);
        player.sendMessage(ChatColor.YELLOW + "Home set.");
        return true;
    }
}

Homes are held in memory while the server runs and written once on disable. A real plugin might save on every change instead; the lifecycle hooks stay the same.

Run it

Build the example as described in the overview, drop the jar into a server's plugins/ folder, run /sethome, wander off, and /home brings you back.

To see the aborted startup, put garbage into plugins/PlayerHomes/homes.yml and restart: the plugin logs the parse failure and disables itself instead of coming up empty.