Jumper251 / AdvancedReplay

Minecraft plugin to record players on your server
GNU General Public License v3.0
141 stars 62 forks source link

Add TNT tracking #170

Closed Nubebuster closed 2 months ago

Nubebuster commented 2 months ago

Forgive me for the formatting changes. It found the formatting is inconsistent and I've chosen the more common formatting.

This tracks igniting TNT by player, and the TNT that is triggered thereafter. It also sends a destroy packet for the TNT based on when it explodes because the fuseticks are a server side thing and the derived TNT has random fuse ticks.

I've tested this and it works pretty great. Only problem is that due to TNTPrimeEvent being bugged (in 1.8) we cannot track the TNT movement on spawn so well. Furthermore, TNT being launched by other TNT unfortunately does not show in the replay. But this is already much better than nothing.

Jumper251 commented 2 months ago

Thanks for the PR. What version(s) did you test this on?

Nubebuster commented 2 months ago

1.8 I don't see any reason why it wouldn't work on other versions though. It only sends a spawn and destroy packet. As for the event handlers I don't see any reason why it wouldn't work either.

Nubebuster commented 2 months ago

Please hold this pull request. I've decided I want to add the velocity and handle the tracking better overall.

Nubebuster commented 2 months ago

I've gotten further than I expected. Unfortunately I went down a rabbit hole creating the nonexistent WrapperPlayServerExplosion packet. I've used the build tools to decompile many versions as there is no reliable documentation for this kind of thing.

1.20 is now fully working! I am going to finish 1.8 this weekend or monday.

Side note: I also fixed/am fixing a few other compatibility issues.

Jumper251 commented 2 months ago

Looks like this packet is sent after an explosion https://wiki.vg/Protocol#Explosion, it will probably look a bit different for 1.8

Nubebuster commented 2 months ago

Looks like this packet is sent after an explosion https://wiki.vg/Protocol#Explosion, it will probably look a bit different for 1.8

Correct. I have this part already working and implemented.

I now need to figure out at which version the server stops sending the sound packet. I already know 1.8 needs a sound packet. And 1.20 has it included. What happens with the versions in between I dont know.

I've also rewritten the logic to be more packet based, as I was getting my fingers knotted trying to keep track of the entity ids for the velocity packets. But I realised that most of the logic can be done in the packet listener. I only use the spigot listener for determining which block needs to be removed for the tnt spawn, and what the block data was before the explosion. But sending the block remove packet is included in the explosion packet. So I've added a no-send flag to the BlockData class. I will push these changes in the coming days.

I'm excited to contribute. I was disappointed at first that the first video shows TNT not working, so I decided I wanted to fix that.

Jumper251 commented 2 months ago

I now need to figure out at which version the server stops sending the sound packet. I already know 1.8 needs a sound packet. And 1.20 has it included. What happens with the versions in between I dont know.

Don't worry too much about other versions, I think going forward it makes the most sense to mainly support 1.8 and the current latest minecraft version (1.21) for new features.

Nubebuster commented 2 months ago
                        Material material = getBlockMaterial(blockChange.getBefore());

                        System.out.println(material);

                        if (material == null) {
                            System.out.println("Material is null");
                        } else {
                            // Use equals for comparison
                            System.out.println(material.equals(Material.TNT));
                            System.out.println(material == Material.TNT);

                            // Log the material's class name to check if it's a standard Material enum
                            System.out.println("Material class: " + material.getClass().getName());

                            try {
                                // Convert to string safely
                                System.out.println(material.toString());
                            } catch (Exception e) {
                                System.out.println("Error converting material to string: " + e.getMessage());
                            }

                            try {
                                // Convert to name safely
                                System.out.println(material.name());
                            } catch (Exception e) {
                                System.out.println("Error converting material to name: " + e.getMessage());
                            }
                        }

TNT false false Material class: org.bukkit.Material Error converting material to string: Range [7, 3) out of bounds for length 3 Error converting material to name: Range [7, 3) out of bounds for length 3

I am having some trouble detecting if I should play a fuse sound. Could someone explain how the above happens in 1.20/1.21? It's quite confusing to me. In the new versions blockChange.getAfter().getId() returns 0 always

Nubebuster commented 2 months ago

It has to do with specifying an api-version in the plugin.yml says someone from papermc. I am completely inexperienced with the multi version material stuff. So I'd like to request some help from you @Jumper251

