[Resource] Server Side lighting, no it isn't just client side.

Discussion in 'Resources' started by RingOfStorms, Jun 21, 2013.

  1. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Edit: Updated code with some of the things people have said so far.
    Edit2: Updated code again after testing and needing to change some things.
    Edit3: Updated code based on some comments below.
    Edit4: Updated code based on some comments below.
    Edit5: Added New Version by Comphenix

    Well after some requests on making non light emitting blocks emit light, @BeYkeRYkt informed me that it is acutally possible, after I had said it was only client side. In BeYkeRYkt's plugin BkrTorchLight, people can hold a torch and have it emit light, instead of placing it. There is a GitHub page that contains how BeYkeRYkt does it. However, the code is messy, and honestly does way more than what is needed. It can also cause phantom permanent light if not used correctly.

    In BeYk's code I noticed that he add's chunks to chunkCoordIntPairQueue. I did some research and found it in the NMS code and found out what he was actually doing. This led me to the packet that is sent for the chunks. Now with that information, I was able to take what BeYk had and reduce it a lot to only contain a few lines of code.

    Here is what I came up with, go ahead and use/change it.

    Video


    Comphenix's Version
    Gist at GitHub
    Download at Gist link above.
    Want to see the cool changes that people have recommended!?
    Revision Log
    Code:java
    1.  
    2. package com.comphenix.example;
    3.  
    4. import java.lang.reflect.Field;
    5. import java.lang.reflect.Method;
    6. import java.util.List;
    7.  
    8. import net.minecraft.server.v1_5_R3.Entity;
    9. import net.minecraft.server.v1_5_R3.EntityHuman;
    10. import net.minecraft.server.v1_5_R3.EnumSkyBlock;
    11. import net.minecraft.server.v1_5_R3.IWorldAccess;
    12. import net.minecraft.server.v1_5_R3.PlayerChunkMap;
    13. import net.minecraft.server.v1_5_R3.World;
    14. import net.minecraft.server.v1_5_R3.WorldServer;
    15.  
    16. import org.bukkit.Location;
    17. import org.bukkit.block.Block;
    18. import org.bukkit.block.BlockFace;
    19. import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    20.  
    21. public class LightSourceEx {
    22. private static Method cachedPlayerChunk;
    23. private static Field cachedDirtyField;
    24.  
    25. // For choosing an adjacent air block
    26. private static BlockFace[] SIDES = {
    27. BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH,
    28. BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST };
    29.  
    30. /**
    31. * Create light with level at a location.
    32. * @param loc - which block to update.
    33. * @param level - the new light level.
    34. */
    35. public static void createLightSource(Location loc, int level) {
    36. WorldServer nmsWorld = ((CraftWorld) loc.getWorld()).getHandle();
    37. int oldLevel = loc.getBlock().getLightLevel();
    38.  
    39. // Sets the light source at the location to the level
    40. nmsWorld.b(EnumSkyBlock.BLOCK, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), level);
    41.  
    42. // Send packets to the area telling players to see this level
    43. updateChunk(nmsWorld, loc);
    44.  
    45. // If you comment this out it is more likely to get light sources you can't remove
    46. // but if you do comment it, light is consistent on relog and what not.
    47. nmsWorld.b(EnumSkyBlock.BLOCK, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), oldLevel);
    48. }
    49.  
    50. private static Block getAdjacentAirBlock(Block block) {
    51. // Find the first adjacent air block
    52. for (BlockFace face : SIDES) {
    53. // Don't use these sides
    54. if (block.getY() == 0x0 && face == BlockFace.DOWN)
    55. continue;
    56. if (block.getY() == 0xFF && face == BlockFace.UP)
    57. continue;
    58.  
    59. Block candidate = block.getRelative(face);
    60.  
    61. if (candidate.getType().isTransparent()) {
    62. return candidate;
    63. }
    64. }
    65. return block;
    66. }
    67.  
    68. /**
    69. * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
    70. * @param loc - location to the block that was updated.
    71. */
    72. @SuppressWarnings("rawtypes")
    73. private static void updateChunk(WorldServer nmsWorld, Location loc) {
    74. try {
    75. PlayerChunkMap map = nmsWorld.getPlayerChunkMap();
    76. IWorldAccess access = countLightUpdates(loc.getWorld(), map);
    77. nmsWorld.addIWorldAccess(access);
    78.  
    79. // Update the light itself
    80. Block adjacent = getAdjacentAirBlock(loc.getBlock());
    81. nmsWorld.A(adjacent.getX(), adjacent.getY(), adjacent.getZ());
    82.  
    83. // Remove it again
    84. Field field = World.class.getDeclaredField("u");
    85. field.setAccessible(true);
    86. ((List) field.get(nmsWorld)).remove(access);
    87.  
    88. int chunkX = loc.getBlockX() >> 4;
    89. int chunkZ = loc.getBlockZ() >> 4;
    90.  
    91. // Make sure the block itself is marked
    92. map.flagDirty(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
    93.  
    94. // Look for player chunks immediately around the block
    95. for (int dX = -1; dX <= 1; dX++) {
    96. for (int dZ =-1; dZ <=1; dZ++) {
    97. // That class is package private unfortunately
    98. Object playerChunk = getPlayerCountMethod().invoke(map, chunkX + dX, chunkZ + dZ, false);
    99.  
    100. if (playerChunk != null) {
    101. Field dirtyField = getDirtyField(playerChunk);
    102. int dirtyCount = (Integer) dirtyField.get(playerChunk);
    103.  
    104. System.out.println("Dirty count: " + dirtyCount);
    105.  
    106. // Minecraft will automatically send out a Packet51MapChunk for us,
    107. // with only those segments (16x16x16) that are needed.
    108. if (dirtyCount > 0) {
    109. dirtyField.set(playerChunk, 64);
    110. }
    111. }
    112. }
    113. }
    114. map.flush();
    115.  
    116. } catch (SecurityException e) {
    117. throw new RuntimeException("Access denied", e);
    118. } catch (ReflectiveOperationException e) {
    119. throw new RuntimeException("Reflection problem.", e);
    120. }
    121. }
    122.  
    123. private static IWorldAccess countLightUpdates(final org.bukkit.World world, final PlayerChunkMap map) {
    124. return new IWorldAccess() {
    125. @Override
    126. //markBlockForUpdate
    127. public void a(int x, int y, int z) {
    128. map.flagDirty(x, y, z);
    129. }
    130.  
    131. @Override
    132. //markBlockForRenderUpdate
    133. public void b(int x, int y, int z) {
    134. map.flagDirty(x, y, z);
    135. }
    136.  
    137. @Override
    138. //destroyBlockPartially
    139. public void b(int arg0, int arg1, int arg2, int arg3, int arg4) { }
    140.  
    141. @Override
    142. //playAuxSFX
    143. public void a(EntityHuman arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { }
    144.  
    145. @Override
    146. //markBlockRangeForRenderUpdate
    147. public void a(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
    148. // Ignore
    149. }
    150.  
    151. @Override
    152. //broadcastSound
    153. public void a(int arg0, int arg1, int arg2, int arg3, int arg4) { }
    154.  
    155. @Override
    156. //playSound
    157. public void a(String arg0, double arg1, double arg2, double arg3, float arg4, float arg5) {
    158. }
    159.  
    160. @Override
    161. //playSoundToNearExcept
    162. public void a(EntityHuman arg0, String arg1, double arg2, double arg3, double arg4, float arg5,float arg6) {
    163. }
    164.  
    165. @Override
    166. //spawnParticle
    167. public void a(String arg0, double arg1, double arg2, double arg3, double arg4, double arg5, double arg6) { }
    168.  
    169. @Override
    170. //playRecord
    171. public void a(String arg0, int arg1, int arg2, int arg3) { }
    172.  
    173. @Override
    174. //onEntityCreate
    175. public void a(Entity arg0) { }
    176.  
    177. @Override
    178. //onEntityDestroy (probably)
    179. public void b(Entity arg0) { }
    180. };
    181. }
    182.  
    183. private static Method getPlayerCountMethod() throws NoSuchMethodException, SecurityException {
    184. if (cachedPlayerChunk == null) {
    185. cachedPlayerChunk = PlayerChunkMap.class.getDeclaredMethod("a", int.class, int.class, boolean.class);
    186. cachedPlayerChunk.setAccessible(true);
    187. }
    188. return cachedPlayerChunk;
    189. }
    190.  
    191. private static Field getDirtyField(Object playerChunk) throws NoSuchFieldException, SecurityException {
    192. if (cachedDirtyField == null) {
    193. cachedDirtyField = playerChunk.getClass().getDeclaredField("dirtyCount");
    194. cachedDirtyField.setAccessible(true);
    195. }
    196. return cachedDirtyField;
    197. }
    198. }
    199.  


    Original Version
    Gist at GitHub
    Download at Gist link above.
    Want to see the cool changes that people have recommended!?
    Revision Log
    Code:java
    1.  
    2. package YourAwesomPlugin;
    3.  
    4. import java.util.ArrayList;
    5. import java.util.List;
    6.  
    7. import net.minecraft.server.v1_5_R3.Chunk;
    8. import net.minecraft.server.v1_5_R3.EnumSkyBlock;
    9. import net.minecraft.server.v1_5_R3.Packet56MapChunkBulk;
    10.  
    11. import org.bukkit.Bukkit;
    12. import org.bukkit.Location;
    13. import org.bukkit.craftbukkit.v1_5_R3.CraftChunk;
    14. import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    15. import org.bukkit.craftbukkit.v1_5_R3.entity.CraftPlayer;
    16. import org.bukkit.entity.Player;
    17.  
    18. public class LightSource {
    19. /*
    20. * MINI README
    21. *
    22. * This is free and you can use it/change it all you want.
    23. *
    24. * There is a bukkit forum post on for this code:
    25. * [url]http://forums.bukkit.org/threads/resource-server-side-lighting-no-it-isnt-just-client-side.154503/[/url]
    26. */
    27.  
    28. /**
    29. * Create light with level at a location. Players can be added to make them only see it.
    30. * @param l
    31. * @param level
    32. * @param players
    33. */
    34. public static void createLightSource (Location l, int level, Player... players) {
    35. CraftWorld w = (CraftWorld) l.getWorld();
    36. int oLevel = l.getBlock().getLightLevel();
    37.  
    38. //Sets the light source at the location to the level
    39. w.getHandle().b(EnumSkyBlock.BLOCK, l.getBlockX(), l.getBlockY(), l.getBlockZ(), level);
    40.  
    41. //Send packets to the area telling players to see this level
    42. updateChunk(l, players);
    43.  
    44. //If you comment this out it is more likely to get light sources you can't remove
    45. // but if you do comment it, light is consistent on relog and what not.
    46. w.getHandle().b(EnumSkyBlock.BLOCK, l.getBlockX(), l.getBlockY(), l.getBlockZ(), oLevel);
    47. }
    48.  
    49. /**
    50. * Updates the block making the light source return to what it actually is
    51. * @param l
    52. */
    53. public static void deleteLightSource (Location l, Player... players) {
    54. int t = l.getBlock().getTypeId();
    55. l.getBlock().setTypeId(t == 1 ? 2 : 1);
    56.  
    57. updateChunk(l, players);
    58.  
    59. l.getBlock().setTypeId(t);
    60. }
    61.  
    62. /**
    63. * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
    64. * @param l
    65. */
    66. private static void updateChunk (Location l, Player... players) {
    67. List<Chunk> cs = new ArrayList<Chunk>();
    68.  
    69. for(int x=-1; x<=1; x++) {
    70. for(int z=-1; z<=1; z++) {
    71. cs.add(((CraftChunk)l.clone().add(16 * x, 0, 16 * z).getChunk()).getHandle());
    72. }
    73. }
    74.  
    75. Packet56MapChunkBulk packet = new Packet56MapChunkBulk(cs);
    76. int t = l.clone().add(0, 1, 0).getBlock().getTypeId();
    77. l.clone().add(0, 1, 0).getBlock().setTypeId(t == 1 ? 2 : 1);
    78.  
    79. Player[] playerArray = ((players != null && players.length > 0) ? players : l.getWorld().getPlayers().toArray(new Player[l.getWorld().getPlayers().size()]));
    80.  
    81. for(Player p1 : playerArray) {
    82. if(p1.getLocation().distance(l) <= Bukkit.getServer().getViewDistance()*16) {
    83. ((CraftPlayer)p1).getHandle().playerConnection.sendPacket(packet);
    84. }
    85. }
    86.  
    87. l.clone().add(0, 1, 0).getBlock().setTypeId(t);
    88. }
    89. }
    90.  

    This post has been edited 10 times. It was last edited by RingOfStorms Jun 22, 2013.
  2. Offline

    dillyg10

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Made a small modification so that light removal works a lot better :).

    Code:java
    1.  
    2.  
    3. /**
    4.   * Creates a light source at the location with the level indicated
    5.   * @param l
    6.   * @param level
    7.   */
    8. public static void createLightSource (Location l, int level) {
    9. l.setPitch(0);
    10. l.setYaw(0);
    11. l.setX(l.getBlockX());
    12. l.setY(l.getBlockY());
    13. l.setZ(l.getBlockZ());
    14.  
    15. CraftWorld w = (CraftWorld) l.getWorld();
    16. int oLevel = l.getBlock().getLightLevel();
    17. //Sets the light source at the location to the level
    18. w.getHandle().b(EnumSkyBlock.BLOCK, l.getBlockX(), l.getBlockY(), l.getBlockZ(), level);
    19. //Send packets to the area telling players to see this level
    20. updateChunk(l);
    21. //Set the level back to what it is supposed to be w/o updating clients.
    22. //This is important because if you don't then you can get some very hard to remove lights.
    23. w.getHandle().b(EnumSkyBlock.BLOCK, l.getBlockX(), l.getBlockY(), l.getBlockZ(), oLevel);
    24. }
    25.  
    26. /**
    27.   * Updates the block making the light source return to what it actually is
    28.   * @param l
    29.   */
    30. public static void deleteLightSource (Location l) {
    31. int t = l.getBlock().getType().getId();
    32. l.getBlock().setTypeId(t == 1 ? 2 : 1);
    33. l.getBlock().setTypeId(t);
    34. }
    35.  
    36. /**
    37.   * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
    38.   * @param l
    39.   */
    40. private static void updateChunk (Location l) {
    41. for(Player p : l.getWorld().getPlayers()) {
    42. List<Chunk> cs = new ArrayList<Chunk>();
    43.  
    44. cs.add( ((CraftChunk)l.getChunk()).getHandle() );
    45.  
    46. cs.add( ((CraftChunk)l.add(16, 0, 0).getChunk()).getHandle() );
    47. cs.add( ((CraftChunk)l.add(16, 0, 16).getChunk()).getHandle() );
    48. cs.add( ((CraftChunk)l.add(16, 0, -16).getChunk()).getHandle() );
    49.  
    50. cs.add( ((CraftChunk)l.add(-16, 0, 0).getChunk()).getHandle() );
    51. cs.add( ((CraftChunk)l.add(-16, 0, 16).getChunk()).getHandle() );
    52. cs.add( ((CraftChunk)l.add(-16, 0, -16).getChunk()).getHandle() );
    53.  
    54. cs.add( ((CraftChunk)l.add(0, 0, 16).getChunk()).getHandle() );
    55. cs.add( ((CraftChunk)l.add(0, 0, -16).getChunk()).getHandle() );
    56.  
    57. Packet56MapChunkBulk packet = new Packet56MapChunkBulk(cs);
    58. int t = l.clone().add(0, 1, 0).getBlock().getTypeId();
    59. l.clone().add(0, 1, 0).getBlock().setTypeId(t == 1 ? 2 : 1);
    60. ((CraftPlayer)p).getHandle().playerConnection.sendPacket(packet);
    61. l.clone().add(0, 1, 0).getBlock().setTypeId(t);
    62. }
    63.  
    64. }
    65.  
    Micius and mkremins like this.
  3. Offline

    sn00pbom

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Very cool resource. Thanks much
  4. Offline

    zack6849

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Perhaps this should be re-posted/moved to the development resources sub-forum?
  5. Offline

    foodyling

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Added a bit of my own touch to the code and used a for loop instead of adding each chunk like previously. Also, what is the purpose of resetting the location to itself and clearing the pitch and yaw? Tell me if I missed something or I removed the functionality completely, I haven't had a chance to actually test this. All in all, a very useful resource that can be applied to a variety of plugins.
    Code:
    import java.util.ArrayList;
    import java.util.List;
    import net.minecraft.server.v1_5_R3.Chunk;
    import org.bukkit.Location;
    import org.bukkit.entity.Player;
     
    import net.minecraft.server.v1_5_R3.EnumSkyBlock;
    import net.minecraft.server.v1_5_R3.Packet56MapChunkBulk;
    import org.bukkit.block.Block;
    import org.bukkit.craftbukkit.v1_5_R3.CraftChunk;
    import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    import org.bukkit.craftbukkit.v1_5_R3.entity.CraftPlayer;
     
    public class LightSource {
        /**
        * Creates a light source at the location with the level indicated
        * @param location
        * @param level
        */
        public static void createLightSource(Location location, int level) {
            // What is the purpose of this? Setting the pitch and yaw serves no purpose since it is not used
            //location.setPitch(0);
            //location.setYaw(0);
            //Why reset the location to itself exactly?
            //location.setX(location.getBlockX());
            //location.setY(location.getBlockY());
            //location.setZ(location.getBlockZ());
           
            CraftWorld world = (CraftWorld) location.getWorld();
            int oLevel = location.getBlock().getLightLevel();
            //Sets the light source at the location to the level
            world.getHandle().b(EnumSkyBlock.BLOCK, location.getBlockX(), location.getBlockY(), location.getBlockZ(), level);
            //Send packets to the area telling players to see this level
            updateChunk(location);
            //Set the level back to what it is supposed to be w/o updating clients.
            //This is important because if you don't then you can get some very hard to remove lights.
            world.getHandle().b(EnumSkyBlock.BLOCK, location.getBlockX(), location.getBlockY(), location.getBlockZ(), oLevel);
        }
     
        /**
        * Updates the block making the light source return to what it actually is
        * @param location
        */
        public static void deleteLightSource(Location location) {
            location.getBlock().setType(location.getBlock().getType());
            updateChunk(location);
        }
     
        /**
        * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
        * @param location
        */
        private static void updateChunk(Location location) {
            for (Player player : location.getWorld().getPlayers()) {
                List<Chunk> chunkList = new ArrayList<Chunk>();
                for (int x = -1; x <= 1; x++) {
                    for (int z = -1; z <= 1; z++) {
                        chunkList.add(((CraftChunk) location.add(16 * x, 0, 16 * x).getChunk()).getHandle());
                    }
                }
                Packet56MapChunkBulk packet = new Packet56MapChunkBulk(chunkList);
                Block block = location.add(0, 1, 0).getBlock();
                int type = block.getTypeId();
                block.setTypeId(type == 1 ? 2 : 1);
                ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
                block.setTypeId(type);
            }
        }
    }
    mkremins likes this.
  6. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Woops I had thought I deleted the set yaw pitch and what not :3 - that was from something I was doing before that didn't work out. So yea ill remove from gist.

    And thanks for for loop, I knew there was a better way to do it, I was just drawing complete blanks on how to do the for loop.

    This post has been edited 1 time. It was last edited by RingOfStorms Jun 21, 2013.
  7. Offline

    foodyling

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    I'm still amazed on how small the code actually is and works.
  8. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    @Zarius may be able to move the post, I did derp. Do scold me for that ^_^ I always scold others for wrong postings :3
    foodyling likes this.
  9. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Yea I was drawing blanks on how to do the for loop at the time, and the setyaw.. were leftovers from experimenting with other things.

    Edit: Updated code above.

    This post has been edited 1 time. It was last edited by RingOfStorms Jun 21, 2013.
    bennie3211 and foodyling like this.
  10. Offline

    kreashenz

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    This is AWESOME! Thank you so much for this!
  11. Offline

    GreySwordz

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    @RingOfStorms it isn't always you can find free, working, NMS code to use to your heart's desire. Thank You. :)
  12. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Also thank @BeYkeRYkt because if he didn't share BkrTorchLight then I would have never went into the NMS code to see how it worked and simplified it :3
    BeYkeRYkt, GreySwordz and kreashenz like this.
  13. Offline

    BeYkeRYkt

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Nice work :)
    kreashenz and RingOfStorms like this.
  14. Offline

    desht

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Nice work. Be careful, though: every call to createLightSource() will be sending a Map Chunk Bulk packet containing 9 chunks to every player on the server, and each chunk could contain many kilobytes of data (up to 65536 blocks, plus the data byte, plus lighting information...). True, not every chunk slice will be sent, and the data is compressed, but even with that, the amount of data sent is non-trivial. You do not want to be calling this method on a regular basis on a busy server.

    You might find that simply calling world.refreshChunk(...) for the chunk and surrounding chunks is more efficient (I believe chunk updates will only be sent to players in viewing range of the chunk) - that would need some testing, though. In addition, it should not be necessary to send a 3x3 chunk array - a bit of simple arithmetic should be sufficient to determine the correct 2x2 chunk array to be sent, reducing the data load by better than half. A max-level (15) light source will never affect blocks more than 15 blocks away from it, so it can't span more than 2 chunks in either X/Z direction.

    Even with those optimisations, it's still not a cheap method call. It sucks that the Minecraft protocol doesn't allow lighting updates to be sent per-block, but that's what we have to deal with.
    mkremins likes this.
  15. Offline

    stirante

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    I think that refreshChunk don't work very well. I would change some things it this class:
    1. Get 16 net.minecraft.server.PlayerChunk objects (it's 4x4 square around light) and perform method sendAll(packet) instead of sending it to every player in the world
    2. Create one packet for all players instead of creating packets for every player
    3. For some magic plugins, method to show light only to one player would be cool

    EDIT: I forgot to say that this is awesome :D

    This post has been edited 1 time. It was last edited by stirante Jun 21, 2013.
  16. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    It can be in up to 4 chunks easily. imagine a light source in the corner of a chunk, it will be in the 2c2 chunks around the corner, 4 total.


    I'll look into the smaller chunk sizes and refreshChunk function sometime soon.

    Added in the 1 packet sending, and also something that only sends packets to people within the view distance set by server.
  17. Offline

    desht

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Yes. 4 chunks, or in other words a 2x2 chunk array, exactly like I said.

    Edit: I probably wasn't terribly clear in my first post. If you're updating a block in the middle of a chunk, then up to 9 chunks could be affected, depending on the light level you're setting. E.g. a light level of 14 (torch light) in the middle of a chunk would affect that chunk, plus the chunks to the N, E, S & W. But not the diagonally adjacent chunks, because of the way Minecraft lighting works: http://www.minecraftwiki.net/wiki/Light#Light_Spread (i.e. it's a diamond pattern, not a circle or square). If you're near the corner of the chunk, then likely only 4 chunks will be affected.

    In other words, some more careful calculation on just which chunks are affected could save a lot of data needing to be sent.

    This post has been edited 1 time. It was last edited by desht Jun 21, 2013.
    RingOfStorms likes this.
  18. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Woop's misread :3
  19. Offline

    desht

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    NP. Just updated my last post with some more clarifications...
  20. Offline

    desht

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Oh, line 71 of your current code: you're calling l.add(...) repeatedly inside your loops. You're going to end up with unexpected values for l (a mutable Location object) there...
    RingOfStorms likes this.
  21. Offline

    chasechocolate

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Nice job, very useful!
  22. Offline

    TnT Trinitrotoluene Maximus Administrator Bukkit Help

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Moved to the correct forum.
    RingOfStorms likes this.
  23. Offline

    foodyling

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Alright, I had a better idea on possibly only updating the chunks the player needs, also I would recommend using List<Player> rather than Player... players because most plugins that use this would probably rather use a list than do updateChunks(location, player1, player2, player3). The new way of updating chunks checks in each direction of the location and doesn't add chunks already added. I *think* this'll be better than sending 9 chunks at a time if it's not needed, however code is needed to account for the pattern of the light like @desht stated
    Code:
    import java.util.ArrayList;
    import java.util.List;
     
    import org.bukkit.block.Block;
    import org.bukkit.Bukkit;
    import org.bukkit.entity.Player;
    import org.bukkit.Location;
    import org.bukkit.craftbukkit.v1_5_R3.CraftChunk;
    import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    import org.bukkit.craftbukkit.v1_5_R3.entity.CraftPlayer;
     
    import net.minecraft.server.v1_5_R3.Chunk;
    import net.minecraft.server.v1_5_R3.EnumSkyBlock;
    import net.minecraft.server.v1_5_R3.Packet56MapChunkBulk;
     
    public class LightSource {
        public static void createLightSource (Location location, int level, List<Player> players) {
            CraftWorld world = (CraftWorld) location.getWorld();
            int originalLevel = location.getBlock().getLightLevel();
            //Sets the light source at the location to the level
            world.getHandle().b(EnumSkyBlock.BLOCK, location.getBlockX(), location.getBlockY(), location.getBlockZ(), level);
            //Send packets to the area telling players to see this level
            updateChunk(location, players, level);
            //If you comment this out it is more likely to get light sources you can't remove
            // but if you do comment it, light is consistent on relog and what not.
            world.getHandle().b(EnumSkyBlock.BLOCK, location.getBlockX(), location.getBlockY(), location.getBlockZ(), originalLevel);
        }
     
        /**
        * Updates the block making the light source return to what it actually is
        * @param location
        */
        public static void deleteLightSource (Location location, List<Player> players) {
            int TypeID = location.getBlock().getTypeId();
            location.getBlock().setTypeId(TypeID == 1 ? 2 : 1);
            updateChunk(location, players, location.getBlock().getLightLevel());
            location.getBlock().setTypeId(TypeID);
        }
     
        /**
        * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
        * @param location
        */
        private static void updateChunk (Location location, List<Player> players, int distance) {
            List<Chunk> chunkList = new ArrayList<Chunk>();
            // Get a list of chunks needed to update depending on the light level, it will only update chunks that are needed
            for (int x = -1; x <= 1; x++) {
                for (int z = -1; z <= 1; z++) {
                    Chunk chunk = ((CraftChunk)location.clone().add(distance * x, 0, distance * z).getChunk()).getHandle();
                    if (!chunkList.contains(chunk)) {
                        chunkList.add(chunk);
                    }
                }
            }
     
            Packet56MapChunkBulk packet = new Packet56MapChunkBulk(chunkList);
            Block block = location.clone().add(0, 1, 0).getBlock();
            int TypeID = block.getTypeId();
            block.setTypeId(TypeID == 1 ? 2 : 1);
            // Use distance squared since Math.sqrt will take longer, updates faster
            int ViewDistance = (int) Math.pow(Bukkit.getServer().getViewDistance(), 2) * 256;
            //Find players needed to update
            for (Player player : players != null ? players : location.getWorld().getPlayers()) {
                if (player.getLocation().distanceSquared(location) <= ViewDistance) {
                    ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
                }
            }
            // A better algorithm would check which chunks would be needed for each player, however would take longer
            // but probably be less taxing on the connection (a player might have the edge of a chunk that got a new
            // light level at the far point of the viewing distance chunks
            block.setTypeId(TypeID);
        }
    }

    This post has been edited 1 time. It was last edited by foodyling Jun 22, 2013.
  24. Offline

    ferrybig

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    make it version independent using reflections and its the best code
  25. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    I know how to, but I wont because most people don't know how to get the correct version code. And i feel like keeping it that way, so if you know how, you can take the class and change it to how you want. But the main post won't be using reflection bypass.
  26. Offline

    Comphenix

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    This is very intriguing, but unfortunately it has it's share of problems. First, as @desht pointed out, to chance the light of a single block requires the server to process, compress and send a sizable area of map data to each player. I used ProtocolLib and measured the size of packet 56 and 51, and it clocks in at around 18 kB in a freshly generated Minecraft world:
    Code:
    2013-06-22 05:09:47 [INFO] Size of Packet56MapChunkBulk: 17,8 KiB
    2013-06-22 05:10:07 [INFO] Size of Packet56MapChunkBulk: 18,2 KiB
    2013-06-22 05:10:28 [INFO] Size of Packet56MapChunkBulk: 18,7 KiB
    Though more developed worlds might be larger as they don't compress as well. Of course, you have to multiply this by the number of observing players (up to the player cap) and number of times you relight blocks.

    Alternatively, you could send a single Packet51MapChunk updating ONLY the one or two 16x16x16 regions that we modify, instead of the immediate 3x3 chunks around the block (48x48x256), at a cost to simplicity (download):
    Code:java
    1. public class LightSource {
    2. private static Method cachedPlayerChunk;
    3. private static Field cachedDirtyField;
    4.  
    5. // For choosing an adjacent air block
    6. private static BlockFace[] SIDES = {
    7. BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH,
    8. BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST };
    9.  
    10. /**
    11.   * Create light with level at a location.
    12.   * @param loc - which block to update.
    13.   * @param level - the new light level.
    14.   */
    15. public static void createLightSource(Location loc, int level) {
    16. WorldServer nmsWorld = ((CraftWorld) loc.getWorld()).getHandle();
    17. int oldLevel = loc.getBlock().getLightLevel();
    18.  
    19. // Sets the light source at the location to the level
    20. nmsWorld.b(EnumSkyBlock.BLOCK, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), level);
    21.  
    22. // Send packets to the area telling players to see this level
    23. updateChunk(nmsWorld, loc);
    24.  
    25. // If you comment this out it is more likely to get light sources you can't remove
    26. // but if you do comment it, light is consistent on relog and what not.
    27. nmsWorld.b(EnumSkyBlock.BLOCK, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), oldLevel);
    28. }
    29.  
    30. private static Block getAdjacentAirBlock(Block block) {
    31. // Find the first adjacent air block
    32. for (BlockFace face : SIDES) {
    33. // Don't use these sides
    34. if (block.getY() == 0x0 && face == BlockFace.DOWN)
    35. continue;
    36. if (block.getY() == 0xFF && face == BlockFace.UP)
    37. continue;
    38.  
    39. Block candidate = block.getRelative(face);
    40.  
    41. if (candidate.getType().isTransparent()) {
    42. return candidate;
    43. }
    44. }
    45. return block;
    46. }
    47.  
    48. /**
    49.   * Gets all the chunks touching/diagonal to the chunk the location is in and updates players with them.
    50.   * @param loc - location to the block that was updated.
    51.   */
    52. @SuppressWarnings("rawtypes")
    53. private static void updateChunk(WorldServer nmsWorld, Location loc) {
    54. try {
    55. PlayerChunkMap map = nmsWorld.getPlayerChunkMap();
    56.  
    57. // Update the light itself
    58. Block adjacent = getAdjacentAirBlock(loc.getBlock());
    59. nmsWorld.A(adjacent.getX(), adjacent.getY(), adjacent.getZ());
    60.  
    61. int chunkX = loc.getBlockX() >> 4;
    62. int chunkZ = loc.getBlockZ() >> 4;
    63.  
    64. // Make sure the block itself is marked
    65. map.flagDirty(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
    66.  
    67. // See if the current segment can be updated
    68. Object playerChunk = getPlayerCountMethod().invoke(map, chunkX, chunkZ, false);
    69.  
    70. if (playerChunk != null) {
    71. Field dirtyField = getDirtyField(playerChunk);
    72. int dirtyCount = (Integer) dirtyField.get(playerChunk);
    73.  
    74. // Minecraft will automatically send out a Packet51MapChunk for us,
    75. // with only those segments (16x16x16) that are needed.
    76. if (dirtyCount > 0) {
    77. dirtyField.set(playerChunk, 64);
    78. }
    79. }
    80.  
    81. map.flush();
    82.  
    83. } catch (SecurityException e) {
    84. throw new RuntimeException("Access denied", e);
    85. } catch (ReflectiveOperationException e) {
    86. throw new RuntimeException("Reflection problem.", e);
    87. }
    88. }
    89.  
    90. private static Method getPlayerCountMethod() throws NoSuchMethodException, SecurityException {
    91. if (cachedPlayerChunk == null) {
    92. cachedPlayerChunk = PlayerChunkMap.class.getDeclaredMethod("a", int.class, int.class, boolean.class);
    93. cachedPlayerChunk.setAccessible(true);
    94. }
    95. return cachedPlayerChunk;
    96. }
    97.  
    98. private static Field getDirtyField(Object playerChunk) throws NoSuchFieldException, SecurityException {
    99. if (cachedDirtyField == null) {
    100. cachedDirtyField = playerChunk.getClass().getDeclaredField("dirtyCount");
    101. cachedDirtyField.setAccessible(true);
    102. }
    103. return cachedDirtyField;
    104. }
    105. }

    You won't have to figure out which players are nearby, and thus able to see the update, but conversely you can't customize who gets to see the changes either, at least not without ProtocolLib.

    Then again, these packets are much smaller than the Bulk packets:
    Code:
    2013-06-22 05:06:07 [INFO] Size of Packet51MapChunk: 297 B
    2013-06-22 05:06:23 [INFO] Size of Packet51MapChunk: 300 B
    2013-06-22 05:07:15 [INFO] Size of Packet51MapChunk: 544 B
    2013-06-22 05:08:17 [INFO] Size of Packet51MapChunk: 569 B
    Of course, this will only update the current chunk, not the nearby chunks. If the client is nearby, this absent lighting will actually be corrected, though not always. And occasionally, the client will reset the lighting entirely, though this problem exists for RingOfStorm's version as well.

    The solution to that problem is probably to track which blocks are affected by the relighting, and mark them as "dirty". But it does add to the complexity a bit:
    https://gist.github.com/aadnk/5841942

    I didn't include deleteLightSource method, as I couldn't get it to work properly (nor RingOfStorm's version). I might take a look at that later.

    This post has been edited 1 time. It was last edited by Comphenix Jun 22, 2013.
  27. Offline

    BeYkeRYkt

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    By the way when using mobile code, all the mobs (possibly) become immortal.
  28. Offline

    RingOfStorms

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    What?
  29. Offline

    BeYkeRYkt

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME

    This post has been edited 2 times. It was last edited by BeYkeRYkt Jun 23, 2013.
  30. Offline

    ChrisixStudios

    dev.bukkit.org profile:
    CFUSERNAME
    My Plugins (CFCOUNT)
    Minecraft account:
    MCUSERNAME
    Deleting the light source doesn't work. I stop and restart the server and it still isn't working.

Share This Page