diff --git a/geyser/libs/Geyser-Standalone.jar b/geyser/libs/Geyser-Standalone.jar index 0c922fc..c572189 100644 Binary files a/geyser/libs/Geyser-Standalone.jar and b/geyser/libs/Geyser-Standalone.jar differ diff --git a/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/GeyserUtils.java b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/GeyserUtils.java index 868cdca..25b974f 100644 --- a/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/GeyserUtils.java +++ b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/GeyserUtils.java @@ -20,10 +20,15 @@ import me.zimzaza4.geyserutils.common.packet.*; import me.zimzaza4.geyserutils.geyser.form.NpcDialogueForm; import me.zimzaza4.geyserutils.geyser.form.NpcDialogueForms; import me.zimzaza4.geyserutils.geyser.form.element.Button; +import me.zimzaza4.geyserutils.geyser.mappings.ItemParticlesMappings; +import me.zimzaza4.geyserutils.geyser.replace.JavaAddEntityTranslatorReplace; import me.zimzaza4.geyserutils.geyser.scoreboard.EntityScoreboard; import me.zimzaza4.geyserutils.geyser.translator.NPCFormResponseTranslator; import me.zimzaza4.geyserutils.geyser.util.Converter; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.skin.ImageData; import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; @@ -33,31 +38,32 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.bedrock.camera.CameraShake; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.connection.GeyserConnection; -import org.geysermc.geyser.api.entity.EntityDefinition; -import org.geysermc.geyser.api.entity.EntityIdentifier; import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; -import org.geysermc.geyser.api.event.java.ServerSpawnEntityEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent; -import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntitiesEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent; import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.skin.Cape; import org.geysermc.geyser.api.skin.Skin; import org.geysermc.geyser.api.skin.SkinData; import org.geysermc.geyser.api.skin.SkinGeometry; +import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.skin.SkinProvider; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.mcprotocollib.network.Session; import org.geysermc.mcprotocollib.network.event.session.PacketSendingEvent; import org.geysermc.mcprotocollib.network.event.session.SessionAdapter; import org.geysermc.mcprotocollib.network.packet.Packet; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ItemParticleData; import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundCustomPayloadPacket; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelParticlesPacket; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; @@ -65,17 +71,13 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; public class GeyserUtils implements Extension { - NbtMap CLEAR_INSTRUCTION_TAG = NbtMap.builder().putByte("clear", Integer.valueOf(1).byteValue()).build(); @Getter - public static PacketManager packetManager; + public static PacketManager packetManager = new PacketManager(); @Getter public static Map LOADED_SKIN_DATA = new HashMap<>(); @@ -89,9 +91,11 @@ public class GeyserUtils implements Extension { @Getter public static Map scoreboards = new ConcurrentHashMap<>(); + public static ItemParticlesMappings particlesMappings = new ItemParticlesMappings(); static Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, true); + public static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); @Subscribe @@ -102,13 +106,17 @@ public class GeyserUtils implements Extension { CameraPreset.load(); + replaceTranslator(); LOADED_ENTITY_DEFINITIONS .forEach((s, entityDefinition) -> { logger().info("DEF ENTITY:" + s); }); + + particlesMappings.read(dataFolder().resolve("item_particles_mappings.json")); } public static void addCustomEntity(String id) { + /* LOADED_ENTITY_DEFINITIONS.put(id, EntityDefinition.builder() .identifier(EntityIdentifier.builder().identifier(id) @@ -117,7 +125,26 @@ public class GeyserUtils implements Extension { .height(0.6f) .width(0.6f) .build()); + + */ + NbtMap registry = Registries.BEDROCK_ENTITY_IDENTIFIERS.get(); + List idList = new ArrayList<>(registry.getList("idlist", NbtType.COMPOUND)); + idList.add(NbtMap.builder() + .putString("id", id) + .putString("bid", "") + .putBoolean("hasspawnegg", false) + .putInt("rid", idList.size() + 1) + .putBoolean("summonable", false).build() + ); + + Registries.BEDROCK_ENTITY_IDENTIFIERS.set(NbtMap.builder() + .putList("idlist", NbtType.COMPOUND, idList).build() + ); + EntityDefinition def = EntityDefinition.builder(null).height(0.1f).width(0.1f).identifier(id).build(); + + LOADED_ENTITY_DEFINITIONS.put(id, def); } + public void loadEntities() { Gson gson = new Gson(); @@ -143,12 +170,9 @@ public class GeyserUtils implements Extension { } } - @Subscribe - public void onEntitiesDefine(GeyserDefineEntitiesEvent event) { - loadEntities(); - for (EntityDefinition value : LOADED_ENTITY_DEFINITIONS.values()) { - event.register(value); - } + public void replaceTranslator() { + Registries.JAVA_PACKET_TRANSLATORS + .register(ClientboundAddEntityPacket.class, new JavaAddEntityTranslatorReplace()); } @Subscribe @@ -249,7 +273,7 @@ public class GeyserUtils implements Extension { if (payloadPacket.getChannel().equals("minecraft:register")) { String channels = new String(payloadPacket.getData(), StandardCharsets.UTF_8); channels = channels + "\0" + GeyserUtilsChannels.MAIN; - event.setPacket(new ServerboundCustomPayloadPacket("minecraft:register", channels.getBytes(StandardCharsets.UTF_8))); + event.setPacket(new ServerboundCustomPayloadPacket(Key.key("minecraft:register"), channels.getBytes(StandardCharsets.UTF_8))); } } } @@ -294,14 +318,14 @@ public class GeyserUtils implements Extension { buttons.add(new Button(button.text(), button.commands(), button.mode(), () -> { if (button.mode() == NpcDialogueButton.ButtonMode.BUTTON_MODE) { - session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(GeyserUtilsChannels.MAIN, packetManager.encodePacket(new NpcFormResponseCustomPayloadPacket(formData.formId(), finalI)))); + session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(Key.key(GeyserUtilsChannels.MAIN), packetManager.encodePacket(new NpcFormResponseCustomPayloadPacket(formData.formId(), finalI)))); } }, button.hasNextForm())); i++; } } - form.closeHandler(() -> session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(GeyserUtilsChannels.MAIN, packetManager.encodePacket(new NpcFormResponseCustomPayloadPacket(formData.formId(), -1))))); + form.closeHandler(() -> session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(Key.key(GeyserUtilsChannels.MAIN), packetManager.encodePacket(new NpcFormResponseCustomPayloadPacket(formData.formId(), -1))))); form.buttons(buttons); form.createAndSend(session); @@ -368,6 +392,44 @@ public class GeyserUtils implements Extension { Entity entity = (session.getEntityCache().getEntityByJavaId(updateEntityScorePacket.getEntityId())); if (entity != null) { scoreboard.updateScore(updateEntityScorePacket.getObjective(), entity.getGeyserId(), updateEntityScorePacket.getScore()); + } + } + } + } else if (packet instanceof ClientboundLevelParticlesPacket particlesPacket) { + if (particlesPacket.getParticle().getData() instanceof ItemParticleData data) { + GeyserItemStack itemStack = GeyserItemStack.from(data.getItemStack()); + Map map = particlesMappings.getMappings().get(itemStack.asItem().javaIdentifier()); + if (map != null) { + int id = itemStack.getOrCreateComponents().getOrDefault(DataComponentType.CUSTOM_MODEL_DATA, -1); + String particle = map.get(id); + if (particle != null) { + + int dimensionId = DimensionUtils.javaToBedrock(session.getDimension()); + + SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket(); + stringPacket.setIdentifier(particle); + stringPacket.setDimensionId(dimensionId); + stringPacket.setMolangVariablesJson(Optional.empty()); + session.sendUpstreamPacket(stringPacket); + + if (particlesPacket.getAmount() == 0) { + // 0 means don't apply the offset + Vector3f position = Vector3f.from(particlesPacket.getX(), particlesPacket.getY(), particlesPacket.getZ()); + stringPacket.setPosition(position); + } else { + Random random = ThreadLocalRandom.current(); + for (int i = 0; i < particlesPacket.getAmount(); i++) { + double offsetX = random.nextGaussian() * (double) particlesPacket.getOffsetX(); + double offsetY = random.nextGaussian() * (double) particlesPacket.getOffsetY(); + double offsetZ = random.nextGaussian() * (double) particlesPacket.getOffsetZ(); + Vector3f position = Vector3f.from(particlesPacket.getX() + offsetX, particlesPacket.getY() + offsetY, particlesPacket.getZ() + offsetZ); + stringPacket.setPosition(position); + } + } + session.sendUpstreamPacket(stringPacket); + + + } } } @@ -379,12 +441,6 @@ public class GeyserUtils implements Extension { }, 80, TimeUnit.MILLISECONDS); } - @Subscribe - public void onEntitySpawn(ServerSpawnEntityEvent event) { - String def = CUSTOM_ENTITIES.get(event.connection()).getIfPresent(event.entityId()); - if (def == null) return; - event.entityDefinition(LOADED_ENTITY_DEFINITIONS.getOrDefault(def, event.entityDefinition())); - } @NotNull private static AnimateEntityPacket getAnimateEntityPacket(AnimateEntityCustomPayloadPacket animateEntityCustomPayloadPacket) { @@ -404,7 +460,6 @@ public class GeyserUtils implements Extension { SkinGeometry geometry = skinData.geometry(); if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { - // TODO is this special behavior needed? PlayerListPacket.Entry updatedEntry = buildEntryManually( session, entity.getUuid(), @@ -436,7 +491,6 @@ public class GeyserUtils implements Extension { SkinGeometry geometry) { SerializedSkin serializedSkin = getSkin(skin.textureUrl(), skin, cape, geometry); - // This attempts to find the XUID of the player so profile images show up for Xbox accounts String xuid = ""; GeyserSession playerSession = GeyserImpl.getInstance().connectionByUuid(uuid); @@ -445,9 +499,6 @@ public class GeyserUtils implements Extension { } PlayerListPacket.Entry entry; - - // If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID - // as Bedrock expects to get back its own provided UUID if (session.getPlayerEntity().getUuid().equals(uuid)) { entry = new PlayerListPacket.Entry(session.getAuthData().uuid()); } else { diff --git a/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/mappings/ItemParticlesMappings.java b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/mappings/ItemParticlesMappings.java new file mode 100644 index 0000000..0b2050d --- /dev/null +++ b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/mappings/ItemParticlesMappings.java @@ -0,0 +1,45 @@ +package me.zimzaza4.geyserutils.geyser.mappings; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import it.unimi.dsi.fastutil.Pair; +import lombok.Getter; +import me.zimzaza4.geyserutils.geyser.GeyserUtils; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.item.GeyserCustomItemOptions; +import org.geysermc.geyser.registry.mappings.MappingsConfigReader; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; + +@Getter +public class ItemParticlesMappings { + + Map> mappings; + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + public void read(Path file) { + if (!file.toFile().exists()) { + mappings = new HashMap<>(); + mappings.put("minecraft:stone", Map.of(10001, "custom:test")); + + try (FileWriter writer = new FileWriter(file.toFile())){ + file.toFile().createNewFile(); + String json = GSON.toJson(mappings); + writer.write(json); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + mappings = GSON.fromJson(new FileReader(file.toFile()), new TypeToken>>(){}.getType()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/replace/JavaAddEntityTranslatorReplace.java b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/replace/JavaAddEntityTranslatorReplace.java new file mode 100644 index 0000000..86bc4c0 --- /dev/null +++ b/geyser/src/main/java/me/zimzaza4/geyserutils/geyser/replace/JavaAddEntityTranslatorReplace.java @@ -0,0 +1,136 @@ +/* + * 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 me.zimzaza4.geyserutils.geyser.replace; + +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.FallingBlockData; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.ProjectileData; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.WardenData; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.*; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; + +import static me.zimzaza4.geyserutils.geyser.GeyserUtils.CUSTOM_ENTITIES; +import static me.zimzaza4.geyserutils.geyser.GeyserUtils.LOADED_ENTITY_DEFINITIONS; + +public class JavaAddEntityTranslatorReplace extends PacketTranslator { + @Override + public void translate(GeyserSession session, ClientboundAddEntityPacket packet) { + EntityDefinition definition = Registries.ENTITY_DEFINITIONS.get(packet.getType()); + if (definition == null) { + session.getGeyser().getLogger().debug("Could not find an entity definition with type " + packet.getType()); + return; + } + + Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ()); + Vector3f motion = Vector3f.from(packet.getMotionX(), packet.getMotionY(), packet.getMotionZ()); + float yaw = packet.getYaw(); + float pitch = packet.getPitch(); + float headYaw = packet.getHeadYaw(); + + if (packet.getType() == EntityType.PLAYER) { + + PlayerEntity entity; + if (packet.getUuid().equals(session.getPlayerEntity().getUuid())) { + // Server is sending a fake version of the current player + entity = new PlayerEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), + session.getPlayerEntity().getUuid(), position, motion, yaw, pitch, headYaw, session.getPlayerEntity().getUsername(), + session.getPlayerEntity().getTexturesProperty()); + } else { + entity = session.getEntityCache().getPlayerEntity(packet.getUuid()); + if (entity == null) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.entity.player.failed_list", packet.getUuid())); + return; + } + + entity.setEntityId(packet.getEntityId()); + entity.setPosition(position); + entity.setYaw(yaw); + entity.setPitch(pitch); + entity.setHeadYaw(headYaw); + entity.setMotion(motion); + } + session.getEntityCache().cacheEntity(entity); + + entity.sendPlayer(); + SkinManager.requestAndHandleSkinAndCape(entity, session, null); + return; + } + + Entity entity; + if (packet.getType() == EntityType.FALLING_BLOCK) { + entity = new FallingBlockEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(), + position, motion, yaw, pitch, headYaw, ((FallingBlockData) packet.getData()).getId()); + } else if (packet.getType() == EntityType.ITEM_FRAME || packet.getType() == EntityType.GLOW_ITEM_FRAME) { + // Item frames need the hanging direction + entity = new ItemFrameEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(), + definition, position, motion, yaw, pitch, headYaw, (Direction) packet.getData()); + } else if (packet.getType() == EntityType.PAINTING) { + entity = new PaintingEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(), + definition, position, motion, yaw, pitch, headYaw, (Direction) packet.getData()); + } else if (packet.getType() == EntityType.FISHING_BOBBER) { + // Fishing bobbers need the owner for the line + int ownerEntityId = ((ProjectileData) packet.getData()).getOwnerId(); + Entity owner = session.getEntityCache().getEntityByJavaId(ownerEntityId); + // Java clients only spawn fishing hooks with a player as its owner + if (owner instanceof PlayerEntity) { + entity = new FishingHookEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(), + position, motion, yaw, pitch, headYaw, (PlayerEntity) owner); + } else { + return; + } + } else { + entity = definition.factory().create(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), + packet.getUuid(), definition, position, motion, yaw, pitch, headYaw); + } + + if (packet.getType() == EntityType.WARDEN) { + WardenData wardenData = (WardenData) packet.getData(); + if (wardenData.isEmerging()) { + entity.setPose(Pose.EMERGING); + } + } + + String def = CUSTOM_ENTITIES.get(session).getIfPresent(entity.getEntityId()); + if (def != null) { + // System.out.println("CUSTOM ENTITY :" + event.entityId() + " | " + def); + entity.setDefinition(LOADED_ENTITY_DEFINITIONS.getOrDefault(def, entity.getDefinition())); + } + + session.getEntityCache().spawnEntity(entity); + } +} \ No newline at end of file