Initial commit

This commit is contained in:
Kristian Knarvik 2023-04-05 22:02:29 +02:00
commit ea3f25e278
13 changed files with 809 additions and 0 deletions

113
.gitignore vendored Normal file
View File

@ -0,0 +1,113 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# Placeholder Signs
This is a minimal plugin created for a single purpose: Displaying placeholders from PlaceholderAPI on signs. Note that
this plugin only works for placeholders which do not require a player, as the same text will be displayed to everyone!
How it works: After installing this plugin, whenever a sign is changed to contain a placeholder replaced by
PlaceholderAPI, the location of the sign, and the lines containing placeholders are saved. Those lines are updated at a
set pace by using the saved original lines, and letting PlaceholderAPI replace them. Any color, formatting or RGB color
codes in the original text will be converted each time the sign is updated.
The /editSign command is basically just a command to allow placeholders that won't fit on a sign to be used. As an
additional benefit, formatting, color and RGB color codes are automatically converted whenever the command is used to
change sign text.
Note that when clicking a sign after using /editSign, a SignChangeEvent is triggered. This means that the sign text
won't be changed unless the player passes all world protection checks.
## Commands
| Command | Arguments | Description |
|-----------|-----------------------------|-----------------------------------------------------------------------------------------------|
| /editSign | \<line> \<text> \<text> ... | Sets the text of the sign line (1-4) to the given input. Then right-click the sign to update. |
## Permissions
| Permission | Description |
|-----------------------|-----------------------------------------|
| placeholdersigns.edit | Allows the use of the /editSign command |

90
pom.xml Normal file
View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.knarcraft</groupId>
<artifactId>PlaceholderSigns</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>PlaceholderSigns</name>
<properties>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.10.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,181 @@
package net.knarcraft.placeholdersigns;
import me.clip.placeholderapi.PlaceholderAPI;
import net.knarcraft.placeholdersigns.command.EditSignCommand;
import net.knarcraft.placeholdersigns.container.LineChangeRequest;
import net.knarcraft.placeholdersigns.container.PlaceholderSign;
import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler;
import net.knarcraft.placeholdersigns.listener.SignBreakListener;
import net.knarcraft.placeholdersigns.listener.SignClickListener;
import net.knarcraft.placeholdersigns.listener.SignTextListener;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Sign;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This plugin's main class
*/
public final class PlaceholderSigns extends JavaPlugin {
private static PlaceholderSigns instance;
private PlaceholderSignHandler signHandler;
private Map<Player, LineChangeRequest> changeRequests;
/**
* Gets an instance of this plugin
*
* @return <p>A plugin instance</p>
*/
public static @NotNull PlaceholderSigns getInstance() {
return instance;
}
/**
* Gets this instance's placeholder sign handler
*
* @return <p>The sign handler</p>
*/
public @NotNull PlaceholderSignHandler getSignHandler() {
return this.signHandler;
}
/**
* Registers a sign change request
*
* <p>A sign change request is basically the result of running the editSign command, which must be stored until the
* player clicks a sign.</p>
*
* @param request <p>The sign change request to register</p>
*/
public void addChangeRequest(@NotNull LineChangeRequest request) {
changeRequests.put(request.player(), request);
}
/**
* Gets a sign change request
*
* @param player <p>The player to get the request for</p>
* @return <p>The sign change request, or null if not found</p>
*/
public @Nullable LineChangeRequest getChangeRequest(@NotNull Player player) {
return changeRequests.remove(player);
}
@Override
public void onLoad() {
super.onLoad();
// Register serialization classes
ConfigurationSerialization.registerClass(PlaceholderSign.class);
ConfigurationSerialization.registerClass(PlaceholderSignHandler.class);
}
@Override
public void onEnable() {
instance = this;
signHandler = new PlaceholderSignHandler();
changeRequests = new HashMap<>();
signHandler.load();
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") == null) {
getLogger().log(Level.WARNING, "Could not find PlaceholderAPI! This plugin is required.");
Bukkit.getPluginManager().disablePlugin(this);
return;
}
// Update signs' placeholders every second
Bukkit.getScheduler().runTaskTimer(this, this::updateSigns, 20 * 10, 20 * 5);
Bukkit.getPluginManager().registerEvents(new SignBreakListener(), this);
Bukkit.getPluginManager().registerEvents(new SignTextListener(), this);
Bukkit.getPluginManager().registerEvents(new SignClickListener(), this);
PluginCommand editCommand = Bukkit.getPluginCommand("editSign");
if (editCommand != null) {
editCommand.setExecutor(new EditSignCommand());
}
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
/**
* Updates all loaded and registered placeholder signs
*/
private void updateSigns() {
for (PlaceholderSign placeholderSign : signHandler.getSigns()) {
// Ignore signs away from players
Location location = placeholderSign.location();
if (!location.getChunk().isLoaded()) {
continue;
}
// If no longer a sign, remove
if (!(location.getBlock().getState() instanceof Sign sign)) {
signHandler.unregisterSign(placeholderSign);
continue;
}
// Update placeholders
Map<Integer, String> placeholders = placeholderSign.placeholders();
String[] lines = sign.getLines();
boolean updateRequired = false;
for (int i = 0; i < lines.length; i++) {
String oldText = sign.getLine(i);
// The new text of the sign is either the same, or the original placeholder
String newText;
if (!placeholders.containsKey(i) || placeholders.get(i) == null) {
newText = oldText;
} else {
newText = PlaceholderAPI.setPlaceholders(null, placeholders.get(i));
}
// Convert color codes
newText = translateAllColorCodes(newText);
// Only change the line if the text has changed
if (!newText.equals(oldText)) {
sign.setLine(i, newText);
updateRequired = true;
}
}
// Only update the sign if the text has changed
if (updateRequired) {
sign.update();
}
}
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
private static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("&?(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group(1)));
}
return message;
}
}