Here is my current code in the ReplayingUtils

    private void setBlockChange(BlockChangeData blockChange) {
        final Location loc = LocationData.toLocation(blockChange.getLocation());

        if (ConfigManager.WORLD_RESET && !this.replayer.getBlockChanges().containsKey(loc)) {
            this.replayer.getBlockChanges().put(loc, blockChange.getBefore());
        }

        new BukkitRunnable() {

            @SuppressWarnings("deprecation")
            @Override
            public void run() {
                if (blockChange.doPlayEffect()) {
                    if (blockChange.getAfter().getId() == 0 && blockChange.getBefore().getId() != 0 && MaterialBridge.fromID(blockChange.getBefore().getId()) != Material.FIRE && blockChange.getBefore().getId() != 11 && blockChange.getBefore().getId() != 9 && blockChange.getBefore().getId() != 10 && blockChange.getBefore().getId() != 8) {
                        loc.getWorld().playEffect(loc, Effect.STEP_SOUND, blockChange.getBefore().getId(), 15);
                    }
                } else if (blockChange.doBlockChange()) {// && MaterialBridge.fromID(blockChange.getBefore().getId()) == Material.TNT
                    if (VersionUtil.isCompatible(VersionEnum.V1_8)) {
                        loc.getWorld().playSound(loc, Sound.valueOf("FUSE"), 1, 1);
                    } else {
                        System.out.println("PLAY FUZE");

                        Material material = getBlockMaterial(blockChange.getBefore());

                        if (material == null) {
                            System.out.println("Material is null");
                        } else {
                            // Log material details
                            System.out.println("Material: " + material);
                            System.out.println("Material class: " + material.getClass().getName());
                            System.out.println("Material.equals(Material.TNT): " + material.equals(Material.TNT));
                            System.out.println("Material hashCode: " + material.hashCode());

                            // Inspect methods and properties
                            try {
                                System.out.println("Material toString: " + material.toString());
                            } catch (Exception e) {
                                System.out.println("[AdvancedReplay] [STDOUT] Error converting material to string: ");
                                e.printStackTrace();
                            }

                            try {
                                System.out.println("Material name: " + material.name());
                            } catch (Exception e) {
                                System.out.println("[AdvancedReplay] [STDOUT] Error converting material to name: ");
                                e.printStackTrace();
                            }

                            // Additional checks if needed
                            try {
                                System.out.println("Material ordinal: " + material.ordinal());
                            } catch (Exception e) {
                                System.out.println("[AdvancedReplay] [STDOUT] Error accessing material ordinal: ");
                                e.printStackTrace();
                            }
                        }

                        loc.getWorld().playSound(loc, Sound.ENTITY_TNT_PRIMED, 1, 1);
                    }
                }

                int id = blockChange.getAfter().getId();
                int subId = blockChange.getAfter().getSubId();

                if (id == Material.STATIONARY_WATER.getId()) id = Material.WATER.getId();
                if (id == Material.STATIONARY_LAVA.getId()) id = Material.LAVA.getId();

                if (ConfigManager.REAL_CHANGES) {
                    if (VersionUtil.isCompatible(VersionEnum.V1_13) || VersionUtil.isCompatible(VersionEnum.V1_14) || VersionUtil.isCompatible(VersionEnum.V1_15) || VersionUtil.isCompatible(VersionEnum.V1_16) || VersionUtil.isCompatible(VersionEnum.V1_17) || VersionUtil.isCompatible(VersionEnum.V1_18) || VersionUtil.isCompatible(VersionEnum.V1_19) || VersionUtil.isCompatible(VersionEnum.V1_20) || VersionUtil.isCompatible(VersionEnum.V1_21)) {
                        loc.getBlock().setType(getBlockMaterial(blockChange.getAfter()), true);
                    } else {
                        loc.getBlock().setTypeIdAndData(id, (byte) subId, true);
                    }
                } else if (blockChange.doBlockChange()) {
                    if (VersionUtil.isCompatible(VersionEnum.V1_13) || VersionUtil.isCompatible(VersionEnum.V1_14) || VersionUtil.isCompatible(VersionEnum.V1_15) || VersionUtil.isCompatible(VersionEnum.V1_16) || VersionUtil.isCompatible(VersionEnum.V1_17) || VersionUtil.isCompatible(VersionEnum.V1_18) || VersionUtil.isCompatible(VersionEnum.V1_19) || VersionUtil.isCompatible(VersionEnum.V1_20) || VersionUtil.isCompatible(VersionEnum.V1_21)) {
                        replayer.getWatchingPlayer().sendBlockChange(loc, getBlockMaterial(blockChange.getAfter()), (byte) subId);
                    } else {
                        replayer.getWatchingPlayer().sendBlockChange(loc, id, (byte) subId);
                    }
                }

            }
        }.runTask(ReplaySystem.getInstance());
    }

The goal is to check if the blockChange.getBefore() was TNT. If so, we need to play the fuse sound. This works in 1.8 with

&& MaterialBridge.fromID(blockChange.getBefore().getId()) == Material.TNT

But I cannot get it to work in the newer versions

Jumper251 commented 2 months ago

At some point I want to switch to the new API, until then a workaround might be to directly acccess the SerializableItemStack in the BlockChangeData when above 1.13.

blockChange.getBefore().getItemStack().getItemStack().get("type").equals("TNT")
Nubebuster commented 2 months ago

https://youtu.be/x3SuwYr0Oyk No sounds recorded unfortunately. But they work :)

As far as I am aware this pull request is ready to merge.

Jumper251 commented 2 months ago

Looks good, thank you for the contribution. I will post the update in the next couple of days.

PedroMPagani commented 1 month ago

It'd be nice if this PR can become a end-crystal explosion support as well.