I refactored so hard I might as well have rewritten the entire thing.

* Added `/wasteland reload` command, which was trivial as a result of the rewrite.
* Added support for using the alternative ranks via `wasteland.prefer-rank`.
* Improved configuration format.
master
James T. Martin 2020-11-20 18:38:49 -08:00
parent 2b4d8c95a2
commit 2f1fb0ca92
Signed by: james
GPG Key ID: 4B7F3DA9351E577C
52 changed files with 2019 additions and 1029 deletions

View File

@ -1,5 +1,4 @@
plugins {
id 'java'
id 'java-library'
}
@ -35,4 +34,6 @@ repositories {
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.16.4-R0.1-SNAPSHOT'
compileOnly 'com.palmergames.bukkit.towny:Towny:0.96.2.17'
compileOnly 'com.google.guava:guava:21.0'
// compileOnly 'com.google.guava:guava:27.1
}

View File

@ -0,0 +1,31 @@
package me.jamestmartin.wasteland;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
class CommandWasteland implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 1) {
sender.sendMessage("Not enough arguments.");
return false;
}
if (args.length > 1) {
sender.sendMessage("Too many arguments.");
return false;
}
if (!args[0].equals("reload")) {
sender.sendMessage("Unknown subcommand: " + args[0]);
return false;
}
sender.sendMessage("Reloading...");
Wasteland.getInstance().reload();
sender.sendMessage("Done.");
return true;
}
}

View File

@ -0,0 +1,8 @@
package me.jamestmartin.wasteland;
import org.bukkit.plugin.java.JavaPlugin;
public interface Substate {
void register(JavaPlugin plugin);
void unregister(JavaPlugin plugin);
}

View File

@ -5,126 +5,113 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.logging.Level;
import me.jamestmartin.wasteland.commands.CommandDebugSpawn;
import me.jamestmartin.wasteland.commands.CommandDebugSpawnWeights;
import me.jamestmartin.wasteland.commands.CommandOfficial;
import me.jamestmartin.wasteland.commands.CommandRank;
import me.jamestmartin.wasteland.commands.CommandRankEligibleMobs;
import me.jamestmartin.wasteland.commands.CommandRanks;
import me.jamestmartin.wasteland.commands.CommandSetKills;
import me.jamestmartin.wasteland.config.WastelandConfig;
import me.jamestmartin.wasteland.listeners.ChatListener;
import me.jamestmartin.wasteland.listeners.RankListener;
import me.jamestmartin.wasteland.spawns.WastelandSpawner;
import me.jamestmartin.wasteland.towny.TownyDependency;
import me.jamestmartin.wasteland.config.ConfigParser;
import me.jamestmartin.wasteland.store.SqliteDatabase;
import me.jamestmartin.wasteland.towny.TownyEnabled;
import me.jamestmartin.wasteland.towny.TownyDisabled;
import me.jamestmartin.wasteland.towny.TownAbbreviationProvider;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import me.jamestmartin.wasteland.towny.TownyDependency;
import org.bukkit.plugin.java.JavaPlugin;
public class Wasteland extends JavaPlugin {
private static Wasteland instance;
private Database database;
private WastelandConfig config;
private RankListener rankListener;
private TownAbbreviationProvider townyAbbreviationProvider;
private WastelandSpawner spawner;
private static TownyDependency towny;
private WastelandConfig config;
private SqliteDatabase database;
private WastelandState state;
public static Wasteland getInstance() {
return instance;
}
public Database getDatabase() {
return database;
}
public WastelandConfig getSettings() {
return config;
}
public TownAbbreviationProvider getTownyAbbreviationProvider() {
return townyAbbreviationProvider;
}
public WastelandSpawner getSpawner() {
return spawner;
}
public void updatePlayerRank(Player player) throws SQLException {
rankListener.updatePlayerRank(player);
}
private void initializeConfig() {
this.saveDefaultConfig();
this.config = new WastelandConfig(this.getConfig());
}
private void initializeTowny() {
if (Wasteland.getInstance().getServer().getPluginManager().isPluginEnabled("Towny")) {
this.townyAbbreviationProvider = new TownyDependency();
} else {
this.townyAbbreviationProvider = new TownyDisabled();
}
}
private void initializeDatabase() {
String databaseFilename = this.getConfig().getString("databaseFile");
try {
database = new Database(new File(this.getDataFolder(), databaseFilename));
} catch (IOException e) {
this.getLogger().log(Level.SEVERE, "Failed to write database file: " + e.getMessage());
this.getPluginLoader().disablePlugin(this);
} catch (ClassNotFoundException e) {
this.getLogger().log(Level.SEVERE, "You need the SQLite JBDC library. Google it. Put it in /lib folder.");
this.getPluginLoader().disablePlugin(this);
} catch (SQLException e) {
this.getLogger().log(Level.SEVERE, "SQLite exception on initialize.", e);
this.getPluginLoader().disablePlugin(this);
}
}
private void registerCommands() {
this.getCommand("rank").setExecutor(new CommandRank(config.eligibleMobsName()));
this.getCommand("rankeligiblemobs").setExecutor(new CommandRankEligibleMobs(config.eligibleMobsName(), config.eligibleMobs()));
this.getCommand("ranks").setExecutor(new CommandRanks());
this.getCommand("setkills").setExecutor(new CommandSetKills());
this.getCommand("official").setExecutor(new CommandOfficial(config.chat()));
// debug commands
this.getCommand("debugspawn").setExecutor(new CommandDebugSpawn());
this.getCommand("debugspawnweights").setExecutor(new CommandDebugSpawnWeights());
}
private void registerListeners() {
PluginManager manager = this.getServer().getPluginManager();
manager.registerEvents(new RankListener(config.eligibleMobs()), this);
manager.registerEvents(new ChatListener(config.chat()), this);
public static TownyDependency getTowny() {
return towny;
}
@Override
public void onEnable() {
instance = this;
initializeConfig();
initializeTowny();
initializeDatabase();
registerCommands();
registerListeners();
this.spawner = new WastelandSpawner(config.spawns());
if (Wasteland.getInstance().getServer().getPluginManager().isPluginEnabled("Towny")) {
towny = new TownyEnabled();
} else {
towny = new TownyDisabled();
}
initialize();
register();
}
@Override
public void onDisable() {
if (rankListener != null)
rankListener.close();
rankListener = null;
try {
if (database != null)
database.close();
} catch (SQLException e) {
this.getLogger().log(Level.SEVERE, "Failed to close database.", e);
}
// No need to unregister stuff; Bukkit will do that for us.
state = null;
deinitialize();
towny = null;
instance = null;
}
public void reload() {
reloadConfig();
reinitialize();
unregister();
register();
}
private void reinitialize() {
deinitialize();
initialize();
}
private void initialize() {
initializeConfig();
initializeDatabase();
}
private void initializeConfig() {
saveDefaultConfig();
config = ConfigParser.parseConfig(getConfig());
}
private void initializeDatabase() {
String databaseFilename = config.getDatabaseFilename();
try {
database = new SqliteDatabase(new File(this.getDataFolder(), databaseFilename));
} catch (IOException e) {
this.getLogger().log(Level.SEVERE, "Failed to write database file: " + e.getMessage());
this.getPluginLoader().disablePlugin(this);
} catch (ClassNotFoundException e) {
this.getLogger().log(Level.SEVERE, "You need the SQLite JBDC library. Google it. Put it in /lib folder.");
this.getPluginLoader().disablePlugin(this);
} catch (SQLException e) {
this.getLogger().log(Level.SEVERE, "SQLite exception on initialize.", e);
this.getPluginLoader().disablePlugin(this);
}
}
private void deinitialize() {
config = null;
try {
if (database != null)
database.close();
} catch (SQLException e) {
this.getLogger().log(Level.SEVERE, "Failed to close database.", e);
}
database = null;
}
private void register() {
state = new WastelandState(config, database);
state.register(this);
}
private void unregister() {
state.unregister(this);
state = null;
}
}

View File

@ -0,0 +1,47 @@
package me.jamestmartin.wasteland;
import me.jamestmartin.wasteland.chat.ChatConfig;
import me.jamestmartin.wasteland.kills.KillsConfig;
import me.jamestmartin.wasteland.ranks.AllRanks;
import me.jamestmartin.wasteland.spawns.SpawnsConfig;
public class WastelandConfig {
private final String databaseFilename;
private final ChatConfig chatConfig;
private final KillsConfig killsConfig;
private final AllRanks ranks;
private final SpawnsConfig spawnsConfig;
public WastelandConfig(
String databaseFilename,
ChatConfig chatConfig,
KillsConfig killsConfig,
AllRanks ranks,
SpawnsConfig spawnsConfig) {
this.databaseFilename = databaseFilename;
this.chatConfig = chatConfig;
this.killsConfig = killsConfig;
this.ranks = ranks;
this.spawnsConfig = spawnsConfig;
}
public String getDatabaseFilename() {
return databaseFilename;
}
public ChatConfig getChatConfig() {
return chatConfig;
}
public KillsConfig getKillsConfig() {
return killsConfig;
}
public AllRanks getRanks() {
return ranks;
}
public SpawnsConfig getSpawnsConfig() {
return spawnsConfig;
}
}

View File

@ -0,0 +1,53 @@
package me.jamestmartin.wasteland;
import org.bukkit.plugin.java.JavaPlugin;
import me.jamestmartin.wasteland.chat.ChatState;
import me.jamestmartin.wasteland.kills.KillsState;
import me.jamestmartin.wasteland.ranks.PermissionsPlayerRankProvider;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
import me.jamestmartin.wasteland.ranks.RanksState;
import me.jamestmartin.wasteland.spawns.SpawnsState;
import me.jamestmartin.wasteland.store.SqliteDatabase;
public class WastelandState implements Substate {
private final CommandWasteland commandWasteland;
private final ChatState chatState;
private final KillsState killsState;
private final RanksState ranksState;
private final SpawnsState spawnsState;
public WastelandState(WastelandConfig config, SqliteDatabase database) {
this.commandWasteland = new CommandWasteland();
PlayerRankProvider rankProvider = new PermissionsPlayerRankProvider(config.getRanks());
this.ranksState = new RanksState(config.getKillsConfig(), database, rankProvider);
this.chatState = new ChatState(config.getChatConfig(), rankProvider);
this.killsState = new KillsState(config.getKillsConfig(), ranksState.getPlayerKillsStore(), rankProvider);
this.spawnsState = new SpawnsState(config.getSpawnsConfig());
}
private Substate[] getSubstates() {
Substate[] substates = { chatState, killsState, ranksState, spawnsState };
return substates;
}
@Override
public void register(JavaPlugin plugin) {
plugin.getCommand("wasteland").setExecutor(commandWasteland);
for (Substate substate : getSubstates()) {
substate.register(plugin);
}
}
@Override
public void unregister(JavaPlugin plugin) {
plugin.getCommand("wasteland").setExecutor(null);
for (Substate substate : getSubstates()) {
substate.unregister(plugin);
}
}
}

View File

@ -0,0 +1,119 @@
package me.jamestmartin.wasteland.chat;
import java.util.Optional;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.ranks.PlayerRanks;
import me.jamestmartin.wasteland.ranks.Rank;
public class ChatConfig implements ChatFormatter {
private final String chatFormat;
private final String officerChatFormat;
private final String officialFormat;
private final String consoleFormat;
private final String rankPrefixFormat;
private final String townyPrefixFormat;
public ChatConfig(
final String chatFormat,
final String officerChatFormat,
final String officialFormat,
final String consoleFormat,
final String rankPrefixFormat,
final String townyPrefixFormat
) {
this.chatFormat = chatFormat;
this.officerChatFormat = officerChatFormat;
this.officialFormat = officialFormat;
this.consoleFormat = consoleFormat;
this.rankPrefixFormat = rankPrefixFormat;
this.townyPrefixFormat = townyPrefixFormat;
}
public String getChatFormat() {
return chatFormat;
}
public String getOfficerChatFormat() {
return officerChatFormat;
}
public String getOfficialFormat() {
return officialFormat;
}
public String getConsoleFormat() {
return consoleFormat;
}
public String getRankPrefixFormat() {
return rankPrefixFormat;
}
public String getTownyPrefixFormat() {
return townyPrefixFormat;
}
private String formatRankPrefix(Rank rank) {
return getRankPrefixFormat().replace("{abbr}", rank.formatAbbreviated());
}
private String formatPlayerRankPrefix(Optional<Rank> rank) {
if (rank.isEmpty()) {
return "";
}
return formatRankPrefix(rank.get());
}
private String formatPlayerTownPrefix(Player player) {
Optional<String> townTag = Wasteland.getTowny().getTownAbbreviation(player);
if (townTag.isEmpty()) {
return "";
}
return getTownyPrefixFormat().replace("{tag}", townTag.get());
}
private String substituteRankPrefixes(PlayerRanks ranks, String format) {
// This is an inefficient way to do it, but chat doesn't need optimization for now.
String enlistedRankPrefix = formatPlayerRankPrefix(ranks.getEnlistedRank().map(x -> x));
String officerRankPrefix = formatPlayerRankPrefix(ranks.getOfficerRank());
String highestRankPrefix = formatPlayerRankPrefix(ranks.getHighestRank());
return format
.replace("{enlisted}", enlistedRankPrefix)
.replace("{officer}", officerRankPrefix)
.replace("{rank}", highestRankPrefix);
}
private String substitutePlayerTownPrefix(Player player, String format) {
return format.replace("{towny}", formatPlayerTownPrefix(player));
}
private String substitutePlayerPrefixes(Player player, PlayerRanks ranks, String format) {
return substitutePlayerTownPrefix(player, substituteRankPrefixes(ranks, format));
}
@Override
public String formatPlayerChat(Player player, PlayerRanks ranks) {
return substitutePlayerPrefixes(player, ranks, getChatFormat());
}
@Override
public String formatOfficerChat(Player player, PlayerRanks ranks) {
return substitutePlayerPrefixes(player, ranks, getOfficerChatFormat());
}
@Override
public String formatOfficial(Player player, PlayerRanks ranks) {
return substitutePlayerPrefixes(player, ranks, getOfficialFormat());
}
@Override
public String formatConsole(Rank consoleRank) {
return getConsoleFormat()
.replace("{console}", consoleRank.formatAbbreviated())
.replace("{console_full}", consoleRank.formatFull());
}
}

View File

@ -0,0 +1,33 @@
package me.jamestmartin.wasteland.chat;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.ranks.PlayerRanks;
import me.jamestmartin.wasteland.ranks.Rank;
/** Creates chat format strings for players. */
interface ChatFormatter {
/**
* Calculates the chat format string for a player.
* It will contain two `%s` variables: the player's name, and the actual message.
*/
String formatPlayerChat(Player player, PlayerRanks ranks);
/**
* Calculates the officer format string for a player.
* It will contain two `%s` variables: the player's name, and the actual message.
*/
String formatOfficerChat(Player player, PlayerRanks ranks);
/**
* Calculates the `/official` format string for a player.
* It will contain two `%s` variables: the player's name, and the actual message.
*/
String formatOfficial(Player player, PlayerRanks ranks);
/**
* Calculates the `/official` format string for the console.
* It will contain just one `%s` variable: the actual message.
*/
String formatConsole(Rank consoleRank);
}

View File

@ -0,0 +1,29 @@
package me.jamestmartin.wasteland.chat;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
class ChatListener implements Listener {
private final ChatFormatter formatter;
private final PlayerRankProvider ranks;
public ChatListener(ChatFormatter formatter, PlayerRankProvider ranks) {
this.formatter = formatter;
this.ranks = ranks;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerChat(AsyncPlayerChatEvent event) {
final Player player = event.getPlayer();
if (player.hasPermission("wasteland.chat.officer")) {
event.setFormat(formatter.formatOfficerChat(player, ranks.getPlayerRanks(player)));
} else {
event.setFormat(formatter.formatPlayerChat(player, ranks.getPlayerRanks(player)));
}
}
}

View File

@ -0,0 +1,29 @@
package me.jamestmartin.wasteland.chat;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.plugin.java.JavaPlugin;
import me.jamestmartin.wasteland.Substate;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
public class ChatState implements Substate {
private final ChatListener chatListener;
private final CommandOfficial commandOfficial;
public ChatState(ChatConfig config, PlayerRankProvider rankProvider) {
this.chatListener = new ChatListener(config, rankProvider);
this.commandOfficial = new CommandOfficial(config, rankProvider);
}
@Override
public void register(JavaPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(chatListener, plugin);
plugin.getCommand("official").setExecutor(commandOfficial);
}
@Override
public void unregister(JavaPlugin plugin) {
AsyncPlayerChatEvent.getHandlerList().unregister(chatListener);
plugin.getCommand("official").setExecutor(null);
}
}

View File

@ -0,0 +1,46 @@
package me.jamestmartin.wasteland.chat;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
import me.jamestmartin.wasteland.ranks.PlayerRanks;
class CommandOfficial implements CommandExecutor {
private final PlayerRankProvider ranks;
private final ChatFormatter formatter;
public CommandOfficial(ChatFormatter formatter, PlayerRankProvider ranks) {
this.formatter = formatter;
this.ranks = ranks;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 1) {
sender.sendMessage("Don't you actually have something to say?");
return false;
}
String message = String.join(" ", args);
if (sender instanceof Player) {
Player player = (Player) sender;
PlayerRanks playerRanks = ranks.getPlayerRanks(player);
if (!playerRanks.getOfficerRank().isPresent()) {
sender.sendMessage("You are not an officer.");
return true;
}
sender.getServer().broadcastMessage(
String.format(formatter.formatOfficial(player, playerRanks), player.getDisplayName(), message));
} else {
sender.getServer().broadcastMessage(
String.format(formatter.formatConsole(ranks.getRanks().getConsoleRank()), message));
}
return true;
}
}

View File

@ -1,48 +0,0 @@
package me.jamestmartin.wasteland.commands;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.config.ChatConfig;
import me.jamestmartin.wasteland.ranks.Rank;
public class CommandOfficial implements CommandExecutor {
private final ChatConfig config;
public CommandOfficial(ChatConfig config) {
this.config = config;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 1) {
sender.sendMessage("Don't you actually have something to say?");
return false;
}
final String message = String.join(" ", args);
final String format;
if (sender instanceof Player) {
final Player player = (Player) sender;
if (!Rank.getOfficerRank(player).isPresent()) {
sender.sendMessage("You are not a staff member.");
return true;
}
format = config.getOfficialFormat(player).replaceFirst("%s", player.getDisplayName());
} else {
if (!Rank.getConsoleRank().isPresent()) {
sender.sendMessage("No console rank is configured to send messages with!");
return true;
}
format = config.getConsoleFormat();
}
sender.getServer().broadcastMessage(format.replaceFirst("%s", message));
return true;
}
}

