From 733c22bf9532dfb2bcdb8de11393ee7d68a381b2 Mon Sep 17 00:00:00 2001 From: psygate Date: Fri, 8 Jul 2022 03:05:02 +0200 Subject: [PATCH 1/5] Updated editorconfig to reflect civmc coding guidelines. --- .editorconfig | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) 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 From 92957a76cb168a7d8d54d8b1a9bedeade515ca53 Mon Sep 17 00:00:00 2001 From: psygate Date: Fri, 8 Jul 2022 03:05:34 +0200 Subject: [PATCH 2/5] Added vehicle management. (Boats and minecarts for now.) --- .../configs/BetterVehiclesConfig.java | 85 ++++ .../hacks/BetterVehicles.java | 481 ++++++++++++++++++ paper/src/main/resources/config.yml | 66 +-- 3 files changed, 603 insertions(+), 29 deletions(-) create mode 100644 paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java create mode 100644 paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java 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..d2ba93f2 --- /dev/null +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java @@ -0,0 +1,85 @@ +package com.programmerdan.minecraft.simpleadminhacks.configs; + +import com.programmerdan.minecraft.simpleadminhacks.SimpleAdminHacks; +import com.programmerdan.minecraft.simpleadminhacks.framework.SimpleHackConfig; +import org.bukkit.configuration.ConfigurationSection; + +/** + * @author psygate + */ +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; + + 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"); + } + + 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; + } +} 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..8ed3ca95 --- /dev/null +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java @@ -0,0 +1,481 @@ +package com.programmerdan.minecraft.simpleadminhacks.hacks; + +import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent; +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.inventory.ItemStack; +import org.spigotmc.event.entity.EntityMountEvent; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * + */ +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 indexChunkEntities(Chunk chunk) { + Arrays.stream(Objects.requireNonNull(chunk).getEntities()).filter(BetterVehicles::isTrackedEntity).forEach(BetterVehicles.this::setupEntityTracking); + } + + /** + * Starts tracking of entities in the chunk. If the chunk entities aren't loaded, the indexing is deferred to a later + * tick, up until cutoff tries. After that the indexing is given up. See {@link #indexChunkEntities(Chunk)} for the + * api call that should be used from the outside. + * + * @param chunk Chunk to index entities in. + * @param counter Counter index. Incremented by one every new try. + * @param cutoff Cutoff where to give up on indexing. + */ + private void indexChunkEntitiesOnEvent(Chunk chunk, int counter, int cutoff) { + if (counter >= cutoff) { + debugLog("Giving up on indexing %d, %d, %s after %d tries.", chunk.getX(), chunk.getZ(), chunk.getWorld().getName(), counter); + } else if (chunk.isEntitiesLoaded()) { + indexChunkEntities(chunk); + } else { + onNextTick(() -> indexChunkEntitiesOnEvent(chunk, counter + 1, cutoff)); + } + } + + private void garbageCollectEntitiesOnChunkLoad(Chunk chunk) { + garbageCollectEntitiesOnChunkLoad(chunk, 0, 100); + } + + /** + * Garbage collect entities in the chunk. If the chunk entities aren't loaded, the gc is deferred to a later + * tick, up until cutoff tries. After that the indexing is given up. See {@link #garbageCollectEntitiesOnChunkLoad(Chunk)} + * for the api call that should be used from the outside. + * + * @param chunk Chunk to index entities in. + * @param counter Counter index. Incremented by one every new try. + * @param cutoff Cutoff where to give up on indexing. + */ + private void garbageCollectEntitiesOnChunkLoad(Chunk chunk, int counter, int cutoff) { + if (counter >= cutoff) { + debugLog("Giving up on garbage collecting %d, %d, %s after %d tries.", chunk.getX(), chunk.getZ(), chunk.getWorld().getName(), counter); + } else if (chunk.isEntitiesLoaded()) { + long starttime = System.currentTimeMillis(); + + for (Entity en : chunk.getEntities()) { + trackedEntities.computeIfPresent(en.getUniqueId(), (key, value) -> { + if (evictByAge(value, starttime)) { + return null; + } else { + return value; + } + }); + } + } else { + onNextTick(() -> garbageCollectEntitiesOnChunkLoad(chunk, counter + 1, cutoff)); + } + } + + /** + * Garbage collect entities in the chunk. If the chunk entities aren't loaded, the gc is deferred to a later + * tick, up until cutoff tries. After that the indexing is given up. See {@link #garbageCollectEntitiesOnChunkLoad(Chunk)} + * for the api call that should be used from the outside. + * + * @param chunk Chunk to index entities in. + */ + private void indexChunkEntitiesOnEvent(Chunk chunk) { + indexChunkEntitiesOnEvent(chunk, 0, 100); + } + + /** + * Register the basic listeners that are always used. + */ + private void addBaseListeners() { + addListener(new Listener() { + @EventHandler + public void onChunkCreate(ChunkPopulateEvent ev) { + indexChunkEntitiesOnEvent(ev.getChunk()); + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent ev) { + indexChunkEntitiesOnEvent(ev.getChunk()); + } + + @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(ChunkLoadEvent ev) { + garbageCollectEntitiesOnChunkLoad(ev.getChunk()); + } + }); + } + } + } + + 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); + } + } + + 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; + } + } + + 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; + } + + 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()); + } + } + + private void notifyOwnerOnGCEvent(TrackedEntity value) { + Entity en = Bukkit.getEntity(value.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(), () -> value.owner().map(Bukkit::getPlayer).ifPresent(c -> c.sendMessage(msg))); + } + + private void startEntityTracking() { + indexCurrentlyLoadedEntities(); + addBaseListeners(); + addInteractionListeners(); + addGarbageCollectors(); + } + + @Override + public void onEnable() { + startEntityTracking(); + } + + private void onNextTick(Runnable runnable) { + // The following line does NOT run on the next tick, but on the same tick. + // Bukkit.getScheduler().runTask(plugin(), Objects.requireNonNull(runnable)); + // This is a workaround. + Bukkit.getScheduler().runTaskLater(plugin(), Objects.requireNonNull(runnable), 1); + } + + + public static BetterVehiclesConfig generate(SimpleAdminHacks plugin, ConfigurationSection config) { + return new BetterVehiclesConfig(plugin, config); + } +} diff --git a/paper/src/main/resources/config.yml b/paper/src/main/resources/config.yml index 2fbe274b..8bdfaf24 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,11 @@ 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 + gc_interval_in_ticks: 20 From 5dab6deb279f324eb73f8837fc2477925a5ab6c4 Mon Sep 17 00:00:00 2001 From: psygate Date: Fri, 8 Jul 2022 03:47:13 +0200 Subject: [PATCH 3/5] Added vehicle management. (Boats and minecarts for now.) --- .../configs/BetterVehiclesConfig.java | 20 ++ .../hacks/BetterVehicles.java | 190 +++++++++++------- paper/src/main/resources/config.yml | 4 + 3 files changed, 137 insertions(+), 77 deletions(-) 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 index d2ba93f2..1ca4a37f 100644 --- a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java @@ -21,6 +21,8 @@ public enum GarbageCollectVehicleStrategy { 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); @@ -33,6 +35,8 @@ protected void wireup(ConfigurationSection config) { 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() { @@ -82,4 +86,20 @@ public long getGcIntervalInTicks() { 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 index 8ed3ca95..203ed011 100644 --- a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java @@ -1,6 +1,9 @@ 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; @@ -20,12 +23,22 @@ 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; /** * @@ -178,86 +191,40 @@ private void indexCurrentlyLoadedEntities() { Bukkit.getWorlds().stream().map(World::getEntities).flatMap(Collection::stream).filter(BetterVehicles::isTrackedEntity).forEach(this::setupEntityTracking); } - private void indexChunkEntities(Chunk chunk) { - Arrays.stream(Objects.requireNonNull(chunk).getEntities()).filter(BetterVehicles::isTrackedEntity).forEach(BetterVehicles.this::setupEntityTracking); + private void indexChunkEntitiesOnEvent(List entities) { + entities.stream().filter(BetterVehicles::isTrackedEntity).forEach(BetterVehicles.this::setupEntityTracking); } /** - * Starts tracking of entities in the chunk. If the chunk entities aren't loaded, the indexing is deferred to a later - * tick, up until cutoff tries. After that the indexing is given up. See {@link #indexChunkEntities(Chunk)} for the - * api call that should be used from the outside. + * Garbage collect entities from the list of entities. * - * @param chunk Chunk to index entities in. - * @param counter Counter index. Incremented by one every new try. - * @param cutoff Cutoff where to give up on indexing. + * @param entities Entities to check if they have expired. */ - private void indexChunkEntitiesOnEvent(Chunk chunk, int counter, int cutoff) { - if (counter >= cutoff) { - debugLog("Giving up on indexing %d, %d, %s after %d tries.", chunk.getX(), chunk.getZ(), chunk.getWorld().getName(), counter); - } else if (chunk.isEntitiesLoaded()) { - indexChunkEntities(chunk); - } else { - onNextTick(() -> indexChunkEntitiesOnEvent(chunk, counter + 1, cutoff)); - } - } - - private void garbageCollectEntitiesOnChunkLoad(Chunk chunk) { - garbageCollectEntitiesOnChunkLoad(chunk, 0, 100); - } - - /** - * Garbage collect entities in the chunk. If the chunk entities aren't loaded, the gc is deferred to a later - * tick, up until cutoff tries. After that the indexing is given up. See {@link #garbageCollectEntitiesOnChunkLoad(Chunk)} - * for the api call that should be used from the outside. - * - * @param chunk Chunk to index entities in. - * @param counter Counter index. Incremented by one every new try. - * @param cutoff Cutoff where to give up on indexing. - */ - private void garbageCollectEntitiesOnChunkLoad(Chunk chunk, int counter, int cutoff) { - if (counter >= cutoff) { - debugLog("Giving up on garbage collecting %d, %d, %s after %d tries.", chunk.getX(), chunk.getZ(), chunk.getWorld().getName(), counter); - } else if (chunk.isEntitiesLoaded()) { - long starttime = System.currentTimeMillis(); - - for (Entity en : chunk.getEntities()) { - trackedEntities.computeIfPresent(en.getUniqueId(), (key, value) -> { - if (evictByAge(value, starttime)) { - return null; - } else { - return value; - } - }); - } - } else { - onNextTick(() -> garbageCollectEntitiesOnChunkLoad(chunk, counter + 1, cutoff)); + 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; + } + }); } } - /** - * Garbage collect entities in the chunk. If the chunk entities aren't loaded, the gc is deferred to a later - * tick, up until cutoff tries. After that the indexing is given up. See {@link #garbageCollectEntitiesOnChunkLoad(Chunk)} - * for the api call that should be used from the outside. - * - * @param chunk Chunk to index entities in. - */ - private void indexChunkEntitiesOnEvent(Chunk chunk) { - indexChunkEntitiesOnEvent(chunk, 0, 100); - } - /** * Register the basic listeners that are always used. */ private void addBaseListeners() { addListener(new Listener() { @EventHandler - public void onChunkCreate(ChunkPopulateEvent ev) { - indexChunkEntitiesOnEvent(ev.getChunk()); - } - - @EventHandler - public void onChunkLoad(ChunkLoadEvent ev) { - indexChunkEntitiesOnEvent(ev.getChunk()); + public void onEntityLoad(EntitiesLoadEvent ev) { + indexChunkEntitiesOnEvent(ev.getEntities()); } @EventHandler @@ -354,8 +321,8 @@ public void vehicleTracking(ChunkUnloadEvent ev) { } @EventHandler - public void vehicleTracking(ChunkLoadEvent ev) { - garbageCollectEntitiesOnChunkLoad(ev.getChunk()); + public void vehicleTracking(EntitiesLoadEvent ev) { + garbageCollectEntitiesOnChunkLoad(ev.getEntities()); } }); } @@ -372,6 +339,12 @@ private ItemStack toItemStack(Vehicle 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(); @@ -386,6 +359,15 @@ private boolean canEvict(TrackedEntity en) { } } + /** + * 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)) { @@ -427,6 +409,9 @@ private boolean evictByAge(TrackedEntity rec, long starttime) { 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. @@ -442,8 +427,15 @@ private void addGarbageCollectors() { } } - private void notifyOwnerOnGCEvent(TrackedEntity value) { - Entity en = Bukkit.getEntity(value.id()); + /** + * 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) { @@ -452,7 +444,7 @@ private void notifyOwnerOnGCEvent(TrackedEntity value) { msg = Component.text(String.format("Your %s was removed.", en.getType())); } - Bukkit.getScheduler().runTaskAsynchronously(plugin(), () -> value.owner().map(Bukkit::getPlayer).ifPresent(c -> c.sendMessage(msg))); + Bukkit.getScheduler().runTaskAsynchronously(plugin(), () -> record.owner().map(Bukkit::getPlayer).ifPresent(c -> c.sendMessage(msg))); } private void startEntityTracking() { @@ -462,20 +454,64 @@ private void startEntityTracking() { addGarbageCollectors(); } + private void startRecordFlushTask() { + Bukkit.getScheduler().runTaskTimer(plugin(), this::storeRecords, config().getFlushRecordIntervalInTicks(), config.getFlushRecordIntervalInTicks()); + } + @Override public void onEnable() { + loadRecords(); startEntityTracking(); + startRecordFlushTask(); } - private void onNextTick(Runnable runnable) { - // The following line does NOT run on the next tick, but on the same tick. - // Bukkit.getScheduler().runTask(plugin(), Objects.requireNonNull(runnable)); - // This is a workaround. - Bukkit.getScheduler().runTaskLater(plugin(), Objects.requireNonNull(runnable), 1); + @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 8bdfaf24..3fac3431 100644 --- a/paper/src/main/resources/config.yml +++ b/paper/src/main/resources/config.yml @@ -495,4 +495,8 @@ hacks: #"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 From 0b6dcafdf815d01f8b3809757f17adf0d2299dd2 Mon Sep 17 00:00:00 2001 From: psygate Date: Fri, 8 Jul 2022 03:49:26 +0200 Subject: [PATCH 4/5] Removed debug logging. --- .../minecraft/simpleadminhacks/hacks/BetterVehicles.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 203ed011..add9f348 100644 --- a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java @@ -119,9 +119,9 @@ private void addListener(Listener listener) { } private static void debugLog(String format, Object... values) { - var msg = String.format(format, values); - System.out.println(msg); - Bukkit.broadcast(Component.text(msg)); +// var msg = String.format(format, values); +// System.out.println(msg); +// Bukkit.broadcast(Component.text(msg)); } /** From 913354205cbf4764cc3f059ea58a947dbe811526 Mon Sep 17 00:00:00 2001 From: psygate Date: Fri, 8 Jul 2022 03:51:21 +0200 Subject: [PATCH 5/5] Copyright updated on classes. --- .../simpleadminhacks/configs/BetterVehiclesConfig.java | 3 ++- .../minecraft/simpleadminhacks/hacks/BetterVehicles.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 index 1ca4a37f..c930c70d 100644 --- a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/configs/BetterVehiclesConfig.java @@ -4,8 +4,9 @@ import com.programmerdan.minecraft.simpleadminhacks.framework.SimpleHackConfig; import org.bukkit.configuration.ConfigurationSection; + /** - * @author psygate + * Create by psygate (github.com/psygate), 2022 */ public class BetterVehiclesConfig extends SimpleHackConfig { 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 index add9f348..8cc6d360 100644 --- a/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java +++ b/paper/src/main/java/com/programmerdan/minecraft/simpleadminhacks/hacks/BetterVehicles.java @@ -41,7 +41,7 @@ import java.util.logging.Level; /** - * + * Create by psygate (github.com/psygate), 2022 */ public class BetterVehicles extends SimpleHack {