View File

@ -0,0 +1,72 @@
package net.knarcraft.placeholdersigns.command;
import net.knarcraft.placeholdersigns.PlaceholderSigns;
import net.knarcraft.placeholdersigns.container.LineChangeRequest;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A command for setting a sign's text to anything longer than normally possible
*/
public class EditSignCommand implements TabExecutor {
private static final List<String> lineNumbers;
static {
lineNumbers = new ArrayList<>();
for (int i = 1; i < 5; i++) {
lineNumbers.add(String.valueOf(i));
}
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] args) {
if (args.length < 2 || !(commandSender instanceof Player player)) {
return false;
}
// Parse the specified line number
int lineNumber;
try {
lineNumber = Integer.parseInt(args[0]);
if (lineNumber < 0 || lineNumber > 4) {
return false;
}
} catch (NumberFormatException exception) {
return false;
}
// Get all arguments as a space-separated string
StringBuilder builder = new StringBuilder(args[1]);
for (int i = 2; i < args.length; i++) {
builder.append(" ").append(args[i]);
}
// Register the line change request
LineChangeRequest request = new LineChangeRequest(player, lineNumber - 1, builder.toString());
PlaceholderSigns.getInstance().addChangeRequest(request);
commandSender.sendMessage("Please click the sign you want to change.");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] args) {
if (args.length == 1) {
return lineNumbers;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,13 @@
package net.knarcraft.placeholdersigns.container;
import org.bukkit.entity.Player;
/**
* A record of a player's request to change a sign
*
* @param player <p>The player requesting the sign change</p>
* @param line <p>The line the player wants to change</p>
* @param text <p>The new text the player provided for the line</p>
*/
public record LineChangeRequest(Player player, int line, String text) {
}

View File

@ -0,0 +1,39 @@
package net.knarcraft.placeholdersigns.container;
import org.bukkit.Location;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A sign containing one or more placeholders
*
* @param location <p>The location of the sign</p>
* @param placeholders <p>The original placeholders typed on the sign</p>
*/
public record PlaceholderSign(Location location,
Map<Integer, String> placeholders) implements ConfigurationSerializable {
@Override
@NotNull
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("location", location);
data.put("placeholders", placeholders);
return data;
}
/**
* Deserializes the placeholder-sign specified in the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized placeholder sign</p>
*/
@SuppressWarnings({"unchecked", "unused"})
public static PlaceholderSign deserialize(Map<String, Object> data) {
return new PlaceholderSign((Location) data.get("location"), (Map<Integer, String>) data.get("placeholders"));
}
}

View File

@ -0,0 +1,118 @@
package net.knarcraft.placeholdersigns.handler;
import net.knarcraft.placeholdersigns.PlaceholderSigns;
import net.knarcraft.placeholdersigns.container.PlaceholderSign;
import org.bukkit.Location;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
/**
* A handler for keeping track of placeholder signs
*/
public class PlaceholderSignHandler implements ConfigurationSerializable {
private static final File signsFile = new File(PlaceholderSigns.getInstance().getDataFolder(), "signs.yml");
private Set<PlaceholderSign> placeholderSigns;
private Map<Location, PlaceholderSign> locationLookup;
/**
* Gets all registered signs
*
* @return <p>All registered signs</p>
*/
public @NotNull Set<PlaceholderSign> getSigns() {
return new HashSet<>(placeholderSigns);
}
/**
* Gets a placeholder sign from the given location
*
* @param location <p>The location of the sign</p>
* @return <p>The sign at the location, or null if no such sign exists</p>
*/
public PlaceholderSign getFromLocation(@NotNull Location location) {
return locationLookup.get(location);
}
/**
* Registers a new placeholder sign
*
* @param sign <p>The sign to register</p>
*/
public void registerSign(@NotNull PlaceholderSign sign) {
this.placeholderSigns.add(sign);
locationLookup.put(sign.location(), sign);
save();
}
/**
* Un-registers a placeholder sign
*
* @param sign <p>The sign to un-register</p>
*/
public void unregisterSign(@NotNull PlaceholderSign sign) {
locationLookup.remove(sign.location());
this.placeholderSigns.remove(sign);
save();
}
/**
* Loads all placeholder signs from disk
*/
public void load() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
PlaceholderSignHandler loadedHandler = (PlaceholderSignHandler) configuration.get("signHandler");
this.placeholderSigns = loadedHandler != null ? loadedHandler.placeholderSigns : new HashSet<>();
this.locationLookup = loadedHandler != null ? loadedHandler.locationLookup : new HashMap<>();
}
/**
* Saves all current placeholder signs
*/
public void save() {
try {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set("signHandler", this);
configuration.save(signsFile);
} catch (IOException exception) {
PlaceholderSigns.getInstance().getLogger().log(Level.SEVERE, "Unable to save placeholder signs!");
}
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("signs", this.placeholderSigns);
return data;
}
/**
* Deserializes the given placeholder sign handler data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized sign handler</p>
*/
@SuppressWarnings({"unchecked", "unused"})
public static PlaceholderSignHandler deserialize(@NotNull Map<String, Object> data) {
PlaceholderSignHandler placeholderSignHandler = new PlaceholderSignHandler();
placeholderSignHandler.placeholderSigns = (Set<PlaceholderSign>) data.get("signs");
Map<Location, PlaceholderSign> lookup = new HashMap<>();
for (PlaceholderSign sign : placeholderSignHandler.placeholderSigns) {
lookup.put(sign.location(), sign);
}
placeholderSignHandler.locationLookup = lookup;
return placeholderSignHandler;
}
}

