Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-12-26 16:12:46 +01: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
Dieser Commit ist enthalten in:
Ursprung
db13b4c276
Commit
b33cc512b4
@ -101,6 +101,10 @@ public interface GeyserConfiguration {
|
|||||||
|
|
||||||
boolean isAllowCustomSkulls();
|
boolean isAllowCustomSkulls();
|
||||||
|
|
||||||
|
int getMaxVisibleCustomSkulls();
|
||||||
|
|
||||||
|
int getCustomSkullRenderDistance();
|
||||||
|
|
||||||
IMetricsInfo getMetrics();
|
IMetricsInfo getMetrics();
|
||||||
|
|
||||||
int getPendingAuthenticationTimeout();
|
int getPendingAuthenticationTimeout();
|
||||||
|
@ -130,6 +130,12 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
|||||||
@JsonProperty("allow-custom-skulls")
|
@JsonProperty("allow-custom-skulls")
|
||||||
private boolean allowCustomSkulls = true;
|
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")
|
@JsonProperty("add-non-bedrock-items")
|
||||||
private boolean addNonBedrockItems = true;
|
private boolean addNonBedrockItems = true;
|
||||||
|
|
||||||
|
@ -26,33 +26,28 @@
|
|||||||
package org.geysermc.geyser.entity.type.player;
|
package org.geysermc.geyser.entity.type.player;
|
||||||
|
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
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.GameType;
|
||||||
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
||||||
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
|
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.GeyserSession;
|
||||||
|
import org.geysermc.geyser.session.cache.SkullCache;
|
||||||
|
import org.geysermc.geyser.skin.SkullSkinManager;
|
||||||
|
|
||||||
import java.util.UUID;
|
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
|
* A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
|
||||||
* custom player skulls in Bedrock.
|
* custom player skulls in Bedrock.
|
||||||
*/
|
*/
|
||||||
public class SkullPlayerEntity extends PlayerEntity {
|
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) {
|
public SkullPlayerEntity(GeyserSession session, long geyserId) {
|
||||||
super(session, 0, geyserId, UUID.randomUUID(), position, Vector3f.ZERO, rotation, 0, rotation, "", texturesProperty);
|
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
|
||||||
this.blockState = blockState;
|
|
||||||
setPlayerList(false);
|
setPlayerList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +90,57 @@ public class SkullPlayerEntity extends PlayerEntity {
|
|||||||
session.sendUpstreamPacket(addPlayerPacket);
|
session.sendUpstreamPacket(addPlayerPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void despawnEntity(Vector3i position) {
|
/**
|
||||||
this.despawnEntity();
|
* Hide the player entity so that it can be reused for a different skull.
|
||||||
session.getSkullCache().remove(position, this);
|
*/
|
||||||
|
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 LodestoneCache lodestoneCache;
|
||||||
private final PistonCache pistonCache;
|
private final PistonCache pistonCache;
|
||||||
private final PreferencesCache preferencesCache;
|
private final PreferencesCache preferencesCache;
|
||||||
|
private final SkullCache skullCache;
|
||||||
private final TagCache tagCache;
|
private final TagCache tagCache;
|
||||||
private final WorldCache worldCache;
|
private final WorldCache worldCache;
|
||||||
|
|
||||||
@ -220,7 +221,6 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
|||||||
@Setter
|
@Setter
|
||||||
private ItemMappings itemMappings;
|
private ItemMappings itemMappings;
|
||||||
|
|
||||||
private final Map<Vector3i, SkullPlayerEntity> skullCache = new Object2ObjectOpenHashMap<>();
|
|
||||||
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = new Long2ObjectOpenHashMap<>();
|
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -530,6 +530,7 @@ public class GeyserSession implements GeyserConnection, CommandSender {
|
|||||||
this.lodestoneCache = new LodestoneCache();
|
this.lodestoneCache = new LodestoneCache();
|
||||||
this.pistonCache = new PistonCache(this);
|
this.pistonCache = new PistonCache(this);
|
||||||
this.preferencesCache = new PreferencesCache(this);
|
this.preferencesCache = new PreferencesCache(this);
|
||||||
|
this.skullCache = new SkullCache(this);
|
||||||
this.tagCache = new TagCache();
|
this.tagCache = new TagCache();
|
||||||
this.worldCache = new WorldCache(this);
|
this.worldCache = new WorldCache(this);
|
||||||
|
|
||||||
|
211
core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java
vendored
Normale Datei
211
core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java
vendored
Normale Datei
@ -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.CompoundTag;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
|
||||||
import com.nukkitx.math.vector.Vector3i;
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
import com.nukkitx.nbt.NbtMapBuilder;
|
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.level.block.BlockStateValues;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.skin.SkinProvider;
|
import org.geysermc.geyser.skin.SkinProvider;
|
||||||
import org.geysermc.geyser.skin.SkullSkinManager;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@BlockEntity(type = BlockEntityType.SKULL)
|
@BlockEntity(type = BlockEntityType.SKULL)
|
||||||
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
||||||
@ -74,65 +69,18 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void spawnPlayer(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
|
public static void translateSkull(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
||||||
Vector3f entityPosition = Vector3f.from(x, y, z);
|
|
||||||
|
|
||||||
getTextures(tag).whenComplete((texturesProperty, throwable) -> {
|
getTextures(tag).whenComplete((texturesProperty, throwable) -> {
|
||||||
if (texturesProperty == null) {
|
if (texturesProperty == null) {
|
||||||
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
|
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.getEventLoop().inEventLoop()) {
|
if (session.getEventLoop().inEventLoop()) {
|
||||||
spawnPlayer(session, texturesProperty, blockPosition, entityPosition, rotation, blockState);
|
session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState);
|
||||||
} else {
|
} 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.sendUpstreamPacket(movePlayerPacket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.getSkullCache().updateVisibleSkulls();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not a valid move
|
// Not a valid move
|
||||||
|
@ -65,7 +65,7 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
|
|||||||
packet.getNbt(), blockState), packet.getPosition());
|
packet.getNbt(), blockState), packet.getPosition());
|
||||||
// Check for custom skulls.
|
// Check for custom skulls.
|
||||||
if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt() != null && packet.getNbt().contains("SkullOwner")) {
|
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
|
// 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.translator.protocol.Translator;
|
||||||
import org.geysermc.geyser.util.ChunkUtils;
|
import org.geysermc.geyser.util.ChunkUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Translator(packet = ClientboundForgetLevelChunkPacket.class)
|
@Translator(packet = ClientboundForgetLevelChunkPacket.class)
|
||||||
public class JavaForgetLevelChunkTranslator extends PacketTranslator<ClientboundForgetLevelChunkPacket> {
|
public class JavaForgetLevelChunkTranslator extends PacketTranslator<ClientboundForgetLevelChunkPacket> {
|
||||||
@ -41,19 +43,18 @@ public class JavaForgetLevelChunkTranslator extends PacketTranslator<Clientbound
|
|||||||
public void translate(GeyserSession session, ClientboundForgetLevelChunkPacket packet) {
|
public void translate(GeyserSession session, ClientboundForgetLevelChunkPacket packet) {
|
||||||
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
|
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
|
||||||
|
|
||||||
//Checks if a skull is in an unloaded chunk then removes it
|
// Checks if a skull is in an unloaded chunk then removes it
|
||||||
Iterator<Vector3i> iterator = session.getSkullCache().keySet().iterator();
|
List<Vector3i> removedSkulls = new ArrayList<>();
|
||||||
while (iterator.hasNext()) {
|
for (Vector3i position : session.getSkullCache().getSkulls().keySet()) {
|
||||||
Vector3i position = iterator.next();
|
|
||||||
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
||||||
session.getSkullCache().get(position).despawnEntity();
|
removedSkulls.add(position);
|
||||||
iterator.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
removedSkulls.forEach(session.getSkullCache()::removeSkull);
|
||||||
|
|
||||||
if (!session.getGeyser().getWorldManager().shouldExpectLecternHandled()) {
|
if (!session.getGeyser().getWorldManager().shouldExpectLecternHandled()) {
|
||||||
// Do the same thing with lecterns
|
// Do the same thing with lecterns
|
||||||
iterator = session.getLecternCache().iterator();
|
Iterator<Vector3i> iterator = session.getLecternCache().iterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
Vector3i position = iterator.next();
|
Vector3i position = iterator.next();
|
||||||
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
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
|
// Check for custom skulls
|
||||||
if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) {
|
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.level.chunk.bitarray.SingletonBitArray;
|
||||||
import org.geysermc.geyser.registry.BlockRegistries;
|
import org.geysermc.geyser.registry.BlockRegistries;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
import org.geysermc.geyser.session.cache.SkullCache;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.level.BedrockDimension;
|
import org.geysermc.geyser.level.BedrockDimension;
|
||||||
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
|
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
|
// 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 (BlockStateValues.getSkullVariant(blockState) == -1) {
|
||||||
if (skull != null && blockState != skull.getBlockState()) {
|
|
||||||
// Skull is gone
|
// Skull is gone
|
||||||
skull.despawnEntity(position);
|
session.getSkullCache().removeSkull(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent moving_piston from being placed
|
// 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.
|
# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.
|
||||||
allow-custom-skulls: true
|
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.
|
# 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.
|
# 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.
|
# If this is disabled, furnace minecart items will be mapped to hopper minecart items.
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren