diff --git a/.editorconfig b/.editorconfig index 9887763d..9caa7239 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,14 +12,47 @@ end_of_line = lf insert_final_newline = true continuation_indent_size = 8 +ij_continuation_indent_size = 4 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = false +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 1 +ij_java_blank_lines_before_imports = 1 +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_imports_layout = $*,|,* +ij_java_layout_static_imports_separately = true +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_use_single_class_imports = true + [*.java] indent_style = tab indent_size = 4 +ij_java_imports_layout = $*,|,* -[*.xml] -indent_style = tab -indent_size = 2 - -[*.yml] +[*.{xml, yml}] indent_style = space indent_size = 2 + +[*.csv] +insert_final_newline = false + +[*.md] +trim_trailing_whitespace = false + +[*.{cmd,bat}] +end_of_line = crlf + +[*.sh] +end_of_line = lf + + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false diff --git a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java new file mode 100644 index 00000000..c930c70d --- /dev/null +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java @@ -0,0 +1,106 @@ +package com.programmerdan.minecraft.simpleadminhacks.configs; + +import com.programmerdan.minecraft.simpleadminhacks.SimpleAdminHacks; +import com.programmerdan.minecraft.simpleadminhacks.framework.SimpleHackConfig; +import org.bukkit.configuration.ConfigurationSection; + + +/** + * Create by psygate (github.com/psygate), 2022 + */ +public class BetterVehiclesConfig extends SimpleHackConfig { + + public enum GarbageCollectVehicleStrategy { + DROP_ITEMSTACK, + REMOVE; + } + + private boolean returnVehiclesToInventoryOnExit = true; + private boolean garbageCollectVehicles = true; + private GarbageCollectVehicleStrategy garbageCollectVehicleStrategy; + private long maxVehicleAgeInSeconds = 10; + private boolean vehicleNotifyOnRemoval = true; + + private long gcIntervalInTicks = 20 * 5; + private String persistenceFilePath; + private long flushRecordIntervalInTicks = 10; + + public BetterVehiclesConfig(SimpleAdminHacks plugin, ConfigurationSection base) { + super(plugin, base); + } + + @Override + protected void wireup(ConfigurationSection config) { + returnVehiclesToInventoryOnExit = config.getBoolean("return_vehicle_to_inventory_on_exit"); + garbageCollectVehicles = config.getBoolean("garbage_collect_vehicle"); + garbageCollectVehicleStrategy = GarbageCollectVehicleStrategy.valueOf(config.getString("garbage_collect_vehicle_strategy").toUpperCase()); + maxVehicleAgeInSeconds = config.getLong("max_boat_age_in_seconds"); + gcIntervalInTicks = config.getLong("gc_interval_in_ticks"); + persistenceFilePath = config.getString("persistence_file_path"); + flushRecordIntervalInTicks = config.getLong("flush_record_interval_in_seconds"); + } + + public boolean isReturnVehiclesToInventoryOnExit() { + return returnVehiclesToInventoryOnExit; + } + + public void setReturnVehiclesToInventoryOnExit(boolean returnVehiclesToInventoryOnExit) { + this.returnVehiclesToInventoryOnExit = returnVehiclesToInventoryOnExit; + } + + public boolean isGarbageCollectVehicles() { + return garbageCollectVehicles; + } + + public void setGarbageCollectVehicles(boolean garbageCollectVehicles) { + this.garbageCollectVehicles = garbageCollectVehicles; + } + + public GarbageCollectVehicleStrategy getGarbageCollectVehicleStrategy() { + return garbageCollectVehicleStrategy; + } + + public void setGarbageCollectVehicleStrategy(GarbageCollectVehicleStrategy garbageCollectVehicleStrategy) { + this.garbageCollectVehicleStrategy = garbageCollectVehicleStrategy; + } + + public long getMaxVehicleAgeInSeconds() { + return maxVehicleAgeInSeconds; + } + + public void setMaxVehicleAgeInSeconds(long maxVehicleAgeInSeconds) { + this.maxVehicleAgeInSeconds = maxVehicleAgeInSeconds; + } + + public boolean isVehicleNotifyOnRemoval() { + return vehicleNotifyOnRemoval; + } + + public void setVehicleNotifyOnRemoval(boolean vehicleNotifyOnRemoval) { + this.vehicleNotifyOnRemoval = vehicleNotifyOnRemoval; + } + + public long getGcIntervalInTicks() { + return gcIntervalInTicks; + } + + public void setGcIntervalInTicks(long gcIntervalInTicks) { + this.gcIntervalInTicks = gcIntervalInTicks; + } + + public String getPersistenceFilePath() { + return persistenceFilePath; + } + + public void setPersistenceFilePath(String persistenceFilePath) { + this.persistenceFilePath = persistenceFilePath; + } + + public long getFlushRecordIntervalInTicks() { + return flushRecordIntervalInTicks; + } + + public void setFlushRecordIntervalInTicks(long flushRecordIntervalInTicks) { + this.flushRecordIntervalInTicks = flushRecordIntervalInTicks; + } +} diff --git a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java new file mode 100644 index 00000000..8cc6d360 --- /dev/null +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java @@ -0,0 +1,517 @@ +package com.programmerdan.minecraft.simpleadminhacks.hacks; + +import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.programmerdan.minecraft.simpleadminhacks.SimpleAdminHacks; +import com.programmerdan.minecraft.simpleadminhacks.configs.BetterVehiclesConfig; +import com.programmerdan.minecraft.simpleadminhacks.framework.SimpleHack; +import net.kyori.adventure.text.Component; +import org.bukkit.*; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Boat; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Minecart; +import org.bukkit.entity.Player; +import org.bukkit.entity.Vehicle; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityPlaceEvent; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.event.vehicle.VehicleExitEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkPopulateEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.event.world.EntitiesLoadEvent; +import org.bukkit.inventory.ItemStack; +import org.spigotmc.event.entity.EntityMountEvent; + +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +/** + * Create by psygate (github.com/psygate), 2022 + */ +public class BetterVehicles extends SimpleHack { + + public static final String NAME = "BetterVehicles"; + + private final Map trackedEntities = new HashMap<>(); + + private static final class TrackedEntity { + + private final UUID id; + private long touched; + private UUID owner; + + public TrackedEntity(Entity entity, UUID owner) { + this(entity.getUniqueId(), System.currentTimeMillis(), owner); + } + + public TrackedEntity(Entity entity) { + this(entity.getUniqueId(), System.currentTimeMillis(), null); + } + + public TrackedEntity(UUID id, long touched, UUID owner) { + this.id = id; + this.touched = touched; + this.owner = owner; + } + + public UUID id() { + return id; + } + + public long touched() { + return touched; + } + + public Optional owner() { + if (owner == null) { + return Optional.empty(); + } else { + return Optional.of(owner); + } + } + + public void setOwner(UUID owner) { + this.owner = owner; + } + + public Entity toEntity() { + return Bukkit.getEntity(id); + } + + public void touch() { + touched = System.currentTimeMillis(); + } + + private static TrackedEntity of(TrackedEntity en, Player owner) { + return new TrackedEntity(Objects.requireNonNull(en).id(), System.currentTimeMillis(), Objects.requireNonNull(owner).getUniqueId()); + } + + public static TrackedEntity of(Entity en, Player owner) { + return new TrackedEntity(Objects.requireNonNull(en).getUniqueId(), System.currentTimeMillis(), Objects.requireNonNull(owner).getUniqueId()); + } + + public static TrackedEntity of(Entity en) { + return new TrackedEntity(Objects.requireNonNull(en).getUniqueId(), System.currentTimeMillis(), null); + } + } + + public BetterVehicles(SimpleAdminHacks plugin, BetterVehiclesConfig config) { + super(plugin, config); + } + + private void addListener(Listener listener) { + Bukkit.getServer().getPluginManager().registerEvents(listener, plugin()); + } + + private static void debugLog(String format, Object... values) { +// var msg = String.format(format, values); +// System.out.println(msg); +// Bukkit.broadcast(Component.text(msg)); + } + + /** + * Sets up the tracking for an entity, adding it to the tracked map. This will NOT set an owner on the entity. + * This action is idempotent and will not add the entitiy if it is already tracked. + * + * @param e Entity to track, cannot be null. + */ + private void setupEntityTracking(Entity e) { + debugLog("Adding entity: %s (%d, %d, %d, %s)\n", e.getType(), e.getLocation().getBlockX(), e.getLocation().getBlockY(), e.getLocation().getBlockZ(), e.getLocation().getWorld().getName()); + + trackedEntities.put(Objects.requireNonNull(e).getUniqueId(), TrackedEntity.of(e)); + } + + /** + * Sets up the tracking for an entity, adding it to the tracked map. This will set an owner on the entity. + * This action is idempotent and will not add the entitiy if it is already tracked. + * + * @param e Entity to track, cannot be null. + * @param owner Owner of the entity, cannot be null. + */ + private void setupEntityTracking(Entity e, Player owner) { + debugLog("Adding entity: %s (%d, %d, %d, %s) - %s\n", e.getType(), e.getLocation().getBlockX(), e.getLocation().getBlockY(), e.getLocation().getBlockZ(), e.getLocation().getWorld().getName(), owner); + + trackedEntities.put(Objects.requireNonNull(e).getUniqueId(), TrackedEntity.of(e, owner)); + } + + /** + * Return true if this entity type is tracked by this hack, false if not. + * + * @param entity Entity to check if tracked. Cannot be null. + * @return True if the entity is a tracked type. + */ + private static boolean isTrackedEntity(Entity entity) { + switch (Objects.requireNonNull(entity).getType()) { + case BOAT: + case MINECART: + return true; + default: + return false; + } + } + + /** + * Setup tracking or update tracking, depending if the tracking record already exists. If it does, the owner + * will be updated. + * + * @param e Entity to setup or update tracking on. Cannot be null. + * @param owner Owner to set / update. Cannot be null. + */ + private void setupOrUpdateEntityTracking(Entity e, Player owner) { + debugLog("Updating entity: %s (%d, %d, %d, %s)\n", e.getType(), e.getLocation().getBlockX(), e.getLocation().getBlockY(), e.getLocation().getBlockZ(), e.getLocation().getWorld().getName()); + + Objects.requireNonNull(e); + Objects.requireNonNull(owner); + trackedEntities.compute(e.getUniqueId(), (key, value) -> { + if (value == null) { + return TrackedEntity.of(e, owner); + } else { + value.setOwner(owner.getUniqueId()); + return value; + } + }); + } + + private void indexCurrentlyLoadedEntities() { + Bukkit.getWorlds().stream().map(World::getEntities).flatMap(Collection::stream).filter(BetterVehicles::isTrackedEntity).forEach(this::setupEntityTracking); + } + + private void indexChunkEntitiesOnEvent(List entities) { + entities.stream().filter(BetterVehicles::isTrackedEntity).forEach(BetterVehicles.this::setupEntityTracking); + } + + /** + * Garbage collect entities from the list of entities. + * + * @param entities Entities to check if they have expired. + */ + private void garbageCollectEntitiesOnChunkLoad(List entities) { + long starttime = System.currentTimeMillis(); + + var it = entities.iterator(); + while (it.hasNext()) { + var en = it.next(); + trackedEntities.computeIfPresent(en.getUniqueId(), (key, value) -> { + if (evictByAge(value, starttime)) { + it.remove(); + return null; + } else { + return value; + } + }); + } + } + + /** + * Register the basic listeners that are always used. + */ + private void addBaseListeners() { + addListener(new Listener() { + @EventHandler + public void onEntityLoad(EntitiesLoadEvent ev) { + indexChunkEntitiesOnEvent(ev.getEntities()); + } + + @EventHandler + public void onEntitySpawn(EntitySpawnEvent ev) { + if (isTrackedEntity(ev.getEntity())) { + debugLog("Spawn: %s", ev.getEntityType()); + setupEntityTracking(ev.getEntity()); + } + } + + @EventHandler + public void onPlayerPlaceEntity(EntityPlaceEvent ev) { + if (isTrackedEntity(ev.getEntity())) { + debugLog("Place: %s", ev.getEntityType()); + setupEntityTracking(ev.getEntity(), ev.getPlayer()); + } + } + + @EventHandler + public void onPlayerMount(EntityMountEvent ev) { + if (isTrackedEntity(ev.getEntity()) && ev.getEntity() instanceof Player) { + debugLog("Mount: %s", ev.getEntityType()); + setupOrUpdateEntityTracking(ev.getMount(), (Player) ev.getEntity()); + } + } + + @EventHandler + public void onEntityRemove(EntityRemoveFromWorldEvent ev) { + if (isTrackedEntity(ev.getEntity())) { + debugLog("Untracking: %s", ev.getEntityType()); + untrackEntity(ev.getEntity()); + } + } + }); + } + + /** + * Removes the tracking data on an entity. This will not drop an item or interact with the world in any way. + * This will remove tracking internally in this plugin only. + * + * @param entity Entity to remove from internal tracking. + */ + private void untrackEntity(Entity entity) { + trackedEntities.remove(entity.getUniqueId()); + } + + /** + * Add interaction listeners which may or may not be enabled. + */ + private void addInteractionListeners() { + if (config().isReturnVehiclesToInventoryOnExit()) { + debugLog("Vehicles will be returned to the owner on vehicle exit."); + addListener(new Listener() { + @EventHandler + public void vehicleExit(VehicleExitEvent ev) { + if (ev.getExited() instanceof Player) { + var player = (Player) ev.getExited(); + if (ev.getVehicle() instanceof Vehicle) { + var vehicle = (Vehicle) ev.getVehicle(); + // Return the vehicle to the owners inventory, or leave it alone if the inventory is full. + // Adding more than one stack will requires this section to be adjusted. + var stack = toItemStack(vehicle); + var unstorable = player.getInventory().addItem(stack); + + if (unstorable.isEmpty()) { + vehicle.remove(); + trackedEntities.remove(vehicle.getUniqueId()); + + player.sendMessage(String.format("%s returned to inventory.", vehicle.getType())); + } else { + player.sendMessage(String.format("Inventory full, %s will remain here.", vehicle.getType())); + } + } + } + } + }); + + if (config().isGarbageCollectVehicles()) { + debugLog("Vehicles will be garbage collected. Max. Age: %s sec", config().getMaxVehicleAgeInSeconds()); + + addListener(new Listener() { + @EventHandler + public void vehicleTracking(ChunkUnloadEvent ev) { + long starttime = System.currentTimeMillis(); + for (Entity en : ev.getChunk().getEntities()) { + trackedEntities.computeIfPresent(en.getUniqueId(), (key, value) -> { + if (evictByAge(value, starttime)) { + return null; + } else { + return value; + } + }); + } + } + + @EventHandler + public void vehicleTracking(EntitiesLoadEvent ev) { + garbageCollectEntitiesOnChunkLoad(ev.getEntities()); + } + }); + } + } + } + + private ItemStack toItemStack(Vehicle vehicle) { + if (vehicle instanceof Boat) { + return new ItemStack(((Boat) vehicle).getBoatMaterial(), 1); + } else if (vehicle instanceof Minecart) { + return new ItemStack(Material.MINECART, 1); + } else { + throw new IllegalStateException("Cannot map vehicle to item stack: " + vehicle); + } + } + + /** + * Check whether an entity can be evicted or not. + * + * @param en Entity record to check against. + * @return True if the entity can be evicted for any number of reasons. + */ + private boolean canEvict(TrackedEntity en) { + Entity e = en.toEntity(); + + if (e == null) { + return true; + } else if (e instanceof Vehicle) { + Vehicle v = (Vehicle) e; + // Players in vehicles will stop them from being evicted. + return !v.getPassengers().stream().anyMatch(p -> p instanceof Player); + } else { + return true; + } + } + + /** + * Evict an entity by age. If the entity can't be evicted (#canEvict) or the entity is not old enough to be evicted, + * this method does nothing. This method will do internal checks and may drop items in the world, or modify + * player inventory. + * + * @param rec Entity record to check. + * @param starttime Time when the eviction process was started. (May be System.currentTimeMillis()) + * @return True if the entity was removed, else false. + */ + private boolean evictByAge(TrackedEntity rec, long starttime) { + //TODO prevent removal if someone is in the boat. + if (canEvict(rec)) { + var ageInSeconds = TimeUnit.MILLISECONDS.toSeconds(starttime - rec.touched()); + debugLog("Age: %d / %d", ageInSeconds, config().getMaxVehicleAgeInSeconds()); + if (ageInSeconds > config().getMaxVehicleAgeInSeconds()) { + debugLog("Evicting on gc: " + rec.id()); + notifyOwnerOnGCEvent(rec); + // For some ungodly reason, if you do not defer this until the next tick, it will throw a + // ConcurrentModificationException. + Bukkit.getScheduler().runTask(plugin(), () -> { + var en = rec.toEntity(); + var loc = en.getLocation(); + if (en != null) { + debugLog("Strategy: %s", config().getGarbageCollectVehicleStrategy()); + switch (config().getGarbageCollectVehicleStrategy()) { + case DROP_ITEMSTACK: + debugLog("Dropping itemstack of vehicle."); + var vehicle = (Vehicle) en; + var stack = toItemStack(vehicle); + en.remove(); + loc.getWorld().dropItemNaturally(loc, stack); + break; + case REMOVE: + debugLog("Removing vehicle."); + en.remove(); + break; + default: + throw new IllegalStateException("Unknown collections strategy: " + config().getGarbageCollectVehicleStrategy()); + } + } + }); + return true; + } else { + return false; + } + } + + return false; + } + + /** + * Add the garbage collectors, and necessary ticking infrastructure to support the garbage collecting process. + */ + private void addGarbageCollectors() { + if (config().isGarbageCollectVehicles()) { + //Skip cycles where we take longer or run twice at the same time. + final AtomicBoolean runLock = new AtomicBoolean(false); + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin(), () -> { + if (runLock.getAndSet(true)) return; + + debugLog("Running vehicle GC."); + long starttime = System.currentTimeMillis(); + trackedEntities.entrySet().removeIf(en -> evictByAge(en.getValue(), starttime)); + runLock.set(false); + }, config().getGcIntervalInTicks(), config().getGcIntervalInTicks()); + } + } + + /** + * Notify the owner of a tracked entity if their vehicle was removed. Entity (NOT RECORD) and owner may be null in + * this case. If the entity is null, this method only notifies about a removal. If the owner is null, nobody + * is notified. + * + * @param record Record to notify about. + */ + private void notifyOwnerOnGCEvent(TrackedEntity record) { + Entity en = Bukkit.getEntity(record.id()); + Component msg; + + if (en != null) { + msg = Component.text(String.format("Your %s @%d,%d was removed.", en.getType(), en.getLocation().getBlockY(), en.getLocation().getBlockY())); + } else { + msg = Component.text(String.format("Your %s was removed.", en.getType())); + } + + Bukkit.getScheduler().runTaskAsynchronously(plugin(), () -> record.owner().map(Bukkit::getPlayer).ifPresent(c -> c.sendMessage(msg))); + } + + private void startEntityTracking() { + indexCurrentlyLoadedEntities(); + addBaseListeners(); + addInteractionListeners(); + addGarbageCollectors(); + } + + private void startRecordFlushTask() { + Bukkit.getScheduler().runTaskTimer(plugin(), this::storeRecords, config().getFlushRecordIntervalInTicks(), config.getFlushRecordIntervalInTicks()); + } + + @Override + public void onEnable() { + loadRecords(); + startEntityTracking(); + startRecordFlushTask(); + } + + @Override + public void onDisable() { + storeRecords(); + } + + public static BetterVehiclesConfig generate(SimpleAdminHacks plugin, ConfigurationSection config) { + return new BetterVehiclesConfig(plugin, config); + } + + private void loadRecords() { + final var listType = new TypeToken>() { + }.getType(); + + var gson = new GsonBuilder().create(); + var path = Paths.get(config().getPersistenceFilePath()); + + if (Files.exists(path)) { + + List list = Collections.emptyList(); + if (Files.exists(Paths.get(config().getPersistenceFilePath()))) { + try (FileReader reader = new FileReader(config().getPersistenceFilePath())) { + list = gson.fromJson(reader, listType); + } catch (Exception e) { + plugin().getLogger().log(Level.SEVERE, "Failed to persist BetterVehicle records."); + } + } + + list.forEach(en -> trackedEntities.put(en.id(), en)); + } + } + + private void storeRecords() { + var gson = new GsonBuilder().create(); + var path = Paths.get(config().getPersistenceFilePath()); + + if (!Files.exists(path.getParent())) { + try { + Files.createDirectories(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + gson.toJson(new ArrayList<>(trackedEntities.values()), writer); + } catch (Exception e) { + plugin().getLogger().log(Level.SEVERE, "Failed to persist BetterVehicle records."); + } + } +} diff --git a/paper/src/main/resources/config.yml b/paper/src/main/resources/config.yml index 2fbe274b..3fac3431 100644 --- a/paper/src/main/resources/config.yml +++ b/paper/src/main/resources/config.yml @@ -256,7 +256,7 @@ hacks: enabled: false delay: 10000 message: '%Victim% was combat tagged by %Attacker%' - broadcast: [OP, CONSOLE] + broadcast: [ OP, CONSOLE ] DisableAI: enabled: true # Specify below the living entities and their spawning circumstances you want to disable the AI for. @@ -264,7 +264,7 @@ hacks: # NOTE: See https://papermc.io/javadocs/paper/1.16/org/bukkit/event/entity/CreatureSpawnEvent.SpawnReason.html # Note: You can also just state "ALL" for all spawn circumstances VILLAGER: ALL - ENDERMITE: [DISPENSE_EGG, EGG, SPAWNER_EGG] + ENDERMITE: [ DISPENSE_EGG, EGG, SPAWNER_EGG ] ElytraFeatures: enabled: true # Whether Elytra flight should be outright disabled @@ -356,8 +356,8 @@ hacks: REDSTONE_COMPARATOR: 4 OBSERVER: 2 exempt: - - Steve - - Alex + - Steve + - Alex daytimeBed: enabled: true spawnSetMessage: '&7Your spawn has been set.' @@ -384,7 +384,7 @@ hacks: badOmen: false #a list of materials players shouldn't be allowed to place at all noplace: - - DRAGON_EGG + - DRAGON_EGG rainReduction: enabled: false rainOccurrenceChance: .5 @@ -432,36 +432,36 @@ hacks: enabled: true announce: '&f%Player% is brand new!' giveIntroKitToRandomSpawners: true - broadcast: [PERM, CONSOLE] + broadcast: [ PERM, CONSOLE ] helptips: on helptips_end: 20m introkit: enabled: true contents: - - ==: org.bukkit.inventory.ItemStack - v: 1 - type: COOKIE - amount: 32 - meta: - ==: ItemMeta - meta-type: UNSPECIFIC - display-name: 'Manna' - lore: - - "Gift from the Admins as you" - - "begin your journey on Devotion." - - ==: org.bukkit.inventory.ItemStack - v: 1 - type: RED_BED - amount: 1 - meta: - ==: ItemMeta - meta-type: UNSPECIFIC - lore: - - "This world is unforgiving." - - "Be sure to get a good night's" - - "rest soon." + - ==: org.bukkit.inventory.ItemStack + v: 1 + type: COOKIE + amount: 32 + meta: + ==: ItemMeta + meta-type: UNSPECIFIC + display-name: 'Manna' + lore: + - "Gift from the Admins as you" + - "begin your journey on Devotion." + - ==: org.bukkit.inventory.ItemStack + v: 1 + type: RED_BED + amount: 1 + meta: + ==: ItemMeta + meta-type: UNSPECIFIC + lore: + - "This world is unforgiving." + - "Be sure to get a good night's" + - "rest soon." ReinforcedChestBreak: - enabled: true + enabled: false # in seconds delay: 180 message: "&4%player% is raiding a chest at %x% %y% %z%." @@ -488,3 +488,15 @@ hacks: ToggleLamp: enabled: false cooldownTime: 100 + BetterVehicles: + enabled: true + return_vehicle_to_inventory_on_exit: true + garbage_collect_vehicle: true + #"drop_itemstack" or "remove" + garbage_collect_vehicle_strategy: drop_itemstack + max_vehicle_age_in_seconds: 3 + # this interval should be no smaller than the max_vehicle_age_in_seconds value. Else vehicles WILL exceed + # the preset maximum age + gc_interval_in_ticks: 20 + flush_record_interval_in_ticks: 200 + persistence_file_path: plugins/SimpleAdminHacks/BetterVehicles.json