View File

@ -0,0 +1,32 @@
package net.knarcraft.placeholdersigns.listener;
import net.knarcraft.placeholdersigns.PlaceholderSigns;
import net.knarcraft.placeholdersigns.container.PlaceholderSign;
import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
/**
* A listener for placeholder signs being broken
*/
public class SignBreakListener implements Listener {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onSignBreak(BlockBreakEvent event) {
Block block = event.getBlock();
if (!(block.getState() instanceof Sign)) {
return;
}
PlaceholderSignHandler signHandler = PlaceholderSigns.getInstance().getSignHandler();
PlaceholderSign sign = signHandler.getFromLocation(block.getLocation());
if (sign != null) {
signHandler.unregisterSign(sign);
}
}
}

View File

@ -0,0 +1,53 @@
package net.knarcraft.placeholdersigns.listener;
import net.knarcraft.placeholdersigns.PlaceholderSigns;
import net.knarcraft.placeholdersigns.container.LineChangeRequest;
import org.bukkit.Bukkit;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
/**
* A listener for placeholder signs being clicked
*/
public class SignClickListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onSignClick(PlayerInteractEvent event) {
// Ignore if not a clicked sign
if (!event.hasBlock() || event.getClickedBlock() == null ||
!(event.getClickedBlock().getState() instanceof Sign sign)) {
return;
}
// Check if the player has run the /editSign command
LineChangeRequest request = PlaceholderSigns.getInstance().getChangeRequest(event.getPlayer());
if (request == null) {
return;
}
String[] lines = sign.getLines();
lines[request.line()] = request.text();
// Run the sign change event to allow protection plugins to cancel, and allow the sign text listener to trigger
SignChangeEvent changeEvent = new SignChangeEvent(event.getClickedBlock(), event.getPlayer(), lines);
Bukkit.getPluginManager().callEvent(changeEvent);
if (changeEvent.isCancelled()) {
return;
}
// Update the sign with the new text
String[] finalLines = changeEvent.getLines();
for (int i = 0; i < finalLines.length; i++) {
sign.setLine(i, finalLines[i]);
}
sign.update();
event.getPlayer().sendMessage("The sign line was successfully changed.");
}
}

