feat: Add advanced helmet height rules configuration and logic for dynamic nametag positioning
This commit is contained in:
1 parent
385f505c99
commit
3e1aa9d39e
9 files changed
+259
-4
No files matched your search
@@ -73,6 +73,9 @@ Enhance your server's customization by tailoring player name tags, integrating d
|
||||
5. **Configure**
|
||||
Customize the `settings.yml` file located in the plugin's folder to suit your server’s needs.
|
||||
|
||||
6. **Optional `advanced.yml`**
|
||||
For manual helmet → nametag height rules (e.g. ItemsAdder / custom CMD without relying on resource-pack parsing), copy [`advanced.example.yml`](advanced.example.yml) to `plugins/UnlimitedNameTags/advanced.yml` and adjust. The file is never created automatically; `/unt reload` reloads it when present.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Commands**
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copy to plugins/UnlimitedNameTags/advanced.yml and edit.
|
||||
# Optional: if this file is missing, the plugin behaves as before.
|
||||
# Keys use camelCase (ConfigLib default).
|
||||
|
||||
helmetHeightRules:
|
||||
# Example: custom head with a specific CustomModelData
|
||||
- priority: 10
|
||||
height: 28
|
||||
material: PLAYER_HEAD
|
||||
customModelData: 12345
|
||||
|
||||
# Example: any leather helmet with CMD in a range (inclusive)
|
||||
- priority: 5
|
||||
height: 20
|
||||
material: LEATHER_HELMET
|
||||
customModelDataMin: 1000
|
||||
customModelDataMax: 1999
|
||||
|
||||
# Example: equippable item model (1.21.3+)
|
||||
- priority: 8
|
||||
height: 32
|
||||
equippableModel: "mypack:item/tall_hat"
|
||||
|
||||
# Optional filters on any rule:
|
||||
# worlds: [world, world_nether]
|
||||
# permission: "myserver.bighelmet"
|
||||
@@ -186,6 +186,8 @@ public final class UnlimitedNameTags extends JavaPlugin {
|
||||
}
|
||||
|
||||
private void loadHooks() {
|
||||
hatHooks.add(new AdvancedHatHook(this));
|
||||
|
||||
if (Bukkit.getPluginManager().isPluginEnabled("TypeWriter")) {
|
||||
hooks.put(TypeWriterListener.class, new TypeWriterListener(this));
|
||||
getLogger().info("TypeWriter found, hooking into it");
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.alexdev.unlimitednametags.config;
|
||||
|
||||
import de.exlll.configlib.Comment;
|
||||
import de.exlll.configlib.Configuration;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
|
||||
@Getter
|
||||
public class Advanced {
|
||||
|
||||
@Comment({
|
||||
"Optional rules to raise the nametag when the worn helmet matches.",
|
||||
"Higher priority is evaluated first. The first matching rule wins (same behavior as other hat hooks).",
|
||||
"Create advanced.yml in the plugin folder manually; it is never auto-generated."
|
||||
})
|
||||
private List<HelmetHeightRule> helmetHeightRules = new ArrayList<>();
|
||||
|
||||
@Configuration
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public static class HelmetHeightRule {
|
||||
|
||||
@Comment("Higher value = checked before lower priority rules.")
|
||||
private int priority;
|
||||
|
||||
@Comment("Vertical offset (same unit as Nexo/Oraxen hat hooks). Must be > 0.")
|
||||
private double height;
|
||||
|
||||
@Comment("Bukkit material name, e.g. LEATHER_HELMET. Omit to ignore material.")
|
||||
private String material;
|
||||
|
||||
@Comment("Exact CustomModelData. Ignored if custom-model-data range below is set.")
|
||||
private Integer customModelData;
|
||||
|
||||
@Comment("Inclusive CMD range; both must be set to use a range.")
|
||||
private Integer customModelDataMin;
|
||||
|
||||
private Integer customModelDataMax;
|
||||
|
||||
@Comment("Equippable model key (1.21.3+), e.g. mypack:item/my_hat")
|
||||
private String equippableModel;
|
||||
|
||||
@Comment("If non-empty, the player must be in one of these world names.")
|
||||
private List<String> worlds = new ArrayList<>();
|
||||
|
||||
@Comment("If set, the player must have this permission.")
|
||||
private String permission;
|
||||
|
||||
public boolean definesItemMatch() {
|
||||
final boolean mat = material != null && !material.isEmpty();
|
||||
final boolean eq = equippableModel != null && !equippableModel.isEmpty();
|
||||
final boolean cmdExact = customModelData != null;
|
||||
final boolean cmdRange = customModelDataMin != null && customModelDataMax != null;
|
||||
return mat || eq || cmdExact || cmdRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,16 @@ import de.exlll.configlib.YamlConfigurations;
|
||||
import lombok.Getter;
|
||||
import org.alexdev.unlimitednametags.UnlimitedNameTags;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public class ConfigManager {
|
||||
@@ -27,6 +30,7 @@ public class ConfigManager {
|
||||
|
||||
private final UnlimitedNameTags plugin;
|
||||
private Settings settings;
|
||||
private Advanced advanced = new Advanced();
|
||||
private boolean compiled;
|
||||
|
||||
public ConfigManager(@NotNull UnlimitedNameTags plugin) {
|
||||
@@ -55,6 +59,7 @@ public class ConfigManager {
|
||||
PROPERTIES
|
||||
);
|
||||
checkData();
|
||||
loadAdvancedConfig(false);
|
||||
return Optional.empty();
|
||||
} catch (Exception e) {
|
||||
return Optional.of(e);
|
||||
@@ -64,6 +69,58 @@ public class ConfigManager {
|
||||
public void reload() {
|
||||
settings = YamlConfigurations.load(new File(plugin.getDataFolder(), "settings.yml").toPath(), Settings.class, PROPERTIES);
|
||||
checkData();
|
||||
loadAdvancedConfig(true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Advanced getAdvanced() {
|
||||
return advanced;
|
||||
}
|
||||
|
||||
private void loadAdvancedConfig(boolean keepPreviousOnFailure) {
|
||||
final File file = new File(plugin.getDataFolder(), "advanced.yml");
|
||||
if (!file.exists()) {
|
||||
advanced = new Advanced();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
advanced = YamlConfigurations.load(file.toPath(), Advanced.class, PROPERTIES);
|
||||
validateAdvancedRules();
|
||||
plugin.getLogger().info("Loaded optional advanced.yml");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to load advanced.yml", e);
|
||||
if (!keepPreviousOnFailure) {
|
||||
advanced = new Advanced();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAdvancedRules() {
|
||||
final List<Advanced.HelmetHeightRule> rules = advanced.getHelmetHeightRules();
|
||||
if (rules == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < rules.size(); i++) {
|
||||
final Advanced.HelmetHeightRule rule = rules.get(i);
|
||||
if (!rule.definesItemMatch()) {
|
||||
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] has no item matcher; it will never apply.");
|
||||
}
|
||||
if (rule.getHeight() <= 0) {
|
||||
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] height must be > 0; it will never apply.");
|
||||
}
|
||||
final boolean minSet = rule.getCustomModelDataMin() != null;
|
||||
final boolean maxSet = rule.getCustomModelDataMax() != null;
|
||||
if (minSet != maxSet) {
|
||||
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] must set both customModelDataMin and customModelDataMax for a CMD range.");
|
||||
}
|
||||
if (minSet && maxSet && rule.getCustomModelData() != null) {
|
||||
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] customModelData is ignored when customModelDataMin/Max range is set.");
|
||||
}
|
||||
if (rule.getMaterial() != null && !rule.getMaterial().isEmpty()
|
||||
&& Material.matchMaterial(rule.getMaterial(), false) == null) {
|
||||
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] unknown material: " + rule.getMaterial());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkData() {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.alexdev.unlimitednametags.hook;
|
||||
|
||||
import org.alexdev.unlimitednametags.UnlimitedNameTags;
|
||||
import org.alexdev.unlimitednametags.config.Advanced;
|
||||
import org.alexdev.unlimitednametags.hook.hat.HatHook;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public final class AdvancedHatHook implements HatHook {
|
||||
|
||||
private final UnlimitedNameTags plugin;
|
||||
|
||||
public AdvancedHatHook(@NotNull UnlimitedNameTags plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getHigh(@NotNull Player player) {
|
||||
final List<Advanced.HelmetHeightRule> rules = plugin.getConfigManager().getAdvanced().getHelmetHeightRules();
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final ItemStack helmet = player.getInventory().getHelmet();
|
||||
if (helmet == null || helmet.getType().isAir()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return rules.stream()
|
||||
.sorted(Comparator.comparingInt(Advanced.HelmetHeightRule::getPriority).reversed())
|
||||
.filter(rule -> rule.getHeight() > 0)
|
||||
.filter(Advanced.HelmetHeightRule::definesItemMatch)
|
||||
.filter(rule -> matches(rule, player, helmet))
|
||||
.mapToDouble(Advanced.HelmetHeightRule::getHeight)
|
||||
.findFirst()
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private boolean matches(@NotNull Advanced.HelmetHeightRule rule, @NotNull Player player, @NotNull ItemStack helmet) {
|
||||
if (rule.getPermission() != null && !rule.getPermission().isEmpty() && !player.hasPermission(rule.getPermission())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rule.getWorlds() != null && !rule.getWorlds().isEmpty()
|
||||
&& !rule.getWorlds().contains(player.getWorld().getName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rule.getMaterial() != null && !rule.getMaterial().isEmpty()) {
|
||||
final Material mat = Material.matchMaterial(rule.getMaterial(), false);
|
||||
if (mat == null || helmet.getType() != mat) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.getEquippableModel() != null && !rule.getEquippableModel().isEmpty()) {
|
||||
final ItemMeta meta = helmet.getItemMeta();
|
||||
if (meta == null || !meta.hasEquippable()) {
|
||||
return false;
|
||||
}
|
||||
final NamespacedKey modelKey = meta.getEquippable().getModel();
|
||||
if (modelKey == null) {
|
||||
return false;
|
||||
}
|
||||
final NamespacedKey wanted = NamespacedKey.fromString(rule.getEquippableModel());
|
||||
if (wanted == null || !wanted.equals(modelKey)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final boolean wantsRange = rule.getCustomModelDataMin() != null && rule.getCustomModelDataMax() != null;
|
||||
if (wantsRange) {
|
||||
final ItemMeta meta = helmet.getItemMeta();
|
||||
if (meta == null || !meta.hasCustomModelData()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int cmd = meta.getCustomModelData();
|
||||
return cmd >= rule.getCustomModelDataMin() && cmd <= rule.getCustomModelDataMax();
|
||||
} else if (rule.getCustomModelData() != null) {
|
||||
final ItemMeta meta = helmet.getItemMeta();
|
||||
if (meta == null || !meta.hasCustomModelData()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return meta.getCustomModelData() == rule.getCustomModelData();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public class PlayerListener implements PackSendHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().showToTrackedPlayers(event.getPlayer()), 1);
|
||||
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().showToTrackedPlayers(event.getPlayer()), 5);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
|
||||
@@ -122,6 +122,9 @@ public class PacketNameTag {
|
||||
}
|
||||
|
||||
perPlayerEntity.modify(user, e -> {
|
||||
if (e == null) {
|
||||
return; // Entity removed (e.g. player disconnected during async refresh)
|
||||
}
|
||||
final TextDisplayMeta meta = (TextDisplayMeta) e.getEntityMeta();
|
||||
consumer.accept(meta);
|
||||
});
|
||||
@@ -530,7 +533,10 @@ public class PacketNameTag {
|
||||
return;
|
||||
}
|
||||
|
||||
perPlayerEntity.getEntityOf(user).refresh();
|
||||
final WrapperEntity entity = perPlayerEntity.getEntityOf(user);
|
||||
if (entity != null) {
|
||||
entity.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
@@ -73,6 +73,7 @@ public class PlaceholderManager {
|
||||
reloadPlaceholdersReplacements();
|
||||
createDecimalFormat();
|
||||
reload();
|
||||
updateFormattedPhaseValues(); // Populate immediately to avoid race: gradient with #-phase-mm-g# fails when map is empty
|
||||
startIndexTask();
|
||||
}
|
||||
|
||||
@@ -242,10 +243,10 @@ public class PlaceholderManager {
|
||||
return value;
|
||||
}
|
||||
|
||||
String result = value.replace("#phase-mm-g#", formattedPhaseValues.getOrDefault(PHASE_MM_G_KEY, ""));
|
||||
String result = value.replace("#phase-mm-g#", formattedPhaseValues.getOrDefault(PHASE_MM_G_KEY, "0"));
|
||||
if (result.indexOf('#') == -1) return result;
|
||||
|
||||
result = result.replace("#-phase-mm-g#", formattedPhaseValues.getOrDefault(NEG_PHASE_MM_G_KEY, ""));
|
||||
result = result.replace("#-phase-mm-g#", formattedPhaseValues.getOrDefault(NEG_PHASE_MM_G_KEY, "0"));
|
||||
if (result.indexOf('#') == -1) return result;
|
||||
|
||||
result = result.replace("#phase-md#", formattedPhaseValues.getOrDefault(PHASE_MD_KEY, ""));
|
||||
|
||||
Reference in new issue
Block a user