Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ These features give other TF-Minecraft plugins common building blocks while also
- **Resource-pack texture guard** — warns the console and staff when the ItemsAdder pack holds a texture over 16384 px, which stops the whole pack loading on most Intel graphics.
- **Required pack on join** — opt-in (`resource-pack.require-on-join`); sends the pack as required and publishes `tfmccore:required-pack` player metadata (`loading`, `ready`, `failed`) for RPCharacters. Staff retry a failed load with `/tcore packretry <player>`.
- **Book appearance** — optionally hides the default glint on signed books through ProtocolLib.
- **Book copies** — written books can be copied with a Book and Quill at a crafting table, even though MMOItems blocks vanilla recipes that use a disable-crafting MMOItem (`allow-book-copies`).
- **Cross-plugin statistics** — records supported vehicle, character, crafting, skill, and faction events for staff queries.

TFMC Core works alongside the server's specialist plugins, connecting their systems with the smaller details that make the roleplay world feel consistent.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/TFMCCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;

import net.tfminecraft.tlibs.database.SqliteProvider;
import net.tfminecraft.tfmccore.books.BookCopyUnblocker;
import net.tfminecraft.tfmccore.books.BookGlintHider;
import net.tfminecraft.tfmccore.commands.CoreCommands;
import net.tfminecraft.tfmccore.commands.CoreTabCompletion;
Expand Down Expand Up @@ -76,6 +77,7 @@ public class TFMCCore extends JavaPlugin{
private net.tfminecraft.tfmccore.resourcepack.MultipartPackService multipartPacks;
private net.tfminecraft.tfmccore.resourcepack.PackTextureGuard textureGuard;
private net.tfminecraft.tfmccore.resourcepack.RequiredPackGate requiredPacks;
private BookCopyUnblocker bookCopies;

@Override
public void onEnable() {
Expand Down Expand Up @@ -181,6 +183,7 @@ public void onDisable() {
if (requiredPacks != null) requiredPacks.close();
if (multipartPacks != null) multipartPacks.close();
if (textureGuard != null) textureGuard.close();
if (bookCopies != null) bookCopies.unwrap();
if (stoneListener != null) {
stoneListener.refundAll();
}
Expand Down Expand Up @@ -329,6 +332,7 @@ public void registerListeners() {
// Covers a plugin reload; on a normal start nobody is online yet
getServer().getOnlinePlayers().forEach(XaeroFairPlayListener::send);
BookGlintHider.register(this);
bookCopies = BookCopyUnblocker.register(this);
}

public void createConfigs() {
Expand Down
137 changes: 137 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/books/BookCopyUnblocker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package net.tfminecraft.tfmccore.books;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.java.JavaPlugin;

import net.tfminecraft.tfmccore.cache.Cache;

/**
* Lets players copy written books at a crafting table. MIReplacer makes every Book and
* Quill an MMOItem with disable-crafting, and MMOItems cancels any vanilla recipe that
* uses one, so the copy showed but could not be taken. MMOItems' own click handlers are
* wrapped so that only a book copy cancel made by that handler call is undone; cancels
* from every other plugin, at any priority, stay.
*/
public final class BookCopyUnblocker implements Listener {

static final NamespacedKey BOOK_CLONING = NamespacedKey.minecraft("book_cloning");
static final String MMOITEMS_LISTENER = "net.Indyuce.mmoitems.listener.DisableInteractions";

private final JavaPlugin plugin;
private final HandlerList handlers;
private final List<Wrapped> wrapped = new ArrayList<>();

BookCopyUnblocker(JavaPlugin plugin, HandlerList handlers) {
this.plugin = plugin;
this.handlers = handlers;
}

public static BookCopyUnblocker register(JavaPlugin plugin) {
BookCopyUnblocker unblocker = new BookCopyUnblocker(plugin, InventoryClickEvent.getHandlerList());
plugin.getServer().getPluginManager().registerEvents(unblocker, plugin);
// MMOItems is not a dependency: it registers its handlers in its own onEnable, before or after ours
if (plugin.getServer().getPluginManager().isPluginEnabled("MMOItems")) unblocker.wrapAndLog();
return unblocker;
}

@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
if (event.getPlugin().getName().equals("MMOItems")) wrapAndLog();
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
void wrapAndLog() {
int count = wrap();
if (wrapped.isEmpty()) {
plugin.getLogger().warning("MMOItems craft handler not found; book copies stay blocked");
} else if (count > 0) {
plugin.getLogger().info("Book copies: wrapped " + count + " MMOItems click handlers");
}
}

/** Replaces each unwrapped MMOItems DisableInteractions registration with one that undoes its book copy cancels. */
int wrap() {
int count = 0;
for (RegisteredListener original : handlers.getRegisteredListeners()) {
if (original instanceof Wrapped
|| !original.getListener().getClass().getName().equals(MMOITEMS_LISTENER)) continue;
Wrapped wrapper = new Wrapped(original);
replace(original, wrapper);
wrapped.add(wrapper);
count++;
}
return count;
}

/**
* Swaps one registration for another in the same place. HandlerList only appends, so the
* listeners after it in its priority slot are taken out and added back behind the replacement.
*/
private void replace(RegisteredListener old, RegisteredListener replacement) {
List<RegisteredListener> later = new ArrayList<>();
boolean found = false;
for (RegisteredListener registered : handlers.getRegisteredListeners()) {
if (registered == old) {
found = true;
} else if (found && registered.getPriority() == old.getPriority()) {
later.add(registered);
}
}
handlers.unregister(old);
later.forEach(handlers::unregister);
handlers.register(replacement);
later.forEach(handlers::register);
}

/** Puts MMOItems' own registrations back, so a TFMCCore disable or reload leaves nothing of ours behind. */
public void unwrap() {
List<RegisteredListener> current = Arrays.asList(handlers.getRegisteredListeners());
for (Wrapped wrapper : wrapped) {
// Gone when MMOItems was disabled meanwhile; its original must not come back then
if (!current.contains(wrapper)) continue;
replace(wrapper, wrapper.original);
}
wrapped.clear();
}

static final class Wrapped extends RegisteredListener {
final RegisteredListener original;

Wrapped(RegisteredListener original) {
super(original.getListener(), executor(original), original.getPriority(),
original.getPlugin(), original.isIgnoringCancelled());
this.original = original;
}
}

static EventExecutor executor(RegisteredListener original) {
return (listener, event) -> {
boolean copy = Cache.allowBookCopies && isBookCopy(event);
boolean cancelledBefore = copy && ((Cancellable) event).isCancelled();
original.callEvent(event);
if (copy && !cancelledBefore && ((Cancellable) event).isCancelled()) {
((Cancellable) event).setCancelled(false);
}
};
}

private static boolean isBookCopy(Event event) {
return event instanceof CraftItemEvent craft
&& craft.getRecipe() instanceof Keyed keyed
&& BOOK_CLONING.equals(keyed.getKey());
}
}
1 change: 1 addition & 0 deletions src/main/java/net/tfminecraft/tfmccore/cache/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Cache {
public static boolean xaeroFairPlay = true;
// Read from packet threads
public static volatile boolean hideBookGlint = true;
public static boolean allowBookCopies = true;

public static int armourTime = 7;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public boolean loadConfig(File configFile) {
Cache.dropsDebug = config.getBoolean("drops-debug", true);
Cache.xaeroFairPlay = config.getBoolean("xaero-fair-play", true);
Cache.hideBookGlint = config.getBoolean("hide-book-glint", true);
Cache.allowBookCopies = config.getBoolean("allow-book-copies", true);

Cache.armourTime = config.getInt("armour-time", 7);

Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ xaero-fair-play: true
# Hide the enchantment glint on signed books (letters and book skins too). Only changes
# what players see; needs ProtocolLib. After a reload, items update as they are resent.
hide-book-glint: true
# Let written books be copied with a Book and Quill at a crafting table. MMOItems
# otherwise blocks the vanilla recipe, because every Book and Quill is a disable-crafting MMOItem.
allow-book-copies: true

drops-debug: true

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.Indyuce.mmoitems.listener;

import org.bukkit.event.Listener;

/** Stands in for MMOItems' listener, which BookCopyUnblocker finds by class name. */
public class DisableInteractions implements Listener {}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import net.tfminecraft.tfmccore.stats.categories.skills.SkillsStatConfig;
import net.tfminecraft.tfmccore.stats.categories.vehicles.VehiclesStatConfig;
import net.tfminecraft.tfmccore.stones.LorestoneConfigLoader;
import net.tfminecraft.tfmccore.books.BookCopyUnblocker;
import net.tfminecraft.tfmccore.stones.StoneItems;
import net.tfminecraft.tfmccore.stones.StoneListener;
import net.tfminecraft.tfmccore.testsupport.TestRegistryAccess;
Expand Down Expand Up @@ -260,8 +261,8 @@
.when(Bukkit::getUnsafe)
.thenReturn(mock(org.bukkit.UnsafeValues.class, RETURNS_DEEP_STUBS));
services
.when(() -> net.kyori.adventure.util.Services.service(PluginLoader.class))

Check warning on line 264 in src/test/java/net/tfminecraft/tfmccore/PluginLifecycleCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

org.bukkit.plugin.PluginLoader in org.bukkit.plugin has been deprecated and marked for removal
.thenReturn(Optional.of(mock(PluginLoader.class)));

Check warning on line 265 in src/test/java/net/tfminecraft/tfmccore/PluginLifecycleCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

org.bukkit.plugin.PluginLoader in org.bukkit.plugin has been deprecated and marked for removal
TestPluginLoader loader = new TestPluginLoader(description, server, logger, directory);

JavaPlugin plugin =
Expand Down Expand Up @@ -293,7 +294,8 @@
verify(rig.coreCommand).setTabCompleter(rig.tabCompletion);
verify(rig.silentCommand).setExecutor(any(SilentPermissionCommand.class));
verify(rig.silentCommand).setTabCompleter(any(SilentPermissionCommand.class));
assertEquals(9, rig.listeners.size());
assertEquals(10, rig.listeners.size());
assertTrue(rig.listeners.stream().anyMatch(BookCopyUnblocker.class::isInstance));
assertTrue(rig.listeners.contains(rig.whistles.constructed().getFirst()));
assertTrue(rig.listeners.contains(rig.stones.constructed().getFirst()));
assertSame(rig.stones.constructed().getFirst(), TFMCCore.getStoneListener());
Expand Down
Loading
Loading