View File

@ -1,34 +0,0 @@
package me.jamestmartin.wasteland.commands;
import java.util.Collection;
import java.util.stream.Collectors;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
public class CommandRankEligibleMobs implements CommandExecutor {
private final String eligibleMobsName;
private final Collection<EntityType> eligibleMobs;
public CommandRankEligibleMobs(String eligibleMobsName, Collection<EntityType> eligibleMobs) {
this.eligibleMobsName = eligibleMobsName;
this.eligibleMobs = eligibleMobs;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0) {
sender.sendMessage("Too many arguments!");
sender.sendMessage(command.getUsage());
return false;
}
sender.sendMessage("Killing " + eligibleMobsName + " will count towards your next promotion.");
sender.sendMessage("Specifically, any of these mobs will work: " +
eligibleMobs.stream().map(Object::toString).sorted().collect(Collectors.joining(", ")));
return true;
}
}

View File

@ -1,89 +0,0 @@
package me.jamestmartin.wasteland.config;
import java.util.Optional;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.ranks.Rank;
import me.jamestmartin.wasteland.ranks.RankType;
public class ChatConfig {
private final String chatFormat;
private final String officerChatFormat;
private final String officialFormat;
private final String consoleFormat;
private final String rankPrefixFormat;
private final String townyPrefixFormat;
public ChatConfig(ConfigurationSection c) {
ConfigurationSection formats = c.getConfigurationSection("formats");
this.chatFormat = formats.getString("chat");
this.officerChatFormat = formats.getString("officer");
this.officialFormat = formats.getString("official");
this.consoleFormat = formats.getString("console");
ConfigurationSection prefixes = c.getConfigurationSection("prefixes");
this.rankPrefixFormat = prefixes.getString("rank");
this.townyPrefixFormat = prefixes.getString("towny");
}
public String formatRankPrefix(Player player, Rank rank) {
return rankPrefixFormat.replace("{abbr}", rank.formatAbbreviated());
}
public String formatPlayerRankPrefix(Player player, RankType rankType) {
Optional<Rank> rank = Rank.getRank(rankType, player);
if (rank.isEmpty()) {
return "";
}
return formatRankPrefix(player, rank.get());
}
public String formatPlayerTownPrefix(Player player) {
Optional<String> townTag = Wasteland.getInstance().getTownyAbbreviationProvider().getTownAbbreviation(player);
if (townTag.isEmpty()) {
return "";
}
return townyPrefixFormat.replace("{tag}", townTag.get());
}
private String substituteRankPrefixes(String format, Player player) {
// This is an inefficient way to do it, but chat doesn't need optimization.
String enlistedRankPrefix = formatPlayerRankPrefix(player, RankType.ENLISTED);
String officerRankPrefix = formatPlayerRankPrefix(player, RankType.OFFICER);
String highestRankPrefix = formatPlayerRankPrefix(player, RankType.HIGHEST);
return format
.replace("{enlisted}", enlistedRankPrefix)
.replace("{officer}", officerRankPrefix)
.replace("{rank}", highestRankPrefix);
}
private String substitutePlayerTownPrefix(String format, Player player) {
return format.replace("{towny}", formatPlayerTownPrefix(player));
}
private String substitutePlayerPrefixes(String format, Player player) {
return substitutePlayerTownPrefix(substituteRankPrefixes(format, player), player);
}
public String getPlayerChatFormat(Player player) {
return substitutePlayerPrefixes(chatFormat, player);
}
public String getOfficerChatFormat(Player player) {
return substitutePlayerPrefixes(officerChatFormat, player);
}
public String getOfficialFormat(Player player) {
return substitutePlayerPrefixes(officialFormat, player);
}
public String getConsoleFormat() {
return consoleFormat
.replace("{console}", Rank.getConsoleRank().get().formatAbbreviated())
.replace("{console_full}", Rank.getConsoleRank().get().formatFull());
}
}

View File

