diff --git a/README.md b/README.md index 25fa76f..d42cd83 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Spigot plugin for 1.20+ allowing the claiming of chunks. Usage and more information can be found [on the wiki](https://github.com/cjburkey01/ClaimChunk/wiki). * **1.20-1.21+** | The latest version works seamlessly (excluding bugs, of course). - * **Note for 0.0.23**: When updating the server from 1.20 to 1.21, ClaimChunk will throw errors that it can't find entities by whatever names due to the enum API change. + * **Note for 0.0.23+**! When updating the server from 1.20 to 1.21, ClaimChunk will throw errors that it can't find entities by whatever names due to the enum API change. * If you keep getting those errors on server start, stop the server, open your old profiles at `/plugins/ClaimChunk/worlds/.txt`, then copy these lines and replace the old (similar looking) ones in each world profile file: ``` _._@B_: diff --git a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java index ffd7b39..2ffade2 100644 --- a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java +++ b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java @@ -1,5 +1,6 @@ package com.cjburkey.claimchunk; +import com.cjburkey.claimchunk.access.CCInteractClasses; import com.cjburkey.claimchunk.api.IClaimChunkPlugin; import com.cjburkey.claimchunk.api.layer.ClaimChunkLayerHandler; import com.cjburkey.claimchunk.chunk.*; @@ -111,6 +112,8 @@ public final class ClaimChunk extends JavaPlugin implements IClaimChunkPlugin { @Getter private MainHandler mainHandler; @Getter private ChunkOutlineHandler chunkOutlineHandler; + @Getter private CCInteractClasses interactClasses; + // Config conversion storage private FromPre0023 fromPre0023; @@ -204,6 +207,21 @@ public void onLoad() { } else { Utils.log("Skipped registering WorldGuard flag, it's already initialized"); } + + // Initialize block/entity classes and write the file that lists them for admin reference + interactClasses = new CCInteractClasses(true); + File interactClassesFile = new File(getDataFolder(), "classes.yml"); + try { + boolean existed = interactClassesFile.exists(); + interactClasses.toYaml().save(interactClassesFile); + if (!existed) { + Utils.log("Created classes reference file at classes.yml :)"); + } + } catch (Exception e) { + Utils.warn( + "Failed to write classes.yml file. This doesn't really matter, but something" + + " else is probably wrong!"); + } } boolean checkBStats(Metrics metrics) { diff --git a/src/main/java/com/cjburkey/claimchunk/Utils.java b/src/main/java/com/cjburkey/claimchunk/Utils.java index ec890c0..457ed46 100644 --- a/src/main/java/com/cjburkey/claimchunk/Utils.java +++ b/src/main/java/com/cjburkey/claimchunk/Utils.java @@ -172,10 +172,7 @@ public static boolean hasAdmin(@Nullable CommandSender sender) { private static String prepMsg(String msg, Object... data) { // Prepare a safe console message - String out = (msg == null) ? "null" : msg; - - // Output with the ClaimChunk prefix - return "[ClaimChunk] " + color(String.format(out, data)); + return color(String.format((msg == null) ? "null" : msg, data)); } public static Map getDefaultPermissionsMap() { diff --git a/src/main/java/com/cjburkey/claimchunk/access/CCInteractClasses.java b/src/main/java/com/cjburkey/claimchunk/access/CCInteractClasses.java new file mode 100644 index 0000000..0ae6012 --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/access/CCInteractClasses.java @@ -0,0 +1,125 @@ +package com.cjburkey.claimchunk.access; + +import com.cjburkey.claimchunk.config.ClaimChunkWorldProfileHandler; + +import org.bukkit.Material; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +public final class CCInteractClasses { + + private final HashMap> classBlocks = new HashMap<>(); + private final HashMap> classEntities = new HashMap<>(); + + private final HashMap> blockClasses = new HashMap<>(); + private final HashMap> entityClasses = new HashMap<>(); + + public CCInteractClasses(boolean includeDefault) { + if (includeDefault) { + for (Map.Entry> classes : + ClaimChunkWorldProfileHandler.getDefaultBlockAccessClasses().entrySet()) { + addBlockClass(classes.getKey(), classes.getValue()); + } + for (Map.Entry> classes : + ClaimChunkWorldProfileHandler.getDefaultEntityAccessClasses().entrySet()) { + addEntityClass(classes.getKey(), classes.getValue()); + } + } + } + + public void addBlockClass(String className, Collection materials) { + classBlocks(className).addAll(materials); + materials.forEach(block -> blockClasses(block).add(className)); + } + + public void addBlockClass(String className, Material... materials) { + Collections.addAll(classBlocks(className), materials); + Arrays.stream(materials).forEach(block -> blockClasses(block).add(className)); + } + + public void addEntityClass(String className, Collection entities) { + classEntities(className).addAll(entities); + entities.forEach(entity -> entityClasses(entity).add(className)); + } + + public void addEntityClass(String className, EntityType... entities) { + Collections.addAll(classEntities(className), entities); + Arrays.stream(entities).forEach(entity -> entityClasses(entity).add(className)); + } + + // RETURNED SET IS UNMODIFIABLE! + public @NotNull Set getClassBlocks(String className) { + return classBlocks.containsKey(className) + ? Collections.unmodifiableSet(classBlocks(className)) + : Collections.emptySet(); + } + + // RETURNED SET IS UNMODIFIABLE! + public @NotNull Set getClassEntities(String className) { + return classEntities.containsKey(className) + ? Collections.unmodifiableSet(classEntities(className)) + : Collections.emptySet(); + } + + // RETURNED SET IS UNMODIFIABLE! + public @NotNull Set getBlockClasses(Material block) { + return blockClasses.containsKey(block) + ? Collections.unmodifiableSet(blockClasses(block)) + : Collections.emptySet(); + } + + // RETURNED SET IS UNMODIFIABLE! + public @NotNull Set getEntityClasses(EntityType entity) { + return entityClasses.containsKey(entity) + ? Collections.unmodifiableSet(entityClasses(entity)) + : Collections.emptySet(); + } + + public YamlConfiguration toYaml() { + YamlConfiguration config = new YamlConfiguration(); + config.options() + .setHeader( + Arrays.asList( + "Do not modify!", + "This file lists classes that may be used in the flag config file" + + " and world profiles", + "This file is reset every server launch! Changes will not be" + + " reflected in-game!")); + + // Write block classes + for (Map.Entry> entry : classBlocks.entrySet()) { + config.set( + "blockClasses." + entry.getKey(), + entry.getValue().stream().map(Material::name).collect(Collectors.toList())); + } + + // Write entity classes + for (Map.Entry> entry : classEntities.entrySet()) { + config.set( + "entityClasses." + entry.getKey(), + entry.getValue().stream().map(EntityType::name).collect(Collectors.toList())); + } + + return config; + } + + private @NotNull HashSet classBlocks(String className) { + return classBlocks.computeIfAbsent(className, ignoredKey -> new HashSet<>()); + } + + private @NotNull HashSet classEntities(String className) { + return classEntities.computeIfAbsent(className, ignoredKey -> new HashSet<>()); + } + + private @NotNull HashSet blockClasses(Material block) { + return blockClasses.computeIfAbsent(block, ignoredKey -> new HashSet<>()); + } + + private @NotNull HashSet entityClasses(EntityType entity) { + return entityClasses.computeIfAbsent(entity, ignoredKey -> new HashSet<>()); + } +} diff --git a/src/main/java/com/cjburkey/claimchunk/flags/PermFlags.java b/src/main/java/com/cjburkey/claimchunk/access/CCPermFlags.java similarity index 86% rename from src/main/java/com/cjburkey/claimchunk/flags/PermFlags.java rename to src/main/java/com/cjburkey/claimchunk/access/CCPermFlags.java index 3127123..6944202 100644 --- a/src/main/java/com/cjburkey/claimchunk/flags/PermFlags.java +++ b/src/main/java/com/cjburkey/claimchunk/access/CCPermFlags.java @@ -1,4 +1,4 @@ -package com.cjburkey.claimchunk.flags; +package com.cjburkey.claimchunk.access; import com.cjburkey.claimchunk.Utils; import com.google.common.base.Charsets; @@ -21,12 +21,22 @@ * * @since 0.0.26 */ -public class PermFlags { +public class CCPermFlags { public final HashMap blockControls = new HashMap<>(); public final HashMap entityControls = new HashMap<>(); - /** Read the flags defined in the flag definitions file. */ + /** + * Read the flags defined in the flag definitions file. + * + * @param flagsFile The file within the /plugins/ClaimChunk directory that stores the flag + * configurations. + * @param plugin An instance of the plugin whose jar contains the default flags resource + * (probably an instance of ClaimChunk, used to call {@link + * JavaPlugin#getResource(String)}). + * @param defaultFlagsResource The path to/name of the default flags resource in the plugin jar + * file. + */ public void load(File flagsFile, JavaPlugin plugin, String defaultFlagsResource) { // Load the flags.yml file while ensuring the default exists YamlConfiguration config = readFlagFile(flagsFile, plugin, defaultFlagsResource); @@ -37,6 +47,12 @@ public void load(File flagsFile, JavaPlugin plugin, String defaultFlagsResource) loadFromConfig(config); } + /** + * Load from the provided configuration data. The config should contain a section named + * `permissionFlags` containing the flags. + * + * @param config The config file from which to load the user-defined flags. + */ public void loadFromConfig(YamlConfiguration config) { // Read the flag section ConfigurationSection flagSection = config.getConfigurationSection("permissionFlags"); @@ -60,7 +76,7 @@ public void loadFromConfig(YamlConfiguration config) { if (interactType == null) { Utils.err( "Missing interaction type in one of the flag protection maps in flag" - + " \"%s\"", + + " \"%s\"", flagName); } @@ -89,7 +105,7 @@ public void loadFromConfig(YamlConfiguration config) { if (flagData == null) { Utils.err( "Failed to load flag includes/excludes from flag \"%s\" for" - + " block protections", + + " block protections", flagName); } @@ -120,7 +136,7 @@ public void loadFromConfig(YamlConfiguration config) { if (flagData == null) { Utils.err( "Failed to load flag includes/excludes from flag \"%s\" for" - + " entity protections", + + " entity protections", flagName); } @@ -128,9 +144,10 @@ public void loadFromConfig(YamlConfiguration config) { EntityFlagData entityFlagData = new EntityFlagData(flagType, flagData); entityControls.put(flagName, entityFlagData); } - default -> Utils.err( - "Invalid flag protection target \"%s\" for flag \"%s\"", - forType, flagName); + default -> + Utils.err( + "Invalid flag protection target \"%s\" for flag \"%s\"", + forType, flagName); } } diff --git a/src/main/java/com/cjburkey/claimchunk/config/ClaimChunkWorldProfileHandler.java b/src/main/java/com/cjburkey/claimchunk/config/ClaimChunkWorldProfileHandler.java index 82493ca..88f46fe 100644 --- a/src/main/java/com/cjburkey/claimchunk/config/ClaimChunkWorldProfileHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/config/ClaimChunkWorldProfileHandler.java @@ -281,10 +281,19 @@ public void unloadAllProfiles() { entityType.getEntityClass()))) .forEach(vehicles::add); + // Containers (Keep up to date? Need to work on this) + HashSet containers = new HashSet<>(); + Collections.addAll( + containers, + EntityType.CHEST_BOAT, + EntityType.CHEST_MINECART, + EntityType.HOPPER_MINECART); + entityAccessMapping.put("MONSTERS", monsters); entityAccessMapping.put("HANGING_ENTITIES", hangingEntities); entityAccessMapping.put("ANIMALS", animals); entityAccessMapping.put("VEHICLES", vehicles); + entityAccessMapping.put("CONTAINER_ENTITIES", containers); return entityAccessMapping; } diff --git a/src/main/resources/defaultFlags.yml b/src/main/resources/defaultFlags.yml deleted file mode 100644 index e9e62ad..0000000 --- a/src/main/resources/defaultFlags.yml +++ /dev/null @@ -1,60 +0,0 @@ -# List of flags that players can customize within their claims. - -permissionFlags: - breakBlocks: - # Blocks - - for: BLOCKS # BLOCKS or ENTITIES - type: BREAK # For blocks, can be BREAK, PLACE, INTERACT, or EXPLODE - # If no `include` or `exclude` is present, the default is to - # include all blocks/entities. - # If only `include` is present, only the provided entities/entity - # classes will be included. - # The opposite is true of `exclude`, which includes all - # default blocks/items and excludes the provided ones. - # If both are provided, then exclusions are considered after - # inclusions. - placeBlocks: - - for: BLOCKS - type: PLACE - interactBlocks: - - for: BLOCKS - type: INTERACT - exclude: ['@REDSTONE', '@DOORS', '@BLOCK_CONTAINERS'] # Handle these separately - redstone: - - for: BLOCKS - type: INTERACT - include: ['@REDSTONE'] - doors: - - for: BLOCKS - type: INTERACT - include: ['@DOORS'] - - # Entities - damageEntities: - - for: ENTITIES - type: DAMAGE - interactEntities: - - for: ENTITIES - type: INTERACT - exclude: ['@VEHICLES'] - vehicles: - - for: ENTITIES - type: INTERACT - include: ['@VEHICLES'] - - # Can also handle both types with one flag - containers: - - for: BLOCKS - type: INTERACT - include: ['@BLOCK_CONTAINERS'] - - for: ENTITIES - type: INTERACT - include: ['@VEHICLE_CONTAINERS'] - - # Explosions - explodeHurt: - - for: ENTITIES - type: EXPLODE - explodeDamage: - - for: BLOCKS - type: EXPLODE diff --git a/src/main/resources/flags.yml b/src/main/resources/flags.yml new file mode 100644 index 0000000..616ae94 --- /dev/null +++ b/src/main/resources/flags.yml @@ -0,0 +1,71 @@ +# This file contains a list of the valid permissions that players can +# enable/disable in their chunks to determine what other are allowed to do in +# their claimed chunk(s). +# +# When specifying included/excluded blocks and entities, you can use either the +# resource name (like `minecraft:dirt`, for example), the Spigot/Bukkit +# name (like `DIRT`), or predefined "classes" (like `@CONTAINER_ENTITIES`) +# +# Class references (preceded by `@`) reference the DEFAULT classes that appear +# at the top of newly generated world profile files (to make my life easier) +# (TODO: Move the definition of those classes to a separate file to make +# *everyone's* life easier) + +permissionFlags: + breakBlocks: + # Blocks + - for: BLOCKS # BLOCKS or ENTITIES + type: BREAK # For blocks, can be BREAK, PLACE, INTERACT, or EXPLODE + # If no `include` is present, the default is to include all + # blocks/entities. + # If only `include` is present, only the provided + # entities/entity classes will be included. + # Similarly, `exclude` includes all default blocks/items and + # excludes the provided ones. + # If both `include` and `exclude` are provided, then + # exclusions are considered after inclusions. + placeBlocks: + - for: BLOCKS + type: PLACE + interactBlocks: + - for: BLOCKS + type: INTERACT + exclude: ['@REDSTONE', '@DOORS', '@CONTAINER'] # Handle these separately + redstone: + - for: BLOCKS + type: INTERACT + include: ['@REDSTONE'] + doors: + - for: BLOCKS + type: INTERACT + include: ['@DOORS'] + + # Entities + damageEntities: + - for: ENTITIES + type: DAMAGE + interactEntities: + - for: ENTITIES + type: INTERACT + exclude: ['@VEHICLES', '@CONTAINER_ENTITIES'] + vehicles: + - for: ENTITIES + type: INTERACT + include: ['@VEHICLES'] + + # Can also handle both types with one flag + containers: + - for: BLOCKS + type: INTERACT + include: ['@CONTAINER'] + - for: ENTITIES + type: INTERACT + include: ['@CONTAINER_ENTITIES'] + + # Explosions + explodeHurt: + - for: ENTITIES + type: EXPLODE + explodeDamage: + - for: BLOCKS + type: EXPLODE diff --git a/src/test/java/com/cjburkey/claimchunk/CCConfigTests.java b/src/test/java/com/cjburkey/claimchunk/CCConfigTests.java index 06223b8..0e0f394 100644 --- a/src/test/java/com/cjburkey/claimchunk/CCConfigTests.java +++ b/src/test/java/com/cjburkey/claimchunk/CCConfigTests.java @@ -11,8 +11,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; class CCConfigTests { @@ -60,19 +58,25 @@ void testConfigToString() { // The expected output final String expected = - "# Example comment :)\n" - + "# A different comment\n\n" - + "bob:\n" - + " a_different_float 20.0 ;\n" - + " a_float 20.0 ;\n" - + " a_string this is my value :) ;\n" - + " an_int 10 ;\n\n" - + "bob.says:\n" - + " a_bool true ;\n\n" - + "jim.says.yells:\n" - + " a_bool false ;\n\n" - + "jim.yells.says:\n" - + " a_bool true ;\n"; + """ + # Example comment :) + # A different comment + + bob: + a_different_float 20.0 ; + a_float 20.0 ; + a_string this is my value :) ; + an_int 10 ; + + bob.says: + a_bool true ; + + jim.says.yells: + a_bool false ; + + jim.yells.says: + a_bool true ; + """; assertEquals(serializedConfig, expected); } @@ -80,17 +84,23 @@ void testConfigToString() { @Test void testConfigFromString() { final String input = - "\nbob:\n" - + " a_different_float 30.0 ;\n" - + " a_float 20.0 ;\n" - + " a_string this is my value :) ;\n" - + " an_i9nt 10 ;\n\n" - + "bob.says:\n" - + " a_bool true ;\n\n" - + "jim.says.yells:\n" - + " a_bool false ;\n\n" - + "jim.yells.says:\n" - + " a_bool true ;\n"; + """ + +bob: + a_different_float 30.0 ; + a_float 20.0 ; + a_string this is my value :) ; + an_i9nt 10 ; + +bob.says: + a_bool true ; + +jim.says.yells: + a_bool false ; + +jim.yells.says: + a_bool true ; +"""; // Initialize a config final CCConfig config = new CCConfig("", ""); @@ -98,10 +108,6 @@ void testConfigFromString() { // Parse the config and make sure there aren't any errors final List parseErrors = configParser.parse(config, input); - System.out.println( - parseErrors.stream() - .map(Objects::toString) - .collect(Collectors.joining(", ", "[ ", "] "))); assertEquals(new ArrayList<>(), parseErrors); // Ensure all the values were set correctly diff --git a/src/test/java/com/cjburkey/claimchunk/InteractClassTests.java b/src/test/java/com/cjburkey/claimchunk/InteractClassTests.java new file mode 100644 index 0000000..ff498ce --- /dev/null +++ b/src/test/java/com/cjburkey/claimchunk/InteractClassTests.java @@ -0,0 +1,38 @@ +package com.cjburkey.claimchunk; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.cjburkey.claimchunk.access.CCInteractClasses; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Objects; + +@SuppressWarnings("unchecked") +public class InteractClassTests { + + @Test + void testToYml() { + CCInteractClasses classes = new CCInteractClasses(false); + classes.addEntityClass("VEHICLES", EntityType.MINECART, EntityType.FURNACE_MINECART); + + YamlConfiguration yaml = classes.toYaml(); + List entities = + (List) Objects.requireNonNull(yaml.getList("entityClasses.VEHICLES")); + assertEquals(2, entities.size()); + assert entities.contains("MINECART") && entities.contains("FURNACE_MINECART"); + } + + @Test + void testDefaults() { + CCInteractClasses classes = new CCInteractClasses(true); + YamlConfiguration yaml = classes.toYaml(); + List entities = + (List) + Objects.requireNonNull(yaml.getList("entityClasses.CONTAINER_ENTITIES")); + assert entities.contains("CHEST_MINECART"); + } +} diff --git a/src/test/java/com/cjburkey/claimchunk/PermFlagTests.java b/src/test/java/com/cjburkey/claimchunk/PermFlagTests.java index ad21f89..c25e0af 100644 --- a/src/test/java/com/cjburkey/claimchunk/PermFlagTests.java +++ b/src/test/java/com/cjburkey/claimchunk/PermFlagTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import com.cjburkey.claimchunk.flags.PermFlags; +import com.cjburkey.claimchunk.access.CCPermFlags; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; @@ -30,14 +30,14 @@ void testLoadFlags() { type: INTERACT include: ['@REDSTONE'] """)); - PermFlags permFlags = new PermFlags(); + CCPermFlags permFlags = new CCPermFlags(); permFlags.loadFromConfig(config); - PermFlags.BlockFlagData breakBlocks = permFlags.blockControls.get("breakBlocks"); - assertEquals(breakBlocks.flagType(), PermFlags.BlockFlagType.BREAK); + CCPermFlags.BlockFlagData breakBlocks = permFlags.blockControls.get("breakBlocks"); + assertEquals(breakBlocks.flagType(), CCPermFlags.BlockFlagType.BREAK); - PermFlags.EntityFlagData damageEntities = permFlags.entityControls.get("damageEntities"); - assertEquals(damageEntities.flagType(), PermFlags.EntityFlagType.DAMAGE); + CCPermFlags.EntityFlagData damageEntities = permFlags.entityControls.get("damageEntities"); + assertEquals(damageEntities.flagType(), CCPermFlags.EntityFlagType.DAMAGE); assert Objects.requireNonNull( permFlags.blockControls.get("redstone").flagData().include(), @@ -58,7 +58,7 @@ void testTwo() { - for: BLOCKS type: BREAK """)); - PermFlags permFlags = new PermFlags(); + CCPermFlags permFlags = new CCPermFlags(); permFlags.loadFromConfig(config); assert permFlags.blockControls.containsKey("ruinStuff");