9
0
mirror of https://github.com/Xiao-MoMi/Custom-Crops.git synced 2025-12-26 02:19:28 +00:00
This commit is contained in:
XiaoMoMi
2024-08-31 22:57:45 +08:00
parent 3cbd1f65a6
commit bbde4ebd47
475 changed files with 24733 additions and 24341 deletions

View File

@@ -0,0 +1,11 @@
package net.momirealms.customcrops.common.annotation;
import org.jetbrains.annotations.ApiStatus;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@ApiStatus.Internal
@Target({ElementType.FIELD})
public @interface DoNotUse {
}

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.customcrops.common.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.momirealms.customcrops.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 CustomCropsCommandManager<C> commandManager;
protected CommandConfig<C> commandConfig;
public AbstractCommandFeature(CustomCropsCommandManager<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 CustomCropsCommandManager<C> getCustomCropsCommandManager() {
return commandManager;
}
@Override
public CommandConfig<C> getCommandConfig() {
return commandConfig;
}
public void setCommandConfig(CommandConfig<C> commandConfig) {
this.commandConfig = commandConfig;
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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.customcrops.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.customcrops.common.locale.CustomCropsCaptionFormatter;
import net.momirealms.customcrops.common.locale.CustomCropsCaptionProvider;
import net.momirealms.customcrops.common.locale.TranslationManager;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.common.sender.Sender;
import net.momirealms.customcrops.common.util.ArrayUtils;
import net.momirealms.customcrops.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 CustomCropsCommandManager<C> {
protected final HashSet<CommandComponent<C>> registeredRootCommandComponents = new HashSet<>();
protected final HashSet<CommandFeature<C>> registeredFeatures = new HashSet<>();
protected final CommandManager<C> commandManager;
protected final CustomCropsPlugin plugin;
private final CustomCropsCaptionFormatter<C> captionFormatter = new CustomCropsCaptionFormatter<C>();
private final MinecraftExceptionHandler.Decorator<C> decorator = (formatter, ctx, msg) -> msg;
private TriConsumer<C, String, Component> feedbackConsumer;
public AbstractCommandManager(CustomCropsPlugin 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 CustomCropsCaptionProvider<>());
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);
}
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.customcrops.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.customcrops.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.customcrops.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);
CustomCropsCommandManager<C> getCustomCropsCommandManager();
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.customcrops.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.customcrops.common.util.TriConsumer;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
public interface CustomCropsCommandManager<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,69 @@
/*
* 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.customcrops.common.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);
/**
* Loads a YAML data file with a custom route separator.
*
* @param file the {@link File} object representing the data file
* @param routeSeparator the custom route separator character
* @return the loaded {@link YamlDocument}
*/
YamlDocument loadData(File file, char routeSeparator);
/**
* 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

@@ -0,0 +1,268 @@
/*
* 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.customcrops.common.dependency;
import net.momirealms.customcrops.common.dependency.relocation.Relocation;
import net.momirealms.customcrops.common.plugin.CustomCropsProperties;
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 CustomCrops.
*/
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"
),
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")
),
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")
),
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"
),
ZSTD(
"com.github.luben",
"zstd-jni",
"maven",
"zstd-jni"
);
private final List<Relocation> relocations;
private final String repo;
private final String groupId;
private String rawArtifactId;
private String customArtifactID;
private static final String MAVEN_FORMAT = "%s/%s/%s/%s-%s.jar";
Dependency(String groupId, String rawArtifactId, String repo, String customArtifactID) {
this(groupId, rawArtifactId, repo, customArtifactID, new Relocation[0]);
}
Dependency(String groupId, String rawArtifactId, String repo, String customArtifactID, Relocation... relocations) {
this.rawArtifactId = rawArtifactId;
this.groupId = groupId;
this.relocations = new ArrayList<>(Arrays.stream(relocations).toList());
this.repo = repo;
this.customArtifactID = customArtifactID;
}
public Dependency setCustomArtifactID(String customArtifactID) {
this.customArtifactID = customArtifactID;
return this;
}
public Dependency setRawArtifactID(String artifactId) {
this.rawArtifactId = artifactId;
return this;
}
public String getVersion() {
return CustomCropsProperties.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(rawArtifactId),
getVersion(),
rewriteEscaping(rawArtifactId),
getVersion()
);
}
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.customcrops.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.customcrops.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,227 @@
/*
* 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.customcrops.common.dependency;
import net.momirealms.customcrops.common.dependency.classloader.IsolatedClassLoader;
import net.momirealms.customcrops.common.dependency.relocation.Relocation;
import net.momirealms.customcrops.common.dependency.relocation.RelocationHandler;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.common.plugin.classpath.ClassPathAppender;
import net.momirealms.customcrops.common.util.FileUtils;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
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 CustomCropsPlugin plugin;
public DependencyManagerImpl(CustomCropsPlugin 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(CustomCropsPlugin plugin) {
Path cacheDirectory = plugin.getDataDirectory().resolve("libs");
try {
FileUtils.createDirectoriesIfNotExists(cacheDirectory);
} 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.customcrops.common.dependency;
import com.google.gson.JsonElement;
/**
* Applies CustomCrops 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, ZSTD -> 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.customcrops.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 (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.customcrops.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 CustomCrops 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.customcrops.common.dependency.relocation;
import java.util.Objects;
public final class Relocation {
private static final String RELOCATION_PREFIX = "net.momirealms.customcrops.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,90 @@
/*
* 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.customcrops.common.dependency.relocation;
import net.momirealms.customcrops.common.dependency.Dependency;
import net.momirealms.customcrops.common.dependency.DependencyManager;
import net.momirealms.customcrops.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.customcrops.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,285 @@
/*
* 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.customcrops.common.helper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.title.Title;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* Helper class for handling Adventure components and related functionalities.
*/
public class AdventureHelper {
private final MiniMessage miniMessage;
private final MiniMessage miniMessageStrict;
private final GsonComponentSerializer gsonComponentSerializer;
private final Cache<String, String> miniMessageToJsonCache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
public static boolean legacySupport = false;
private AdventureHelper() {
this.miniMessage = MiniMessage.builder().build();
this.miniMessageStrict = MiniMessage.builder().strict(true).build();
this.gsonComponentSerializer = GsonComponentSerializer.builder().build();
}
private static class SingletonHolder {
private static final AdventureHelper INSTANCE = new AdventureHelper();
}
/**
* Retrieves the singleton instance of AdventureHelper.
*
* @return the singleton instance
*/
public static AdventureHelper getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* Converts a MiniMessage string to a Component.
*
* @param text the MiniMessage string
* @return the resulting Component
*/
public static Component miniMessage(String text) {
if (legacySupport) {
return getMiniMessage().deserialize(legacyToMiniMessage(text));
} else {
return getMiniMessage().deserialize(text);
}
}
/**
* Retrieves the MiniMessage instance.
*
* @return the MiniMessage instance
*/
public static MiniMessage getMiniMessage() {
return getInstance().miniMessage;
}
/**
* Retrieves the GsonComponentSerializer instance.
*
* @return the GsonComponentSerializer instance
*/
public static GsonComponentSerializer getGson() {
return getInstance().gsonComponentSerializer;
}
/**
* Converts a MiniMessage string to a JSON string.
*
* @param miniMessage the MiniMessage string
* @return the JSON string representation
*/
public static String miniMessageToJson(String miniMessage) {
AdventureHelper instance = getInstance();
return instance.miniMessageToJsonCache.get(miniMessage, (text) -> instance.gsonComponentSerializer.serialize(miniMessage(text)));
}
/**
* Sends a title to an audience.
*
* @param audience the audience to send the title to
* @param title the title component
* @param subtitle the subtitle component
* @param fadeIn the fade-in duration in ticks
* @param stay the stay duration in ticks
* @param fadeOut the fade-out duration in ticks
*/
public static void sendTitle(Audience audience, Component title, Component subtitle, int fadeIn, int stay, int fadeOut) {
audience.showTitle(Title.title(title, subtitle, Title.Times.times(Duration.ofMillis(fadeIn * 50L), Duration.ofMillis(stay * 50L), Duration.ofMillis(fadeOut * 50L))));
}
/**
* Sends an action bar message to an audience.
*
* @param audience the audience to send the action bar message to
* @param actionBar the action bar component
*/
public static void sendActionBar(Audience audience, Component actionBar) {
audience.sendActionBar(actionBar);
}
/**
* Sends a message to an audience.
*
* @param audience the audience to send the message to
* @param message the message component
*/
public static void sendMessage(Audience audience, Component message) {
audience.sendMessage(message);
}
/**
* Plays a sound for an audience.
*
* @param audience the audience to play the sound for
* @param sound the sound to play
*/
public static void playSound(Audience audience, Sound sound) {
audience.playSound(sound);
}
/**
* Surrounds text with a MiniMessage font tag.
*
* @param text the text to surround
* @param font the font as a {@link Key}
* @return the text surrounded by the MiniMessage font tag
*/
public static String surroundWithMiniMessageFont(String text, Key font) {
return "<font:" + font.asString() + ">" + text + "</font>";
}
/**
* Surrounds text with a MiniMessage font tag.
*
* @param text the text to surround
* @param font the font as a {@link String}
* @return the text surrounded by the MiniMessage font tag
*/
public static String surroundWithMiniMessageFont(String text, String font) {
return "<font:" + font + ">" + text + "</font>";
}
/**
* Converts a JSON string to a MiniMessage string.
*
* @param json the JSON string
* @return the MiniMessage string representation
*/
public static String jsonToMiniMessage(String json) {
return getInstance().miniMessageStrict.serialize(getInstance().gsonComponentSerializer.deserialize(json));
}
/**
* Converts a JSON string to a Component.
*
* @param json the JSON string
* @return the resulting Component
*/
public static Component jsonToComponent(String json) {
return getInstance().gsonComponentSerializer.deserialize(json);
}
/**
* Converts a Component to a JSON string.
*
* @param component the Component to convert
* @return the JSON string representation
*/
public static String componentToJson(Component component) {
return getGson().serialize(component);
}
/**
* Checks if a character is a legacy color code.
*
* @param c the character to check
* @return true if the character is a color code, false otherwise
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isLegacyColorCode(char c) {
return c == '§' || c == '&';
}
/**
* Converts a legacy color code string to a MiniMessage string.
*
* @param legacy the legacy color code string
* @return the MiniMessage string representation
*/
public static String legacyToMiniMessage(String legacy) {
StringBuilder stringBuilder = new StringBuilder();
char[] chars = legacy.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (!isLegacyColorCode(chars[i])) {
stringBuilder.append(chars[i]);
continue;
}
if (i + 1 >= chars.length) {
stringBuilder.append(chars[i]);
continue;
}
switch (chars[i+1]) {
case '0' -> stringBuilder.append("<black>");
case '1' -> stringBuilder.append("<dark_blue>");
case '2' -> stringBuilder.append("<dark_green>");
case '3' -> stringBuilder.append("<dark_aqua>");
case '4' -> stringBuilder.append("<dark_red>");
case '5' -> stringBuilder.append("<dark_purple>");
case '6' -> stringBuilder.append("<gold>");
case '7' -> stringBuilder.append("<gray>");
case '8' -> stringBuilder.append("<dark_gray>");
case '9' -> stringBuilder.append("<blue>");
case 'a' -> stringBuilder.append("<green>");
case 'b' -> stringBuilder.append("<aqua>");
case 'c' -> stringBuilder.append("<red>");
case 'd' -> stringBuilder.append("<light_purple>");
case 'e' -> stringBuilder.append("<yellow>");
case 'f' -> stringBuilder.append("<white>");
case 'r' -> stringBuilder.append("<r><!i>");
case 'l' -> stringBuilder.append("<b>");
case 'm' -> stringBuilder.append("<st>");
case 'o' -> stringBuilder.append("<i>");
case 'n' -> stringBuilder.append("<u>");
case 'k' -> stringBuilder.append("<obf>");
case 'x' -> {
if (i + 13 >= chars.length
|| !isLegacyColorCode(chars[i+2])
|| !isLegacyColorCode(chars[i+4])
|| !isLegacyColorCode(chars[i+6])
|| !isLegacyColorCode(chars[i+8])
|| !isLegacyColorCode(chars[i+10])
|| !isLegacyColorCode(chars[i+12])) {
stringBuilder.append(chars[i]);
continue;
}
stringBuilder
.append("<#")
.append(chars[i+3])
.append(chars[i+5])
.append(chars[i+7])
.append(chars[i+9])
.append(chars[i+11])
.append(chars[i+13])
.append(">");
i += 12;
}
default -> {
stringBuilder.append(chars[i]);
continue;
}
}
i++;
}
return stringBuilder.toString();
}
}

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.customcrops.common.helper;
import net.objecthunter.exp4j.ExpressionBuilder;
/**
* Helper class for evaluating mathematical expressions.
*/
public class ExpressionHelper {
/**
* Evaluates a mathematical expression provided as a string.
*
* @param expression the mathematical expression to evaluate
* @return the result of the evaluation as a double
*/
public static double evaluate(String expression) {
return new ExpressionBuilder(expression).build().evaluate();
}
}

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.customcrops.common.helper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Helper class for managing Gson instances.
*/
public class GsonHelper {
private final Gson gson;
public GsonHelper() {
this.gson = new GsonBuilder()
.create();
}
/**
* Retrieves the Gson instance.
*
* @return the Gson instance
*/
public Gson getGson() {
return gson;
}
/**
* Retrieves the singleton Gson instance from GsonHelper.
*
* @return the singleton Gson instance
*/
public static Gson get() {
return SingletonHolder.INSTANCE.getGson();
}
/**
* Static inner class for holding the singleton instance of GsonHelper.
*/
private static class SingletonHolder {
private static final GsonHelper INSTANCE = new GsonHelper();
}
}

