/* * This file is a part of the SteamWar software. * * Copyright (C) 2025 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.lobby.portal; import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.display.Hologram; import de.steamwar.lobby.listener.Portals; import org.bukkit.Location; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.entity.Player; import org.bukkit.util.Vector; import java.util.*; import java.util.function.Function; public class Portal implements PortalHandler, ConfigurationSerializable { private static final Map portals = new HashMap<>(); private static final Map> chunkPortals = new HashMap<>(); public static List getPortals() { return new ArrayList<>(portals.values()); } public static Set getPortalNames() { return portals.keySet(); } public static Portal getPortal(Location from, Location to) { ChunkCoords in = new ChunkCoords(from); ChunkCoords out = new ChunkCoords(to); for (ChunkCoords coords : perChunk(Math.min(in.x, out.x), Math.max(in.x, out.x), Math.min(in.z, out.z), Math.max(in.z, out.z))) { for (Portal portal : chunkPortals.getOrDefault(coords, Collections.emptyList())) { Vector normalizedFrom = portal.normalize(from); Vector normalizedTo = portal.normalize(to); if (portal.depth == 0.0) { normalizedFrom.setX(normalizedFrom.getX() > 0 ? 2 : -1); normalizedTo.setX(normalizedTo.getX() > 0 ? 2 : -1); } if (inside(normalizedFrom.getX(), normalizedTo.getX()) && inside(normalizedFrom.getY(), normalizedTo.getY()) && inside(normalizedFrom.getZ(), normalizedTo.getZ())) { return portal; } } } return null; } public static Portal getPortal(String id) { return portals.get(id); } private static boolean inside(double v1, double v2) { return (v1 >= 0.0 || v2 >= 0.0) && (v1 <= 1.0 || v2 <= 1.0); } private final Location pos1; private final Location pos2; private final double depth; private final Vector pos1Vector; private final double yRotation; private final Vector rotatedShape; private final int minChunkX; private final int minChunkZ; private final int maxChunkX; private final int maxChunkZ; private final String id; private final PortalType type; private final PortalHandler handler; private final Hologram debugPos1; private final Hologram debugPos2; private final Hologram debugPos3; public Portal(Map map) { this( (String) map.get("id"), (Location) map.get("pos1"), (Location) map.get("pos2"), (double) map.getOrDefault("depth", 0.0), portal -> PortalType.valueOf((String) map.get("type")).deserialize(map, portal) ); } public Portal(String id, Location pos1, Location pos2, Function handlerConstructor) { this(id, pos1, pos2, 0.0, handlerConstructor); LobbySystem.config().save(); } public Portal(String id, Location pos1, Location pos2, double depth, Function handlerConstructor) { this.id = id; this.pos1 = pos1; this.pos2 = pos2; this.depth = depth; this.minChunkX = Math.min(pos1.getBlockX(), pos2.getBlockX()) >> 4; this.minChunkZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()) >> 4; this.maxChunkX = Math.max(pos1.getBlockX(), pos2.getBlockX()) >> 4; this.maxChunkZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()) >> 4; Vector orientation = pos2.toVector().subtract(pos1.toVector()); this.yRotation = Math.atan2(orientation.getX(), orientation.getZ()); this.pos1Vector = pos1.toVector().subtract(new Vector(depth / 2, 0, 0).rotateAroundY(yRotation)); this.rotatedShape = new Vector(depth == 0.0 ? 1.0 : depth, orientation.getY(), orientation.clone().setY(0).length()); this.handler = handlerConstructor.apply(this); this.type = handler.type(); this.debugPos1 = new Hologram(null, pos1, id + " POS1", true); this.debugPos2 = new Hologram(null, pos2, id + " POS2", true); this.debugPos3 = new Hologram(null, pos1.clone().add(pos2).toVector().divide(new Vector(2, 2, 2)).toLocation(pos1.getWorld()), handler.toString(), true); portals.put(id, this); perChunk(minChunkX, maxChunkX, minChunkZ, maxChunkZ).forEach(coords -> chunkPortals.computeIfAbsent(coords, coords1 -> new ArrayList<>()).add(this)); } public Vector normalize(Location location) { return location.toVector().subtract(pos1Vector).rotateAroundY(-yRotation).divide(rotatedShape); } public Vector denormalize(Vector vector) { return vector.multiply(rotatedShape).rotateAroundY(yRotation).add(pos1Vector); } public double getYrotation() { return yRotation; } public double getDepth() { return depth; } private static Iterable perChunk(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ) { return () -> new Iterator() { private int x = minChunkX; private int z = minChunkZ; @Override public boolean hasNext() { return z <= maxChunkZ; } @Override public ChunkCoords next() { ChunkCoords coords = new ChunkCoords(x++, z); if (x > maxChunkX) { x = minChunkX; z++; } return coords; } }; } @Override public void handle(Player player, Location from, Location to) { handler.handle(player, from, to); Portals.getStack(player).push(this); } @Override public void serialize(Map map) { map.put("id", id); map.put("pos1", pos1); map.put("pos2", pos2); map.put("depth", depth); map.put("type", type.name()); handler.serialize(map); } @Override public PortalType type() { return handler.type(); } @Override public void delete() { debugPos1.delete(); debugPos2.delete(); debugPos3.delete(); handler.delete(); perChunk(minChunkX, maxChunkX, minChunkZ, maxChunkZ).forEach(coords -> chunkPortals.getOrDefault(coords, new ArrayList<>()).remove(this)); portals.remove(id); LobbySystem.config().save(); } @Override public Map serialize() { Map map = new HashMap<>(); serialize(map); return map; } public Location getPos1() { return pos1; } public Location getPos2() { return pos2; } public String getId() { return id; } public void setDepth(double depth) { delete(); Map handlerMap = new HashMap<>(); handler.serialize(handlerMap); new Portal(id, pos1, pos2, depth, portal -> handler.type().deserialize(handlerMap, portal)); LobbySystem.config().save(); } public PortalHandler getHandler() { return handler; } @Override public String toString() { return getId() + " " + type().name(); } private static class ChunkCoords { private final int x; private final int z; private ChunkCoords(Location location) { this.x = location.getBlockX() >> 4; this.z = location.getBlockZ() >> 4; } private ChunkCoords(int x, int z) { this.x = x; this.z = z; } @Override public int hashCode() { return x << 16 + z; } @Override public boolean equals(Object obj) { if (!(obj instanceof ChunkCoords)) return false; ChunkCoords coords = (ChunkCoords) obj; return x == coords.x && z == coords.z; } } }