EasyCooldown

Discussion in 'Resources' started by Tirelessly, Feb 8, 2013.

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

    Tirelessly

    I made a cooldown class because I thought Timerlib was too complicated. That's all.

    EDIT: There's more than one way to skin a cat. I wrote this a while ago and it's definitely not the best, but it's still a lot better than methods which use threads. I encourage you all to go look at Comphenix 's method in the comments as well.

    Code:
    public class Cooldown {
      private static Set<PlayerCooldown> cooldowns = new HashSet<PlayerCooldown>();
     
      public static void addCooldown(String cooldownName, String player, long lengthInMillis) {
          PlayerCooldown pc = new PlayerCooldown(cooldownName, player, lengthInMillis);
          Iterator<PlayerCooldown> it = cooldowns.iterator();
          //This section prevents duplicate cooldowns
          while(it.hasNext()) {
            PlayerCooldown iterated = it.next();
            if(iterated.getPlayerName().equalsIgnoreCase(pc.getPlayerName())) {
                if(iterated.getCooldownName().equalsIgnoreCase(pc.getCooldownName())) {
                  it.remove();
                }
            }
          }
          cooldowns.add(pc);
      }
     
      public static PlayerCooldown getCooldown(String cooldownName, String playerName, long length) {
          Iterator<PlayerCooldown> it = cooldowns.iterator();
          while(it.hasNext()) {
            PlayerCooldown pc = it.next();
            if(pc.getCooldownName().equalsIgnoreCase(cooldownName)) {
                if(pc.getPlayerName().equalsIgnoreCase(playerName)) {
                  return pc;
                }
            }
          }
          return new PlayerCooldown(cooldownName, playerName, length, true);
      }
     
    }
     
    class PlayerCooldown {
     
      private long startTime;
      private String playerName;
      private String cooldownName;
      private long lengthInMillis;
      private long endTime;
      private boolean overFirst = false;
     
      PlayerCooldown(String cooldownName, String player, long lengthInMillis) {
          this.cooldownName = cooldownName;
          this.startTime = System.currentTimeMillis();
          this.playerName = player;
          this.lengthInMillis = lengthInMillis;
          this.endTime = startTime + this.lengthInMillis;
      }
     
      private PlayerCooldown() {
      }
     
      public PlayerCooldown(String cooldownName, String playerName, long length, boolean over) {
          this(cooldownName, playerName, length);
          overFirst = true;
     
      }
     
      public boolean isOver() {
          if(overFirst) {
            overFirst = false;
            return true;
          }
          return endTime < System.currentTimeMillis();
      }
     
      public int getTimeLeft() {
          return (int)(endTime - System.currentTimeMillis());
      }
     
      public String getPlayerName() {
          return playerName;
      }
     
      public String getCooldownName() {
          return cooldownName;
      }
     
      public void reset() {
          startTime = System.currentTimeMillis();
          endTime = startTime + lengthInMillis;
      }
    }
    
    Just copy and paste that to a new file in your project called Cooldown.java.
    Here's an example usage, in sort of pseudo code:
    Code:
    on interact{
      if player is using custom magic wand thing
      //The following line has a length added. This is incase the player hasn't activated a cooldown yet.
      PlayerCooldown pc = Cooldown.getCooldown("WandCooldown", player.getName(), 15000);
      if(pc.isOver())
          let player use magic wand
          pc.reset();
      else
      player.sendMessage("You have: " + pc.getTimeLeft() + "Seconds left" );
      }
    }
    
    Don't instantiate PlayerCooldowns, use the static Cooldown.add(..) method.
    The name for cooldown part lets you define cooldowns for multiple purposes.
    If you have any suggestions let me know.

    Milkywayz This is how I thought it should be done. A bit simpler, I think.
     
  2. Offline

    Darq

    Small suggestion: As this class seems oriented towards newbies who don't know how to implement a cooldown on their own, perhaps include proper javadoc style comments on the methods, and fix the indentation (although yes, ide's can format), so people can more easily learn from the code?
     
  3. Offline

    Tirelessly

    Every time you edit a post it ruins the indenting. As far as commenting.. I try to make my names descriptive enough so that isn't necessary, but I'll do it when I get the opportunity.
     
  4. Offline

    Darq

    Weigh the benefits of some ugly syntax highlighting, vs proper indenting when using [code] instead of [syntax=java]? :p I would go for proper indentation any day.
     
  5. Offline

    chasechocolate

    Yeah, I tend to use the ["CODE"] (no quotations) when posting longer snippets of code, it will save the indentation.
     
    Darq likes this.
  6. Offline

    mike2033

    Thanks a lot for sharing your source! I wanted to ask where I have to put actions, if I for example want to say send hello to the player which run a command do I have to right paste it under it? Or into the first "if(pc==null)"?

    Greets
     
  7. Offline

    Tirelessly

    Where it says "let the player use the magic wand" is where the code should be when the cooldown is over. I will be changing a section shortly to remove repetition.
     
  8. Offline

    Comphenix

    If your goal is simplicity and usability, I'd recommend an even more bare-bone solution (download):
    Code:java
    1. public class Cooldowns {
    2. private static Table<String, String, Long> cooldowns = HashBasedTable.create();
    3.  
    4. /**
    5.   * Retrieve the number of milliseconds left until a given cooldown expires.
    6.   * <p>
    7.   * Check for a negative value to determine if a given cooldown has expired. <br>
    8.   * Cooldowns that have never been defined will return {@link Long#MIN_VALUE}.
    9.   * @param player - the player.
    10.   * @param key - cooldown to locate.
    11.   * @return Number of milliseconds until the cooldown expires.
    12.   */
    13. public static long getCooldown(Player player, String key) {
    14. return calculateRemainder(cooldowns.get(player.getName(), key));
    15. }
    16.  
    17. /**
    18.   * Update a cooldown for the specified player.
    19.   * @param player - the player.
    20.   * @param key - cooldown to update.
    21.   * @param delay - number of milliseconds until the cooldown will expire again.
    22.   * @return The previous number of milliseconds until expiration.
    23.   */
    24. public static long setCooldown(Player player, String key, long delay) {
    25. return calculateRemainder(
    26. cooldowns.put(player.getName(), key, System.currentTimeMillis() + delay));
    27. }
    28.  
    29. /**
    30.   * Determine if a given cooldown has expired. If it has, refresh the cooldown. If not, do nothing.
    31.   * @param player - the player.
    32.   * @param key - cooldown to update.
    33.   * @param delay - number of milliseconds until the cooldown will expire again.
    34.   * @return TRUE if the cooldown was expired/unset and has now been reset, FALSE otherwise.
    35.   */
    36. public static boolean tryCooldown(Player player, String key, long delay) {
    37. if (getCooldown(player, key) <= 0) {
    38. setCooldown(player, key, delay);
    39. return true;
    40. }
    41. return false;
    42. }
    43.  
    44. private static long calculateRemainder(Long expireTime) {
    45. return expireTime != null ? expireTime - System.currentTimeMillis() : Long.MIN_VALUE;
    46. }
    47. }

    You can make it even more compact by removing all the JavaDoc comments.

    It's very simple to use:
    Code:java
    1. if (Cooldowns.tryCooldown(player, "MagicHand", 15000)) {
    2. // Do what you need to do here. You don't have to set the cooldown when you're done.
    3. } else {
    4. // Cooldown hasn't expired yet
    5. player.sendMessage("You have " + (Cooldowns.getCooldown(player, "MagicHand") / 1000) + " seconds left.");
    6. }


    Note: This implementation doesn't clean up any cooldowns when a player disconnects. This means they will still be "ticking away" at the same rate while the player is gone, provided the server doesn't restart.
     
    TigerHix, Blah1, Retherz_ and 8 others like this.
  9. Offline

    bob7

    Am i the only one who simply stores current mils, then subtract my old mils by the new mils for the time lol?
     
  10. Offline

    mike2033

    Does this work with more than 1 player?
     
  11. Offline

    hubeb

    Yes
     
  12. Offline

    mike2033

    Thank you both!
     
  13. Offline

    hubeb

    Your Welcome
     
  14. Offline

    Tirelessly

    If you read my code you'd see that that's what I do. Adding the delay to the millis first makes you need to do less calculations.
     
  15. Offline

    ocomobock

    This works perfectly for me, but do you know how to let a certain player be able to do something more than once and once they've done it a certain amount of times it starts the cooldown?
     
  16. Offline

    Comphenix

    That requires a bit more work. I'd probably use something like this (download):
    Code:java
    1. package com.comphenix.example;
    2.  
    3. import java.util.HashMap;
    4. import java.util.Map;
    5. import java.util.concurrent.TimeUnit;
    6.  
    7. import org.bukkit.entity.Player;
    8.  
    9. public class Ability {
    10. /**
    11.   * Contains the number of charges and cooldown.
    12.   * @author Kristian Stangeland
    13.   */
    14. public static class Status {
    15. private int charges;
    16.  
    17. private long cooldown;
    18. private boolean recharged;
    19.  
    20. public Status(int charges) {
    21. this.charges = charges;
    22. this.cooldown = 0;
    23. this.recharged = true;
    24. }
    25.  
    26. /**
    27.   * Retrieve the number of times the current player can use this ability before initiating a new cooldown.
    28.   * @return Number of charges.
    29.   */
    30. public int getCharges() {
    31. return charges;
    32. }
    33.  
    34. /**
    35.   * Set the number of valid charges left
    36.   * @param charges - the new number of charges.
    37.   */
    38. public void setCharges(int charges) {
    39. this.charges = charges;
    40. }
    41.  
    42. /**
    43.   * Determine if this ability has been recharged when the cooldown last expired.
    44.   * @return TRUE if it has, FALSE otherwise.
    45.   */
    46. public boolean isRecharged() {
    47. return recharged;
    48. }
    49.  
    50. /**
    51.   * Set if this ability has been recharged when the cooldown last expired.
    52.   * @param recharged - whether or not the ability has been recharged.
    53.   */
    54. public void setRecharged(boolean recharged) {
    55. this.recharged = recharged;
    56. }
    57.  
    58. /**
    59.   * Lock the current ability for the duration of the cooldown.
    60.   * @param delay - the number of milliseconds to wait.
    61.   */
    62. public void setCooldown(long delay, TimeUnit unit) {
    63. this.cooldown = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(delay, unit);
    64. }
    65.  
    66. /**
    67.   * Determine if the cooldown has expired.
    68.   * @return TRUE if it has, FALSE otherwise.
    69.   */
    70. public boolean isExpired() {
    71. long rem = getRemainingTime(TimeUnit.MILLISECONDS);
    72. return rem < 0;
    73. }
    74.  
    75. /**
    76.   * Retrieve the of milliseconds until the cooldown expires.
    77.   * @param unit - the unit of the resulting time.
    78.   * @return Number of milliseconds until expiration.
    79.   */
    80. public long getRemainingTime(TimeUnit unit) {
    81. return unit.convert(cooldown - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    82. }
    83. }
    84.  
    85. private Map<String, Status> playerStatus = new HashMap<String, Status>();
    86.  
    87. private final int defaultCharges;
    88. private final long defaultDelay;
    89.  
    90. public Ability(int defaultCharges) {
    91. this(defaultCharges, 5, TimeUnit.SECONDS);
    92. }
    93.  
    94. public Ability(int defaultCharges, int defaultDelay, TimeUnit unit) {
    95. this.defaultCharges = defaultCharges;
    96. this.defaultDelay = TimeUnit.MILLISECONDS.convert(defaultDelay, unit);
    97. }
    98.  
    99. /**
    100.   * Retrieve the cooldown time and charge count for a given player.
    101.   * @param player - the player.
    102.   * @return Associated status.
    103.   */
    104. public Status getStatus(Player player) {
    105. Status status = playerStatus.get(player.getName());
    106.  
    107. if (status == null) {
    108. status = createStatus(player);
    109. playerStatus.put(player.getName(), status);
    110. } else {
    111. checkStatus(player, status);
    112. }
    113. return status;
    114. }
    115.  
    116. /**
    117.   * Attempt to use this ability. The player must have at least once charge for this operation
    118.   * to be successful. The player's charge count will be decremented by the given amount.
    119.   * <p>
    120.   * Otherwise, initiate the recharging cooldown and return FALSE.
    121.   * @param player - the player.
    122.   * @param charges - the number of charges to consume.
    123.   * @return TRUE if the operation was successful, FALSE otherwise.
    124.   */
    125. public boolean tryUse(Player player) {
    126. return tryUse(player, 1, defaultDelay, TimeUnit.MILLISECONDS);
    127. }
    128.  
    129. /**
    130.   * Attempt to use this ability. The player must have at least once charge for this operation
    131.   * to be successful. The player's charge count will be decremented by the given amount.
    132.   * <p>
    133.   * Otherwise, initiate the recharging cooldown and return FALSE.
    134.   * @param player - the player.
    135.   * @param delay - the duration of the potential cooldown.
    136.   * @param unit - the unit of the delay parameter.
    137.   * @return TRUE if the operation was successful, FALSE otherwise.
    138.   */
    139. public boolean tryUse(Player player, long delay, TimeUnit unit) {
    140. return tryUse(player, delay, unit);
    141. }
    142.  
    143. /**
    144.   * Attempt to use this ability. The player must have at least once charge for this operation
    145.   * to be successful. The player's charge count will be decremented by the given amount.
    146.   * <p>
    147.   * Otherwise, initiate the recharging cooldown and return FALSE.
    148.   * @param player - the player.
    149.   * @param charges - the number of charges to consume.
    150.   * @param delay - the duration of the potential cooldown.
    151.   * @param unit - the unit of the delay parameter.
    152.   * @return TRUE if the operation was successful, FALSE otherwise.
    153.   */
    154. public boolean tryUse(Player player, int charges, long delay, TimeUnit unit) {
    155. Status status = getStatus(player);
    156. int current = status.getCharges();
    157.  
    158. // Check cooldown
    159. if (!status.isExpired())
    160. return false;
    161.  
    162. if (current <= charges) {
    163. status.setRecharged(false);
    164. status.setCharges(0);
    165. status.setCooldown(delay, unit);
    166. } else {
    167. status.setCharges(current - charges);
    168. }
    169. return current > 0;
    170. }
    171.  
    172. private void checkStatus(Player player, Status status) {
    173. if (!status.isRecharged() && status.isExpired()) {
    174. rechargeStatus(player, status);
    175. }
    176. }
    177.  
    178. /**
    179.   * Invoked when a status must be recharged.
    180.   * @param player - the player to recharge.
    181.   * @param status - the status to update.
    182.   * @return The updated status.
    183.   */
    184. protected Status rechargeStatus(Player player, Status status) {
    185. status.setRecharged(true);
    186. status.setCharges(defaultCharges);
    187. return status;
    188. }
    189.  
    190. /**
    191.   * Invoked when we need to create a status object for a player.
    192.   * @param player - the player to create for.
    193.   * @return The new status object.
    194.   */
    195. protected Status createStatus(Player player) {
    196. return new Status(defaultCharges);
    197. }
    198. }

    Then all you need, is to declare the different abilitites with the number of charges and the default cooldown inside your plugin class:
    Code:java
    1.  
    2. public class ExamplePlugin extends JavaPlugin {
    3. private Ability throwStone = new Ability(3, 30, TimeUnit.SECONDS);
    4. private Ability shortTeleport = new Ability(3, 1, TimeUnit.MINUTES);
    5. private Ability invurnability = new Ability(1, 5, TimeUnit.MINUTES);
    6.  
    7. public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
    8. if (!(sender instanceof Player)) {
    9. sender.sendMessage(ChatColor.RED + "Must be a player.");
    10. }
    11. Player player = (Player) sender;
    12.  
    13. if (command.getName().equals("throw")) {
    14. if (throwStone.tryUse(player)) {
    15. // Throw the stone here
    16. } else {
    17. // If you want - it's not necessary
    18. player.sendMessage(Color.CYAN +
    19. "Cooldown expires in " + throwStone.getStatus(player).getRemainingTime(TimeUnit.SECONDS));
    20. }
    21. }
    22. // etc.
    23. return true;
    24. }
    25. }
     
  17. Offline

    ocomobock

    That sort of works. When there are two cooldowns going on at the same time, they get confused with eachother and they both have the same cooldown time. Here's my code, broken down into different sections:


    Code:java
    1. private Ability thorHammer = new Ability(3, 1, TimeUnit.MINUTES);
    2. private Ability explode = new Ability(4, 2, TimeUnit.MINUTES);
    3.  
    4. if (thorHammer.tryUse(p))
    5. {
    6. w.strikeLightning(e.getClickedBlock().getLocation());
    7. p.setVelocity(p.getLocation().getDirection().multiply(-1.5));
    8. }else
    9. {
    10. p.sendMessage(ChatColor.DARK_AQUA + "Cooldown expires in " + thorHammer.getStatus(p).getRemainingTime(TimeUnit.SECONDS));
    11. }
    12.  
    13. if (explode.tryUse(p))
    14. {
    15. explodeIgnore = true;
    16. destroyBlocks = false;
    17. w.createExplosion(p.getLocation(), 3.0F);
    18. destroyBlocks = true;
    19. explodeIgnore = false;
    20. hitByCreeper = true;
    21. if (hitByCreeper)
    22. {
    23. if (!p.isOnGround())
    24. {
    25. hitByCreeperAndInAir = true;
    26. }else
    27. {
    28. hitByCreeperAndInAir = false;
    29. }
    30. }
    31. p.setVelocity(p.getLocation().getDirection().multiply(-1.25));
    32. }else
    33. {
    34. p.sendMessage(ChatColor.DARK_AQUA + "Cooldown expires in " + thorHammer.getStatus(p).getRemainingTime(TimeUnit.SECONDS));
    35. }


    And you're a genius by the way, your code you posted before was really easy to use and understand. Thank you for that
     
  18. Offline

    Comphenix

    You forgot to change thorHammer into explode on line 34. You're simply printing the wrong cooldown. :p

    Other than that, it all seems to work fine:
    Code:java
    1. public class ExampleMod extends JavaPlugin implements Listener {
    2. private Ability thorHammer = new Ability(3, 1, TimeUnit.MINUTES);
    3. private Ability explode = new Ability(4, 2, TimeUnit.MINUTES);
    4.  
    5. [USER=90830440]Override[/USER]
    6. public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
    7. if (sender instanceof Player) {
    8. process((Player) sender, args);
    9. } else {
    10. sender.sendMessage(ChatColor.RED + "Must be a player.");
    11. }
    12. return true;
    13. }
    14.  
    15. private void process(Player player, String[] args) {
    16. if ("hammer".equals(args[0])) {
    17. if (thorHammer.tryUse(player)) {
    18. Block target = player.getTargetBlock(null, 120);
    19.  
    20. player.getWorld().strikeLightning(target.getLocation());
    21. player.setVelocity(player.getLocation().getDirection().multiply(-1.5));
    22. } else {
    23. player.sendMessage(ChatColor.DARK_AQUA + "Cooldown expires in " + thorHammer.getStatus(player).getRemainingTime(TimeUnit.SECONDS));
    24. }
    25. } else if ("explode".equals(args[0])) {
    26. if (explode.tryUse(player)) {
    27. player.getWorld().createExplosion(player.getLocation(), 2);
    28.  
    29. } else {
    30. player.sendMessage(ChatColor.DARK_AQUA + "Cooldown expires in " + explode.getStatus(player).getRemainingTime(TimeUnit.SECONDS));
    31. }
    32. } else {
    33. player.sendMessage(ChatColor.RED + "Unrecognized command.");
    34. }
    35. }
    36.  
    37. [USER=90830436]EventHandler[/USER]
    38. public void onEntityDeath(EntityDeathEvent e) {
    39. System.out.println(e);
    40. }
    41. }

    Yeah, I think I managed to strike a pretty good balance between simplicity and features. I'm not always that lucky though. :p

    Also, forgot to mention - you can customize the cooldown per user by using one of the overloads of tryUse. That might be useful if you want, say, VIP members to have shorter cooldowns.
     
    ocomobock likes this.
  19. Offline

    ocomobock

    I can be really stupid sometimes ._. I should have checked that. You may have noticed that I'm fairly new to programming :p Anyways, it's working fine now. Thank you very much :)

    And that feature will probably come in handy, that was a nice touch.

    I'm probably just doing something wrong again, but when the cooldown time runs out for a certain ability, it can keep being used. I'm using the same code as before. Any idea why this might be happening?

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 31, 2016
    Comphenix likes this.
  20. Offline

    Comphenix

    Ah, sorry. There's a small bug in the Ability class. Just place the following line inside rechargeStatus:
    Code:java
    1. status.setRecharged(true);

    I've updated the version on gist with this bug fix.
     
    ocomobock likes this.
  21. Offline

    ocomobock

    Yep, that works. Thanks again
     
  22. Offline

    AxmeD

    Hm... I didn't understand how to use that ._.
    Code:
    PlayerCooldown pc = Cooldown.getCooldown("ThiefSearch", player.getName(), 3000);
                                if(pc.isOver()){
                                      sender.sendMessage(ChatColor.YELLOW + "Вы скрылись от стражи");
                                      this.getConfig().set("Guards.wanted." + sender.getName(), 0);
                                      this.getConfig().set("Thiefs." + sender.getName() + ".target." + player.getName(), 0);
                                      this.saveConfig();
                                      pc.reset();
                                  }
    Cooldown isn't starting.
     
  23. Offline

    gameunited6155

    "WandCooldown"
    What is that?
     
  24. lol me too...
     
  25. That's the cooldown "id". Hence the line
    Code:
     public static PlayerCooldown getCooldown(String cooldownName, String playerName, long length) {
    "String cooldownName".
     
  26. I hate to bump an "old" thread like this (and double post (kind of) :O), but Comphenix , how would I go about getting the time left on the cooldown?

    I'm using this at the moment
    Code:
    public Ability freeze = new Ability(1, 1, TimeUnit.MINUTES);
    And later on in my code
    Code:
    user.sendMessage(ability.getStatus(user).getRemainingTime(TimeUnit.SECONDS) + " seconds of cooldown left.");
    That prints out a big negative value, which keeps increasing every second by 1. Any idea why? I'm using the Ability class you posted above.
     
  27. Offline

    Comphenix

    That's because the cooldown has expired (or yet to be triggered), and the ability is ready to use.

    You are using tryUse(), right?
     
  28. Comphenix
    Yeah, I am. The cooldowns should be 1 minute, which is 60 seconds, but it tells me something like -165412636, and keeps increasing.
    Code:java
    1. if (freeze.tryUse(user)) {
    2. for (Location pos: util.getSphere(user.getTargetBlock(null, 10).getLocation(), 5, 5, true, true, 1)) {
    3. try {
    4. fp.playFirework(l.getWorld(), pos, FireworkEffect.builder().with(Type.BALL).withColor(Color.BLUE).build());
    5. } catch (Exception e) {
    6. e.printStackTrace();
    7. }
    8. }
    9.  
    10. } else {
    11. user.sendMessage(ability.getStatus(user).getRemainingTime(TimeUnit.SECONDS) + " seconds of cooldown left.");
    12. }
     
  29. Offline

    Comphenix

    And what about the freeze-ability itself? How do you declare that?

    I any case, I suggest you try debugging this yourself. Just put it in an empty Java application with something like this:
    Code:java
    1. public class Test {
    2. // My attempt at reproducing your problem -- no luck though.
    3. private Ability ability = new Ability(3, 5, TimeUnit.SECONDS);
    4.  
    5. public void game() {
    6. Scanner scan = new Scanner(System.in);
    7.  
    8. while (true) {
    9. System.out.print("Attempt to use ability? (Y/N)");
    10.  
    11. if ("y".equalsIgnoreCase(scan.nextLine())) {
    12. useAbility();
    13. } else {
    14. break;
    15. }
    16. }
    17. }
    18.  
    19. private void useAbility() {
    20. Player player = new Player("Test");
    21.  
    22. if (ability.tryUse(player)) {
    23. System.out.println("Used ability.");
    24. } else {
    25. System.out.println("Cooldown: " +
    26. ability.getStatus(player).getRemainingTime(TimeUnit.SECONDS) + " seconds");
    27. }
    28. }
    29.  
    30. public static void main(String[] args) {
    31. new Test().game();
    32. }
    33. }

    And this class, if you don't want to import CraftBukkit just to run the test project:
    Code:java
    1. public class Player {
    2. private final String name;
    3.  
    4. public Player(String name) {
    5. this.name = name;
    6. }
    7.  
    8. public String getName() {
    9. return name;
    10. }
    11. }

    Then you can use the standard debugger in your favorite IDE, and not have to fiddle about with print-statements or try to remote debug CraftBukkit.
     
  30. Offline

    Retherz_

    Awesome
     
Thread Status:
Not open for further replies.

Share This Page