etc etc etc - all vibecoded - wood stonecutter feature
This commit is contained in:
parent
7fcaaa3221
commit
e2b7f31caf
11 changed files with 407 additions and 107 deletions
|
|
@ -5,9 +5,10 @@ A quality-of-life SMP plugin for Paper/Folia.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Join messages** — a "Welcome to the SMP" title on every join, friendly welcome messages (including a first-join message that lists the server's plugins and a warning that `/nopvp` abuse is bannable and that combat logging is a bad idea), and a special announcement when the owner logs in.
|
- **Join messages** — a "Welcome to the SMP" title on every join, friendly welcome messages (including a first-join message that lists the server's plugins and a warning that `/nopvp` abuse is bannable and that combat logging is a bad idea), and a special announcement when the owner logs in.
|
||||||
- **`/nopvp`** — toggle no-PvP mode. While active you cannot damage other players and other players cannot damage you (direct hits, projectiles, pets, lingering potions, and player-deployed explosives such as TNT, TNT minecarts, end crystals and bed bombs). While enabled a floating **"Pacifist"** label appears above you and, if you're in a team with glow on, you glow in the team's colour (visible to everyone). To stop the immunity being used offensively, protected players (and anyone within **25 blocks** of one) are also blocked from placing, priming or detonating PvP explosives — including redstone-primed TNT whose placer is remembered, and dispensers near a protected player firing TNT/lava/fire charges. No-PvP also can't be switched on while you're in combat. Turning no-PvP *off* starts a **1-hour cooldown** before you can enable it again, and the state persists across restarts so relogging can't bypass it.
|
- **`/nopvp`** — toggle no-PvP mode. While active you cannot damage other players and other players cannot damage you (direct hits, projectiles, pets, lingering potions, and player-deployed explosives such as TNT, TNT minecarts, end crystals and bed bombs). While enabled a floating **"Pacifist"** label appears above you. Team members glow in their team's colour whenever the team glow is on (independent of no-PvP). To stop the immunity being used offensively, protected players (and anyone within **25 blocks** of one) are also blocked from placing, priming or detonating PvP explosives — including redstone-primed TNT whose placer is remembered, and dispensers near a protected player firing TNT/lava/fire charges. Flint & steel is restricted by proximity too: a protected player can't ignite anything within 25 blocks of another player, and a non-protected player can't ignite anything within 25 blocks of a protected player (lighting portals while alone still works). No-PvP also can't be switched on while you're in combat. Turning no-PvP *off* starts a **5-minute cooldown** before you can enable it again, and the state persists across restarts so relogging can't bypass it.
|
||||||
- **Anti-combat-logging** — whenever you hit another player or get hit by one, you're tagged as being in combat for **15 seconds** with a chat warning. Logging off while tagged kills you on the spot and announces it to the server.
|
- **Anti-combat-logging** — whenever you hit another player or get hit by one, you're tagged as being in combat for **15 seconds** with a chat warning. Logging off while tagged kills you on the spot and announces it to the server.
|
||||||
- **Cosmetic teams** — `/team create/delete/invite/leave/join/color/admin/glow/chat`. Teams are purely cosmetic (friendly fire stays on). The owner runs the show and can promote **admins** who may also invite players and change the colour; membership is request-based rather than open. A team's colour becomes the glow colour of any member who has no-PvP enabled (toggleable with `/team glow`, on by default), and `/team chat` gives the team a private chat channel. Stored in `plugins/qol-tweaks/teams.yml`.
|
- **Cosmetic teams** — `/team create/delete/invite/leave/join/color/admin/glow/chat`. Teams are purely cosmetic (friendly fire stays on). The owner runs the show and can promote **admins** who may also invite players and change the colour; membership is request-based rather than open. A team's colour is applied as the glow colour of all its members whenever their personal glow is on — each member can toggle their own with `/team glow` (on by default and remembered across restarts, independent of no-PvP). Members show a `[Team]` tag next to their name in the tab list and above their head, and in normal chat, all coloured with the team's colour; `/team chat` gives the team a private chat channel. Stored in `plugins/qol-tweaks/teams.yml`.
|
||||||
|
- **Woodcutting** — wood works in the stonecutter. A log/wood block (including stripped and stem/hyphae variants) cuts into **4 planks**; planks cut into stairs, slabs, fences, fence gates, buttons, signs and hanging signs; and — because a stonecutter can only ever consume one item — the pricier blocks are cut from a whole log instead: 1 log → 2 doors or 2 pressure plates (2 planks each), 1 trapdoor (~4 planks) and 1 boat (bamboo gives a raft; crimson/warped have none). The cutter also strips bark one-for-one: log/wood/stem/hyphae (and bamboo block) → its stripped form. Covers every wood family. No resource pack needed.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ A quality-of-life SMP plugin for Paper/Folia.
|
||||||
| `/team join <team>` | Accept an invite, or send a join request to the team. | `qoltweaks.team` |
|
| `/team join <team>` | Accept an invite, or send a join request to the team. | `qoltweaks.team` |
|
||||||
| `/team color <clr>` | Set the team glow colour, or `none` to clear (owner/admin). | `qoltweaks.team` |
|
| `/team color <clr>` | Set the team glow colour, or `none` to clear (owner/admin). | `qoltweaks.team` |
|
||||||
| `/team admin <add\|remove> <p>`| Promote/demote a member to admin (owner only). | `qoltweaks.team` |
|
| `/team admin <add\|remove> <p>`| Promote/demote a member to admin (owner only). | `qoltweaks.team` |
|
||||||
| `/team glow` | Toggle the whole team's no-PvP glow (owner/admin). | `qoltweaks.team` |
|
| `/team glow` | Toggle your **own** no-PvP glow (default on, remembered). | `qoltweaks.team` |
|
||||||
| `/team chat [message]` | Send a team-only message, or toggle team chat mode. | `qoltweaks.team` |
|
| `/team chat [message]` | Send a team-only message, or toggle team chat mode. | `qoltweaks.team` |
|
||||||
| `/team leave` | Leave your team (owners must delete instead). | `qoltweaks.team` |
|
| `/team leave` | Leave your team (owners must delete instead). | `qoltweaks.team` |
|
||||||
| `/team delete` | Disband the whole team (owner only). | `qoltweaks.team` |
|
| `/team delete` | Disband the whole team (owner only). | `qoltweaks.team` |
|
||||||
|
|
@ -28,7 +29,7 @@ All permissions default to `true`. Note `/team` takes over the vanilla scoreboar
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
Player no-PvP state and cooldown timestamps are stored in `plugins/qol-tweaks/nopvp.yml` so they survive restarts and reloads. Teams, ownership, admins, members, colours and the glow toggle are stored in `plugins/qol-tweaks/teams.yml`.
|
Player no-PvP state and cooldown timestamps are stored in `plugins/qol-tweaks/nopvp.yml` so they survive restarts and reloads. Teams, ownership, admins, members, colours and each player's glow preference are stored in `plugins/qol-tweaks/teams.yml`.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.dvorakmortonxd.qoltweaks.nopvp.NoPvPProtectionListener;
|
||||||
import com.dvorakmortonxd.qoltweaks.teams.TeamCommand;
|
import com.dvorakmortonxd.qoltweaks.teams.TeamCommand;
|
||||||
import com.dvorakmortonxd.qoltweaks.teams.TeamListener;
|
import com.dvorakmortonxd.qoltweaks.teams.TeamListener;
|
||||||
import com.dvorakmortonxd.qoltweaks.teams.TeamManager;
|
import com.dvorakmortonxd.qoltweaks.teams.TeamManager;
|
||||||
|
import com.dvorakmortonxd.qoltweaks.woodcutter.WoodcutterRecipes;
|
||||||
import org.bukkit.command.PluginCommand;
|
import org.bukkit.command.PluginCommand;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,8 @@ public final class QoLtweaks extends JavaPlugin {
|
||||||
getServer().getPluginManager().registerEvents(new NoPvPProtectionListener(noPvPManager), this);
|
getServer().getPluginManager().registerEvents(new NoPvPProtectionListener(noPvPManager), this);
|
||||||
getServer().getPluginManager().registerEvents(new CombatLogListener(this, combatLogManager), this);
|
getServer().getPluginManager().registerEvents(new CombatLogListener(this, combatLogManager), this);
|
||||||
|
|
||||||
|
WoodcutterRecipes.register(this);
|
||||||
|
|
||||||
PluginCommand noPvPCommand = getCommand("nopvp");
|
PluginCommand noPvPCommand = getCommand("nopvp");
|
||||||
if (noPvPCommand != null) {
|
if (noPvPCommand != null) {
|
||||||
noPvPCommand.setExecutor(new NoPvPCommand(noPvPManager, combatLogManager));
|
noPvPCommand.setExecutor(new NoPvPCommand(noPvPManager, combatLogManager));
|
||||||
|
|
@ -43,6 +46,8 @@ public final class QoLtweaks extends JavaPlugin {
|
||||||
teamCommand.setExecutor(executor);
|
teamCommand.setExecutor(executor);
|
||||||
teamCommand.setTabCompleter(executor);
|
teamCommand.setTabCompleter(executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noPvPManager.applyToAllOnline();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public final class NoPvPCommand implements CommandExecutor {
|
||||||
if (manager.isEnabled(player)) {
|
if (manager.isEnabled(player)) {
|
||||||
manager.disable(player);
|
manager.disable(player);
|
||||||
player.sendMessage("§cYou turned off no-PvP. You can now be attacked by other players again.");
|
player.sendMessage("§cYou turned off no-PvP. You can now be attacked by other players again.");
|
||||||
player.sendMessage("§cYou must wait 1 hour before you can turn no-PvP back on.");
|
player.sendMessage("§cYou must wait 5 minutes before you can turn no-PvP back on.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public final class NoPvPManager {
|
public final class NoPvPManager {
|
||||||
|
|
||||||
public static final long COOLDOWN_MILLIS = 60 * 60 * 1000L; // 1 hour
|
public static final long COOLDOWN_MILLIS = 5 * 60 * 1000L; // 5 minutes
|
||||||
|
|
||||||
/** How close a no-PvP player has to be before PvP gear can't be placed or detonated. */
|
/** How close a no-PvP player has to be before PvP gear can't be placed or detonated. */
|
||||||
public static final double PROTECTION_RADIUS = 25.0;
|
public static final double PROTECTION_RADIUS = 25.0;
|
||||||
|
|
@ -52,6 +52,19 @@ public final class NoPvPManager {
|
||||||
return !world.getNearbyPlayers(location, radius, this::isEnabled).isEmpty();
|
return !world.getNearbyPlayers(location, radius, this::isEnabled).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether any player other than {@code exclude} is within {@code radius} of
|
||||||
|
* the location.
|
||||||
|
*/
|
||||||
|
public boolean isPlayerWithin(Location location, double radius, UUID exclude) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !world.getNearbyPlayers(location, radius,
|
||||||
|
player -> !player.getUniqueId().equals(exclude)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
public long getRemainingCooldownMillis(Player player) {
|
public long getRemainingCooldownMillis(Player player) {
|
||||||
long until = cooldownUntil.getOrDefault(player.getUniqueId(), 0L);
|
long until = cooldownUntil.getOrDefault(player.getUniqueId(), 0L);
|
||||||
return Math.max(0L, until - System.currentTimeMillis());
|
return Math.max(0L, until - System.currentTimeMillis());
|
||||||
|
|
@ -74,16 +87,26 @@ public final class NoPvPManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the no-PvP visuals for a player: the Pacifist hologram plus, if
|
* Applies the no-PvP visuals for a player: the Pacifist hologram while
|
||||||
* they are in a team with glow enabled, the team-coloured glow.
|
* no-PvP is on, plus the team-coloured glow whenever they are in a team and
|
||||||
|
* haven't turned their own glow off (independent of no-PvP).
|
||||||
*/
|
*/
|
||||||
public void applyState(Player player) {
|
public void applyState(Player player) {
|
||||||
if (isEnabled(player)) {
|
if (isEnabled(player)) {
|
||||||
hologram.show(player);
|
hologram.show(player);
|
||||||
player.setGlowing(teamManager != null && teamManager.shouldGlow(player));
|
|
||||||
} else {
|
} else {
|
||||||
hologram.hide(player);
|
hologram.hide(player);
|
||||||
player.setGlowing(false);
|
}
|
||||||
|
player.setGlowing(teamManager != null && teamManager.shouldGlow(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-applies visuals to every online player on their own region thread.
|
||||||
|
* Used on startup/reload when there is no join event.
|
||||||
|
*/
|
||||||
|
public void applyToAllOnline() {
|
||||||
|
for (Player player : plugin.getServer().getOnlinePlayers()) {
|
||||||
|
refreshLater(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import org.bukkit.event.block.Action;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
import org.bukkit.event.block.BlockDispenseEvent;
|
import org.bukkit.event.block.BlockDispenseEvent;
|
||||||
import org.bukkit.event.block.BlockExplodeEvent;
|
import org.bukkit.event.block.BlockExplodeEvent;
|
||||||
|
import org.bukkit.event.block.BlockIgniteEvent;
|
||||||
import org.bukkit.event.block.BlockPlaceEvent;
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
import org.bukkit.event.block.TNTPrimeEvent;
|
import org.bukkit.event.block.TNTPrimeEvent;
|
||||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
|
@ -154,6 +155,29 @@ public final class NoPvPProtectionListener implements Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flint & steel: a no-PvP player can't ignite anything within 25 blocks of
|
||||||
|
* another player, and a non-protected player can't ignite anything within 25
|
||||||
|
* blocks of a protected player. Lighting portals etc. while alone still works.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onBlockIgnite(BlockIgniteEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
if (player == null || event.getCause() != BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location location = event.getBlock().getLocation();
|
||||||
|
if (manager.isEnabled(player)) {
|
||||||
|
if (manager.isPlayerWithin(location, NoPvPManager.PROTECTION_RADIUS, player.getUniqueId())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage("§cYou can't use flint and steel near other players while no-PvP is on.");
|
||||||
|
}
|
||||||
|
} else if (manager.isProtectedWithin(location, NoPvPManager.PROTECTION_RADIUS)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage("§cYou can't use flint and steel near a no-PvP player.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void onEntityExplode(EntityExplodeEvent event) {
|
public void onEntityExplode(EntityExplodeEvent event) {
|
||||||
Entity explosive = event.getEntity();
|
Entity explosive = event.getEntity();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.dvorakmortonxd.qoltweaks.nopvp;
|
||||||
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
|
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Color;
|
import org.bukkit.Color;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Display;
|
import org.bukkit.entity.Display;
|
||||||
|
|
@ -15,29 +16,35 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a floating "Pacifist" label above a no-PvP player. The label is a
|
* Shows a floating "Pacifist" label above a no-PvP player.
|
||||||
* non-persistent {@link TextDisplay} that follows its player via that player's
|
* <p>
|
||||||
* Folia entity scheduler, so it stays on the owning region thread.
|
* The label is a plain (non-persistent) {@link TextDisplay} that is teleported
|
||||||
|
* to an absolute height above the player by a global follow task, with the
|
||||||
|
* actual teleport dispatched on the region that owns the label. Using an
|
||||||
|
* absolute height (rather than a passenger mount) keeps the label clear of the
|
||||||
|
* player's head and name tag.
|
||||||
*/
|
*/
|
||||||
public final class PacifistHologram {
|
public final class PacifistHologram {
|
||||||
|
|
||||||
private static final String TAG = "qol_pacifist";
|
private static final String TAG = "qol_pacifist";
|
||||||
/** Blocks above the player's feet; ~1 block above the head, one line above the name tag. */
|
/** Height of the label's centre, in blocks above the player's feet. */
|
||||||
private static final double HEIGHT_OFFSET = 2.8;
|
private static final double TARGET_HEIGHT = 2.5;
|
||||||
|
|
||||||
private final JavaPlugin plugin;
|
private final JavaPlugin plugin;
|
||||||
private final Map<UUID, TextDisplay> displays = new ConcurrentHashMap<>();
|
private final Map<UUID, TextDisplay> displays = new ConcurrentHashMap<>();
|
||||||
private final Map<UUID, ScheduledTask> tasks = new ConcurrentHashMap<>();
|
private final ScheduledTask followTask;
|
||||||
|
|
||||||
public PacifistHologram(JavaPlugin plugin) {
|
public PacifistHologram(JavaPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.followTask = plugin.getServer().getGlobalRegionScheduler()
|
||||||
|
.runAtFixedRate(plugin, task -> followAll(), 1L, 2L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show(Player player) {
|
public void show(Player player) {
|
||||||
UUID uuid = player.getUniqueId();
|
UUID uuid = player.getUniqueId();
|
||||||
TextDisplay current = displays.get(uuid);
|
TextDisplay current = displays.get(uuid);
|
||||||
if (current != null && current.isValid() && current.getWorld().equals(player.getWorld())) {
|
if (current != null && current.isValid() && current.getWorld().equals(player.getWorld())) {
|
||||||
current.teleport(positionOf(player));
|
current.teleport(targetOf(player));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hide(player);
|
hide(player);
|
||||||
|
|
@ -45,31 +52,22 @@ public final class PacifistHologram {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hide(Player player) {
|
public void hide(Player player) {
|
||||||
UUID uuid = player.getUniqueId();
|
removeEntity(displays.remove(player.getUniqueId()));
|
||||||
ScheduledTask task = tasks.remove(uuid);
|
|
||||||
if (task != null) {
|
|
||||||
task.cancel();
|
|
||||||
}
|
|
||||||
removeEntity(displays.remove(uuid));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeAll() {
|
public void removeAll() {
|
||||||
|
if (followTask != null) {
|
||||||
|
followTask.cancel();
|
||||||
|
}
|
||||||
for (UUID uuid : displays.keySet().toArray(new UUID[0])) {
|
for (UUID uuid : displays.keySet().toArray(new UUID[0])) {
|
||||||
ScheduledTask task = tasks.remove(uuid);
|
|
||||||
if (task != null) {
|
|
||||||
task.cancel();
|
|
||||||
}
|
|
||||||
removeEntity(displays.remove(uuid));
|
removeEntity(displays.remove(uuid));
|
||||||
}
|
}
|
||||||
tasks.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawn(Player player) {
|
private void spawn(Player player) {
|
||||||
UUID uuid = player.getUniqueId();
|
TextDisplay display = player.getWorld().spawn(targetOf(player), TextDisplay.class, entity -> {
|
||||||
Location location = positionOf(player);
|
|
||||||
TextDisplay display = location.getWorld().spawn(location, TextDisplay.class, entity -> {
|
|
||||||
entity.text(Component.text("Pacifist", NamedTextColor.GOLD));
|
entity.text(Component.text("Pacifist", NamedTextColor.GOLD));
|
||||||
entity.setBillboard(Display.Billboard.CENTER);
|
entity.setBillboard(Display.Billboard.VERTICAL);
|
||||||
entity.setSeeThrough(false);
|
entity.setSeeThrough(false);
|
||||||
entity.setShadowed(true);
|
entity.setShadowed(true);
|
||||||
entity.setDefaultBackground(false);
|
entity.setDefaultBackground(false);
|
||||||
|
|
@ -78,37 +76,33 @@ public final class PacifistHologram {
|
||||||
entity.setPersistent(false);
|
entity.setPersistent(false);
|
||||||
entity.addScoreboardTag(TAG);
|
entity.addScoreboardTag(TAG);
|
||||||
});
|
});
|
||||||
displays.put(uuid, display);
|
displays.put(player.getUniqueId(), display);
|
||||||
|
}
|
||||||
|
|
||||||
ScheduledTask task = player.getScheduler().runAtFixedRate(plugin,
|
private void followAll() {
|
||||||
scheduled -> tick(player, display, scheduled), null, 1L, 2L);
|
if (displays.isEmpty()) {
|
||||||
if (task != null) {
|
return;
|
||||||
tasks.put(uuid, task);
|
}
|
||||||
|
for (Map.Entry<UUID, TextDisplay> entry : displays.entrySet()) {
|
||||||
|
Player player = Bukkit.getPlayer(entry.getKey());
|
||||||
|
TextDisplay display = entry.getValue();
|
||||||
|
if (player == null || !player.isOnline() || !display.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!display.getWorld().equals(player.getWorld())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Location target = targetOf(player);
|
||||||
|
Bukkit.getRegionScheduler().run(plugin, target, task -> {
|
||||||
|
if (display.isValid()) {
|
||||||
|
display.teleport(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tick(Player player, TextDisplay display, ScheduledTask task) {
|
private Location targetOf(Player player) {
|
||||||
UUID uuid = player.getUniqueId();
|
return player.getLocation().add(0, TARGET_HEIGHT, 0);
|
||||||
if (displays.get(uuid) != display) {
|
|
||||||
task.cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!display.isValid() || !player.isOnline()) {
|
|
||||||
displays.remove(uuid, display);
|
|
||||||
tasks.remove(uuid);
|
|
||||||
task.cancel();
|
|
||||||
removeEntity(display);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!display.getWorld().equals(player.getWorld())) {
|
|
||||||
displays.remove(uuid, display);
|
|
||||||
tasks.remove(uuid);
|
|
||||||
task.cancel();
|
|
||||||
removeEntity(display);
|
|
||||||
spawn(player);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
display.teleport(positionOf(player));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeEntity(TextDisplay display) {
|
private void removeEntity(TextDisplay display) {
|
||||||
|
|
@ -116,13 +110,9 @@ public final class PacifistHologram {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
display.getScheduler().run(plugin, scheduled -> display.remove(), null);
|
display.getScheduler().run(plugin, task -> display.remove(), null);
|
||||||
} catch (RuntimeException ignored) {
|
} catch (RuntimeException ignored) {
|
||||||
// Plugin is shutting down / scheduler unavailable; entity is non-persistent.
|
// Plugin shutting down / scheduler unavailable; the entity is non-persistent.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Location positionOf(Player player) {
|
|
||||||
return player.getLocation().add(0, HEIGHT_OFFSET, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A purely cosmetic player team. Membership is tracked by UUID, the colour is
|
* A purely cosmetic player team. Membership is tracked by UUID and the team's
|
||||||
* applied to every member's no-PvP glow, and the glow can be switched off for
|
* colour is applied to each member's no-PvP glow (which each member can toggle
|
||||||
* the whole team.
|
* off for themselves).
|
||||||
*/
|
*/
|
||||||
public final class Team {
|
public final class Team {
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ public final class Team {
|
||||||
private final Set<UUID> members = new LinkedHashSet<>();
|
private final Set<UUID> members = new LinkedHashSet<>();
|
||||||
private final Set<UUID> admins = new LinkedHashSet<>();
|
private final Set<UUID> admins = new LinkedHashSet<>();
|
||||||
private NamedTextColor color;
|
private NamedTextColor color;
|
||||||
private boolean glowEnabled = true;
|
|
||||||
|
|
||||||
public Team(UUID id, String name, UUID owner) {
|
public Team(UUID id, String name, UUID owner) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
@ -85,14 +84,6 @@ public final class Team {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isGlowEnabled() {
|
|
||||||
return glowEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGlowEnabled(boolean glowEnabled) {
|
|
||||||
this.glowEnabled = glowEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addMember(UUID uuid) {
|
public void addMember(UUID uuid) {
|
||||||
members.add(uuid);
|
members.add(uuid);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ public final class TeamCommand implements CommandExecutor, TabCompleter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
manager.create(player, name);
|
manager.create(player, name);
|
||||||
|
noPvPManager.refreshLater(player);
|
||||||
player.sendMessage("§aTeam §e" + name + "§a created! Invite players with §e/team invite <player>§a.");
|
player.sendMessage("§aTeam §e" + name + "§a created! Invite players with §e/team invite <player>§a.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,20 +286,11 @@ public final class TeamCommand implements CommandExecutor, TabCompleter {
|
||||||
player.sendMessage("§cYou're not in a team.");
|
player.sendMessage("§cYou're not in a team.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!team.isAdmin(player.getUniqueId())) {
|
boolean enabled = manager.toggleGlow(player.getUniqueId());
|
||||||
player.sendMessage("§cOnly the team owner or an admin can toggle the team glow.");
|
noPvPManager.refreshLater(player);
|
||||||
return;
|
player.sendMessage(enabled
|
||||||
}
|
? "§aYour no-PvP glow was turned on."
|
||||||
boolean enabled = manager.toggleGlow(team);
|
: "§cYour no-PvP glow was turned off.");
|
||||||
for (UUID member : new ArrayList<>(team.getMembers())) {
|
|
||||||
Player online = Bukkit.getPlayer(member);
|
|
||||||
if (online != null) {
|
|
||||||
noPvPManager.refreshLater(online);
|
|
||||||
online.sendMessage(enabled
|
|
||||||
? "§aYour team's no-PvP glow was turned on."
|
|
||||||
: "§cYour team's no-PvP glow was turned off.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void chat(Player player, String[] args) {
|
private void chat(Player player, String[] args) {
|
||||||
|
|
@ -359,7 +351,7 @@ public final class TeamCommand implements CommandExecutor, TabCompleter {
|
||||||
player.sendMessage("§e/team join <team> §7- request or accept an invite");
|
player.sendMessage("§e/team join <team> §7- request or accept an invite");
|
||||||
player.sendMessage("§e/team color <color> §7- set the team's glow colour");
|
player.sendMessage("§e/team color <color> §7- set the team's glow colour");
|
||||||
player.sendMessage("§e/team admin <add|remove> <player> §7- manage admins");
|
player.sendMessage("§e/team admin <add|remove> <player> §7- manage admins");
|
||||||
player.sendMessage("§e/team glow §7- toggle the team glow");
|
player.sendMessage("§e/team glow §7- toggle your own glow");
|
||||||
player.sendMessage("§e/team chat [message] §7- team chat / toggle it");
|
player.sendMessage("§e/team chat [message] §7- team chat / toggle it");
|
||||||
player.sendMessage("§e/team leave §7- leave your team");
|
player.sendMessage("§e/team leave §7- leave your team");
|
||||||
player.sendMessage("§e/team delete §7- disband your team (owner)");
|
player.sendMessage("§e/team delete §7- disband your team (owner)");
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,39 @@ public final class TeamListener implements Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redirects chat into the team channel when a player has team chat toggled on. */
|
/**
|
||||||
|
* Redirects chat into the team channel when a player has team chat toggled
|
||||||
|
* on, and otherwise adds the team's tag in front of normal chat messages.
|
||||||
|
*/
|
||||||
@EventHandler(priority = EventPriority.LOWEST)
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
public void onChat(AsyncChatEvent event) {
|
public void onChat(AsyncChatEvent event) {
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
if (!manager.isTeamChat(player.getUniqueId())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Team team = manager.teamOf(player);
|
Team team = manager.teamOf(player);
|
||||||
if (team == null) {
|
|
||||||
manager.clearTeamChat(player.getUniqueId());
|
if (manager.isTeamChat(player.getUniqueId())) {
|
||||||
|
if (team == null) {
|
||||||
|
manager.clearTeamChat(player.getUniqueId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redirectToTeam(event, team);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (team == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NamedTextColor color = team.getColor() != null ? team.getColor() : NamedTextColor.AQUA;
|
||||||
|
Component tag = TeamManager.tag(team.getName(), color);
|
||||||
|
Component name = Component.text(player.getName(), color);
|
||||||
|
event.renderer((source, sourceDisplayName, message, viewer) ->
|
||||||
|
tag.append(Component.text("<", NamedTextColor.WHITE))
|
||||||
|
.append(name)
|
||||||
|
.append(Component.text("> ", NamedTextColor.WHITE))
|
||||||
|
.append(message.colorIfAbsent(NamedTextColor.WHITE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redirectToTeam(AsyncChatEvent event, Team team) {
|
||||||
Set<Audience> viewers = event.viewers();
|
Set<Audience> viewers = event.viewers();
|
||||||
viewers.clear();
|
viewers.clear();
|
||||||
for (UUID member : team.getMembers()) {
|
for (UUID member : team.getMembers()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.dvorakmortonxd.qoltweaks.teams;
|
package com.dvorakmortonxd.qoltweaks.teams;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
|
@ -44,6 +45,7 @@ public final class TeamManager {
|
||||||
private final Map<UUID, UUID> joinRequests = new ConcurrentHashMap<>(); // player -> team id
|
private final Map<UUID, UUID> joinRequests = new ConcurrentHashMap<>(); // player -> team id
|
||||||
private final Map<UUID, Set<UUID>> invites = new ConcurrentHashMap<>(); // team id -> invited players
|
private final Map<UUID, Set<UUID>> invites = new ConcurrentHashMap<>(); // team id -> invited players
|
||||||
private final Set<UUID> teamChat = ConcurrentHashMap.newKeySet(); // players with team chat toggled on
|
private final Set<UUID> teamChat = ConcurrentHashMap.newKeySet(); // players with team chat toggled on
|
||||||
|
private final Set<UUID> glowOff = ConcurrentHashMap.newKeySet(); // players who turned their own glow off
|
||||||
|
|
||||||
public TeamManager(JavaPlugin plugin) {
|
public TeamManager(JavaPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
|
@ -77,6 +79,14 @@ public final class TeamManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The {@code [Team]} tag shown in chat and next to the name in the tab list. */
|
||||||
|
public static Component tag(String teamName, NamedTextColor color) {
|
||||||
|
NamedTextColor tagColor = color != null ? color : NamedTextColor.AQUA;
|
||||||
|
return Component.text("[", NamedTextColor.DARK_GRAY)
|
||||||
|
.append(Component.text(teamName, tagColor))
|
||||||
|
.append(Component.text("] ", NamedTextColor.DARK_GRAY));
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized Team teamOf(Player player) {
|
public synchronized Team teamOf(Player player) {
|
||||||
UUID teamId = teamByMember.get(player.getUniqueId());
|
UUID teamId = teamByMember.get(player.getUniqueId());
|
||||||
return teamId == null ? null : teams.get(teamId);
|
return teamId == null ? null : teams.get(teamId);
|
||||||
|
|
@ -173,22 +183,26 @@ public final class TeamManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean toggleGlow(Team team) {
|
/**
|
||||||
|
* Toggles this player's own glow preference and returns the new state
|
||||||
|
* ({@code true} = glowing).
|
||||||
|
*/
|
||||||
|
public boolean toggleGlow(UUID player) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
boolean enabled = !team.isGlowEnabled();
|
if (glowOff.remove(player)) {
|
||||||
team.setGlowEnabled(enabled);
|
scheduleSave();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
glowOff.add(player);
|
||||||
scheduleSave();
|
scheduleSave();
|
||||||
return enabled;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the player should glow: they must be in a team and not have opted out. */
|
||||||
public synchronized boolean shouldGlow(Player player) {
|
public synchronized boolean shouldGlow(Player player) {
|
||||||
UUID teamId = teamByMember.get(player.getUniqueId());
|
UUID uuid = player.getUniqueId();
|
||||||
if (teamId == null) {
|
return teamByMember.containsKey(uuid) && !glowOff.contains(uuid);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Team team = teams.get(teamId);
|
|
||||||
return team != null && team.isGlowEnabled();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTeamChat(UUID player) {
|
public boolean isTeamChat(UUID player) {
|
||||||
|
|
@ -275,6 +289,7 @@ public final class TeamManager {
|
||||||
|
|
||||||
private void refreshScoreboardNow(UUID teamId) {
|
private void refreshScoreboardNow(UUID teamId) {
|
||||||
NamedTextColor color;
|
NamedTextColor color;
|
||||||
|
String teamName;
|
||||||
List<String> names = new ArrayList<>();
|
List<String> names = new ArrayList<>();
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Team team = teams.get(teamId);
|
Team team = teams.get(teamId);
|
||||||
|
|
@ -282,6 +297,7 @@ public final class TeamManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
color = team.getColor();
|
color = team.getColor();
|
||||||
|
teamName = team.getName();
|
||||||
for (UUID member : team.getMembers()) {
|
for (UUID member : team.getMembers()) {
|
||||||
Player online = Bukkit.getPlayer(member);
|
Player online = Bukkit.getPlayer(member);
|
||||||
if (online != null) {
|
if (online != null) {
|
||||||
|
|
@ -298,6 +314,9 @@ public final class TeamManager {
|
||||||
board.setAllowFriendlyFire(true);
|
board.setAllowFriendlyFire(true);
|
||||||
board.setCanSeeFriendlyInvisibles(true);
|
board.setCanSeeFriendlyInvisibles(true);
|
||||||
board.color(color);
|
board.color(color);
|
||||||
|
// The prefix is what the client shows next to the name in the tab list
|
||||||
|
// (and above the player's head).
|
||||||
|
board.prefix(tag(teamName, color));
|
||||||
|
|
||||||
Set<String> wanted = Set.copyOf(names);
|
Set<String> wanted = Set.copyOf(names);
|
||||||
for (String entry : new ArrayList<>(board.getEntries())) {
|
for (String entry : new ArrayList<>(board.getEntries())) {
|
||||||
|
|
@ -321,6 +340,12 @@ public final class TeamManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile);
|
YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile);
|
||||||
|
for (String entry : config.getStringList("glow-off")) {
|
||||||
|
UUID uuid = parseUuid(entry);
|
||||||
|
if (uuid != null) {
|
||||||
|
glowOff.add(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
ConfigurationSection root = config.getConfigurationSection("teams");
|
ConfigurationSection root = config.getConfigurationSection("teams");
|
||||||
if (root == null) {
|
if (root == null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -350,7 +375,6 @@ public final class TeamManager {
|
||||||
team.addAdmin(uuid);
|
team.addAdmin(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
team.setGlowEnabled(section.getBoolean("glow", true));
|
|
||||||
NamedTextColor color = parseColor(section.getString("color"));
|
NamedTextColor color = parseColor(section.getString("color"));
|
||||||
if (color != null) {
|
if (color != null) {
|
||||||
team.setColor(color);
|
team.setColor(color);
|
||||||
|
|
@ -388,8 +412,12 @@ public final class TeamManager {
|
||||||
admins.add(admin.toString());
|
admins.add(admin.toString());
|
||||||
}
|
}
|
||||||
root.set(path + ".admins", admins);
|
root.set(path + ".admins", admins);
|
||||||
root.set(path + ".glow", team.isGlowEnabled());
|
|
||||||
}
|
}
|
||||||
|
List<String> glowOffList = new ArrayList<>();
|
||||||
|
for (UUID uuid : glowOff) {
|
||||||
|
glowOffList.add(uuid.toString());
|
||||||
|
}
|
||||||
|
config.set("glow-off", glowOffList);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
package com.dvorakmortonxd.qoltweaks.woodcutter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.RecipeChoice;
|
||||||
|
import org.bukkit.inventory.StonecuttingRecipe;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes wood usable in the stonecutter.
|
||||||
|
* <p>
|
||||||
|
* The vanilla stonecutter only accepts an item when at least one stonecutting
|
||||||
|
* recipe matches it, so registering wood recipes both lists them in the UI and
|
||||||
|
* makes the whole wood family valid input. Recipes are added to the server's
|
||||||
|
* recipe manager at runtime; the server rebuilds its cached stonecutter recipe
|
||||||
|
* set and resyncs clients automatically, so this works on a running vanilla
|
||||||
|
* client with no resource pack.
|
||||||
|
* <p>
|
||||||
|
* Yields mirror the stonecutter's usual "one input, N output" style: a log is
|
||||||
|
* worth 4 planks (as when crafting), and a plank cuts into one shaped block
|
||||||
|
* (two for slabs). Doors, trapdoors and pressure plates are cut from a whole log
|
||||||
|
* rather than a single plank, so they cost about a log's worth of planks instead
|
||||||
|
* of one. The cutter can also strip bark (log to stripped log, and so on)
|
||||||
|
* one-for-one.
|
||||||
|
*/
|
||||||
|
public final class WoodcutterRecipes {
|
||||||
|
|
||||||
|
private WoodcutterRecipes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An output block type, named by the suffix appended to the wood prefix. */
|
||||||
|
private record Shape(String suffix, int amount) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wood family: the {@code <prefix>_PLANKS} block, the blocks treated as
|
||||||
|
* logs, and the matching boat material ({@code null} for families without
|
||||||
|
* one, i.e. crimson and warped).
|
||||||
|
*/
|
||||||
|
private record Family(String prefix, List<String> logMaterials, String boat) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A block that can be stripped into another by the stonecutter. */
|
||||||
|
private record Strip(String input, String output) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks cut straight from a single plank. Doors, trapdoors and pressure
|
||||||
|
* plates are deliberately absent: they are cut from a whole log instead (see
|
||||||
|
* {@link #COSTLY}) so they cost a log's worth of planks rather than one.
|
||||||
|
*/
|
||||||
|
private static final List<Shape> SHAPES = List.of(
|
||||||
|
new Shape("STAIRS", 1),
|
||||||
|
new Shape("SLAB", 2),
|
||||||
|
new Shape("FENCE", 1),
|
||||||
|
new Shape("FENCE_GATE", 1),
|
||||||
|
new Shape("BUTTON", 1),
|
||||||
|
new Shape("SIGN", 1),
|
||||||
|
new Shape("HANGING_SIGN", 1));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "expensive" blocks, cut from a whole log (worth 4 planks) so the cost
|
||||||
|
* is closer to crafting: 1 log gives 2 doors or 2 pressure plates (2 planks
|
||||||
|
* each) and 1 trapdoor (~4 planks). A boat is handled separately.
|
||||||
|
*/
|
||||||
|
private static final List<Shape> COSTLY = List.of(
|
||||||
|
new Shape("DOOR", 2),
|
||||||
|
new Shape("TRAPDOOR", 1),
|
||||||
|
new Shape("PRESSURE_PLATE", 2));
|
||||||
|
|
||||||
|
private static final List<Family> FAMILIES = List.of(
|
||||||
|
new Family("OAK", standardLogs("OAK"), "OAK_BOAT"),
|
||||||
|
new Family("SPRUCE", standardLogs("SPRUCE"), "SPRUCE_BOAT"),
|
||||||
|
new Family("BIRCH", standardLogs("BIRCH"), "BIRCH_BOAT"),
|
||||||
|
new Family("JUNGLE", standardLogs("JUNGLE"), "JUNGLE_BOAT"),
|
||||||
|
new Family("ACACIA", standardLogs("ACACIA"), "ACACIA_BOAT"),
|
||||||
|
new Family("DARK_OAK", standardLogs("DARK_OAK"), "DARK_OAK_BOAT"),
|
||||||
|
new Family("MANGROVE", withExtra(standardLogs("MANGROVE"),
|
||||||
|
"MANGROVE_ROOTS", "MUDDY_MANGROVE_ROOTS"), "MANGROVE_BOAT"),
|
||||||
|
new Family("CHERRY", standardLogs("CHERRY"), "CHERRY_BOAT"),
|
||||||
|
new Family("PALE_OAK", standardLogs("PALE_OAK"), "PALE_OAK_BOAT"),
|
||||||
|
new Family("BAMBOO", List.of("BAMBOO_BLOCK", "STRIPPED_BAMBOO_BLOCK"), "BAMBOO_RAFT"),
|
||||||
|
new Family("CRIMSON", stemLogs("CRIMSON"), null),
|
||||||
|
new Family("WARPED", stemLogs("WARPED"), null));
|
||||||
|
|
||||||
|
/** Bark-stripping recipes: barked block in, stripped block out. */
|
||||||
|
private static final List<Strip> STRIPS = buildStrips();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers every woodcutting recipe with the server. Safe to call on every
|
||||||
|
* enable: recipes are keyed, so re-registering simply replaces them.
|
||||||
|
*/
|
||||||
|
public static void register(Plugin plugin) {
|
||||||
|
Logger log = plugin.getLogger();
|
||||||
|
int added = 0;
|
||||||
|
for (Family family : FAMILIES) {
|
||||||
|
Material planks = match(family.prefix() + "_PLANKS");
|
||||||
|
if (planks == null) {
|
||||||
|
log.warning("Woodcutter: unknown planks material for " + family.prefix());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Material> logs = resolve(log, family.logMaterials());
|
||||||
|
if (!logs.isEmpty()) {
|
||||||
|
if (add(plugin, key(family, "planks_from_logs"), new ItemStack(planks, 4),
|
||||||
|
new RecipeChoice.MaterialChoice(logs))) {
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Shape shape : COSTLY) {
|
||||||
|
Material output = match(family.prefix() + "_" + shape.suffix());
|
||||||
|
if (output == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (add(plugin, key(family, shape.suffix().toLowerCase(Locale.ROOT) + "_from_logs"),
|
||||||
|
new ItemStack(output, shape.amount()),
|
||||||
|
new RecipeChoice.MaterialChoice(logs))) {
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family.boat() != null) {
|
||||||
|
Material boat = match(family.boat());
|
||||||
|
if (boat != null && add(plugin, key(family, "boat_from_logs"),
|
||||||
|
new ItemStack(boat), new RecipeChoice.MaterialChoice(logs))) {
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Shape shape : SHAPES) {
|
||||||
|
Material output = match(family.prefix() + "_" + shape.suffix());
|
||||||
|
if (output == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (add(plugin, key(family, shape.suffix().toLowerCase(Locale.ROOT) + "_from_planks"),
|
||||||
|
new ItemStack(output, shape.amount()),
|
||||||
|
new RecipeChoice.MaterialChoice(planks))) {
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Strip strip : STRIPS) {
|
||||||
|
Material input = match(strip.input());
|
||||||
|
Material output = match(strip.output());
|
||||||
|
if (input == null || output == null) {
|
||||||
|
log.warning("Woodcutter: unknown stripping material "
|
||||||
|
+ strip.input() + " -> " + strip.output());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = strip.output().toLowerCase(Locale.ROOT)
|
||||||
|
+ "_from_" + strip.input().toLowerCase(Locale.ROOT);
|
||||||
|
if (add(plugin, key, new ItemStack(output), new RecipeChoice.MaterialChoice(input))) {
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Registered " + added + " woodcutting recipes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Strip> buildStrips() {
|
||||||
|
List<Strip> strips = new ArrayList<>();
|
||||||
|
for (String prefix : List.of("OAK", "SPRUCE", "BIRCH", "JUNGLE", "ACACIA",
|
||||||
|
"DARK_OAK", "MANGROVE", "CHERRY", "PALE_OAK")) {
|
||||||
|
strips.add(new Strip(prefix + "_LOG", "STRIPPED_" + prefix + "_LOG"));
|
||||||
|
strips.add(new Strip(prefix + "_WOOD", "STRIPPED_" + prefix + "_WOOD"));
|
||||||
|
}
|
||||||
|
strips.add(new Strip("BAMBOO_BLOCK", "STRIPPED_BAMBOO_BLOCK"));
|
||||||
|
for (String prefix : List.of("CRIMSON", "WARPED")) {
|
||||||
|
strips.add(new Strip(prefix + "_STEM", "STRIPPED_" + prefix + "_STEM"));
|
||||||
|
strips.add(new Strip(prefix + "_HYPHAE", "STRIPPED_" + prefix + "_HYPHAE"));
|
||||||
|
}
|
||||||
|
return List.copyOf(strips);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean add(Plugin plugin, String key, ItemStack result, RecipeChoice input) {
|
||||||
|
StonecuttingRecipe recipe = new StonecuttingRecipe(new NamespacedKey(plugin, key), result, input);
|
||||||
|
if (!Bukkit.addRecipe(recipe, true)) {
|
||||||
|
plugin.getLogger().warning("Woodcutter: could not register recipe " + key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(Family family, String suffix) {
|
||||||
|
return family.prefix().toLowerCase(Locale.ROOT) + "_" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> standardLogs(String prefix) {
|
||||||
|
return List.of(prefix + "_LOG", prefix + "_WOOD",
|
||||||
|
"STRIPPED_" + prefix + "_LOG", "STRIPPED_" + prefix + "_WOOD");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> stemLogs(String prefix) {
|
||||||
|
return List.of(prefix + "_STEM", prefix + "_HYPHAE",
|
||||||
|
"STRIPPED_" + prefix + "_STEM", "STRIPPED_" + prefix + "_HYPHAE");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> withExtra(List<String> base, String... extra) {
|
||||||
|
List<String> all = new ArrayList<>(base);
|
||||||
|
all.addAll(List.of(extra));
|
||||||
|
return List.copyOf(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Material> resolve(Logger log, List<String> names) {
|
||||||
|
List<Material> materials = new ArrayList<>(names.size());
|
||||||
|
for (String name : names) {
|
||||||
|
Material material = match(name);
|
||||||
|
if (material == null) {
|
||||||
|
log.warning("Woodcutter: unknown log material " + name);
|
||||||
|
} else {
|
||||||
|
materials.add(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return materials;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Material match(String name) {
|
||||||
|
return Material.matchMaterial(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue