9
0
mirror of https://github.com/Xiao-MoMi/Custom-Nameplates.git synced 2025-12-28 19:29:17 +00:00
This commit is contained in:
XiaoMoMi
2024-10-05 22:42:28 +08:00
parent d8324da1d9
commit 3aa7e5c012
859 changed files with 28657 additions and 22513 deletions

View File

@@ -1,3 +1,33 @@
dependencies {
compileOnly("org.jetbrains:annotations:24.1.0")
compileOnly("net.kyori:adventure-api:${rootProject.properties["adventure_bundle_version"]}") {
exclude(module = "adventure-bom")
exclude(module = "checker-qual")
exclude(module = "annotations")
}
compileOnly("org.incendo:cloud-core:${rootProject.properties["cloud_core_version"]}")
compileOnly("org.incendo:cloud-minecraft-extras:${rootProject.properties["cloud_minecraft_extras_version"]}")
compileOnly("dev.dejvokep:boosted-yaml:${rootProject.properties["boosted_yaml_version"]}")
compileOnly("org.jetbrains:annotations:${rootProject.properties["jetbrains_annotations_version"]}")
compileOnly("org.slf4j:slf4j-api:${rootProject.properties["slf4j_version"]}")
compileOnly("org.apache.logging.log4j:log4j-core:${rootProject.properties["log4j_version"]}")
compileOnly("net.kyori:adventure-text-minimessage:${rootProject.properties["adventure_bundle_version"]}")
compileOnly("net.kyori:adventure-text-serializer-gson:${rootProject.properties["adventure_bundle_version"]}")
compileOnly("com.github.ben-manes.caffeine:caffeine:${rootProject.properties["caffeine_version"]}")
compileOnly("com.google.code.gson:gson:${rootProject.properties["gson_version"]}")
compileOnly("net.objecthunter:exp4j:${rootProject.properties["exp4j_version"]}")
compileOnly("net.bytebuddy:byte-buddy:${rootProject.properties["byte_buddy_version"]}")
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.release.set(17)
dependsOn(tasks.clean)
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common;
import java.util.Objects;
public record SimpleLocation(String worldName, int x, int y, int z) {
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final SimpleLocation other = (SimpleLocation) obj;
if (!Objects.equals(worldName, other.worldName())) {
return false;
}
if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x)) {
return false;
}
if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y)) {
return false;
}
if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + (worldName != null ? worldName.hashCode() : 0);
hash = 19 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.z) ^ (Double.doubleToLongBits(this.z) >>> 32));
return hash;
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common;
public class Tuple<L, M, R> {
private L left;
private M mid;
private R right;
public Tuple(L left, M mid, R right) {
this.left = left;
this.mid = mid;
this.right = right;
}
public static <L, M, R> Tuple<L, M, R> of(final L left, final M mid, final R right) {
return new Tuple<>(left, mid, right);
}
public L getLeft() {
return left;
}
public void setLeft(L left) {
this.left = left;
}
public M getMid() {
return mid;
}
public void setMid(M mid) {
this.mid = mid;
}
public R getRight() {
return right;
}
public void setRight(R right) {
this.right = right;
}
@Override
public String toString() {
return "Tuple{" +
"left=" + left +
", mid=" + mid +
", right=" + right +
'}';
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.momirealms.customnameplates.common.sender.SenderFactory;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
public abstract class AbstractCommandFeature<C> implements CommandFeature<C> {
protected final CustomNameplatesCommandManager<C> commandManager;
protected CommandConfig<C> commandConfig;
public AbstractCommandFeature(CustomNameplatesCommandManager<C> commandManager) {
this.commandManager = commandManager;
}
protected abstract SenderFactory<?, C> getSenderFactory();
public abstract Command.Builder<? extends C> assembleCommand(CommandManager<C> manager, Command.Builder<C> builder);
@Override
@SuppressWarnings("unchecked")
public Command<C> registerCommand(CommandManager<C> manager, Command.Builder<C> builder) {
Command<C> command = (Command<C>) assembleCommand(manager, builder).build();
manager.command(command);
return command;
}
@Override
public void registerRelatedFunctions() {
// empty
}
@Override
public void unregisterRelatedFunctions() {
// empty
}
@Override
@SuppressWarnings("unchecked")
public void handleFeedback(CommandContext<?> context, TranslatableComponent.Builder key, Component... args) {
if (context.flags().hasFlag("silent")) {
return;
}
commandManager.handleCommandFeedback((C) context.sender(), key, args);
}
@Override
public void handleFeedback(C sender, TranslatableComponent.Builder key, Component... args) {
commandManager.handleCommandFeedback(sender, key, args);
}
@Override
public CustomNameplatesCommandManager<C> getCustomFishingCommandManager() {
return commandManager;
}
@Override
public CommandConfig<C> getCommandConfig() {
return commandConfig;
}
public void setCommandConfig(CommandConfig<C> commandConfig) {
this.commandConfig = commandConfig;
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TranslatableComponent;
import net.momirealms.customnameplates.common.locale.CustomNameplatesCaptionFormatter;
import net.momirealms.customnameplates.common.locale.CustomNameplatesCaptionProvider;
import net.momirealms.customnameplates.common.locale.TranslationManager;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.sender.Sender;
import net.momirealms.customnameplates.common.util.ArrayUtils;
import net.momirealms.customnameplates.common.util.TriConsumer;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.caption.Caption;
import org.incendo.cloud.caption.StandardCaptionKeys;
import org.incendo.cloud.component.CommandComponent;
import org.incendo.cloud.exception.*;
import org.incendo.cloud.exception.handling.ExceptionContext;
import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
public abstract class AbstractCommandManager<C> implements CustomNameplatesCommandManager<C> {
protected final HashSet<CommandComponent<C>> registeredRootCommandComponents = new HashSet<>();
protected final HashSet<CommandFeature<C>> registeredFeatures = new HashSet<>();
protected final CommandManager<C> commandManager;
protected final NameplatesPlugin plugin;
private final CustomNameplatesCaptionFormatter<C> captionFormatter = new CustomNameplatesCaptionFormatter<C>();
private final MinecraftExceptionHandler.Decorator<C> decorator = (formatter, ctx, msg) -> msg;
private TriConsumer<C, String, Component> feedbackConsumer;
public AbstractCommandManager(NameplatesPlugin plugin, CommandManager<C> commandManager) {
this.commandManager = commandManager;
this.plugin = plugin;
this.inject();
this.feedbackConsumer = defaultFeedbackConsumer();
}
@Override
public void setFeedbackConsumer(@NotNull TriConsumer<C, String, Component> feedbackConsumer) {
this.feedbackConsumer = feedbackConsumer;
}
@Override
public TriConsumer<C, String, Component> defaultFeedbackConsumer() {
return ((sender, node, component) -> {
wrapSender(sender).sendMessage(
component, true
);
});
}
protected abstract Sender wrapSender(C c);
private void inject() {
getCommandManager().captionRegistry().registerProvider(new CustomNameplatesCaptionProvider<>());
injectExceptionHandler(InvalidSyntaxException.class, MinecraftExceptionHandler.createDefaultInvalidSyntaxHandler(), StandardCaptionKeys.EXCEPTION_INVALID_SYNTAX);
injectExceptionHandler(InvalidCommandSenderException.class, MinecraftExceptionHandler.createDefaultInvalidSenderHandler(), StandardCaptionKeys.EXCEPTION_INVALID_SENDER);
injectExceptionHandler(NoPermissionException.class, MinecraftExceptionHandler.createDefaultNoPermissionHandler(), StandardCaptionKeys.EXCEPTION_NO_PERMISSION);
injectExceptionHandler(ArgumentParseException.class, MinecraftExceptionHandler.createDefaultArgumentParsingHandler(), StandardCaptionKeys.EXCEPTION_INVALID_ARGUMENT);
injectExceptionHandler(CommandExecutionException.class, MinecraftExceptionHandler.createDefaultCommandExecutionHandler(), StandardCaptionKeys.EXCEPTION_UNEXPECTED);
}
@SuppressWarnings("unchecked")
private void injectExceptionHandler(Class<? extends Throwable> type, MinecraftExceptionHandler.MessageFactory<C, ?> factory, Caption key) {
getCommandManager().exceptionController().registerHandler(type, ctx -> {
final @Nullable ComponentLike message = factory.message(captionFormatter, (ExceptionContext) ctx);
if (message != null) {
handleCommandFeedback(ctx.context().sender(), key.key(), decorator.decorate(captionFormatter, ctx, message.asComponent()).asComponent());
}
});
}
@Override
public CommandConfig<C> getCommandConfig(YamlDocument document, String featureID) {
Section section = document.getSection(featureID);
if (section == null) return null;
return new CommandConfig.Builder<C>()
.permission(section.getString("permission"))
.usages(section.getStringList("usage"))
.enable(section.getBoolean("enable", false))
.build();
}
@Override
public Collection<Command.Builder<C>> buildCommandBuilders(CommandConfig<C> config) {
ArrayList<Command.Builder<C>> list = new ArrayList<>();
for (String usage : config.getUsages()) {
if (!usage.startsWith("/")) continue;
String command = usage.substring(1).trim();
String[] split = command.split(" ");
Command.Builder<C> builder = new CommandBuilder.BasicCommandBuilder<>(getCommandManager(), split[0])
.setCommandNode(ArrayUtils.subArray(split, 1))
.setPermission(config.getPermission())
.getBuiltCommandBuilder();
list.add(builder);
}
return list;
}
@Override
public void registerFeature(CommandFeature<C> feature, CommandConfig<C> config) {
if (!config.isEnable()) throw new RuntimeException("Registering a disabled command feature is not allowed");
for (Command.Builder<C> builder : buildCommandBuilders(config)) {
Command<C> command = feature.registerCommand(commandManager, builder);
this.registeredRootCommandComponents.add(command.rootComponent());
}
feature.registerRelatedFunctions();
this.registeredFeatures.add(feature);
((AbstractCommandFeature<C>) feature).setCommandConfig(config);
}
@Override
public void registerDefaultFeatures() {
YamlDocument document = plugin.getConfigManager().loadConfig(commandsFile, '.');
try {
document.save(new File(plugin.getDataDirectory().toFile(), "commands.yml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
this.getFeatures().values().forEach(feature -> {
CommandConfig<C> config = getCommandConfig(document, feature.getFeatureID());
if (config.isEnable()) {
registerFeature(feature, config);
}
});
}
@Override
public void unregisterFeatures() {
this.registeredRootCommandComponents.forEach(component -> this.commandManager.commandRegistrationHandler().unregisterRootCommand(component));
this.registeredRootCommandComponents.clear();
this.registeredFeatures.forEach(CommandFeature::unregisterRelatedFunctions);
this.registeredFeatures.clear();
}
@Override
public CommandManager<C> getCommandManager() {
return commandManager;
}
@Override
public void handleCommandFeedback(C sender, TranslatableComponent.Builder key, Component... args) {
TranslatableComponent component = key.arguments(args).build();
this.feedbackConsumer.accept(sender, component.key(), TranslationManager.render(component));
}
@Override
public void handleCommandFeedback(C sender, String node, Component component) {
this.feedbackConsumer.accept(sender, node, component);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public interface CommandBuilder<C> {
CommandBuilder<C> setPermission(String permission);
CommandBuilder<C> setCommandNode(String... subNodes);
Command.Builder<C> getBuiltCommandBuilder();
class BasicCommandBuilder<C> implements CommandBuilder<C> {
private Command.Builder<C> commandBuilder;
public BasicCommandBuilder(CommandManager<C> commandManager, String rootNode) {
this.commandBuilder = commandManager.commandBuilder(rootNode);
}
@Override
public CommandBuilder<C> setPermission(String permission) {
this.commandBuilder = this.commandBuilder.permission(permission);
return this;
}
@Override
public CommandBuilder<C> setCommandNode(String... subNodes) {
for (String sub : subNodes) {
this.commandBuilder = this.commandBuilder.literal(sub);
}
return this;
}
@Override
public Command.Builder<C> getBuiltCommandBuilder() {
return commandBuilder;
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import java.util.ArrayList;
import java.util.List;
public class CommandConfig<C> {
private boolean enable = false;
private List<String> usages = new ArrayList<>();
private String permission = null;
private CommandConfig() {
}
public CommandConfig(boolean enable, List<String> usages, String permission) {
this.enable = enable;
this.usages = usages;
this.permission = permission;
}
public boolean isEnable() {
return enable;
}
public List<String> getUsages() {
return usages;
}
public String getPermission() {
return permission;
}
public static class Builder<C> {
private final CommandConfig<C> config;
public Builder() {
this.config = new CommandConfig<>();
}
public Builder<C> usages(List<String> usages) {
config.usages = usages;
return this;
}
public Builder<C> permission(String permission) {
config.permission = permission;
return this;
}
public Builder<C> enable(boolean enable) {
config.enable = enable;
return this;
}
public CommandConfig<C> build() {
return config;
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
public interface CommandFeature<C> {
Command<C> registerCommand(CommandManager<C> cloudCommandManager, Command.Builder<C> builder);
String getFeatureID();
void registerRelatedFunctions();
void unregisterRelatedFunctions();
void handleFeedback(CommandContext<?> context, TranslatableComponent.Builder key, Component... args);
void handleFeedback(C sender, TranslatableComponent.Builder key, Component... args);
CustomNameplatesCommandManager<C> getCustomFishingCommandManager();
CommandConfig<C> getCommandConfig();
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.command;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.util.Index;
import net.momirealms.customnameplates.common.util.TriConsumer;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
public interface CustomNameplatesCommandManager<C> {
String commandsFile = "commands.yml";
void unregisterFeatures();
void registerFeature(CommandFeature<C> feature, CommandConfig<C> config);
void registerDefaultFeatures();
Index<String, CommandFeature<C>> getFeatures();
void setFeedbackConsumer(@NotNull TriConsumer<C, String, Component> feedbackConsumer);
TriConsumer<C, String, Component> defaultFeedbackConsumer();
CommandConfig<C> getCommandConfig(YamlDocument document, String featureID);
Collection<Command.Builder<C>> buildCommandBuilders(CommandConfig<C> config);
CommandManager<C> getCommandManager();
void handleCommandFeedback(C sender, TranslatableComponent.Builder key, Component... args);
void handleCommandFeedback(C sender, String node, Component component);
}

View File

@@ -0,0 +1,385 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
import net.momirealms.customnameplates.common.dependency.relocation.Relocation;
import net.momirealms.customnameplates.common.plugin.CustomNameplatesProperties;
import org.jetbrains.annotations.Nullable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* The dependencies used by CustomFishing.
*/
public enum Dependency {
ASM(
"org.ow2.asm",
"asm",
"maven",
"asm"
),
ASM_COMMONS(
"org.ow2.asm",
"asm-commons",
"maven",
"asm-commons"
),
JAR_RELOCATOR(
"me.lucko",
"jar-relocator",
"maven",
"jar-relocator"
),
H2_DRIVER(
"com.h2database",
"h2",
"maven",
"h2-driver"
),
SQLITE_DRIVER(
"org.xerial",
"sqlite-jdbc",
"maven",
"sqlite-driver"
),
CLOUD_CORE(
"org{}incendo",
"cloud-core",
"maven",
"cloud-core",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
CLOUD_BRIGADIER(
"org{}incendo",
"cloud-brigadier",
"maven",
"cloud-brigadier",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
CLOUD_SERVICES(
"org{}incendo",
"cloud-services",
"maven",
"cloud-services",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
CLOUD_BUKKIT(
"org{}incendo",
"cloud-bukkit",
"maven",
"cloud-bukkit",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
CLOUD_PAPER(
"org{}incendo",
"cloud-paper",
"maven",
"cloud-paper",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
CLOUD_MINECRAFT_EXTRAS(
"org{}incendo",
"cloud-minecraft-extras",
"maven",
"cloud-minecraft-extras",
Relocation.of("cloud", "org{}incendo{}cloud"),
Relocation.of("adventure", "net{}kyori{}adventure"),
Relocation.of("option", "net{}kyori{}option"),
Relocation.of("examination", "net{}kyori{}examination"),
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
GEANTY_REF(
"io{}leangen{}geantyref",
"geantyref",
"maven",
"geantyref",
Relocation.of("geantyref", "io{}leangen{}geantyref")
),
BOOSTED_YAML(
"dev{}dejvokep",
"boosted-yaml",
"maven",
"boosted-yaml",
Relocation.of("boostedyaml", "dev{}dejvokep{}boostedyaml")
),
BYTEBUDDY(
"net{}bytebuddy",
"byte-buddy",
"maven",
"byte-buddy",
Relocation.of("bytebuddy", "net{}bytebuddy")
),
MARIADB_DRIVER(
"org{}mariadb{}jdbc",
"mariadb-java-client",
"maven",
"mariadb-java-client",
Relocation.of("mariadb", "org{}mariadb")
),
MYSQL_DRIVER(
"com{}mysql",
"mysql-connector-j",
"maven",
"mysql-connector-j",
Relocation.of("mysql", "com{}mysql")
),
HIKARI_CP(
"com{}zaxxer",
"HikariCP",
"maven",
"hikari-cp",
Relocation.of("hikari", "com{}zaxxer{}hikari")
),
MONGODB_DRIVER_CORE(
"org{}mongodb",
"mongodb-driver-core",
"maven",
"mongodb-driver-core",
Relocation.of("mongodb", "com{}mongodb"),
Relocation.of("bson", "org{}bson")
),
MONGODB_DRIVER_SYNC(
"org{}mongodb",
"mongodb-driver-sync",
"maven",
"mongodb-driver-sync",
Relocation.of("mongodb", "com{}mongodb"),
Relocation.of("bson", "org{}bson")
) {
@Override
public String getVersion() {
return Dependency.MONGODB_DRIVER_CORE.getVersion();
}
},
MONGODB_DRIVER_BSON(
"org{}mongodb",
"bson",
"maven",
"mongodb-bson",
Relocation.of("mongodb", "com{}mongodb"),
Relocation.of("bson", "org{}bson")
) {
@Override
public String getVersion() {
return Dependency.MONGODB_DRIVER_CORE.getVersion();
}
},
COMMONS_POOL_2(
"org{}apache{}commons",
"commons-pool2",
"maven",
"commons-pool",
Relocation.of("commonspool2", "org{}apache{}commons{}pool2")
),
BSTATS_BASE(
"org{}bstats",
"bstats-base",
"maven",
"bstats-base",
Relocation.of("bstats", "org{}bstats")
),
BSTATS_BUKKIT(
"org{}bstats",
"bstats-bukkit",
"maven",
"bstats-bukkit",
Relocation.of("bstats", "org{}bstats")
) {
@Override
public String getVersion() {
return Dependency.BSTATS_BASE.getVersion();
}
},
GSON(
"com.google.code.gson",
"gson",
"maven",
"gson"
),
CAFFEINE(
"com{}github{}ben-manes{}caffeine",
"caffeine",
"maven",
"caffeine",
Relocation.of("caffeine", "com{}github{}benmanes{}caffeine")
),
JEDIS(
"redis{}clients",
"jedis",
"maven",
"jedis",
Relocation.of("jedis", "redis{}clients{}jedis"),
Relocation.of("commonspool2", "org{}apache{}commons{}pool2")
),
EXP4J(
"net{}objecthunter",
"exp4j",
"maven",
"exp4j",
Relocation.of("exp4j", "net{}objecthunter{}exp4j")
),
SLF4J_SIMPLE(
"org.slf4j",
"slf4j-simple",
"maven",
"slf4j_simple"
) {
@Override
public String getVersion() {
return Dependency.SLF4J_API.getVersion();
}
},
SLF4J_API(
"org.slf4j",
"slf4j-api",
"maven",
"slf4j"
),
FONT_BOX(
"org{}apache{}pdfbox",
"fontbox",
"maven",
"fontbox",
Relocation.of("fontbox", "org{}apache{}fontbox"),
Relocation.of("pdfbox", "org{}apache{}pdfbox")
),
PDF_BOX(
"org{}apache{}pdfbox",
"pdfbox-io",
"maven",
"pdfbox-io",
Relocation.of("pdfbox", "org{}apache{}pdfbox")
),
BYTE_BUDDY(
"net{}bytebuddy",
"byte-buddy",
"maven",
"byte-buddy",
Relocation.of("bytebuddy", "net{}bytebuddy")
),
COMMONS_IO(
"commons-io",
"commons-io",
"maven",
"commons-io",
Relocation.of("commons", "org{}apache{}commons")
);
private final List<Relocation> relocations;
private final String repo;
private final String groupId;
private final String artifactId;
private final String customArtifactID;
private String artifactIdSuffix;
private static final String MAVEN_FORMAT = "%s/%s/%s/%s-%s.jar";
Dependency(String groupId, String artifactId, String repo, String customArtifactID) {
this(groupId, artifactId, repo, customArtifactID, new Relocation[0]);
}
Dependency(String groupId, String artifactId, String repo, String customArtifactID, Relocation... relocations) {
this.artifactId = artifactId;
this.artifactIdSuffix = "";
this.groupId = groupId;
this.relocations = new ArrayList<>(Arrays.stream(relocations).toList());
this.repo = repo;
this.customArtifactID = customArtifactID;
}
Dependency(String groupId, String artifactId, String repo, String customArtifactID, String artifactIdSuffix, Relocation... relocations) {
this.artifactId = artifactId;
this.artifactIdSuffix = artifactIdSuffix;
this.groupId = groupId;
this.relocations = new ArrayList<>(Arrays.stream(relocations).toList());
this.repo = repo;
this.customArtifactID = customArtifactID;
}
public void setArtifactIdSuffix(String artifactIdSuffix) {
this.artifactIdSuffix = artifactIdSuffix;
}
public String getVersion() {
return CustomNameplatesProperties.getValue(customArtifactID);
}
private static String rewriteEscaping(String s) {
return s.replace("{}", ".");
}
public String getFileName(String classifier) {
String name = customArtifactID.toLowerCase(Locale.ROOT).replace('_', '-');
String extra = classifier == null || classifier.isEmpty()
? ""
: "-" + classifier;
return name + "-" + this.getVersion() + extra + ".jar";
}
String getMavenRepoPath() {
return String.format(MAVEN_FORMAT,
rewriteEscaping(groupId).replace(".", "/"),
rewriteEscaping(artifactId),
getVersion(),
rewriteEscaping(artifactId),
getVersion() + artifactIdSuffix
);
}
public List<Relocation> getRelocations() {
return this.relocations;
}
/**
* Creates a {@link MessageDigest} suitable for computing the checksums
* of dependencies.
*
* @return the digest
*/
public static MessageDigest createDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Nullable
public String getRepo() {
return repo;
}
}

View File

@@ -0,0 +1,48 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
/**
* Exception thrown if a dependency cannot be downloaded.
*/
public class DependencyDownloadException extends Exception {
public DependencyDownloadException() {
}
public DependencyDownloadException(String message) {
super(message);
}
public DependencyDownloadException(String message, Throwable cause) {
super(message, cause);
}
public DependencyDownloadException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,53 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
import java.util.Collection;
import java.util.Set;
/**
* Loads and manages runtime dependencies for the plugin.
*/
public interface DependencyManager extends AutoCloseable {
/**
* Loads dependencies.
*
* @param dependencies the dependencies to load
*/
void loadDependencies(Collection<Dependency> dependencies);
/**
* Obtains an isolated classloader containing the given dependencies.
*
* @param dependencies the dependencies
* @return the classloader
*/
ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies);
@Override
void close();
}

View File

@@ -0,0 +1,235 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
import net.momirealms.customnameplates.common.dependency.classloader.IsolatedClassLoader;
import net.momirealms.customnameplates.common.dependency.relocation.Relocation;
import net.momirealms.customnameplates.common.dependency.relocation.RelocationHandler;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.plugin.classpath.ClassPathAppender;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
/**
* Loads and manages runtime dependencies for the plugin.
*/
public class DependencyManagerImpl implements DependencyManager {
/** A registry containing plugin specific behaviour for dependencies. */
private final DependencyRegistry registry;
/** The path where library jars are cached. */
private final Path cacheDirectory;
/** The classpath appender to preload dependencies into */
private final ClassPathAppender classPathAppender;
/** A map of dependencies which have already been loaded. */
private final EnumMap<Dependency, Path> loaded = new EnumMap<>(Dependency.class);
/** A map of isolated classloaders which have been created. */
private final Map<Set<Dependency>, IsolatedClassLoader> loaders = new HashMap<>();
/** Cached relocation handler instance. */
private final RelocationHandler relocationHandler;
private final Executor loadingExecutor;
private final NameplatesPlugin plugin;
public DependencyManagerImpl(NameplatesPlugin plugin) {
this.plugin = plugin;
this.registry = new DependencyRegistry();
this.cacheDirectory = setupCacheDirectory(plugin);
this.classPathAppender = plugin.getClassPathAppender();
this.loadingExecutor = plugin.getScheduler().async();
this.relocationHandler = new RelocationHandler(this);
}
@Override
public ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies) {
Set<Dependency> set = new HashSet<>(dependencies);
for (Dependency dependency : dependencies) {
if (!this.loaded.containsKey(dependency)) {
throw new IllegalStateException("Dependency " + dependency + " is not loaded.");
}
}
synchronized (this.loaders) {
IsolatedClassLoader classLoader = this.loaders.get(set);
if (classLoader != null) {
return classLoader;
}
URL[] urls = set.stream()
.map(this.loaded::get)
.map(file -> {
try {
return file.toUri().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
classLoader = new IsolatedClassLoader(urls);
this.loaders.put(set, classLoader);
return classLoader;
}
}
@Override
public void loadDependencies(Collection<Dependency> dependencies) {
CountDownLatch latch = new CountDownLatch(dependencies.size());
for (Dependency dependency : dependencies) {
if (this.loaded.containsKey(dependency)) {
latch.countDown();
continue;
}
this.loadingExecutor.execute(() -> {
try {
loadDependency(dependency);
} catch (Throwable e) {
this.plugin.getPluginLogger().warn("Unable to load dependency " + dependency.name(), e);
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void loadDependency(Dependency dependency) throws Exception {
if (this.loaded.containsKey(dependency)) {
return;
}
Path file = remapDependency(dependency, downloadDependency(dependency));
this.loaded.put(dependency, file);
if (this.classPathAppender != null && this.registry.shouldAutoLoad(dependency)) {
this.classPathAppender.addJarToClasspath(file);
}
}
private Path downloadDependency(Dependency dependency) throws DependencyDownloadException {
String fileName = dependency.getFileName(null);
Path file = this.cacheDirectory.resolve(fileName);
// if the file already exists, don't attempt to re-download it.
if (Files.exists(file)) {
return file;
}
DependencyDownloadException lastError = null;
String forceRepo = dependency.getRepo();
List<DependencyRepository> repository = DependencyRepository.getByID(forceRepo);
if (!repository.isEmpty()) {
int i = 0;
while (i < repository.size()) {
try {
plugin.getPluginLogger().info("Downloading dependency(" + fileName + ")[" + repository.get(i).getUrl() + dependency.getMavenRepoPath() + "]");
repository.get(i).download(dependency, file);
plugin.getPluginLogger().info("Successfully downloaded " + fileName);
return file;
} catch (DependencyDownloadException e) {
lastError = e;
i++;
}
}
}
throw Objects.requireNonNull(lastError);
}
private Path remapDependency(Dependency dependency, Path normalFile) throws Exception {
List<Relocation> rules = new ArrayList<>(dependency.getRelocations());
if (rules.isEmpty()) {
return normalFile;
}
Path remappedFile = this.cacheDirectory.resolve(dependency.getFileName(DependencyRegistry.isGsonRelocated() ? "remapped-legacy" : "remapped"));
// if the remapped source exists already, just use that.
if (Files.exists(remappedFile)) {
return remappedFile;
}
plugin.getPluginLogger().info("Remapping " + dependency.getFileName(null));
relocationHandler.remap(normalFile, remappedFile, rules);
plugin.getPluginLogger().info("Successfully remapped " + dependency.getFileName(null));
return remappedFile;
}
private static Path setupCacheDirectory(NameplatesPlugin plugin) {
Path cacheDirectory = plugin.getDataDirectory().resolve("libs");
try {
if (Files.exists(cacheDirectory) && (Files.isDirectory(cacheDirectory) || Files.isSymbolicLink(cacheDirectory))) {
return cacheDirectory;
}
try {
Files.createDirectories(cacheDirectory);
} catch (FileAlreadyExistsException e) {
// ignore
}
} catch (IOException e) {
throw new RuntimeException("Unable to create libs directory", e);
}
return cacheDirectory;
}
@Override
public void close() {
IOException firstEx = null;
for (IsolatedClassLoader loader : this.loaders.values()) {
try {
loader.close();
} catch (IOException ex) {
if (firstEx == null) {
firstEx = ex;
} else {
firstEx.addSuppressed(ex);
}
}
}
if (firstEx != null) {
plugin.getPluginLogger().severe(firstEx.getMessage(), firstEx);
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
import com.google.gson.JsonElement;
/**
* Applies CustomFishing specific behaviour for {@link Dependency}s.
*/
public class DependencyRegistry {
public boolean shouldAutoLoad(Dependency dependency) {
return switch (dependency) {
// all used within 'isolated' classloaders, and are therefore not
// relocated.
case ASM, ASM_COMMONS, JAR_RELOCATOR, H2_DRIVER, SQLITE_DRIVER -> false;
default -> true;
};
}
@SuppressWarnings("ConstantConditions")
public static boolean isGsonRelocated() {
return JsonElement.class.getName().startsWith("net.momirealms");
}
private static boolean classExists(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static boolean slf4jPresent() {
return classExists("org.slf4j.Logger") && classExists("org.slf4j.LoggerFactory");
}
}

View File

@@ -0,0 +1,149 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Represents a repository which contains {@link Dependency}s.
*/
public enum DependencyRepository {
/**
* Maven Central
*/
MAVEN_CENTRAL("maven", "https://repo1.maven.org/maven2/") {
@Override
protected URLConnection openConnection(Dependency dependency) throws IOException {
URLConnection connection = super.openConnection(dependency);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
return connection;
}
},
/**
* Maven Central Mirror
*/
MAVEN_CENTRAL_MIRROR("maven", "https://maven.aliyun.com/repository/public/");
private final String url;
private final String id;
DependencyRepository(String id, String url) {
this.url = url;
this.id = id;
}
public String getUrl() {
return url;
}
public static List<DependencyRepository> getByID(String id) {
ArrayList<DependencyRepository> repositories = new ArrayList<>();
for (DependencyRepository repository : values()) {
if (id.equals(repository.id)) {
repositories.add(repository);
}
}
// 中国大陆优先使用国内阿里云镜像
if (id.equals("maven") && Locale.getDefault() == Locale.SIMPLIFIED_CHINESE) {
Collections.reverse(repositories);
}
return repositories;
}
/**
* Opens a connection to the given {@code dependency}.
*
* @param dependency the dependency to download
* @return the connection
* @throws IOException if unable to open a connection
*/
protected URLConnection openConnection(Dependency dependency) throws IOException {
URL dependencyUrl = new URL(this.url + dependency.getMavenRepoPath());
return dependencyUrl.openConnection();
}
/**
* Downloads the raw bytes of the {@code dependency}.
*
* @param dependency the dependency to download
* @return the downloaded bytes
* @throws DependencyDownloadException if unable to download
*/
public byte[] downloadRaw(Dependency dependency) throws DependencyDownloadException {
try {
URLConnection connection = openConnection(dependency);
try (InputStream in = connection.getInputStream()) {
byte[] bytes = in.readAllBytes();
if (bytes.length == 0) {
throw new DependencyDownloadException("Empty stream");
}
return bytes;
}
} catch (Exception e) {
throw new DependencyDownloadException(e);
}
}
/**
* @param dependency the dependency to download
* @return the downloaded bytes
* @throws DependencyDownloadException if unable to download
*/
public byte[] download(Dependency dependency) throws DependencyDownloadException {
return downloadRaw(dependency);
}
/**
* Downloads the the {@code dependency} to the {@code file}, ensuring the
* downloaded bytes match the checksum.
*
* @param dependency the dependency to download
* @param file the file to write to
* @throws DependencyDownloadException if unable to download
*/
public void download(Dependency dependency, Path file) throws DependencyDownloadException {
try {
Files.write(file, download(dependency));
} catch (IOException e) {
throw new DependencyDownloadException(e);
}
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,54 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency.classloader;
import java.net.URL;
import java.net.URLClassLoader;
/**
* A classloader "isolated" from the rest of the Minecraft server.
*
* <p>Used to load specific CustomFishing dependencies without causing conflicts
* with other plugins, or libraries provided by the server implementation.</p>
*/
public class IsolatedClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
public IsolatedClassLoader(URL[] urls) {
/*
* ClassLoader#getSystemClassLoader returns the AppClassLoader
*
* Calling #getParent on this returns the ExtClassLoader (Java 8) or
* the PlatformClassLoader (Java 9). Since we want this classloader to
* be isolated from the Minecraft server (the app), we set the parent
* to be the platform class loader.
*/
super(urls, ClassLoader.getSystemClassLoader().getParent());
}
}

View File

@@ -0,0 +1,66 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency.relocation;
import java.util.Objects;
public final class Relocation {
private static final String RELOCATION_PREFIX = "net.momirealms.customnameplates.libraries.";
public static Relocation of(String id, String pattern) {
return new Relocation(pattern.replace("{}", "."), RELOCATION_PREFIX + id);
}
private final String pattern;
private final String relocatedPattern;
private Relocation(String pattern, String relocatedPattern) {
this.pattern = pattern;
this.relocatedPattern = relocatedPattern;
}
public String getPattern() {
return this.pattern;
}
public String getRelocatedPattern() {
return this.relocatedPattern;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Relocation that = (Relocation) o;
return Objects.equals(this.pattern, that.pattern) &&
Objects.equals(this.relocatedPattern, that.relocatedPattern);
}
@Override
public int hashCode() {
return Objects.hash(this.pattern, this.relocatedPattern);
}
}

View File

@@ -0,0 +1,91 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency.relocation;
import net.momirealms.customnameplates.common.dependency.Dependency;
import net.momirealms.customnameplates.common.dependency.DependencyManager;
import net.momirealms.customnameplates.common.dependency.classloader.IsolatedClassLoader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.*;
/**
* Handles class runtime relocation of packages in downloaded dependencies
*/
public class RelocationHandler {
public static final Set<Dependency> DEPENDENCIES = EnumSet.of(Dependency.ASM, Dependency.ASM_COMMONS, Dependency.JAR_RELOCATOR);
private static final String JAR_RELOCATOR_CLASS = "me.lucko.jarrelocator.JarRelocator";
private static final String JAR_RELOCATOR_RUN_METHOD = "run";
private final Constructor<?> jarRelocatorConstructor;
private final Method jarRelocatorRunMethod;
public RelocationHandler(DependencyManager dependencyManager) {
ClassLoader classLoader = null;
try {
// download the required dependencies for remapping
dependencyManager.loadDependencies(DEPENDENCIES);
// get a classloader containing the required dependencies as sources
classLoader = dependencyManager.obtainClassLoaderWith(DEPENDENCIES);
// load the relocator class
Class<?> jarRelocatorClass = classLoader.loadClass(JAR_RELOCATOR_CLASS);
// prepare the the reflected constructor & method instances
this.jarRelocatorConstructor = jarRelocatorClass.getDeclaredConstructor(File.class, File.class, Map.class);
this.jarRelocatorConstructor.setAccessible(true);
this.jarRelocatorRunMethod = jarRelocatorClass.getDeclaredMethod(JAR_RELOCATOR_RUN_METHOD);
this.jarRelocatorRunMethod.setAccessible(true);
} catch (Exception e) {
try {
if (classLoader instanceof IsolatedClassLoader isolatedClassLoader) {
isolatedClassLoader.close();
}
} catch (IOException ex) {
e.addSuppressed(ex);
}
throw new RuntimeException(e);
}
}
public void remap(Path input, Path output, List<Relocation> relocations) throws Exception {
Map<String, String> mappings = new HashMap<>();
for (Relocation relocation : relocations) {
mappings.put(relocation.getPattern(), relocation.getRelocatedPattern());
}
// create and invoke a new relocator
Object relocator = this.jarRelocatorConstructor.newInstance(input.toFile(), output.toFile(), mappings);
this.jarRelocatorRunMethod.invoke(relocator);
}
}

View File

@@ -0,0 +1,36 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.dependency.relocation;
public final class RelocationHelper {
// screw maven shade
public static final String OKIO_STRING = String.valueOf(new char[]{'o', 'k', 'i', 'o'});
public static final String OKHTTP3_STRING = String.valueOf(new char[]{'o', 'k', 'h', 't', 't', 'p', '3'});
private RelocationHelper() {
}
}

View File

@@ -0,0 +1,14 @@
package net.momirealms.customnameplates.common.event;
public interface Cancellable {
/**
* Gets the cancelled state.
*/
boolean cancelled();
/**
* Sets the cancelled state.
*/
void cancelled(final boolean cancelled);
}

View File

@@ -0,0 +1,143 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
public interface EventConfig {
/**
* The default value for {@link #order()}.
*
*/
int DEFAULT_ORDER = 0;
/**
* The default value for {@link #acceptsCancelled()}.
*
*/
boolean DEFAULT_ACCEPTS_CANCELLED = true;
/**
* The default value for {@link #exact()}.
*
*/
boolean DEFAULT_EXACT = false;
/**
* Gets the post order.
*
* @return the post order
*/
int order();
/**
* Sets the post order.
*
* @param order the post order
* @return an {@link EventConfig}
*/
EventConfig order(final int order);
/**
* Gets if cancelled events are accepted.
*
* @return if cancelled events are accepted
*/
boolean acceptsCancelled();
/**
* Sets if cancelled events are accepted.
*
* @param acceptsCancelled if cancelled events are accepted
* @return an {@link EventConfig}
*/
EventConfig acceptsCancelled(final boolean acceptsCancelled);
/**
* Gets if only the exact event type is accepted.
*
* @return if only the exact event type is accepted
*/
boolean exact();
/**
* Sets if only the exact event type is accepted.
*
* @param exact if only the exact event type is accepted
* @return an {@link EventConfig}
*/
EventConfig exact(final boolean exact);
static EventConfig defaults() {
return EventConfigImpl.DEFAULTS;
}
static EventConfig of(
final int order,
final boolean acceptsCancelled,
final boolean exact
) {
return new EventConfigImpl(order, acceptsCancelled, exact);
}
static Builder builder() {
return new EventConfigImpl.BuilderImpl();
}
/**
* Builder.
*/
interface Builder {
/**
* Sets the post order.
*
* @param order the post order
* @return {@code this}
*/
Builder order(final int order);
/**
* Sets if cancelled events are accepted.
*
* @param acceptsCancelled if cancelled events are accepted
* @return {@code this}
*/
Builder acceptsCancelled(final boolean acceptsCancelled);
/**
* Sets if only the exact event type is accepted.
*
* @param exact if only the exact event type is accepted
* @return {@code this}
*/
Builder exact(final boolean exact);
/**
* Builds.
*
* @return an {@link EventConfig}
*/
EventConfig build();
}
}

View File

@@ -0,0 +1,88 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
record EventConfigImpl(
int order,
boolean acceptsCancelled,
boolean exact
) implements EventConfig {
static final EventConfigImpl DEFAULTS = new EventConfigImpl(DEFAULT_ORDER, DEFAULT_ACCEPTS_CANCELLED, DEFAULT_EXACT);
static EventConfigImpl create(
final int order,
final boolean acceptsCancelled,
final boolean exact
) {
if (order == DEFAULT_ORDER && acceptsCancelled == DEFAULT_ACCEPTS_CANCELLED && exact == DEFAULT_EXACT) {
return DEFAULTS;
}
return new EventConfigImpl(order, acceptsCancelled, exact);
}
@Override
public EventConfig order(final int order) {
return create(order, this.acceptsCancelled, this.exact);
}
@Override
public EventConfig acceptsCancelled(final boolean acceptsCancelled) {
return create(this.order, acceptsCancelled, this.exact);
}
@Override
public EventConfig exact(final boolean exact) {
return create(this.order, this.acceptsCancelled, exact);
}
static final class BuilderImpl implements Builder {
private int order = DEFAULT_ORDER;
private boolean acceptsCancelled = DEFAULT_ACCEPTS_CANCELLED;
private boolean exact = DEFAULT_EXACT;
@Override
public Builder order(final int order) {
this.order = order;
return this;
}
@Override
public Builder acceptsCancelled(final boolean acceptsCancelled) {
this.acceptsCancelled = acceptsCancelled;
return this;
}
@Override
public Builder exact(final boolean exact) {
this.exact = exact;
return this;
}
@Override
public EventConfig build() {
return create(this.order, this.acceptsCancelled, this.exact);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
@FunctionalInterface
public interface EventConsumer<E> {
/**
* Invokes this event consumer.
*/
void onEvent(final E event) throws Throwable;
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.customnameplates.common.event;
import net.momirealms.customnameplates.common.event.bus.EventBus;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.util.OptionalInt;
public interface EventManager {
class SingletonHolder {
private static EventManager INSTANCE = null;
}
static EventManager create(NameplatesPlugin plugin) {
if (SingletonHolder.INSTANCE == null) {
SingletonHolder.INSTANCE = new EventManagerImpl(plugin);
}
return SingletonHolder.INSTANCE;
}
<T extends NameplatesEvent> EventSubscription<T> subscribe(Class<T> event, EventSubscriber<? super T> subscriber);
<T extends NameplatesEvent> EventSubscription<T> subscribe(Class<T> event, EventConfig config, EventSubscriber<? super T> subscriber);
NameplatesEvent dispatch(Class<? extends NameplatesEvent> eventClass, Object... params);
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
NameplatesEvent dispatch(Class<? extends NameplatesEvent> eventClass, OptionalInt order, Object... params);
EventBus<?> getEventBus();
}

View File

@@ -0,0 +1,65 @@
package net.momirealms.customnameplates.common.event;
import net.momirealms.customnameplates.common.event.bus.EventBus;
import net.momirealms.customnameplates.common.event.bus.SimpleEventBus;
import net.momirealms.customnameplates.common.event.gen.EventGenerator;
import net.momirealms.customnameplates.common.event.registry.EventRegistry;
import net.momirealms.customnameplates.common.event.registry.SimpleEventRegistry;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.util.OptionalInt;
public class EventManagerImpl implements EventManager {
private final EventBus<NameplatesEvent> eventBus;
private final EventRegistry<NameplatesEvent> registry;
private final NameplatesPlugin plugin;
protected EventManagerImpl(NameplatesPlugin plugin) {
this.plugin = plugin;
this.registry = new SimpleEventRegistry<>(NameplatesEvent.class);
this.eventBus = new SimpleEventBus<>(registry, new EventBus.EventExceptionHandler() {
@Override
public <E> void eventExceptionCaught(EventBus<? super E> bus, EventSubscription<? super E> subscription, E event, Throwable throwable) {
plugin.getPluginLogger().severe("Exception caught in event handler", throwable);
}
});
}
@Override
public <T extends NameplatesEvent> EventSubscription<T> subscribe(final Class<T> event, final EventSubscriber<? super T> subscriber) {
return registry.subscribe(event, subscriber);
}
@Override
public <T extends NameplatesEvent> EventSubscription<T> subscribe(final Class<T> event, EventConfig config, final EventSubscriber<? super T> subscriber) {
return registry.subscribe(event, config, subscriber);
}
@Override
public NameplatesEvent dispatch(Class<? extends NameplatesEvent> eventClass, Object... params) {
NameplatesEvent event = generate(eventClass, params);
this.eventBus.post(event, OptionalInt.empty());
return event;
}
@Override
public NameplatesEvent dispatch(Class<? extends NameplatesEvent> eventClass, OptionalInt order, Object... params) {
NameplatesEvent event = generate(eventClass, params);
this.eventBus.post(event, order);
return event;
}
@Override
public EventBus<?> getEventBus() {
return this.eventBus;
}
private NameplatesEvent generate(Class<? extends NameplatesEvent> eventClass, Object... params) {
try {
return EventGenerator.generate(eventClass).newInstance(this.plugin, params);
} catch (Throwable e) {
throw new RuntimeException("Exception occurred whilst generating event instance", e);
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
public interface EventSubscriber<E> extends EventConsumer<E> {
}

View File

@@ -0,0 +1,50 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
public interface EventSubscription<E> {
/**
* Gets the event type.
*/
Class<E> event();
/**
* Gets the configuration.
*/
EventConfig config();
/**
* Gets the subscriber.
*/
EventSubscriber<? super E> subscriber();
/**
* Disposes this subscription.
*
* <p>The subscriber held by this subscription will no longer receive events.</p>
*/
void dispose();
}

View File

@@ -0,0 +1,21 @@
package net.momirealms.customnameplates.common.event;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import org.jetbrains.annotations.NotNull;
public interface NameplatesEvent {
/**
* Get the plugin instance this event was dispatched from
*/
@NotNull
NameplatesPlugin plugin();
/**
* Gets the type of the event.
*
* @return the type of the event
*/
@NotNull
Class<? extends NameplatesEvent> eventType();
}

View File

@@ -0,0 +1,49 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Represents the position of a parameter within an event.
*
* <p>This is an implementation detail and should not be relied upon.</p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
/**
* Gets the index of the parameter.
*
* @return the index
*/
int value();
}

View File

@@ -0,0 +1,47 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.bus;
import net.momirealms.customnameplates.common.event.EventSubscription;
import java.util.OptionalInt;
public interface EventBus<E> {
default void post(final E event) {
this.post(event, OptionalInt.empty());
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void post(final E event, final OptionalInt order);
@FunctionalInterface
interface EventExceptionHandler {
/**
* Handles a caught exception.
*/
<E> void eventExceptionCaught(final EventBus<? super E> bus, final EventSubscription<? super E> subscription, final E event, final Throwable throwable);
}
}

View File

@@ -0,0 +1,106 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.bus;
import net.momirealms.customnameplates.common.event.Cancellable;
import net.momirealms.customnameplates.common.event.EventConfig;
import net.momirealms.customnameplates.common.event.EventSubscription;
import net.momirealms.customnameplates.common.event.registry.EventRegistry;
import java.util.List;
import java.util.OptionalInt;
import static java.util.Objects.requireNonNull;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class SimpleEventBus<E> implements EventBus<E> {
protected final EventRegistry<E> registry;
protected final EventExceptionHandler exceptions;
/**
* Constructs a new {@code SimpleEventBus}.
*
* @param registry the event registry
* @param exceptions the event exception handler
*/
public SimpleEventBus(final EventRegistry<E> registry, final EventBus.EventExceptionHandler exceptions) {
this.registry = requireNonNull(registry, "registry");
this.exceptions = requireNonNull(exceptions, "exceptions");
}
@Override
public void post(final E event, final OptionalInt order) {
@SuppressWarnings("unchecked")
final Class<? extends E> type = (Class<? extends E>) event.getClass();
final List<EventSubscription<? super E>> subscriptions = this.registry.subscriptions(type);
if (subscriptions.isEmpty()) {
return;
}
for (final EventSubscription<? super E> subscription : subscriptions) {
if (this.accepts(subscription, event, order)) {
try {
subscription.subscriber().onEvent(event);
} catch (final Throwable t) {
this.exceptions.eventExceptionCaught(this, subscription, event, t);
}
}
}
}
@SuppressWarnings("RedundantIfStatement")
protected boolean accepts(final EventSubscription<? super E> subscription, final E event, final OptionalInt order) {
final EventConfig config = subscription.config();
if (config.exact()) {
if (event.getClass() != subscription.event()) {
return false;
}
}
if (order.isPresent()) {
if (config.order() != order.getAsInt()) {
return false;
}
}
if (!config.acceptsCancelled()) {
if (this.currentlyCancelled(event)) {
return false;
}
}
return true;
}
/**
* Checks if {@code event} is cancelled.
*
* @param event the event
* @return {@code true} if the event is cancelled, {@code false} otherwise
*/
protected boolean currentlyCancelled(final E event) {
return event instanceof Cancellable && ((Cancellable) event).cancelled();
}
}

View File

@@ -0,0 +1,26 @@
package net.momirealms.customnameplates.common.event.gen;
import net.momirealms.customnameplates.common.event.NameplatesEvent;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandles;
public abstract class AbstractNameplatesEvent implements NameplatesEvent {
private final NameplatesPlugin plugin;
protected AbstractNameplatesEvent(NameplatesPlugin plugin) {
this.plugin = plugin;
}
@NotNull
@Override
public NameplatesPlugin plugin() {
return plugin;
}
public MethodHandles.Lookup mhl() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,177 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.gen;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodCall;
import net.momirealms.customnameplates.common.event.Cancellable;
import net.momirealms.customnameplates.common.event.NameplatesEvent;
import net.momirealms.customnameplates.common.event.Param;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import static net.bytebuddy.matcher.ElementMatchers.*;
public class EventGenerator {
/**
* A loading cache of event types to {@link EventGenerator}s.
*/
private static final Map<Class<? extends NameplatesEvent>, EventGenerator> CACHE = new HashMap<>();
/**
* Generate a {@link EventGenerator} for the given {@code event} type.
*
* @param event the event type
* @return the generated class
*/
public static EventGenerator generate(Class<? extends NameplatesEvent> event) {
if (CACHE.containsKey(event)) {
return CACHE.get(event);
} else {
try {
var generator = new EventGenerator(event);
CACHE.put(event, generator);
return generator;
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
}
}
/**
* A method handle for the constructor of the event class.
*/
private final MethodHandle constructor;
/**
* An array of {@link MethodHandle}s, which can set values for each of the properties in the event class.
*/
private final MethodHandle[] setters;
private EventGenerator(Class<? extends NameplatesEvent> eventClass) throws Throwable {
// get a TypeDescription for the event class
TypeDescription eventClassType = new TypeDescription.ForLoadedType(eventClass);
// determine a generated class name of the event
String eventClassSuffix = eventClass.getName().substring(NameplatesEvent.class.getPackage().getName().length());
String packageWithName = EventGenerator.class.getName();
String generatedClassName = packageWithName.substring(0, packageWithName.lastIndexOf('.')) + eventClassSuffix;
DynamicType.Builder<AbstractNameplatesEvent> builder = new ByteBuddy(ClassFileVersion.JAVA_V17)
// create a subclass of AbstractEvent
.subclass(AbstractNameplatesEvent.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
// using the predetermined generated class name
.name(generatedClassName)
// implement the event interface
.implement(eventClassType)
// implement all methods annotated with Param by simply returning the value from the corresponding field with the same name
.method(isAnnotatedWith(Param.class))
.intercept(FieldAccessor.of(NamedElement.WithRuntimeName::getInternalName))
// implement NameplatesEvent#eventType by returning the event class type
.method(named("eventType").and(returns(Class.class)).and(takesArguments(0)))
.intercept(FixedValue.value(eventClassType))
// implement AbstractEvent#mh by calling & returning the value of MethodHandles.lookup()
.method(named("mhl").and(returns(MethodHandles.Lookup.class)).and(takesArguments(0)))
.intercept(MethodCall.invoke(MethodHandles.class.getMethod("lookup")))
// implement a toString method
.withToString();
if (Cancellable.class.isAssignableFrom(eventClass)) {
builder = builder
.defineField("cancelled", boolean.class, Visibility.PRIVATE)
.method(named("cancelled").and(returns(boolean.class)).and(takesArguments(0)))
.intercept(FieldAccessor.ofField("cancelled"))
.method(named("cancelled").and(takesArguments(boolean.class)))
.intercept(FieldAccessor.ofField("cancelled"));
}
// get a sorted array of all methods on the event interface annotated with @Param
Method[] properties = Arrays.stream(eventClass.getMethods())
.filter(m -> m.isAnnotationPresent(Param.class))
.sorted(Comparator.comparingInt(o -> o.getAnnotation(Param.class).value()))
.toArray(Method[]::new);
// for each property, define a field on the generated class to hold the value
for (Method method : properties) {
builder = builder.defineField(method.getName(), method.getReturnType(), Visibility.PRIVATE);
}
// finish building, load the class, get a constructor
Class<? extends AbstractNameplatesEvent> generatedClass = builder.make().load(EventGenerator.class.getClassLoader()).getLoaded();
this.constructor = MethodHandles.publicLookup().in(generatedClass)
.findConstructor(generatedClass, MethodType.methodType(void.class, NameplatesPlugin.class))
.asType(MethodType.methodType(AbstractNameplatesEvent.class, NameplatesPlugin.class));
// create a dummy instance of the generated class & get the method handle lookup instance
MethodHandles.Lookup lookup = ((AbstractNameplatesEvent) this.constructor.invoke((Object) null)).mhl();
// get 'setter' MethodHandles for each property
this.setters = new MethodHandle[properties.length];
for (int i = 0; i < properties.length; i++) {
Method method = properties[i];
this.setters[i] = lookup.findSetter(generatedClass, method.getName(), method.getReturnType())
.asType(MethodType.methodType(void.class, new Class[]{AbstractNameplatesEvent.class, Object.class}));
}
}
/**
* Creates a new instance of the event class.
*/
public NameplatesEvent newInstance(NameplatesPlugin plugin, Object... properties) throws Throwable {
if (properties.length != this.setters.length) {
throw new IllegalStateException("Unexpected number of properties. given: " + properties.length + ", expected: " + this.setters.length);
}
// create a new instance of the event
final AbstractNameplatesEvent event = (AbstractNameplatesEvent) this.constructor.invokeExact(plugin);
// set the properties onto the event instance
for (int i = 0; i < this.setters.length; i++) {
MethodHandle setter = this.setters[i];
Object value = properties[i];
setter.invokeExact(event, value);
}
return event;
}
}

View File

@@ -0,0 +1,87 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.registry;
import net.momirealms.customnameplates.common.event.EventConfig;
import net.momirealms.customnameplates.common.event.EventSubscriber;
import net.momirealms.customnameplates.common.event.EventSubscription;
import java.util.List;
import java.util.function.Predicate;
public interface EventRegistry<E> {
/**
* Gets the base event type.
*
* <p>This is represented by the {@code E} type parameter.</p>
*/
Class<E> type();
/**
* Determines whether the specified event has subscribers.
*
* @param event the event type
* @return whether the specified event has subscribers
*/
default boolean subscribed(final Class<? extends E> event) {
return !this.subscriptions(event).isEmpty();
}
/**
* Registers the given {@code subscriber} to receive events, using the default {@link EventConfig configuration}.
*
* @param event the event type
* @param subscriber the subscriber
* @param <T> the event type
*/
default <T extends E> EventSubscription<T> subscribe(final Class<T> event, final EventSubscriber<? super T> subscriber) {
return this.subscribe(event, EventConfig.defaults(), subscriber);
}
/**
* Registers the given {@code subscriber} to receive events.
*
* @param event the event type
* @param config the event configuration
* @param subscriber the subscriber
* @param <T> the event type
*/
<T extends E> EventSubscription<T> subscribe(final Class<T> event, final EventConfig config, final EventSubscriber<? super T> subscriber);
/**
* Removes subscriptions matching {@code predicate}.
*
* @param predicate the predicate used to determine which subscriptions to remove
*/
void unsubscribeIf(final Predicate<EventSubscription<? super E>> predicate);
/**
* Gets an unmodifiable list containing all subscriptions currently registered for events of type {@code event}.
*
* @return a list of all subscriptions for events of type {@code event}
*/
List<EventSubscription<? super E>> subscriptions(final Class<? extends E> event);
}

View File

@@ -0,0 +1,54 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.registry;
import java.util.ArrayList;
import java.util.List;
final class Internals {
private Internals() {
}
@SuppressWarnings("unchecked")
static <T> List<Class<? super T>> ancestors(final Class<T> type) {
final List<Class<? super T>> types = new ArrayList<>();
types.add(type);
for (int i = 0; i < types.size(); i++) {
final Class<?> next = types.get(i);
final Class<?> superclass = next.getSuperclass();
if (superclass != null) {
types.add((Class<? super T>) superclass);
}
final Class<?>[] interfaces = next.getInterfaces();
for (final Class<?> iface : interfaces) {
// we have a list because we want to preserve order, but we don't want duplicates
if (!types.contains(iface)) {
types.add((Class<? super T>) iface);
}
}
}
return types;
}
}

View File

@@ -0,0 +1,164 @@
/*
* This file is part of event, licensed under the MIT License.
*
* Copyright (c) 2021-2023 Seiama
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.event.registry;
import net.momirealms.customnameplates.common.event.EventConfig;
import net.momirealms.customnameplates.common.event.EventSubscriber;
import net.momirealms.customnameplates.common.event.EventSubscription;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
import static java.util.Objects.requireNonNull;
public class SimpleEventRegistry<E> implements EventRegistry<E> {
private static final Comparator<EventSubscription<?>> ORDER_COMPARATOR = Comparator.comparingInt(subscription -> subscription.config().order());
private final Map<Class<? extends E>, Collection<? extends Class<?>>> classes = new HashMap<>();
private final Map<Class<? extends E>, List<EventSubscription<? super E>>> unbaked = new HashMap<>();
private final Map<Class<? extends E>, List<EventSubscription<? super E>>> baked = new HashMap<>();
private final Object lock = new Object();
private final Class<E> type;
/**
* Constructs a new {@code SimpleEventRegistry}.
*
* @param type the base event type
*/
public SimpleEventRegistry(final Class<E> type) {
this.type = requireNonNull(type, "type");
}
@Override
public Class<E> type() {
return this.type;
}
@Override
public <T extends E> EventSubscription<T> subscribe(final Class<T> event, final EventConfig config, final EventSubscriber<? super T> subscriber) {
requireNonNull(event, "event");
requireNonNull(config, "config");
requireNonNull(subscriber, "subscriber");
final EventSubscription<T> subscription = new EventSubscriptionImpl<>(event, config, subscriber);
synchronized (this.lock) {
final List<EventSubscription<? super T>> subscriptions = yayGenerics(this.unbaked.computeIfAbsent(event, key -> new ArrayList<>()));
subscriptions.add(subscription);
this.baked.clear();
}
return subscription;
}
@Override
public void unsubscribeIf(final Predicate<EventSubscription<? super E>> predicate) {
synchronized (this.lock) {
boolean removedAny = false;
for (final List<EventSubscription<? super E>> subscriptions : this.unbaked.values()) {
removedAny |= subscriptions.removeIf(predicate);
}
if (removedAny) {
this.baked.clear();
}
}
}
@Override
public List<EventSubscription<? super E>> subscriptions(final Class<? extends E> event) {
synchronized (this.lock) {
return this.baked.computeIfAbsent(event, this::computeSubscriptions);
}
}
private List<EventSubscription<? super E>> computeSubscriptions(final Class<? extends E> event) {
final List<EventSubscription<? super E>> subscriptions = new ArrayList<>();
final Collection<? extends Class<?>> types = this.classes.computeIfAbsent(event, this::findClasses);
for (final Class<?> type : types) {
subscriptions.addAll(this.unbaked.getOrDefault(type, Collections.emptyList()));
}
subscriptions.sort(ORDER_COMPARATOR);
return subscriptions;
}
private Collection<? extends Class<?>> findClasses(final Class<?> type) {
final Collection<? extends Class<?>> classes = Internals.ancestors(type);
classes.removeIf(klass -> !this.type.isAssignableFrom(klass));
return classes;
}
@SuppressWarnings("unchecked")
private static <T extends U, U> List<U> yayGenerics(final List<T> list) {
return (List<U>) list;
}
private class EventSubscriptionImpl<T extends E> implements EventSubscription<T> {
private final Class<T> event;
private final EventConfig config;
private final EventSubscriber<? super T> subscriber;
EventSubscriptionImpl(final Class<T> event, final EventConfig config, final EventSubscriber<? super T> subscriber) {
this.event = event;
this.config = config;
this.subscriber = subscriber;
}
@Override
public Class<T> event() {
return this.event;
}
@Override
public EventConfig config() {
return this.config;
}
@Override
public EventSubscriber<? super T> subscriber() {
return this.subscriber;
}
@Override
public void dispose() {
synchronized (SimpleEventRegistry.this.lock) {
final @Nullable List<EventSubscription<? super T>> subscriptions = yayGenerics(SimpleEventRegistry.this.unbaked.get(this.event));
if (subscriptions != null) {
subscriptions.remove(this);
SimpleEventRegistry.this.baked.clear();
}
}
}
@Override
public String toString() {
return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
.add("event=" + this.event)
.add("config=" + this.config)
.add("subscriber=" + this.subscriber)
.toString();
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.caption.Caption;
import org.incendo.cloud.caption.CaptionVariable;
import org.incendo.cloud.minecraft.extras.caption.ComponentCaptionFormatter;
import java.util.List;
public class CustomNameplatesCaptionFormatter<C> implements ComponentCaptionFormatter<C> {
@Override
public @NonNull Component formatCaption(@NonNull Caption captionKey, @NonNull C recipient, @NonNull String caption, @NonNull List<@NonNull CaptionVariable> variables) {
Component component = ComponentCaptionFormatter.translatable().formatCaption(captionKey, recipient, caption, variables);
return TranslationManager.render(component);
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import org.incendo.cloud.caption.Caption;
public final class CustomNameplatesCaptionKeys {
public static final Caption ARGUMENT_PARSE_FAILURE_TIME = Caption.of("argument.parse.failure.time");
public static final Caption ARGUMENT_PARSE_FAILURE_URL = Caption.of("argument.parse.failure.url");
public static final Caption ARGUMENT_PARSE_FAILURE_NAMEDTEXTCOLOR = Caption.of("argument.parse.failure.namedtextcolor");
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.caption.CaptionProvider;
import org.incendo.cloud.caption.DelegatingCaptionProvider;
public final class CustomNameplatesCaptionProvider<C> extends DelegatingCaptionProvider<C> {
private static final CaptionProvider<?> PROVIDER = CaptionProvider.constantProvider()
.putCaption(CustomNameplatesCaptionKeys.ARGUMENT_PARSE_FAILURE_URL, "")
.putCaption(CustomNameplatesCaptionKeys.ARGUMENT_PARSE_FAILURE_TIME, "")
.putCaption(CustomNameplatesCaptionKeys.ARGUMENT_PARSE_FAILURE_NAMEDTEXTCOLOR, "")
.build();
@SuppressWarnings("unchecked")
@Override
public @NonNull CaptionProvider<C> delegate() {
return (CaptionProvider<C>) PROVIDER;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
public interface MessageConstants {
TranslatableComponent.Builder COMMAND_RELOAD_SUCCESS = Component.translatable().key("command.reload.success");
TranslatableComponent.Builder COMMAND_RELOAD_GENERATING = Component.translatable().key("command.reload.generating");
TranslatableComponent.Builder COMMAND_RELOAD_GENERATED = Component.translatable().key("command.reload.generated");
TranslatableComponent.Builder COMMAND_DEBUG_PERFORMANCE = Component.translatable().key("command.debug.performance");
TranslatableComponent.Builder COMMAND_NAMEPLATES_EQUIP_FAILURE_NOT_EXISTS = Component.translatable().key("command.nameplates.equip.failure.not_exists");
TranslatableComponent.Builder COMMAND_NAMEPLATES_EQUIP_FAILURE_PERMISSION = Component.translatable().key("command.nameplates.equip.failure.permission");
TranslatableComponent.Builder COMMAND_NAMEPLATES_EQUIP_FAILURE_NO_CHANGE = Component.translatable().key("command.nameplates.equip.failure.no_change");
TranslatableComponent.Builder COMMAND_NAMEPLATES_EQUIP_SUCCESS = Component.translatable().key("command.nameplates.equip.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_UNEQUIP_FAILURE_NOT_EQUIP = Component.translatable().key("command.nameplates.unequip.failure.not_equip");
TranslatableComponent.Builder COMMAND_NAMEPLATES_UNEQUIP_SUCCESS = Component.translatable().key("command.nameplates.unequip.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_PREVIEW_FAILURE_COOLDOWN = Component.translatable().key("command.nameplates.preview.failure.cooldown");
TranslatableComponent.Builder COMMAND_NAMEPLATES_PREVIEW_SUCCESS = Component.translatable().key("command.nameplates.preview.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_LIST_FAILURE_NONE = Component.translatable().key("command.nameplates.list.failure.none");
TranslatableComponent.Builder COMMAND_NAMEPLATES_LIST_SUCCESS = Component.translatable().key("command.nameplates.list.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_LIST_DELIMITER = Component.translatable().key("command.nameplates.list.delimiter");
TranslatableComponent.Builder COMMAND_NAMEPLATES_FORCE_EQUIP_SUCCESS = Component.translatable().key("command.nameplates.force_equip.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_FORCE_EQUIP_FAILURE_NOT_EXISTS = Component.translatable().key("command.nameplates.force_equip.failure.not_exists");
TranslatableComponent.Builder COMMAND_NAMEPLATES_FORCE_UNEQUIP_SUCCESS = Component.translatable().key("command.nameplates.force_unequip.success");
TranslatableComponent.Builder COMMAND_NAMEPLATES_FORCE_PREVIEW_SUCCESS = Component.translatable().key("command.nameplates.force_preview.success");
TranslatableComponent.Builder COMMAND_BUBBLES_EQUIP_FAILURE_NOT_EXISTS = Component.translatable().key("command.bubbles.equip.failure.not_exists");
TranslatableComponent.Builder COMMAND_BUBBLES_EQUIP_FAILURE_PERMISSION = Component.translatable().key("command.bubbles.equip.failure.permission");
TranslatableComponent.Builder COMMAND_BUBBLES_EQUIP_FAILURE_NO_CHANGE = Component.translatable().key("command.bubbles.equip.failure.no_change");
TranslatableComponent.Builder COMMAND_BUBBLES_EQUIP_SUCCESS = Component.translatable().key("command.bubbles.equip.success");
TranslatableComponent.Builder COMMAND_BUBBLES_UNEQUIP_FAILURE_NOT_EQUIP = Component.translatable().key("command.bubbles.unequip.failure.not_equip");
TranslatableComponent.Builder COMMAND_BUBBLES_UNEQUIP_SUCCESS = Component.translatable().key("command.bubbles.unequip.success");
TranslatableComponent.Builder COMMAND_BUBBLES_LIST_FAILURE_NONE = Component.translatable().key("command.bubbles.list.failure.none");
TranslatableComponent.Builder COMMAND_BUBBLES_LIST_SUCCESS = Component.translatable().key("command.bubbles.list.success");
TranslatableComponent.Builder COMMAND_BUBBLES_LIST_DELIMITER = Component.translatable().key("command.bubbles.list.delimiter");
TranslatableComponent.Builder COMMAND_BUBBLES_FORCE_EQUIP_SUCCESS = Component.translatable().key("command.bubbles.force_equip.success");
TranslatableComponent.Builder COMMAND_BUBBLES_FORCE_EQUIP_FAILURE_NOT_EXISTS = Component.translatable().key("command.bubbles.force_equip.failure.not_exists");
TranslatableComponent.Builder COMMAND_BUBBLES_FORCE_UNEQUIP_SUCCESS = Component.translatable().key("command.bubbles.force_unequip.success");
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.translation.Translator;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
public interface MiniMessageTranslationRegistry extends Translator {
static @NotNull MiniMessageTranslationRegistry create(final Key name, final MiniMessage miniMessage) {
return new MiniMessageTranslationRegistryImpl(requireNonNull(name, "name"), requireNonNull(miniMessage, "MiniMessage"));
}
void register(@NotNull String key, @NotNull Locale locale, @NotNull String format);
void unregister(@NotNull String key);
boolean contains(@NotNull String key);
String miniMessageTranslation(@NotNull String key, @NotNull Locale locale);
void defaultLocale(@NotNull Locale defaultLocale);
default void registerAll(final @NotNull Locale locale, final @NotNull Map<String, String> bundle) {
this.registerAll(locale, bundle.keySet(), bundle::get);
}
default void registerAll(final @NotNull Locale locale, final @NotNull Set<String> keys, final Function<String, String> function) {
IllegalArgumentException firstError = null;
int errorCount = 0;
for (final String key : keys) {
try {
this.register(key, locale, function.apply(key));
} catch (final IllegalArgumentException e) {
if (firstError == null) {
firstError = e;
}
errorCount++;
}
}
if (firstError != null) {
if (errorCount == 1) {
throw firstError;
} else if (errorCount > 1) {
throw new IllegalArgumentException(String.format("Invalid key (and %d more)", errorCount - 1), firstError);
}
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.internal.Internals;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.minimessage.Context;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.ParsingException;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.util.TriState;
import net.kyori.examination.Examinable;
import net.kyori.examination.ExaminableProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
public class MiniMessageTranslationRegistryImpl implements Examinable, MiniMessageTranslationRegistry {
private final Key name;
private final Map<String, Translation> translations = new ConcurrentHashMap<>();
private Locale defaultLocale = Locale.US;
private final MiniMessage miniMessage;
MiniMessageTranslationRegistryImpl(final Key name, final MiniMessage miniMessage) {
this.name = name;
this.miniMessage = miniMessage;
}
@Override
public void register(final @NotNull String key, final @NotNull Locale locale, final @NotNull String format) {
this.translations.computeIfAbsent(key, Translation::new).register(locale, format);
}
@Override
public void unregister(final @NotNull String key) {
this.translations.remove(key);
}
@Override
public boolean contains(final @NotNull String key) {
return this.translations.containsKey(key);
}
@Override
public @NotNull Key name() {
return name;
}
@Override
public @Nullable MessageFormat translate(@NotNull String key, @NotNull Locale locale) {
// No need to implement this method
return null;
}
@Override
public @Nullable Component translate(@NotNull TranslatableComponent component, @NotNull Locale locale) {
Translation translation = translations.get(component.key());
if (translation == null) {
return null;
}
String miniMessageString = translation.translate(locale);
if (miniMessageString == null) {
return null;
}
if (miniMessageString.isEmpty()) {
return Component.empty();
}
final Component resultingComponent;
if (component.arguments().isEmpty()) {
resultingComponent = this.miniMessage.deserialize(miniMessageString);
} else {
resultingComponent = this.miniMessage.deserialize(miniMessageString, new ArgumentTag(component.arguments()));
}
if (component.children().isEmpty()) {
return resultingComponent;
} else {
return resultingComponent.children(component.children());
}
}
@Override
public String miniMessageTranslation(@NotNull String key, @NotNull Locale locale) {
Translation translation = translations.get(key);
if (translation == null) {
return null;
}
return translation.translate(locale);
}
@Override
public @NotNull TriState hasAnyTranslations() {
if (!this.translations.isEmpty()) {
return TriState.TRUE;
}
return TriState.FALSE;
}
@Override
public void defaultLocale(final @NotNull Locale defaultLocale) {
this.defaultLocale = requireNonNull(defaultLocale, "defaultLocale");
}
@Override
public @NotNull Stream<? extends ExaminableProperty> examinableProperties() {
return Stream.of(ExaminableProperty.of("translations", this.translations));
}
@Override
public boolean equals(final Object other) {
if (this == other) return true;
if (!(other instanceof MiniMessageTranslationRegistryImpl that)) return false;
return this.name.equals(that.name)
&& this.translations.equals(that.translations)
&& this.defaultLocale.equals(that.defaultLocale);
}
@Override
public int hashCode() {
return Objects.hash(this.name, this.translations, this.defaultLocale);
}
@Override
public String toString() {
return Internals.toString(this);
}
public static class ArgumentTag implements TagResolver {
private static final String NAME = "argument";
private static final String NAME_1 = "arg";
private final List<? extends ComponentLike> argumentComponents;
public ArgumentTag(final @NotNull List<? extends ComponentLike> argumentComponents) {
this.argumentComponents = Objects.requireNonNull(argumentComponents, "argumentComponents");
}
@Override
public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException {
final int index = arguments.popOr("No argument number provided").asInt().orElseThrow(() -> ctx.newException("Invalid argument number", arguments));
if (index < 0 || index >= argumentComponents.size()) {
throw ctx.newException("Invalid argument number", arguments);
}
return Tag.inserting(argumentComponents.get(index));
}
@Override
public boolean has(final @NotNull String name) {
return name.equals(NAME) || name.equals(NAME_1);
}
}
final class Translation implements Examinable {
private final String key;
private final Map<Locale, String> formats;
Translation(final @NotNull String key) {
this.key = requireNonNull(key, "translation key");
this.formats = new ConcurrentHashMap<>();
}
void register(final @NotNull Locale locale, final @NotNull String format) {
if (this.formats.putIfAbsent(requireNonNull(locale, "locale"), requireNonNull(format, "message format")) != null) {
throw new IllegalArgumentException(String.format("Translation already exists: %s for %s", this.key, locale));
}
}
@Nullable String translate(final @NotNull Locale locale) {
String format = this.formats.get(requireNonNull(locale, "locale"));
if (format == null) {
format = this.formats.get(new Locale(locale.getLanguage())); // try without country
if (format == null) {
format = this.formats.get(MiniMessageTranslationRegistryImpl.this.defaultLocale); // try local default locale
}
}
return format;
}
@Override
public @NotNull Stream<? extends ExaminableProperty> examinableProperties() {
return Stream.of(
ExaminableProperty.of("key", this.key),
ExaminableProperty.of("formats", this.formats)
);
}
@Override
public boolean equals(final Object other) {
if (this == other) return true;
if (!(other instanceof Translation that)) return false;
return this.key.equals(that.key) &&
this.formats.equals(that.formats);
}
@Override
public int hashCode() {
return Objects.hash(this.key, this.formats);
}
@Override
public String toString() {
return Internals.toString(this);
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.translation.Translator;
import net.kyori.examination.Examinable;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
public interface MiniMessageTranslator extends Translator, Examinable {
static @NotNull MiniMessageTranslator translator() {
return MiniMessageTranslatorImpl.INSTANCE;
}
static @NotNull TranslatableComponentRenderer<Locale> renderer() {
return MiniMessageTranslatorImpl.INSTANCE.renderer;
}
static @NotNull Component render(final @NotNull Component component, final @NotNull Locale locale) {
return renderer().render(component, locale);
}
@NotNull Iterable<? extends Translator> sources();
boolean addSource(final @NotNull Translator source);
boolean removeSource(final @NotNull Translator source);
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.translation.Translator;
import net.kyori.adventure.util.TriState;
import net.kyori.examination.ExaminableProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
public class MiniMessageTranslatorImpl implements MiniMessageTranslator {
private static final Key NAME = Key.key("customfishing", "main");
static final MiniMessageTranslatorImpl INSTANCE = new MiniMessageTranslatorImpl();
final TranslatableComponentRenderer<Locale> renderer = TranslatableComponentRenderer.usingTranslationSource(this);
private final Set<Translator> sources = Collections.newSetFromMap(new ConcurrentHashMap<>());
@Override
public @NotNull Key name() {
return NAME;
}
@Override
public @NotNull TriState hasAnyTranslations() {
if (!this.sources.isEmpty()) {
return TriState.TRUE;
}
return TriState.FALSE;
}
@Override
public @Nullable MessageFormat translate(@NotNull String key, @NotNull Locale locale) {
// No need to implement this method
return null;
}
@Override
public @Nullable Component translate(@NotNull TranslatableComponent component, @NotNull Locale locale) {
for (final Translator source : this.sources) {
final Component translation = source.translate(component, locale);
if (translation != null) return translation;
}
return null;
}
@Override
public @NotNull Iterable<? extends Translator> sources() {
return Collections.unmodifiableSet(this.sources);
}
@Override
public boolean addSource(final @NotNull Translator source) {
if (source == this) throw new IllegalArgumentException("MiniMessageTranslationSource");
return this.sources.add(source);
}
@Override
public boolean removeSource(final @NotNull Translator source) {
return this.sources.remove(source);
}
@Override
public @NotNull Stream<? extends ExaminableProperty> examinableProperties() {
return Stream.of(ExaminableProperty.of("sources", this.sources));
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.locale;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.translation.Translator;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.util.Pair;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TranslationManager {
private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
private static Locale FORCE_LOCALE = null;
private static final List<String> locales = List.of("en", "zh_cn");
private static TranslationManager instance;
private final NameplatesPlugin plugin;
private final Set<Locale> installed = ConcurrentHashMap.newKeySet();
private MiniMessageTranslationRegistry registry;
private final Path translationsDirectory;
public TranslationManager(NameplatesPlugin plugin) {
this.plugin = plugin;
this.translationsDirectory = this.plugin.getConfigDirectory().resolve("translations");
instance = this;
}
public static void forceLocale(Locale locale) {
FORCE_LOCALE = locale;
}
public void reload() {
// remove any previous registry
if (this.registry != null) {
MiniMessageTranslator.translator().removeSource(this.registry);
this.installed.clear();
}
for (String lang : locales) {
this.plugin.getConfigManager().saveResource("translations/" + lang + ".yml");
}
this.registry = MiniMessageTranslationRegistry.create(Key.key("customnameplates", "main"), MiniMessage.miniMessage());
this.registry.defaultLocale(DEFAULT_LOCALE);
this.loadFromFileSystem(this.translationsDirectory, false);
MiniMessageTranslator.translator().addSource(this.registry);
}
public static String miniMessageTranslation(String key) {
return miniMessageTranslation(key, null);
}
public static String miniMessageTranslation(String key, @Nullable Locale locale) {
if (FORCE_LOCALE != null) {
return instance.registry.miniMessageTranslation(key, FORCE_LOCALE);
}
if (locale == null) {
locale = Locale.getDefault();
if (locale == null) {
locale = DEFAULT_LOCALE;
}
}
return instance.registry.miniMessageTranslation(key, locale);
}
public static Component render(Component component) {
return render(component, null);
}
public static Component render(Component component, @Nullable Locale locale) {
if (FORCE_LOCALE != null) {
return MiniMessageTranslator.render(component, FORCE_LOCALE);
}
if (locale == null) {
locale = Locale.getDefault();
if (locale == null) {
locale = DEFAULT_LOCALE;
}
}
return MiniMessageTranslator.render(component, locale);
}
public void loadFromFileSystem(Path directory, boolean suppressDuplicatesError) {
List<Path> translationFiles;
try (Stream<Path> stream = Files.list(directory)) {
translationFiles = stream.filter(TranslationManager::isTranslationFile).collect(Collectors.toList());
} catch (IOException e) {
translationFiles = Collections.emptyList();
}
if (translationFiles.isEmpty()) {
return;
}
Map<Locale, Map<String, String>> loaded = new HashMap<>();
for (Path translationFile : translationFiles) {
try {
Pair<Locale, Map<String, String>> result = loadTranslationFile(translationFile);
loaded.put(result.left(), result.right());
} catch (Exception e) {
if (!suppressDuplicatesError || !isAdventureDuplicatesException(e)) {
this.plugin.getPluginLogger().warn("Error loading locale file: " + translationFile.getFileName(), e);
}
}
}
// try registering the locale without a country code - if we don't already have a registration for that
loaded.forEach((locale, bundle) -> {
Locale localeWithoutCountry = new Locale(locale.getLanguage());
if (!locale.equals(localeWithoutCountry) && !localeWithoutCountry.equals(DEFAULT_LOCALE) && this.installed.add(localeWithoutCountry)) {
try {
this.registry.registerAll(localeWithoutCountry, bundle);
} catch (IllegalArgumentException e) {
// ignore
}
}
});
Locale localLocale = Locale.getDefault();
if (!this.installed.contains(localLocale) && FORCE_LOCALE == null) {
plugin.getPluginLogger().warn(localLocale.toString().toLowerCase(Locale.ENGLISH) + ".yml not exists, using " + DEFAULT_LOCALE.toString().toLowerCase(Locale.ENGLISH) + ".yml as default locale.");
}
}
public static boolean isTranslationFile(Path path) {
return path.getFileName().toString().endsWith(".yml");
}
private static boolean isAdventureDuplicatesException(Exception e) {
return e instanceof IllegalArgumentException && (e.getMessage().startsWith("Invalid key") || e.getMessage().startsWith("Translation already exists"));
}
@SuppressWarnings("unchecked")
private Pair<Locale, Map<String, String>> loadTranslationFile(Path translationFile) {
String fileName = translationFile.getFileName().toString();
String localeString = fileName.substring(0, fileName.length() - ".yml".length());
Locale locale = parseLocale(localeString);
if (locale == null) {
throw new IllegalStateException("Unknown locale '" + localeString + "' - unable to register.");
}
Map<String, String> bundle = new HashMap<>();
YamlDocument document = plugin.getConfigManager().loadConfig("translations" + File.separator + translationFile.getFileName(), '@');
try {
document.save(new File(plugin.getDataDirectory().toFile(), "translations" + File.separator + translationFile.getFileName()));
} catch (IOException e) {
throw new IllegalStateException("Could not update translation file: " + translationFile.getFileName(), e);
}
Map<String, Object> map = document.getStringRouteMappedValues(false);
map.remove("config-version");
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof List<?> list) {
List<String> strList = (List<String>) list;
StringJoiner stringJoiner = new StringJoiner("<reset><newline>");
for (String str : strList) {
stringJoiner.add(str);
}
bundle.put(entry.getKey(), stringJoiner.toString());
} else if (entry.getValue() instanceof String str) {
bundle.put(entry.getKey(), str);
}
}
this.registry.registerAll(locale, bundle);
this.installed.add(locale);
return Pair.of(locale, bundle);
}
public static @Nullable Locale parseLocale(@Nullable String locale) {
return locale == null || locale.isEmpty() ? null : Translator.parseLocale(locale);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.plugin;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class CustomNameplatesProperties {
private final HashMap<String, String> propertyMap;
private CustomNameplatesProperties(HashMap<String, String> propertyMap) {
this.propertyMap = propertyMap;
}
public static String getValue(String key) {
if (!SingletonHolder.INSTANCE.propertyMap.containsKey(key)) {
throw new RuntimeException("Unknown key: " + key);
}
return SingletonHolder.INSTANCE.propertyMap.get(key);
}
private static class SingletonHolder {
private static final CustomNameplatesProperties INSTANCE = getInstance();
private static CustomNameplatesProperties getInstance() {
try (InputStream inputStream = CustomNameplatesProperties.class.getClassLoader().getResourceAsStream("custom-nameplates.properties")) {
HashMap<String, String> versionMap = new HashMap<>();
Properties properties = new Properties();
properties.load(inputStream);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
if (entry.getKey() instanceof String key && entry.getValue() instanceof String value) {
versionMap.put(key, value);
}
}
return new CustomNameplatesProperties(versionMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.plugin;
import net.momirealms.customnameplates.common.dependency.DependencyManager;
import net.momirealms.customnameplates.common.locale.TranslationManager;
import net.momirealms.customnameplates.common.plugin.classpath.ClassPathAppender;
import net.momirealms.customnameplates.common.plugin.config.ConfigLoader;
import net.momirealms.customnameplates.common.plugin.logging.PluginLogger;
import net.momirealms.customnameplates.common.plugin.scheduler.SchedulerAdapter;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.function.Supplier;
/**
* Interface representing the main CustomFishing plugin.
*/
public interface NameplatesPlugin {
/**
* Retrieves an input stream for a resource file within the plugin.
*
* @param filePath the path to the resource file
* @return an {@link InputStream} for the resource file
*/
InputStream getResourceStream(String filePath);
/**
* Retrieves the plugin logger.
*
* @return the {@link PluginLogger} instance
*/
PluginLogger getPluginLogger();
/**
* Retrieves the class path appender.
*
* @return the {@link ClassPathAppender} instance
*/
ClassPathAppender getClassPathAppender();
/**
* Retrieves the scheduler adapter.
*
* @return the {@link SchedulerAdapter} instance
*/
SchedulerAdapter<?> getScheduler();
/**
* Retrieves the data directory path.
*
* @return the {@link Path} to the data directory
*/
Path getDataDirectory();
/**
* Retrieves the configuration directory path.
* By default, this is the same as the data directory.
*
* @return the {@link Path} to the configuration directory
*/
default Path getConfigDirectory() {
return getDataDirectory();
}
/**
* Retrieves the dependency manager.
*
* @return the {@link DependencyManager} instance
*/
DependencyManager getDependencyManager();
/**
* Retrieves the translation manager.
*
* @return the {@link TranslationManager} instance
*/
TranslationManager getTranslationManager();
/**
* Retrieves the configuration manager.
*
* @return the {@link ConfigLoader} instance
*/
ConfigLoader getConfigManager();
/**
* Retrieves the server version.
*
* @return the server version as a string
*/
String getServerVersion();
/**
* Retrieves the plugin version.
*
* @return the plugin version as a string
*/
String getPluginVersion();
/**
* Loads the plugin.
* This method is called during the plugin's loading phase.
*/
void load();
/**
* Enables the plugin.
* This method is called during the plugin's enabling phase.
*/
void enable();
/**
* Disables the plugin.
* This method is called during the plugin's disabling phase.
*/
void disable();
/**
* Reloads the plugin.
*/
void reload();
/**
* Debug
*
* @param supplier debug message supplier
*/
void debug(Supplier<String> supplier);
boolean isUpToDate();
default File getDataFolder() {
return getDataDirectory().toFile();
}
}

View File

@@ -0,0 +1,41 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.classpath;
import java.nio.file.Path;
/**
* Interface which allows access to add URLs to the plugin classpath at runtime.
*/
public interface ClassPathAppender extends AutoCloseable {
void addJarToClasspath(Path file);
@Override
default void close() {
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.classpath;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.nio.file.Path;
public class ReflectionClassPathAppender implements ClassPathAppender {
private final URLClassLoaderAccess classLoaderAccess;
public ReflectionClassPathAppender(ClassLoader classLoader) throws IllegalStateException {
if (classLoader instanceof URLClassLoader) {
this.classLoaderAccess = URLClassLoaderAccess.create((URLClassLoader) classLoader);
} else {
throw new IllegalStateException("ClassLoader is not instance of URLClassLoader");
}
}
public ReflectionClassPathAppender(NameplatesPlugin plugin) throws IllegalStateException {
this(plugin.getClass().getClassLoader());
}
@Override
public void addJarToClasspath(Path file) {
try {
this.classLoaderAccess.addURL(file.toUri().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,190 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.classpath;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
/**
* Provides access to {@link URLClassLoader}#addURL.
*/
public abstract class URLClassLoaderAccess {
/**
* Creates a {@link URLClassLoaderAccess} for the given class loader.
*
* @param classLoader the class loader
* @return the access object
*/
public static URLClassLoaderAccess create(URLClassLoader classLoader) {
if (Reflection.isSupported()) {
return new Reflection(classLoader);
} else if (Unsafe.isSupported()) {
return new Unsafe(classLoader);
} else {
return Noop.INSTANCE;
}
}
private final URLClassLoader classLoader;
protected URLClassLoaderAccess(URLClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Adds the given URL to the class loader.
*
* @param url the URL to add
*/
public abstract void addURL(@NotNull URL url);
private static void throwError(Throwable cause) throws UnsupportedOperationException {
throw new UnsupportedOperationException("CustomNameplates is unable to inject into the plugin URLClassLoader.\n" +
"You may be able to fix this problem by adding the following command-line argument " +
"directly after the 'java' command in your start script: \n'--add-opens java.base/java.lang=ALL-UNNAMED'", cause);
}
/**
* Accesses using reflection, not supported on Java 9+.
*/
private static class Reflection extends URLClassLoaderAccess {
private static final Method ADD_URL_METHOD;
static {
Method addUrlMethod;
try {
addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addUrlMethod.setAccessible(true);
} catch (Exception e) {
addUrlMethod = null;
}
ADD_URL_METHOD = addUrlMethod;
}
private static boolean isSupported() {
return ADD_URL_METHOD != null;
}
Reflection(URLClassLoader classLoader) {
super(classLoader);
}
@Override
public void addURL(@NotNull URL url) {
try {
ADD_URL_METHOD.invoke(super.classLoader, url);
} catch (ReflectiveOperationException e) {
URLClassLoaderAccess.throwError(e);
}
}
}
/**
* Accesses using sun.misc.Unsafe, supported on Java 9+.
*
* @author Vaishnav Anil (https://github.com/slimjar/slimjar)
*/
private static class Unsafe extends URLClassLoaderAccess {
private static final sun.misc.Unsafe UNSAFE;
static {
sun.misc.Unsafe unsafe;
try {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (sun.misc.Unsafe) unsafeField.get(null);
} catch (Throwable t) {
unsafe = null;
}
UNSAFE = unsafe;
}
private static boolean isSupported() {
return UNSAFE != null;
}
private final Collection<URL> unopenedURLs;
private final Collection<URL> pathURLs;
@SuppressWarnings("unchecked")
Unsafe(URLClassLoader classLoader) {
super(classLoader);
Collection<URL> unopenedURLs;
Collection<URL> pathURLs;
try {
Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp");
unopenedURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "unopenedUrls");
pathURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "path");
} catch (Throwable e) {
unopenedURLs = null;
pathURLs = null;
}
this.unopenedURLs = unopenedURLs;
this.pathURLs = pathURLs;
}
private static Object fetchField(final Class<?> clazz, final Object object, final String name) throws NoSuchFieldException {
Field field = clazz.getDeclaredField(name);
long offset = UNSAFE.objectFieldOffset(field);
return UNSAFE.getObject(object, offset);
}
@Override
public void addURL(@NotNull URL url) {
if (this.unopenedURLs == null || this.pathURLs == null) {
URLClassLoaderAccess.throwError(new NullPointerException("unopenedURLs or pathURLs"));
}
synchronized (this.unopenedURLs) {
this.unopenedURLs.add(url);
this.pathURLs.add(url);
}
}
}
private static class Noop extends URLClassLoaderAccess {
private static final Noop INSTANCE = new Noop();
private Noop() {
super(null);
}
@Override
public void addURL(@NotNull URL url) {
URLClassLoaderAccess.throwError(null);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.plugin.config;
import dev.dejvokep.boostedyaml.YamlDocument;
import java.io.File;
/**
* Interface for loading and managing configuration files.
*/
public interface ConfigLoader {
/**
* Loads a YAML configuration file from the specified file path.
*
* @param filePath the path to the configuration file
* @return the loaded {@link YamlDocument}
*/
YamlDocument loadConfig(String filePath);
/**
* Loads a YAML configuration file from the specified file path with a custom route separator.
*
* @param filePath the path to the configuration file
* @param routeSeparator the custom route separator character
* @return the loaded {@link YamlDocument}
*/
YamlDocument loadConfig(String filePath, char routeSeparator);
/**
* Loads a YAML data file.
*
* @param file the {@link File} object representing the data file
* @return the loaded {@link YamlDocument}
*/
YamlDocument loadData(File file);
/**
* Saves a resource file from the plugin's jar to the specified file path.
*
* @param filePath the path where the resource file will be saved
*/
void saveResource(String filePath);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,32 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.team;
package net.momirealms.customnameplates.common.plugin.feature;
import java.util.Locale;
public interface Reloadable {
public enum TeamColor {
default void reload() {
unload();
load();
}
BLACK,
DARK_BLUE,
DARK_GREEN,
DARK_AQUA,
DARK_RED,
DARK_PURPLE,
GOLD,
GRAY,
DARK_GRAY,
BLUE,
GREEN,
AQUA,
RED,
LIGHT_PURPLE,
YELLOW,
WHITE,
NONE,
CUSTOM;
default void unload() {
}
public TeamColor getById(String id) throws IllegalArgumentException {
return valueOf(id.toUpperCase(Locale.ENGLISH));
default void load() {
}
default void disable() {
unload();
}
}

View File

@@ -0,0 +1,62 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.logging;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JavaPluginLogger implements PluginLogger {
private final Logger logger;
public JavaPluginLogger(Logger logger) {
this.logger = logger;
}
@Override
public void info(String s) {
this.logger.info(s);
}
@Override
public void warn(String s) {
this.logger.warning(s);
}
@Override
public void warn(String s, Throwable t) {
this.logger.log(Level.WARNING, s, t);
}
@Override
public void severe(String s) {
this.logger.severe(s);
}
@Override
public void severe(String s, Throwable t) {
this.logger.log(Level.SEVERE, s, t);
}
}

View File

@@ -0,0 +1,61 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.logging;
import org.apache.logging.log4j.Logger;
public class Log4jPluginLogger implements PluginLogger {
private final Logger logger;
public Log4jPluginLogger(Logger logger) {
this.logger = logger;
}
@Override
public void info(String s) {
this.logger.info(s);
}
@Override
public void warn(String s) {
this.logger.warn(s);
}
@Override
public void warn(String s, Throwable t) {
this.logger.warn(s, t);
}
@Override
public void severe(String s) {
this.logger.error(s);
}
@Override
public void severe(String s, Throwable t) {
this.logger.error(s, t);
}
}

View File

@@ -0,0 +1,46 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.logging;
/**
* Represents the logger instance being used by CustomFishing on the platform.
*
* <p>Messages sent using the logger are sent prefixed with the CustomFishing tag,
* and on some implementations will be colored depending on the message type.</p>
*/
public interface PluginLogger {
void info(String s);
void warn(String s);
void warn(String s, Throwable t);
void severe(String s);
void severe(String s, Throwable t);
}

View File

@@ -0,0 +1,61 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.logging;
import org.slf4j.Logger;
public class Slf4jPluginLogger implements PluginLogger {
private final Logger logger;
public Slf4jPluginLogger(Logger logger) {
this.logger = logger;
}
@Override
public void info(String s) {
this.logger.info(s);
}
@Override
public void warn(String s) {
this.logger.warn(s);
}
@Override
public void warn(String s, Throwable t) {
this.logger.warn(s, t);
}
@Override
public void severe(String s) {
this.logger.error(s);
}
@Override
public void severe(String s, Throwable t) {
this.logger.error(s, t);
}
}

View File

@@ -0,0 +1,136 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.scheduler;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Arrays;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Abstract implementation of {@link SchedulerAdapter} using a {@link ScheduledExecutorService}.
*/
public abstract class AbstractJavaScheduler<T> implements SchedulerAdapter<T> {
private static final int PARALLELISM = 16;
private final NameplatesPlugin plugin;
private final ScheduledThreadPoolExecutor scheduler;
private final ForkJoinPool worker;
public AbstractJavaScheduler(NameplatesPlugin plugin) {
this.plugin = plugin;
this.scheduler = new ScheduledThreadPoolExecutor(4, r -> {
Thread thread = Executors.defaultThreadFactory().newThread(r);
thread.setName("nameplates-scheduler");
return thread;
});
this.scheduler.setRemoveOnCancelPolicy(true);
this.scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
this.worker = new ForkJoinPool(PARALLELISM, new WorkerThreadFactory(), new ExceptionHandler(), false);
}
@Override
public Executor async() {
return this.worker;
}
@Override
public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) {
if (delay == 0) {
async().execute(task);
return new DummyTask();
}
ScheduledFuture<?> future = this.scheduler.schedule(() -> this.worker.execute(task), delay, unit);
return new AsyncTask(future);
}
@Override
public SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit) {
ScheduledFuture<?> future = this.scheduler.scheduleAtFixedRate(() -> this.worker.execute(task), delay, interval, unit);
return new AsyncTask(future);
}
@Override
public void shutdownScheduler() {
this.scheduler.shutdown();
try {
if (!this.scheduler.awaitTermination(1, TimeUnit.MINUTES)) {
this.plugin.getPluginLogger().severe("Timed out waiting for the CustomFishing scheduler to terminate");
reportRunningTasks(thread -> thread.getName().equals("customfishing-scheduler"));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void shutdownExecutor() {
this.worker.shutdown();
try {
if (!this.worker.awaitTermination(1, TimeUnit.MINUTES)) {
this.plugin.getPluginLogger().severe("Timed out waiting for the CustomFishing worker thread pool to terminate");
reportRunningTasks(thread -> thread.getName().startsWith("nameplates-worker-"));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void reportRunningTasks(Predicate<Thread> predicate) {
Thread.getAllStackTraces().forEach((thread, stack) -> {
if (predicate.test(thread)) {
this.plugin.getPluginLogger().warn("Thread " + thread.getName() + " is blocked, and may be the reason for the slow shutdown!\n" +
Arrays.stream(stack).map(el -> " " + el).collect(Collectors.joining("\n"))
);
}
});
}
private static final class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
private static final AtomicInteger COUNT = new AtomicInteger(0);
@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
thread.setDaemon(true);
thread.setName("nameplates-worker-" + COUNT.getAndIncrement());
return thread;
}
}
private final class ExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
AbstractJavaScheduler.this.plugin.getPluginLogger().warn("Thread " + t.getName() + " threw an uncaught exception", e);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,34 +15,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.team;
package net.momirealms.customnameplates.common.plugin.scheduler;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ScheduledFuture;
import java.util.Arrays;
public class AsyncTask implements SchedulerTask {
public enum TeamCollisionRule {
private final ScheduledFuture<?> future;
ALWAYS("always"),
NEVER("never"),
PUSH_OTHER_TEAMS("pushOtherTeams"),
PUSH_OWN_TEAM("pushOwnTeam");
private final String id;
TeamCollisionRule(@NotNull String id) {
this.id = id;
public AsyncTask(ScheduledFuture<?> future) {
this.future = future;
}
public String getId() {
return id;
@Override
public void cancel() {
future.cancel(false);
}
@NotNull
public static TeamCollisionRule byId(String id) {
return Arrays.stream(values())
.filter(mode -> mode.id.equals(id))
.findFirst()
.orElse(ALWAYS);
@Override
public boolean cancelled() {
return future.isCancelled();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,11 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.message;
package net.momirealms.customnameplates.common.plugin.scheduler;
public class MessageType {
public class DummyTask implements SchedulerTask {
public static final String CREATE = "0";
public static final String REMOVE = "1";
public static final String UPDATE = "2";
}
@Override
public void cancel() {
}
@Override
public boolean cancelled() {
return true;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.plugin.scheduler;
public interface RegionExecutor<T> {
void run(Runnable r, T l);
SchedulerTask runLater(Runnable r, long delayTicks, T l);
SchedulerTask runRepeating(Runnable r, long delayTicks, long period, T l);
}

View File

@@ -0,0 +1,106 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.scheduler;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* A scheduler for running tasks using the systems provided by the platform
*/
public interface SchedulerAdapter<T> {
/**
* Gets an async executor instance
*
* @return an async executor instance
*/
Executor async();
/**
* Gets a sync executor instance
*
* @return a sync executor instance
*/
RegionExecutor<T> sync();
/**
* Executes a task async
*
* @param task the task
*/
default void executeAsync(Runnable task) {
async().execute(task);
}
/**
* Executes a task sync
*
* @param task the task
*/
default void executeSync(Runnable task, T location) {
sync().run(task, location);
}
default void executeSync(Runnable task) {
sync().run(task, null);
}
/**
* Executes the given task with a delay.
*
* @param task the task
* @param delay the delay
* @param unit the unit of delay
* @return the resultant task instance
*/
SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit);
/**
* Executes the given task repeatedly at a given interval.
*
* @param task the task
* @param interval the interval
* @param unit the unit of interval
* @return the resultant task instance
*/
SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit);
/**
* Shuts down the scheduler instance.
*
* <p>{@link #asyncLater(Runnable, long, TimeUnit)} and {@link #asyncRepeating(Runnable, long, long, TimeUnit)}.</p>
*/
void shutdownScheduler();
/**
* Shuts down the executor instance.
*
* <p>{@link #async()} and {@link #executeAsync(Runnable)}.</p>
*/
void shutdownExecutor();
}

View File

@@ -0,0 +1,39 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.plugin.scheduler;
/**
* Represents a scheduled task
*/
public interface SchedulerTask {
/**
* Cancels the task.
*/
void cancel();
boolean cancelled();
}

View File

@@ -0,0 +1,116 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.sender;
import net.kyori.adventure.text.Component;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.util.Tristate;
import java.util.UUID;
/**
* Simple implementation of {@link Sender} using a {@link SenderFactory}
*
* @param <T> the command sender type
*/
public final class AbstractSender<T> implements Sender {
private final NameplatesPlugin plugin;
private final SenderFactory<?, T> factory;
private final T sender;
private final UUID uniqueId;
private final String name;
private final boolean isConsole;
AbstractSender(NameplatesPlugin plugin, SenderFactory<?, T> factory, T sender) {
this.plugin = plugin;
this.factory = factory;
this.sender = sender;
this.uniqueId = factory.getUniqueId(this.sender);
this.name = factory.getName(this.sender);
this.isConsole = this.factory.isConsole(this.sender);
}
@Override
public NameplatesPlugin getPlugin() {
return this.plugin;
}
@Override
public UUID getUniqueId() {
return this.uniqueId;
}
@Override
public String getName() {
return this.name;
}
@Override
public void sendMessage(Component message) {
this.factory.sendMessage(this.sender, message);
}
@Override
public void sendMessage(Component message, boolean ignoreEmpty) {
if (ignoreEmpty && message.equals(Component.empty())) {
return;
}
sendMessage(message);
}
@Override
public Tristate getPermissionValue(String permission) {
return (isConsole() && this.factory.consoleHasAllPermissions()) ? Tristate.TRUE : this.factory.getPermissionValue(this.sender, permission);
}
@Override
public boolean hasPermission(String permission) {
return (isConsole() && this.factory.consoleHasAllPermissions()) || this.factory.hasPermission(this.sender, permission);
}
@Override
public void performCommand(String commandLine) {
this.factory.performCommand(this.sender, commandLine);
}
@Override
public boolean isConsole() {
return this.isConsole;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof AbstractSender<?> that)) return false;
return this.getUniqueId().equals(that.getUniqueId());
}
@Override
public int hashCode() {
return this.uniqueId.hashCode();
}
}

View File

@@ -0,0 +1,63 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.sender;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import java.util.UUID;
public abstract class DummyConsoleSender implements Sender {
private final NameplatesPlugin platform;
public DummyConsoleSender(NameplatesPlugin plugin) {
this.platform = plugin;
}
@Override
public void performCommand(String commandLine) {
}
@Override
public boolean isConsole() {
return true;
}
@Override
public NameplatesPlugin getPlugin() {
return this.platform;
}
@Override
public UUID getUniqueId() {
return Sender.CONSOLE_UUID;
}
@Override
public String getName() {
return Sender.CONSOLE_NAME;
}
}

View File

@@ -0,0 +1,121 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.sender;
import net.kyori.adventure.text.Component;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.util.Tristate;
import java.util.UUID;
/**
* Wrapper interface to represent a CommandSender/CommandSource within the common command implementations.
*/
public interface Sender {
/** The uuid used by the console sender. */
UUID CONSOLE_UUID = new UUID(0, 0); // 00000000-0000-0000-0000-000000000000
/** The name used by the console sender. */
String CONSOLE_NAME = "Console";
/**
* Gets the plugin instance the sender is from.
*
* @return the plugin
*/
NameplatesPlugin getPlugin();
/**
* Gets the sender's username
*
* @return a friendly username for the sender
*/
String getName();
/**
* Gets the sender's unique id.
*
* <p>See {@link #CONSOLE_UUID} for the console's UUID representation.</p>
*
* @return the sender's uuid
*/
UUID getUniqueId();
/**
* Send a json message to the Sender.
*
* @param message the message to send.
*/
void sendMessage(Component message);
/**
* Send a json message to the Sender.
*
* @param message the message to send.
* @param ignoreEmpty whether to ignore empty component
*/
void sendMessage(Component message, boolean ignoreEmpty);
/**
* Gets the tristate a permission is set to.
*
* @param permission the permission to check for
* @return a tristate
*/
Tristate getPermissionValue(String permission);
/**
* Check if the Sender has a permission.
*
* @param permission the permission to check for
* @return true if the sender has the permission
*/
boolean hasPermission(String permission);
/**
* Makes the sender perform a command.
*
* @param commandLine the command
*/
void performCommand(String commandLine);
/**
* Gets whether this sender is the console
*
* @return if the sender is the console
*/
boolean isConsole();
/**
* Gets whether this sender is still valid & receiving messages.
*
* @return if this sender is valid
*/
default boolean isValid() {
return true;
}
}

View File

@@ -0,0 +1,82 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.sender;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.momirealms.customnameplates.common.plugin.NameplatesPlugin;
import net.momirealms.customnameplates.common.util.Tristate;
import java.util.Objects;
import java.util.UUID;
/**
* Factory class to make a thread-safe sender instance
*
* @param <P> the plugin type
* @param <T> the command sender type
*/
public abstract class SenderFactory<P extends NameplatesPlugin, T> implements AutoCloseable {
private final P plugin;
public SenderFactory(P plugin) {
this.plugin = plugin;
}
protected P getPlugin() {
return this.plugin;
}
protected abstract UUID getUniqueId(T sender);
protected abstract String getName(T sender);
public abstract Audience getAudience(T sender);
protected abstract void sendMessage(T sender, Component message);
protected abstract Tristate getPermissionValue(T sender, String node);
protected abstract boolean hasPermission(T sender, String node);
protected abstract void performCommand(T sender, String command);
protected abstract boolean isConsole(T sender);
protected boolean consoleHasAllPermissions() {
return true;
}
public final Sender wrap(T sender) {
Objects.requireNonNull(sender, "sender");
return new AbstractSender<>(this.plugin, this, sender);
}
@Override
public void close() {
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.team;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public enum TeamTagVisibility {
ALWAYS("always"),
HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"),
HIDE_FOR_OWN_TEAM("hideForOwnTeam"),
NEVER("never");
private final String id;
TeamTagVisibility(@NotNull String id) {
this.id = id;
}
public String getId() {
return id;
}
@NotNull
public static TeamTagVisibility byId(String id) {
return Arrays.stream(values())
.filter(mode -> mode.id.equals(id))
.findFirst()
.orElse(ALWAYS);
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Utility class for handling operations with arrays.
*/
public class ArrayUtils {
private ArrayUtils() {}
/**
* Creates a subarray from the specified array starting from the given index.
*
* @param array the original array
* @param index the starting index for the subarray
* @param <T> the type of the elements in the array
* @return the subarray starting from the given index
* @throws IllegalArgumentException if the index is less than 0
*/
public static <T> T[] subArray(T[] array, int index) {
if (index < 0) {
throw new IllegalArgumentException("Index should be a value no lower than 0");
}
if (array.length <= index) {
@SuppressWarnings("unchecked")
T[] emptyArray = (T[]) Array.newInstance(array.getClass().getComponentType(), 0);
return emptyArray;
}
@SuppressWarnings("unchecked")
T[] subArray = (T[]) Array.newInstance(array.getClass().getComponentType(), array.length - index);
System.arraycopy(array, index, subArray, 0, array.length - index);
return subArray;
}
/**
* Splits the specified array into a list of subarrays, each with the specified chunk size.
*
* @param array the original array
* @param chunkSize the size of each chunk
* @param <T> the type of the elements in the array
* @return a list of subarrays
*/
public static <T> List<T[]> splitArray(T[] array, int chunkSize) {
List<T[]> result = new ArrayList<>();
for (int i = 0; i < array.length; i += chunkSize) {
int end = Math.min(array.length, i + chunkSize);
@SuppressWarnings("unchecked")
T[] chunk = (T[]) Array.newInstance(array.getClass().getComponentType(), end - i);
System.arraycopy(array, i, chunk, 0, end - i);
result.add(chunk);
}
return result;
}
/**
* Appends an element to the specified array.
*
* @param array the original array
* @param element the element to append
* @param <T> the type of the elements in the array
* @return a new array with the appended element
*/
public static <T> T[] appendElementToArray(T[] array, T element) {
T[] newArray = Arrays.copyOf(array, array.length + 1);
newArray[array.length] = element;
return newArray;
}
/**
* Splits a string value into an array of substrings based on comma separation.
* The input string is expected to be in the format "[value1, value2, ...]".
*
* @param value the string value to split
* @return an array of substrings
*/
public static String[] splitValue(String value) {
return value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
.replaceAll("\\s", "")
.split(",");
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
/**
* Utility class for handling classes.
*/
public class ClassUtils {
private ClassUtils() {}
/**
* Attempts to find a class within a JAR file that extends or implements a given class or interface.
*
* @param file The JAR file in which to search for the class.
* @param clazz The base class or interface to match against.
* @param <T> The type of the base class or interface.
* @return A Class object representing the found class, or null if not found.
* @throws IOException If there is an issue reading the JAR file.
* @throws ClassNotFoundException If the specified class cannot be found.
*/
@Nullable
public static <T> Class<? extends T> findClass(
@NotNull File file,
@NotNull Class<T> clazz
) throws IOException, ClassNotFoundException {
if (!file.exists()) {
return null;
}
URL jar = file.toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{jar}, clazz.getClassLoader());
List<String> matches = new ArrayList<>();
List<Class<? extends T>> classes = new ArrayList<>();
try (JarInputStream stream = new JarInputStream(jar.openStream())) {
JarEntry entry;
while ((entry = stream.getNextJarEntry()) != null) {
final String name = entry.getName();
if (!name.endsWith(".class")) {
continue;
}
matches.add(name.substring(0, name.lastIndexOf('.')).replace('/', '.'));
}
for (String match : matches) {
try {
Class<?> loaded = loader.loadClass(match);
if (clazz.isAssignableFrom(loaded)) {
classes.add(loaded.asSubclass(clazz));
}
} catch (NoClassDefFoundError ignored) {
}
}
}
if (classes.isEmpty()) {
loader.close();
return null;
}
return classes.get(0);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
/**
* Interface representing a value that can be either of two types, primary or fallback.
* Provides methods to create instances of either type and to map between them.
*
* @param <U> the type of the primary value
* @param <V> the type of the fallback value
*/
public interface Either<U, V> {
/**
* Creates an {@link Either} instance with a primary value.
*
* @param value the primary value
* @param <U> the type of the primary value
* @param <V> the type of the fallback value
* @return an {@link Either} instance with the primary value
*/
static <U, V> @NotNull Either<U, V> ofPrimary(final @NotNull U value) {
return EitherImpl.of(requireNonNull(value, "value"), null);
}
/**
* Creates an {@link Either} instance with a fallback value.
*
* @param value the fallback value
* @param <U> the type of the primary value
* @param <V> the type of the fallback value
* @return an {@link Either} instance with the fallback value
*/
static <U, V> @NotNull Either<U, V> ofFallback(final @NotNull V value) {
return EitherImpl.of(null, requireNonNull(value, "value"));
}
/**
* Retrieves the primary value, if present.
*
* @return an {@link Optional} containing the primary value, or empty if not present
*/
@NotNull
Optional<U> primary();
/**
* Retrieves the fallback value, if present.
*
* @return an {@link Optional} containing the fallback value, or empty if not present
*/
@NotNull
Optional<V> fallback();
/**
* Retrieves the primary value, or maps the fallback value to the primary type if the primary is not present.
*
* @param mapFallback a function to map the fallback value to the primary type
* @return the primary value, or the mapped fallback value if the primary is not present
*/
default @Nullable U primaryOrMapFallback(final @NotNull Function<V, U> mapFallback) {
return this.primary().orElseGet(() -> mapFallback.apply(this.fallback().get()));
}
/**
* Retrieves the fallback value, or maps the primary value to the fallback type if the fallback is not present.
*
* @param mapPrimary a function to map the primary value to the fallback type
* @return the fallback value, or the mapped primary value if the fallback is not present
*/
default @Nullable V fallbackOrMapPrimary(final @NotNull Function<U, V> mapPrimary) {
return this.fallback().orElseGet(() -> mapPrimary.apply(this.primary().get()));
}
/**
* Maps either the primary or fallback value to a new type.
*
* @param mapPrimary a function to map the primary value
* @param mapFallback a function to map the fallback value
* @param <R> the type of the result
* @return the mapped result
*/
default @NotNull <R> R mapEither(
final @NotNull Function<U, R> mapPrimary,
final @NotNull Function<V, R> mapFallback
) {
return this.primary()
.map(mapPrimary)
.orElseGet(() -> this.fallback().map(mapFallback).get());
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.Optional;
final class EitherImpl<U, V> implements Either<U, V> {
private final @Nullable U primary;
private final @Nullable V fallback;
private EitherImpl(Optional<? extends U> primary, Optional<? extends V> fallback) {
this.primary = primary.orElse(null);
this.fallback = fallback.orElse(null);
}
private EitherImpl(@Nullable U primary, @Nullable V fallback) {
this.primary = primary;
this.fallback = fallback;
}
private EitherImpl(
EitherImpl<U, V> original,
@Nullable U primary,
@Nullable V fallback
) {
this.primary = primary;
this.fallback = fallback;
}
@Override
public @NotNull Optional<U> primary() {
return Optional.ofNullable(primary);
}
@Override
public @NotNull Optional<V> fallback() {
return Optional.ofNullable(fallback);
}
public final EitherImpl<U, V> withPrimary(@Nullable U value) {
@Nullable U newValue = value;
if (this.primary == newValue) return this;
return new EitherImpl<>(this, newValue, this.fallback);
}
public EitherImpl<U, V> withPrimary(Optional<? extends U> optional) {
@Nullable U value = optional.orElse(null);
if (this.primary == value) return this;
return new EitherImpl<>(this, value, this.fallback);
}
public EitherImpl<U, V> withFallback(@Nullable V value) {
@Nullable V newValue = value;
if (this.fallback == newValue) return this;
return new EitherImpl<>(this, this.primary, newValue);
}
public EitherImpl<U, V> withFallback(Optional<? extends V> optional) {
@Nullable V value = optional.orElse(null);
if (this.fallback == value) return this;
return new EitherImpl<>(this, this.primary, value);
}
@Override
public boolean equals(@Nullable Object another) {
if (this == another) return true;
return another instanceof EitherImpl<?, ?>
&& equalTo((EitherImpl<?, ?>) another);
}
private boolean equalTo(EitherImpl<?, ?> another) {
return Objects.equals(primary, another.primary)
&& Objects.equals(fallback, another.fallback);
}
@Override
public int hashCode() {
int h = 5381;
h += (h << 5) + Objects.hashCode(primary);
h += (h << 5) + Objects.hashCode(fallback);
return h;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Either{");
if (primary != null) {
builder.append("primary=").append(primary);
}
if (fallback != null) {
if (builder.length() > 7) builder.append(", ");
builder.append("fallback=").append(fallback);
}
return builder.append("}").toString();
}
public static <U, V> EitherImpl<U, V> of(Optional<? extends U> primary, Optional<? extends V> fallback) {
return new EitherImpl<>(primary, fallback);
}
public static <U, V> EitherImpl<U, V> of(@Nullable U primary, @Nullable V fallback) {
return new EitherImpl<>(primary, fallback);
}
public static <U, V> EitherImpl<U, V> copyOf(Either<U, V> instance) {
if (instance instanceof EitherImpl<?, ?>) {
return (EitherImpl<U, V>) instance;
}
return EitherImpl.of(instance.primary(), instance.fallback());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,19 +15,39 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common;
package net.momirealms.customnameplates.common.util;
/**
* Represents a key consisting of a namespace and a value.
* This class provides methods for creating and manipulating keys.
*/
public record Key(String namespace, String value) {
/**
* Creates a new {@link Key} instance with the specified namespace and value.
*
* @param namespace the namespace of the key
* @param value the value of the key
* @return a new {@link Key} instance
*/
public static Key of(String namespace, String value) {
return new Key(namespace, value);
}
/**
* Creates a new {@link Key} instance from a string in the format "namespace:value".
*
* @param key the string representation of the key
* @return a new {@link Key} instance
*/
public static Key fromString(String key) {
String[] split = key.split(":", 2);
return of(split[0], split[1]);
}
@Override
public int hashCode() {
int result = this.namespace.hashCode();
result = (31 * result) + this.value.hashCode();
return result;
return toString().hashCode();
}
@Override
@@ -43,4 +63,4 @@ public record Key(String namespace, String value) {
public String toString() {
return namespace + ":" + value;
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.List;
/**
* Utility class for handling operations related to lists.
*/
public class ListUtils {
private ListUtils() {
}
/**
* Converts an object to a list of strings.
* If the object is a string, it returns a list containing the string.
* If the object is a list, it casts and returns the list as a list of strings.
*
* @param obj the object to convert
* @return the resulting list of strings
* @throws IllegalArgumentException if the object cannot be converted to a list of strings
*/
@SuppressWarnings("unchecked")
public static List<String> toList(final Object obj) {
if (obj instanceof String s) {
return List.of(s);
} else if (obj instanceof List<?> list) {
return (List<String>) list;
}
throw new IllegalArgumentException("Cannot convert " + obj + " to a list");
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
public class OSUtils {
public static String getOSName() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
return "windows";
} else if (os.contains("mac")) {
return "macos";
} else if (os.contains("nux") || os.contains("nix")) {
return "linux";
} else if (os.contains("freebsd")) {
return "freebsd";
} else {
return "unknown";
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.Objects;
/**
* A generic class representing a pair of values.
* This class provides methods to create and access pairs of values.
*
* @param <L> the type of the left value
* @param <R> the type of the right value
*/
public record Pair<L, R>(L left, R right) {
/**
* Creates a new {@link Pair} with the specified left and right values.
*
* @param left the left value
* @param right the right value
* @param <L> the type of the left value
* @param <R> the type of the right value
* @return a new {@link Pair} with the specified values
*/
public static <L, R> Pair<L, R> of(final L left, final R right) {
return new Pair<>(left, right);
}
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Pair<?, ?> pair = (Pair<?, ?>) object;
return Objects.equals(left, pair.left) && Objects.equals(right, pair.right);
}
@Override
public int hashCode() {
return Objects.hash(this.left, this.right);
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
* Utility class for generating random values.
*/
public class RandomUtils {
private final Random random;
private RandomUtils() {
random = ThreadLocalRandom.current();
}
/**
* Static inner class to hold the singleton instance of RandomUtils.
*/
private static class SingletonHolder {
private static final RandomUtils INSTANCE = new RandomUtils();
}
/**
* Retrieves the singleton instance of RandomUtils.
*
* @return the singleton instance
*/
private static RandomUtils getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* Generates a random integer between the specified minimum and maximum values (inclusive).
*
* @param min the minimum value
* @param max the maximum value
* @return a random integer between min and max (inclusive)
*/
public static int generateRandomInt(int min, int max) {
return getInstance().random.nextInt(max - min + 1) + min;
}
/**
* Generates a random double between the specified minimum and maximum values (inclusive).
*
* @param min the minimum value
* @param max the maximum value
* @return a random double between min and max (inclusive)
*/
public static double generateRandomDouble(double min, double max) {
return min + (max - min) * getInstance().random.nextDouble();
}
/**
* Generates a random float between the specified minimum and maximum values (inclusive).
*
* @param min the minimum value
* @param max the maximum value
* @return a random float between min and max (inclusive)
*/
public static float generateRandomFloat(float min, float max) {
return min + (max - min) * getInstance().random.nextFloat();
}
/**
* Generates a random boolean value.
*
* @return a random boolean value
*/
public static boolean generateRandomBoolean() {
return getInstance().random.nextBoolean();
}
/**
* Selects a random element from the specified array.
*
* @param array the array to select a random element from
* @param <T> the type of the elements in the array
* @return a random element from the array
*/
public static <T> T getRandomElementFromArray(T[] array) {
int index = getInstance().random.nextInt(array.length);
return array[index];
}
/**
* Generates a random value based on a triangular distribution.
*
* @param mode the mode (peak) of the distribution
* @param deviation the deviation from the mode
* @return a random value based on a triangular distribution
*/
public static double triangle(double mode, double deviation) {
return mode + deviation * (generateRandomDouble(0,1) - generateRandomDouble(0,1));
}
/**
* Selects a specified number of random elements from the given array.
*
* @param array the array to select random elements from
* @param count the number of random elements to select
* @param <T> the type of the elements in the array
* @return an array containing the selected random elements
* @throws IllegalArgumentException if the count is greater than the array length
*/
public static <T> T[] getRandomElementsFromArray(T[] array, int count) {
if (count > array.length) {
throw new IllegalArgumentException("Count cannot be greater than array length");
}
@SuppressWarnings("unchecked")
T[] result = (T[]) new Object[count];
for (int i = 0; i < count; i++) {
int index = getInstance().random.nextInt(array.length - i);
result[i] = array[index];
array[index] = array[array.length - i - 1];
}
return result;
}
}

View File

@@ -0,0 +1,323 @@
package net.momirealms.customnameplates.common.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ReflectionUtils {
public static Class<?> getClazz(String... classes) {
for (String className : classes) {
Class<?> clazz = getClazz(className);
if (clazz != null) {
return clazz;
}
}
return null;
}
public static Class<?> getClazz(String clazz) {
try {
return Class.forName(clazz);
} catch (Throwable e) {
return null;
}
}
public static boolean classExists(@NotNull final String clazz) {
try {
Class.forName(clazz);
return true;
} catch (Throwable e) {
return false;
}
}
public static boolean methodExists(@NotNull final Class<?> clazz, @NotNull final String method, @NotNull final Class<?>... parameterTypes) {
try {
clazz.getMethod(method, parameterTypes);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
@Nullable
public static Field getDeclaredField(final Class<?> clazz, final String field) {
try {
return setAccessible(clazz.getDeclaredField(field));
} catch (NoSuchFieldException e) {
return null;
}
}
@NotNull
public static Field getDeclaredField(@NotNull Class<?> clazz, @NotNull String... possibleNames) {
List<String> possibleNameList = Arrays.asList(possibleNames);
for (Field field : clazz.getDeclaredFields()) {
if (possibleNameList.contains(field.getName())) {
return field;
}
}
throw new RuntimeException("Class " + clazz.getName() + " does not contain a field with possible names " + Arrays.toString(possibleNames));
}
@Nullable
public static Field getDeclaredField(final Class<?> clazz, final int index) {
int i = 0;
for (final Field field : clazz.getDeclaredFields()) {
if (index == i) {
return setAccessible(field);
}
i++;
}
return null;
}
@Nullable
public static Field getInstanceDeclaredField(final Class<?> clazz, final int index) {
int i = 0;
for (final Field field : clazz.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
if (index == i) {
return setAccessible(field);
}
i++;
}
}
return null;
}
@Nullable
public static Field getDeclaredField(final Class<?> clazz, final Class<?> type, int index) {
int i = 0;
for (final Field field : clazz.getDeclaredFields()) {
if (field.getType() == type) {
if (index == i) {
return setAccessible(field);
}
i++;
}
}
return null;
}
@Nullable
public static Field getInstanceDeclaredField(@NotNull Class<?> clazz, final Class<?> type, int index) {
int i = 0;
for (final Field field : clazz.getDeclaredFields()) {
if (field.getType() == type && !Modifier.isStatic(field.getModifiers())) {
if (index == i) {
return setAccessible(field);
}
i++;
}
}
return null;
}
@NotNull
public static List<Field> getDeclaredFields(final Class<?> clazz) {
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
fields.add(setAccessible(field));
}
return fields;
}
@NotNull
public static List<Field> getInstanceDeclaredFields(@NotNull Class<?> clazz) {
List<Field> list = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
list.add(setAccessible(field));
}
}
return list;
}
@NotNull
public static List<Field> getDeclaredFields(@NotNull final Class<?> clazz, @NotNull final Class<?> type) {
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.getType() == type) {
fields.add(setAccessible(field));
}
}
return fields;
}
@NotNull
public static List<Field> getInstanceDeclaredFields(@NotNull Class<?> clazz, @NotNull Class<?> type) {
List<Field> list = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.getType() == type && !Modifier.isStatic(field.getModifiers())) {
list.add(setAccessible(field));
}
}
return list;
}
@Nullable
public static Method getMethod(final Class<?> clazz, final String[] possibleMethodNames, final Class<?>... parameterTypes) {
outer:
for (Method method : clazz.getMethods()) {
if (method.getParameterCount() != parameterTypes.length) {
continue;
}
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
if (types[i] != parameterTypes[i]) {
continue outer;
}
}
for (String name : possibleMethodNames) {
if (name.equals(method.getName())) return method;
}
}
return null;
}
@Nullable
public static Method getMethod(final Class<?> clazz, Class<?> returnType, final Class<?>... parameterTypes) {
outer:
for (Method method : clazz.getMethods()) {
if (method.getParameterCount() != parameterTypes.length) {
continue;
}
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
if (types[i] != parameterTypes[i]) {
continue outer;
}
}
if (returnType.isAssignableFrom(method.getReturnType())) return method;
}
return null;
}
@Nullable
public static Method getStaticMethod(final Class<?> clazz, Class<?> returnType, final Class<?>... parameterTypes) {
outer:
for (Method method : clazz.getMethods()) {
if (method.getParameterCount() != parameterTypes.length) {
continue;
}
if (!Modifier.isStatic(method.getModifiers())) {
continue;
}
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
if (types[i] != parameterTypes[i]) {
continue outer;
}
}
if (returnType.isAssignableFrom(method.getReturnType())) return method;
}
return null;
}
public static Method getStaticMethod(final Class<?> clazz, int index) {
int i = 0;
for (Method method : clazz.getMethods()) {
if (Modifier.isStatic(method.getModifiers())) {
if (i == index) {
return setAccessible(method);
}
i++;
}
}
return null;
}
@Nullable
public static Method getMethod(final Class<?> clazz, int index) {
int i = 0;
for (Method method : clazz.getMethods()) {
if (i == index) {
return setAccessible(method);
}
i++;
}
return null;
}
public static Method getMethodOrElseThrow(final Class<?> clazz, final String[] possibleMethodNames, final Class<?>[] parameterTypes) throws NoSuchMethodException {
Method method = getMethod(clazz, possibleMethodNames, parameterTypes);
if (method == null) {
throw new NoSuchMethodException("No method found with possible names " + Arrays.toString(possibleMethodNames) + " with parameters " +
Arrays.toString(parameterTypes) + " in class " + clazz.getName());
}
return method;
}
@NotNull
public static List<Method> getMethods(@NotNull Class<?> clazz, @NotNull Class<?> returnType, @NotNull Class<?>... parameterTypes) {
List<Method> list = new ArrayList<>();
for (Method method : clazz.getMethods()) {
if (!returnType.isAssignableFrom(method.getReturnType()) // check type
|| method.getParameterCount() != parameterTypes.length // check length
) continue;
Class<?>[] types = method.getParameterTypes();
outer: {
for (int i = 0; i < types.length; i++) {
if (types[i] != parameterTypes[i]) {
break outer;
}
}
list.add(method);
}
}
return list;
}
@NotNull
public static <T extends AccessibleObject> T setAccessible(@NotNull final T o) {
o.setAccessible(true);
return o;
}
@Nullable
public static Constructor<?> getConstructor(Class<?> clazz, Class<?>... parameterTypes) {
try {
return clazz.getConstructor(parameterTypes);
} catch (NoSuchMethodException | SecurityException ignore) {
return null;
}
}
@Nullable
public static Constructor<?> getDeclaredConstructor(Class<?> clazz, Class<?>... parameterTypes) {
try {
return setAccessible(clazz.getDeclaredConstructor(parameterTypes));
} catch (NoSuchMethodException | SecurityException ignore) {
return null;
}
}
@Nullable
public static Constructor<?> getConstructor(Class<?> clazz, int index) {
try {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
if (index < 0 || index >= constructors.length) {
throw new IndexOutOfBoundsException("Invalid constructor index: " + index);
}
return constructors[index];
} catch (SecurityException e) {
return null;
}
}
@NotNull
public static Constructor<?> getTheOnlyConstructor(Class<?> clazz) {
Constructor<?>[] constructors = clazz.getConstructors();
if (constructors.length != 1) {
throw new RuntimeException("This class is expected to have only one constructor but it has " + constructors.length);
}
return constructors[0];
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class SerializationUtils {
private SerializationUtils() {
}
static class ClassLoaderAwareObjectInputStream extends ObjectInputStream {
private static final Map<String, Class<?>> PRIMITIVE_TYPES = new HashMap<>();
static {
PRIMITIVE_TYPES.put("byte", byte.class);
PRIMITIVE_TYPES.put("short", short.class);
PRIMITIVE_TYPES.put("int", int.class);
PRIMITIVE_TYPES.put("long", long.class);
PRIMITIVE_TYPES.put("float", float.class);
PRIMITIVE_TYPES.put("double", double.class);
PRIMITIVE_TYPES.put("boolean", boolean.class);
PRIMITIVE_TYPES.put("char", char.class);
PRIMITIVE_TYPES.put("void", void.class);
}
private final ClassLoader classLoader;
ClassLoaderAwareObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException {
super(in);
this.classLoader = classLoader;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
try {
return Class.forName(className, false, classLoader);
} catch (ClassNotFoundException e) {
Class<?> primitiveClass = PRIMITIVE_TYPES.get(className);
if (primitiveClass != null) {
return primitiveClass;
}
return super.resolveClass(desc);
}
}
}
public static <T extends Serializable> T clone(T object) {
if (object == null) {
return null;
}
byte[] objectData = serialize(object);
try (ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
ClassLoaderAwareObjectInputStream ois = new ClassLoaderAwareObjectInputStream(bais, object.getClass().getClassLoader())) {
@SuppressWarnings("unchecked")
T clonedObject = (T) ois.readObject();
return clonedObject;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Exception occurred while cloning object", e);
}
}
public static <T> T deserialize(byte[] objectData) {
Objects.requireNonNull(objectData);
return deserialize(new ByteArrayInputStream(objectData));
}
public static <T> T deserialize(InputStream inputStream) {
Objects.requireNonNull(inputStream);
try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
@SuppressWarnings("unchecked")
T obj = (T) ois.readObject();
return obj;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Exception occurred while deserializing object", e);
}
}
public static byte[] serialize(Serializable obj) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Exception occurred while serializing object", e);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,11 +15,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common;
package net.momirealms.customnameplates.common.util;
public record Pair<L, R>(L left, R right) {
public static <L, R> Pair<L, R> of(final L left, final R right) {
return new Pair<>(left, right);
}
public interface TriConsumer<K, V, S> {
void accept(K k, V v, S s);
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.Objects;
import java.util.function.Function;
public interface TriFunction<T, U, V, R> {
R apply(T var1, U var2, V var3);
default <W> TriFunction<T, U, V, W> andThen(Function<? super R, ? extends W> after) {
Objects.requireNonNull(after);
return (t, u, v) -> {
return after.apply(this.apply(t, u, v));
};
}
}

View File

@@ -0,0 +1,98 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.momirealms.customnameplates.common.util;
import org.jetbrains.annotations.NotNull;
/**
* Represents three different states of a setting.
*
* <p>Possible values:</p>
* <p></p>
* <ul>
* <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li>
* <li>{@link #UNDEFINED} - a non-existent setting</li>
* </ul>
*/
public enum Tristate {
/**
* A value indicating a positive setting
*/
TRUE(true),
/**
* A value indicating a negative (negated) setting
*/
FALSE(false),
/**
* A value indicating a non-existent setting
*/
UNDEFINED(false);
/**
* Returns a {@link Tristate} from a boolean
*
* @param val the boolean value
* @return {@link #TRUE} or {@link #FALSE}, if the value is <code>true</code> or <code>false</code>, respectively.
*/
public static @NotNull Tristate of(boolean val) {
return val ? TRUE : FALSE;
}
/**
* Returns a {@link Tristate} from a nullable boolean.
*
* <p>Unlike {@link #of(boolean)}, this method returns {@link #UNDEFINED}
* if the value is null.</p>
*
* @param val the boolean value
* @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value
* is <code>null</code>, <code>true</code> or <code>false</code>, respectively.
*/
public static @NotNull Tristate of(Boolean val) {
return val == null ? UNDEFINED : val ? TRUE : FALSE;
}
private final boolean booleanValue;
Tristate(boolean booleanValue) {
this.booleanValue = booleanValue;
}
/**
* Returns the value of the Tristate as a boolean.
*
* <p>A value of {@link #UNDEFINED} converts to false.</p>
*
* @return a boolean representation of the Tristate.
*/
public boolean asBoolean() {
return this.booleanValue;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.Objects;
/**
* A generic class representing a tuple with three values.
* This class provides methods for creating and accessing tuples with three values.
*
* @param <L> the type of the left value
* @param <M> the type of the middle value
* @param <R> the type of the right value
*/
public record Tuple<L, M, R>(L left, M mid, R right) {
/**
* Creates a new {@link Tuple} with the specified left, middle, and right values.
*
* @param left the left value
* @param mid the middle value
* @param right the right value
* @param <L> the type of the left value
* @param <M> the type of the middle value
* @param <R> the type of the right value
* @return a new {@link Tuple} with the specified values
*/
public static <L, M, R> Tuple<L, M, R> of(final L left, final M mid, final R right) {
return new Tuple<>(left, mid, right);
}
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Tuple<?, ?, ?> tuple = (Tuple<?, ?, ?>) object;
return Objects.equals(mid, tuple.mid) && Objects.equals(left, tuple.left) && Objects.equals(right, tuple.right);
}
@Override
public int hashCode() {
return Objects.hash(left, mid, right);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.math.BigInteger;
import java.util.UUID;
/**
* Utility class for handling operations related to UUIDs.
* Provides methods for converting between different UUID formats and representations.
*/
public class UUIDUtils {
/**
* Converts a UUID string without dashes to a {@link UUID} object.
*
* @param id the UUID string without dashes
* @return the corresponding {@link UUID} object, or null if the input string is null
*/
public static UUID fromUnDashedUUID(String id) {
return id == null ? null : new UUID(
new BigInteger(id.substring(0, 16), 16).longValue(),
new BigInteger(id.substring(16, 32), 16).longValue()
);
}
/**
* Converts a {@link UUID} object to a string without dashes.
*
* @param uuid the {@link UUID} object
* @return the UUID string without dashes
*/
public static String toUnDashedUUID(UUID uuid) {
return uuid.toString().replace("-", "");
}
/**
* Converts an integer array to a {@link UUID} object.
* The array must contain exactly four integers.
*
* @param array the integer array
* @return the corresponding {@link UUID} object
* @throws IllegalArgumentException if the array length is not four
*/
public static UUID uuidFromIntArray(int[] array) {
return new UUID((long)array[0] << 32 | (long)array[1] & 4294967295L, (long)array[2] << 32 | (long)array[3] & 4294967295L);
}
/**
* Converts a {@link UUID} object to an integer array.
* The resulting array contains exactly four integers.
*
* @param uuid the {@link UUID} object
* @return the integer array representation of the UUID
*/
public static int[] uuidToIntArray(UUID uuid) {
long l = uuid.getMostSignificantBits();
long m = uuid.getLeastSignificantBits();
return leastMostToIntArray(l, m);
}
/**
* Converts the most significant and least significant bits of a UUID to an integer array.
*
* @param uuidMost the most significant bits of the UUID
* @param uuidLeast the least significant bits of the UUID
* @return the integer array representation of the UUID bits
*/
private static int[] leastMostToIntArray(long uuidMost, long uuidLeast) {
return new int[]{(int)(uuidMost >> 32), (int)uuidMost, (int)(uuidLeast >> 32), (int)uuidLeast};
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customnameplates.common.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Utility class for selecting random items based on weights.
*/
@SuppressWarnings("DuplicatedCode")
public class WeightUtils {
private WeightUtils() {}
/**
* Get a random item from a list of pairs, each associated with a weight.
*
* @param pairs A list of pairs where the left element is the item and the right element is its weight.
* @param <T> The type of items in the list.
* @return A randomly selected item from the list, or null if no item was selected.
*/
public static <T> T getRandom(List<Pair<T, Double>> pairs) {
List<T> available = new ArrayList<>();
double[] weights = new double[pairs.size()];
int index = 0;
for (Pair<T, Double> pair : pairs){
double weight = pair.right();
T key = pair.left();
if (weight <= 0) continue;
available.add(key);
weights[index++] = weight;
}
return getRandom(weights, available, index);
}
/**
* Get a random item from a map where each entry is associated with a weight.
*
* @param map A map where each entry's key is an item, and the value is its weight.
* @param <T> The type of items in the map.
* @return A randomly selected item from the map, or null if no item was selected.
*/
public static <T> T getRandom(Map<T, Double> map) {
List<T> available = new ArrayList<>();
double[] weights = new double[map.size()];
int index = 0;
for (Map.Entry<T, Double> entry : map.entrySet()){
double weight = entry.getValue();
T key = entry.getKey();
if (weight <= 0) continue;
available.add(key);
weights[index++] = weight;
}
return getRandom(weights, available, index);
}
/**
* Get a random item from a list of items with associated weights.
*
* @param weights An array of weights corresponding to the available items.
* @param available A list of available items.
* @param effectiveSize The effective size of the array and list after filtering out items with non-positive weights.
* @param <T> The type of items.
* @return A randomly selected item from the list, or null if no item was selected.
*/
private static <T> T getRandom(double[] weights, List<T> available, int effectiveSize) {
if (available.isEmpty()) return null;
double total = Arrays.stream(weights).sum();
double[] weightRatios = new double[effectiveSize];
for (int i = 0; i < effectiveSize; i++){
weightRatios[i] = weights[i]/total;
}
double[] weightRange = new double[effectiveSize];
double startPos = 0;
for (int i = 0; i < effectiveSize; i++) {
weightRange[i] = startPos + weightRatios[i];
startPos += weightRatios[i];
}
double random = Math.random();
int pos = Arrays.binarySearch(weightRange, random);
if (pos < 0) {
pos = -pos - 1;
}
if (pos < weightRange.length && random < weightRange[pos]) {
return available.get(pos);
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
builder=${builder}
git=${git_version}
config=${config_version}
asm=${asm_version}
asm-commons=${asm_commons_version}
jar-relocator=${jar_relocator_version}
cloud-core=${cloud_core_version}
cloud-brigadier=${cloud_brigadier_version}
cloud-services=${cloud_services_version}
cloud-bukkit=${cloud_bukkit_version}
cloud-paper=${cloud_paper_version}
cloud-minecraft-extras=${cloud_minecraft_extras_version}
boosted-yaml=${boosted_yaml_version}
bstats-base=${bstats_version}
geantyref=${geantyref_version}
gson=${gson_version}
caffeine=${caffeine_version}
exp4j=${exp4j_version}
slf4j=${slf4j_version}
mongodb-driver-core=${mongodb_driver_version}
mariadb-java-client=${mariadb_driver_version}
mysql-connector-j=${mysql_driver_version}
hikari-cp=${hikari_version}
commons-pool=${commons_pool_version}
jedis=${jedis_version}
h2-driver=${h2_driver_version}
sqlite-driver=${sqlite_driver_version}
fontbox=${fontbox_version}
pdfbox-io=${pdfbox_io_version}
byte-buddy=${byte_buddy_version}
commons-io=${commons_io_version}

View File

@@ -0,0 +1,80 @@
# Don't change this
config-version: "36"
exception.invalid_syntax: "<red>Invalid syntax. Correct syntax: <white><arg:0></white></red>"
exception.invalid_argument: "<red>Invalid argument. Reason: <white><arg:0></white></red>"
exception.invalid_sender: "<red><arg:0> is not allowed to execute that command. Must be of type <arg:1></red>"
exception.unexpected: "<red>An internal error occurred while attempting to perform this command</red>"
exception.no_permission: "<red>I'm sorry, but you do not have permission to perform this command</red>"
exception.no_such_command: "Unknown command."
argument.entity.notfound.player: "<red><lang:argument.entity.notfound.player></red>"
argument.entity.notfound.entity: "<red><lang:argument.entity.notfound.entity></red>"
argument.parse.failure.time: "<red>'<arg:0>' is not a valid time format</red>"
argument.parse.failure.material: "<red>'<arg:0>' is not a valid material name</red>"
argument.parse.failure.enchantment: "<red>'<arg:0>' is not a valid enchantment</red>"
argument.parse.failure.offlineplayer: "<red>No player found for input '<arg:0>'</red>"
argument.parse.failure.player: "<red>No player found for input '<arg:0>'</red>"
argument.parse.failure.world: "<red>'<arg:0>' is not a valid Minecraft world</red>"
argument.parse.failure.location.invalid_format: "<red>'<arg:0>' is not a valid location. Required format is '<arg:1> <arg:2> <arg:3></red>"
argument.parse.failure.location.mixed_local_absolute: "<red>Cannot mix local and absolute coordinates. (either all coordinates use '^' or none do)</red>"
argument.parse.failure.namespacedkey.namespace: "<red>Invalid namespace '<arg:0>'. Must be [a-z0-9._-]</red>"
argument.parse.failure.namespacedkey.key: "<red>Invalid key '<arg:0>'. Must be [a-z0-9/._-]</red>"
argument.parse.failure.namespacedkey.need_namespace: "<red>Invalid input '<arg:0>', requires an explicit namespace</red>"
argument.parse.failure.boolean: "<red>Could not parse boolean from '<arg:0>'</red>"
argument.parse.failure.number: "<red>'<arg:0>' is not a valid number in the range <arg:1> to <arg:2></red>"
argument.parse.failure.char: "<red>'<arg:0>' is not a valid character</red>"
argument.parse.failure.string: "<red>'<arg:0>' is not a valid string of type <arg:1></red>"
argument.parse.failure.uuid: "<red>'<arg:0>' is not a valid UUID</red>"
argument.parse.failure.enum: "<red>'<arg:0>' is not one of the following: <arg:1></red>"
argument.parse.failure.regex: "<red>'<arg:0>' does not match '<arg:1>'</red>"
argument.parse.failure.flag.unknown: "<red>Unknown flag '<arg:0>'</red>"
argument.parse.failure.flag.duplicate_flag: "<red>Duplicate flag '<arg:0>'</red>"
argument.parse.failure.flag.no_flag_started: "<red>No flag started. Don't know what to do with '<arg:0>'</red>"
argument.parse.failure.flag.missing_argument: "<red>Missing argument for '<arg:0>'</red>"
argument.parse.failure.flag.no_permission: "<red>You don't have permission to use '<arg:0>'</red>"
argument.parse.failure.color: "<red>'<arg:0>' is not a valid color</red>"
argument.parse.failure.duration: "<red>'<arg:0>' is not a duration format</red>"
argument.parse.failure.aggregate.missing: "<red>Missing component '<arg:0>'</red>"
argument.parse.failure.aggregate.failure: "<red>Invalid component '<arg:0>': <arg:1></red>"
argument.parse.failure.either: "<red>Could not resolve <arg:1> or <arg:2> from '<arg:0>'</red>"
argument.parse.failure.namedtextcolor: "<red>'<arg:0>' is not a named text color</red>"
command.reload.success: "<white>Reloaded. Took <green><arg:0></green> ms.</white>"
command.reload.generating: "<white>Generating resource packs...</white>"
command.reload.generated: "<white>Generated. Took <green><arg:0></green> ms.</white>"
command.debug.performance:
- "<white>Average thread load in the past minute: <arg:0>%</white> <gray>(<arg:1>/50ms)</gray>"
- "<white>Check conditions<arg:2>ms</white>"
- "<white>Update placeholders<arg:3>ms</white>"
command.nameplates.equip.failure.not_exists: "<red><arg:0> doesn't exist</red>"
command.nameplates.equip.failure.permission: "<red>You don't have permission to use this nameplate</red>"
command.nameplates.equip.failure.no_change: "<red>Nothing changed as you have already equipped this nameplate</red>"
command.nameplates.equip.success: "<white>Successfully equipped <arg:0> </white><yellow><click:run_command:/nameplates preview>[click to preview]</click></yellow>" # use <arg:1> for display name
command.nameplates.unequip.failure.not_equip: "<red>You are not equipping any nameplate</red>"
command.nameplates.unequip.success: "<white>You removed your nameplate</white>"
command.nameplates.preview.failure.cooldown: "<red>Previewing is still Ongoing!</red>"
command.nameplates.preview.success: "<white>Now previewing your nameplate (Go to third person to view)!</white>"
command.nameplates.list.failure.none: "<white>You don't have any nameplate yet</white>"
command.nameplates.list.success: "<white>Available nameplates: <arg:0></white>" # use <arg:1> for display names
command.nameplates.list.delimiter: ", "
command.nameplates.force_equip.failure.not_exists: "<red>Failed to equip nameplate for <arg:0> because <arg:1> doesn't exist</red>"
command.nameplates.force_equip.success: "<white>Forced <arg:0> to equip <arg:1></white>" # use <arg:2> for display name
command.nameplates.force_unequip.success: "<white>Removed <arg:0>'s nameplate</white>"
command.nameplates.force_preview.success: "<white>Forced <arg:0> to preview nameplates</white>"
command.bubbles.equip.failure.not_exists: "<red><arg:0> doesn't exist</red>"
command.bubbles.equip.failure.permission: "<red>You don't have permission to use this bubble</red>"
command.bubbles.equip.failure.no_change: "<red>Nothing changed as you have already equipped this bubble</red>"
command.bubbles.equip.success: "<white>Successfully equipped <arg:0></white>" # use <arg:1> for display name
command.bubbles.unequip.failure.not_equip: "<red>You don't have any bubble enabled</red>"
command.bubbles.unequip.success: "<white>You removed your bubbles</white>"
command.bubbles.list.failure.none: "<white>You don't have any bubble yet</white>"
command.bubbles.list.success: "<white>Available bubbles: <arg:0></white>" # use <arg:1> for display names
command.bubbles.list.delimiter: ", "
command.bubbles.force_equip.failure.not_exists: "<red>Failed to apply bubbles to <arg:0> because <arg:1> doesn't exist</red>"
command.bubbles.force_equip.success: "<white>Forced to apply <arg:1> to <arg:0></white>" # use <arg:2> for display name
command.bubbles.force_unequip.success: "<white>Removed <arg:0>'s bubbles</white>"

View File

@@ -0,0 +1,74 @@
# 别动这个
config-version: "36"
exception.invalid_syntax: "<red>无效语法. 正确语法:<white><arg:0></white></red>"
exception.invalid_argument: "<red>无效参数. 原因:<white><arg:0></white></red>"
exception.invalid_sender: "<red><arg:0> 不允许执行该命令. 执行者必须是 <arg:1></red>"
exception.unexpected: "<red>执行该命令时发生内部错误</red>"
exception.no_permission: "<red>抱歉, 您没有权限执行该命令</red>"
exception.no_such_command: "未知命令"
argument.entity.notfound.player: "<red>找不到玩家 '<arg:0>'</red>"
argument.entity.notfound.entity: "<red>找不到实体 '<arg:0>'</red>"
argument.parse.failure.time: "<red>'<arg:0>' 不是有效的时间格式</red>"
argument.parse.failure.material: "<red>'<arg:0>' 不是有效的材料</red>"
argument.parse.failure.enchantment: "<red>'<arg:0>' 不是有效的魔咒</red>"
argument.parse.failure.offlineplayer: "<red>输入的玩家 '<arg:0>' 已离线</red>"
argument.parse.failure.player: "<red>找不到输入的玩家 '<arg:0>'</red>"
argument.parse.failure.world: "<red>'<arg:0>' 不是有效的 Minecraft 世界名称</red>"
argument.parse.failure.location.invalid_format: "<red>'<arg:0>' 不是有效的位置格式.必须格式为 '<arg:1> <arg:2> <arg:3>'</red>"
argument.parse.failure.location.mixed_local_absolute: "<red>不能混用相对和绝对坐标.坐标要么全部使用 '^',要么全部不用</red>"
argument.parse.failure.namespacedkey.namespace: "<red>无效的命名空间 '<arg:0>'.必须为 [a-z0-9._-]</red>"
argument.parse.failure.namespacedkey.key: "<red>无效的键 '<arg:0>'.必须为 [a-z0-9/._-]</red>"
argument.parse.failure.namespacedkey.need_namespace: "<red>无效的输入 '<arg:0>', 需要显式指定命名空间</red>"
argument.parse.failure.boolean: "<red>无法解析布尔值 '<arg:0>'</red>"
argument.parse.failure.number: "<red>'<arg:0>' 不是从 <arg:1> 到 <arg:2> 范围内的有效数字</red>"
argument.parse.failure.char: "<red>'<arg:0>' 不是有效的字符</red>"
argument.parse.failure.string: "<red>'<arg:0>' 不是类型为 <arg:1> 的有效字符串</red>"
argument.parse.failure.uuid: "<red>'<arg:0>' 不是有效的 UUID</red>"
argument.parse.failure.enum: "<red>'<arg:0>' 不是以下任何一种情况之一: <arg:1></red>"
argument.parse.failure.regex: "<red>'<arg:0>' 不匹配 '<arg:1>'</red>"
argument.parse.failure.flag.unknown: "<red>未知标志 '<arg:0>'</red>"
argument.parse.failure.flag.duplicate_flag: "<red>重复的标志 '<arg:0>'</red>"
argument.parse.failure.flag.no_flag_started: "<red>没有开始标志. 不知道如何处理 '<arg:0>'</red>"
argument.parse.failure.flag.missing_argument: "<red>缺少 '<arg:0>' 参数</red>"
argument.parse.failure.flag.no_permission: "<red>您没有权限使用 '<arg:0>'</red>"
argument.parse.failure.color: "<red>'<arg:0>' 不是有效的颜色</red>"
argument.parse.failure.duration: "<red>'<arg:0>' 不是有效的持续时间格式</red>"
argument.parse.failure.aggregate.missing: "<red>缺少组件 '<arg:0>'</red>"
argument.parse.failure.aggregate.failure: "<red>无效的组件 '<arg:0>': <arg:1></red>"
argument.parse.failure.either: "<red>无法从 '<arg:0>' 解析 <arg:1> 或 <arg:2></red>"
argument.parse.failure.namedtextcolor: "<red>'<arg:0>' 不是颜色代码</red>"
command.reload.success: "<white>重新加载完成. 耗时 <green><arg:0></green> 毫秒</white>"
command.reload.generating: "<white>生成资源包中...</white>"
command.reload.generated: "<white>生成完毕. 耗时 <green><arg:0></green> 毫秒</white>"
command.debug.performance:
- "<white>过去一分钟的线程负载: <arg:0>%</white> <gray>(<arg:1>/50ms)</gray>"
- "<white>刷新条件耗时:<arg:2>ms</white>"
- "<white>更新变量耗时:<arg:3>ms</white>"
command.nameplates.equip.failure.not_exists: "<red>铭牌 [<arg:0>] 不存在</red>"
command.nameplates.equip.failure.permission: "<red>你尚未解锁这个铭牌</red>"
command.nameplates.equip.failure.no_change: "<red>你已经佩戴这个铭牌了</red>"
command.nameplates.equip.success: "<white>你佩戴上了 <arg:0></white> <yellow><click:run_command:/nameplates preview>[点击预览]</click></yellow>"
command.nameplates.unequip.failure.not_equip: "<red>你当前未佩戴任何铭牌</red>"
command.nameplates.unequip.success: "<white>你移除了你的铭牌</white>"
command.nameplates.preview.failure.cooldown: "<red>上一轮预览尚未结束</red>"
command.nameplates.preview.success: "<white>你正在预览你的铭牌 (前往第三人称查看)!</white>"
command.nameplates.list.failure.none: "<white>你尚未拥有任何铭牌</white>"
command.nameplates.list.success: "<white>可用的铭牌: <arg:0></white>"
command.nameplates.list.delimiter: ", "
command.nameplates.force_equip.failure.not_exists: "<red>由于 <arg:1> 不存在,无法为 <arg:0> 佩戴铭牌</red>"
command.nameplates.force_equip.success: "<white>强制 <arg:0> 佩戴了铭牌 <arg:1></white>"
command.nameplates.force_unequip.success: "<white>移除了 <arg:0> 的铭牌</white>"
command.nameplates.force_preview.success: "<white>强制 <arg:0> 预览他的铭牌</white>"
command.bubbles.equip.failure.not_exists: "<red>气泡 <arg:0> 不存在</red>"
command.bubbles.equip.failure.permission: "<red>你尚未解锁这个气泡</red>"
command.bubbles.equip.failure.no_change: "<red>你已经应用这个气泡了</red>"
command.bubbles.equip.success: "<white>你正在使用气泡 <arg:0></white>"
command.bubbles.unequip.failure.not_equip: "<red>你尚未应用气泡</red>"
command.bubbles.unequip.success: "<white>你移除了你的气泡</white>"
command.bubbles.list.failure.none: "<white>你尚未拥有任何气泡</white>"
command.bubbles.list.success: "<white>可用的气泡: <arg:0></white>"
command.bubbles.list.delimiter: ", "
command.bubbles.force_equip.failure.not_exists: "<red>由于 <arg:0> 不存在,无法为 <arg:1> 应用气泡</red>"
command.bubbles.force_equip.success: "<white>强制 <arg:0> 应用了气泡 <arg:1></white>"
command.bubbles.force_unequip.success: "<white>移除了 <arg:0> 的气泡</white>"