@ -0,0 +1,290 @@
package me.jamestmartin.wasteland.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import com.google.common.graph.ImmutableValueGraph;
import com.google.common.graph.MutableValueGraph;
import com.google.common.graph.ValueGraphBuilder;
import me.jamestmartin.wasteland.WastelandConfig;
import me.jamestmartin.wasteland.chat.ChatConfig;
import me.jamestmartin.wasteland.kills.KillsConfig;
import me.jamestmartin.wasteland.ranks.AllRanks;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
import me.jamestmartin.wasteland.ranks.EnlistedRanks;
import me.jamestmartin.wasteland.ranks.Rank;
import me.jamestmartin.wasteland.ranks.Ranks;
import me.jamestmartin.wasteland.spawns.MonsterSpawnConfig;
import me.jamestmartin.wasteland.spawns.MonsterType;
import me.jamestmartin.wasteland.spawns.SpawnsConfig;
import me.jamestmartin.wasteland.util.Pair;
import me.jamestmartin.wasteland.world.MoonPhase;
public class ConfigParser {
public static WastelandConfig parseConfig(ConfigurationSection c) {
String databaseFilename = c.getString("databaseFile", "wasteland.sqlite3");
ChatConfig chatConfig = parseChatConfig(c.getConfigurationSection("chat"));
KillsConfig killsConfig = parseKillsConfig(c.getConfigurationSection("kills"));
SpawnsConfig spawnsConfig = parseSpawnsConfig(c.getConfigurationSection("spawns"));
ConfigurationSection enlistedSection = c.getConfigurationSection("enlisted");
ConfigurationSection officerSection = c.getConfigurationSection("officer");
AllRanks ranks = parseRanks(enlistedSection, officerSection);
return new WastelandConfig(databaseFilename, chatConfig, killsConfig, ranks, spawnsConfig);
}
private static ChatConfig parseChatConfig(ConfigurationSection c) {
final ConfigurationSection formats = c.getConfigurationSection("formats");
final String chatFormat = formats.getString("chat");
final String officerChatFormat = formats.getString("officer");
final String officialFormat = formats.getString("official");
final String consoleFormat = formats.getString("console");
final ConfigurationSection prefixes = c.getConfigurationSection("prefixes");
final String rankPrefixFormat = prefixes.getString("rank");
final String townyPrefixFormat = prefixes.getString("towny");
return new ChatConfig(chatFormat, officerChatFormat, officialFormat, consoleFormat, rankPrefixFormat, townyPrefixFormat);
}
private static KillsConfig parseKillsConfig(ConfigurationSection c) {
final ConfigurationSection eligibleSection = c.getConfigurationSection("eligible");
final String eligibleMobsName = eligibleSection.getString("name");
final Set<EntityType> eligibleMobs = new HashSet<>();
final List<String> eligibleMobTypes = eligibleSection.getStringList("entities");
for (final String mobType : eligibleMobTypes) {
eligibleMobs.addAll(Arrays.asList(EntityTypes.lookupEntityType(mobType)));
}
return new KillsConfig(eligibleMobsName, eligibleMobs);
}
private static AllRanks parseRanks(ConfigurationSection enlisted, ConfigurationSection officer) {
EnlistedRanks enlistedRanks = parseEnlistedRanks(enlisted);
Ranks<Rank> officerRanks = parseOfficerRanks(officer);
Rank consoleRank = officerRanks.getRank(officer.getString("console")).get();
return new AllRanks(enlistedRanks, officerRanks, consoleRank);
}
private static EnlistedRanks parseEnlistedRanks(ConfigurationSection c) {
Optional<ChatColor> defaultDecoration = readColor(c, "decoration");
Optional<String> defaultDescription = Optional.ofNullable(c.getString("description"));
List<EnlistedRank> originalOrder = new ArrayList<>();
Map<String, EnlistedRank> ranks = new HashMap<>();
Map<String, String> predecessors = new HashMap<>();
Map<String, String> preferredSuccessors = new HashMap<>();
ConfigurationSection ranksSection = c.getConfigurationSection("ranks");
for (String id : ranksSection.getKeys(false)) {
Pair<EnlistedRank, Pair<Optional<String>, Optional<String>>> parse =
parseEnlistedRank(ranksSection.getConfigurationSection(id), id, defaultDecoration, defaultDescription);
EnlistedRank rank = parse.x;
Optional<String> predecessor = parse.y.x;
Optional<String> preferredSuccessor = parse.y.y;
originalOrder.add(rank);
ranks.put(id, rank);
predecessor.ifPresent(x -> predecessors.put(id, x));
preferredSuccessor.ifPresent(x -> preferredSuccessors.put(id, x));
}
for (Entry<String, String> entry : predecessors.entrySet()) {
String rank = entry.getKey();
String predecessor = entry.getValue();
// Read: There is only one rank whose predecessor is this rank's predecessor,
// i.e. the predecessor has only this rank as a successor.
if (predecessors.values().stream().filter(predecessor::equals).count() == 1) {
// If there is only one successor, that successor will be preferred automatically.
// TODO: Allow explicitly *not* making the only successor preferred.
preferredSuccessors.put(predecessor, rank);
}
}
// The commented code should be used instead once Guava is upgraded to 28+.
// ImmutableValueGraph.Builder<EnlistedRank, Boolean> builder = ValueGraphBuilder.directed().immutable();
MutableValueGraph<EnlistedRank, Boolean> builder = ValueGraphBuilder.directed().build();
for (EnlistedRank rank : ranks.values()) {
builder.addNode(rank);
}
for (Entry<String, String> entry : predecessors.entrySet()) {
EnlistedRank successor = ranks.get(entry.getKey());
EnlistedRank predecessor = ranks.get(entry.getValue());
boolean preferred = successor.getId().equals(preferredSuccessors.get(predecessor.getId()));
builder.putEdgeValue(predecessor, successor, preferred);
}
// return new EnlistedRanks(builder.build());
return new EnlistedRanks(ImmutableValueGraph.copyOf(builder), originalOrder);
}
private static Pair<EnlistedRank, Pair<Optional<String>, Optional<String>>> parseEnlistedRank(
ConfigurationSection c,
String id,
Optional<ChatColor> defaultDecoration,
Optional<String> defaultDescription) {
final String name = c.getString("name");
final String abbr = c.getString("abbreviation");
final Optional<Integer> kills;
if (c.contains("kills")) {
kills = Optional.of(c.getInt("kills"));
} else {
kills = Optional.empty();
}
final Optional<ChatColor> color = readColor(c, "color");
final Optional<ChatColor> decoration = readColor(c, "decoration").or(() -> defaultDecoration);
final Optional<String> description =
Optional.ofNullable(c.getString("description")).or(() -> defaultDescription)
.map(descr -> kills.map(k -> descr.replace("{kills}", k.toString())).orElse(descr));
final Optional<String> predecessor = Optional.ofNullable(c.getString("succeeds"));
final Map<String, Boolean> permissionChildren = predecessor.isPresent() ? Map.of("wasteland.rank." + predecessor.get(), true) : Map.of();
final Optional<String> preferredSuccessor = Optional.ofNullable(c.getString("preferred"));
final boolean isDefault = kills.isPresent() && kills.get() == 0;
final PermissionDefault permissionDefault = isDefault ? PermissionDefault.TRUE : PermissionDefault.FALSE;
final Permission permission = new Permission("wasteland.rank." + id, permissionDefault, permissionChildren);
final Permission preferencePermission = new Permission("wasteland.prefer-rank." + id, PermissionDefault.FALSE);
final Pair<Optional<String>, Optional<String>> relatedRanks = new Pair<>(predecessor, preferredSuccessor);
return new Pair<>(new EnlistedRank(id, name, abbr, description, color, decoration, permission, kills, preferencePermission), relatedRanks);
}
private static Ranks<Rank> parseOfficerRanks(ConfigurationSection c) {
Optional<ChatColor> defaultDecoration = readColor(c, "decoration");
Optional<String> defaultDescription = Optional.ofNullable(c.getString("description"));
List<Rank> originalOrder = new ArrayList<>();
Map<String, Rank> ranks = new HashMap<>();
Map<String, String> predecessors = new HashMap<>();
ConfigurationSection ranksSection = c.getConfigurationSection("ranks");
for (String id : ranksSection.getKeys(false)) {
Pair<Rank,Optional<String>> parse =
parseOfficerRank(ranksSection.getConfigurationSection(id), id, defaultDecoration, defaultDescription);
Rank rank = parse.x;
Optional<String> predecessor = parse.y;
originalOrder.add(rank);
ranks.put(id, rank);
predecessor.ifPresent(x -> predecessors.put(id, x));
}
// The commented code should be used instead once Guava is upgraded to 28+.
// ImmutableValueGraph.Builder<Rank, Boolean> builder = ValueGraphBuilder.directed().immutable();
MutableValueGraph<Rank, Boolean> builder = ValueGraphBuilder.directed().build();
for (Rank rank : ranks.values()) {
builder.addNode(rank);
}
for (Entry<String, String> entry : predecessors.entrySet()) {
Rank successor = ranks.get(entry.getKey());
Rank predecessor = ranks.get(entry.getValue());
builder.putEdgeValue(predecessor, successor, false);
}
//return new Ranks<>(builder.build());
return new Ranks<>(ImmutableValueGraph.copyOf(builder), originalOrder);
}
private static Pair<Rank, Optional<String>> parseOfficerRank(
ConfigurationSection c,
String id,
Optional<ChatColor> defaultDecoration,
Optional<String> defaultDescription) {
final String name = c.getString("name");
final String abbr = c.getString("abbreviation");
final Optional<ChatColor> color = readColor(c, "color");
final Optional<ChatColor> decoration = readColor(c, "decoration").or(() -> defaultDecoration);
final Optional<String> description =
Optional.ofNullable(c.getString("description")).or(() -> defaultDescription);
final Optional<String> predecessor = Optional.ofNullable(c.getString("succeeds"));
final Map<String, Boolean> permissionChildren = predecessor.isPresent() ? Map.of(predecessor.get(), true) : Map.of();
final Permission permission = new Permission("wasteland.rank." + id, PermissionDefault.FALSE, permissionChildren);
return new Pair<>(new Rank(id, name, abbr, description, color, decoration, permission), predecessor);
}
private static SpawnsConfig parseSpawnsConfig(ConfigurationSection c) {
final HashMap<MonsterType, MonsterSpawnConfig> spawns = new HashMap<>();
if (c != null) {
for (String typeName : c.getKeys(false)) {
MonsterType type = MonsterType.valueOf(typeName);
MonsterSpawnConfig config = parseMonsterSpawnConfig(c.getConfigurationSection(typeName));
spawns.put(type, config);
}
}
return new SpawnsConfig(spawns);
}
private static MonsterSpawnConfig parseMonsterSpawnConfig(ConfigurationSection c) {
final double maximumLightLevel;
final double blocklightWeight;
final double sunlightWeight;
final double moonlightWeight;
if (!c.isConfigurationSection("light")) {
maximumLightLevel = c.getInt("light", 9);
blocklightWeight = 1.0f;
sunlightWeight = 1.0f;
moonlightWeight = 0.0f;
} else {
final ConfigurationSection cLight = c.getConfigurationSection("light");
maximumLightLevel = cLight.getInt("maximum", 9);
blocklightWeight = (float) cLight.getDouble("weights.block", 1.0);
sunlightWeight = (float) cLight.getDouble("weights.sun", 1.0);
moonlightWeight = (float) cLight.getDouble("weights.moon", 0.0);
}
final int minimumYLevel = c.getInt("height.minimum", 0);
final int maximumYLevel = c.getInt("height.maximum", Integer.MAX_VALUE);
final ConfigurationSection cPhases = c.getConfigurationSection("phases");
final HashMap<MoonPhase, Double> phaseMultipliers = new HashMap<>();
if (cPhases != null) {
for (String phaseKey : cPhases.getKeys(false)) {
MoonPhase phase = MoonPhase.valueOf(phaseKey);
double multiplier = cPhases.getDouble(phaseKey);
phaseMultipliers.put(phase, multiplier);
}
}
return new MonsterSpawnConfig(
maximumLightLevel,
blocklightWeight, sunlightWeight, moonlightWeight,
minimumYLevel, maximumYLevel,
phaseMultipliers);
}
/** Orphaned method. */
private static Optional<ChatColor> readColor(ConfigurationSection c, String path) {
return (Optional<ChatColor>) Optional.ofNullable(c.getString(path)).map(ChatColor::valueOf);
}
}

View File

@ -1,76 +0,0 @@
package me.jamestmartin.wasteland.config;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
import me.jamestmartin.wasteland.world.MoonPhase;
public class MonsterSpawnConfig {
private final int maximumLightLevel;
private final float blocklightWeight;
private final float sunlightWeight;
private final float moonlightWeight;
private final int maximumYLevel;
private final int minimumYLevel;
private final Map<MoonPhase, Float> phaseMultipliers;
public MonsterSpawnConfig(final ConfigurationSection c) {
if (!c.isConfigurationSection("light")) {
this.maximumLightLevel = c.getInt("light", 9);
this.blocklightWeight = 1.0f;
this.sunlightWeight = 1.0f;
this.moonlightWeight = 0.0f;
} else {
final ConfigurationSection cLight = c.getConfigurationSection("light");
this.maximumLightLevel = cLight.getInt("maximum", 9);
this.blocklightWeight = (float) cLight.getDouble("weights.block", 1.0);
this.sunlightWeight = (float) cLight.getDouble("weights.sun", 1.0);
this.moonlightWeight = (float) cLight.getDouble("weights.moon", 0.0);
}
this.minimumYLevel = c.getInt("height.minimum", 0);
this.maximumYLevel = c.getInt("height.maximum", Integer.MAX_VALUE);
final ConfigurationSection cPhases = c.getConfigurationSection("phases");
this.phaseMultipliers = new HashMap<>();
if (cPhases != null) {
for (String phaseKey : cPhases.getKeys(false)) {
MoonPhase phase = MoonPhase.valueOf(phaseKey);
float multiplier = (float) cPhases.getDouble(phaseKey);
this.phaseMultipliers.put(phase, multiplier);
}
}
}
public int maximumLightLevel() {
return maximumLightLevel;
}
public float blocklightWeight() {
return blocklightWeight;
}
public float sunlightWeight() {
return sunlightWeight;
}
public float moonlightWeight() {
return moonlightWeight;
}
public int minimumYLevel() {
return minimumYLevel;
}
public int maximumYLevel() {
return maximumYLevel;
}
public Map<MoonPhase, Float> phaseMultipliers() {
return phaseMultipliers;
}
}

View File

@ -1,129 +0,0 @@
package me.jamestmartin.wasteland.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
import me.jamestmartin.wasteland.ranks.Rank;
import me.jamestmartin.wasteland.spawns.MonsterType;
public class WastelandConfig {
private final String databaseFile;
private final ChatConfig chat;
private final Collection<EnlistedRank> enlistedRanks;
private final Collection<Rank> officerRanks;
private final Optional<Rank> consoleRank;
private final Set<EntityType> eligibleMobs;
private final String eligibleMobsName;
private final Map<MonsterType, MonsterSpawnConfig> spawns;
/** Orphaned method. */
public static Optional<ChatColor> readColor(ConfigurationSection c, String path) {
return (Optional<ChatColor>) Optional.ofNullable(c.getString(path)).map(ChatColor::valueOf);
}
public WastelandConfig(ConfigurationSection c) {
this.databaseFile = c.getString("databaseFile", "wasteland.sqlite3");
this.chat = new ChatConfig(c.getConfigurationSection("chat"));
ConfigurationSection enlistedSection = c.getConfigurationSection("enlisted");
ArrayList<EnlistedRank> enlistedRanks = new ArrayList<>();
this.enlistedRanks = enlistedRanks;
this.eligibleMobs = new HashSet<>();
if (enlistedSection != null) {
Optional<ChatColor> defaultDecoration = readColor(enlistedSection, "decoration");
Optional<String> defaultDescription =
Optional.ofNullable(enlistedSection.getString("description"));
ConfigurationSection enlistedRanksSection = enlistedSection.getConfigurationSection("ranks");
Set<String> rankIDs = enlistedRanksSection.getKeys(false);
enlistedRanks.ensureCapacity(rankIDs.size());
for (String id : rankIDs) {
EnlistedRank result = new EnlistedRank(defaultDescription, defaultDecoration,
enlistedRanksSection.getConfigurationSection(id));
enlistedRanks.add(result);
}
enlistedRanks.sort(new EnlistedRank.EnlistedRankComparator(enlistedRanks));
ConfigurationSection promotionSection = enlistedSection.getConfigurationSection("promotions");
ConfigurationSection eligibleSection = promotionSection.getConfigurationSection("eligible");
List<String> eligibleMobTypes = eligibleSection.getStringList("entities");
for (String mobType : eligibleMobTypes) {
this.eligibleMobs.addAll(Arrays.asList(EntityTypes.lookupEntityType(mobType)));
}
this.eligibleMobsName = eligibleSection.getString("name");
} else {
this.eligibleMobsName = "nothing";
}
ConfigurationSection officerSection = c.getConfigurationSection("officer");
ArrayList<Rank> officerRanks = new ArrayList<>();
this.officerRanks = officerRanks;
if (officerSection != null) {
Optional<ChatColor> defaultDecoration = readColor(officerSection, "decoration");
ConfigurationSection officerRanksSection = officerSection.getConfigurationSection("ranks");
Set<String> rankIDs = officerRanksSection.getKeys(false);
officerRanks.ensureCapacity(rankIDs.size());
for (String id : rankIDs) {
ConfigurationSection rank = officerRanksSection.getConfigurationSection(id);
Rank result = new Rank(Optional.empty(), defaultDecoration, rank);
officerRanks.add(result);
}
officerRanks.sort(new Rank.RankComparator(officerRanks));
String consoleRankID = officerSection.getString("console", null);
if (consoleRankID == null) {
this.consoleRank = Optional.of(officerRanks.get(officerRanks.size() - 1));
} else {
this.consoleRank = Optional.of(officerRanks.stream().filter(rank -> rank.getId().equals(consoleRankID)).findFirst().get());
}
} else {
this.consoleRank = Optional.empty();
}
ConfigurationSection mss = c.getConfigurationSection("spawns");
this.spawns = new HashMap<>();
if (mss != null) {
for (String typeName : mss.getKeys(false)) {
MonsterType type = MonsterType.valueOf(typeName);
MonsterSpawnConfig config = new MonsterSpawnConfig(mss.getConfigurationSection(typeName));
this.spawns.put(type, config);
}
}
}
public String databaseFile() { return this.databaseFile; }
public ChatConfig chat() { return this.chat; }
public Collection<EnlistedRank> enlistedRanks() { return this.enlistedRanks; }
public Collection<Rank> officerRanks() { return this.officerRanks; }
public Optional<Rank> consoleRank() { return this.consoleRank; }
/** The entity types which, if killed, will count towards your enlisted rank. */
public Set<EntityType> eligibleMobs() { return this.eligibleMobs; }
/** The term for the eligible mobs, e.g. "zombies" or "hostile mobs". */
public String eligibleMobsName() { return this.eligibleMobsName; }
public Map<MonsterType, MonsterSpawnConfig> spawns() { return this.spawns; }
}

View File

@ -0,0 +1,30 @@
package me.jamestmartin.wasteland.kills;
import java.util.stream.Collectors;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
class CommandRankEligibleMobs implements CommandExecutor {
private final KillsConfig config;
public CommandRankEligibleMobs(KillsConfig config) {
this.config = config;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0) {
sender.sendMessage("Too many arguments!");
sender.sendMessage(command.getUsage());
return false;
}
sender.sendMessage("Killing " + config.getEligibleMobsName() + " will count towards your next promotion.");
sender.sendMessage("Specifically, any of these mobs will work: " +
config.getEligibleMobs().stream().map(Object::toString).sorted().collect(Collectors.joining(", ")));
return true;
}
}

