diff --git a/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/skript/event/EvtCustomClick.java b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/skript/event/EvtCustomClick.java
index 68bcdb48b..8c37c2a04 100644
--- a/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/skript/event/EvtCustomClick.java
+++ b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/skript/event/EvtCustomClick.java
@@ -22,6 +22,7 @@ public class EvtCustomClick extends SkriptEvent {
private final static int RIGHT = 1, LEFT = 2, ANY = RIGHT | LEFT;
public final static ClickEventTracker interactTracker = new ClickEventTracker(Skript.getInstance());
+ @SuppressWarnings("unchecked")
public static void register() {
Skript.registerEvent("Interact Custom Block Furniture", EvtCustomClick.class, new Class[]{CustomBlockInteractEvent.class, FurnitureInteractEvent.class},
"[(" + RIGHT + ":right|" + LEFT + ":left)(| |-)][mouse(| |-)]click[ing] [on %-unsafeblockstatematchers/strings%] [(with|using|holding) %-itemtype%]",
diff --git a/bukkit/loader/src/main/resources/translations/en.yml b/bukkit/loader/src/main/resources/translations/en.yml
index 7937c5b2b..546101339 100644
--- a/bukkit/loader/src/main/resources/translations/en.yml
+++ b/bukkit/loader/src/main/resources/translations/en.yml
@@ -67,7 +67,7 @@ warning.config.not_a_section: "Issue found in file - '.Issue found in file - Duplicated image ''."
warning.config.image.lack_height: "Issue found in file - The image '' is missing the required 'height' argument."
warning.config.image.height_smaller_than_ascent: "Issue found in file - The image '' violates the bitmap image rule: 'height' should be no lower than 'ascent'."
-warning.config.image.no_file: "Issue found in file - The image '' is missing the required 'file' argument."
+warning.config.image.lack_file: "Issue found in file - The image '' is missing the required 'file' argument."
warning.config.image.invalid_resource_location: "Issue found in file - The image '' has a 'file' argument [] that contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters"
warning.config.image.invalid_font_name: "Issue found in file - The image '' has a 'font' argument [] that contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters"
warning.config.image.lack_char: "Issue found in file - The image '' is missing the required 'char' argument."
@@ -81,6 +81,7 @@ warning.config.vanilla_loot.type_not_exist: "Issue found in file
warning.config.vanilla_loot.block.invalid_target: "Issue found in file - Invalid block target [] in vanilla loot ''."
warning.config.sound.duplicated: "Issue found in file - Duplicated sound ''."
warning.config.jukebox_song.duplicated: "Issue found in file - Duplicated jukebox song ''."
+warning.config.jukebox_song.lack_sound: "Issue found in file - The jukebox song '' is missing the required 'sound' argument."
warning.config.furniture.duplicated: "Issue found in file - Duplicated furniture ''."
warning.config.furniture.lack_placement: "Issue found in file - The furniture '' is missing the required 'placement' argument."
warning.config.furniture.element.lack_item: "Issue found in file - The furniture '' is missing the required 'item' argument for one of its elements."
@@ -93,6 +94,22 @@ warning.config.item.invalid_material: "Issue found in file - The
warning.config.item.bad_custom_model_data_value: "Issue found in file - The item '' is using a custom model data [] that is too large. It's recommended to use a value lower than 16,777,216."
warning.config.item.custom_model_data_conflict: "Issue found in file - The item '' is using a custom model data [] that has been occupied by item ''"
warning.config.item.lack_model_id: "Issue found in file - The item '' is missing the required 'custom-model-data' or 'item-model' argument."
+warning.config.item.behavior.lack_type: "Issue found in file - The item '' is missing the required 'type' argument for its item behavior."
+warning.config.item.behavior.invalid_type: "Issue found in file - The item '' is using an invalid item behavior type ''."
+warning.config.item.behavior.block.lack_block: "Issue found in file - The item '' is missing the required 'block' argument for 'block_item' behavior."
+warning.config.item.behavior.furniture.lack_furniture: "Issue found in file - The item '' is missing the required 'furniture' argument for 'furniture_item' behavior."
+warning.config.item.behavior.liquid_collision_block.lack_block: "Issue found in file - The item '' is missing the required 'block' argument for 'liquid_collision_block_item' behavior."
+warning.config.item.model.invalid_type: "Issue found in file - The item '' is using an invalid model type ''."
+warning.config.item.model.base.lack_path: "Issue found in file - The item '' is missing the required 'path' argument for 'minecraft:model'."
+warning.config.item.model.base.invalid_resource_location: "Issue found in file - The item '' has an invalid 'path' argument [] for 'minecraft:model' which contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters."
+warning.config.item.model.condition.lack_on_true: "Issue found in file - The item '' is missing the required 'on-true' argument for 'minecraft:condition'."
+warning.config.item.model.condition.lack_on_false: "Issue found in file - The item '' is missing the required 'on-false' argument for 'minecraft:condition'."
+warning.config.item.model.composite.lack_models: "Issue found in file - The item '' is missing the required 'models' argument for 'minecraft:composite'."
+warning.config.item.model.range_dispatch.lack_entries: "Issue found in file - The item '' is missing the required 'entries' argument for 'minecraft:composite'."
+warning.config.item.model.range_dispatch.entry.lack_model: "Issue found in file - The item '' is missing the required 'model' argument for one of the entries in 'minecraft:composite'."
+warning.config.item.model.select.lack_cases: "Issue found in file - The item '' is missing the required 'cases' argument for 'minecraft:select'."
+warning.config.item.model.select.case.lack_when: "Issue found in file - The item '' is missing the required 'when' argument for one of the cases in 'minecraft:select'."
+warning.config.item.model.select.case.lack_model: "Issue found in file - The item '' is missing the required 'model' argument for one of the cases in 'minecraft:select'."
warning.config.block.duplicated: "Issue found in file - Duplicated block ''."
warning.config.block.lack_state: "Issue found in file - The block '' is missing the required 'state' argument."
warning.config.block.state.lack_real_id: "Issue found in file - The block '' is missing the required 'id' argument for 'state'."
@@ -113,6 +130,18 @@ warning.config.block.state.no_model_set: "Issue found in file -
warning.config.block.state.invalid_real_state_id: "Issue found in file - The block '' is using a real block state '' that exceeds the available slot range '0~'. Consider adding more real states in 'additional-real-blocks.yml' if the slots are used up."
warning.config.block.state.model.lack_path: "Issue found in file - The block '' is missing the required 'path' option for 'model'."
warning.config.block.state.model.invalid_resource_location: "Issue found in file - The block '' has a 'path' argument [] that contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters"
+warning.config.block.settings.unknown: "Issue found in file - The block '' is using an unknown setting type ''."
+warning.config.block.behavior.lack_type: "Issue found in file - The block '' is missing the required 'type' argument for its block behavior."
+warning.config.block.behavior.invalid_type: "Issue found in file - The block '' is using an invalid block behavior type ''."
+warning.config.block.behavior.concrete.lack_solid_block: "Issue found in file - The block '' is missing the required 'solid-block' option for 'concrete_block' behavior."
+warning.config.block.behavior.crop.lack_age: "Issue found in file - The block '' is missing the required 'age' property for 'crop_block' behavior."
+warning.config.block.behavior.sugar_cane.lack_age: "Issue found in file - The block '' is missing the required 'age' property for 'sugar_cane_block' behavior."
+warning.config.block.behavior.leaves.lack_persistent: "Issue found in file - The block '' is missing the required 'persistent' property for 'leaves_block' behavior."
+warning.config.block.behavior.leaves.lack_distance: "Issue found in file - The block '' is missing the required 'distance' property for 'leaves_block' behavior."
+warning.config.block.behavior.sapling.lack_stage: "Issue found in file - The block '' is missing the required 'stage' property for 'sapling_block' behavior."
+warning.config.block.behavior.sapling.lack_feature: "Issue found in file - The block '' is missing the required 'feature' argument for 'sapling_block' behavior."
+warning.config.block.behavior.strippable.lack_stripped: "Issue found in file - The block '' is missing the required 'stripped' argument for 'strippable_block' behavior."
+warning.config.model.generation.lack_parent: "Issue found in file - The config '' is missing the required 'parent' argument in 'generation' section."
warning.config.model.generation.conflict: "Issue found in file - Failed to generate model for '' as two or more configurations attempt to generate different json models with the same path: ''"
warning.config.model.generation.texture.invalid_resource_location: "Issue found in file - The config '' has a '' texture argument [] that contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters"
warning.config.model.generation.parent.invalid_resource_location: "Issue found in file - The config '' has a parent argument [] that contains legal characters. Please read https://minecraft.wiki/w/Resource_location#Legal_characters"
@@ -162,4 +191,6 @@ warning.config.loot_table.entry.invalid_type: "Issue found in file Issue found in file - '' has a misconfigured loot table, entry 'exp' is missing the required argument 'count'."
warning.config.loot_table.entry.item.lack_item: "Issue found in file - '' has a misconfigured loot table, entry 'item' is missing the required argument 'item'."
warning.config.loot_table.condition.lack_type: "Issue found in file - '' has a misconfigured loot table, one of the conditions is missing the required argument 'type'."
-warning.config.loot_table.condition.invalid_type: "Issue found in file - '' has a misconfigured loot table, one of the conditions is using an invalid condition type ''."
\ No newline at end of file
+warning.config.loot_table.condition.invalid_type: "Issue found in file - '' has a misconfigured loot table, one of the conditions is using an invalid condition type ''."
+warning.config.loot_table.number.lack_type: "Issue found in file - '' has a misconfigured loot table, one of the numbers is missing the required argument 'type'."
+warning.config.loot_table.number.invalid_type: "Issue found in file - '' has a misconfigured loot table, one of the numbers is using an invalid number type ''."
\ No newline at end of file
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
index 42f8e9c4d..4660f3744 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
@@ -26,6 +26,7 @@ import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigSectionParser;
+import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
@@ -331,13 +332,28 @@ public class BukkitBlockManager extends AbstractBlockManager {
public void parseSection(Pack pack, Path path, Key id, Map section) {
// check duplicated config
if (byId.containsKey(id)) {
- TranslationManager.instance().log("warning.config.block.duplicated", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.duplicated", path, id);
}
// read block settings
- BlockSettings settings = BlockSettings.fromMap(MiscUtils.castToMap(section.getOrDefault("settings", Map.of()), false));
+ BlockSettings settings;
+ try {
+ settings = BlockSettings.fromMap(MiscUtils.castToMap(section.getOrDefault("settings", Map.of()), false));
+ } catch (LocalizedResourceConfigException e) {
+ e.setPath(path);
+ e.setId(id);
+ throw e;
+ }
+
// read loot table
- LootTable lootTable = LootTable.fromMap(MiscUtils.castToMap(section.getOrDefault("loot", Map.of()), false));
+ LootTable lootTable;
+ try {
+ lootTable = LootTable.fromMap(MiscUtils.castToMap(section.getOrDefault("loot", Map.of()), false));
+ } catch (LocalizedResourceConfigException e) {
+ e.setPath(path);
+ e.setId(id);
+ throw e;
+ }
+
// read states
Map> properties;
Map appearances;
@@ -347,8 +363,7 @@ public class BukkitBlockManager extends AbstractBlockManager {
properties = Map.of();
int internalId = MiscUtils.getAsInt(stateSection.getOrDefault("id", -1));
if (internalId < 0) {
- TranslationManager.instance().log("warning.config.block.state.lack_real_id", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.lack_real_id", path, id);
}
Pair pair = parseAppearanceSection(pack, path, id, stateSection);
@@ -358,34 +373,27 @@ public class BukkitBlockManager extends AbstractBlockManager {
Key internalBlockId = Key.of(CraftEngine.NAMESPACE, pair.left().value() + "_" + internalId);
int internalBlockRegistryId = MiscUtils.getAsInt(internalId2StateId.getOrDefault(internalBlockId, -1));
if (internalBlockRegistryId == -1) {
- TranslationManager.instance().log("warning.config.block.state.invalid_real_state_id",
- path.toString(),
- id.toString(),
+ throw new LocalizedResourceConfigException("warning.config.block.state.invalid_real_state_id", path, id,
pair.left().value() + "_" + internalId,
- String.valueOf(MiscUtils.getAsInt(registeredRealBlockSlots.get(pair.left()))-1)
- );
- return;
+ String.valueOf(MiscUtils.getAsInt(registeredRealBlockSlots.get(pair.left()))-1));
}
variants = Map.of("", new VariantState("default", settings, internalBlockRegistryId));
} else {
// states
Map statesSection = MiscUtils.castToMap(section.get("states"), true);
if (statesSection == null) {
- TranslationManager.instance().log("warning.config.block.lack_state", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.lack_state", path, id);
}
// properties
Map propertySection = MiscUtils.castToMap(statesSection.get("properties"), true);
if (propertySection == null) {
- TranslationManager.instance().log("warning.config.block.state.lack_properties", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.lack_properties", path, id);
}
properties = parseProperties(path, id, propertySection);
// appearance
Map appearancesSection = MiscUtils.castToMap(statesSection.get("appearances"), true);
if (appearancesSection == null) {
- TranslationManager.instance().log("warning.config.block.state.lack_appearances", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.lack_appearances", path, id);
}
appearances = new HashMap<>();
Map tempTypeMap = new HashMap<>();
@@ -400,8 +408,7 @@ public class BukkitBlockManager extends AbstractBlockManager {
// variants
Map variantsSection = MiscUtils.castToMap(statesSection.get("variants"), true);
if (variantsSection == null) {
- TranslationManager.instance().log("warning.config.block.state.lack_variants", path.toString(), id.toString());
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.lack_variants", path, id);
}
variants = new HashMap<>();
for (Map.Entry variantEntry : variantsSection.entrySet()) {
@@ -410,25 +417,19 @@ public class BukkitBlockManager extends AbstractBlockManager {
String variantName = variantEntry.getKey();
String appearance = (String) variantSection.get("appearance");
if (appearance == null) {
- TranslationManager.instance().log("warning.config.block.state.variant.lack_appearance", path.toString(), id.toString(), variantName);
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.variant.lack_appearance", path, id, variantName);
}
if (!appearances.containsKey(appearance)) {
- TranslationManager.instance().log("warning.config.block.state.variant.invalid_appearance", path.toString(), id.toString(), variantName, appearance);
- return;
+ throw new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", path, id, variantName, appearance);
}
int internalId = MiscUtils.getAsInt(variantSection.getOrDefault("id", -1));
Key baseBlock = tempTypeMap.get(appearance);
Key internalBlockId = Key.of(CraftEngine.NAMESPACE, baseBlock.value() + "_" + internalId);
int internalBlockRegistryId = MiscUtils.getAsInt(internalId2StateId.getOrDefault(internalBlockId, -1));
if (internalBlockRegistryId == -1) {
- TranslationManager.instance().log("warning.config.block.state.invalid_real_state_id",
- path.toString(),
- id.toString(),
+ throw new LocalizedResourceConfigException("warning.config.block.state.invalid_real_state_id", path, id,
internalBlockId.toString(),
- String.valueOf(MiscUtils.getAsInt(registeredRealBlockSlots.getOrDefault(baseBlock, 1)) - 1)
- );
- return;
+ String.valueOf(MiscUtils.getAsInt(registeredRealBlockSlots.getOrDefault(baseBlock, 1)) - 1));
}
Map anotherSetting = MiscUtils.castToMap(variantSection.get("settings"), true);
variants.put(variantName, new VariantState(appearance, anotherSetting == null ? settings : BlockSettings.ofFullCopy(settings, anotherSetting), internalBlockRegistryId));
@@ -436,12 +437,23 @@ public class BukkitBlockManager extends AbstractBlockManager {
}
}
- // create or get block holder
- Holder.Reference holder = BuiltInRegistries.BLOCK.get(id).orElseGet(() ->
- ((WritableRegistry) BuiltInRegistries.BLOCK).registerForHolder(new ResourceKey<>(BuiltInRegistries.BLOCK.key().location(), id)));
- // create block
- Map behaviorSection = MiscUtils.castToMap(section.getOrDefault("behavior", Map.of()), false);
- BukkitCustomBlock block = new BukkitCustomBlock(id, holder, properties, appearances, variants, settings, behaviorSection, lootTable);
+ Map behaviors = MiscUtils.castToMap(section.getOrDefault("behavior", Map.of()), false);
+
+ CustomBlock block;
+ try {
+ block = BukkitCustomBlock.builder(id)
+ .appearances(appearances)
+ .variantMapper(variants)
+ .lootTable(lootTable)
+ .properties(properties)
+ .settings(settings)
+ .behavior(behaviors)
+ .build();
+ } catch (LocalizedResourceConfigException e) {
+ e.setPath(path);
+ e.setId(id);
+ throw e;
+ }
// bind appearance and real state
for (ImmutableBlockState state : block.variantProvider().states()) {
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitCustomBlock.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitCustomBlock.java
index 2bb530a75..209e34662 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitCustomBlock.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitCustomBlock.java
@@ -9,8 +9,11 @@ import net.momirealms.craftengine.core.block.*;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.plugin.CraftEngine;
+import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
+import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.Key;
+import net.momirealms.craftengine.core.util.ResourceKey;
import net.momirealms.craftengine.core.util.Tristate;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.shared.ObjectHolder;
@@ -25,17 +28,17 @@ import java.util.Set;
public class BukkitCustomBlock extends CustomBlock {
- public BukkitCustomBlock(
+ protected BukkitCustomBlock(
Key id,
Holder.Reference holder,
Map> properties,
Map appearances,
Map variantMapper,
BlockSettings settings,
- Map behaviorSettings,
+ Map behavior,
@Nullable LootTable> lootTable
) {
- super(id, holder, properties, appearances, variantMapper, settings, behaviorSettings, lootTable);
+ super(id, holder, properties, appearances, variantMapper, settings, behavior, lootTable);
}
@SuppressWarnings("unchecked")
@@ -139,4 +142,23 @@ public class BukkitCustomBlock extends CustomBlock {
CraftEngine.instance().logger().warn("Failed to init block settings", e);
}
}
+
+ public static Builder builder(Key id) {
+ return new Builder(id);
+ }
+
+ public static class Builder extends CustomBlock.Builder {
+
+ protected Builder(Key id) {
+ super(id);
+ }
+
+ @Override
+ public CustomBlock build() {
+ // create or get block holder
+ Holder.Reference holder = BuiltInRegistries.BLOCK.get(id).orElseGet(() ->
+ ((WritableRegistry) BuiltInRegistries.BLOCK).registerForHolder(new ResourceKey<>(BuiltInRegistries.BLOCK.key().location(), id)));
+ return new BukkitCustomBlock(id, holder, properties, appearances, variantMapper, settings, behavior, lootTable);
+ }
+ }
}
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/ConcretePowderBlockBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/ConcretePowderBlockBehavior.java
index 6f8cd8f8b..0ab2e2870 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/ConcretePowderBlockBehavior.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/ConcretePowderBlockBehavior.java
@@ -13,6 +13,7 @@ import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
import net.momirealms.craftengine.core.plugin.CraftEngine;
+import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
@@ -159,7 +160,7 @@ public class ConcretePowderBlockBehavior extends FallingBlockBehavior {
int hurtMax = MiscUtils.getAsInt(arguments.getOrDefault("max-hurt", -1));
String solidBlock = (String) arguments.get("solid-block");
if (solidBlock == null) {
- throw new IllegalArgumentException("No `solid-block` specified for concrete powder block behavior");
+ throw new LocalizedResourceConfigException("warning.config.block.behavior.concrete.lack_solid_block", new NullPointerException("No `solid-block` specified for concrete powder block behavior"));
}
return new ConcretePowderBlockBehavior(block, hurtAmount, hurtMax, Key.of(solidBlock));
}
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/CropBlockBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/CropBlockBehavior.java
index 1141f6815..ddf2a9bae 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/CropBlockBehavior.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/behavior/CropBlockBehavior.java
@@ -16,6 +16,7 @@ import net.momirealms.craftengine.core.loot.LootContext;
import net.momirealms.craftengine.core.loot.number.NumberProvider;
import net.momirealms.craftengine.core.loot.number.NumberProviders;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
+import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.RandomUtils;
import net.momirealms.craftengine.core.util.Tuple;
@@ -170,7 +171,7 @@ public class CropBlockBehavior extends BushBlockBehavior {
Tuple, Set