View File

@@ -0,0 +1,166 @@
/*
* 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.customcrops.common.helper;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
/**
* This class implements the VersionManager interface and is responsible for managing version-related information.
*/
public class VersionHelper {
// Method to asynchronously check for plugin updates
public static final Function<CustomCropsPlugin, CompletableFuture<Boolean>> UPDATE_CHECKER = (plugin) -> {
CompletableFuture<Boolean> updateFuture = new CompletableFuture<>();
plugin.getScheduler().async().execute(() -> {
try {
URL url = new URL("https://api.polymart.org/v1/getResourceInfoSimple/?resource_id=2625&key=version");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(60000);
InputStream inputStream = conn.getInputStream();
String newest = new BufferedReader(new InputStreamReader(inputStream)).readLine();
String current = plugin.getPluginVersion();
inputStream.close();
if (!compareVer(newest, current)) {
updateFuture.complete(false);
return;
}
updateFuture.complete(true);
} catch (Exception exception) {
plugin.getPluginLogger().warn("Error occurred when checking update.", exception);
updateFuture.complete(false);
}
});
return updateFuture;
};
private static float version;
private static boolean mojmap;
private static boolean folia;
public static void init(String serverVersion) {
String[] split = serverVersion.split("\\.");
version = Float.parseFloat(split[1] + "." + (split.length == 3 ? split[2] : "0"));
checkMojMap();
checkFolia();
}
private static void checkMojMap() {
// Check if the server is Mojmap
try {
Class.forName("net.minecraft.network.protocol.game.ClientboundBossEventPacket");
mojmap = true;
} catch (ClassNotFoundException ignored) {
}
}
private static void checkFolia() {
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
folia = true;
} catch (ClassNotFoundException ignored) {
}
}
public static boolean isVersionNewerThan1_18() {
return version >= 18;
}
public static boolean isVersionNewerThan1_19() {
return version >= 19;
}
public static boolean isVersionNewerThan1_19_4() {
return version >= 19.4;
}
public static boolean isVersionNewerThan1_19_3() {
return version >= 19.3;
}
public static boolean isVersionNewerThan1_20() {
return version >= 20.0;
}
public static boolean isVersionNewerThan1_20_5() {
return version >= 20.5;
}
public static boolean isFolia() {
return folia;
}
public static boolean isMojmap() {
return mojmap;
}
// Method to compare two version strings
private static boolean compareVer(String newV, String currentV) {
if (newV == null || currentV == null || newV.isEmpty() || currentV.isEmpty()) {
return false;
}
String[] newVS = newV.split("\\.");
String[] currentVS = currentV.split("\\.");
int maxL = Math.min(newVS.length, currentVS.length);
for (int i = 0; i < maxL; i++) {
try {
String[] newPart = newVS[i].split("-");
String[] currentPart = currentVS[i].split("-");
int newNum = Integer.parseInt(newPart[0]);
int currentNum = Integer.parseInt(currentPart[0]);
if (newNum > currentNum) {
return true;
} else if (newNum < currentNum) {
return false;
} else if (newPart.length > 1 && currentPart.length > 1) {
String[] newHotfix = newPart[1].split("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)");
String[] currentHotfix = currentPart[1].split("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)");
if (newHotfix.length == 2 && currentHotfix.length == 1) return true;
else if (newHotfix.length > 1 && currentHotfix.length > 1) {
int newHotfixNum = Integer.parseInt(newHotfix[1]);
int currentHotfixNum = Integer.parseInt(currentHotfix[1]);
if (newHotfixNum > currentHotfixNum) {
return true;
} else if (newHotfixNum < currentHotfixNum) {
return false;
} else {
return newHotfix[0].compareTo(currentHotfix[0]) > 0;
}
}
} else if (newPart.length > 1) {
return true;
} else if (currentPart.length > 1) {
return false;
}
}
catch (NumberFormatException ignored) {
return false;
}
}
return newVS.length > currentVS.length;
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.customcrops.common.item;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import java.util.List;
import java.util.Optional;
public class AbstractItem<R, I> implements Item<I> {
private final CustomCropsPlugin plugin;
private final ItemFactory<?, R, I> factory;
private final R item;
AbstractItem(CustomCropsPlugin plugin, ItemFactory<?, R, I> factory, R item) {
this.plugin = plugin;
this.factory = factory;
this.item = item;
}
@Override
public Item<I> damage(Integer data) {
factory.damage(item, data);
return this;
}
@Override
public Optional<Integer> damage() {
return factory.damage(item);
}
@Override
public Item<I> maxDamage(Integer data) {
factory.maxDamage(item, data);
return this;
}
@Override
public Optional<Integer> maxDamage() {
return factory.maxDamage(item);
}
@Override
public Item<I> customModelData(Integer data) {
factory.customModelData(item, data);
return this;
}
@Override
public Optional<Integer> customModelData() {
return factory.customModelData(item);
}
@Override
public Item<I> lore(List<String> lore) {
factory.lore(item, lore);
return this;
}
@Override
public Optional<List<String>> lore() {
return factory.lore(item);
}
@Override
public boolean unbreakable() {
return factory.unbreakable(item);
}
@Override
public Optional<Object> getTag(Object... path) {
return factory.getTag(item, path);
}
@Override
public Item<I> setTag(Object value, Object... path) {
factory.setTag(item, value, path);
return this;
}
@Override
public boolean hasTag(Object... path) {
return factory.hasTag(item, path);
}
@Override
public boolean removeTag(Object... path) {
return factory.removeTag(item, path);
}
@Override
public I getItem() {
return factory.getItem(item);
}
@Override
public I load() {
return factory.load(item);
}
@Override
public I loadCopy() {
return factory.loadCopy(item);
}
@Override
public void update() {
factory.update(item);
}
public R getRTagItem() {
return item;
}
}

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.customcrops.common.item;
import net.kyori.adventure.key.Key;
public class ComponentKeys {
public static final String CUSTOM_MODEL_DATA = Key.key("minecraft", "custom_model_data").asString();
public static final String CUSTOM_NAME = Key.key("minecraft", "custom_name").asString();
public static final String LORE = Key.key("minecraft", "lore").asString();
public static final String DAMAGE = Key.key("minecraft", "damage").asString();
public static final String MAX_DAMAGE = Key.key("minecraft", "max_damage").asString();
public static final String ENCHANTMENT_GLINT_OVERRIDE = Key.key("minecraft", "enchantment_glint_override").asString();
public static final String ENCHANTMENTS = Key.key("minecraft", "enchantments").asString();
public static final String STORED_ENCHANTMENTS = Key.key("minecraft", "stored_enchantments").asString();
}

View File

@@ -0,0 +1,157 @@
/*
* 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.customcrops.common.item;
import java.util.List;
import java.util.Optional;
/**
* Interface representing an item.
* This interface provides methods for managing item properties such as custom model data,
* damage, display name, lore, enchantments, and tags.
*
* @param <I> the type of the item implementation
*/
public interface Item<I> {
/**
* Sets the custom model data for the item.
*
* @param data the custom model data to set
* @return the current {@link Item} instance for method chaining
*/
Item<I> customModelData(Integer data);
/**
* Retrieves the custom model data of the item.
*
* @return an {@link Optional} containing the custom model data, or empty if not set
*/
Optional<Integer> customModelData();
/**
* Sets the damage value for the item.
*
* @param data the damage value to set
* @return the current {@link Item} instance for method chaining
*/
Item<I> damage(Integer data);
/**
* Retrieves the damage value of the item.
*
* @return an {@link Optional} containing the damage value, or empty if not set
*/
Optional<Integer> damage();
/**
* Sets the maximum damage value for the item.
*
* @param data the maximum damage value to set
* @return the current {@link Item} instance for method chaining
*/
Item<I> maxDamage(Integer data);
/**
* Retrieves the maximum damage value of the item.
*
* @return an {@link Optional} containing the maximum damage value, or empty if not set
*/
Optional<Integer> maxDamage();
/**
* Sets the lore for the item.
*
* @param lore the lore to set
* @return the current {@link Item} instance for method chaining
*/
Item<I> lore(List<String> lore);
/**
* Retrieves the lore of the item.
*
* @return an {@link Optional} containing the lore, or empty if not set
*/
Optional<List<String>> lore();
/**
* Checks if the item is unbreakable.
*
* @return true if the item is unbreakable, false otherwise
*/
boolean unbreakable();
/**
* Retrieves the tag value at the specified path.
*
* @param path the path to the tag value
* @return an {@link Optional} containing the tag value, or empty if not found
*/
Optional<Object> getTag(Object... path);
/**
* Sets the tag value at the specified path.
*
* @param value the value to set
* @param path the path to the tag value
* @return the current {@link Item} instance for method chaining
*/
Item<I> setTag(Object value, Object... path);
/**
* Checks if the item has a tag value at the specified path.
*
* @param path the path to the tag value
* @return true if the tag value exists, false otherwise
*/
boolean hasTag(Object... path);
/**
* Removes the tag value at the specified path.
*
* @param path the path to the tag value
* @return true if the tag was removed, false otherwise
*/
boolean removeTag(Object... path);
/**
* Retrieves the underlying item implementation.
*
* @return the item implementation of type {@link I}
*/
I getItem();
/**
* Loads changes to the item.
*
* @return the loaded item implementation of type {@link I}
*/
I load();
/**
* Loads the changes and gets a copy of the item.
*
* @return a copy of the loaded item implementation of type {@link I}
*/
I loadCopy();
/**
* Loads the {@link I}'s changes to the {@link Item} instance.
*/
void update();
}

View File

@@ -0,0 +1,72 @@
/*
* 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.customcrops.common.item;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public abstract class ItemFactory<P extends CustomCropsPlugin, R, I> {
protected final P plugin;
protected ItemFactory(P plugin) {
this.plugin = plugin;
}
public Item<I> wrap(R item) {
Objects.requireNonNull(item, "item");
return new AbstractItem<>(this.plugin, this, item);
}
protected abstract Optional<Object> getTag(R item, Object... path);
protected abstract void setTag(R item, Object value, Object... path);
protected abstract boolean hasTag(R item, Object... path);
protected abstract boolean removeTag(R item, Object... path);
protected abstract void update(R item);
protected abstract I load(R item);
protected abstract I getItem(R item);
protected abstract I loadCopy(R item);
protected abstract void customModelData(R item, Integer data);
protected abstract Optional<Integer> customModelData(R item);
protected abstract Optional<List<String>> lore(R item);
protected abstract void lore(R item, List<String> lore);
protected abstract Optional<Integer> damage(R item);
protected abstract void damage(R item, Integer damage);
protected abstract Optional<Integer> maxDamage(R item);
protected abstract void maxDamage(R item, Integer damage);
protected abstract boolean unbreakable(R item);
}

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.customcrops.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 CustomCropsCaptionFormatter<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.customcrops.common.locale;
import org.incendo.cloud.caption.Caption;
public final class CustomCropsCaptionKeys {
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.customcrops.common.locale;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.caption.CaptionProvider;
import org.incendo.cloud.caption.DelegatingCaptionProvider;
public final class CustomCropsCaptionProvider<C> extends DelegatingCaptionProvider<C> {
private static final CaptionProvider<?> PROVIDER = CaptionProvider.constantProvider()
.putCaption(CustomCropsCaptionKeys.ARGUMENT_PARSE_FAILURE_URL, "")
.putCaption(CustomCropsCaptionKeys.ARGUMENT_PARSE_FAILURE_TIME, "")
.putCaption(CustomCropsCaptionKeys.ARGUMENT_PARSE_FAILURE_NAMEDTEXTCOLOR, "")
.build();
@SuppressWarnings("unchecked")
@Override
public @NonNull CaptionProvider<C> delegate() {
return (CaptionProvider<C>) PROVIDER;
}
}

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.customcrops.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 SEASON_SPRING = Component.translatable().key("season.spring");
TranslatableComponent.Builder SEASON_SUMMER = Component.translatable().key("season.summer");
TranslatableComponent.Builder SEASON_AUTUMN = Component.translatable().key("season.autumn");
TranslatableComponent.Builder SEASON_WINTER = Component.translatable().key("season.winter");
TranslatableComponent.Builder SEASON_DISABLE = Component.translatable().key("season.disable");
}

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.customcrops.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.customcrops.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.customcrops.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.customcrops.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("customcrops", "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.customcrops.common.locale;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.translation.Translator;
import net.momirealms.customcrops.common.helper.AdventureHelper;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.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 {
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
private static final List<String> locales = List.of("en", "zh_cn");
private static TranslationManager instance;
private static Locale FORCE_LOCALE = null;
private final CustomCropsPlugin plugin;
private final Set<Locale> installed = ConcurrentHashMap.newKeySet();
private MiniMessageTranslationRegistry registry;
private final Path translationsDirectory;
public TranslationManager(CustomCropsPlugin 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("customcrops", "main"), AdventureHelper.getMiniMessage());
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)) {
plugin.getPluginLogger().warn(localLocale.toString().toLowerCase(Locale.ENGLISH) + ".yml not exists, using en.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" + "\\" + translationFile.getFileName(), '@');
try {
document.save(new File(plugin.getDataDirectory().toFile(), "translations" + "\\" + 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,138 @@
/*
* 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.customcrops.common.plugin;
import net.momirealms.customcrops.common.config.ConfigLoader;
import net.momirealms.customcrops.common.dependency.DependencyManager;
import net.momirealms.customcrops.common.locale.TranslationManager;
import net.momirealms.customcrops.common.plugin.classpath.ClassPathAppender;
import net.momirealms.customcrops.common.plugin.logging.PluginLogger;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerAdapter;
import java.io.InputStream;
import java.nio.file.Path;
/**
* Interface representing the main CustomCrops plugin.
*/
public interface CustomCropsPlugin {
/**
* 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();
}

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.customcrops.common.plugin;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class CustomCropsProperties {
private final HashMap<String, String> propertyMap;
private CustomCropsProperties(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 CustomCropsProperties INSTANCE = getInstance();
private static CustomCropsProperties getInstance() {
try (InputStream inputStream = CustomCropsProperties.class.getClassLoader().getResourceAsStream("custom-crops.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 CustomCropsProperties(versionMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

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.customcrops.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.customcrops.common.plugin.classpath;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
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(CustomCropsPlugin 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.customcrops.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("CustomCrops 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,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.customcrops.common.plugin.feature;
public interface Reloadable {
default void reload() {
unload();
load();
}
default void unload() {
}
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.customcrops.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.customcrops.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.customcrops.common.plugin.logging;
/**
* Represents the logger instance being used by CustomCrops on the platform.
*
* <p>Messages sent using the logger are sent prefixed with the CustomCrops 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.customcrops.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,151 @@
/*
* 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.customcrops.common.plugin.scheduler;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
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, W> implements SchedulerAdapter<T, W> {
private static final int PARALLELISM = 16;
private final CustomCropsPlugin plugin;
private final ScheduledThreadPoolExecutor scheduler;
private final ForkJoinPool worker;
public AbstractJavaScheduler(CustomCropsPlugin plugin) {
this.plugin = plugin;
this.scheduler = new ScheduledThreadPoolExecutor(4, r -> {
Thread thread = Executors.defaultThreadFactory().newThread(r);
thread.setName("customcrops-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) {
ScheduledFuture<?> future = this.scheduler.schedule(() -> this.worker.execute(task), delay, unit);
return new JavaCancellable(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 JavaCancellable(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 CustomCrops scheduler to terminate");
reportRunningTasks(thread -> thread.getName().equals("customcrops-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 CustomCrops worker thread pool to terminate");
reportRunningTasks(thread -> thread.getName().startsWith("customcrops-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("customcrops-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);
}
}
public static class JavaCancellable implements SchedulerTask {
private final ScheduledFuture<?> future;
public JavaCancellable(ScheduledFuture<?> future) {
this.future = future;
}
@Override
public void cancel() {
this.future.cancel(false);
}
@Override
public boolean isCancelled() {
return future.isCancelled();
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.customcrops.common.plugin.scheduler;
public interface RegionExecutor<T, W> {
void run(Runnable r, T l);
default void run(Runnable r) {
run(r, null);
}
void run(Runnable r, W world, int x, int z);
SchedulerTask runLater(Runnable r, long delayTicks, T l);
SchedulerTask runRepeating(Runnable r, long delayTicks, long period, T l);
}

View File

@@ -0,0 +1,111 @@
/*
* 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.customcrops.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, W> {
/**
* Gets an async executor instance
*
* @return an async executor instance
*/
Executor async();
/**
* Gets a sync executor instance
*
* @return a sync executor instance
*/
RegionExecutor<T, W> 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);
}
/**
* Executes a task sync
*
* @param task the task
*/
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.customcrops.common.plugin.scheduler;
/**
* Represents a scheduled task
*/
public interface SchedulerTask {
/**
* Cancels the task.
*/
void cancel();
boolean isCancelled();
}

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.customcrops.common.sender;
import net.kyori.adventure.text.Component;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.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 CustomCropsPlugin plugin;
private final SenderFactory<?, T> factory;
private final T sender;
private final UUID uniqueId;
private final String name;
private final boolean isConsole;
AbstractSender(CustomCropsPlugin 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 CustomCropsPlugin 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,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.customcrops.common.sender;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import java.util.UUID;
public abstract class DummyConsoleSender implements Sender {
private final CustomCropsPlugin platform;
public DummyConsoleSender(CustomCropsPlugin plugin) {
this.platform = plugin;
}
@Override
public void performCommand(String commandLine) {
}
@Override
public boolean isConsole() {
return true;
}
@Override
public CustomCropsPlugin 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.customcrops.common.sender;
import net.kyori.adventure.text.Component;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.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
*/
CustomCropsPlugin 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.customcrops.common.sender;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.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 CustomCropsPlugin, 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

@@ -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.customcrops.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,88 @@
/*
* 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.customcrops.common.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
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() {}
@Nullable
public static <T, C> Class<? extends T> findClass(
@NotNull File file,
@NotNull Class<T> clazz,
@NotNull Class<C> type
) throws IOException, ClassNotFoundException {
if (!file.exists()) {
return null;
}
URL jarUrl = file.toURI().toURL();
List<Class<? extends T>> classes = new ArrayList<>();
try (URLClassLoader loader = new URLClassLoader(new URL[]{jarUrl}, clazz.getClassLoader());
JarInputStream jarStream = new JarInputStream(jarUrl.openStream())) {
JarEntry entry;
while ((entry = jarStream.getNextJarEntry()) != null) {
String name = entry.getName();
if (!name.endsWith(".class")) {
continue;
}
String className = name.substring(0, name.lastIndexOf('.')).replace('/', '.');
try {
Class<?> loadedClass = loader.loadClass(className);
if (clazz.isAssignableFrom(loadedClass)) {
Type superclassType = loadedClass.getGenericSuperclass();
if (superclassType instanceof ParameterizedType parameterizedType) {
Type[] typeArguments = parameterizedType.getActualTypeArguments();
if (typeArguments.length > 0 && typeArguments[0].equals(type)) {
classes.add(loadedClass.asSubclass(clazz));
}
}
}
} catch (ClassNotFoundException | NoClassDefFoundError ignored) {
}
}
}
if (classes.isEmpty()) {
return null;
}
return classes.get(0);
}
}

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.customcrops.common.util;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collector;
import java.util.stream.Stream;
/**
* Utility class for handling operations with {@link CompletableFuture}.
*/
public class CompletableFutures {
private CompletableFutures() {}
/**
* A collector for collecting a stream of CompletableFuture instances into a single CompletableFuture that completes
* when all of the input CompletableFutures complete.
*
* @param <T> The type of CompletableFuture.
* @return A collector for CompletableFuture instances.
*/
public static <T extends CompletableFuture<?>> Collector<T, ImmutableList.Builder<T>, CompletableFuture<Void>> collector() {
return Collector.of(
ImmutableList.Builder::new,
ImmutableList.Builder::add,
(l, r) -> l.addAll(r.build()),
builder -> allOf(builder.build())
);
}
/**
* Combines multiple CompletableFuture instances into a single CompletableFuture that completes when all of the input
* CompletableFutures complete.
*
* @param futures A stream of CompletableFuture instances.
* @return A CompletableFuture that completes when all input CompletableFutures complete.
*/
public static CompletableFuture<Void> allOf(Stream<? extends CompletableFuture<?>> futures) {
CompletableFuture<?>[] arr = futures.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(arr);
}
/**
* Combines multiple CompletableFuture instances into a single CompletableFuture that completes when all of the input
* CompletableFutures complete.
*
* @param futures A collection of CompletableFuture instances.
* @return A CompletableFuture that completes when all input CompletableFutures complete.
*/
public static CompletableFuture<Void> allOf(Collection<? extends CompletableFuture<?>> futures) {
CompletableFuture<?>[] arr = futures.toArray(new CompletableFuture[0]);
return CompletableFuture.allOf(arr);
}
}

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.customcrops.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.customcrops.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

@@ -0,0 +1,112 @@
/*
* 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.customcrops.common.util;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Utility class for handling file and directory operations.
*/
public class FileUtils {
private FileUtils() {}
/**
* Creates a file if it does not already exist.
*
* @param path the path to the file
* @return the path to the file
* @throws IOException if an I/O error occurs
*/
public static Path createFileIfNotExists(Path path) throws IOException {
if (!Files.exists(path)) {
Files.createFile(path);
}
return path;
}
/**
* Creates a directory if it does not already exist.
*
* @param path the path to the directory
* @return the path to the directory
* @throws IOException if an I/O error occurs
*/
public static Path createDirectoryIfNotExists(Path path) throws IOException {
if (Files.exists(path) && (Files.isDirectory(path) || Files.isSymbolicLink(path))) {
return path;
}
try {
Files.createDirectory(path);
} catch (FileAlreadyExistsException e) {
// ignore
}
return path;
}
/**
* Creates directories if they do not already exist.
*
* @param path the path to the directories
* @return the path to the directories
* @throws IOException if an I/O error occurs
*/
public static Path createDirectoriesIfNotExists(Path path) throws IOException {
if (Files.exists(path) && (Files.isDirectory(path) || Files.isSymbolicLink(path))) {
return path;
}
try {
Files.createDirectories(path);
} catch (FileAlreadyExistsException e) {
// ignore
}
return path;
}
/**
* Deletes a directory and all its contents.
*
* @param path the path to the directory
* @throws IOException if an I/O error occurs
*/
public static void deleteDirectory(Path path) throws IOException {
if (!Files.exists(path) || !Files.isDirectory(path)) {
return;
}
try (DirectoryStream<Path> contents = Files.newDirectoryStream(path)) {
for (Path file : contents) {
if (Files.isDirectory(file)) {
deleteDirectory(file);
} else {
Files.delete(file);
}
}
}
Files.delete(path);
}
}

View File

@@ -0,0 +1,56 @@
package net.momirealms.customcrops.common.util;
public class Key {
private final String namespace;
private final String value;
public Key(String namespace, String value) {
this.namespace = namespace;
this.value = value;
}
public static Key key(String namespace, String value) {
return new Key(namespace, value);
}
public static Key key(String key) {
int index = key.indexOf(":");
String namespace = index >= 1 ? key.substring(0, index) : "minecraft";
String value = index >= 0 ? key.substring(index + 1) : key;
return key(namespace, value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
return namespace.equals(key.namespace) && value.equals(key.value);
}
@Override
public int hashCode() {
return asString().hashCode();
}
public String asString() {
return namespace + ":" + value;
}
public String namespace() {
return namespace;
}
public String value() {
return value;
}
@Override
public String toString() {
return "Key{" +
"namespace='" + namespace + '\'' +
", value='" + value + '\'' +
'}';
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.customcrops.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;
} else {
return List.of();
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.customcrops.common.util;
/**
* 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 class Pair<L, R> {
private L left;
private R right;
/**
* Constructs a new {@link Pair} with the specified left and right values.
*
* @param left the left value
* @param right the right value
*/
public Pair(L left, R right) {
this.left = left;
this.right = right;
}
/**
* Returns the left value.
*
* @return the left value
*/
public L left() {
return left;
}
/**
* Sets the left value.
*
* @param left the new left value
*/
public void left(L left) {
this.left = left;
}
/**
* Returns the right value.
*
* @return the right value
*/
public R right() {
return right;
}
/**
* Sets the right value.
*
* @param right the new right value
*/
public void right(R right) {
this.right = 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);
}
}

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.customcrops.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,22 @@
/*
* 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.customcrops.common.util;
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.customcrops.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.customcrops.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,114 @@
/*
* 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.customcrops.common.util;
/**
* 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 class Tuple<L, M, R> {
private L left;
private M mid;
private R right;
/**
* Constructs 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
*/
public Tuple(L left, M mid, R right) {
this.left = left;
this.mid = mid;
this.right = right;
}
/**
* Returns the left value.
*
* @return the left value
*/
public L left() {
return left;
}
/**
* Sets the left value.
*
* @param left the new left value
*/
public void left(L left) {
this.left = left;
}
/**
* Returns the middle value.
*
* @return the middle value
*/
public M mid() {
return mid;
}
/**
* Sets the middle value.
*
* @param mid the new middle value
*/
public void mid(M mid) {
this.mid = mid;
}
/**
* Returns the right value.
*
* @return the right value
*/
public R right() {
return right;
}
/**
* Sets the right value.
*
* @param right the new right value
*/
public void right(R right) {
this.right = 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);
}
}

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.customcrops.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.customcrops.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,20 @@
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}
zstd-jni=${zstd_version}