View File

@ -1,6 +1,5 @@
package me.jamestmartin.wasteland.commands;
package me.jamestmartin.wasteland.kills;
import java.sql.SQLException;
import java.util.logging.Level;
import org.bukkit.command.Command;
@ -10,7 +9,13 @@ import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
public class CommandSetKills implements CommandExecutor {
class CommandSetKills implements CommandExecutor {
private final PlayerKillsStore store;
public CommandSetKills(PlayerKillsStore store) {
this.store = store;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("wasteland.kills.set")) {
@ -66,13 +71,11 @@ public class CommandSetKills implements CommandExecutor {
}
try {
int previousKills = Wasteland.getInstance().getDatabase().getPlayerKills(subject);
Wasteland.getInstance().getDatabase().setPlayerKills(subject, kills);
Wasteland.getInstance().updatePlayerRank(subject);
int previousKills = store.getPlayerKills(subject);
store.setPlayerKills(subject, kills);
sender.sendMessage(playerNowHas + " " + kills + " kills.");
Wasteland.getInstance().getLogger().info(sender.getName() + " has changed the number of kills " + subject.getName() + " has from " + previousKills + " to " + kills);
} catch (SQLException e) {
} catch (Exception e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to set player kills.", e);
sender.sendMessage("ERROR: Failed to update player kills. Please notify a server administrator.");
}

View File

@ -0,0 +1,32 @@
package me.jamestmartin.wasteland.kills;
import java.util.Set;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
public class KillsConfig {
private final String eligibleMobsName;
private final Set<EntityType> eligibleMobs;
public KillsConfig(String eligibleMobsName, Set<EntityType> eligibleMobs) {
this.eligibleMobsName = eligibleMobsName;
this.eligibleMobs = eligibleMobs;
}
public String getEligibleMobsName() {
return eligibleMobsName;
}
public Set<EntityType> getEligibleMobs() {
return eligibleMobs;
}
public boolean isMobEligible(EntityType type) {
return getEligibleMobs().contains(type);
}
public boolean isMobEligible(Entity entity) {
return isMobEligible(entity.getType());
}
}

View File

@ -0,0 +1,58 @@
package me.jamestmartin.wasteland.kills;
import java.util.Optional;
import java.util.logging.Level;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
class KillsListener implements Listener {
private final KillsConfig config;
private final PlayerKillsStore store;
private final PlayerRankProvider provider;
public KillsListener(KillsConfig config, PlayerKillsStore store, PlayerRankProvider provider) {
this.config = config;
this.store = store;
this.provider = provider;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityDeath(EntityDeathEvent event) {
Player player = event.getEntity().getKiller();
if (player == null) return;
if (!config.isMobEligible(event.getEntityType())) return;
try {
// TODO: Rank-related logic does not belong in this listener.
Optional<EnlistedRank> oldRank = provider.getEnlistedRank(player);
store.incrementPlayerKills(player);
Optional<EnlistedRank> newRank = provider.getEnlistedRank(player);
if (newRank.isPresent()) {
final String formatString;
if (oldRank.isPresent()) {
if (!newRank.get().equals(oldRank.get())) {
formatString = "%s" + ChatColor.RESET + " has been promoted from %s " + ChatColor.RESET + "to %s" + ChatColor.RESET + "!";
player.getServer().broadcastMessage(
String.format(formatString, player.getDisplayName(),
oldRank.get().formatFull(), newRank.get().formatFull()));
}
} else {
formatString = "%s" + ChatColor.RESET + " has been promoted to %s" + ChatColor.RESET + "!";
player.getServer().broadcastMessage(
String.format(formatString, player.getDisplayName(), newRank.get()));
}
}
} catch (Exception e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to increment player kills.", e);
}
}
}

View File

@ -0,0 +1,32 @@
package me.jamestmartin.wasteland.kills;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.plugin.java.JavaPlugin;
import me.jamestmartin.wasteland.Substate;
import me.jamestmartin.wasteland.ranks.PlayerRankProvider;
public class KillsState implements Substate {
private final KillsListener killsListener;
private final CommandRankEligibleMobs commandRankEligibleMobs;
private final CommandSetKills commandSetKills;
public KillsState(KillsConfig config, PlayerKillsStore store, PlayerRankProvider rankProvider) {
this.killsListener = new KillsListener(config, store, rankProvider);
this.commandRankEligibleMobs = new CommandRankEligibleMobs(config);
this.commandSetKills = new CommandSetKills(store);
}
@Override
public void register(JavaPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(killsListener, plugin);
plugin.getCommand("setkills").setExecutor(commandSetKills);
plugin.getCommand("rankeligiblemobs").setExecutor(commandRankEligibleMobs);
}
@Override
public void unregister(JavaPlugin plugin) {
EntityDeathEvent.getHandlerList().unregister(killsListener);
plugin.getCommand("setkills").setExecutor(null);
}
}

View File

@ -0,0 +1,8 @@
package me.jamestmartin.wasteland.kills;
import org.bukkit.entity.Player;
public interface PlayerKillsProvider {
/** Get how many monsters a player has killed. */
public int getPlayerKills(Player player) throws Exception;
}

View File

@ -0,0 +1,18 @@
package me.jamestmartin.wasteland.kills;
import org.bukkit.entity.Player;
/** A data store which stores how many monsters a player has killed. */
public interface PlayerKillsStore extends PlayerKillsProvider {
/** Set how many monsters a player has killed. */
public void setPlayerKills(Player player, int kills) throws Exception;
/** Add to the number of monsters a player has killed. */
public void addPlayerKills(Player player, int kills) throws Exception;
/** Add one to the number of monsters a player has killed. */
public default void incrementPlayerKills(Player player) throws Exception {
addPlayerKills(player, 1);
}
/** Add a player to the store if they are not already present, if necessary. */
public default void initPlayer(Player player) throws Exception { }
}

View File

@ -1,26 +0,0 @@
package me.jamestmartin.wasteland.listeners;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import me.jamestmartin.wasteland.config.ChatConfig;
public class ChatListener implements Listener {
private final ChatConfig config;
public ChatListener(final ChatConfig config) {
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerChat(final AsyncPlayerChatEvent event) {
final Player player = event.getPlayer();
if (player.hasPermission("wasteland.chat.officer")) {
event.setFormat(config.getOfficerChatFormat(player));
} else {
event.setFormat(config.getPlayerChatFormat(player));
}
}
}

View File

@ -1,126 +0,0 @@
package me.jamestmartin.wasteland.listeners;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.ChatColor;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.permissions.PermissionAttachment;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
public class RankListener implements Listener, AutoCloseable {
private Map<UUID, PermissionAttachment> attachments = new HashMap<>();
private final Collection<EntityType> eligibleMobs;
public RankListener(Collection<EntityType> eligibleMobs) {
this.eligibleMobs = eligibleMobs;
for (Player player : Wasteland.getInstance().getServer().getOnlinePlayers()) {
try {
initializePlayer(player);
} catch (SQLException e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to get player's kills.", e);
}
}
}
private void createAttachment(Player player) throws SQLException {
PermissionAttachment attachment = player.addAttachment(Wasteland.getInstance());
attachments.put(player.getUniqueId(), attachment);
int kills = Wasteland.getInstance().getDatabase().getPlayerKills(player);
Optional<EnlistedRank> rank = EnlistedRank.getRankFromKills(kills);
if (rank.isPresent()) {
attachment.setPermission(rank.get().getPermission(), true);
}
}
private void removeAttachment(Player player) {
PermissionAttachment attachment = attachments.remove(player.getUniqueId());
player.removeAttachment(attachment);
}
private void initializePlayer(Player player) throws SQLException {
Wasteland.getInstance().getDatabase().initPlayerKills(player);
createAttachment(player);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
try {
initializePlayer(player);
} catch (SQLException e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to get player's kills.", e);
return;
}
}
public void updatePlayerRank(Player player) throws SQLException {
removeAttachment(player);
createAttachment(player);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityDeath(EntityDeathEvent event) {
Player player = event.getEntity().getKiller();
if (player == null) return;
if (!eligibleMobs.contains(event.getEntityType())) return;
try {
Wasteland.getInstance().getDatabase().incrementPlayerKills(player);
Optional<EnlistedRank> oldRank = EnlistedRank.getEnlistedRank(player);
updatePlayerRank(player);
Optional<EnlistedRank> newRank = EnlistedRank.getEnlistedRank(player);
if (newRank.isPresent()) {
final String formatString;
if (oldRank.isPresent()) {
if (!newRank.get().equals(oldRank.get())) {
formatString = "%s" + ChatColor.RESET + " has been promoted from %s " + ChatColor.RESET + "to %s" + ChatColor.RESET + "!";
player.getServer().broadcastMessage(
String.format(formatString, player.getDisplayName(),
oldRank.get().formatFull(), newRank.get().formatFull()));
}
} else {
formatString = "%s" + ChatColor.RESET + " has been promoted to %s" + ChatColor.RESET + "!";
player.getServer().broadcastMessage(
String.format(formatString, player.getDisplayName(), newRank.get()));
}
}
} catch (SQLException e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to increment player kills.", e);
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLeave(PlayerQuitEvent event) {
PermissionAttachment attachment = attachments.remove(event.getPlayer().getUniqueId());
event.getPlayer().removeAttachment(attachment);
}
@Override
public void close() {
// Trying to remove attachments throws an error. Perhaps it's done automatically?
/*for(Map.Entry<UUID, PermissionAttachment> attachment : attachments.entrySet()) {
Wasteland.getInstance().getServer().getPlayer(attachment.getKey())
.removeAttachment(attachment.getValue());
attachments.remove(attachment.getKey());
}*/
attachments = null;
}
}

View File

@ -0,0 +1,25 @@
package me.jamestmartin.wasteland.ranks;
public class AllRanks {
private final EnlistedRanks enlistedRanks;
private final Ranks<Rank> officerRanks;
private final Rank consoleRank;
public AllRanks(EnlistedRanks enlistedRanks, Ranks<Rank> officerRanks, Rank consoleRank) {
this.enlistedRanks = enlistedRanks;
this.officerRanks = officerRanks;
this.consoleRank = consoleRank;
}
public EnlistedRanks getEnlistedRanks() {
return enlistedRanks;
}
public Ranks<Rank> getOfficerRanks() {
return officerRanks;
}
public Rank getConsoleRank() {
return consoleRank;
}
}

View File

@ -1,6 +1,5 @@
package me.jamestmartin.wasteland.commands;
package me.jamestmartin.wasteland.ranks;
import java.sql.SQLException;
import java.util.Optional;
import java.util.logging.Level;
@ -11,13 +10,18 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
import me.jamestmartin.wasteland.kills.KillsConfig;
import me.jamestmartin.wasteland.kills.PlayerKillsProvider;
public class CommandRank implements CommandExecutor {
private final PlayerKillsProvider killsProvider;
private final PlayerRankProvider rankProvider;
private final String eligibleMobsName;
public CommandRank(String eligibleMobsName) {
this.eligibleMobsName = eligibleMobsName;
public CommandRank(KillsConfig killsConfig, PlayerKillsProvider killsProvider, PlayerRankProvider rankProvider) {
this.killsProvider = killsProvider;
this.rankProvider = rankProvider;
this.eligibleMobsName = killsConfig.getEligibleMobsName();
}
@Override
@ -70,9 +74,9 @@ public class CommandRank implements CommandExecutor {
}
try {
int kills = Wasteland.getInstance().getDatabase().getPlayerKills(subject);
Optional<EnlistedRank> rank = EnlistedRank.getRankFromKills(kills);
Optional<EnlistedRank> nextRank = EnlistedRank.getNextRank(subject);
int kills = killsProvider.getPlayerKills(subject);
Optional<EnlistedRank> rank = rankProvider.getEnlistedRank(subject);
Optional<EnlistedRank> nextRank = rankProvider.getNextRank(subject);
if (rank.isPresent()) {
sender.sendMessage(playerName + is + "rank " + rank.get().formatFull() + ChatColor.RESET + ".");
}
@ -92,7 +96,7 @@ public class CommandRank implements CommandExecutor {
} else {
sender.sendMessage(playerName + has + "reached maximum rank.");
}
} catch (SQLException e) {
} catch (Exception e) {
sender.sendMessage("Command failed due to database exception. Contact the server administrator.");
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to get player kills.", e);
}

View File

@ -1,15 +1,16 @@
package me.jamestmartin.wasteland.commands;
package me.jamestmartin.wasteland.ranks;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.config.WastelandConfig;
import me.jamestmartin.wasteland.ranks.EnlistedRank;
import me.jamestmartin.wasteland.ranks.Rank;
public class CommandRanks implements CommandExecutor {
private final AllRanks ranks;
public CommandRanks(AllRanks ranks) {
this.ranks = ranks;
}
private String makeElement(Rank rank) {
if (rank.getDescription().isPresent())
return rank.formatExtended() + ": " + rank.getDescription().get();
@ -23,15 +24,13 @@ public class CommandRanks implements CommandExecutor {
return false;
}
WastelandConfig config = Wasteland.getInstance().getSettings();
sender.sendMessage("Enlisted ranks:");
for (EnlistedRank rank : config.enlistedRanks()) {
for (EnlistedRank rank : ranks.getEnlistedRanks().getRanksList()) {
sender.sendMessage("* " + makeElement(rank));
}
sender.sendMessage("Officer (server staff) ranks:");
for (Rank rank : config.officerRanks()) {
for (Rank rank : ranks.getOfficerRanks().getRanksList()) {
sender.sendMessage("* " + makeElement(rank));
}

View File

@ -1,90 +1,45 @@
package me.jamestmartin.wasteland.ranks;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionDefault;
import me.jamestmartin.wasteland.Wasteland;
import org.bukkit.permissions.Permission;
public class EnlistedRank extends Rank {
private final Optional<Integer> kills;
public EnlistedRank(Optional<String> defaultDescription, Optional<ChatColor> defaultDecoration,
ConfigurationSection c) {
super(defaultDescription, defaultDecoration, c);
if (c.getKeys(false).contains("kills")) this.kills = Optional.of(c.getInt("kills"));
else this.kills = Optional.empty();
PermissionDefault def;
if (kills.isPresent() && kills.get() == 0) def = PermissionDefault.TRUE;
else def = PermissionDefault.FALSE;
super.getPermission().setDefault(def);
}
@Override
public Optional<String> getDescription() {
Optional<String> format = super.getDescription();
String descriptionFormat;
if (format.isPresent()) descriptionFormat = format.get();
else return Optional.empty();
String result;
if (kills.isPresent()) result = descriptionFormat.replace("{kills}", kills.get().toString());
else result = descriptionFormat;
return Optional.of(result);
}
public final Optional<Integer> getKills() {
return kills;
}
public static Optional<EnlistedRank> getEnlistedRank(Player player) {
EnlistedRank result = null;
for (EnlistedRank rank : Wasteland.getInstance().getSettings().enlistedRanks()) {
if (player.hasPermission(rank.getPermission())) result = rank;
}
return Optional.ofNullable(result);
}
public static Optional<EnlistedRank> getRankFromKills(int kills) {
if (kills < 0) throw new IllegalArgumentException("Number of kills must not be negative.");
return Wasteland.getInstance().getSettings().enlistedRanks()
.stream().filter(rank -> rank.getKills().map(k -> k <= kills).orElse(false))
.reduce((acc, rank) -> rank);
}
public static Optional<EnlistedRank> getNextRank(Player player) {
Optional<EnlistedRank> currentRank = getEnlistedRank(player);
Stream<EnlistedRank> ranks = Wasteland.getInstance().getSettings().enlistedRanks().stream();
if (!currentRank.isPresent()) {
return ranks.filter(rank -> rank.kills.isPresent()).findFirst();
}
return ranks.filter(rank -> rank.kills.isPresent() && rank.isSuccessorOf(
Wasteland.getInstance().getSettings().enlistedRanks().stream().map(x -> (Rank) x).collect(Collectors.toList())
, currentRank.get())).findFirst();
}
public static class EnlistedRankComparator implements Comparator<EnlistedRank> {
private final Comparator<Rank> delegate;
public EnlistedRankComparator(Collection<EnlistedRank> ranks) {
this.delegate = new RankComparator(
ranks.stream().map((EnlistedRank x) -> (Rank) x).collect(Collectors.toList()));
}
private final Optional<Integer> kills;
private final Permission preferencePermission;
@Override
public int compare(EnlistedRank a, EnlistedRank b) {
if (a.getKills().isPresent() && b.getKills().isPresent()) {
if (a.getKills().get() > b.getKills().get()) return 1;
if (b.getKills().get() > a.getKills().get()) return -1;
}
return delegate.compare(a, b);
}
}
public EnlistedRank(
String id,
String fullName,
String abbreviation,
Optional<String> description,
Optional<ChatColor> color,
Optional<ChatColor> decoration,
Permission permission,
Optional<Integer> kills,
Permission preferencePermission) {
super(id, fullName, abbreviation, description, color, decoration, permission);
this.kills = kills;
this.preferencePermission = preferencePermission;
}
/** The number of kills you must receive to be promoted to this rank. */
public Optional<Integer> getKills() {
return kills;
}
/** Whether this rank has a set number of kills required to receive promotion. */
public boolean hasKills() {
return getKills().isPresent();
}
/**
* If a player has this permission set, they will receive this rank
* instead of any alternative ranks, even if another rank is preferred by default.
*/
public Permission getPreferencePermission() {
return preferencePermission;
}
}

View File

@ -0,0 +1,94 @@
package me.jamestmartin.wasteland.ranks;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.entity.Player;
import com.google.common.graph.ImmutableValueGraph;
/** A collection of enlisted ranks and their successor/predecessor relationships. */
public class EnlistedRanks extends Ranks<EnlistedRank> {
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
* @param originalOrder
* The order in which the ranks are listed in the configuration file.
*/
public EnlistedRanks(ImmutableValueGraph<EnlistedRank, Boolean> ranks, Optional<List<EnlistedRank>> originalOrder) {
super(ranks, originalOrder);
}
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
* @param originalOrder
* The order in which the ranks are listed in the configuration file.
*/
public EnlistedRanks(ImmutableValueGraph<EnlistedRank, Boolean> ranks, List<EnlistedRank> originalOrder) {
super(ranks, originalOrder);
}
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
*/
public EnlistedRanks(ImmutableValueGraph<EnlistedRank, Boolean> ranks) {
super(ranks);
}
/** Get the direct successors to the rank which can be automatically rewarded for killing monsters. */
public Set<EnlistedRank> getPromotableSuccessors(EnlistedRank rank) {
return getSuccessors(rank).stream().filter(EnlistedRank::hasKills).collect(Collectors.toUnmodifiableSet());
}
/** Get the preferred direct successor to the rank if it can be automatically rewarded for killing monsters. */
public Optional<EnlistedRank> getPreferredPromotableSuccessor(EnlistedRank rank) {
return getPromotableSuccessors(rank).stream().filter(super::isPreferredSuccessor).findAny();
}
/** Get the next rank that a player would get promoted to after the given rank, if any exists. */
public Optional<EnlistedRank> getNextRank(Player player, EnlistedRank rank) {
for (EnlistedRank successor : getPromotableSuccessors(rank)) {
if (player.hasPermission(successor.getPreferencePermission())) {
return Optional.of(successor);
}
}
return getPreferredPromotableSuccessor(rank);
}
/** The rank which is automatically assigned to all players. */
public EnlistedRank getDefaultRank() {
return getRanks().stream().filter(rank -> rank.getKills().map(x -> x == 0).orElse(false)).findAny().get();
}
/** The rank a player would have if they killed some number of monsters. */
public EnlistedRank getRankAt(Player player, int kills) {
EnlistedRank rank = getDefaultRank();
while (true) {
Optional<EnlistedRank> maybe = getNextRank(player, rank);
if (!maybe.isPresent()) {
break;
}
EnlistedRank nextRank = maybe.get();
if (!nextRank.getKills().map(k -> kills >= k).orElseGet(() -> player.hasPermission(nextRank.getPermission()))) {
break;
}
rank = nextRank;
}
return rank;
}
}

View File

@ -0,0 +1,41 @@
package me.jamestmartin.wasteland.ranks;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import org.bukkit.entity.Player;
public class PermissionsPlayerRankProvider implements PlayerRankProvider {
private final AllRanks ranks;
public PermissionsPlayerRankProvider(AllRanks ranks) {
this.ranks = ranks;
}
@Override
public AllRanks getRanks() {
return ranks;
}
private<T extends Rank> Optional<T> getRank(Player player, Ranks<T> ranks) {
Set<T> playerRanks = new HashSet<>();
for (T rank : ranks.getRanks()) {
if (player.hasPermission(rank.getPermission())) {
playerRanks.add(rank);
}
}
return ranks.getHighestRank(playerRanks);
}
@Override
public Optional<EnlistedRank> getEnlistedRank(Player player) {
return getRank(player, ranks.getEnlistedRanks());
}
@Override
public Optional<Rank> getOfficerRank(Player player) {
return getRank(player, ranks.getOfficerRanks());
}
}

View File

@ -0,0 +1,40 @@
package me.jamestmartin.wasteland.ranks;
import java.util.Optional;
import org.bukkit.entity.Player;
public interface PlayerRankProvider {
/** Get the ranks this player rank provider can choose from. */
AllRanks getRanks();
/** Get the player's highest rank achieved by monster kills. */
Optional<EnlistedRank> getEnlistedRank(Player player);
/** Get the player's highest staff rank. */
Optional<Rank> getOfficerRank(Player player);
default PlayerRanks getPlayerRanks(final Player player) {
return new PlayerRanks() {
@Override
public Optional<EnlistedRank> getEnlistedRank() {
return PlayerRankProvider.this.getEnlistedRank(player);
}
@Override
public Optional<Rank> getOfficerRank() {
return PlayerRankProvider.this.getOfficerRank(player);
}
};
}
/** Get the player's highest rank overall. */
default Optional<Rank> getHighestRank(Player player) {
return getPlayerRanks(player).getHighestRank();
}
/** The next rank a player will get promoted to from killing monsters. */
default Optional<EnlistedRank> getNextRank(Player player) {
return getEnlistedRank(player).flatMap(rank -> getRanks().getEnlistedRanks().getNextRank(player, rank));
}
}

View File

@ -0,0 +1,16 @@
package me.jamestmartin.wasteland.ranks;
import java.util.Optional;
public interface PlayerRanks {
/** Get the player's highest rank achieved by monster kills. */
Optional<EnlistedRank> getEnlistedRank();
/** Get the player's highest staff rank. */
Optional<Rank> getOfficerRank();
/** Get the player's highest rank overall. */
default Optional<Rank> getHighestRank() {
return getOfficerRank().or(() -> getOfficerRank());
}
}

View File

@ -1,151 +1,101 @@
package me.jamestmartin.wasteland.ranks;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.config.WastelandConfig;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
public class Rank {
private final String id, abbreviation, name;
private final Optional<String> description;
private final Optional<ChatColor> color, decoration;
private final Permission permission;
private final Optional<String> predecessor;
private final boolean preferred;
public Rank(Optional<String> defaultDescription, Optional<ChatColor> defaultDecoration,
ConfigurationSection c) {
this.id = c.getName();
this.name = c.getString("name", id);
this.abbreviation = c.getString("abbreviation", name);
Optional<String> description = Optional.ofNullable(c.getString("description"));
if (!description.isPresent()) this.description = defaultDescription;
else this.description = description;
this.color = WastelandConfig.readColor(c, "color");
Optional<ChatColor> decoration = WastelandConfig.readColor(c, "decoration");
if (!decoration.isPresent()) this.decoration = defaultDecoration;
else this.decoration = decoration;
this.predecessor = Optional.ofNullable(c.getString("succeeds"));
this.preferred = c.getBoolean("preferred", true);
Map<String, Boolean> children = new HashMap<>();
if (predecessor.isPresent()) {
children.put(predecessor.get(), true);
}
this.permission =
new Permission("wasteland.rank." + id, description.orElse(null), PermissionDefault.FALSE, children);
}
public String getId() { return this.id; }
public Permission getPermission() { return this.permission; }
public String getAbbreviation() { return this.abbreviation; }
public String getFullName() { return this.name; }
public Optional<String> getDescription() { return description; }
public Optional<ChatColor> getColor() { return this.color; }
public Optional<ChatColor> getDecoration() { return this.decoration; }
public Optional<String> getPredecessor() { return this.predecessor; }
public boolean isPreferred() { return this.preferred; }
public boolean isSuccessorOf(Collection<Rank> ranks, Rank other) {
if (this == other) return false;
if (this.getId().equals(other.getId())) return false;
if (!this.getPredecessor().isPresent()) return false;
if (this.getPredecessor().get().equals(other.getId())) return true;
String predecessorId = this.getPredecessor().get();
for (Rank predecessor : ranks) {
if (predecessor.getId().equals(predecessorId))
return predecessor.isSuccessorOf(ranks, other);
}
throw new IllegalArgumentException("Predecessor rank " + predecessorId + " not found.");
}
public final String getFormat() {
String color = this.color.map(ChatColor::toString).orElse("");
String decoration = this.decoration.map(ChatColor::toString).orElse("");
return color + decoration;
}
/** The rank abbreviation with chat formatting codes. */
public final String formatAbbreviated() {
return getFormat() + getAbbreviation();
}
/** The rank name with chat formatting codes. */
public final String formatFull() {
return getFormat() + getFullName();
}
/** `Rank Name (RankAbbr)` with chat formatting codes. */
public final String formatExtended() {
return formatFull() + ChatColor.RESET + " (" + formatAbbreviated() + ChatColor.RESET + ")";
}
/**
* @param player
* @return Does the player have this rank?
*/
public final boolean hasRank(Player player) {
return player.hasPermission(getPermission());
}
@Override
public String toString() {
return this.abbreviation;
}
public static Optional<Rank> getOfficerRank(Player player) {
Rank result = null;
for (Rank rank : Wasteland.getInstance().getSettings().officerRanks()) {
if (player.hasPermission(rank.getPermission())) result = rank;
}
return Optional.ofNullable(result);
}
public static Optional<Rank> getHighestRank(Player player) {
Optional<Rank> officerRank = getOfficerRank(player);
if (officerRank.isPresent()) return officerRank;
return EnlistedRank.getEnlistedRank(player).map(x -> (Rank) x);
}
public static Optional<Rank> getConsoleRank() {
return Wasteland.getInstance().getSettings().consoleRank();
}
public static Optional<Rank> getRank(RankType type, Player player) {
switch (type) {
case CONSOLE:
return getConsoleRank();
case ENLISTED:
return EnlistedRank.getEnlistedRank(player).map(x -> x);
case HIGHEST:
return getHighestRank(player);
case OFFICER:
return getOfficerRank(player);
}
throw new IllegalStateException("Unknown rank type.");
}
public static class RankComparator implements Comparator<Rank> {
private final Collection<Rank> ranks;
public RankComparator(Collection<Rank> ranks) { this.ranks = ranks; }
@Override
public int compare(Rank a, Rank b) {
if (a == b) return 0;
if (a.getId().equals(b.getId())) return 0;
if (a.isSuccessorOf(ranks, b)) return 1;
if (b.isSuccessorOf(ranks, a)) return -1;
// incomparable
return 0;
}
}
private final String id;
private final String fullName;
private final String abbreviation;
private final Optional<String> description;
private final Optional<ChatColor> color;
private final Optional<ChatColor> decoration;
private final Permission permission;
public Rank(
String id,
String fullName,
String abbreviation,
Optional<String> description,
Optional<ChatColor> color,
Optional<ChatColor> decoration,
Permission permission) {
this.id = id;
this.fullName = fullName;
this.abbreviation = abbreviation;
this.description = description;
this.color = color;
this.decoration = decoration;
this.permission = permission;
}
/** Get this rank's internal identifier, e.g. gysgt. */
public String getId() {
return id;
}
/** Get the full name of this rank, e.g. Gunnery Sergeant. */
public String getFullName() {
return fullName;
}
/** Get the abbreviation for this rank, e.g. GySgt. */
public String getAbbreviation() {
return abbreviation;
}
/** Get this rank's description, if it has one. */
public Optional<String> getDescription() {
return description;
}
/** Get this rank's color, if it has one. */
public Optional<ChatColor> getColor() {
return color;
}
/** Get this rank's decoration (e.g. bold or italic), if it has one. */
public Optional<ChatColor> getDecoration() {
return decoration;
}
/** Get the permission which all players with this rank have. */
public Permission getPermission() {
return permission;
}
/** Check whether a player has this rank via permissions. */
public boolean hasRank(Player player) {
return player.hasPermission(getPermission());
}
/** Get the chat formatting codes for this rank, i.e. the color and decoration combined. */
public String getFormat() {
String color = this.color.map(ChatColor::toString).orElse("");
String decoration = this.decoration.map(ChatColor::toString).orElse("");
return color + decoration;
}
/** The rank abbreviation with chat formatting codes. */
public String formatAbbreviated() {
return getFormat() + getAbbreviation();
}
/** The rank name with chat formatting codes. */
public String formatFull() {
return getFormat() + getFullName();
}
/** `Rank Name (RankAbbr)` with chat formatting codes. */
public String formatExtended() {
return formatFull() + ChatColor.RESET + " (" + formatAbbreviated() + ChatColor.RESET + ")";
}
@Override
public String toString() {
return this.getAbbreviation();
}
}

View File

@ -0,0 +1,97 @@
package me.jamestmartin.wasteland.ranks;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionAttachment;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.kills.PlayerKillsStore;
public class RankAttachments {
private final EnlistedRanks ranks;
private final PlayerKillsStore killsStore;
private final Map<UUID, PermissionAttachment> attachments = new HashMap<>();
public RankAttachments(EnlistedRanks ranks, PlayerKillsStore killsStore) {
this.ranks = ranks;
this.killsStore = killsStore;
}
public void removeAttachment(Player player) {
PermissionAttachment attachment = attachments.remove(player.getUniqueId());
player.removeAttachment(attachment);
}
private void createAttachment(Player player, int kills) throws Exception {
PermissionAttachment attachment = player.addAttachment(Wasteland.getInstance());
attachments.put(player.getUniqueId(), attachment);
EnlistedRank rank = ranks.getRankAt(player, kills);
for (EnlistedRank child : ranks.getTransitiveReflexivePredecessors(rank)) {
attachment.setPermission(child.getPermission(), true);
}
}
private void createAttachment(Player player) throws Exception {
int kills = killsStore.getPlayerKills(player);
createAttachment(player, kills);
}
public void initializePlayer(Player player) throws Exception {
killsStore.initPlayer(player);
createAttachment(player);
}
public void updatePlayerRank(Player player) throws Exception {
removeAttachment(player);
createAttachment(player);
}
private void updatePlayerRank(Player player, int kills) throws Exception {
removeAttachment(player);
createAttachment(player, kills);
}
public void register() {
for (Player player : Wasteland.getInstance().getServer().getOnlinePlayers()) {
try {
initializePlayer(player);
} catch (Exception e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to get player's kills.", e);
}
}
}
public void unregister() {
// Trying to remove attachments throws an error. Perhaps it's done automatically?
for(Map.Entry<UUID, PermissionAttachment> attachment : attachments.entrySet()) {
Wasteland.getInstance().getServer().getPlayer(attachment.getKey())
.removeAttachment(attachment.getValue());
attachments.remove(attachment.getKey());
}
attachments.clear();
}
public class AttachmentUpdatingPlayerKillsStore implements PlayerKillsStore {
@Override
public int getPlayerKills(Player player) throws Exception {
return killsStore.getPlayerKills(player);
}
@Override
public void setPlayerKills(Player player, int kills) throws Exception {
killsStore.setPlayerKills(player, kills);
RankAttachments.this.updatePlayerRank(player, kills);
}
@Override
public void addPlayerKills(Player player, int kills) throws Exception {
killsStore.addPlayerKills(player, kills);
RankAttachments.this.updatePlayerRank(player);
}
}
}

View File

@ -0,0 +1,36 @@
package me.jamestmartin.wasteland.ranks;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import me.jamestmartin.wasteland.Wasteland;
public class RankAttachmentsListener implements Listener {
private final RankAttachments attachments;
public RankAttachmentsListener(RankAttachments attachments) {
this.attachments = attachments;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
try {
attachments.initializePlayer(player);
} catch (Exception e) {
Wasteland.getInstance().getLogger().log(Level.SEVERE, "Failed to get player's kills.", e);
return;
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLeave(PlayerQuitEvent event) {
attachments.removeAttachment(event.getPlayer());
}
}

View File

@ -1,9 +0,0 @@
package me.jamestmartin.wasteland.ranks;
public enum RankType {
ENLISTED,
OFFICER,
CONSOLE,
/** Either enlisted, or officer if the player is one. */
HIGHEST;
}

View File

@ -0,0 +1,161 @@
package me.jamestmartin.wasteland.ranks;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import com.google.common.graph.ImmutableValueGraph;
/** A collection of ranks and their successor/predecessor relationships. */
public class Ranks<T extends Rank> {
protected final ImmutableValueGraph<T, Boolean> ranks;
protected final Optional<List<T>> originalOrder;
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
* @param originalOrder
* The order in which the ranks are listed in the configuration file.
*/
public Ranks(ImmutableValueGraph<T, Boolean> ranks, Optional<List<T>> originalOrder) {
this.ranks = ranks;
this.originalOrder = originalOrder;
}
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
* @param originalOrder
* The order in which the ranks are listed in the configuration file.
*/
public Ranks(ImmutableValueGraph<T, Boolean> ranks, List<T> originalOrder) {
this(ranks, Optional.of(originalOrder));
}
/**
* @param ranks
* The edges represent predecessor/successor relationships,
* and the edge value represents whether the successor is the preferred successor.
* This graph *must* be a tree.
*/
public Ranks(ImmutableValueGraph<T, Boolean> ranks) {
this(ranks, Optional.empty());
}
/** Get all of the ranks in this collection. */
public Set<T> getRanks() {
return ranks.nodes();
}
/** Get a list of the ranks in their original order, or, if that is not specified, ordered least to greatest. */
public List<T> getRanksList() {
return originalOrder.orElseGet(() -> Lists.reverse(getRanks().stream().sorted(getComparator()).collect(Collectors.toUnmodifiableList())));
}
/** Get a rank by its id. */
public Optional<T> getRank(String id) {
return getRanks().stream().filter(rank -> rank.getId().equals(id)).findAny();
}
/** The rank's unique predecessor, if it exists. */
public Optional<T> getPredecessor(T rank) {
return ranks.predecessors(rank).stream().findAny();
}
public boolean hasPredecessor(T rank) {
return getPredecessor(rank).isPresent();
}
/** Every rank which is a transitive predecessor to the given rank. */
public Set<T> getTransitivePredecessors(T rank) {
Set<T> predecessors = new HashSet<>();
while (hasPredecessor(rank)) {
rank = getPredecessor(rank).get();
predecessors.add(rank);
}
return predecessors;
}
/** Every rank which is a transitive predecessor to the given rank, including the rank itself. */
public Set<T> getTransitiveReflexivePredecessors(T rank) {
Set<T> predecessors = getTransitivePredecessors(rank);
predecessors.add(rank);
return predecessors;
}
/** The set of *direct* successors to the rank. */
public Set<T> getSuccessors(T rank) {
return ranks.successors(rank);
}
/** The rank's preferred successor, if it exists. */
public Optional<T> getPreferredSuccessor(T rank) {
return getSuccessors(rank).stream().filter(successor -> ranks.edgeValueOrDefault(rank, successor, false)).findAny();
}
/** Is the rank the preferred successor to its predecessor, if it has one? */
public boolean isPreferredSuccessor(final T rank) {
// Future Guava versions return an Optional here in the first place.
return getPredecessor(rank).flatMap(predecessor -> Optional.ofNullable(ranks.edgeValue(predecessor, rank))).orElse(false);
}
/** Is the second rank a *direct* successor of the first rank? */
public boolean isSuccessorOf(T predecessor, T successor) {
// Replace with this code for Guava 25+.
//return ranks.hasEdgeConnecting(predecessor, successor);
return ranks.successors(predecessor).contains(successor);
}
/** Is the second rank a transitive successor of the first rank? */
public boolean isTransitiveSuccessorOf(T predecessor, T successor) {
return isSuccessorOf(predecessor, successor)
|| getPredecessor(successor).map(rank -> isTransitiveSuccessorOf(predecessor, rank)).orElse(false);
}
/**
* The highest ranks of a collection of ranks is any rank which has no transitive successor.
* If there are multiple such ranks in this collection, the first is returned.
*/
public Optional<T> getHighestRank(Collection<T> ranks) {
return ranks.stream().filter(rank -> !ranks.stream().anyMatch(other -> isTransitiveSuccessorOf(rank, other))).findFirst();
}
/** Compare ranks by their predecessor/successor relationship. */
public class RankComparator implements Comparator<T> {
@Override
public int compare(T a, T b) {
if (a == b) return 0;
if (a.getId().equals(b.getId())) return 0;
if (Ranks.this.isSuccessorOf(a, b)) return 1;
if (Ranks.this.isSuccessorOf(b, a)) return -1;
// Preferred ranks are given precedence over other ranks.
Optional<T> predA = Ranks.this.getPredecessor(a);
Optional<T> predB = Ranks.this.getPredecessor(b);
if (predA.isPresent() && predB.isPresent() && predA.get().getId().equals(predB.get().getId())) {
if (Ranks.this.isPreferredSuccessor(a)) {
return 1;
}
if (Ranks.this.isPreferredSuccessor(b)) {
return -1;
}
}
// incomparable
return 0;
}
}
public RankComparator getComparator() {
return new RankComparator();
}
}

View File

@ -0,0 +1,52 @@
package me.jamestmartin.wasteland.ranks;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import me.jamestmartin.wasteland.Substate;
import me.jamestmartin.wasteland.kills.KillsConfig;
import me.jamestmartin.wasteland.kills.PlayerKillsStore;
public class RanksState implements Substate {
private final RankAttachmentsListener rankAttachmentsListener;
private final CommandRank commandRank;
private final CommandRanks commandRanks;
private final RankAttachments rankAttachments;
private final PlayerKillsStore killsStore;
public RanksState(KillsConfig killsConfig, PlayerKillsStore killsStore, PlayerRankProvider rankProvider) {
this.rankAttachments = new RankAttachments(rankProvider.getRanks().getEnlistedRanks(), killsStore);
this.rankAttachmentsListener = new RankAttachmentsListener(rankAttachments);
this.killsStore = rankAttachments.new AttachmentUpdatingPlayerKillsStore();
this.commandRank = new CommandRank(killsConfig, this.killsStore, rankProvider);
this.commandRanks = new CommandRanks(rankProvider.getRanks());
}
public PlayerKillsStore getPlayerKillsStore() {
return killsStore;
}
@Override
public void register(JavaPlugin plugin) {
rankAttachments.register();
plugin.getServer().getPluginManager().registerEvents(rankAttachmentsListener, plugin);
plugin.getCommand("rank").setExecutor(commandRank);
plugin.getCommand("ranks").setExecutor(commandRanks);
}
@Override
public void unregister(JavaPlugin plugin) {
PlayerJoinEvent.getHandlerList().unregister(rankAttachmentsListener);
PlayerQuitEvent.getHandlerList().unregister(rankAttachmentsListener);
plugin.getCommand("rank").setExecutor(null);
plugin.getCommand("ranks").setExecutor(null);
rankAttachments.unregister();
}
}

View File

@ -1,4 +1,4 @@
package me.jamestmartin.wasteland.commands;
package me.jamestmartin.wasteland.spawns;
import java.util.Optional;
import java.util.Random;
@ -10,9 +10,12 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
public class CommandDebugSpawn implements CommandExecutor {
private final WastelandSpawner spawner;
public CommandDebugSpawn(WastelandSpawner spawner) {
this.spawner = spawner;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
@ -40,7 +43,7 @@ public class CommandDebugSpawn implements CommandExecutor {
int successfulSpawns = 0;
for (int attempt = 0; attempt < attempts; attempt++) {
Optional<LivingEntity> tryMonster = Wasteland.getInstance().getSpawner().trySpawn(rand, player.getLocation());
Optional<LivingEntity> tryMonster = spawner.trySpawn(rand, player.getLocation());
if (tryMonster.isEmpty()) {
continue;
}

View File

@ -1,4 +1,4 @@
package me.jamestmartin.wasteland.commands;
package me.jamestmartin.wasteland.spawns;
import java.util.HashMap;
import java.util.Map.Entry;
@ -10,12 +10,15 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
import me.jamestmartin.wasteland.spawns.MonsterType;
import me.jamestmartin.wasteland.spawns.WastelandSpawner;
import me.jamestmartin.wasteland.world.MoonPhase;
public class CommandDebugSpawnWeights implements CommandExecutor {
private final WastelandSpawner spawner;
public CommandDebugSpawnWeights(WastelandSpawner spawner) {
this.spawner = spawner;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0) {
@ -28,8 +31,6 @@ public class CommandDebugSpawnWeights implements CommandExecutor {
return false;
}
WastelandSpawner spawner = Wasteland.getInstance().getSpawner();
Player player = (Player) sender;
Block target = player.getTargetBlockExact(25).getRelative(BlockFace.UP);
@ -39,18 +40,18 @@ public class CommandDebugSpawnWeights implements CommandExecutor {
int moonlight = MoonPhase.getMoonlight(target);
int sunlight = MoonPhase.getSunlight(target);
int blocklight = target.getLightFromBlocks();
HashMap<MonsterType, Float> weights = spawner.calculateSpawnProbabilities(target);
HashMap<MonsterType, Double> weights = spawner.calculateSpawnProbabilities(target);
sender.sendMessage("Moon phase: " + phase);
sender.sendMessage("Moonlight: " + moonlight + ", Sunlight: " + sunlight + ", Blocklight: " + blocklight);
sender.sendMessage("Monsters: " + monsters + ", Players: " + players);
sender.sendMessage("Monster weights at " + target.getX() + ", " + target.getY() + ", " + target.getZ() + ":");
for (Entry<MonsterType, Float> entry : weights.entrySet()) {
for (Entry<MonsterType, Double> entry : weights.entrySet()) {
MonsterType type = entry.getKey();
float weight = entry.getValue();
float lightProb = spawner.calculateLightLevelProbability(type, target);
float phaseMult = spawner.getMoonPhaseMultiplier(type, target);
float entitiesMult = WastelandSpawner.calculateNearbyEntitiesMultiplier(target);
double weight = entry.getValue();
double lightProb = spawner.calculateLightLevelProbability(type, target);
double phaseMult = spawner.getMoonPhaseMultiplier(type, target);
double entitiesMult = WastelandSpawner.calculateNearbyEntitiesMultiplier(target);
sender.sendMessage(String.format("* %s: %.2f (light: %.2f, phase: %.2f, entities: %.2f)", type, weight, lightProb, phaseMult, entitiesMult));
}

View File

@ -0,0 +1,124 @@
package me.jamestmartin.wasteland.spawns;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.block.Block;
import me.jamestmartin.wasteland.world.MoonPhase;
public class MonsterSpawnConfig {
private final double maximumLightLevel;
private final double blocklightWeight;
private final double sunlightWeight;
private final double moonlightWeight;
private final int maximumYLevel;
private final int minimumYLevel;
private final Map<MoonPhase, Double> phaseMultipliers;
public MonsterSpawnConfig(
double maximumLightLevel,
double blocklightWeight,
double sunlightWeight,
double moonlightWeight,
int maximumYLevel,
int minimumYLevel,
Map<MoonPhase, Double> phaseMultipliers
) {
if (maximumLightLevel < 0) {
throw new IllegalArgumentException("Maximum light level cannot be negative.");
}
if (maximumYLevel < 0) {
throw new IllegalArgumentException("Maximum Y level cannot be negative.");
}
if (minimumYLevel < 0) {
throw new IllegalArgumentException("Minimum Y level cannot be negative.");
}
this.maximumLightLevel = maximumLightLevel;
this.blocklightWeight = blocklightWeight;
this.sunlightWeight = sunlightWeight;
this.moonlightWeight = moonlightWeight;
this.maximumYLevel = maximumYLevel;
this.minimumYLevel = minimumYLevel;
this.phaseMultipliers = phaseMultipliers;
}
public double getMaximumLightLevel() {
return maximumLightLevel;
}
public double getBlocklightWeight() {
return blocklightWeight;
}
public double getSunlightWeight() {
return sunlightWeight;
}
public double getMoonlightWeight() {
return moonlightWeight;
}
public double calculateWeightedBlocklightLevel(int blocklight) {
return blocklight * getBlocklightWeight();
}
public double calculateWeightedBlocklightLevel(Block block) {
return calculateWeightedBlocklightLevel(block.getLightFromBlocks());
}
public double calculateWeightedSkylightLevel(int sunlight, int moonlight) {
return sunlight * getSunlightWeight() + moonlight * getMoonlightWeight();
}
public double calculateWeightedSkylightLevel(Block block) {
return calculateWeightedSkylightLevel(MoonPhase.getSunlight(block), MoonPhase.getMoonlight(block));
}
public double calculateWeightedLightLevel(int blocklight, int sunlight, int moonlight) {
return Math.max(calculateWeightedBlocklightLevel(blocklight), calculateWeightedSkylightLevel(sunlight, moonlight));
}
public double calculateWeightedLightLevel(Block block) {
return Math.max(calculateWeightedBlocklightLevel(block), calculateWeightedSkylightLevel(block));
}
public boolean isWeightedLightLevelWithinBounds(int blocklight, int sunlight, int moonlight) {
return calculateWeightedLightLevel(blocklight, sunlight, moonlight) < getMaximumLightLevel();
}
public boolean isWeightedLightLevelWithinBounds(Block block) {
return calculateWeightedLightLevel(block) < getMaximumLightLevel();
}
public int getMinimumYLevel() {
return minimumYLevel;
}
public int getMaximumYLevel() {
return maximumYLevel;
}
public boolean isYLevelWithinBounds(int yLevel) {
return yLevel > getMinimumYLevel() && yLevel < getMaximumYLevel();
}
public boolean isWithinBounds(Block block) {
return isYLevelWithinBounds(block.getY());
}
public boolean isWithinBounds(Location location) {
return isYLevelWithinBounds(location.getBlockY());
}
public Map<MoonPhase, Double> getPhaseMultipliers() {
return phaseMultipliers;
}
public Double getPhaseMultiplier(MoonPhase phase) {
return getPhaseMultipliers().get(phase);
}
}

View File

@ -0,0 +1,19 @@
package me.jamestmartin.wasteland.spawns;
import java.util.Map;
public class SpawnsConfig {
private final Map<MonsterType, MonsterSpawnConfig> monsterConfigs;
public SpawnsConfig(Map<MonsterType, MonsterSpawnConfig> monsterConfigs) {
this.monsterConfigs = monsterConfigs;
}
public Map<MonsterType, MonsterSpawnConfig> getMonsterConfigs() {
return monsterConfigs;
}
public MonsterSpawnConfig getMonster(MonsterType type) {
return monsterConfigs.get(type);
}
}

View File

@ -0,0 +1,29 @@
package me.jamestmartin.wasteland.spawns;
import org.bukkit.plugin.java.JavaPlugin;
import me.jamestmartin.wasteland.Substate;
public class SpawnsState implements Substate {
private final CommandDebugSpawn commandDebugSpawn;
private final CommandDebugSpawnWeights commandDebugSpawnWeights;
public SpawnsState(SpawnsConfig config) {
WastelandSpawner spawner = new WastelandSpawner(config);
commandDebugSpawn = new CommandDebugSpawn(spawner);
commandDebugSpawnWeights = new CommandDebugSpawnWeights(spawner);
}
@Override
public void register(JavaPlugin plugin) {
plugin.getCommand("debugspawn").setExecutor(commandDebugSpawn);
plugin.getCommand("debugspawnweights").setExecutor(commandDebugSpawnWeights);
}
@Override
public void unregister(JavaPlugin plugin) {
plugin.getCommand("debugspawn").setExecutor(commandDebugSpawn);
plugin.getCommand("debugspawnweights").setExecutor(commandDebugSpawnWeights);
}
}

View File

@ -17,15 +17,14 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Monster;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.config.MonsterSpawnConfig;
import me.jamestmartin.wasteland.util.Pair;
import me.jamestmartin.wasteland.world.MoonPhase;
public class WastelandSpawner {
private final Map<MonsterType, MonsterSpawnConfig> monsters;
private final SpawnsConfig config;
public WastelandSpawner(Map<MonsterType, MonsterSpawnConfig> monsters) {
this.monsters = monsters;
public WastelandSpawner(SpawnsConfig config) {
this.config = config;
}
public static Collection<MonsterType> spawnableMonstersAt(Location location) {
@ -70,47 +69,39 @@ public class WastelandSpawner {
*
* Graph: https://www.wolframalpha.com/input/?i=f%28x%29+%3D++e%5E%286%282x+-+1%29%29+%2F+%28e%5E%286%282x+-+1%29%29+%2B+1%29++from+0+to+1
*/
private static float logistic(float x) {
float eToX = (float) Math.pow(Math.E, 12 * x - 6);
private static double logistic(double x) {
double eToX = Math.pow(Math.E, 12 * x - 6);
return eToX / (eToX + 1);
}
public static float calculateWeightedLightLevel(Block block, float sunlightWeight, float moonlightWeight, float blocklightWeight) {
float weightedSkylight = MoonPhase.getMoonlight(block) * moonlightWeight + MoonPhase.getSunlight(block) * sunlightWeight;
float weightedBlocklight = block.getLightFromBlocks() * blocklightWeight;
return Math.max(weightedSkylight, weightedBlocklight);
}
public static float calculateWeightedLightLevelProbability(Block block, int maximumLight, float sunlightWeight, float moonlightWeight, float blocklightWeight) {
float weightedLightLevel = calculateWeightedLightLevel(block, sunlightWeight, moonlightWeight, blocklightWeight);
if (weightedLightLevel >= maximumLight) {
public static double calculateWeightedLightLevelProbability(Block block, MonsterSpawnConfig cfg) {
if (!cfg.isWeightedLightLevelWithinBounds(block)) {
return 0.0f;
}
return 1 - logistic(weightedLightLevel / maximumLight);
return 1 - logistic(cfg.calculateWeightedLightLevel(block) / cfg.getMaximumLightLevel());
}
public float calculateLightLevelProbability(MonsterType type, Block block) {
MonsterSpawnConfig cfg = monsters.get(type);
return calculateWeightedLightLevelProbability(block, cfg.maximumLightLevel(), cfg.sunlightWeight(), cfg.moonlightWeight(), cfg.blocklightWeight());
public double calculateLightLevelProbability(MonsterType type, Block block) {
return calculateWeightedLightLevelProbability(block, config.getMonster(type));
}
public float calculateLightLevelProbability(MonsterType type, Location location) {
public double calculateLightLevelProbability(MonsterType type, Location location) {
return calculateLightLevelProbability(type, location.getBlock());
}
public float getMoonPhaseMultiplier(MonsterType type, MoonPhase phase) {
return monsters.get(type).phaseMultipliers().getOrDefault(phase, 1.0f);
public double getMoonPhaseMultiplier(MonsterType type, MoonPhase phase) {
return config.getMonster(type).getPhaseMultipliers().getOrDefault(phase, 1.0);
}
public float getMoonPhaseMultiplier(MonsterType type, World world) {
public double getMoonPhaseMultiplier(MonsterType type, World world) {
return getMoonPhaseMultiplier(type, MoonPhase.fromWorld(world));
}
public float getMoonPhaseMultiplier(MonsterType type, Location location) {
public double getMoonPhaseMultiplier(MonsterType type, Location location) {
return getMoonPhaseMultiplier(type, location.getWorld());
}
public float getMoonPhaseMultiplier(MonsterType type, Block block) {
public double getMoonPhaseMultiplier(MonsterType type, Block block) {
return getMoonPhaseMultiplier(type, block.getWorld());
}
@ -126,7 +117,7 @@ public class WastelandSpawner {
return countNearbyEntities(Player.class, location);
}
public static float calculateNearbyEntitiesMultiplier(Location location) {
public static double calculateNearbyEntitiesMultiplier(Location location) {
final int nearbyMonsters = countNearbyMonsters(location);
// TODO: Change to be a weighted value based on player distance.
final int nearbyPlayers = countNearbyPlayers(location);
@ -138,22 +129,22 @@ public class WastelandSpawner {
// and screwed unless all the other players were helping you fight.)
// It's also not a hard spawn cap, but past a point, the spawn rate needs to be severely diminished
// to prevent massive lag and infinitely-sized hordes.
final float idealMobQuantityMultiplier = (float) Math.max(0.5, Math.sqrt(nearbyPlayers)) * 25;
final double idealMobQuantityMultiplier = Math.max(0.5, Math.sqrt(nearbyPlayers)) * 25;
// constant factor of 2 allows some mobs to spawn beyond the ideal quantity,
// which in the logistic curve makes spawning mobs up to that point far more likely.
return 1 - logistic(nearbyMonsters / idealMobQuantityMultiplier / 2);
}
public static float calculateNearbyEntitiesMultiplier(Block block) {
public static double calculateNearbyEntitiesMultiplier(Block block) {
return calculateNearbyEntitiesMultiplier(block.getLocation());
}
public float calculateSpawnProbability(MonsterType type, Location location) {
final float lightLevelProbability = calculateLightLevelProbability(type, location);
public double calculateSpawnProbability(MonsterType type, Location location) {
final double lightLevelProbability = calculateLightLevelProbability(type, location);
long time = location.getWorld().getTime();
float moonPhaseMultiplier = 0f;
double moonPhaseMultiplier = 0f;
if (time > 12000) { // dawn, dusk, or night
moonPhaseMultiplier = getMoonPhaseMultiplier(type, location);
if (time < 13000 || time > 23000) { // dawn or dusk
@ -161,24 +152,24 @@ public class WastelandSpawner {
}
}
final float nearbyEntitiesMultiplier = calculateNearbyEntitiesMultiplier(location);
final double nearbyEntitiesMultiplier = calculateNearbyEntitiesMultiplier(location);
return lightLevelProbability * moonPhaseMultiplier * nearbyEntitiesMultiplier;
}
public float calculateSpawnProbability(MonsterType type, Block block) {
public double calculateSpawnProbability(MonsterType type, Block block) {
return calculateSpawnProbability(type, block.getLocation());
}
public HashMap<MonsterType, Float> calculateSpawnProbabilities(Location location) {
HashMap<MonsterType, Float> spawnWeights = new HashMap<>();
public HashMap<MonsterType, Double> calculateSpawnProbabilities(Location location) {
HashMap<MonsterType, Double> spawnWeights = new HashMap<>();
for (MonsterType type : spawnableMonstersAt(location)) {
spawnWeights.put(type, calculateSpawnProbability(type, location));
}
return spawnWeights;
}
public HashMap<MonsterType, Float> calculateSpawnProbabilities(Block block) {
public HashMap<MonsterType, Double> calculateSpawnProbabilities(Block block) {
return calculateSpawnProbabilities(block.getLocation());
}
@ -200,14 +191,14 @@ public class WastelandSpawner {
return new Location(center.getWorld(), center.getX() + offX, center.getY() + offY, center.getZ() + offZ);
}
public static Optional<Pair<MonsterType, Double>> chooseWeightedRandomMonster(Random rand, Map<MonsterType, Float> weights) {
double overallSpawnProbability = weights.values().stream().reduce(0.0f, (x, y) -> x + y);
public static Optional<Pair<MonsterType, Double>> chooseWeightedRandomMonster(Random rand, Map<MonsterType, Double> weights) {
double overallSpawnProbability = weights.values().stream().reduce(0.0, (x, y) -> x + y);
if (rand.nextDouble() >= overallSpawnProbability) {
return Optional.empty();
}
double whichMonster = rand.nextDouble() * overallSpawnProbability;
for (Entry<MonsterType, Float> monster : weights.entrySet()) {
for (Entry<MonsterType, Double> monster : weights.entrySet()) {
double successMargin = monster.getValue() - whichMonster;
if (successMargin > 0) {
return Optional.of(new Pair<>(monster.getKey(), successMargin));
@ -220,7 +211,7 @@ public class WastelandSpawner {
}
public Optional<Pair<MonsterType, Double>> pickRandomMonster(Random rand, Block block) {
Map<MonsterType, Float> weights = calculateSpawnProbabilities(block);
Map<MonsterType, Double> weights = calculateSpawnProbabilities(block);
return chooseWeightedRandomMonster(rand, weights);
}

View File

@ -1,4 +1,4 @@
package me.jamestmartin.wasteland;
package me.jamestmartin.wasteland.store;
import java.io.File;
import java.io.IOException;
@ -10,7 +10,9 @@ import java.sql.SQLException;
import java.sql.Statement;
import org.bukkit.entity.Player;
public class Database implements AutoCloseable {
import me.jamestmartin.wasteland.kills.PlayerKillsStore;
public class SqliteDatabase implements AutoCloseable, PlayerKillsStore {
private static final String CREATE_KILLS_TABLE =
"CREATE TABLE IF NOT EXISTS player_kills"
+ "( `player` VARCHAR(36) PRIMARY KEY"
@ -20,18 +22,18 @@ public class Database implements AutoCloseable {
"INSERT OR IGNORE INTO `player_kills`(`player`, `kills`) VALUES (?, 0)";
private static final String GET_PLAYER_KILLS =
"SELECT `kills` FROM `player_kills` WHERE `player` = ?";
private static final String INCREMENT_PLAYER_KILLS =
"UPDATE `player_kills` SET `kills`=`kills` + 1 WHERE `player` = ?";
private static final String ADD_PLAYER_KILLS =
"UPDATE `player_kills` SET `kills`=`kills` + ? WHERE `player` = ?";
private static final String SET_PLAYER_KILLS =
"UPDATE `player_kills` SET `kills` = ? WHERE `player` = ?";
private final Connection connection;
private final PreparedStatement psInitPlayerKills;
private final PreparedStatement psGetPlayerKills;
private final PreparedStatement psIncrementPlayerKills;
private final PreparedStatement psAddPlayerKills;
private final PreparedStatement psSetPlayerKills;
public Database(File file)
public SqliteDatabase(File file)
throws IOException, ClassNotFoundException, SQLException {
if (!file.exists()) {
file.getParentFile().mkdirs();
@ -51,17 +53,19 @@ public class Database implements AutoCloseable {
psInitPlayerKills = connection.prepareStatement(INIT_PLAYER_KILLS);
psGetPlayerKills = connection.prepareStatement(GET_PLAYER_KILLS);
psIncrementPlayerKills = connection.prepareStatement(INCREMENT_PLAYER_KILLS);
psAddPlayerKills = connection.prepareStatement(ADD_PLAYER_KILLS);
psSetPlayerKills = connection.prepareStatement(SET_PLAYER_KILLS);
}
public void initPlayerKills(Player player) throws SQLException {
@Override
public void initPlayer(Player player) throws SQLException {
String playerUUID = player.getUniqueId().toString();
psInitPlayerKills.setString(1, playerUUID);
psInitPlayerKills.executeUpdate();
connection.commit();
}
@Override
public int getPlayerKills(Player player) throws SQLException
{
String playerUUID = player.getUniqueId().toString();
@ -72,10 +76,12 @@ public class Database implements AutoCloseable {
}
}
public void incrementPlayerKills(Player player) throws SQLException {
@Override
public void addPlayerKills(Player player, int kills) throws SQLException {
String playerUUID = player.getUniqueId().toString();
psIncrementPlayerKills.setString(1, playerUUID);
psIncrementPlayerKills.executeUpdate();;
psAddPlayerKills.setInt(1, kills);
psAddPlayerKills.setString(2, playerUUID);
psAddPlayerKills.executeUpdate();;
connection.commit();
}
@ -85,7 +91,7 @@ public class Database implements AutoCloseable {
String playerUUID = player.getUniqueId().toString();
psSetPlayerKills.setInt(1, kills);
psSetPlayerKills.setString(2, playerUUID);
psSetPlayerKills.executeUpdate();;
psSetPlayerKills.executeUpdate();
connection.commit();
}
@ -93,7 +99,7 @@ public class Database implements AutoCloseable {
public void close() throws SQLException {
psInitPlayerKills.close();
psGetPlayerKills.close();
psIncrementPlayerKills.close();
psAddPlayerKills.close();
psSetPlayerKills.close();
connection.commit();
connection.close();

View File

@ -1,24 +1,5 @@
package me.jamestmartin.wasteland.towny;
import java.util.Optional;
public interface TownyDependency extends TownAbbreviationProvider {
import org.bukkit.entity.Player;
import com.palmergames.bukkit.towny.TownyAPI;
import com.palmergames.bukkit.towny.exceptions.NotRegisteredException;
import com.palmergames.bukkit.towny.object.Resident;
public class TownyDependency implements TownAbbreviationProvider {
@Override
public Optional<String> getTownAbbreviation(Player player) {
try {
Resident resident = TownyAPI.getInstance().getDataSource().getResident(player.getName());
String tag = resident.getTown().getTag();
if (tag != null && !tag.equals("")) {
return Optional.of(tag);
}
} catch (NotRegisteredException e) {
}
return Optional.empty();
}
}

View File

@ -4,7 +4,7 @@ import java.util.Optional;
import org.bukkit.entity.Player;
public class TownyDisabled implements TownAbbreviationProvider {
public class TownyDisabled implements TownyDependency {
@Override
public Optional<String> getTownAbbreviation(Player player) {
return Optional.empty();

View File

@ -0,0 +1,24 @@
package me.jamestmartin.wasteland.towny;
import java.util.Optional;
import org.bukkit.entity.Player;
import com.palmergames.bukkit.towny.TownyAPI;
import com.palmergames.bukkit.towny.exceptions.NotRegisteredException;
import com.palmergames.bukkit.towny.object.Resident;
public class TownyEnabled implements TownyDependency {
@Override
public Optional<String> getTownAbbreviation(Player player) {
try {
Resident resident = TownyAPI.getInstance().getDataSource().getResident(player.getName());
String tag = resident.getTown().getTag();
if (tag != null && !tag.equals("")) {
return Optional.of(tag);
}
} catch (NotRegisteredException e) {
}
return Optional.empty();
}
}

View File

@ -32,16 +32,30 @@ chat:
# abbreviation: <rank abbreviation> # optional, defaults to the name
# description: <rank description> # optional, defaults to `enlisted.description`.
# succeeds: <rankID> # optional, the prior rank (defaults to nothing)
# preferred: <bool> # optional, used to disambiguate multiple ranks
# preferred: <rankID> # optional, used to disambiguate multiple ranks
# # which both succeed the same rank.
# # Defaults to `true`.
# # If there is only one successor, it defaults to that.
# color: <ColorCode> # optional
# decoration: <ColorCode> # optional, defaults to `enlisted.decoration`
# kills: <0+> # optional, but the rank won't be used without it
# kills: <0+> # optional, but the rank won't be automatically used without it
# ...
#
# The last listed rank that a player qualifies for will always be the one chosen.
# Players will automatically be promoted to any ranks they have enough kills for.
# You *must* set a default enlisted rank by setting the rank's kills to 0; the plugin will break if you don't.
# Setting multiple default ranks is undefined behavior.
# It may be possible for a player to have no rank in the future, but it is not for now.
#
# Players will automatically be promoted to their preferred rank if they have enough kills for it.
# If there are ambiguous ranks, then:
# * If the player has permission `wasteland.prefer-rank.<rankId>`, then they will be promoted to their preferred rank.
# * If one of the next ranks is `preferred`, then they will be promoted to that rank.
# * Otherwise, they will *not* be promoted until a `prefer-rank` permission is set.
#
# Setting a player's preferred rank will retroactively update their rank to that rank
# (e.g. if a player is MGySgt and you set `wasteland.prefer-rank.fstsgt`, they will become SgtMaj),
# but only when the plugin's configuration (or the whole plugin) is reloaded.
#
# It is also possible to set a player's rank to a rank they do not have the kills for,
# by using the `wasteland.rank.<rankID>` permission.
#
# The default ranks are based on those of the U.S. marines.
@ -51,28 +65,6 @@ chat:
enlisted:
description: "{kills} kills."
promotions:
# The eligible monsters are the entity types which, if killed, will count towards your next promotion.
# Each value must be a valid Bukkit EntityType (e.g. `WITHER_SKELETON` or `ENDERMAN`),
# or one of these built-in entitiy collections:
# * `bosses`: The ender dragon or wither.
# * `hostiles`: Mobs which will attack you on sight, including the bosses.
# * `neutrals`: Mobs which will only attack you under certain conditions,
# e.g. wolves attack you if attacked, enderman attack you if looked at, spiders attack in the dark.
# * `monsters`: Mobs which are eligiable for the "Monster Hunter" achievement,
# which is to say all of the hostile mobs, endermen, spiders, and zombified piglins.
# * `zombies`: All zombies, including zombified piglins.
# * `spiders`: All spiders: regular spiders and cave spiders.
#
# The wiki lists which mobs are bosses, hostile, or neutral: https://minecraft.gamepedia.com/Mob#List_of_mobs
# Monsters are defined as the mobs which are eligible for the Monster Hunter achievement
# (all hostile mobs plus endermen, spiders, and zombified piglins).
#
# This defaults to monsters.
eligible:
# You must give a name for messages to the player, for e.g. "You must kill X more ??? before your next promotion."
name: monsters
entities: [bosses, monsters]
ranks:
# A default rank which is not part of the marines.
fodder:
@ -123,15 +115,15 @@ enlisted:
name: Gunnery Sergeant
abbreviation: GySgt
succeeds: ssgt
preferred: msgt
color: AQUA
kills: 10000
# Alternative DARK_RED rank, currently unused.
# Alternative DARK_RED rank.
fstsgt:
name: First Sergeant
abbreviation: 1stSgt
description: Unused alternative rank for {kills} kills.
description: {kills} kills. Alternative rank to GySgt.
succeeds: gysgt
preferred: false
color: DARK_RED
kills: 25000
msgt:
@ -140,11 +132,11 @@ enlisted:
succeeds: gysgt
color: DARK_RED
kills: 25000
# Alternative DARK_PURPLE rank, currently unused.
# Alternative DARK_PURPLE rank.
sgtmaj:
name: Sergeant Major
abbreviation: SgtMaj
description: Unused alternative rank for {kills} kills.
description: A survivor of the wasteland, with over {kills} kills. Alternative rank to MGySgt, promoted from 1stSgt.
succeeds: fstsgt
color: DARK_PURPLE
kills: 50000
@ -168,7 +160,7 @@ enlisted:
officer:
decoration: BOLD
# The rank used by the console when it makes an official message.
# Defaults to whatever the last rank listed is.
# This *must* be specified or the plugin will break.
console: gas
ranks:
sndlt:
@ -226,6 +218,27 @@ officer:
description: The server console.
color: GOLD
kills:
# The eligible monsters are the entity types which, if killed, will count towards your next promotion.
# Each value must be a valid Bukkit EntityType (e.g. `WITHER_SKELETON` or `ENDERMAN`),
# or one of these built-in entitiy collections:
# * `bosses`: The ender dragon or wither.
# * `hostiles`: Mobs which will attack you on sight, including the bosses.
# * `neutrals`: Mobs which will only attack you under certain conditions,
# e.g. wolves attack you if attacked, enderman attack you if looked at, spiders attack in the dark.
# * `monsters`: Mobs which are eligiable for the "Monster Hunter" achievement,
# which is to say all of the hostile mobs, endermen, spiders, and zombified piglins.
# * `zombies`: All zombies, including zombified piglins.
# * `spiders`: All spiders: regular spiders and cave spiders.
#
# The wiki lists which mobs are bosses, hostile, or neutral: https://minecraft.gamepedia.com/Mob#List_of_mobs
# Monsters are defined as the mobs which are eligible for the Monster Hunter achievement
# (all hostile mobs plus endermen, spiders, and zombified piglins).
eligible:
# You must give a name for messages to the player, for e.g. "You must kill X more ??? before your next promotion."
name: monsters
entities: [bosses, monsters]
#
# spawns:
# # Valid monster types are are CREEPER, SKELETON, SPIDER, and ZOMBIE.

View File

@ -7,30 +7,38 @@ api-version: 1.16
softdepend: [Towny]
commands:
rank:
description: View your rank and how many monsters you have killed.
usage: "Usage: /<command> [<player>]"
permission: wasteland.view-rank
permission-message: You do not have permission to view your rank.
rankeligiblemobs:
description: View a list of monsters which you can kill to get promoted.
usage: "Usage: /<command>"
permission: wasteland.view-eligible-mobs
permission-message: You cannot view the list of promotion-eligible monsters.
ranks:
description: List the server ranks.
usage: "Usage: /<command>"
setkills:
description: Set another player's number of kills for debugging.
usage: "Usage: /<command> [<player>] <kills>"
permission: wasteland.kills.set
permission-message: You do not have permission to set anyone's kills.
wasteland:
description: Reload the plugin's configuration.
usage: "Usage: /<command> reload"
permission: wasteland.reload
permission-message: You do not have permission to reload the plugin's configuration.
official:
description: Make an official announcement using your staff rank.
usage: "Usage: /<command> <message>"
permission: wasteland.official
permission-message: You are not a staff member with an officer rank.
rank:
description: View your rank and how many monsters you have killed.
usage: "Usage: /<command> [<player>]"
permission: wasteland.view-rank
permission-message: You do not have permission to view your rank.
ranks:
description: List the server ranks.
usage: "Usage: /<command>"
rankeligiblemobs:
description: View a list of monsters which you can kill to get promoted.
usage: "Usage: /<command>"
permission: wasteland.view-eligible-mobs
permission-message: You cannot view the list of promotion-eligible monsters.
setkills:
description: Set another player's number of kills for debugging.
usage: "Usage: /<command> [<player>] <kills>"
permission: wasteland.kills.set
permission-message: You do not have permission to set anyone's kills.
# debug commands
debugspawn:
description: Spawn a random monster.
@ -44,6 +52,10 @@ commands:
permission-message: You do not have permission to view spawn debug information.
permissions:
wasteland.reload:
description: Allows you to reload the plugin's configuration.
default: op
wasteland.chat.officer:
description: Show a player's officer rank even in normal chat.
default: false