diff --git a/BauSystem/BauSystem_Main/src/BauSystem.properties b/BauSystem/BauSystem_Main/src/BauSystem.properties
index c03b2605..4900c612 100644
--- a/BauSystem/BauSystem_Main/src/BauSystem.properties
+++ b/BauSystem/BauSystem_Main/src/BauSystem.properties
@@ -393,6 +393,9 @@ INVENTORY_FILL_DISABLE=§cInventoryFiller deactivated
INVENTORY_FILL_GUI_NAME=Inventory Filler
INVENTORY_FILL_GUI_POWER=§ePower§8:§7 {0}
INVENTORY_FILL_GUI_TNT=§eTNT
+# Ray Visualizer
+RAY_VISUALIZER_ENABLE=§aRayVisualizer activated
+RAY_VISUALIZER_DISABLE=§aRayVisualizer deactivated
# Killchecker
KILLCHECKER_HELP_ENABLE=§8/§ekillchecker enable §8- §7Enables Killchecker / Recalculates kills
KILLCHECKER_HELP_DISABLE=§8/§ekillchecker disable §8- §7Disables Killchecker
diff --git a/BauSystem/BauSystem_Main/src/BauSystem_de.properties b/BauSystem/BauSystem_Main/src/BauSystem_de.properties
index a8f67bcb..dac4f57c 100644
--- a/BauSystem/BauSystem_Main/src/BauSystem_de.properties
+++ b/BauSystem/BauSystem_Main/src/BauSystem_de.properties
@@ -350,8 +350,11 @@ SMART_PLACE_DISABLE=§cSmartPlace deaktiviert
# InventoryFiller
INVENTORY_FILL_HELP=§8/§einventoryfill §8- §7Toggled InventoryFill
INVENTORY_FILL_INFO=§7Hilft dir, Behälter zu füllen, indem du sie beim sneaken ansiehst und den Gegenstand fallen lässt. Oder scrolle einfach auf einen Behälter, um die Menge des gehaltenen Gegenstandes darin zu ändern.
-INVENTORY_FILL_ENABLE=§aInventoryFiller activated
-INVENTORY_FILL_DISABLE=§cInventoryFiller deactivated
+INVENTORY_FILL_ENABLE=§aInventoryFiller aktiviert
+INVENTORY_FILL_DISABLE=§cInventoryFiller deaktiviert
+# Ray Visualizer
+RAY_VISUALIZER_ENABLE=§aRayVisualizer aktiviert
+RAY_VISUALIZER_DISABLE=§aRayVisualizer deaktiviert
# Killchecker
KILLCHECKER_HELP_ENABLE=§8/§ekillchecker enable §8- §7Aktiviert Killchecker / Berechnet kills neu
KILLCHECKER_HELP_DISABLE=§8/§ekillchecker disable §8- §7Deaktiviert Killchecker
diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java
new file mode 100644
index 00000000..741996a2
--- /dev/null
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java
@@ -0,0 +1,201 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.bausystem.features.rayvisualizer;
+
+import de.steamwar.bausystem.BauSystem;
+import de.steamwar.bausystem.Permission;
+import de.steamwar.bausystem.SWUtils;
+import de.steamwar.bausystem.configplayer.Config;
+import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
+import de.steamwar.command.SWCommand;
+import de.steamwar.entity.CRay;
+import de.steamwar.entity.REntityServer;
+import de.steamwar.linkage.Linked;
+import de.steamwar.linkage.MinVersion;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.TNTPrimed;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.util.RayTraceResult;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Linked
+@MinVersion(20)
+public class RayVisualizerCommand extends SWCommand implements Listener {
+
+ private class CRayData {
+ private CRay[] rays = new CRay[27 * 400];
+ private boolean[] used = new boolean[27 * 400];
+
+ public void reset() {
+ for (int i = 0; i < 27 * 400; i++) {
+ if (!used[i] && rays[i] != null) {
+ rays[i].die();
+ rays[i] = null;
+ }
+ used[i] = false;
+ }
+ }
+
+ private boolean locEquals(Location loc1, Location loc2) {
+ if ((long)(loc1.getX() * 1000) != (long)(loc2.getX() * 1000)) return false;
+ if ((long)(loc1.getY() * 1000) != (long)(loc2.getY() * 1000)) return false;
+ if ((long)(loc1.getZ() * 1000) != (long)(loc2.getZ() * 1000)) return false;
+ return true;
+ }
+
+ public CRay get(Location from, Location to) {
+ for (int i = 0; i < rays.length; i++) {
+ if (rays[i] != null && locEquals(from, rays[i].getFrom()) && locEquals(to, rays[i].getTo())) {
+ used[i] = true;
+ return rays[i];
+ }
+ }
+ for (int i = 0; i < rays.length; i++) {
+ if (rays[i] != null && locEquals(from, rays[i].getFrom()) && !used[i]) {
+ used[i] = true;
+ return rays[i];
+ }
+ }
+ for (int i = 0; i < rays.length; i++) {
+ if (used[i]) continue;
+ CRay ray = rays[i];
+ if (ray != null) {
+ return ray;
+ }
+ ray = new CRay(server);
+ ray.setBlock(Material.LIME_CONCRETE.createBlockData());
+ ray.setWidth(1 / 32f);
+ rays[i] = ray;
+ used[i] = true;
+ return ray;
+ }
+ return null;
+ }
+ }
+
+ private CRayData rayData = new CRayData();
+
+ private final REntityServer server = new REntityServer();
+ private final World WORLD = Bukkit.getWorlds().get(0);
+
+ public RayVisualizerCommand() {
+ super("rayvisualizer");
+
+ Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
+ if (server.getPlayers().isEmpty()) return;
+ Map> primedList = WORLD.getEntitiesByClass(TNTPrimed.class)
+ .stream()
+ .collect(Collectors.groupingBy(TNTPrimed::getFuseTicks));
+ rayData.reset();
+ if (primedList.isEmpty()) return;
+
+ List fuseTicks = primedList.keySet().stream().sorted().collect(Collectors.toList());
+ List current = new ArrayList<>();
+ for (int i = 0; i < fuseTicks.size(); i++) {
+ List tnts = primedList.get(fuseTicks.get(i));
+ calculateRays(current, tnts);
+ current.addAll(tnts);
+ }
+ }, 1, 1);
+ }
+
+ private void calculateRays(List fromTNTs, List toTNTs) {
+ for (TNTPrimed from : fromTNTs) {
+ if (!from.isInWater()) continue;
+ for (TNTPrimed to : toTNTs) {
+ if (from == to) continue;
+ if (to.getLocation().distanceSquared(from.getLocation()) > 25) continue;
+
+ Location fromLoc = from.getLocation();
+ Location toLoc = to.getLocation().clone().add(-0.49, 0, -0.49);
+
+ final double minX = 0.5 * (1 - Math.floor(2 * 0.98 + 1) / (2 * 0.98 + 1));
+ final double minZ = 0.5 * (1 - Math.floor(2 * 0.98 + 1) / (2 * 0.98 + 1));
+ final double spacing = 0.98 / (2 * 0.98 + 1);
+
+ for (int dx = 0; dx < 3; dx++) {
+ for (int dy = 0; dy < 3; dy++) {
+ for (int dz = 0; dz < 3; dz++) {
+ Location end = toLoc.clone().add(minX + dx * spacing, 0 + dy * spacing, minZ + dz * spacing);
+ RayTraceResult result = fromLoc.getWorld().rayTraceBlocks(fromLoc, end.clone().subtract(fromLoc).toVector(), end.distance(fromLoc));
+ if (result != null && result.getHitBlock() != null) {
+ continue;
+ }
+ CRay cRay = rayData.get(fromLoc, end);
+ if (cRay == null) continue;
+ cRay.setFrom(fromLoc);
+ cRay.setTo(end);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
+ boolean rayvisualizer = Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("rayvisualizer", false);
+ if (rayvisualizer) server.addPlayer(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ server.removePlayer(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onBauMemberUpdate(BauMemberUpdateEvent event) {
+ Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (Permission.BUILD.hasPermission(player)) {
+ boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
+ if (rayvisualizer) server.addPlayer(player);
+ } else {
+ server.removePlayer(player);
+ }
+ }
+ }, 1);
+ }
+
+ @Register
+ public void toggle(@Validator Player player) {
+ boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
+ Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer);
+ if (!rayvisualizer) {
+ SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player));
+ server.addPlayer(player);
+ } else {
+ SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_DISABLE", player));
+ server.removePlayer(player);
+ }
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java
index f3b02deb..f8f01928 100644
--- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java
@@ -19,6 +19,7 @@
package de.steamwar.entity;
+import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Display;
@@ -31,6 +32,10 @@ import org.joml.Vector3f;
import java.util.Objects;
+/**
+ * Can be used for Axis Aligned Lines of near infinite length.
+ */
+@Getter
public class CLine extends CEntity {
public static final float DEFAULT_WIDTH = 1 / 16f;
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CRay.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CRay.java
new file mode 100644
index 00000000..f48badb0
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CRay.java
@@ -0,0 +1,131 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.entity;
+
+import lombok.Getter;
+import org.bukkit.Location;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.entity.Display;
+import org.bukkit.util.Consumer;
+import org.bukkit.util.Transformation;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+import java.util.Objects;
+
+/**
+ * Can be used for Lines of short length.
+ */
+@Getter
+public class CRay extends CEntity {
+
+ public static final float DEFAULT_WIDTH = 1 / 16f;
+
+ private Location from;
+ private Location to;
+ private float width = DEFAULT_WIDTH;
+ private BlockData blockData = RBlockDisplay.DEFAULT_BLOCK;
+ private boolean hide = false;
+
+ public CRay(REntityServer server) {
+ super(server);
+ }
+
+ private CRay checkAndSet(T currentValue, T newValue, Consumer setter) {
+ if (Objects.equals(currentValue, newValue)) return this;
+ setter.accept(newValue);
+ tick();
+ return this;
+ }
+
+ public CRay setFrom(Location from) {
+ return checkAndSet(this.from, from, location -> this.from = location);
+ }
+
+ public CRay setTo(Location to) {
+ return checkAndSet(this.to, to, location -> this.to = location);
+ }
+
+ public CRay setWidth(float width) {
+ return checkAndSet(this.width, width, location -> this.width = width);
+ }
+
+ public CRay setBlock(BlockData blockData) {
+ if (this.blockData.equals(blockData)) return this;
+ if (blockData == null) {
+ this.blockData = RBlockDisplay.DEFAULT_BLOCK;
+ } else {
+ this.blockData = blockData;
+ }
+ if (display != null) {
+ display.setBlock(blockData);
+ }
+ return this;
+ }
+
+ @Override
+ public void hide(boolean hide) {
+ if (hide == this.hide) return;
+ this.hide = hide;
+ if (hide) {
+ if (display != null) display.hide(true);
+ } else {
+ tick();
+ }
+ }
+
+ @Override
+ void tick() {
+ if (from == null || to == null) return;
+ if (hide) return;
+ updateDisplay();
+ }
+
+ private RBlockDisplay display;
+
+ private void updateDisplay() {
+ if (display == null) {
+ display = new RBlockDisplay(server, new Location(null, 0, 0, 0));
+ display.setBrightness(new Display.Brightness(15, 15));
+ display.setViewRange(560);
+ display.setBlock(blockData);
+ entities.add(display);
+ } else {
+ display.hide(false);
+ }
+
+ Vector3f pointA = new Vector3f((float) from.getX(), (float) from.getY(), (float) from.getZ());
+ Vector3f pointB = new Vector3f((float) to.getX(), (float) to.getY(), (float) to.getZ());
+ Vector3f direction = new Vector3f(pointB).sub(pointA);
+ float length = direction.length();
+
+ display.move(from);
+
+ Vector3f normalizedDir = new Vector3f(direction).normalize();
+
+ final Vector3f defaultAxis = new Vector3f(1, 0, 0);
+ Quaternionf rotation = new Quaternionf().rotationTo(defaultAxis, normalizedDir);
+
+ Vector3f scale = new Vector3f(length, width, width);
+
+ Transformation transformation = new Transformation(new Vector3f(0, 0, 0), rotation, scale, new Quaternionf());
+ display.setTransform(transformation);
+ }
+}