diff --git a/.run/Minecraft Client.run.xml b/.run/Minecraft Client.run.xml new file mode 100644 index 00000000..46b7a191 --- /dev/null +++ b/.run/Minecraft Client.run.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.run/Minecraft Server.run.xml b/.run/Minecraft Server.run.xml new file mode 100644 index 00000000..73ac751e --- /dev/null +++ b/.run/Minecraft Server.run.xml @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/.run/joinpoints server.run.xml b/.run/joinpoints server.run.xml new file mode 100644 index 00000000..7f805263 --- /dev/null +++ b/.run/joinpoints server.run.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index a924c397..083c5fd3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,9 @@ plugins { build { dependsOn ':ec-core:build' + // Don't build joinpoints by default since it requires EC jars to exist first + // Build joinpoints separately with: ./gradlew :joinpoints:build + // Or for testing: ./gradlew :joinpoints:runServer (which auto-builds EC jars) } ext.env = loadenv() @@ -49,8 +52,11 @@ allprojects { testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}" subprojects.each { - implementation project(path: ":${it.name}", configuration: "namedElements") - include project("${it.name}:") + // Skip joinpoints - it's a separate mod that depends on EC, not a library to include in EC + if (it.name != 'joinpoints') { + implementation project(path: ":${it.name}", configuration: "namedElements") + include project("${it.name}:") + } } } @@ -79,7 +85,7 @@ allprojects { repositories { gradlePluginPortal() mavenCentral() -// mavenLocal() + mavenLocal() maven { name 'Fabric'; url 'https://maven.fabricmc.net/' } // maven { name 'TerraformersMC'; url 'https://maven.terraformersmc.com/' } // Add repositories to retrieve artifacts from in here. @@ -123,17 +129,16 @@ dependencies { include "org.yaml:snakeyaml:${project.snakeyaml_version}" modImplementation "io.github.ladysnake:PlayerAbilityLib:${pal_version}" include "io.github.ladysnake:PlayerAbilityLib:${pal_version}" - - // SQLite for joinpoints storage - implementation "org.xerial:sqlite-jdbc:3.47.1.0" - include "org.xerial:sqlite-jdbc:3.47.1.0" // mod compatibility modCompileOnly "maven.modrinth:vanish:${project.vanish_version}" subprojects.each { - implementation project(path: ":${it.name}", configuration: "namedElements") - include project("${it.name}:") + // Skip joinpoints - it's a separate mod that depends on EC, not a library to include in EC + if (it.name != 'joinpoints') { + implementation project(path: ":${it.name}", configuration: "namedElements") + include project("${it.name}:") + } } } diff --git a/joinpoints/build.gradle b/joinpoints/build.gradle new file mode 100644 index 00000000..d17a8fde --- /dev/null +++ b/joinpoints/build.gradle @@ -0,0 +1,88 @@ +plugins { + id 'maven-publish' +} + +// Ensure EC projects are evaluated and built before joinpoints configuration +// This is needed because modLocalRuntime needs the jars to exist +evaluationDependsOnChildren() +project.evaluationDependsOn(':') +project.evaluationDependsOn(':ec-core') + +base.archivesName = "joinpoints" + +dependencies { + // Essential Commands dependency (joinpoints hooks into EC) + // Reference the compiled classes directories directly + compileOnly files(rootProject.sourceSets.main.output.classesDirs) + compileOnly files(project(':ec-core').sourceSets.main.output.classesDirs) + + // Make EC available as a mod at runtime for testing + // Only add if jars exist (handles clean builds where jars don't exist yet) + def ecJar = rootProject.file("build/libs/essential_commands-${rootProject.version}.jar") + def ecCoreJar = project(':ec-core').file("build/libs/ec-core-${project(':ec-core').version}.jar") + if (ecJar.exists() && ecCoreJar.exists()) { + modLocalRuntime(project(':')) { transitive = false } + modLocalRuntime(project(':ec-core')) { transitive = false } + } + + // EC's transitive dependencies for runtime + modRuntimeOnly "eu.pb4:placeholder-api:${project.placeholder_api_version}" + modRuntimeOnly "org.yaml:snakeyaml:${project.snakeyaml_version}" + modRuntimeOnly "io.github.ladysnake:PlayerAbilityLib:${project.pal_version}" + + // Fabric API - needed for command registration + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // Permissions API - used by joinpoints + modImplementation "me.lucko:fabric-permissions-api:${project.permissions_api_version}" + + // SQLite for joinpoints storage + implementation "org.xerial:sqlite-jdbc:3.47.1.0" + include "org.xerial:sqlite-jdbc:3.47.1.0" + + testImplementation platform("org.junit:junit-bom:${project.junit_bom_version}") + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}" +} + +// Ensure EC is built before joinpoints compiles +tasks.named('compileJava').configure { + dependsOn(rootProject.tasks.named('classes')) + dependsOn(project(':ec-core').tasks.named('classes')) +} + +// Task to clean Loom cache for EC jars when EC code changes +// Run this manually if EC changes aren't being picked up: ./gradlew :joinpoints:cleanECCache +tasks.register('cleanECCache') { + group = 'fabric' + description = 'Clears cached EC jars from Loom to pick up latest changes' + doLast { + // Only delete EC-related cached jars, not all remapped mods + delete rootProject.fileTree('.gradle/loom-cache/remapped_mods') { + include '**/essential_commands-*/**' + include '**/ec-core-*/**' + } + logger.lifecycle('Cleared EC jars from Loom cache') + } +} + +// Ensure EC jars are rebuilt before running the server +tasks.matching { it.name.startsWith('run') }.configureEach { + dependsOn(rootProject.tasks.named('remapJar')) + dependsOn(project(':ec-core').tasks.named('remapJar')) +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId 'joinpoints' + groupId project.maven_group + version project.version + artifact(remapJar) { builtBy remapJar } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } +} diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/Joinpoints.java b/joinpoints/src/main/java/com/fibermc/joinpoints/Joinpoints.java new file mode 100644 index 00000000..26d08731 --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/Joinpoints.java @@ -0,0 +1,103 @@ +package com.fibermc.joinpoints; + +import java.nio.file.Path; + +import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; +import com.fibermc.essentialcommands.events.NicknameChangeCallback; +import com.fibermc.essentialcommands.events.PlayerConnectCallback; +import com.fibermc.essentialcommands.text.ECText; +import com.fibermc.joinpoints.config.JoinpointsConfig; +import com.fibermc.joinpoints.database.JoinpointDatabase; +import com.google.gson.JsonElement; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.mojang.serialization.JsonOps; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.TextCodecs; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; + +public class Joinpoints implements ModInitializer { + public static final String MOD_ID = "joinpoints"; + public static final Logger LOGGER = LogManager.getLogger(MOD_ID); + + private static final JoinpointsConfig config = new JoinpointsConfig( + Path.of("./config/joinpoints.properties"), + "Joinpoints Configuration", + "https://github.com/John-Paul-R/Essential-Commands" // TODO: Update with joinpoints docs link + ); + private static JoinpointDatabase database; + private static MinecraftServer server; + + @Override + public void onInitialize() { + LOGGER.info("Initializing Joinpoints mod"); + + // Initialize config early + config.loadOrCreateProperties(); + + // Register joinpoints lang file with ECText + ECText.registerAdditionalLangPath("/assets/joinpoints/lang/%s.json"); + + // Register commands + JoinpointsCommandRegistry.register(); + + // Register player connect callback to update player cache + PlayerConnectCallback.EVENT.register((connection, player) -> updatePlayerCache(player)); + + // Register nickname change callback to update player cache + NicknameChangeCallback.EVENT.register(Joinpoints::updatePlayerCache); + + ServerLifecycleEvents.SERVER_STARTING.register(this::onServerStarting); + ServerLifecycleEvents.SERVER_STOPPED.register(this::onServerStopped); + } + + private void onServerStarting(MinecraftServer server) { + Joinpoints.server = server; + + // Initialize database + database = new JoinpointDatabase(server.getSavePath(net.minecraft.util.WorldSavePath.ROOT).toFile()); + + LOGGER.info("Joinpoints mod initialized successfully"); + } + + private void onServerStopped(MinecraftServer server) { + Joinpoints.server = null; + } + + public static JoinpointsConfig getConfig() { + return config; + } + + public static JoinpointDatabase getDatabase() { + return database; + } + + public static MinecraftServer getServer() { + return server; + } + + private static void updatePlayerCache(ServerPlayerEntity player) { + try { + var playerData = ((ServerPlayerEntityAccess) player).ec$getPlayerData(); + + String nicknameJson = playerData.getNickname() + .map(text -> TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, text).getOrThrow()) + .map(JsonElement::toString) + .orElse(null); + database + .updatePlayerCacheAsync(player.getUuid(), player.getName().getString(), nicknameJson) + .exceptionally(err -> { + LOGGER.error(err); + return null; + }); + } catch (Exception e) { + // Log but don't crash on cache update failure - joinpoint database might not be initialized yet + LOGGER.error(e); + } + } +} diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsCommandRegistry.java b/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsCommandRegistry.java new file mode 100644 index 00000000..635b501a --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsCommandRegistry.java @@ -0,0 +1,95 @@ +package com.fibermc.joinpoints; + +import com.fibermc.joinpoints.commands.*; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +import static net.minecraft.server.command.CommandManager.argument; + +public final class JoinpointsCommandRegistry { + private JoinpointsCommandRegistry() {} + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + if (!Joinpoints.getConfig().ENABLE_JOINPOINT.getValue()) { + return; + } + + var joinpointBuilder = CommandManager.literal("joinpoint"); + var joinpointSetBuilder = CommandManager.literal("set"); + var joinpointTpBuilder = CommandManager.literal("tp"); + var joinpointDeleteBuilder = CommandManager.literal("delete"); + var joinpointOverwriteBuilder = CommandManager.literal("overwrite"); + var joinpointShareBuilder = CommandManager.literal("share"); + var joinpointListBuilder = CommandManager.literal("list"); + + joinpointSetBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_set, 0)) + .then(argument("joinpoint_name", StringArgumentType.word()) + .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.SET)) + .then(argument("global", BoolArgumentType.bool()) + .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.SET)))); + + joinpointTpBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_tp, 0)) + .then(argument(JoinpointTpCommand.OWNER_PLAYER_ARG, StringArgumentType.word()) + .suggests(JoinpointTpCommand.Suggestion.OWNERS_OF_ACCESSIBLE_JOINPOINTS) + .then(argument("joinpoint_name", StringArgumentType.word()) + .suggests(JoinpointTpCommand.Suggestion.ACCESSIBLE_TARGET_PLAYER_JOINPOINTS) + .executes(new JoinpointTpCommand()))); + + joinpointDeleteBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_delete, 0)) + .then(argument("joinpoint_name", StringArgumentType.word()) + .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) + .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.DELETE))); + + joinpointOverwriteBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_set, 0)) + .then(argument("joinpoint_name", StringArgumentType.word()) + .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) + .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.OVERWRITE)) + .then(argument("global", BoolArgumentType.bool()) + .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.OVERWRITE)))); + + joinpointShareBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_set, 0)) + .then(argument("joinpoint_name", StringArgumentType.word()) + .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) + .then(CommandManager.literal("add") + .then(argument("target_players", EntityArgumentType.players()) + .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.ADD)))) + .then(CommandManager.literal("remove") + .then(argument("target_players", EntityArgumentType.players()) + .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.REMOVE)))) + .then(CommandManager.literal("list") + .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.LIST))) + .then(CommandManager.literal("clear") + .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.CLEAR)))); + + joinpointListBuilder + .requires(JoinpointsPerms.require(JoinpointsPerms.Registry.joinpoint_tp, 0)) + .executes(new JoinpointListCommand()::runDefault) + .then(argument("filter", StringArgumentType.word()) + .suggests(JoinpointListCommand.Suggestion.FILTER_TYPES) + .executes(new JoinpointListCommand())); + + LiteralCommandNode joinpointNode = joinpointBuilder + .requires(JoinpointsPerms.requireAny(JoinpointsPerms.Registry.Group.joinpoint_group, 0)) + .build(); + joinpointNode.addChild(joinpointSetBuilder.build()); + joinpointNode.addChild(joinpointTpBuilder.build()); + joinpointNode.addChild(joinpointDeleteBuilder.build()); + joinpointNode.addChild(joinpointOverwriteBuilder.build()); + joinpointNode.addChild(joinpointShareBuilder.build()); + joinpointNode.addChild(joinpointListBuilder.build()); + + dispatcher.getRoot().addChild(joinpointNode); + }); + } +} diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsPerms.java b/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsPerms.java new file mode 100644 index 00000000..9c730a25 --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/JoinpointsPerms.java @@ -0,0 +1,58 @@ +package com.fibermc.joinpoints; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.fibermc.joinpoints.types.JoinpointLimit; +import me.lucko.fabric.api.permissions.v0.Permissions; +import org.jetbrains.annotations.NotNull; + +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +public final class JoinpointsPerms { + private JoinpointsPerms() {} + + @SuppressWarnings({"checkstyle:constantname", "checkstyle:staticvariablename"}) + public static final class Registry { + public static final String joinpoint_set = "joinpoints.set"; + public static final String joinpoint_tp = "joinpoints.tp"; + public static final String joinpoint_delete = "joinpoints.delete"; + + public static final class Group { + public static final String[] joinpoint_group = {joinpoint_set, joinpoint_tp, joinpoint_delete}; + public static Map joinpoint_limit_groups = new HashMap<>(); + } + } + + public static Predicate require(String permission, int opLevel) { + return Permissions.require(permission, opLevel); + } + + public static Predicate requireAny(String[] permissions, int opLevel) { + return src -> Arrays.stream(permissions).anyMatch(perm -> Permissions.check(src, perm, opLevel)); + } + + public static String[] makeNumericPermissionGroup(String basePermission, @NotNull Collection limits) { + return limits.stream() + .map(limit -> basePermission + "." + limit) + .toArray(String[]::new); + } + + public static int getMaximumNumericalPermission(ServerPlayerEntity player, String basePermission, String[] validPermissions) { + return Stream.of(validPermissions) + .filter(perm -> Permissions.check(player, basePermission + "." + perm)) + .map(Integer::parseInt) + .max(Integer::compare) + .orElse(0); + } + + // Delegate to ECPerms for the shared permission logic + public static int getHighestNumericPermission(@NotNull ServerCommandSource source, @NotNull String[] permissionGroup) { + return com.fibermc.essentialcommands.ECPerms.getHighestNumericPermission(source, permissionGroup); + } +} diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/codec/JoinpointCodecs.java b/joinpoints/src/main/java/com/fibermc/joinpoints/codec/JoinpointCodecs.java new file mode 100644 index 00000000..87e8a8bb --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/codec/JoinpointCodecs.java @@ -0,0 +1,42 @@ +package com.fibermc.joinpoints.codec; + +import java.util.HashSet; +import java.util.Optional; +import java.util.UUID; + +import com.fibermc.essentialcommands.types.MinecraftLocation; +import com.fibermc.joinpoints.types.JoinpointLocation; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.world.World; + +public final class JoinpointCodecs { + private JoinpointCodecs() {} + + public static final Codec> WORLD_KEY = RegistryKey.createCodec(RegistryKeys.WORLD); + + public static final Codec JOINPOINT_LOCATION = RecordCodecBuilder.create(instance -> + instance.group( + // Inherit all fields from NamedMinecraftLocation + WORLD_KEY.fieldOf("WorldRegistryKey").forGetter(JoinpointLocation::dim), + Codec.DOUBLE.fieldOf("x").forGetter(MinecraftLocation::x), + Codec.DOUBLE.fieldOf("y").forGetter(MinecraftLocation::y), + Codec.DOUBLE.fieldOf("z").forGetter(MinecraftLocation::z), + Codec.FLOAT.optionalFieldOf("headYaw", 0.0f).forGetter(JoinpointLocation::headYaw), + Codec.FLOAT.optionalFieldOf("pitch", 0.0f).forGetter(JoinpointLocation::pitch), + // loaded from the map + Codec.STRING.optionalFieldOf("name").forGetter(joinpoint -> Optional.of(((JoinpointLocation)joinpoint).getName())), + + // Joinpoint-specific fields + Codec.STRING.xmap(UUID::fromString, UUID::toString).fieldOf("owner").forGetter(JoinpointLocation::getOwner), + Codec.BOOL.optionalFieldOf("isGlobal", false).forGetter(JoinpointLocation::isGlobal), + Codec.STRING.xmap(UUID::fromString, UUID::toString).listOf().optionalFieldOf("sharedWith", java.util.List.of()) + .xmap(HashSet::new, java.util.List::copyOf) + .forGetter(JoinpointLocation::getSharedWith) + + ).apply(instance, JoinpointLocation::new) + ); +} diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/Async.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/Async.java similarity index 97% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/Async.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/Async.java index a30cd638..254fd1ae 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/Async.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/Async.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointException.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointException.java similarity index 98% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointException.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointException.java index 375b4bee..f9cb5d8e 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointException.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointException.java @@ -1,8 +1,8 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import com.fibermc.essentialcommands.text.ECText; import com.fibermc.essentialcommands.text.TextFormatType; -import com.fibermc.essentialcommands.types.JoinpointLimit; +import com.fibermc.joinpoints.types.JoinpointLimit; import net.minecraft.text.Text; diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointListCommand.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointListCommand.java similarity index 92% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointListCommand.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointListCommand.java index d13af50d..7f65a032 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointListCommand.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointListCommand.java @@ -1,15 +1,15 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; -import com.fibermc.essentialcommands.ManagerLocator; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; -import com.fibermc.essentialcommands.database.JoinpointDatabase; import com.fibermc.essentialcommands.playerdata.PlayerData; import com.fibermc.essentialcommands.text.ECText; -import com.fibermc.essentialcommands.types.JoinpointLocation; +import com.fibermc.joinpoints.Joinpoints; +import com.fibermc.joinpoints.database.JoinpointDatabase; +import com.fibermc.joinpoints.types.JoinpointLocation; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; @@ -67,7 +67,7 @@ private int exec(ServerPlayerEntity senderPlayer, FilterType filter) { Async.runCommand(() -> { var senderPlayerId = senderPlayer.getUuid(); PlayerData playerData = ((ServerPlayerEntityAccess) senderPlayer).ec$getPlayerData(); - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); var joinpoints = database.getAccessibleJoinpointsWithNamesAsync(senderPlayer).join(); // List ownedJoinpoints = database.getOwnedJoinpointsAsync(senderPlayer.getUuid()).join(); @@ -127,15 +127,14 @@ private int exec(ServerPlayerEntity senderPlayer, FilterType filter) { message.append(" ").append(Text.literal("[Private+]").formatted(Formatting.BLUE)); } - // This causes an exception on chat packet sending due to a null text component -// message -// .append(" ") -// .append(Text.literal("by ").formatted(Formatting.GRAY)) -// .append( -// entry.isOwned -// ? ecText.literal("You") -// : entry.ownerDisplayName -// ); + message + .append(" ") + .append(Text.literal("by ").formatted(Formatting.GRAY)) + .append( + entry.isOwned + ? ecText.literal("You") + : entry.ownerDisplayName + ); senderPlayer.sendMessage(message); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointSetCommand.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointSetCommand.java similarity index 90% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointSetCommand.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointSetCommand.java index c0359765..baa664ec 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointSetCommand.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointSetCommand.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import java.util.HashSet; import java.util.Set; @@ -6,15 +6,15 @@ import java.util.function.Consumer; import java.util.function.Function; -import com.fibermc.essentialcommands.ECPerms; -import com.fibermc.essentialcommands.ManagerLocator; +import com.fibermc.joinpoints.JoinpointsPerms; +import com.fibermc.joinpoints.Joinpoints; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; -import com.fibermc.essentialcommands.database.JoinpointDatabase; +import com.fibermc.joinpoints.database.JoinpointDatabase; import com.fibermc.essentialcommands.playerdata.PlayerData; import com.fibermc.essentialcommands.text.ChatConfirmationPrompt; import com.fibermc.essentialcommands.text.ECText; -import com.fibermc.essentialcommands.types.JoinpointLimit; -import com.fibermc.essentialcommands.types.JoinpointLocation; +import com.fibermc.joinpoints.types.JoinpointLimit; +import com.fibermc.joinpoints.types.JoinpointLocation; import com.fibermc.essentialcommands.types.MinecraftLocation; import com.mojang.brigadier.Command; @@ -75,7 +75,7 @@ private int exec(CommandContext context, String joinpointNa Async.runCommand(() -> { PlayerData playerData = ((ServerPlayerEntityAccess) senderPlayer).ec$getPlayerData(); - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); return switch (action) { case SET -> handleSetAsync(finalIsGlobal, joinpointName, senderPlayer, playerData, database); @@ -119,11 +119,11 @@ private Void handleSetAsync( ? JoinpointLimit.JoinpointType.GLOBAL : JoinpointLimit.JoinpointType.SHARED; - var targetTypePerms = ECPerms.Registry.Group.joinpoint_limit_groups.get(joinpointType); - var anyTypePerms = ECPerms.Registry.Group.joinpoint_limit_groups.get(JoinpointLimit.JoinpointType.ANY); + var targetTypePerms = JoinpointsPerms.Registry.Group.joinpoint_limit_groups.get(joinpointType); + var anyTypePerms = JoinpointsPerms.Registry.Group.joinpoint_limit_groups.get(JoinpointLimit.JoinpointType.ANY); - int playerAllowedOfAnyType = anyTypePerms.length == 0 ? -1 : ECPerms.getHighestNumericPermission(senderPlayer.getCommandSource(), anyTypePerms); - int playerAllowedCountOfTargetType = targetTypePerms.length == 0 ? -1 : ECPerms.getHighestNumericPermission(senderPlayer.getCommandSource(), targetTypePerms); + int playerAllowedOfAnyType = anyTypePerms.length == 0 ? -1 : JoinpointsPerms.getHighestNumericPermission(senderPlayer.getCommandSource(), anyTypePerms); + int playerAllowedCountOfTargetType = targetTypePerms.length == 0 ? -1 : JoinpointsPerms.getHighestNumericPermission(senderPlayer.getCommandSource(), targetTypePerms); // any(5) -> up to 5 shared or global, any combindation // any(5),shared(3) -> no more then 3 shared. Could have 5 global:0 shared to 2 global:3 shared diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointShareCommand.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointShareCommand.java similarity index 96% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointShareCommand.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointShareCommand.java index 517ba67f..81476561 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointShareCommand.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointShareCommand.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import java.util.Collection; import java.util.HashSet; @@ -7,12 +7,12 @@ import java.util.function.Consumer; import java.util.function.Function; -import com.fibermc.essentialcommands.ManagerLocator; +import com.fibermc.joinpoints.Joinpoints; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; -import com.fibermc.essentialcommands.database.JoinpointDatabase; +import com.fibermc.joinpoints.database.JoinpointDatabase; import com.fibermc.essentialcommands.playerdata.PlayerData; import com.fibermc.essentialcommands.text.ECText; -import com.fibermc.essentialcommands.types.JoinpointLocation; +import com.fibermc.joinpoints.types.JoinpointLocation; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; @@ -67,7 +67,7 @@ private int exec(CommandContext context, String joinpointNa Async.runCommand(() -> { PlayerData playerData = ((ServerPlayerEntityAccess) senderPlayer).ec$getPlayerData(); - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); return switch (action) { case ADD -> handleAddAsync(finalTargetPlayers, joinpointName, senderPlayer, playerData, database); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointTpCommand.java b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointTpCommand.java similarity index 91% rename from src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointTpCommand.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointTpCommand.java index d7302e54..e59915d1 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/joinpoints/JoinpointTpCommand.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/commands/JoinpointTpCommand.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.commands.joinpoints; +package com.fibermc.joinpoints.commands; import java.util.List; import java.util.UUID; @@ -8,14 +8,14 @@ import java.util.stream.Collectors; import com.fibermc.essentialcommands.EssentialCommands; -import com.fibermc.essentialcommands.ManagerLocator; +import com.fibermc.joinpoints.Joinpoints; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; -import com.fibermc.essentialcommands.database.JoinpointDatabase; +import com.fibermc.joinpoints.database.JoinpointDatabase; import com.fibermc.essentialcommands.playerdata.PlayerData; import com.fibermc.essentialcommands.teleportation.PlayerTeleporter; import com.fibermc.essentialcommands.text.ECText; import com.fibermc.essentialcommands.text.TextFormatType; -import com.fibermc.essentialcommands.types.JoinpointLocation; +import com.fibermc.joinpoints.types.JoinpointLocation; import com.fibermc.essentialcommands.types.NamedMinecraftLocation; import org.apache.commons.lang3.function.TriFunction; import org.apache.logging.log4j.message.ParameterizedMessage; @@ -69,20 +69,27 @@ private Consumer> sendErrorToPlayer(ServerPlayerEntity se */ private @Nullable UUID getJoinpointOwnerIdAsync(ServerPlayerEntity senderPlayer, String ownerName) { + var server = senderPlayer.getEntityWorld().getServer(); + var apis = server.getApiServices(); + var userCache = apis.nameToIdCache(); + + // player to owned joinpoints + // player to accessible joinpoints + // If owner is self, use sender's UUID if (ownerName.equals(senderPlayer.getName().getString())) { return senderPlayer.getUuid(); } { // Try to find owner by name from online players first - var ownerPlayer = senderPlayer.getServer().getPlayerManager().getPlayer(ownerName); + var ownerPlayer = senderPlayer.getEntityWorld().getServer().getPlayerManager().getPlayer(ownerName); if (ownerPlayer != null) { return ownerPlayer.getUuid(); } } { // Go to our cached list of all player names as a last resort (particularly for offline players) - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); var ownerUuid = database.getOwnerPlayerIdByNameAsync(ownerName, senderPlayer.getUuid()).join(); if (ownerUuid != null) { return ownerUuid; @@ -96,7 +103,7 @@ private int exec(ServerPlayerEntity senderPlayer, String ownerName, String joinp { Async.runCommand(() -> { PlayerData senderPlayerData = ((ServerPlayerEntityAccess) senderPlayer).ec$getPlayerData(); - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); var ownerUuid = getJoinpointOwnerIdAsync(senderPlayer, ownerName); @@ -143,7 +150,7 @@ private static SuggestionProvider suggestFromDb( return (context, builder) -> { try { ServerPlayerEntity player = context.getSource().getPlayerOrThrow(); - JoinpointDatabase database = ManagerLocator.getInstance().getJoinpointDatabase(); + JoinpointDatabase database = Joinpoints.getDatabase(); return getSuggestionsAsync.apply(database, player, context) .thenApply(items -> { for (String item : items) { diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/config/JoinpointsConfig.java b/joinpoints/src/main/java/com/fibermc/joinpoints/config/JoinpointsConfig.java new file mode 100644 index 00000000..24f9b1dc --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/config/JoinpointsConfig.java @@ -0,0 +1,44 @@ +package com.fibermc.joinpoints.config; + +import java.nio.file.Path; + +import com.fibermc.joinpoints.JoinpointsPerms; +import com.fibermc.joinpoints.types.JoinpointLimit; +import dev.jpcode.eccore.config.Config; +import dev.jpcode.eccore.config.ConfigOption; +import dev.jpcode.eccore.config.Option; + +@SuppressWarnings("checkstyle:all") +public final class JoinpointsConfig extends Config { + + @ConfigOption + public final Option ENABLE_JOINPOINT = new Option<>("enable_joinpoint", true, Boolean::parseBoolean); + + @ConfigOption + public final Option JOINPOINT_LIMIT = new Option<>( + "joinpoint_limit", + JoinpointLimit.any(3, 5, 10), + JoinpointLimit::parse, + JoinpointLimit::serialize + ); + + public JoinpointsConfig(Path savePath, String displayName, String documentationLink) { + super(savePath, displayName, documentationLink); + + JOINPOINT_LIMIT.changeEvent.register(joinpointLimit -> { + JoinpointsPerms.Registry.Group.joinpoint_limit_groups.clear(); + for (var limitGroup : joinpointLimit.getLimits().entrySet()) { + var key = limitGroup.getKey(); + var limitNums = limitGroup.getValue(); + + JoinpointsPerms.Registry.Group.joinpoint_limit_groups.put( + key, + JoinpointsPerms.makeNumericPermissionGroup( + "joinpoints.limit." + key.name().toLowerCase(), + limitNums + ) + ); + } + }); + } +} diff --git a/joinpoints/src/main/java/com/fibermc/joinpoints/database/DatabaseHelper.java b/joinpoints/src/main/java/com/fibermc/joinpoints/database/DatabaseHelper.java new file mode 100644 index 00000000..817732c7 --- /dev/null +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/database/DatabaseHelper.java @@ -0,0 +1,31 @@ +package com.fibermc.joinpoints.database; + +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import com.fibermc.joinpoints.Joinpoints; + +import net.minecraft.util.Util; + +public final class DatabaseHelper { + private DatabaseHelper() {} + + private static final Executor EXECUTOR = Util.getIoWorkerExecutor(); + + public static CompletableFuture async(SqlSupplier supplier) { + return CompletableFuture.supplyAsync(() -> { + try { + return supplier.get(); + } catch (SQLException e) { + Joinpoints.LOGGER.error("Database error", e); + throw new RuntimeException("Database error", e); + } + }, EXECUTOR); + } + + @FunctionalInterface + public interface SqlSupplier { + T get() throws SQLException; + } +} diff --git a/src/main/java/com/fibermc/essentialcommands/database/JoinpointDatabase.java b/joinpoints/src/main/java/com/fibermc/joinpoints/database/JoinpointDatabase.java similarity index 99% rename from src/main/java/com/fibermc/essentialcommands/database/JoinpointDatabase.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/database/JoinpointDatabase.java index a6fd48ca..e8043192 100644 --- a/src/main/java/com/fibermc/essentialcommands/database/JoinpointDatabase.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/database/JoinpointDatabase.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.database; +package com.fibermc.joinpoints.database; import java.io.File; import java.sql.*; @@ -7,7 +7,7 @@ import com.fibermc.essentialcommands.EssentialCommands; import com.fibermc.essentialcommands.playerdata.PlayerDataManager; -import com.fibermc.essentialcommands.types.JoinpointLocation; +import com.fibermc.joinpoints.types.JoinpointLocation; import com.fibermc.essentialcommands.types.MinecraftLocation; import org.apache.logging.log4j.Level; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/fibermc/essentialcommands/types/JoinpointLimit.java b/joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLimit.java similarity index 99% rename from src/main/java/com/fibermc/essentialcommands/types/JoinpointLimit.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLimit.java index b4ad574d..c50b4b9f 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/JoinpointLimit.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLimit.java @@ -1,4 +1,4 @@ -package com.fibermc.essentialcommands.types; +package com.fibermc.joinpoints.types; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/com/fibermc/essentialcommands/types/JoinpointLocation.java b/joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLocation.java similarity index 90% rename from src/main/java/com/fibermc/essentialcommands/types/JoinpointLocation.java rename to joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLocation.java index e4e49150..5e789690 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/JoinpointLocation.java +++ b/joinpoints/src/main/java/com/fibermc/joinpoints/types/JoinpointLocation.java @@ -1,11 +1,13 @@ -package com.fibermc.essentialcommands.types; +package com.fibermc.joinpoints.types; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.UUID; -import com.fibermc.essentialcommands.codec.Codecs; +import com.fibermc.essentialcommands.types.MinecraftLocation; +import com.fibermc.essentialcommands.types.NamedMinecraftLocation; +import com.fibermc.joinpoints.codec.JoinpointCodecs; import com.mojang.serialization.Codec; @@ -16,7 +18,7 @@ import net.minecraft.world.World; public class JoinpointLocation extends NamedMinecraftLocation { - public static final Codec CODEC = Codecs.JOINPOINT_LOCATION; + public static final Codec CODEC = JoinpointCodecs.JOINPOINT_LOCATION; private final UUID owner; private final boolean isGlobal; diff --git a/joinpoints/src/main/resources/assets/joinpoints/icon.jpg b/joinpoints/src/main/resources/assets/joinpoints/icon.jpg new file mode 100644 index 00000000..ec6f5f01 Binary files /dev/null and b/joinpoints/src/main/resources/assets/joinpoints/icon.jpg differ diff --git a/joinpoints/src/main/resources/assets/joinpoints/lang/en_us.json b/joinpoints/src/main/resources/assets/joinpoints/lang/en_us.json new file mode 100644 index 00000000..53ddb661 --- /dev/null +++ b/joinpoints/src/main/resources/assets/joinpoints/lang/en_us.json @@ -0,0 +1,39 @@ +{ + "cmd.joinpoint.tp.error.not_found": "No joinpoint with the name '${0}' could be found for owner '${1}'.", + "cmd.joinpoint.tp.error.owner_not_found": "Player '${0}' not found or has no accessible joinpoints.", + "cmd.joinpoint.tp.error.no_access": "You don't have access to joinpoint '${0}' owned by '${1}'.", + "cmd.joinpoint.location_name": "joinpoint: ${0}", + "cmd.joinpoint.location_name_with_owner": "joinpoint: ${0} (owned by ${1})", + "cmd.joinpoint.delete.feedback": "Joinpoint '${0}' has been deleted.", + "cmd.joinpoint.delete.error": "Joinpoint '${0}' could not be deleted.", + "cmd.joinpoint.set.feedback": "Joinpoint '${0}' set.", + "cmd.joinpoint.set.feedback.global": "Global joinpoint '${0}' set.", + "cmd.joinpoint.set.feedback.shared": "Shared joinpoint '${0}' set.", + "cmd.joinpoint.set.error.limit": "Joinpoint '${0}' could not be set. Joinpoint limit (${1}) for joinpoint type '${2}' reached.", + "cmd.joinpoint.set.error.exists": "Joinpoint '${0}' could not be set. A joinpoint with the specified name already exists.", + "cmd.joinpoint.set.overwrite": "Joinpoint '${0}' already exists.\nWould you like to overwrite the existing joinpoint?", + "cmd.joinpoint.overwrite.feedback": "Joinpoint '${0}' moved to current location.", + "cmd.joinpoint.overwrite.feedback.global": "Global joinpoint '${0}' moved to current location.", + "cmd.joinpoint.overwrite.feedback.shared": "Shared joinpoint '${0}' moved to current location.", + "cmd.joinpoint.list.header.all": "All accessible joinpoints (${0}):", + "cmd.joinpoint.list.header.owned": "Your owned joinpoints (${0}):", + "cmd.joinpoint.list.header.shared_with": "Joinpoints shared with you (${0}):", + "cmd.joinpoint.list.header.global": "Global joinpoints (${0}):", + "cmd.joinpoint.list.empty.all": "You have no accessible joinpoints.", + "cmd.joinpoint.list.empty.owned": "You don't own any joinpoints.", + "cmd.joinpoint.list.empty.shared_with": "No joinpoints have been shared with you.", + "cmd.joinpoint.list.empty.global": "No global joinpoints are available.", + "cmd.joinpoint.error.database": "Database error: ${0}", + "cmd.joinpoint.error.unknown": "Joinpoint action failed - an unknown error occurred: ${0}", + "cmd.joinpoint.error.not_found": "Joinpoint '${0}' not found.", + "cmd.joinpoint.share.error.already_global": "Joinpoint '${0}' is already global and accessible to all players.", + "cmd.joinpoint.share.error.no_new_players": "No new players to share with.", + "cmd.joinpoint.share.error.players_not_shared": "None of the specified players have access to this joinpoint.", + "cmd.joinpoint.share.error.cannot_clear_global": "Cannot clear sharing for global joinpoint '${0}'. Use '/joinpoint set' to make it private first.", + "cmd.joinpoint.share.add.feedback": "Shared joinpoint '${0}' with: ${1}", + "cmd.joinpoint.share.remove.feedback": "Removed access to joinpoint '${0}' from: ${1}", + "cmd.joinpoint.share.list.global": "Joinpoint '${0}' is global (accessible to all players).", + "cmd.joinpoint.share.list.private": "Joinpoint '${0}' is private (only accessible to you).", + "cmd.joinpoint.share.list.shared": "Joinpoint '${0}' is shared with: ${1}", + "cmd.joinpoint.share.clear.feedback": "Cleared all sharing for joinpoint '${0}' (now private)." +} diff --git a/joinpoints/src/main/resources/fabric.mod.json b/joinpoints/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..38a4ebf4 --- /dev/null +++ b/joinpoints/src/main/resources/fabric.mod.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "id": "joinpoints", + "version": "${version}", + + "name": "Joinpoints", + "description": "Adds joinpoint functionality (player-created teleportation waypoints) to Essential Commands.", + "authors": [ + "John Paul R. (JP79194)" + ], + "contact": { + "homepage": "https://www.jpcode.dev", + "issues": "https://github.com/John-Paul-R/Essential-Commands/issues", + "sources": "https://github.com/John-Paul-R/Essential-Commands", + "discord": "https://discord.jpcode.dev" + }, + + "license": "MIT", + "icon": "assets/joinpoints/icon.jpg", + + "environment": "*", + "entrypoints": { + "main": [ + "com.fibermc.joinpoints.Joinpoints" + ] + }, + "mixins": [ + ], + + "depends": { + "fabricloader": ">=0.11.3", + "fabric": "*", + "essential_commands": "*" + }, + "suggests": { + }, + "jars": [] +} diff --git a/settings.gradle b/settings.gradle index 37e0875c..dfabe142 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,3 +12,4 @@ pluginManagement { } rootProject.name = 'essential_commands' include 'ec-core' +include 'joinpoints' diff --git a/src/main/java/com/fibermc/essentialcommands/ECPerms.java b/src/main/java/com/fibermc/essentialcommands/ECPerms.java index dc3e1a66..119f1cb8 100644 --- a/src/main/java/com/fibermc/essentialcommands/ECPerms.java +++ b/src/main/java/com/fibermc/essentialcommands/ECPerms.java @@ -2,12 +2,9 @@ import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import java.util.function.Predicate; import java.util.stream.Stream; -import com.fibermc.essentialcommands.types.JoinpointLimit; import me.lucko.fabric.api.permissions.v0.Permissions; import org.jetbrains.annotations.NotNull; @@ -36,9 +33,6 @@ public static final class Registry { public static final String warp_delete = "essentialcommands.warp.delete"; public static final String warp_tp_named = "essentialcommands.warp.tp_named"; public static final String warp_tp_others = "essentialcommands.warp_tp_others"; - public static final String joinpoint_set = "essentialcommands.joinpoint.set"; - public static final String joinpoint_tp = "essentialcommands.joinpoint.tp"; - public static final String joinpoint_delete = "essentialcommands.joinpoint.delete"; public static final String back = "essentialcommands.back"; public static final String spawn_tp = "essentialcommands.spawn.tp"; public static final String spawn_set = "essentialcommands.spawn.set"; @@ -94,14 +88,12 @@ public static final class Group { public static final String[] tpa_group = {tpa, tpahere, tpaccept, tpdeny}; public static final String[] home_group = {home_set, home_tp, home_delete}; public static final String[] warp_group = {warp_set, warp_tp, warp_delete}; - public static final String[] joinpoint_group = {joinpoint_set, joinpoint_tp, joinpoint_delete}; public static final String[] spawn_group = {spawn_tp, spawn_set}; public static final String[] nickname_group = {nickname_self, nickname_others, nickname_reveal}; public static final String[] fly_group = {fly_self, fly_others}; public static final String[] invuln_group = {invuln_self, invuln_others}; public static final String[] config_group = {config_reload}; public static String[] home_limit_group; - public static Map joinpoint_limit_groups = new HashMap<>(); public static final String[] stateful_player_abilities = {fly_self, fly_others, invuln_self, invuln_others}; } diff --git a/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java b/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java index 460554ca..6ecadf62 100644 --- a/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java +++ b/src/main/java/com/fibermc/essentialcommands/EssentialCommandRegistry.java @@ -8,10 +8,6 @@ import com.fibermc.essentialcommands.commands.*; import com.fibermc.essentialcommands.commands.bench.*; -import com.fibermc.essentialcommands.commands.joinpoints.JoinpointListCommand; -import com.fibermc.essentialcommands.commands.joinpoints.JoinpointSetCommand; -import com.fibermc.essentialcommands.commands.joinpoints.JoinpointShareCommand; -import com.fibermc.essentialcommands.commands.joinpoints.JoinpointTpCommand; import com.fibermc.essentialcommands.commands.suggestions.ListSuggestion; import com.fibermc.essentialcommands.commands.suggestions.NicknamePlayersSuggestion; import com.fibermc.essentialcommands.commands.suggestions.TeleportResponseSuggestion; @@ -195,83 +191,6 @@ public static void register( essentialCommandsRootNode.addChild(homeOverwriteBuilder.build()); } - if (CONFIG.ENABLE_JOINPOINT) { - LiteralArgumentBuilder joinpointBuilder = CommandManager.literal("joinpoint"); - LiteralArgumentBuilder joinpointSetBuilder = CommandManager.literal("set"); - LiteralArgumentBuilder joinpointTpBuilder = CommandManager.literal("tp"); - LiteralArgumentBuilder joinpointDeleteBuilder = CommandManager.literal("delete"); - LiteralArgumentBuilder joinpointOverwriteBuilder = CommandManager.literal("overwrite"); - LiteralArgumentBuilder joinpointShareBuilder = CommandManager.literal("share"); - LiteralArgumentBuilder joinpointListBuilder = CommandManager.literal("list"); - - joinpointSetBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_set, 0)) - .then(argument("joinpoint_name", StringArgumentType.word()) - .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.SET)) - .then(argument("global", BoolArgumentType.bool()) - .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.SET)))); - - joinpointTpBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_tp, 0)) - .then(argument(JoinpointTpCommand.OWNER_PLAYER_ARG, StringArgumentType.word()) - .suggests(JoinpointTpCommand.Suggestion.OWNERS_OF_ACCESSIBLE_JOINPOINTS) - .then(argument("joinpoint_name", StringArgumentType.word()) - .suggests(JoinpointTpCommand.Suggestion.ACCESSIBLE_TARGET_PLAYER_JOINPOINTS) - .executes(new JoinpointTpCommand()))); - - // this is interfering with the more-important playername suggestions -// .then(argument("joinpoint_name", StringArgumentType.word()) -// .suggests(JoinpointTpCommand.Suggestion.ACCESSIBLE_JOINPOINTS) -// .executes(new JoinpointTpCommand()::runOwnJoinpoint)) - - joinpointDeleteBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_delete, 0)) - .then(argument("joinpoint_name", StringArgumentType.word()) - .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) - .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.DELETE))); - - joinpointOverwriteBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_set, 0)) - .then(argument("joinpoint_name", StringArgumentType.word()) - .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) - .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.OVERWRITE)) - .then(argument("global", BoolArgumentType.bool()) - .executes(new JoinpointSetCommand(JoinpointSetCommand.Action.OVERWRITE)))); - - joinpointShareBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_set, 0)) - .then(argument("joinpoint_name", StringArgumentType.word()) - .suggests(JoinpointTpCommand.Suggestion.OWNED_JOINPOINTS) - .then(CommandManager.literal("add") - .then(argument("target_players", EntityArgumentType.players()) - .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.ADD)))) - .then(CommandManager.literal("remove") - .then(argument("target_players", EntityArgumentType.players()) - .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.REMOVE)))) - .then(CommandManager.literal("list") - .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.LIST))) - .then(CommandManager.literal("clear") - .executes(new JoinpointShareCommand(JoinpointShareCommand.Action.CLEAR)))); - - joinpointListBuilder - .requires(ECPerms.require(ECPerms.Registry.joinpoint_tp, 0)) - .executes(new JoinpointListCommand()::runDefault) - .then(argument("filter", StringArgumentType.word()) - .suggests(JoinpointListCommand.Suggestion.FILTER_TYPES) - .executes(new JoinpointListCommand())); - - LiteralCommandNode joinpointNode = joinpointBuilder - .requires(ECPerms.requireAny(ECPerms.Registry.Group.joinpoint_group, 0)) - .build(); - joinpointNode.addChild(joinpointSetBuilder.build()); - joinpointNode.addChild(joinpointTpBuilder.build()); - joinpointNode.addChild(joinpointDeleteBuilder.build()); - joinpointNode.addChild(joinpointOverwriteBuilder.build()); - joinpointNode.addChild(joinpointShareBuilder.build()); - joinpointNode.addChild(joinpointListBuilder.build()); - - registerNode.accept(joinpointNode); - } //Back if (CONFIG.ENABLE_BACK) { diff --git a/src/main/java/com/fibermc/essentialcommands/ManagerLocator.java b/src/main/java/com/fibermc/essentialcommands/ManagerLocator.java index 83a6378d..e5b5fa20 100644 --- a/src/main/java/com/fibermc/essentialcommands/ManagerLocator.java +++ b/src/main/java/com/fibermc/essentialcommands/ManagerLocator.java @@ -4,7 +4,6 @@ import java.util.function.Consumer; import com.fibermc.essentialcommands.commands.suggestions.OfflinePlayerRepo; -import com.fibermc.essentialcommands.database.JoinpointDatabase; import com.fibermc.essentialcommands.playerdata.PlayerDataManager; import com.fibermc.essentialcommands.teleportation.TeleportManager; @@ -19,7 +18,6 @@ public final class ManagerLocator { private TeleportManager tpManager; private WorldDataManager worldDataManager; private OfflinePlayerRepo offlinePlayerRepo; - private JoinpointDatabase joinpointDatabase; private final HashMap> serverStartActions = new HashMap<>(); public static ManagerLocator instance; @@ -46,7 +44,6 @@ public void onServerStart(MinecraftServer server) { this.tpManager = TeleportManager.getInstance(); this.worldDataManager = WorldDataManager.createForServer(server); this.offlinePlayerRepo = new OfflinePlayerRepo(server); - this.joinpointDatabase = new JoinpointDatabase(server.getSavePath(net.minecraft.util.WorldSavePath.ROOT).toFile()); ServerLifecycleEvents.SERVER_STARTED.register(server1 -> { serverStartActions.values().forEach(a -> a.accept(server)); serverStarted = true; @@ -69,10 +66,6 @@ public OfflinePlayerRepo getOfflinePlayerRepo() { return offlinePlayerRepo; } - public JoinpointDatabase getJoinpointDatabase() { - return joinpointDatabase; - } - public void runAndQueue(String key, Consumer action) { serverStartActions.putIfAbsent(key, action); diff --git a/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java b/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java index 250312e2..99936e4a 100644 --- a/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java +++ b/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java @@ -1,9 +1,7 @@ package com.fibermc.essentialcommands.codec; import java.util.HashMap; -import java.util.HashSet; import java.util.Optional; -import java.util.UUID; import com.fibermc.essentialcommands.WorldData; import com.fibermc.essentialcommands.types.*; @@ -63,28 +61,6 @@ private Codecs() {} ).apply(instance, WarpLocation::new) ); - public static final Codec JOINPOINT_LOCATION = RecordCodecBuilder.create(instance -> - instance.group( - // Inherit all fields from NamedMinecraftLocation - WORLD_KEY.fieldOf("WorldRegistryKey").forGetter(JoinpointLocation::dim), - Codec.DOUBLE.fieldOf("x").forGetter(MinecraftLocation::x), - Codec.DOUBLE.fieldOf("y").forGetter(MinecraftLocation::y), - Codec.DOUBLE.fieldOf("z").forGetter(MinecraftLocation::z), - Codec.FLOAT.optionalFieldOf("headYaw", 0.0f).forGetter(JoinpointLocation::headYaw), - Codec.FLOAT.optionalFieldOf("pitch", 0.0f).forGetter(JoinpointLocation::pitch), - // loaded from the map - Codec.STRING.optionalFieldOf("name").forGetter(joinpoint -> Optional.of(((JoinpointLocation)joinpoint).getName())), - - // Joinpoint-specific fields - Codec.STRING.xmap(UUID::fromString, UUID::toString).fieldOf("owner").forGetter(JoinpointLocation::getOwner), - Codec.BOOL.optionalFieldOf("isGlobal", false).forGetter(JoinpointLocation::isGlobal), - Codec.STRING.xmap(UUID::fromString, UUID::toString).listOf().optionalFieldOf("sharedWith", java.util.List.of()) - .xmap(HashSet::new, java.util.List::copyOf) - .forGetter(JoinpointLocation::getSharedWith) - - ).apply(instance, JoinpointLocation::new) - ); - public static final Codec NAMED_LOCATION_STORAGE = Codec.unboundedMap(Codec.STRING, NAMED_MINECRAFT_LOCATION) .xmap( diff --git a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java index 49235b25..ebd30217 100644 --- a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java +++ b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfig.java @@ -11,7 +11,6 @@ import com.fibermc.essentialcommands.EssentialCommands; import com.fibermc.essentialcommands.ManagerLocator; import com.fibermc.essentialcommands.playerdata.PlayerDataManager; -import com.fibermc.essentialcommands.types.JoinpointLimit; import com.fibermc.essentialcommands.types.RespawnCondition; import com.fibermc.essentialcommands.types.RtpCenter; import org.jetbrains.annotations.NotNull; @@ -48,7 +47,6 @@ public final class EssentialCommandsConfig extends Config ENABLE_SPAWN = new Option<>("enable_spawn", true, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_TPA = new Option<>("enable_tpa", true, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_WARP = new Option<>("enable_warp", true, Boolean::parseBoolean); - @ConfigOption public final Option ENABLE_JOINPOINT = new Option<>("enable_joinpoint", false, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_NICK = new Option<>("enable_nick", true, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_RTP = new Option<>("enable_rtp", true, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_FLY = new Option<>("enable_fly", true, Boolean::parseBoolean); @@ -75,7 +73,6 @@ public final class EssentialCommandsConfig extends Config ENABLE_SLEEP = new Option<>("enable_sleep", false, Boolean::parseBoolean); @ConfigOption public final Option ENABLE_DELETE_ALL_PLAYER_DATA = new Option<>("enable_delete_all_player_data", true, Boolean::parseBoolean); @ConfigOption public final Option> HOME_LIMIT = new Option<>("home_limit", List.of(1, 2, 5), arrayParser(ConfigUtil::parseInt)); - @ConfigOption public final Option JOINPOINT_LIMIT = new Option<>("joinpoint_limit", JoinpointLimit.any(3, 5, 10), JoinpointLimit::parse, JoinpointLimit::serialize); @ConfigOption public final Option TELEPORT_COOLDOWN = new Option<>("teleport_cooldown", 1.0, ConfigUtil::parseDouble); @ConfigOption public final Option TELEPORT_DELAY = new Option<>("teleport_delay", 0.0, ConfigUtil::parseDouble); @ConfigOption public final Option ALLOW_BACK_ON_DEATH = new Option<>("allow_back_on_death", false, Boolean::parseBoolean); @@ -125,21 +122,6 @@ public EssentialCommandsConfig(Path savePath, String displayName, String documen HOME_LIMIT.changeEvent.register(newValue -> ECPerms.Registry.Group.home_limit_group = ECPerms.makeNumericPermissionGroup("essentialcommands.home.limit", newValue) ); - JOINPOINT_LIMIT.changeEvent.register(joinpointLimit -> { - ECPerms.Registry.Group.joinpoint_limit_groups.clear(); - for (var limitGroup : joinpointLimit.getLimits().entrySet()) { - var key = limitGroup.getKey(); - var limitNums = limitGroup.getValue(); - - ECPerms.Registry.Group.joinpoint_limit_groups.put( - key, - ECPerms.makeNumericPermissionGroup( - "essentialcommands.joinpoint_limit." + key.name().toLowerCase(), - limitNums - ) - ); - } - }); // This value is only sent on server start/player connect and, so, cannot be updated for all // players immediately via the config reload command without a fair bit of hackery. // NICKNAME_ABOVE_HEAD.changeEvent.register(ign -> { diff --git a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java index 4f9d1a7f..b920356a 100644 --- a/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java +++ b/src/main/java/com/fibermc/essentialcommands/config/EssentialCommandsConfigSnapshot.java @@ -26,7 +26,6 @@ public final class EssentialCommandsConfigSnapshot { public final boolean ENABLE_SPAWN; public final boolean ENABLE_TPA; public final boolean ENABLE_WARP; - public final boolean ENABLE_JOINPOINT; public final boolean ENABLE_NICK; public final boolean ENABLE_RTP; public final boolean ENABLE_FLY; @@ -106,7 +105,6 @@ private EssentialCommandsConfigSnapshot(EssentialCommandsConfig config) { this.ENABLE_SPAWN = config.ENABLE_SPAWN.getValue(); this.ENABLE_TPA = config.ENABLE_TPA.getValue(); this.ENABLE_WARP = config.ENABLE_WARP.getValue(); - this.ENABLE_JOINPOINT = config.ENABLE_JOINPOINT.getValue(); this.ENABLE_NICK = config.ENABLE_NICK.getValue(); this.ENABLE_RTP = config.ENABLE_RTP.getValue(); this.ENABLE_FLY = config.ENABLE_FLY.getValue(); diff --git a/src/main/java/com/fibermc/essentialcommands/events/NicknameChangeCallback.java b/src/main/java/com/fibermc/essentialcommands/events/NicknameChangeCallback.java new file mode 100644 index 00000000..2512a981 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/events/NicknameChangeCallback.java @@ -0,0 +1,18 @@ +package com.fibermc.essentialcommands.events; + +import net.minecraft.server.network.ServerPlayerEntity; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public interface NicknameChangeCallback { + Event EVENT = EventFactory.createArrayBacked( + NicknameChangeCallback.class, + (listeners) -> (player) -> { + for (NicknameChangeCallback event : listeners) { + event.onNicknameChange(player); + } + }); + + void onNicknameChange(ServerPlayerEntity player); +} diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java index 546d03c8..5f275db5 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java @@ -6,22 +6,15 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.fibermc.essentialcommands.EssentialCommands; import com.fibermc.essentialcommands.ManagerLocator; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; import com.fibermc.essentialcommands.commands.MotdCommand; -import com.fibermc.essentialcommands.events.PlayerConnectCallback; -import com.fibermc.essentialcommands.events.PlayerDataManagerTickCallback; -import com.fibermc.essentialcommands.events.PlayerDeathCallback; -import com.fibermc.essentialcommands.events.PlayerLeaveCallback; +import com.fibermc.essentialcommands.events.*; import com.fibermc.essentialcommands.types.MinecraftLocation; import com.fibermc.essentialcommands.types.RespawnCondition; -import com.google.gson.JsonElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.mojang.serialization.JsonOps; - import net.minecraft.entity.damage.DamageSource; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; @@ -29,7 +22,6 @@ import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.TextCodecs; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; @@ -73,9 +65,6 @@ public static void init() { ServerPlayConnectionEvents.JOIN.register( PlayerDataManager::handleSendMotdForGameJoin ); - ServerPlayConnectionEvents.JOIN.register( - (handler, sender, server) -> updatePlayerCache(handler.player) - ); } public static final Event TICK_EVENT = @@ -101,28 +90,6 @@ private static void handleSendMotdForGameJoin( } } - private static void updatePlayerCache( - ServerPlayerEntity player - ) { - try { - var playerData = ((ServerPlayerEntityAccess) player).ec$getPlayerData(); - var database = ManagerLocator.getInstance().getJoinpointDatabase(); - - String nicknameJson = playerData.getNickname() - .map(text -> TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, text).getOrThrow()) - .map(JsonElement::toString) - .orElse(null); - database - .updatePlayerCacheAsync(player.getUuid(), player.getName().getString(), nicknameJson) - .exceptionally(err -> { - EssentialCommands.LOGGER.error(err); - return null; - }); - } catch (Exception e) { - // Log but don't crash on cache update failure - joinpoint database might not be initialized yet - EssentialCommands.LOGGER.error(e); - } - } public static boolean exists() { return instance != null; @@ -189,7 +156,7 @@ public void tick(MinecraftServer server) { changedNicknames.forEach(playerData -> { playerData.save(); - updatePlayerCache(playerData.getPlayer()); + NicknameChangeCallback.EVENT.invoker().onNicknameChange(playerData.getPlayer()); }); this.changedNicknames.clear(); diff --git a/src/main/java/com/fibermc/essentialcommands/text/ECText.java b/src/main/java/com/fibermc/essentialcommands/text/ECText.java index 339cfa7d..64ca388a 100644 --- a/src/main/java/com/fibermc/essentialcommands/text/ECText.java +++ b/src/main/java/com/fibermc/essentialcommands/text/ECText.java @@ -13,7 +13,6 @@ import com.fibermc.essentialcommands.playerdata.PlayerProfile; import com.fibermc.essentialcommands.types.IStyleProvider; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; @@ -32,10 +31,10 @@ import static com.fibermc.essentialcommands.EssentialCommands.*; public abstract class ECText { - protected final Map stringMap; + protected final Map stringMap = new java.util.HashMap<>(); protected ECText(Map stringMap) { - this.stringMap = stringMap; + this.stringMap.putAll(stringMap); } private static final Gson GSON = new Gson(); @@ -49,12 +48,49 @@ protected ECText(Map stringMap) { BACKING_CONFIG.LANGUAGE.changeEvent.register((langId) -> instance = create(langId)); } + /** + * Register an additional lang file path to be loaded alongside Essential Commands lang files. + * @param resourcePathFormat Format string for the resource path (e.g., "/assets/mymod/lang/%s.json") + */ + public static void registerAdditionalLangPath(String resourcePathFormat) { + loadLangFile(resourcePathFormat, CONFIG.LANGUAGE, instance.stringMap::put); + } + public static void init(MinecraftServer server) { ECText.server = server; } + private static void loadLangFile(String resourcePathFormat, String langId, BiConsumer entryConsumer) { + final String resourceLocation = String.format(resourcePathFormat, langId); + try { + InputStream inputStream = ECText.class.getResourceAsStream(resourceLocation); + if (inputStream == null) { + // Try default language if specified language not found + inputStream = ECText.class.getResourceAsStream(String.format(resourcePathFormat, DEFAULT_LANGUAGE_SPEC)); + } + + if (inputStream != null) { + try { + load(inputStream, entryConsumer); + } catch (Throwable loadEx) { + try { + inputStream.close(); + } catch (Throwable closeEx) { + loadEx.addSuppressed(closeEx); + } + throw loadEx; + } + inputStream.close(); + } + } catch (JsonParseException | IOException ex) { + LOGGER.error("Couldn't read strings from {}", resourceLocation, ex); + } + } + private static ECText create(String langId) { - ImmutableMap.Builder builder = ImmutableMap.builder(); + Map map = new java.util.HashMap<>(); + + // Load Essential Commands lang file final String resourceFString = "/assets/essential_commands/lang/%s.json"; final String resourceLocation = String.format(resourceFString, langId); try { @@ -65,7 +101,7 @@ private static ECText create(String langId) { } try { - load(inputStream, builder::put); + load(inputStream, map::put); } catch (Throwable loadEx) { if (inputStream != null) { try { @@ -85,7 +121,6 @@ private static ECText create(String langId) { LOGGER.error("Couldn't read strings from {}", resourceLocation, ex); } - final Map map = builder.build(); return instance = server == null ? new ECTextImpl(map, ParserContext.of()) : ECTextImpl.forServer(map, server); diff --git a/src/main/resources/assets/essential_commands/lang/en_us.json b/src/main/resources/assets/essential_commands/lang/en_us.json index d8a2b5df..17ba07fa 100644 --- a/src/main/resources/assets/essential_commands/lang/en_us.json +++ b/src/main/resources/assets/essential_commands/lang/en_us.json @@ -20,43 +20,6 @@ "cmd.home.set.overwrite": "Home '${0}' already exists.\nWould you like to overwrite the existing home?", "cmd.home.list.start": "Your current homes are: ", "cmd.overwritehome.feedback": "Home '${0}' moved to current location.", - "cmd.joinpoint.tp.error.not_found": "No joinpoint with the name '${0}' could be found for owner '${1}'.", - "cmd.joinpoint.tp.error.owner_not_found": "Player '${0}' not found or has no accessible joinpoints.", - "cmd.joinpoint.tp.error.no_access": "You don't have access to joinpoint '${0}' owned by '${1}'.", - "cmd.joinpoint.location_name": "joinpoint: ${0}", - "cmd.joinpoint.location_name_with_owner": "joinpoint: ${0} (owned by ${1})", - "cmd.joinpoint.delete.feedback": "Joinpoint '${0}' has been deleted.", - "cmd.joinpoint.delete.error": "Joinpoint '${0}' could not be deleted.", - "cmd.joinpoint.set.feedback": "Joinpoint '${0}' set.", - "cmd.joinpoint.set.feedback.global": "Global joinpoint '${0}' set.", - "cmd.joinpoint.set.feedback.shared": "Shared joinpoint '${0}' set.", - "cmd.joinpoint.set.error.limit": "Joinpoint '${0}' could not be set. Joinpoint limit (${1}) for joinpoint type '${2}' reached.", - "cmd.joinpoint.set.error.exists": "Joinpoint '${0}' could not be set. A joinpoint with the specified name already exists.", - "cmd.joinpoint.set.overwrite": "Joinpoint '${0}' already exists.\nWould you like to overwrite the existing joinpoint?", - "cmd.joinpoint.overwrite.feedback": "Joinpoint '${0}' moved to current location.", - "cmd.joinpoint.overwrite.feedback.global": "Global joinpoint '${0}' moved to current location.", - "cmd.joinpoint.overwrite.feedback.shared": "Shared joinpoint '${0}' moved to current location.", - "cmd.joinpoint.list.header.all": "All accessible joinpoints (${0}):", - "cmd.joinpoint.list.header.owned": "Your owned joinpoints (${0}):", - "cmd.joinpoint.list.header.shared_with": "Joinpoints shared with you (${0}):", - "cmd.joinpoint.list.header.global": "Global joinpoints (${0}):", - "cmd.joinpoint.list.empty.all": "You have no accessible joinpoints.", - "cmd.joinpoint.list.empty.owned": "You don't own any joinpoints.", - "cmd.joinpoint.list.empty.shared_with": "No joinpoints have been shared with you.", - "cmd.joinpoint.list.empty.global": "No global joinpoints are available.", - "cmd.joinpoint.error.database": "Database error: ${0}", - "cmd.joinpoint.error.unknown": "Joinpoint action failed - an unknown error occurred: ${0}", - "cmd.joinpoint.error.not_found": "Joinpoint '${0}' not found.", - "cmd.joinpoint.share.error.already_global": "Joinpoint '${0}' is already global and accessible to all players.", - "cmd.joinpoint.share.error.no_new_players": "No new players to share with.", - "cmd.joinpoint.share.error.players_not_shared": "None of the specified players have access to this joinpoint.", - "cmd.joinpoint.share.error.cannot_clear_global": "Cannot clear sharing for global joinpoint '${0}'. Use '/joinpoint set' to make it private first.", - "cmd.joinpoint.share.add.feedback": "Shared joinpoint '${0}' with: ${1}", - "cmd.joinpoint.share.remove.feedback": "Removed access to joinpoint '${0}' from: ${1}", - "cmd.joinpoint.share.list.global": "Joinpoint '${0}' is global (accessible to all players).", - "cmd.joinpoint.share.list.private": "Joinpoint '${0}' is private (only accessible to you).", - "cmd.joinpoint.share.list.shared": "Joinpoint '${0}' is shared with: ${1}", - "cmd.joinpoint.share.clear.feedback": "Cleared all sharing for joinpoint '${0}' (now private).", "cmd.nickname.set.feedback": "Nickname set to '${0}'.", "cmd.nickname.set.error": "Nickname could not be set to '${0}'. Reason: ${1}", "cmd.nickname.set.error.perms": "Player has insufficient permissions for specified nickname.",