Add CArea, RInteractionCallback

Add DynamicRegionEditor, DynamicRegionVisualizer
This commit is contained in:
2026-03-28 21:36:34 +01:00
parent 067755d1f2
commit 61b72c52c7
25 changed files with 612 additions and 353 deletions

View File

@@ -36,15 +36,15 @@ public class DynamicRegionCommand extends SWCommand {
@Register({"dynamic"})
public void visualizeRegions(Player player) {
Tile tile = Tile.fromLocation(player.getLocation()).orElse(null);
if (tile == null) return;
SWPlayer swPlayer = SWPlayer.of(player);
if (swPlayer.hasComponent(DynamicRegionVisualizer.class)) {
swPlayer.removeComponent(DynamicRegionVisualizer.class);
} else if (Permission.SUPERVISOR.hasPermission(player)) {
swPlayer.setComponent(new DynamicRegionVisualizer());
swPlayer.removeComponent(DynamicRegionEditor.class);
} else {
// TODO: Add Message
swPlayer.setComponent(DynamicRegionVisualizer.INSTANCE);
if (Permission.SUPERVISOR.hasPermission(player)) {
swPlayer.setComponent(new DynamicRegionEditor(player.getPlayer()));
}
}
}
}

View File

@@ -0,0 +1,354 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.region;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.Reflection;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.region.dynamic.DynamicRegion;
import de.steamwar.bausystem.region.dynamic.DynamicRegionRepository;
import de.steamwar.bausystem.region.dynamic.RegionConstructorData;
import de.steamwar.bausystem.region.dynamic.Tile;
import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.core.SWPlayer;
import de.steamwar.entity.CArea;
import de.steamwar.entity.REntityServer;
import de.steamwar.entity.RInteraction;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import lombok.NonNull;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import org.joml.RayAabIntersection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
public class DynamicRegionEditor implements SWPlayer.Component, Listener {
private static final Class<?> position = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Pos");
private static final Class<?> look = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Rot");
private static final Class<?> positionLook = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$PosRot");
static {
BiFunction<Player, Object, Object> function = (player, object) -> {
SWPlayer.of(player).getComponent(DynamicRegionEditor.class).ifPresent(DynamicRegionEditor::calcCursor);
return object;
};
TinyProtocol.instance.addFilter(position, function);
TinyProtocol.instance.addFilter(look, function);
TinyProtocol.instance.addFilter(positionLook, function);
}
private final REntityServer entityServer = new REntityServer();
private final CArea area = new CArea(entityServer);
private static final BlockData DELETE = Material.RED_CONCRETE.createBlockData();
private static final BlockData INVALID = Material.YELLOW_CONCRETE.createBlockData();
private static final BlockData PLACE = Material.LIME_CONCRETE.createBlockData();
{
area.hide(true);
}
private final Player player;
private final BukkitTask task;
private Type type = new Delete();
public DynamicRegionEditor(Player player) {
this.player = player;
entityServer.addPlayer(player);
Bukkit.getPluginManager().registerEvents(this, BauSystem.getInstance());
for (int i = 0; i < 121; i++) {
new RInteraction(entityServer, player.getLocation());
}
moveInteractionEntities(Point.fromLocation(player.getLocation()));
CArea area = new CArea(entityServer);
area.setPos1And2(new Location(null, -Tile.maxTile, 1, -Tile.maxTile), new Location(null, Tile.maxTile, 1, Tile.maxTile));
area.setBlock(Material.BEDROCK.createBlockData());
task = Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), this::showHotbar, 0, 20);
}
private void showHotbar() {
entityServer.tick();
StringBuilder st = new StringBuilder();
st.append(type.title());
st.append(" §8(§7Change with §eSwap Hands§8)");
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(st.toString()));
}
@Override
public void onUnmount(SWPlayer player) {
entityServer.close();
task.cancel();
PlayerMoveEvent.getHandlerList().unregister(this);
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
if (event.getPlayer() != player) return;
if (!Permission.SUPERVISOR.hasPermission(player)) {
SWPlayer.of(player).removeComponent(DynamicRegionEditor.class);
return;
}
if (event.getTo() == null) return;
if (event.getFrom().getBlockX() == event.getTo().getBlockX() && event.getFrom().getBlockZ() == event.getTo().getBlockZ()) {
return;
}
moveInteractionEntities(Point.fromLocation(event.getTo()));
}
private void moveInteractionEntities(Point position) {
List<RInteraction> interactionList = entityServer.getEntitiesByType(RInteraction.class);
for (int x = -5; x <= 5; x++) {
for (int z = -5; z <= 5; z++) {
interactionList.removeFirst().move(position.getX() + x + 0.5, 0, position.getZ() + z + 0.5, 0, 0, (byte) 0);
}
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) {
event.setCancelled(true);
List<SWListInv.SWListEntry<Type>> list = new ArrayList<>();
list.add(new SWListInv.SWListEntry<>(new SWItem(Material.BARRIER, "§cDelete"), new Delete()));
DynamicRegionSystem.constructorDataMap.values()
.stream()
.sorted(Comparator.comparing(RegionConstructorData::name))
.forEach(data -> {
list.add(new SWListInv.SWListEntry<>(new SWItem(data.material(), "§f" + data.name()), new Place(data)));
});
SWListInv<Type> inventory = new SWListInv<>(player, "Select Region to place", list, (click, type) -> {
this.type = type;
player.closeInventory();
showHotbar();
});
inventory.open();
}
private void calcCursor() {
Location startPos = player.getLocation().clone().add(0.0, player.getEyeHeight(), 0.0);
Vector direction = player.getLocation().getDirection();
RayAabIntersection intersection = new RayAabIntersection((float) startPos.getX(), (float) startPos.getY(), (float) startPos.getZ(), (float) direction.getX(), (float) direction.getY(), (float) direction.getZ());
boolean hide = true;
for (RInteraction interaction : entityServer.getEntitiesByType(RInteraction.class)) {
float x = (float) (interaction.getX() - 0.5);
float z = (float) (interaction.getZ() - 0.5);
if (intersection.test(x, (float) 0.95, z, x + 1, (float) 1.05, z + 1)) {
int tileX = (int) x;
int tileZ = (int) z;
if (type.visualize(area, tileX, tileZ)) {
hide = false;
break;
}
}
}
area.hide(hide);
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
Location startPos = player.getLocation().clone().add(0.0, player.getEyeHeight(), 0.0);
Vector direction = player.getLocation().getDirection();
if (direction.getY() > 0) return;
RayAabIntersection intersection = new RayAabIntersection((float) startPos.getX(), (float) startPos.getY(), (float) startPos.getZ(), (float) direction.getX(), (float) direction.getY(), (float) direction.getZ());
for (RInteraction interaction : entityServer.getEntitiesByType(RInteraction.class)) {
float x = (float) (interaction.getX() - 0.5);
float z = (float) (interaction.getZ() - 0.5);
if (intersection.test(x, (float) 0.95, z, x + 1, (float) 1.05, z + 1)) {
int tileX = (int) x;
int tileZ = (int) z;
type.run(event.getPlayer(), tileX, tileZ);
break;
}
}
}
private interface Type {
String title();
boolean visualize(CArea area, int tileX, int tileZ);
void run(Player player, int tileX, int tileZ);
}
private static class Delete implements Type {
private boolean valid = false;
private Delete() {
}
@Override
public String title() {
return "§cDelete";
}
@Override
public boolean visualize(CArea area, int tileX, int tileZ) {
Tile tile = Tile.fromTile(tileX, tileZ).orElse(null);
if (tile == null) {
valid = false;
return false;
}
Region region = DynamicRegionSystem.INSTANCE.get(tile);
if (region.getType().isGlobal()) {
valid = false;
return false;
}
Tile minTile = Tile.fromPoint(region.getArea().getMinPoint(false)).orElse(null);
Tile maxTile = Tile.fromPoint(region.getArea().getMaxPoint(false)).orElse(null);
if (minTile == null || maxTile == null) {
valid = false;
return false;
}
Location minLoc = new Location(null, minTile.getTileX(), 1, minTile.getTileZ());
Location maxLoc = new Location(null, maxTile.getTileX(), 1, maxTile.getTileZ());
area.setBlock(DELETE);
area.setPos1And2(minLoc, maxLoc);
valid = true;
return true;
}
@Override
public void run(Player player, int tileX, int tileZ) {
if (!valid) return;
Tile tile = Tile.fromTile(tileX, tileZ).orElse(null);
if (tile == null) return;
Region region = DynamicRegionSystem.INSTANCE.get(tile);
if (region.getType().isGlobal()) return;
if (region.getType().isPath()) {
region.delete();
return;
}
RegionConstructorData data = DynamicRegionSystem.constructorDataMap.get(region.getClass());
SWInventory inventory = new SWInventory(player, 9, "Confirm delete: " + data.name());
inventory.setItem(0, new SWItem(Material.GRAY_CONCRETE, "§7Cancel", click -> {
player.closeInventory();
}));
inventory.setItem(8, new SWItem(Material.RED_CONCRETE, "§cDelete: " + data.name(), click -> {
Region checkRegion = DynamicRegionSystem.INSTANCE.get(tile);
if (region == checkRegion) region.delete();
player.closeInventory();
}));
inventory.open();
}
}
private static class Place implements Type {
private final RegionConstructorData data;
private final int widthX;
private final int widthZ;
private boolean valid = false;
private Place(@NonNull RegionConstructorData data) {
this.data = data;
this.widthX = data.widthX() - 1;
this.widthZ = data.widthZ() - 1;
}
@Override
public String title() {
return "§ePlace§8: §a" + data.name();
}
private boolean validPlacement(int tileX, int tileZ) {
for (int x = 0; x <= widthX; x++) {
for (int z = 0; z <= widthZ; z++) {
Tile tile = Tile.fromTile(tileX + x, tileZ + z).orElse(null);
if (tile == null) return false;
Region region = DynamicRegionSystem.INSTANCE.get(tile);
if (!region.getType().isGlobal()) return false;
}
}
return true;
}
@Override
public boolean visualize(CArea area, int tileX, int tileZ) {
Tile tile = Tile.fromTile(tileX, tileZ).orElse(null);
if (tile == null) {
valid = false;
return false;
}
// Center Tile placement!
tileX -= widthX / 2;
tileZ -= widthZ / 2;
// Validate placements!
if (validPlacement(tileX, tileZ)) {
area.setBlock(PLACE);
valid = true;
} else {
area.setBlock(INVALID);
valid = false;
}
area.setPos1And2(new Location(null, tileX, 1, tileZ), new Location(null, tileX + widthX, 1, tileZ + widthZ));
return true;
}
@Override
public void run(Player player, int tileX, int tileZ) {
if (!valid) return;
// Center Tile placement!
tileX -= widthX / 2;
tileZ -= widthZ / 2;
if (!validPlacement(tileX, tileZ)) return;
Tile tile = Tile.fromTile(tileX, tileZ).orElse(null);
if (tile == null) return;
DynamicRegion region = DynamicRegionRepository.constructRegion(DynamicRegionSystem.identifierDataMap.get(data.identifier()), tile);
if (region == null) return;
region.getArea().place(new PasteBuilder(), false);
region.updateNeighbours();
}
}
}

