[POC] BlockAPI - Custom Blocks

Discussion in 'Plugin Development' started by Icyene, Sep 30, 2012.

Thread Status:
Not open for further replies.
  1. Offline

    Icyene

    First off, I must ask that if anyone has a better idea for a name, to please share it. With that out of the way, let me begin.

    Just recently I've had an idea that would allow multiple custom blocks to be created, so long as they use a preexisting texture. No, this is not "that thread" again.

    The idea:

    MC allows custom blocks to override pre-existing blocks. You can effectively do something among the lines of : Block.byId[Block.FIRE.id] = null; Block.byId[Block.FIRE.id] = new CustomFire(). That will swap the regular fire with your custom fire.

    All blocks have a slot allocated. If you create a block with an ID the client does not have, it will crash.

    But noone has ever said anything about creating custom IDs, appending custom blocks to them, but sending a legitimate ID to the client. This way, you could have hundreds of blocks, so long as they use a pre-existing texture (this texture is defined by the slot ID).

    This would be done by creating a custom packet handler, most likely using Comphenix 's ProtocolLib, and swap all illegitimate IDs with proper IDs.

    What do you think? Would you find this useful?
     
    Skyost, -_Husky_-, Cirno and 7 others like this.
  2. Offline

    hawkfalcon

    Yes. Woah. If you can make this work...
     
    WarmakerT and exload like this.
  3. Offline

    AstramG

    I'll beta test this if you want, and if this works I'll donate to you :p
     
  4. Offline

    exload

    Yes this would be very helpful! There is so much potential with this idea!
     
    WarmakerT and hawkfalcon like this.
  5. Offline

    Icyene

    Simple POC, will test in an hour or so once I finish my homework:

    Code:Java
    1.  
    2. public class BlockWorker extends BlockStack {
    3.  
    4. private int id;
    5. private Plugin api;
    6. private Semaphore mutex;
    7. private Class<?> blockClass = Block.class;
    8.  
    9. public BlockWorker(Plugin api) {
    10. this.api = api;
    11. this.mutex = new Semaphore(1);
    12. }
    13.  
    14. public void run() {
    15.  
    16. id = Bukkit.getScheduler()
    17. .scheduleSyncRepeatingTask(
    18. api,
    19. new Runnable() {
    20. @Override
    21. public void run() {
    22. try {
    23. //Stack format:
    24. //String name of overriding block
    25. //Object the new block
    26. //The location to place the block at
    27. mutex.acquire();
    28. Triplet qued = stack.get(0);
    29. Object ob = blockClass.getDeclaredField(qued.A.toString());
    30.  
    31. Block set = (Block)ob;
    32.  
    33. //Mod the block
    34. Block.byId[set.id] = null;
    35. Block.byId[set.id] = (Block) qued.B;
    36.  
    37. ((Location)(qued.C)).getBlock().setTypeId(set.id);
    38.  
    39. //Restore block
    40. Block.byId[set.id] = null;
    41. Block.byId[set.id] = set;
    42.  
    43. //Remove from the stack
    44. stack.remove(0);
    45. mutex.release();
    46. } catch (NoSuchFieldException ex) {
    47. Logger.getLogger(BlockWorker.class.getName()).log(Level.SEVERE, null, ex);
    48. } catch (SecurityException ex) {
    49. Logger.getLogger(BlockWorker.class.getName()).log(Level.SEVERE, null, ex);
    50. } catch (InterruptedException ex) {
    51. Logger.getLogger(BlockWorker.class.getName()).log(Level.SEVERE, null, ex);
    52. }
    53.  
    54. }
    55. },
    56. 5L,
    57. 5L);
    58.  
    59. }
    60.  
    61. public void stop() {
    62. Bukkit.getScheduler().cancelTask(id);
    63. }
    64. }
    65.  


    BlockStack:

    Code:Java
    1.  
    2. public class BlockStack {
    3.  
    4. protected ArrayList<Triplet<Object, Object, Location>> stack = new ArrayList<Triplet<Object, Object, Location>>();
    5.  
    6. }
    7.  


    In theory you would add to the stack a String containing the name of the block (e.g. for fire you'd do "FIRE"), your custom block, and a location. The worker task mods this reasonably quickly and should in theory place the block.

    Please note that this is a POC. For all I know, this was just a fluke. If this works, yes, it would be cool. If not, well, it does say POC.
     
    hawkfalcon likes this.
  6. This looks awesome, can't wait to see what happens with this!
     
    hawkfalcon likes this.
  7. Offline

    sayaad

    I like the way you think my friend.

    Anyway...

    Code:java
    1. onBlockBreak(BlockBreakEvent event){
    2. Player player = event.getPlayer();
    3. Block block = event.getPlayer();
    4. World world = block.getLocation().getWorld();
    5.  
    6. if(block.getTypeId == 89){
    7.  
    8. event.setCancelled(true);
    9. block.setTypeId(0);
    10.  
    11. if(block instanceof RedstoneGlowstoneBlock/*simple OOP can do this*/){
    12.  
    13. int random = (int) new Random(5);
    14.  
    15. for(int i = 0; i < random; i++){
    16.  
    17. world.dropItem(block.getLocation(), new ItemStack(Material.GlowStone, 1));
    18. }
    19. }else{
    20. if(block instanceof SugarGlowstoneBlock/*again, some OOP with a boolean*/){
    21.  
    22. world.dropItem(block.getLocation(), new ItemStack(Material.Sugar, 1));
    23. }
    24. }
    25. }
    26. }
    27. //:p
     
  8. Offline

    Icyene

    sayaad
    There is actually a MC method which is called to get the drop type :p No event handling needed.
     
  9. Offline

    Jnorr44

    I got this to work once, but broke it when I decided to re-work the mechanics of the block entirely... Anyways, good luck! This will be awesome!
     
  10. Offline

    sayaad

    ...I need to stay up to date with this API... I feel old now :(
     
    Icyene and hawkfalcon like this.
  11. Offline

    Icyene

    Slight speed bump... I got it to work, yet now it just mods all snow blocks. There must be a way to set a block to a block object, without having to declare the previous block null... Or a way to make the client think its a normal block when really, server side, its completely different.
     
  12. Offline

    AstramG

    How could I use this to make my own block?
     
  13. Offline

    Cirno

    Hi TrustCraft How's it going with your mod?

    Anyways, this is just... unbelievable. How long did it take for you to finalize the details :confused:?
     
    zack6849 and hawkfalcon like this.
  14. Offline

    Icyene

    AstramG You can't, yet.

    Cirno I don't think its too unbelievable, given that it does not seem to work... Block modifications do survive server restart, but they affect all blocks. Remodding the block changes all blocks to that modified state.

    I think I'll just turn this into a simple API which allows custom blocks to be created. Maybe events triggered when blocks are touched. Perhaps light level controls. Dunno. Theoretically block data should be stored separately for each world, so it should be possible to have different blocks on different worlds. Lastly, sending fake chunk changes could possibly create ghost blocks, which could be cool. Imagine a volcano visible to only one player.

    Alas, with the amount of homework I have I doubt I will ever get to finish this. Plus the 1.4 mod API is capable, from what I understand, to do this without insane amounts of work.
     
    hawkfalcon likes this.
  15. Are aware of how net.minecraft.server.Block instances work?
    Unlike CraftBlocks, they are not created by "physical block in the world", but by "block type" in general.
    All of the initialization happens statically in the super Block class:
    https://github.com/Bukkit/CraftBukkit/blob/master/src/main/java/net/minecraft/server/Block.java#L25
    All of them are also written into the "byId" array, so they can be looked up by their numeric id.

    Whatever logic exists in that block always gets the corresponding coordinates and world passed in. Because remember, there is only one instance of the "BlockX" class for each type.
    Random example:
    https://github.com/Bukkit/CraftBukk...java/net/minecraft/server/BlockCrops.java#L21
    In all methods that do something with a block somewhere, you'll see parameters "World world, int i, int j, int k" (world + coordinates).
    In the world, no real "block instance" is stored, just the numerical ids and data values of the blocks.

    Pseudo-code for example for a block break:
    Code:
    when player breaks block with id THE_ID {
        BLOCK_TYPE = Block.byId[THE_ID] // get the global Block instance for the type with id THE_ID
        BLOCK_TYPE.onBroken(world, x, y, z, player)
    }
    So you have the imaginary method "onBroken" in your block, which is the same for all blocks with the same id.

    So here's your problem: While you can easily replace the actual block classes to change behavior of ALL blocks of type X, it's pretty hard to do it on a per-block level.

    In theory, you could go and create wrapper classes for all the hundreds of different blocks, have some sort of pool that stores coordinates, and check it.
    That's about as difficult as recoding the whole game, though :p


    What I could actually imagine a working solution is to introduce new block ids to the game (the same way that mods do it), but intercept network packets to give them a valid "client id" that won't crash the client.
    So you'd have for example the new block id 216, but whenever it gets sent to the client, it is transmuted into id 1, so it will be displayed as stone.
    Makes your world file dependent on the plugin, though, just like modded worlds are.

    Oh, and on the other hand, most of the possibilities that this method creates can already be done through bukkit events - that's one of the big use cases of it anyway.
    But some neat stuff would be easily possible, then, though.

    Sounds like a nice thing to have, but most likely not worth the effort.
     
    Icyene likes this.
  16. Offline

    Icyene

    Bone008 Exactly what I was thinking. As long as the ID sent to the client is valid, you could have as many permutations of a snow class, as long as it is sent as a normal snow. The world file dependency isn't too great: it wouldn't be too hard to unmod everything onDisable. Theoretically, most of the code I have written works for completely swapping a block, but then again that is trivial. It would definitely be cool to have. I reckon something like this would fit snugly in the Bukkit API. It is simple to swap the packet handler for a player, and simply intercept all block packets, and modify all invalid IDs. Do you know which packet this belongs to, though? My best guess it Packet51MapChunk, in the byte[] (assuming those are block IDs, possibly with data values appended).

    I would argue that it is worth the effort, so long as a reasonable way to do it (like intercepting packets) exists.
     
  17. Offline

    Cirno

    Stop being such a killjoy :(
    What matters is that it works.
     
  18. Offline

    Icyene

    Cirno Actually, with what Bone008 mentioned, this is actually a feasible idea. As long as the client thinks its a normal, already existing block, one can have as many versions of it as one wishes. I'll try to do something off this basis tomorrow.

    Got a very basic thing working. Using the Late-Bind-ASM-Agent I wrote about a while ago, I am swapping Packet51MapChunk with my custom packet which replaces all invalid IDs with an alias.

    All aliases are stored in a HashMap<byte, byte>, with the first being the ID of the block, and the second being the alias.

    I will have to rewrite this to use something like ProtocolLib, but for now this should work. Please note it hasn't been tested.

    The code is here. Note the comments. The major part is

    Code:Java
    1.  
    2. byte[] abyte1 = achunksection[l].g();
    3.  
    4. //aliases is the alias of the block IDS. If the key is an ID, it swaps it with
    5. //the value.
    6. for (int id = 0; id < abyte1; ++id)
    7. if (aliases.containsKey(abyte1[id])
    8. abyte1[id] = aliases.get(abyte1[id]);
    9.  


    I am assuming that each ChunkSection is a 16th of a chunk, and has 4096 blocks. abyte1 is a representation of the ChunkSection. I am simply replacing all invalid IDs (anything that has an index in aliases), with their designated equivalents. The server manages blocks on an ID basis, but accepts as many new blocks (until 256, the limit of a byte, in which they are stored). When Packet51MapChunk is sent, the block id is sent as well. If the client tries to render this ID when it doesn't exist, it crashes.

    That simple for loop keeps this from happening. I might have to intercept more packets, for example BlockChange. But after that, everything is trivial.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 29, 2016
  19. Did you test performance?
    In worst-case scenario (no empty chunk sections, all 16 of them are populated), you do 65536 containsKey lookups for autoboxed ints, per chunk, per player.
    That's millions of iterations for a couple of chunks. Not sure if it's as significant as I imagine. It has to be possible, various ore-obfsucator plugins do something like that as well I believe.

    But it looks like a nice start. One probably needs to care about bukkit stuff as well, though. New ids and the Material enum don't like each other by default.

    Oh, and the next pro step would be getting custom TileEntities to work. You could easily create own complex machinery with existing GUIs but without having to simulate everything in the plugin.
    With this, plugins will be very able to go into the direction of what mods can do, as long as they don't need new rendering functionality.

    Custom mobs that simply share the model of existing ones but behave completely different are already common knowledge, maybe blocks will be the next step? ;)
     
  20. Offline

    Icyene

    In terms of performance, I made a MC-optimized version of the validity checker, utilizing the branch prediction fail.

    Code:

    Code:Java
    1.  
    2. import java.util.Arrays;
    3. import java.util.HashMap;
    4. import java.util.Random;
    5.  
    6. public class Test {
    7.  
    8. public static void main(String[] args) {
    9.  
    10. byte[] chunkSection = new byte[65536];
    11. Random rng = new Random();
    12.  
    13. HashMap<Byte, Byte> aliases = new HashMap<Byte, Byte>() {
    14. {
    15.  
    16. put((byte) 0x02, (byte) 0x01);
    17.  
    18. }
    19. };
    20.  
    21. for (int i = 0; i != chunkSection.length; i++) {
    22. chunkSection[I] =[/I] (byte) rng.nextInt();
    23. }
    24.  
    25. Arrays.sort(chunkSection);
    26.  
    27. int players = 20;
    28. int chunksPerPlayer = 16;
    29.  
    30. long start = System.nanoTime();
    31.  
    32. Arrays.sort(chunkSection);
    33.  
    34. for (int chunk = 0; chunk != chunksPerPlayer * players; ++chunk)
    35. for (int i = 0; i != chunkSection.length; i++)
    36. if (chunkSection != 0 && chunkSection != 1
    37. && aliases.containsKey(chunkSection))
    38. chunkSection = aliases.get(chunkSection);
    39.  
    40.  
    41.  
    42. System.out.println("Time: " + (System.nanoTime() - start) / 1000000.);
    43.  
    44. }
    45. }
    46.  
    47.  


    On a relatively bad school computer, with the setting shown above, the code returns in 569.501474 milliseconds. Pretty bad. However, keep in mind:

    • This is random data, so the air/stone optimization will fail. Given that most of MC is air or stone, it is a good idea to add special cases for this. Maybe even water.
    • This is the worst possible case, with the entire chunk loaded. Usually not all chunk sections will be sent.
    • Was tested on a very bad school computer.
    With chunkSection being only 4096, (one chunk section), the code performs extremely well: 17 milliseconds with 20 players + 16 chunks/player.

    One thing I was wondering for future work, how would this affect Bukkit material enums?

    And custom TileEntities would be epic.

    EDIT: Grr... Stupid Curse italics catching arrays with i as an index...

    EDIT 2: Changing chunkSection == 0 && chunkSection == 1 to (chunkSection & 0xF2) speeds up consistently by 50 ms.

    Got it to work.... even faster! I am doing this now by having a HashSet of all aliased IDs. This is faster than using containsKey; I'm just doing it after the HashSet says it contains the item. The new code runs 200ms faster. http://pastie.org/4893344

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 29, 2016
  21. Offline

    brord

    The code looks like girl's thoughts to me lolz :)
    But this looks interesting, Bookmarking
     
    Icyene likes this.
  22. Offline

    Icyene

    HOLY! Using a massive switch case gets the time down to a mere... 84ms... in the worst case scenario of all 65536 blocks loaded per chunk. This is actually very good. Does make the code more unreadable, but meh. The code. In more reasonable tests, it does this in just 2ms. Jackpot. Byte casting in it isn't significant: the compiler optimizes it into this bytecode.
     
  23. Offline

    Comphenix

    Did you try using a simple lookup table? In my limited experience with graphics operations, that's usually a quick, if not the quickest method. This is not that far off from graphics processing either - it's just voxel processing instead of pixel processing.

    In any case, I took the liberty of implementing the BlockAPI with ProtocolLib (the name stuck, but I can change it to something else if you want). It's almost complete - I just have to benchmark the code and add a couple of more packets. Currently, it's intercepting these packets:
    • MAP_CHUNK (51) - A single column (16x16x256) of chunks.
    • MAP_CHUNK_BULK (56) - Multiple MAP_CHUNK packets combined into one. This is probably done to optimize compression.
    • BLOCK_CHANGE (53) - A single block change.
    • MULTI_BLOCK_CHANGE (52) - Multiple blocks have been altered (usually under 64).
    • VEHICLE_SPAWN (23) - For changing the block ID of falling entities (sand or gravel)
    • PICKUP_SPAWN (21) - For changing the texture of item stacks on the ground.
    • SET_SLOT (103) - For changing the block ID of a single item stack in inventories.
    • WINDOW_ITEMS (104) - Entire inventory of item stacks.
    That should probably do it. I also took care to process MAP_CHUNK and MAP_CHUNK_BULK asynchronously, just in case.

    Now, of course, there are a couple of things that are done on the client side. Lighting updates is one of them, so if you're changing from a block that's emitting light (glowstone) to one that isn't, the client might not update the light properly. In addition, mining speed is computed on the client side, causing some minor glitches. And so on. :p

    In any event, you can download the API (with source code) here.

    You must also install the latest version of ProtocolLib (1.2.1).

    To use it, simply put BlockAPI into your Build Path. Then add a dependency to BlockAPI, and you're good to go:
    Code:java
    1.  
    2. public class TestMod extends JavaPlugin {
    3. @Override
    4. public void onLoad() {
    5. // Testing API
    6. BlockAPI api = BlockMod.getAPI();
    7. api.setBlockLookup(MATERIAL_SAND, MATERIAL_GLASS);
    8. api.setItemLookup(MATERIAL_SAND, MATERIAL_GLASS);
    9. }
    10. }
    11.  

    Note that BlockAPI 1.0.0 already adds this conversion (sand -> glass) for testing purposes, so there's no need to make a separate plugin to test it out. I'll remove that once the API is out of the BETA stage.
     
  24. edit: Ninja'd; additions written in italic

    Icyene
    Nice.
    Another idea: How about using an array to store mappings? Either a "boolean[]" of length 256, containing true if the id is an alias.
    Or directly a "byte[]", initialized to a special reserved value (okay, reserve one, we're out of space even when treating it as unsigned, just disallow 0xFF or something as an id). Anything that is an alias has the id as the value.

    Code:
    byte[] aliases = ...;
    final byte specialValue = (byte) 0xFF;
     
    // in the loop
    if(aliases[chunkSection[i] & 0xFF] != specialValue)
        chunkSection[i] = aliases[chunkSection[i] & 0xFF];
    Could be even faster, maybe (if the int conversion for the index isn't too heavy, which I don't think it is).
    Well, maybe not compared to the switch, but it doesn't look as ugly and is more dynamic :p

    Okay, seems like that's more or less the "Lookup table" suggested above. And derpy me didn't consider that you could just map to the same ids for regular blocks instead instead of the if :confused:

    Anyway, I think with that much progress speed is no longer a big issue, a working custom block would be awesome, now :D

    As for bukkit materials, maybe you could just extend the "Material.byId" array? I suppose that's used for Material lookup, and then just map it to the replaced material.
    Well, that would choke horribly at some point, though, since ".getType()" and ".getTypeId()" then returns unmatching results :p
    Maybe ASM your way into bukkit's id methods, could potentially work :p

    Hacky, highly unusual stuff, I love that :)

    Comphenix
    Trying it out right now :D
     
    Icyene and Comphenix like this.
  25. Offline

    Icyene

    Comphenix Awesome! If by lookup table you mean a map or equivalent, then its painfully slow =/ My original tests showed that the conversion, using a map takes ~600ms. Using the massive switch it goes down to 84.

    Once again, awesome start! I'll make a simple API that one can use to register blocks. Very likely, a block using this API would be something like

    Code:Java
    1.  
    2. public class CustomBlock extends BlockAPIBlock {
    3. //And have methods, like
    4.  
    5. public void onEntityTouch(Entity en);
    6.  
    7. public void onBreak();
    8.  
    9. //Most of what MCP has, in any case.
    10. }
    11.  


    Also, a big thing would be a neat way to register blocks, without the user of the API having to use reflection to nms.block methods. I've started working on this :p

    Bone008 ASM would definitely work, though I think this is possible with reflection alone :p
     
  26. Comphenix
    What used to be a desert ... (open)
    [​IMG]

    Awesome work! I'm amazed how simple that step turned out with the ProtocolLib.

    He (and I as well) meant an array. Index is the id, value is the translated id mapping (usually unchanged for all blocks except the custom ones).
    In his BlockAPI example shown in the BlockAPI and Calculations class.
     
  27. Offline

    Comphenix

    Ah, no. I mean using a byte-array like so:
    Code:java
    1. private void translate(ChunkInfo info)
    2. {
    3. // Loop over 16x16x16 chunks in the 16x256x16 column
    4. int dataIndexModifier = 0;
    5.  
    6. for (int i = 0; i < 16; i++) {
    7. // If the bitmask indicates this chunk is sent
    8. if ((info.chunkMask & 1 << i) > 0) {
    9. int indexDataStart = dataIndexModifier * 4096;
    10. int index = info.startIndex + indexDataStart;
    11.  
    12. for (int y = 0; y < 16; y++) {
    13. for (int z = 0; z < 16; z++) {
    14. for (int x = 0; x < 16; x++) {
    15. // Transform block
    16. info.data[index] = blockLookup[info.data[index]];
    17. index++;
    18. }
    19. }
    20. }
    21.  
    22. dataIndexModifier++;
    23. }
    24. }
    25.  
    26. // We're done
    27. }

    As you can see, I simply use a byte[] array to retrieve the correct block ID, for both modified and unmodified blocks. The blockLookup[] array is filled with the numbers 0 - 255 at startup, causing unmodified blocks to be "altered" to the same block ID. That way, we avoid an unnecessary IF-statement.

    Nice. I do a lot of that in ProtocolLib, so I've found it's not terribly difficult, most of the time. You can convert to the NMS classes by using a "getHandle" method, and convert NMS back to Bukkit by using the toBukkit* or getBukkit* methods (they're not consistently named, though).

    Thanks. :)

    Getting the asynchronous code working was the worst part. Both in terms of getting rid of impossible bugs (at first, I had methods that didn't get called even though I called them directly), but also ironing out all the concurrency issues. Designing the API itself was also quite the challenge - I really made me appreciate all the hard work that goes into the Bukkit API itself. The challenge is making the API powerful enough to be useful, yet simple enough to use. :p

    I don't really expect plugins to use ProtocolLib in general, but I can see it being useful for plugin libraries. It didn't take more than a couple of hours to write BlockAPI, so it's definitely nice for prototyping. :)

    I should probably mention that I based the MAP_CHUNK and MAP_CHUNK_BULK code on Orebfuscator, so I have to credit lishid for his hard work. Good thing I'm always releasing my plugins under the GPL. :)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 29, 2016
  28. Offline

    Icyene

    You should call it something like BlockPatcher :p

    On another note, I'm sure I must be doing something horribly wrong, but I cannot seem to get it to work. I have downloaded ProtocolLib and put BlockAPI in the plugin directory, yet blocks still remain sand. Any ideas? No errors appear in console.
     
  29. Offline

    chaseoes

    Lurking here to see where this goes..
    Also, like my new signature?
     
    sayaad and Icyene like this.
  30. Offline

    Cjbolt

    Read everything, never really coded. Mind exploded. *Starts learning code so I can understand fully what this amazing witchcraft is*
     
Thread Status:
Not open for further replies.

Share This Page