mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Add custom skull render distance (#2751)
* Add player skull render distance * Improve updateVisibleSkulls a bit Avoid rechecking visibility on small movements * Periodically despawn unused skull entities * Don't hide skull entity for position/rotation changes Prevents flickering for skulls that are rotating * Update visible skulls when a skull is removed * Only update on removal if an entity is assigned * No need to check for skull in ChunkUtils Update copyright year * Avoid rechecking all skulls when a skull is added/removed * Allow skull render distance and number to be configured Renamed some fields to better match their values * Compare texture property directly from GameProfile * Remove unnecessary blockState field from SkullPlayerEntity * Use binarySearch for insertion Wait for player movement before loading skulls * Allow culling to be disabled by setting max-visible-custom-skulls to -1 * Only remove skulls in inRangeSkulls when culling is enabled * Add suggestions from review * Merge the for loops in updateVisibleSkulls * Fix skulls being leaked on chunk unload
This commit is contained in:
parent
db13b4c276
commit
b33cc512b4
12 changed files with 306 additions and 82 deletions
|
@ -101,6 +101,10 @@ public interface GeyserConfiguration {
|
|||
|
||||
boolean isAllowCustomSkulls();
|
||||
|
||||
int getMaxVisibleCustomSkulls();
|
||||
|
||||
int getCustomSkullRenderDistance();
|
||||
|
||||
IMetricsInfo getMetrics();
|
||||
|
||||
int getPendingAuthenticationTimeout();
|
||||
|
|
|
@ -130,6 +130,12 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
|||
@JsonProperty("allow-custom-skulls")
|
||||
private boolean allowCustomSkulls = true;
|
||||
|
||||
@JsonProperty("max-visible-custom-skulls")
|
||||
private int maxVisibleCustomSkulls = 128;
|
||||
|
||||
@JsonProperty("custom-skull-render-distance")
|
||||
private int customSkullRenderDistance = 32;
|
||||
|
||||
@JsonProperty("add-non-bedrock-items")
|
||||
private boolean addNonBedrockItems = true;
|
||||
|
||||
|
|
|
@ -26,33 +26,28 @@
|
|||
package org.geysermc.geyser.entity.type.player;
|
||||
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.protocol.bedrock.data.GameType;
|
||||
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
||||
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||
import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.geyser.level.block.BlockStateValues;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.cache.SkullCache;
|
||||
import org.geysermc.geyser.skin.SkullSkinManager;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
|
||||
* custom player skulls in Bedrock.
|
||||
*/
|
||||
public class SkullPlayerEntity extends PlayerEntity {
|
||||
/**
|
||||
* Stores the block state that the skull is associated with. Used to determine if the block in the skull's position
|
||||
* has changed
|
||||
*/
|
||||
@Getter
|
||||
private final int blockState;
|
||||
|
||||
public SkullPlayerEntity(GeyserSession session, long geyserId, Vector3f position, float rotation, int blockState, String texturesProperty) {
|
||||
super(session, 0, geyserId, UUID.randomUUID(), position, Vector3f.ZERO, rotation, 0, rotation, "", texturesProperty);
|
||||
this.blockState = blockState;
|
||||
public SkullPlayerEntity(GeyserSession session, long geyserId) {
|
||||
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
|
||||
setPlayerList(false);
|
||||
}
|
||||
|
||||
|
@ -95,8 +90,57 @@ public class SkullPlayerEntity extends PlayerEntity {
|
|||
session.sendUpstreamPacket(addPlayerPacket);
|
||||
}
|
||||
|
||||
public void despawnEntity(Vector3i position) {
|
||||
this.despawnEntity();
|
||||
session.getSkullCache().remove(position, this);
|
||||
/**
|
||||
* Hide the player entity so that it can be reused for a different skull.
|
||||
*/
|
||||
public void free() {
|
||||
setFlag(EntityFlag.INVISIBLE, true);
|
||||
updateBedrockMetadata();
|
||||
|
||||
// Move skull entity out of the way
|
||||
moveAbsolute(session.getPlayerEntity().getPosition().up(128), 0, 0, 0, false, true);
|
||||
}
|
||||
|
||||
public void updateSkull(SkullCache.Skull skull) {
|
||||
if (!skull.getTexturesProperty().equals(getTexturesProperty())) {
|
||||
// Make skull invisible as we change skins
|
||||
setFlag(EntityFlag.INVISIBLE, true);
|
||||
updateBedrockMetadata();
|
||||
|
||||
setTexturesProperty(skull.getTexturesProperty());
|
||||
|
||||
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
|
||||
// Delay to minimize split-second "player" pop-in
|
||||
setFlag(EntityFlag.INVISIBLE, false);
|
||||
updateBedrockMetadata();
|
||||
}, 250, TimeUnit.MILLISECONDS)));
|
||||
} else {
|
||||
// Just a rotation/position change
|
||||
setFlag(EntityFlag.INVISIBLE, false);
|
||||
updateBedrockMetadata();
|
||||
}
|
||||
|
||||
float x = skull.getPosition().getX() + .5f;
|
||||
float y = skull.getPosition().getY() - .01f;
|
||||
float z = skull.getPosition().getZ() + .5f;
|
||||
float rotation;
|
||||
|
||||
int blockState = skull.getBlockState();
|
||||
byte floorRotation = BlockStateValues.getSkullRotation(blockState);
|
||||
if (floorRotation == -1) {
|
||||
// Wall skull
|
||||
y += 0.25f;
|
||||
rotation = BlockStateValues.getSkullWallDirections().get(blockState);
|
||||
switch ((int) rotation) {
|
||||
case 180 -> z += 0.24f; // North
|
||||
case 0 -> z -= 0.24f; // South
|
||||
case 90 -> x += 0.24f; // West
|
||||
case 270 -> x -= 0.24f; // East
|
||||
}
|
||||
} else {
|
||||
rotation = (180f + (floorRotation * 22.5f)) % 360;
|
||||
}
|
||||
|
||||
moveAbsolute(Vector3f.from(x, y, z), rotation, 0, rotation, true, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,6 +173,7 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
|||
private final LodestoneCache lodestoneCache;
|
||||
private final PistonCache pistonCache;
|
||||
private final PreferencesCache preferencesCache;
|
||||
private final SkullCache skullCache;
|
||||
private final TagCache tagCache;
|
||||
private final WorldCache worldCache;
|
||||
|
||||
|
@ -220,7 +221,6 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
|||
@Setter
|
||||
private ItemMappings itemMappings;
|
||||
|
||||
private final Map<Vector3i, SkullPlayerEntity> skullCache = new Object2ObjectOpenHashMap<>();
|
||||
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
/**
|
||||
|
@ -530,6 +530,7 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
|||
this.lodestoneCache = new LodestoneCache();
|
||||
this.pistonCache = new PistonCache(this);
|
||||
this.preferencesCache = new PreferencesCache(this);
|
||||
this.skullCache = new SkullCache(this);
|
||||
this.tagCache = new TagCache();
|
||||
this.worldCache = new WorldCache(this);
|
||||
|
||||
|
|
211
core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java
vendored
Normal file
211
core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java
vendored
Normal file
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright (c) 2019-2022 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.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SkullCache {
|
||||
private final int maxVisibleSkulls;
|
||||
private final boolean cullingEnabled;
|
||||
|
||||
private final int skullRenderDistanceSquared;
|
||||
|
||||
/**
|
||||
* The time in milliseconds before unused skull entities are despawned
|
||||
*/
|
||||
private static final long CLEANUP_PERIOD = 10000;
|
||||
|
||||
@Getter
|
||||
private final Map<Vector3i, Skull> skulls = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
private final List<Skull> inRangeSkulls = new ArrayList<>();
|
||||
|
||||
private final Deque<SkullPlayerEntity> unusedSkullEntities = new ArrayDeque<>();
|
||||
private int totalSkullEntities = 0;
|
||||
|
||||
private final GeyserSession session;
|
||||
|
||||
private Vector3f lastPlayerPosition;
|
||||
|
||||
private long lastCleanup = System.currentTimeMillis();
|
||||
|
||||
public SkullCache(GeyserSession session) {
|
||||
this.session = session;
|
||||
this.maxVisibleSkulls = session.getGeyser().getConfig().getMaxVisibleCustomSkulls();
|
||||
this.cullingEnabled = this.maxVisibleSkulls != -1;
|
||||
|
||||
// Normal skulls are not rendered beyond 64 blocks
|
||||
int distance = Math.min(session.getGeyser().getConfig().getCustomSkullRenderDistance(), 64);
|
||||
this.skullRenderDistanceSquared = distance * distance;
|
||||
}
|
||||
|
||||
public void putSkull(Vector3i position, String texturesProperty, int blockState) {
|
||||
Skull skull = skulls.computeIfAbsent(position, Skull::new);
|
||||
skull.texturesProperty = texturesProperty;
|
||||
skull.blockState = blockState;
|
||||
|
||||
if (skull.entity != null) {
|
||||
skull.entity.updateSkull(skull);
|
||||
} else {
|
||||
if (!cullingEnabled) {
|
||||
assignSkullEntity(skull);
|
||||
return;
|
||||
}
|
||||
if (lastPlayerPosition == null) {
|
||||
return;
|
||||
}
|
||||
skull.distanceSquared = position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ());
|
||||
if (skull.distanceSquared < skullRenderDistanceSquared) {
|
||||
// Keep list in order
|
||||
int i = Collections.binarySearch(inRangeSkulls, skull, Comparator.comparingInt(Skull::getDistanceSquared));
|
||||
if (i < 0) { // skull.distanceSquared is a new distance value
|
||||
i = -i - 1;
|
||||
}
|
||||
inRangeSkulls.add(i, skull);
|
||||
|
||||
if (i < maxVisibleSkulls) {
|
||||
// Reassign entity from the farthest skull to this one
|
||||
if (inRangeSkulls.size() > maxVisibleSkulls) {
|
||||
freeSkullEntity(inRangeSkulls.get(maxVisibleSkulls));
|
||||
}
|
||||
assignSkullEntity(skull);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSkull(Vector3i position) {
|
||||
Skull skull = skulls.remove(position);
|
||||
if (skull != null) {
|
||||
boolean hadEntity = skull.entity != null;
|
||||
freeSkullEntity(skull);
|
||||
|
||||
if (cullingEnabled) {
|
||||
inRangeSkulls.remove(skull);
|
||||
if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) {
|
||||
// Reassign entity to the closest skull without an entity
|
||||
assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateVisibleSkulls() {
|
||||
if (cullingEnabled) {
|
||||
// No need to recheck skull visibility for small movements
|
||||
if (lastPlayerPosition != null && session.getPlayerEntity().getPosition().distanceSquared(lastPlayerPosition) < 4) {
|
||||
return;
|
||||
}
|
||||
lastPlayerPosition = session.getPlayerEntity().getPosition();
|
||||
|
||||
inRangeSkulls.clear();
|
||||
for (Skull skull : skulls.values()) {
|
||||
skull.distanceSquared = skull.position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ());
|
||||
if (skull.distanceSquared > skullRenderDistanceSquared) {
|
||||
freeSkullEntity(skull);
|
||||
} else {
|
||||
inRangeSkulls.add(skull);
|
||||
}
|
||||
}
|
||||
inRangeSkulls.sort(Comparator.comparingInt(Skull::getDistanceSquared));
|
||||
|
||||
for (int i = inRangeSkulls.size() - 1; i >= 0; i--) {
|
||||
if (i < maxVisibleSkulls) {
|
||||
assignSkullEntity(inRangeSkulls.get(i));
|
||||
} else {
|
||||
freeSkullEntity(inRangeSkulls.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Occasionally clean up unused entities as we want to keep skull
|
||||
// entities around for later use, to reduce "player" pop-in
|
||||
if ((System.currentTimeMillis() - lastCleanup) > CLEANUP_PERIOD) {
|
||||
lastCleanup = System.currentTimeMillis();
|
||||
for (SkullPlayerEntity entity : unusedSkullEntities) {
|
||||
entity.despawnEntity();
|
||||
totalSkullEntities--;
|
||||
}
|
||||
unusedSkullEntities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void assignSkullEntity(Skull skull) {
|
||||
if (skull.entity != null) {
|
||||
return;
|
||||
}
|
||||
if (unusedSkullEntities.isEmpty()) {
|
||||
if (!cullingEnabled || totalSkullEntities < maxVisibleSkulls) {
|
||||
// Create a new entity
|
||||
long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
|
||||
skull.entity = new SkullPlayerEntity(session, geyserId);
|
||||
skull.entity.spawnEntity();
|
||||
skull.entity.updateSkull(skull);
|
||||
totalSkullEntities++;
|
||||
}
|
||||
} else {
|
||||
// Reuse an entity
|
||||
skull.entity = unusedSkullEntities.removeFirst();
|
||||
skull.entity.updateSkull(skull);
|
||||
}
|
||||
}
|
||||
|
||||
private void freeSkullEntity(Skull skull) {
|
||||
if (skull.entity != null) {
|
||||
skull.entity.free();
|
||||
unusedSkullEntities.addFirst(skull.entity);
|
||||
skull.entity = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
skulls.clear();
|
||||
inRangeSkulls.clear();
|
||||
unusedSkullEntities.clear();
|
||||
totalSkullEntities = 0;
|
||||
lastPlayerPosition = null;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Data
|
||||
public static class Skull {
|
||||
private String texturesProperty;
|
||||
private int blockState;
|
||||
private SkullPlayerEntity entity;
|
||||
|
||||
private final Vector3i position;
|
||||
private int distanceSquared;
|
||||
}
|
||||
}
|
|
@ -29,19 +29,14 @@ import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
|
|||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.nbt.NbtMapBuilder;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
|
||||
import org.geysermc.geyser.level.block.BlockStateValues;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.skin.SkinProvider;
|
||||
import org.geysermc.geyser.skin.SkullSkinManager;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@BlockEntity(type = BlockEntityType.SKULL)
|
||||
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
||||
|
@ -74,65 +69,18 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
public static void spawnPlayer(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
|
||||
float x = posX + .5f;
|
||||
float y = posY - .01f;
|
||||
float z = posZ + .5f;
|
||||
float rotation;
|
||||
|
||||
byte floorRotation = BlockStateValues.getSkullRotation(blockState);
|
||||
if (floorRotation == -1) {
|
||||
// Wall skull
|
||||
y += 0.25f;
|
||||
rotation = BlockStateValues.getSkullWallDirections().get(blockState);
|
||||
switch ((int) rotation) {
|
||||
case 180 -> z += 0.24f; // North
|
||||
case 0 -> z -= 0.24f; // South
|
||||
case 90 -> x += 0.24f; // West
|
||||
case 270 -> x -= 0.24f; // East
|
||||
}
|
||||
} else {
|
||||
rotation = (180f + (floorRotation * 22.5f)) % 360;
|
||||
}
|
||||
|
||||
public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
|
||||
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
||||
Vector3f entityPosition = Vector3f.from(x, y, z);
|
||||
|
||||
getTextures(tag).whenComplete((texturesProperty, throwable) -> {
|
||||
if (texturesProperty == null) {
|
||||
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.getEventLoop().inEventLoop()) {
|
||||
spawnPlayer(session, texturesProperty, blockPosition, entityPosition, rotation, blockState);
|
||||
session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState);
|
||||
} else {
|
||||
session.executeInEventLoop(() -> spawnPlayer(session, texturesProperty, blockPosition, entityPosition, rotation, blockState));
|
||||
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void spawnPlayer(GeyserSession session, String texturesProperty, Vector3i blockPosition,
|
||||
Vector3f entityPosition, float rotation, int blockState) {
|
||||
long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
|
||||
|
||||
SkullPlayerEntity existingSkull = session.getSkullCache().get(blockPosition);
|
||||
if (existingSkull != null) {
|
||||
// Ensure that two skulls can't spawn on the same point
|
||||
existingSkull.despawnEntity(blockPosition);
|
||||
}
|
||||
|
||||
SkullPlayerEntity player = new SkullPlayerEntity(session, geyserId, entityPosition, rotation, blockState, texturesProperty);
|
||||
|
||||
// Cache entity
|
||||
session.getSkullCache().put(blockPosition, player);
|
||||
|
||||
player.spawnEntity();
|
||||
|
||||
SkullSkinManager.requestAndHandleSkin(player, session, (skin -> session.scheduleInEventLoop(() -> {
|
||||
// Delay to minimize split-second "player" pop-in
|
||||
player.setFlag(EntityFlag.INVISIBLE, false);
|
||||
player.updateBedrockMetadata();
|
||||
}, 250, TimeUnit.MILLISECONDS)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,8 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
|||
session.sendUpstreamPacket(movePlayerPacket);
|
||||
}
|
||||
}
|
||||
|
||||
session.getSkullCache().updateVisibleSkulls();
|
||||
}
|
||||
} else {
|
||||
// Not a valid move
|
||||
|
|
|
@ -65,7 +65,7 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
|
|||
packet.getNbt(), blockState), packet.getPosition());
|
||||
// Check for custom skulls.
|
||||
if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt() != null && packet.getNbt().contains("SkullOwner")) {
|
||||
SkullBlockEntityTranslator.spawnPlayer(session, packet.getNbt(), position.getX(), position.getY(), position.getZ(), blockState);
|
||||
SkullBlockEntityTranslator.translateSkull(session, packet.getNbt(), position.getX(), position.getY(), position.getZ(), blockState);
|
||||
}
|
||||
|
||||
// If block entity is command block, OP permission level is appropriate, player is in creative mode and the NBT is not empty
|
||||
|
|
|
@ -32,7 +32,9 @@ import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
|||
import org.geysermc.geyser.translator.protocol.Translator;
|
||||
import org.geysermc.geyser.util.ChunkUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@Translator(packet = ClientboundForgetLevelChunkPacket.class)
|
||||
public class JavaForgetLevelChunkTranslator extends PacketTranslator<ClientboundForgetLevelChunkPacket> {
|
||||
|
@ -42,18 +44,17 @@ public class JavaForgetLevelChunkTranslator extends PacketTranslator<Clientbound
|
|||
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
|
||||
|
||||
// Checks if a skull is in an unloaded chunk then removes it
|
||||
Iterator<Vector3i> iterator = session.getSkullCache().keySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Vector3i position = iterator.next();
|
||||
List<Vector3i> removedSkulls = new ArrayList<>();
|
||||
for (Vector3i position : session.getSkullCache().getSkulls().keySet()) {
|
||||
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
||||
session.getSkullCache().get(position).despawnEntity();
|
||||
iterator.remove();
|
||||
removedSkulls.add(position);
|
||||
}
|
||||
}
|
||||
removedSkulls.forEach(session.getSkullCache()::removeSkull);
|
||||
|
||||
if (!session.getGeyser().getWorldManager().shouldExpectLecternHandled()) {
|
||||
// Do the same thing with lecterns
|
||||
iterator = session.getLecternCache().iterator();
|
||||
Iterator<Vector3i> iterator = session.getLecternCache().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Vector3i position = iterator.next();
|
||||
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
||||
|
|
|
@ -275,7 +275,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
|
|||
|
||||
// Check for custom skulls
|
||||
if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) {
|
||||
SkullBlockEntityTranslator.spawnPlayer(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState);
|
||||
SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.geysermc.geyser.level.chunk.GeyserChunkSection;
|
|||
import org.geysermc.geyser.level.chunk.bitarray.SingletonBitArray;
|
||||
import org.geysermc.geyser.registry.BlockRegistries;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.cache.SkullCache;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import org.geysermc.geyser.level.BedrockDimension;
|
||||
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
|
||||
|
@ -150,10 +151,9 @@ public class ChunkUtils {
|
|||
// Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now
|
||||
}
|
||||
|
||||
SkullPlayerEntity skull = session.getSkullCache().get(position);
|
||||
if (skull != null && blockState != skull.getBlockState()) {
|
||||
if (BlockStateValues.getSkullVariant(blockState) == -1) {
|
||||
// Skull is gone
|
||||
skull.despawnEntity(position);
|
||||
session.getSkullCache().removeSkull(position);
|
||||
}
|
||||
|
||||
// Prevent moving_piston from being placed
|
||||
|
|
|
@ -148,6 +148,13 @@ cache-images: 0
|
|||
# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.
|
||||
allow-custom-skulls: true
|
||||
|
||||
# The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices.
|
||||
# Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number.
|
||||
max-visible-custom-skulls: 128
|
||||
|
||||
# The radius in blocks around the player in which custom skulls are displayed.
|
||||
custom-skull-render-distance: 32
|
||||
|
||||
# Whether to add (at this time, only) the furnace minecart as a separate item in the game, which normally does not exist in Bedrock Edition.
|
||||
# This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
|
||||
# If this is disabled, furnace minecart items will be mapped to hopper minecart items.
|
||||
|
|
Loading…
Reference in a new issue