Solved How to find closest generated structure?

Discussion in 'Spigot Plugin Development' started by alfalfascout, Sep 9, 2018.

  1. How could I find the closest generated structure to an entity, block, or set of coordinates? I'd like to use this to find monuments and mansions for generating explorer maps. I see that there's a method for this in the minecraft code that is used by cartographers to generate their maps, but it doesn't seem to be accessible through spigot.
     
    • Friendly Friendly x 1
  2. Minecraft 1.11 and 1.12 feature a new command: /locate, so if you looked into the code you can likely find it (when I hop onto my pc I can likely find the code)

    Edited:

    @alfalfascout look into BlockPosition as that's how Spigot uses it for the /locate command.
     
    • Informative Informative x 1
    • Useful Useful x 1
  3. Wouldn't suppressing the console output also suppress it for ops using the command? Or is output suppression something that can be turned on before the command and off after?
     
  4. So it just came to me that if you where to send the command as a console sender you would not get the nearest structure from any "entity, block, or set of coordinates", you would only get the nearest structure from wherever the CommandSender is located, which I found out is the spawn point.

    Secondly, the farther away something is the longer it takes to find said object. When I typed "locate Monument", it gave me a result instantly back for something 1504 blocks away but took at least 17 seconds to grab a Mansion that was 22358 blocks away.

    Is what you are making tied to a specific player? If so you could suppress actual chat and have a toggle set so that whenever you want it will only suppress chat once.


    The last option is to use NMS code to find what method is used to locate structures and just copy that. It would require you to use NMS code however.
     
    • Useful Useful x 1
  5. Ok @alfalfascout, I took some time to actually work out what you can do. Here is what I did and I will walk you through the steps so you can learn by example.


    Step 1. We know Minecraft has a /locate command but Spigot does not have a way to use the method Minecraft uses, so we know we either have to A) Copy the method Minecraft uses, either it be math or whatnot, or B) Use the same Method Minecraft uses.

    So both steps require us to actually look at the minecraft code involved, so I open up my Java Decompiler software, I use something called Luyten (https://github.com/deathmarine/Luyten/releases).

    Now that I have that downloaded I want to open to our latest version of spigot, and go into the net.minecraft.server (followed by version) package.
    I need to now find whatever class is used for the locate command, lucky enough its titled CommandLocate, if it wasn't so easy I would search "locate" and hope I can find it that way, if I couldn't find it that way I would use key words like "Monument" or the likes. In the end its one of the hardest parts of NMS is just finding the methods used.


    Anyways, located in "CommandLocate" shows me this:

    upload_2018-9-9_7-14-3.png
    I can tell based off the "commands.locate.success" the command is a success, and the new ChatMessage is the returning message that gives out the location. based off the message being formed, I can see a.getX() is where the X location is stored, so now I just need to look at the object named a inside that method,
    upload_2018-9-9_7-15-32.png
    Here I can see that a is a BlockPosition object and it is obtaining said block position by using the method a(String, BlockPosition, Int, Boolean) in the World Class.

    So we are getting somewhere. Now we need to get the nms equivalent of World, and to do that we first use Craftbukkit'ts CraftWorld, and then get the handle, something like this:
    Code (Text):

           BlockPosition bp = new BlockPosition(p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ());
           World world = p.getWorld();
           CraftWorld cWorld = (CraftWorld) world;
           WorldServer worldServer = cWorld.getHandle();
           BlockPosition monumentPosition = worldServer.a("Monument", bp, 100,false);
     
    OR SIMPLY
    Code (Text):

           BlockPosition bp = new BlockPosition(p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ());      
           BlockPosition monumentPosition = ((CraftWorld) p.getWorld()).getHandle().a("Monument", bp, 100,false);
     
    The method a() takes in a BlockPosition, so we needed to actually create one. That was easy to figure out, when I typed in new BlockPosition it automatically told me the parameters needed to be 3 int's, so I figured thats X, Y, and Z.
    Also when looking at the a() method just by looking I could tell the first paramater was the thing we where searching since it was a string, second was a BlockPosition so it HAD to be the origin position, the 3rd was hard coded at 100 so no clue what that is but I left it hard coded in, and same for the boolean set as false.

    To do all of this we needed to import different version specific method. If we where to finish our code now we would run into an issue which is that only people running the specific server version you used to code the plugin in could use it, because the package names are server specific.

    upload_2018-9-9_7-19-46.png

    So we need to prevent this by not importing our package and extending it as part of the class method, but with some magic using Reflections. I went ahead and did it for you below but please take note on how I did it so you don't need to ask people in the future. Essentially I grab the class name by getting the bukkits version, and placing it inside of "net.minecraft.server." + "." + (bukkit version) + "." + (class name)
    Once we have that class we can do stuff like grabbing methods and invoking those methods. When using the code below I effectly have removed all imports dealing with net.minecraft.server and craftbukkit as well. This code realisticcly should work until Minecraft themselves decides to change the methods fields or for some reason their obfuscation changes the methods name from "a" to something else.

    To use the following code it could be something like getStructure(player.getLocation(), "Monument")

    What you do with that string is up to you. you can try to parse it however you want. If you can't turn it into a Location let me know and I will spend some more time on it.

    Code (Text):

        private String getStructure(Location l, String structure) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException, InstantiationException {
           Method getHandle = l.getWorld().getClass().getMethod("getHandle");
           Object nmsWorld = getHandle.invoke(l.getWorld());
           Object blockPositionString = nmsWorld.getClass().getMethod("a", new Class[] { String.class, getNMSClass("BlockPosition"), int.class, boolean.class }).invoke(nmsWorld, structure,getBlockPosition(l), 100,false);
           return blockPositionString.toString();
       }

     
       private Class<?> getNMSClass(String nmsClassString) throws ClassNotFoundException {
            String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
            String name = "net.minecraft.server." + version + nmsClassString;
            Class<?> nmsClass = Class.forName(name);
            return nmsClass;
        }
       private Object getBlockPosition(Location loc) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{
            Class<?> nmsBlockPosition = getNMSClass("BlockPosition");
            Object nmsBlockPositionInstance = nmsBlockPosition
                    .getConstructor(new Class[] { Double.TYPE, Double.TYPE, Double.TYPE })
                    .newInstance(new Object[] { loc.getX(), loc.getY(), loc.getZ() });
            return nmsBlockPositionInstance;
        }

     
     
    #6 Ugleh, Sep 9, 2018
    Last edited: Sep 9, 2018
    • Informative Informative x 2
    • Like Like x 1
    • Useful Useful x 1
  6. Thanks @Ugleh !

    Now I have a MapCursor with the closest structure in it, and all I have to do is figure out how to apply that to a FILLED_MAP ItemStack...
     
  7. Look at this post from 2 months ago Map Item changes in 1.13?

    It uses WorldMap and worldServer.b, which I told you we don't want to use because it will import stuff. If you can figure out how to convert it on your own that would be great. (If not I can spend some time doing it for you but it would awesome if you try first)
     
  8. I'm trying, but I can't for the life of me find the functions in the java docs to
    1. get the MapCanvas to attach the MapCursor
    2. get the id of the MapView I had the world create, or its renderer or canvas or something so I can attach it to the MapMeta.

    Maybe tomorrow I'll try looking through the server code.
     
  9. I was wrong in my assumption earlier. I never messed with maps before but you should not have to use NMS code when it comes to maps, but it should look something like this:

    Code (Text):

           Player p = (Player)sender;
            ItemStack i = new ItemStack(Material.FILLED_MAP, 1);
            MapView mapView = Bukkit.createMap(p.getWorld());
           
            MapMeta mapMeta = (MapMeta) i.getItemMeta();
            mapMeta.setMapId(mapView.getId());
            mapView.addRenderer(new TestRenderer());
            i.setItemMeta(mapMeta);
            p.getInventory().addItem(i);
     


    TestRenderer class:

    Code (Text):


    public class TestRenderer extends MapRenderer{

       @Override
       public void render(MapView view, MapCanvas canvas, Player player) {
           MapCursorCollection cursors = new MapCursorCollection();
           cursors.addCursor(60, 30, (byte) 1);
           canvas.setCursors(cursors);
       }

    }

     

    IDK how you get the actual world drawn on there but I am sure you could figure it out.
     
  10. I'm very disheartened. I feel like if there's already a built-in way to make explorer maps, I should be able to use it. I'm staring right at it. I don't want to reinvent the wheel. I'm just updating my plugin to help people customize villager trade lists.

    This is as far as I got before I totally deflated. Maybe it will help someone. I'm going to see if I can try to modify spigot itself.

    Code (Java):
    public ItemStack handleFilledMap(FileConfiguration f, ItemStack mapItem, String path, Location villagerLocation) {
            MapMeta mapMeta = (MapMeta) mapItem.getItemMeta();
            //get user-specified map type
            if (f.contains(path + ".type")) {
                //get structure nearest to villager

                Location structureLocation = getStructureLocation(villagerLocation, f.getString(path + ".type"));

                // create a MapCursor marking the structure
                MapCursor.Type markerType = f.getString(path + ".type").equalsIgnoreCase("mansion") ?
                        MapCursor.Type.MANSION : MapCursor.Type.TEMPLE;
                MapCursorCollection cursors = new MapCursorCollection();
                MapCursor structureMarker = new MapCursor((byte)structureLocation.getX(), (byte)structureLocation.getY(), (byte)0,
                        markerType, true);
                cursors.addCursor(structureMarker);

                MapView mapView = plugin.getServer().createMap(villagerLocation.getWorld());
                mapMeta.setMapId(mapView.getId());

                mapView.setCenterX((int) structureLocation.getX());
                mapView.setCenterZ((int) structureLocation.getY());
                mapView.setUnlimitedTracking(true);

                CVTMapRenderer cursorRenderer = new CVTMapRenderer();
                cursorRenderer.setCursors(cursors);
                mapView.addRenderer(cursorRenderer);

                mapItem.setItemMeta(mapMeta);

            }

            return mapItem;
        }

    class CVTMapRenderer extends MapRenderer {
            MapCursorCollection mapCursors;

            @Override
            public void render(MapView mapView, MapCanvas mapCanvas, Player player) {
                mapCanvas.setCursors(mapCursors);
            }

            public void setCursors(MapCursorCollection mapCursors) {
                this.mapCursors = mapCursors;
            }
        }
     

    Attached Files:

    #11 alfalfascout, Sep 11, 2018
    Last edited: Sep 12, 2018
  11. I didn't know what your initial intentions was. Are you trying to make a 100% identical explorer map, or are you trying to make a map that acts as a compass to the closest (in your case) mansion? Did you want the map to be filled with the surrounding terrain or the terrain by the structure?
     
  12. Sorry I wasn't very clear. I thought if I had the coordinates I could use spigot functions to make one, like you can make a banner if you have the color and shapes. I got very frustrated. I am indeed looking to make identical explorer maps.
     
  13. When I have free time tonight I will look into the nms code to see how Minecraft makes them.
     
  14. It should be in the entity/passive/villager code. A search for "filled_map" should bring it up. I just... don't know how to get it from there to here.
     
  15. This is the code without reflections, as you can see there is a Type.RED_X which is the icon where the structure will be located at.
    The function takes in the World, the location to find the nearest structure, the Structure Type in String form, and the items display name.

    I am converting it to reflections as we speak but I have ran into a problem which I created a new thread about. Will update you when I get the answers needed.

    Code (Text):

           ItemStack monumentMap = createExplorerMap(player.getWorld(), player.getLocation(), "Buried_Treasure", "Buried Treasure Map");
            p.getInventory().addItem(monumentMap);
     
    createExplorerMap:
    Code (Text):

        private ItemStack createExplorerMap(World world, Location loc, String structureType, String mapName) {
           net.minecraft.server.v1_13_R2.World w = ((CraftWorld) world).getHandle();
           BlockPosition somePosition = new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
           BlockPosition structurePosition = w.a(structureType, somePosition, 100, true);
           net.minecraft.server.v1_13_R2.ItemStack itemstack = ItemWorldMap.a(w, structurePosition.getX(), structurePosition.getZ(), (byte)2, true, true);
            ItemWorldMap.a(w, itemstack);
            WorldMap.a(itemstack, structurePosition, "+", Type.RED_X);
            itemstack.a(new ChatMessage(mapName, new Object[0]));
         
            return CraftItemStack.asBukkitCopy(itemstack);
       }
     
     
    • Informative Informative x 1
  16. Done. This sort of thing hurts my head sometimes haha. Anyways, it is always fun working on something you don't know and learning it by doing, which is something I did myself today. The class below is my command class that I used to test it out. The methods are all there and the same, except I no longer set the item name via NMS code. If you don't need to use reflections than don't, just set the item name after obtaining the ItemStack, in this case inside of the command


    Lets finally set this thread to Solved.

    Code (Text):

    package com.ugleh.testplugin;

    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;

    import org.bukkit.Bukkit;
    import org.bukkit.Location;
    import org.bukkit.World;
    import org.bukkit.command.Command;
    import org.bukkit.command.CommandExecutor;
    import org.bukkit.command.CommandSender;
    import org.bukkit.entity.Player;
    import org.bukkit.inventory.ItemStack;

    public class CommandTest implements CommandExecutor{
     
     
       @Override
       public boolean onCommand(CommandSender sender, Command command, String rawCommand, String[] Args) {
           if(!(sender instanceof Player)) return true;
           Player p = (Player)sender;
           try {
               ItemStack monumentMap = (ItemStack) createExplorerMap(p.getWorld(), p.getLocation(), "Mansion");
               p.getInventory().addItem(monumentMap);
             
           } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                   | InvocationTargetException | ClassNotFoundException | InstantiationException e) {
               e.printStackTrace();
           }

           return true;
       }

       private Object createExplorerMap(World world, Location loc, String structureType) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException, InstantiationException {
           Object structurePosition = getStructure(loc, structureType);
           int structureX = (int) getNMSClass("BlockPosition").getMethod("getX").invoke(structurePosition);
           int structureZ = (int) getNMSClass("BlockPosition").getMethod("getZ").invoke(structurePosition);

           Method getHandle = loc.getWorld().getClass().getMethod("getHandle");
            Object nmsWorld = getHandle.invoke(loc.getWorld());
         
            Object itemStack = getNMSClass("ItemWorldMap").getDeclaredMethod("a", new Class[] { getNMSClass("World"), int.class, int.class, byte.class, boolean.class, boolean.class }).invoke(getNMSClass("ItemWorldMap"), nmsWorld, structureX, structureZ, (byte)2, true, true);
            getNMSClass("ItemWorldMap").getDeclaredMethod("a", new Class[] { getNMSClass("World"), getNMSClass("ItemStack") }).invoke(getNMSClass("ItemWorldMap"), nmsWorld, itemStack);
            Object icon = getNMSClass("MapIcon$Type").getMethod("valueOf", new Class[] {String.class}).invoke(getNMSClass("MapIcon$Type"), "RED_X");
            getNMSClass("WorldMap").getMethod("a", new Class[] {getNMSClass("ItemStack"), getNMSClass("BlockPosition"),  String.class, getNMSClass("MapIcon$Type")}).invoke(getNMSClass("WorldMap"), itemStack, structurePosition, "+", icon);
         
            return getBukkitClass("inventory.CraftItemStack").getMethod("asBukkitCopy", new Class[] {getNMSClass("ItemStack")}).invoke(getBukkitClass("inventory.CraftItemStack"), itemStack);
       }
     
     
     

       private Object getStructure(Location l, String structure) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException, InstantiationException {
            Method getHandle = l.getWorld().getClass().getMethod("getHandle");
            Object nmsWorld = getHandle.invoke(l.getWorld());
            Object blockPosition = nmsWorld.getClass().getMethod("a", new Class[] { String.class, getNMSClass("BlockPosition"), int.class, boolean.class }).invoke(nmsWorld, structure,getBlockPosition(l), 100,false);
            return blockPosition;
        }
     
          private Class<?> getNMSClass(String nmsClassString) throws ClassNotFoundException {
               String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
               String name = "net.minecraft.server." + version + nmsClassString;
               Class<?> nmsClass = Class.forName(name);
               return nmsClass;
           }
       
          private Class<?> getBukkitClass(String nmsClassString) throws ClassNotFoundException {
               String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
               String name = "org.bukkit.craftbukkit." + version + nmsClassString;
               Class<?> nmsClass = Class.forName(name);
               return nmsClass;
           }

          private Object getBlockPosition(Location loc) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{
               Class<?> nmsBlockPosition = getNMSClass("BlockPosition");
               Object nmsBlockPositionInstance = nmsBlockPosition
                       .getConstructor(new Class[] { Double.TYPE, Double.TYPE, Double.TYPE })
                       .newInstance(new Object[] { loc.getX(), loc.getY(), loc.getZ() });
               return nmsBlockPositionInstance;
           }
     
    }
     
     
  17. I put it through some testing. It doesn't seem to be generating explorer maps for my villagers, though... just regular maps, but with no markers. Does this only work if a player requests a map?

    Code (Java):
    public ItemStack handleFilledMap(FileConfiguration f, ItemStack mapItem, String path, Location villagerLocation) {
            //get user-specified map type
            if (f.contains(path + ".type")) {

                try {
                    mapItem = (ItemStack) createExplorerMap(villagerLocation.getWorld(), villagerLocation, f.getString(path + ".type"));
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }

            return mapItem;
        }
     

    Attached Files:

  18. You shouldn't need the players location. The first parameter is the world the map should be generated in, the second is the origin point for the "nearest" structure, and the second should be just a string like Mansion, VIllage, Buried_Treasure. If you type in /locate and notice the autocomplete those are all the things you can put in there.

    What does f.getString(path + ".type") return?

    ItemStack explorerMap = (ItemStack) createExplorerMap(p.getWorld(), p.getLocation(), "Mansion");
    p.getInventory().addItem(explorerMap);
     
  19. f.getString(path + ".type") looks in the passed FileConfiguration for a string containing the map type. It looks like this in the config file:

    Code (YAML):
    ...
    cartographer
    :
      tier1
    :
        trade1
    :
          result
    :
            material
    : filled_map
            type
    : monument
    ...