View File

@@ -56,12 +56,14 @@ public class DynamicRegionSystem implements RegionSystem {
regionCache.clear();
regionMap.put(region.getID(), region);
regionTypeMap.computeIfAbsent(region.getType(), __ -> new HashSet<>()).add(region);
DynamicRegionVisualizer.INSTANCE.addRegion(region);
}
public void remove(DynamicRegion region) {
regionCache.clear();
regionMap.remove(region.getID());
regionTypeMap.getOrDefault(region.getType(), Collections.emptySet()).remove(region);
DynamicRegionVisualizer.INSTANCE.removeRegion(region);
}
public static Map<Class<? extends DynamicRegion>, RegionConstructorData> constructorDataMap = new HashMap<>();
@@ -197,31 +199,37 @@ public class DynamicRegionSystem implements RegionSystem {
Set<Neighbour<Region>> neighbours = new HashSet<>();
if (!noCorners) {
neighbours.add(new Neighbour<>(get(minTile.add(-1, -1).orElseThrow(), fastCache, regions), minTile, NeighbourDirection.NorthWest));
neighbours.add(new Neighbour<>(get(minTile.add(-1, -1).orElse(null), fastCache, regions), minTile, NeighbourDirection.NorthWest));
Tile cornerMinMaxSelf = Tile.fromTile(minTile.getTileX(), maxTile.getTileZ()).orElseThrow();
neighbours.add(new Neighbour<>(get(cornerMinMaxSelf.add(-1, 1).orElseThrow(), fastCache, regions), cornerMinMaxSelf, NeighbourDirection.SouthWest));
Tile.fromTile(minTile.getTileX(), maxTile.getTileZ()).ifPresent(cornerMinMaxSelf -> {
neighbours.add(new Neighbour<>(get(cornerMinMaxSelf.add(-1, 1).orElse(null), fastCache, regions), cornerMinMaxSelf, NeighbourDirection.SouthWest));
});
Tile cornerMaxMinSelf = Tile.fromTile(maxTile.getTileX(), minTile.getTileZ()).orElseThrow();
neighbours.add(new Neighbour<>(get(cornerMaxMinSelf.add(1, -1).orElseThrow(), fastCache, regions), cornerMaxMinSelf, NeighbourDirection.NorthEast));
Tile.fromTile(maxTile.getTileX(), minTile.getTileZ()).ifPresent(cornerMaxMinSelf -> {
neighbours.add(new Neighbour<>(get(cornerMaxMinSelf.add(1, -1).orElse(null), fastCache, regions), cornerMaxMinSelf, NeighbourDirection.NorthEast));
});
neighbours.add(new Neighbour<>(get(maxTile.add(1, 1).orElseThrow(), fastCache, regions), maxTile, NeighbourDirection.SouthEast));
neighbours.add(new Neighbour<>(get(maxTile.add(1, 1).orElse(null), fastCache, regions), maxTile, NeighbourDirection.SouthEast));
}
for (int x = minTile.getTileX(); x <= maxTile.getTileX(); x++) {
Tile tileMinZSelf = Tile.fromTile(x, minTile.getTileZ()).orElseThrow();
neighbours.add(new Neighbour<>(get(tileMinZSelf.add(0, -1).orElseThrow(), fastCache, regions), tileMinZSelf, NeighbourDirection.North));
Tile.fromTile(x, minTile.getTileZ()).ifPresent(tileMinZSelf -> {
neighbours.add(new Neighbour<>(get(tileMinZSelf.add(0, -1).orElse(null), fastCache, regions), tileMinZSelf, NeighbourDirection.North));
});
Tile tileMaxZSelf = Tile.fromTile(x, maxTile.getTileZ()).orElseThrow();
neighbours.add(new Neighbour<>(get(tileMaxZSelf.add(0, 1).orElseThrow(), fastCache, regions), tileMaxZSelf, NeighbourDirection.South));
Tile.fromTile(x, maxTile.getTileZ()).ifPresent(tileMaxZSelf -> {
neighbours.add(new Neighbour<>(get(tileMaxZSelf.add(0, 1).orElse(null), fastCache, regions), tileMaxZSelf, NeighbourDirection.South));
});
}
for (int z = minTile.getTileZ(); z <= maxTile.getTileZ(); z++) {
Tile tileMinXSelf = Tile.fromTile(minTile.getTileX(), z).orElseThrow();
neighbours.add(new Neighbour<>(get(tileMinXSelf.add(-1, 0).orElseThrow(), fastCache, regions), tileMinXSelf, NeighbourDirection.West));
Tile.fromTile(minTile.getTileX(), z).ifPresent(tileMinXSelf -> {
neighbours.add(new Neighbour<>(get(tileMinXSelf.add(-1, 0).orElse(null), fastCache, regions), tileMinXSelf, NeighbourDirection.West));
});
Tile tileMaxXSelf = Tile.fromTile(maxTile.getTileX(), z).orElseThrow();
neighbours.add(new Neighbour<>(get(tileMaxXSelf.add(1, 0).orElseThrow(), fastCache, regions), tileMaxXSelf, NeighbourDirection.East));
Tile.fromTile(maxTile.getTileX(), z).ifPresent(tileMaxXSelf -> {
neighbours.add(new Neighbour<>(get(tileMaxXSelf.add(1, 0).orElse(null), fastCache, regions), tileMaxXSelf, NeighbourDirection.East));
});
}
neighbours.removeIf(neighbour -> neighbour.region.getType().isGlobal());

View File

@@ -19,313 +19,96 @@
package de.steamwar.bausystem.region;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.region.dynamic.DynamicRegion;
import de.steamwar.bausystem.region.dynamic.DynamicRegionRepository;
import de.steamwar.bausystem.region.dynamic.RegionConstructorData;
import de.steamwar.bausystem.region.dynamic.Tile;
import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.core.SWPlayer;
import de.steamwar.entity.*;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.World;
import org.bukkit.entity.Display;
import org.bukkit.util.Transformation;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import java.util.*;
import java.util.stream.Collectors;
public class DynamicRegionVisualizer implements SWPlayer.Component {
public class DynamicRegionVisualizer implements SWPlayer.Component, Listener {
public static final DynamicRegionVisualizer INSTANCE = new DynamicRegionVisualizer();
private final REntityServer entityServer;
private Player player;
private Location sourceLocation;
private Tile sourceTile;
private final REntityServer entityServer = new REntityServer();
private Placement placement = null;
private DynamicRegionVisualizer() {
RTextDisplay text = new RTextDisplay(entityServer, new Location(null, 0.5, 1.1, 0.5));
text.setText("Spawn");
text.setBillboard(Display.Billboard.VERTICAL);
text.setBackgroundColor(0);
text.setShadowed(false);
text.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf().rotationX((float) Math.toRadians(270)), new Vector3f(1, 1, 1), new Quaternionf()));
}
public DynamicRegionVisualizer() {
this.entityServer = new REntityServer();
public void addRegion(DynamicRegion region) {
new CRegion(entityServer, region);
}
public void removeRegion(DynamicRegion region) {
entityServer.getEntitiesByType(CRegion.class)
.stream()
.filter(cRegion -> cRegion.region == region)
.forEach(CRegion::die);
}
@Override
public void onMount(SWPlayer player) {
this.player = player.getPlayer();
sourceTile = Tile.fromLocation(player.getLocation()).orElse(null);
if (sourceTile == null) {
player.removeComponent(DynamicRegionVisualizer.class);
return;
}
sourceLocation = player.getLocation().add(0, -5, 0).getBlock().getLocation();
entityServer.addPlayer(player.getPlayer());
Bukkit.getPluginManager().registerEvents(this, BauSystem.getInstance());
renderTiles(0, 0);
}
@Override
public void onUnmount(SWPlayer player) {
Bukkit.getPluginManager().registerEvents(this, BauSystem.getInstance());
entityServer.close();
entityServer.removePlayer(player.getPlayer());
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
if (event.getPlayer() != player) return;
Location position = event.getTo().getBlock().getLocation();
int dx = position.getBlockX() - sourceLocation.getBlockX();
int dz = position.getBlockZ() - sourceLocation.getBlockZ();
renderTiles(dx, dz);
private static final Vector3f VEC_ZERO = new Vector3f(0, 0, 0);
private static final Quaternionf QUT_ZERO = new Quaternionf(0, 0, 0, 1);
public static Point toVisualization(Location worldLocation) {
Tile tile = Tile.fromLocation(worldLocation).orElseThrow();
return new Point(tile.getTileX(), 0, tile.getTileZ());
}
private void renderTiles(int dx, int dz) {
Set<Tile> tileSet = entityServer.getEntitiesByType(CTile.class)
.stream()
.filter(cTile -> {
if (Math.abs(cTile.tile.getTileX() - dx) > 40 || Math.abs(cTile.tile.getTileZ() - dz) > 40) {
cTile.die();
return false;
} else {
return true;
}
})
.map(cTile -> cTile.tile)
.collect(Collectors.toSet());
private static class CRegion extends CEntity {
for (int x = dx - 20; x <= dx + 20; x++) {
for (int z = dz - 20; z <= dz + 20; z++) {
Tile tile = sourceTile.add(x, z).orElse(null);
if (tile == null || tileSet.contains(tile)) continue;
new CTile(entityServer, tile);
}
}
}
private final DynamicRegion region;
private void resetTiles(Set<Tile> tiles) {
entityServer.getEntitiesByType(CTile.class)
.stream()
.filter(cTile -> tiles.contains(cTile.tile))
.peek(CTile::die)
.map(cTile -> cTile.tile)
.forEach(tile -> {
new CTile(entityServer, tile);
});
if (placement != null) {
placement.check();
}
}
private class CTile extends CEntity {
private final Tile tile;
public CTile(REntityServer server, Tile tile) {
private CRegion(REntityServer server, DynamicRegion region) {
super(server);
this.tile = tile;
this.region = region;
RegionType regionType = DynamicRegionSystem.INSTANCE.get(tile).getType();
Material material = switch (regionType) {
case SPAWN, SPAWN_EXTENSION -> Material.LODESTONE;
case SPAWN_PATH, PATH -> Material.DIRT_PATH;
case DRY, DRY_SPECIAL -> Material.IRON_BLOCK;
case WET, WET_SPECIAL -> Material.LAPIS_BLOCK;
default -> Material.WHITE_CARPET;
};
Location location = sourceLocation.clone().add(tile.getTileX() - sourceTile.getTileX(), 0, tile.getTileZ() - sourceTile.getTileZ());
if (tile.equals(Tile.ZERO)) {
CCubedTextDisplay spawn = new CCubedTextDisplay(entityServer, location);
spawn.setText("§eSPAWN");
spawn.setBackgroundColor(0);
spawn.setShadowed(false);
entities.add(spawn);
} else if (tile.equals(sourceTile)) {
CCubedTextDisplay origin = new CCubedTextDisplay(entityServer, location);
origin.setText("§eORIGIN");
origin.setBackgroundColor(0);
origin.setShadowed(false);
entities.add(origin);
RegionConstructorData data = DynamicRegionSystem.constructorDataMap.get(region.getClass());
int widthX = data.widthX();
int widthZ = data.widthZ();
Point point = toVisualization(region.getArea().getMinPoint(false).toLocation((World) null));
if (widthX != 1 || widthZ != 1) {
CArea area = new CArea(server);
area.setBlock(Material.WHITE_CONCRETE.createBlockData());
area.setPos1And2(point.toLocation((World) null).add(0, 1 - CArea.DEFAULT_WIDTH, 0), point.toLocation((World) null).add(widthX - 1, 1 - CArea.DEFAULT_WIDTH, widthZ - 1));
entities.add(area);
RTextDisplay text = new RTextDisplay(server, point.toLocation((World) null).add(widthX / 2.0, 1.1, widthZ / 2.0));
text.setText(data.name());
text.setBillboard(Display.Billboard.VERTICAL);
text.setBackgroundColor(0);
text.setShadowed(false);
text.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf().rotationX((float) Math.toRadians(270)), new Vector3f(1, 1, 1), new Quaternionf()));
entities.add(text);
}
RBlockDisplay blockDisplay = new RBlockDisplay(entityServer, location);
blockDisplay.setBlock(material.createBlockData());
entities.add(blockDisplay);
RInteraction interaction = new RInteraction(entityServer, location.clone().add(0.5, 0, 0.5));
interaction.setInteraction((player, entityAction) -> {
if (placement != null) {
placement.click(tile, regionType.isGlobal());
return;
}
if (!regionType.isGlobal()) {
SWInventory inv = new SWInventory(player, 9, "Delete Region: " + tile.display());
inv.setItem(0, new SWItem(SWItem.getDye(10), "§8Cancel", click -> {
player.closeInventory();
}));
inv.setItem(8, new SWItem(SWItem.getDye(1), "§cDelete", click -> {
player.closeInventory();
Region region = DynamicRegionSystem.INSTANCE.get(tile);
Set<Tile> tiles = DynamicRegionSystem.INSTANCE.getTilesOfRegion(region);
region.delete();
SWPlayer.allWithSingleComponent(DynamicRegionVisualizer.class)
.forEach(pair -> {
pair.getComponent().resetTiles(tiles);
});
}));
inv.open();
} else {
List<SWListInv.SWListEntry<Map.Entry<Class<? extends DynamicRegion>, RegionConstructorData>>> entries = new ArrayList<>();
DynamicRegionSystem.constructorDataMap.entrySet()
.stream()
.filter(entry -> entry.getValue().placeable())
.sorted(Comparator.comparing(entry -> entry.getValue().name()))
.forEach(entry -> {
entries.add(new SWListInv.SWListEntry<>(new SWItem(entry.getValue().material(), entry.getValue().name()), entry));
});
SWListInv<Map.Entry<Class<? extends DynamicRegion>, RegionConstructorData>> listInv = new SWListInv<>(player, "Select Region for: " + tile.display(), entries, (click, entry) -> {
new Placement(entry.getKey(), entry.getValue(), tile);
player.closeInventory();
});
listInv.open();
}
});
entities.add(interaction);
}
}
private class Placement {
private final Class<? extends DynamicRegion> regionType;
private final RegionConstructorData constructorData;
private CWireframe wireframe;
private Tile sourceTile;
private int dx;
private int dz;
private boolean valid = false;
private Location getMinLocation() {
Tile tile = sourceTile.add(-DynamicRegionVisualizer.this.sourceTile.getTileX(), -DynamicRegionVisualizer.this.sourceTile.getTileZ()).orElse(null);
if (tile == null) tile = Tile.ZERO;
return sourceLocation.clone().add(tile.getTileX(), 0, tile.getTileZ());
}
private Location getMaxLocation() {
Tile tile = sourceTile.add(-DynamicRegionVisualizer.this.sourceTile.getTileX(), -DynamicRegionVisualizer.this.sourceTile.getTileZ()).orElse(null);
if (tile == null) tile = Tile.ZERO;
return sourceLocation.clone().add(tile.getTileX(), 0, tile.getTileZ()).add(dx, 0, dz);
}
public Placement(Class<? extends DynamicRegion> regionType, RegionConstructorData constructorData, Tile sourceTile) {
this.regionType = regionType;
this.constructorData = constructorData;
this.sourceTile = sourceTile;
dx = constructorData.widthX() / Tile.tileSize - 1;
dz = constructorData.widthZ() / Tile.tileSize - 1;
if (dx == 0 && dz == 0) {
place();
return;
}
placement = this;
wireframe = new CWireframe(entityServer);
check();
}
private void check() {
wireframe.setPos1And2(getMinLocation(), getMaxLocation());
for (int x = 0; x <= dx; x++) {
for (int z = 0; z <= dz; z++) {
Tile tile = sourceTile.add(x, z).orElse(null);
if (tile == null || !DynamicRegionSystem.INSTANCE.get(tile).getType().isGlobal()) {
wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
valid = false;
return;
}
}
}
wireframe.setBlock(Material.LIME_CONCRETE.createBlockData());
valid = true;
}
public void click(Tile tile, boolean global) {
if (tile.getTileX() >= sourceTile.getTileX() && tile.getTileX() <= sourceTile.getTileX() + dx && tile.getTileZ() >= sourceTile.getTileZ() && tile.getTileZ() <= sourceTile.getTileZ() + dz) {
SWInventory inv = new SWInventory(player, 9, "Place Region: " + constructorData.name());
inv.setItem(0, new SWItem(SWItem.getDye(1), "§cDeselect", click -> {
placement = null;
wireframe.die();
player.closeInventory();
}));
if (valid) {
inv.setItem(8, new SWItem(SWItem.getDye(10), "§aPlace", click -> {
player.closeInventory();
place();
}));
} else {
inv.setItem(8, new SWItem(SWItem.getDye(8), "§8Place", click -> {
}));
}
inv.open();
return;
}
if (!global) {
return;
}
Set<Tile> tiles = new HashSet<>();
for (int x = 0; x <= dx; x++) {
for (int z = 0; z <= dz; z++) {
tiles.add(Tile.fromTile(x + sourceTile.getTileX(), z + sourceTile.getTileZ()).orElse(null));
}
}
tiles.remove(null);
Tile selected = tiles.stream().min(Comparator.comparing(current -> {
int dx = current.getTileX() - tile.getTileX();
int dz = current.getTileZ() - tile.getTileZ();
return dx * dx + dz * dz;
}))
.orElse(null);
if (selected == null) return;
int dx = tile.getTileX() - selected.getTileX();
int dz = tile.getTileZ() - selected.getTileZ();
Tile newSourceTile = sourceTile.add(dx, dz).orElse(null);
if (newSourceTile == null) return;
sourceTile = newSourceTile;
check();
}
private void place() {
DynamicRegion dynamicRegion = DynamicRegionRepository.constructRegion(regionType, sourceTile);
if (dynamicRegion == null) {
// TODO: Give error to user
return;
}
dynamicRegion.getArea().place(new PasteBuilder(), false);
dynamicRegion.updateNeighbours();
placement = null;
if (wireframe != null) wireframe.die();
Set<Tile> tiles = new HashSet<>();
for (int x = 0; x <= dx; x++) {
for (int z = 0; z <= dz; z++) {
tiles.add(Tile.fromTile(x + sourceTile.getTileX(), z + sourceTile.getTileZ()).orElse(null));
}
}
tiles.remove(null);
SWPlayer.allWithSingleComponent(DynamicRegionVisualizer.class)
.forEach(pair -> pair.getComponent().resetTiles(tiles));
RBlockDisplay display = new RBlockDisplay(server, point.toLocation((World) null));
display.setTransform(new Transformation(VEC_ZERO, QUT_ZERO, new Vector3f(widthX, 1, widthZ), QUT_ZERO));
display.setBlock(data.material().createBlockData());
entities.add(display);
}
}
}

View File

@@ -33,11 +33,9 @@ import java.util.stream.Stream;
@EqualsAndHashCode
public class Tile {
public static final Tile ZERO = new Tile(0, 0);
public static final int tileSize = 21;
public static final int tileOffset = tileSize / 2;
public static final int maxTile = 255;
public static final int maxTile = 127;
public static final int minTile = -maxTile;
public static final int tilesPerAxis = maxTile * 2 + 1;

View File

@@ -29,7 +29,6 @@ import org.bukkit.Location;
import org.bukkit.Material;
import javax.annotation.Nullable;
import java.io.File;
import java.util.UUID;
import java.util.function.BiConsumer;

View File

@@ -28,7 +28,6 @@ import de.steamwar.bausystem.utils.PasteBuilder;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import org.bukkit.Location;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AreaBlock implements Region.Area {

View File

@@ -40,9 +40,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "microwargear_display_7",
name = "MicroWarGearDisplay",
material = Material.STONE_BUTTON,
widthX = MiWG7DisplayRegion.TILE_X * Tile.tileSize,
widthZ = MiWG7DisplayRegion.TILE_Z * Tile.tileSize
material = Material.GRAY_CONCRETE,
widthX = MiWG7DisplayRegion.TILE_X,
widthZ = MiWG7DisplayRegion.TILE_Z
)
public class MiWG7DisplayRegion extends DynamicRegion {

View File

@@ -41,9 +41,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "microwargear_plot_7",
name = "MicroWarGearPlot",
material = Material.STONE_BUTTON,
widthX = Tile.tileSize * MiWG7PlotRegion.TILE_X,
widthZ = Tile.tileSize * MiWG7PlotRegion.TILE_Z
material = Material.GRAY_CONCRETE,
widthX = MiWG7PlotRegion.TILE_X,
widthZ = MiWG7PlotRegion.TILE_Z
)
public class MiWG7PlotRegion extends DynamicRegion {

View File

@@ -40,9 +40,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "miniwargear_display",
name = "MiniWarGearDisplay",
material = Material.END_STONE_BRICK_SLAB,
widthX = MWGDisplayRegion.TILE_X * Tile.tileSize,
widthZ = MWGDisplayRegion.TILE_Z * Tile.tileSize
material = Material.YELLOW_CONCRETE,
widthX = MWGDisplayRegion.TILE_X,
widthZ = MWGDisplayRegion.TILE_Z
)
public class MWGDisplayRegion extends DynamicRegion {

View File

@@ -41,9 +41,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "miniwargear_plot",
name = "MiniWarGearPlot",
material = Material.END_STONE_BRICK_SLAB,
widthX = Tile.tileSize * MWGPlotRegion.TILE_X,
widthZ = Tile.tileSize * MWGPlotRegion.TILE_Z
material = Material.YELLOW_CONCRETE,
widthX = MWGPlotRegion.TILE_X,
widthZ = MWGPlotRegion.TILE_Z
)
public class MWGPlotRegion extends DynamicRegion {

View File

@@ -40,9 +40,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "wargear_display_45",
name = "WarGearDisplay 45",
material = Material.END_STONE_BRICKS,
widthX = WG45DisplayRegion.TILE_X * Tile.tileSize,
widthZ = WG45DisplayRegion.TILE_Z * Tile.tileSize
material = Material.YELLOW_CONCRETE,
widthX = WG45DisplayRegion.TILE_X,
widthZ = WG45DisplayRegion.TILE_Z
)
public class WG45DisplayRegion extends DynamicRegion {

View File

@@ -41,9 +41,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "wargear_plot_45",
name = "WarGearPlot 45",
material = Material.END_STONE_BRICKS,
widthX = Tile.tileSize * WG45PlotRegion.TILE_X,
widthZ = Tile.tileSize * WG45PlotRegion.TILE_Z
material = Material.YELLOW_CONCRETE,
widthX = WG45PlotRegion.TILE_X,
widthZ = WG45PlotRegion.TILE_Z
)
public class WG45PlotRegion extends DynamicRegion {

View File

@@ -40,9 +40,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "warship_display_175",
name = "WarShipDisplay 175",
material = Material.BIRCH_BOAT,
widthX = WS175DisplayRegion.TILE_X * Tile.tileSize,
widthZ = WS175DisplayRegion.TILE_Z * Tile.tileSize
material = Material.BLUE_CONCRETE,
widthX = WS175DisplayRegion.TILE_X,
widthZ = WS175DisplayRegion.TILE_Z
)
public class WS175DisplayRegion extends DynamicRegion {

View File

@@ -41,9 +41,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "warship_plot_175",
name = "WarShipPlot 175",
material = Material.BIRCH_BOAT,
widthX = Tile.tileSize * WS175PlotRegion.TILE_X,
widthZ = Tile.tileSize * WS175PlotRegion.TILE_Z
material = Material.BLUE_CONCRETE,
widthX = WS175PlotRegion.TILE_X,
widthZ = WS175PlotRegion.TILE_Z
)
public class WS175PlotRegion extends DynamicRegion {

View File

@@ -40,9 +40,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "warship_display_230",
name = "WarShipDisplay 230",
material = Material.BIRCH_BOAT,
widthX = WS230DisplayRegion.TILE_X * Tile.tileSize,
widthZ = WS230DisplayRegion.TILE_Z * Tile.tileSize
material = Material.BLUE_CONCRETE,
widthX = WS230DisplayRegion.TILE_X,
widthZ = WS230DisplayRegion.TILE_Z
)
public class WS230DisplayRegion extends DynamicRegion {

View File

@@ -41,9 +41,9 @@ import java.util.UUID;
@RegionConstructorData(
identifier = "warship_plot_230",
name = "WarShipPlot 230",
material = Material.BIRCH_BOAT,
widthX = Tile.tileSize * WS230PlotRegion.TILE_X,
widthZ = Tile.tileSize * WS230PlotRegion.TILE_Z
material = Material.BLUE_CONCRETE,
widthX = WS230PlotRegion.TILE_X,
widthZ = WS230PlotRegion.TILE_Z
)
public class WS230PlotRegion extends DynamicRegion {

View File

@@ -37,11 +37,14 @@ import java.util.stream.Collectors;
identifier = "path",
name = "Path",
material = Material.DIRT_PATH,
widthX = Tile.tileSize,
widthZ = Tile.tileSize
widthX = PathRegion.TILE_X,
widthZ = PathRegion.TILE_Z
)
public class PathRegion extends DynamicRegion {
protected static final int TILE_X = 1;
protected static final int TILE_Z = 1;
private final PathArea area;
@Getter
private final Tile tile;

View File

@@ -31,12 +31,9 @@ import de.steamwar.bausystem.utils.PasteBuilder;
import lombok.Getter;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public class SpecialArea implements Region.Area {

View File

@@ -26,8 +26,6 @@ import de.steamwar.bausystem.region.RegionData;
import de.steamwar.bausystem.region.RegionHistory;
import de.steamwar.bausystem.region.RegionType;
import de.steamwar.bausystem.region.dynamic.*;
import de.steamwar.bausystem.region.dynamic.modes.AreaTile;
import de.steamwar.bausystem.region.dynamic.modes.DisplayRegionData;
import de.steamwar.bausystem.region.dynamic.special.SpecialArea;
import de.steamwar.bausystem.region.dynamic.special.SpecialRegionData;
import de.steamwar.sql.GameModeConfig;
@@ -43,12 +41,15 @@ import static de.steamwar.bausystem.region.dynamic.special.SpecialArea.SPECIAL_P
@RegionConstructorData(
identifier = "special_dry",
name = "Dry",
material = Material.SAND,
widthX = Tile.tileSize,
widthZ = Tile.tileSize
material = Material.IRON_BLOCK,
widthX = DryRegion.TILE_X,
widthZ = DryRegion.TILE_Z
)
public class DryRegion extends DynamicRegion {
protected static final int TILE_X = 1;
protected static final int TILE_Z = 1;
private static final VariantSelector DRY = VariantSelector.Get(new File(SPECIAL_PATH_DIR, "dry"));
private final SpecialArea area;

View File

@@ -27,7 +27,6 @@ import de.steamwar.bausystem.region.RegionHistory;
import de.steamwar.bausystem.region.RegionType;
import de.steamwar.bausystem.region.dynamic.*;
import de.steamwar.bausystem.region.dynamic.special.SpecialArea;
import de.steamwar.bausystem.region.dynamic.special.SpecialRegionData;
import de.steamwar.sql.GameModeConfig;
import lombok.NonNull;
import org.bukkit.Material;
@@ -41,12 +40,15 @@ import static de.steamwar.bausystem.region.dynamic.special.SpecialArea.SPECIAL_P
@RegionConstructorData(
identifier = "special_wet",
name = "Wet",
material = Material.LIGHT_BLUE_CONCRETE_POWDER,
widthX = Tile.tileSize,
widthZ = Tile.tileSize
material = Material.LAPIS_BLOCK,
widthX = WetRegion.TILE_X,
widthZ = WetRegion.TILE_Z
)
public class WetRegion extends DynamicRegion {
protected static final int TILE_X = 1;
protected static final int TILE_Z = 1;
private static final VariantSelector WET = VariantSelector.Get(new File(SPECIAL_PATH_DIR, "wet"));
private final SpecialArea area;

View File

@@ -0,0 +1,83 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.entity;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import java.util.List;
public class CArea extends CEntity {
public static final float DEFAULT_WIDTH = 1 / 16f;
private Location pos1;
private Location pos2;
private float width = DEFAULT_WIDTH;
public CArea(REntityServer server) {
super(server);
for (int i = 0; i < 4; i++) {
entities.add(new CLine(server));
}
}
public CArea setPos1And2(Location pos1, Location pos2) {
if (pos1.getY() != pos2.getY()) return this;
this.pos1 = pos1;
this.pos2 = pos2;
updateAndSpawnLines();
return this;
}
public CArea setWidth(float width) {
this.width = width;
updateAndSpawnLines();
getEntitiesByType(CLine.class).forEach(haaLine -> {
haaLine.setWidth(width);
});
return this;
}
public CArea setBlock(BlockData blockData) {
getEntitiesByType(CLine.class).forEach(haaLine -> {
haaLine.setBlock(blockData);
});
return this;
}
private void updateAndSpawnLines() {
List<CLine> lines = getEntitiesByType(CLine.class);
if (pos1 == null || pos2 == null) {
lines.forEach(line -> line.setFromAndTo(null, null));
return;
}
Vector min = Vector.getMinimum(pos1.toVector(), pos2.toVector());
Vector max = Vector.getMaximum(pos1.toVector(), pos2.toVector())
.add(new Vector(1 - width, 0, 1 - width));
lines.get(0).setFromAndTo(new Location(null, min.getX(), min.getY(), min.getZ()), new Location(null, max.getX() + width, min.getY(), min.getZ()));
lines.get(1).setFromAndTo(new Location(null, min.getX(), min.getY(), max.getZ()), new Location(null, max.getX() + width, min.getY(), max.getZ()));
lines.get(2).setFromAndTo(new Location(null, min.getX(), min.getY(), min.getZ()), new Location(null, min.getX(), min.getY(), max.getZ() + width));
lines.get(3).setFromAndTo(new Location(null, max.getX(), min.getY(), min.getZ()), new Location(null, max.getX(), min.getY(), max.getZ() + width));
}
}

View File

@@ -72,9 +72,6 @@ public class REntityServer implements Listener {
private EntityActionListener callback = null;
private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>());
private static final String DE_STEAMWAR_ENTITY_RINTERACTION = "de.steamwar.entity.RInteraction";
private Reflection.Field<BiConsumer> interactionField = Reflection.getField(DE_STEAMWAR_ENTITY_RINTERACTION, "interaction", BiConsumer.class);
private final BiFunction<Player, Object, Object> filter = (player, packet) -> {
REntity entity = entityMap.get(useEntityTarget.get(packet));
if (entity == null)
@@ -87,8 +84,11 @@ public class REntityServer implements Listener {
EntityAction action = getEntityAction.apply(useEntityAction.get(packet)) == 1 ? EntityAction.ATTACK : EntityAction.INTERACT;
Bukkit.getScheduler().runTask(Core.getInstance(), () -> {
playersThatClicked.remove(player);
if (entity.getClass().getTypeName().equals(DE_STEAMWAR_ENTITY_RINTERACTION)) {
interactionField.get(entity).accept(player, action);
if (Core.getVersion() >= 20 && entity instanceof de.steamwar.entity.RInteraction) {
de.steamwar.entity.RInteraction interaction = (de.steamwar.entity.RInteraction) entity;
if (interaction.callback != null) {
interaction.callback.call(player, interaction, action);
}
}
callback.onAction(player, entity, action);
});
@@ -135,7 +135,7 @@ public class REntityServer implements Listener {
}
void addEntity(REntity entity) {
if (callback == null && entity.getClass().getTypeName().equals(DE_STEAMWAR_ENTITY_RINTERACTION)) {
if (callback == null && Core.getVersion() >= 20 && entity instanceof de.steamwar.entity.RInteraction) {
setCallback((player, entity1, action) -> {});
}
entityMap.put(entity.entityId, entity);

View File

@@ -22,7 +22,6 @@ package de.steamwar.entity;
import de.steamwar.core.BountifulWrapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@@ -40,9 +39,8 @@ public class RInteraction extends REntity {
protected final Consumer<Object> updatePacketSink = o -> server.updateEntity(this, o);
@Setter
@Getter(AccessLevel.PRIVATE)
protected BiConsumer<Player, REntityServer.EntityAction> interaction;
protected RInteractionCallback callback;
private float interactionWidth = 1.0f;
private float interactionHeight = 1.0f;
@@ -116,4 +114,12 @@ public class RInteraction extends REntity {
dataSink.accept(responsiveWatcher, responsive);
}
}
public void setCallback(RInteractionCallback callback) {
this.callback = callback;
}
public void setCallback(BiConsumer<Player, REntityServer.EntityAction> callback) {
this.callback = (player, interaction, entityAction) -> callback.accept(player, entityAction);
}
}

View File

@@ -0,0 +1,26 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.entity;
import org.bukkit.entity.Player;
public interface RInteractionCallback {
void call(Player player, RInteraction interaction, REntityServer.EntityAction entityAction);
}