Geyser/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java

300 lines
12 KiB
Java

/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.cache;
import com.nukkitx.math.GenericMath;
import com.nukkitx.math.vector.Vector2d;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerFogPacket;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import javax.annotation.Nonnull;
import java.util.Collections;
public class WorldBorder {
private static final double DEFAULT_WORLD_BORDER_SIZE = 5.9999968E7D;
@Setter
private @Nonnull Vector2d center = Vector2d.ZERO;
/**
* The diameter in blocks of the world border before it got changed or similar to newDiameter if not changed.
*/
@Setter
private double oldDiameter = DEFAULT_WORLD_BORDER_SIZE;
/**
* The diameter in blocks of the new world border.
*/
@Setter
private double newDiameter = DEFAULT_WORLD_BORDER_SIZE;
/**
* The speed to apply an expansion/shrinking of the world border.
* When a client joins they get the actual border oldDiameter and the time left to reach the newDiameter.
*/
@Setter
private long speed = 0;
/**
* The time in seconds before a shrinking world border would hit a not moving player.
* Creates the same visual warning effect as warningBlocks.
*/
@Setter
private int warningDelay = 15;
/**
* Block length before you reach the border to show warning particles.
*/
@Setter
private int warningBlocks = 5;
/**
* The world border cannot go beyond this number, positive or negative, in world coordinates
*/
@Setter
private int absoluteMaxSize = 29999984;
/**
* The world coordinate scale as sent in the dimension registry. Used to scale the center X and Z.
*/
private double worldCoordinateScale = 1.0D;
@Getter
private boolean resizing;
private double currentDiameter;
/*
* Boundaries of the actual world border.
* (The will get updated on expanding or shrinking)
*/
private double minX = 0.0D;
private double minZ = 0.0D;
private double maxX = 0.0D;
private double maxZ = 0.0D;
/*
* The boundaries for the for the warning visuals.
*/
private double warningMaxX = 0.0D;
private double warningMaxZ = 0.0D;
private double warningMinX = 0.0D;
private double warningMinZ = 0.0D;
/**
* To track when to send wall particle packets.
*/
private int currentWallTick;
/**
* If the world border is resizing, this variable saves how many ticks have progressed in the resizing
*/
private long lastUpdatedWorldBorderTime = 0;
private final GeyserSession session;
public WorldBorder(GeyserSession session) {
this.session = session;
// Initialize all min/max/warning variables
update();
}
public void setWorldCoordinateScale(double worldCoordinateScale) {
boolean needsUpdate = worldCoordinateScale != this.worldCoordinateScale;
this.worldCoordinateScale = worldCoordinateScale;
if (needsUpdate) {
this.update();
}
}
/**
* @return true as long the entity is within the world limits.
*/
public boolean isInsideBorderBoundaries() {
Vector3f entityPosition = session.getPlayerEntity().getPosition();
return entityPosition.getX() > minX && entityPosition.getX() < maxX && entityPosition.getZ() > minZ && entityPosition.getZ() < maxZ;
}
/**
* Confirms that the entity is within world border boundaries when they move.
* Otherwise, if {@code adjustPosition} is true, this function will push the player back.
*
* @return if this player was indeed against the world border. Will return false if no world border was defined for us.
*/
public boolean isPassingIntoBorderBoundaries(Vector3f newPosition, boolean adjustPosition) {
boolean isInWorldBorder = isPassingIntoBorderBoundaries(newPosition);
if (isInWorldBorder && adjustPosition) {
PlayerEntity playerEntity = session.getPlayerEntity();
// Move the player back, but allow gravity to take place
// Teleported = true makes going back better, but disconnects the player from their mounted entity
playerEntity.moveAbsolute(Vector3f.from(playerEntity.getPosition().getX(), (newPosition.getY() - EntityDefinitions.PLAYER.offset()), playerEntity.getPosition().getZ()),
playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), playerEntity.isOnGround(), session.getRidingVehicleEntity() == null);
}
return isInWorldBorder;
}
public boolean isPassingIntoBorderBoundaries(Vector3f newEntityPosition) {
int entityX = GenericMath.floor(newEntityPosition.getX());
int entityZ = GenericMath.floor(newEntityPosition.getZ());
Vector3f currentEntityPosition = session.getPlayerEntity().getPosition();
// Make sure we can't move out of the world border, but if we're out of the world border, we can move in
return (entityX == (int) minX && currentEntityPosition.getX() > newEntityPosition.getX()) ||
(entityX == (int) maxX && currentEntityPosition.getX() < newEntityPosition.getX()) ||
(entityZ == (int) minZ && currentEntityPosition.getZ() > newEntityPosition.getZ()) ||
(entityZ == (int) maxZ && currentEntityPosition.getZ() < newEntityPosition.getZ());
}
/**
* Same as {@link #isInsideBorderBoundaries()} but using the warning boundaries.
*
* @return true as long the entity is within the world limits and not in the warning zone at the edge to the border.
*/
public boolean isWithinWarningBoundaries() {
Vector3f entityPosition = session.getPlayerEntity().getPosition();
return entityPosition.getX() > warningMinX && entityPosition.getX() < warningMaxX && entityPosition.getZ() > warningMinZ && entityPosition.getZ() < warningMaxZ;
}
/**
* Updates the world border's minimum and maximum properties
*/
public void update() {
/*
* Setting the correct boundary of our world border's square.
*/
double radius;
if (resizing) {
radius = this.currentDiameter / 2.0D;
} else {
radius = this.newDiameter / 2.0D;
}
double absoluteMinSize = -this.absoluteMaxSize;
// Used in the Nether by default
double centerX = this.center.getX() / this.worldCoordinateScale;
double centerZ = this.center.getY() / this.worldCoordinateScale; // Mapping 2D vector to 3D coordinates >> Y becomes Z
this.minX = GenericMath.clamp(centerX - radius, absoluteMinSize, this.absoluteMaxSize);
this.minZ = GenericMath.clamp(centerZ - radius, absoluteMinSize, this.absoluteMaxSize);
this.maxX = GenericMath.clamp(centerX + radius, absoluteMinSize, this.absoluteMaxSize);
this.maxZ = GenericMath.clamp(centerZ + radius, absoluteMinSize, this.absoluteMaxSize);
/*
* Caching the warning boundaries.
*/
this.warningMinX = this.minX + this.warningBlocks;
this.warningMinZ = this.minZ + this.warningBlocks;
this.warningMaxX = this.maxX - this.warningBlocks;
this.warningMaxZ = this.maxZ - this.warningBlocks;
}
public void resize() {
if (this.lastUpdatedWorldBorderTime >= this.speed) {
// Diameter has now updated to the new diameter
this.resizing = false;
this.lastUpdatedWorldBorderTime = 0;
} else if (resizing) {
this.currentDiameter = this.oldDiameter + ((double) this.lastUpdatedWorldBorderTime / (double) this.speed) * (this.newDiameter - this.oldDiameter);
this.lastUpdatedWorldBorderTime += 50;
}
update();
}
public void setResizing(boolean resizing) {
this.resizing = resizing;
if (!resizing) {
this.lastUpdatedWorldBorderTime = 0;
}
}
private static final LevelEventType WORLD_BORDER_PARTICLE = LevelEventType.PARTICLE_DENY_BLOCK;
/**
* Draws a wall of particles where the world border resides
*/
public void drawWall() {
if (currentWallTick++ != 20) {
// Only draw a wall once every second
return;
}
currentWallTick = 0;
Vector3f entityPosition = session.getPlayerEntity().getPosition();
float particlePosX = entityPosition.getX();
float particlePosY = entityPosition.getY();
float particlePosZ = entityPosition.getZ();
if (entityPosition.getX() > warningMaxX) {
drawWall(Vector3f.from(maxX, particlePosY, particlePosZ), true);
}
if (entityPosition.getX() < warningMinX) {
drawWall(Vector3f.from(minX, particlePosY, particlePosZ), true);
}
if (entityPosition.getZ() > warningMaxZ) {
drawWall(Vector3f.from(particlePosX, particlePosY, maxZ), false);
}
if (entityPosition.getZ() < warningMinZ) {
drawWall(Vector3f.from(particlePosX, particlePosY, minZ), false);
}
}
private void drawWall(Vector3f position, boolean drawWallX) {
int initialY = (int) (position.getY() - EntityDefinitions.PLAYER.offset() - 1);
for (int y = initialY; y < (initialY + 5); y++) {
if (drawWallX) {
float x = position.getX();
for (int z = (int) position.getZ() - 3; z < ((int) position.getZ() + 3); z++) {
if (z < minZ) {
continue;
}
if (z > maxZ) {
break;
}
sendWorldBorderParticle(x, y, z);
}
} else {
float z = position.getZ();
for (int x = (int) position.getX() - 3; x < ((int) position.getX() + 3); x++) {
if (x < minX) {
continue;
}
if (x > maxX) {
break;
}
sendWorldBorderParticle(x, y, z);
}
}
}
}
private void sendWorldBorderParticle(float x, float y, float z) {
LevelEventPacket effectPacket = new LevelEventPacket();
effectPacket.setPosition(Vector3f.from(x, y, z));
effectPacket.setType(WORLD_BORDER_PARTICLE);
session.getUpstream().sendPacket(effectPacket);
}
}