View File

@ -0,0 +1,53 @@
package net.knarcraft.placeholdersigns.listener;
import me.clip.placeholderapi.PlaceholderAPI;
import net.knarcraft.placeholdersigns.PlaceholderSigns;
import net.knarcraft.placeholdersigns.container.PlaceholderSign;
import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import java.util.HashMap;
import java.util.Map;
/**
* A listener for signs being changed to contain on or more placeholders
*/
public class SignTextListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSignCreate(SignChangeEvent event) {
String[] lines = event.getLines();
Map<Integer, String> placeholders = new HashMap<>();
// Register any lines PlaceholderAPI wants to change
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
String replaced = PlaceholderAPI.setPlaceholders(null, line);
if (line.equalsIgnoreCase(replaced)) {
continue;
}
placeholders.put(i, line);
}
Location location = event.getBlock().getLocation();
PlaceholderSignHandler signHandler = PlaceholderSigns.getInstance().getSignHandler();
PlaceholderSign existingSign = signHandler.getFromLocation(location);
// Register the placeholder sign
if (!placeholders.isEmpty() && existingSign == null) {
PlaceholderSign placeholderSign = new PlaceholderSign(event.getBlock().getLocation(), placeholders);
signHandler.registerSign(placeholderSign);
} else if (!placeholders.isEmpty()) {
// Overwrite the placeholders of the existing placeholder sign
for (Map.Entry<Integer, String> entry : placeholders.entrySet()) {
existingSign.placeholders().put(entry.getKey(), entry.getValue());
}
signHandler.save();
}
}
}

View File

View File

@ -0,0 +1,17 @@
name: PlaceholderSigns
version: '${project.version}'
main: net.knarcraft.placeholdersigns.PlaceholderSigns
api-version: 1.19
depend:
- PlaceholderAPI
commands:
editSign:
usage: /<command> <line> <text> [text] ...
permission: placeholdersigns.edit
description: Changes the line of a sign without a text limit
permissions:
placeholdersigns.edit:
description: Allows a player to use the /editSign command
default: op