diff --git a/README.md b/README.md new file mode 100644 index 0000000..c189188 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# OBAmazeingTimer +No, it's not an amazing timer. It's a rudimentary timer for a maze. In particular, for the awesome maze +on our build server called "A-Maze-Ing". + +This plugin will automatically time a players attempt to solve A-Maze-Ing, and update a sign based +leaderboard with the player name, the time and their rank. + +Triggers for the start/entry and stop/exit are customizable, along with the maze dimensions for +detecting when a player leaves the maze. The leaderboard is also configurable for placement +and materials, as well as the text color and direction the leaderboard faces. + +The elapsed time during a run is displayed in the action bar, as well as the final time as a title +message when you exit the maze by triggering the exit/stop trigger. + +Main configuration file contains the start and stop triggers and borders of the maze. The Maze borders +are defined as two corners (so a square maze) and a low and high Y coordinate value. A listener will +detect if you go out of the maze boundary and will cancel the timer, as will leaving the game, getting +killed or warping out etc. + +The leaderboard config file contains players and their times. The file contains the times as a hash with +player uuid as the key and their time. This is so we can have one time for a player - uniquness of key. +However, the in-game hash used for the leaderboard is a TreeMap using the time as the key and player as +the value. This is because a TreeMap will automatically add new entries in numeric ascending order, which +is great the for leaderboard as no sorting required and the signs can be rendered directly from the hash. + +The signs configuration file contains the sign direction, location, sign and backing materials, as well +as the color of the text on the signs. Title and leaderboard are set separately with a flag for whether +you want to draw the title or not. There is also a setting for the number of sign lines the leaderboard +contains. Each sign line on the leaderboard is a set of 3 horizontally oriented signs which show rank, +player name, and the time. Since a sign has 4 lines, a 3 row leaderboard will have 12 ranks, which is +also the default. + +The timer of course can be used for things other than a maze. +Here is my gaudy looking leaderboard showing the title row and 3 leaderboard rows: + +![OBAmazeingTimer-leaderboard](https://ob-mc.net/repo/OBAmazeingTimer-leaderboard.png) +![OBAmazeing](https://ob-mc.net/repo/OBAmazeing.png) + +You can find A-Maze-Ing in our origial amusement park area at X=634, Z=-85. Join ob-mc.net! + +Compiled for 1.19 with Java 17. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e486336 --- /dev/null +++ b/pom.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + net.obmc + OBAmazeingTimer + 1.0 + jar + OBAmazeingTimer + Timer for the build server maze called A-Maze-Ing + + + UTF-8 + + + + + OBAmazeingTimer + + + + . + true + ${basedir}/src/main/resources/ + + *.yml + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + + + + spigot repo + https://hub.spigotmc.org/nexus/content/repositories/public + + + jetbrains repo + https://mvnrepository.com/artifact/org.jetbrains/annotations + + + apache lang + https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 + + + + + + org.spigotmc + spigot-api + 1.19.4-R0.1-SNAPSHOT + provided + + + org.jetbrains + annotations + 24.0.1 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + diff --git a/src/main/java/net/obmc/OBAmazeingTimer/EventListener.java b/src/main/java/net/obmc/OBAmazeingTimer/EventListener.java new file mode 100644 index 0000000..8077d4d --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/EventListener.java @@ -0,0 +1,214 @@ +package net.obmc.OBAmazeingTimer; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.Material; + +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.PlayerDeathEvent; + +public class EventListener implements Listener +{ + static Logger log = Logger.getLogger("Minecraft"); + private String logmsgprefix = null; + + public EventListener() { + + logmsgprefix = OBAmazeingTimer.getLogMsgPrefix(); + + } + /* + * Detect a players movement and see if they have triggered a maze timer stop/start event + * Look at the location of the player and see if that matches the triggers from the config + * + * @param {@link PlayerMoveEvent} + */ + @EventHandler + public void onPlayerMove( PlayerMoveEvent event ) { + + Player player = event.getPlayer(); + Location playerloc = player.getLocation(); + MazeTrigger entryloc = OBAmazeingTimer.getInstance().getStartTimerTrigger(); + MazeTrigger exitloc = OBAmazeingTimer.getInstance().getStopTimerTrigger(); + + // Entering maze - only process event if there's no timer running for a player + if ( !OBAmazeingTimer.getInstance().getMazeTimer().timerRunning( player.getUniqueId() ) ) { + + // check if we've moved to the entry trigger block + if ((int)playerloc.getX() == entryloc.getPadLocationX() && + (int)playerloc.getY() == entryloc.getPadLocationY() && + (int)playerloc.getZ() == entryloc.getPadLocationZ() && + playerloc.getBlock().getType().equals( entryloc.getPadMaterial() ) ) { + + log.log(Level.INFO, logmsgprefix + "Maze entry detected for " + player.getName() + "!" ); + + // yes, so start a timer and task for the player + OBAmazeingTimer.getInstance().getMazeTimer().StartTimer( player.getUniqueId() ); + + //TODO: do something? + //player.playSound(player.getLocation(), OBAmazeingTimer.getInstance().getSound(), 1.0f, 1.0f); + //displayEffect(OBAmazeingTimer.getInstance().getEffect(), player.getLocation(), 1.0f, 1.0f, 1.0f, 1.0f, OBAmazeingTimer.getInstance().getParticleCount()); + } + } + + // Exit maze - checks when a timer is running for a player + if ( OBAmazeingTimer.getInstance().getMazeTimer().timerRunning( player.getUniqueId() ) ) { + + // check if we've exited the maze - ignore if not activated + if ((int)playerloc.getX() == exitloc.getPadLocationX() && + (int)playerloc.getY() == exitloc.getPadLocationY() && + (int)playerloc.getZ() == exitloc.getPadLocationZ() && + playerloc.getBlock().getType().equals( exitloc.getPadMaterial() ) ) { + + log.log(Level.INFO, logmsgprefix + "Maze exit detected for " + player.getName() + "!" ); + + OBAmazeingTimer.getInstance().getMazeTimer().StopTimerOnExit( player.getUniqueId() ); + + //TODO: do sometthing? + //player.playSound(player.getLocation(), OBAmazeingTimer.getInstance().getSound(), 1.0f, 1.0f); + //displayEffect(OBAmazeingTimer.getInstance().getEffect(), player.getLocation(), 1.0f, 1.0f, 1.0f, 1.0f, OBAmazeingTimer.getInstance().getParticleCount()); + } + + // check if player has somehow moved outside of maze boundaries whilst being timed and cancel timer if they have + // we check the X and Z coordinates of our corners and high and low Y coordinates to form essentially a box + if ((int)playerloc.getX() > (int)OBAmazeingTimer.getInstance().getMazeSideHigh().getX() || (int)playerloc.getX() < (int)OBAmazeingTimer.getInstance().getMazeSideLow().getX() || + (int)playerloc.getZ() > (int)OBAmazeingTimer.getInstance().getMazeSideHigh().getZ() || (int)playerloc.getZ() < (int)OBAmazeingTimer.getInstance().getMazeSideLow().getZ() || + (int)playerloc.getY() >= (int)OBAmazeingTimer.getInstance().getMazeHigh() || (int)playerloc.getY() < (int)OBAmazeingTimer.getInstance().getMazeLow()) { + + log.log(Level.INFO, logmsgprefix + "Out of boundary detected for " + player.getName() + "!" ); + + OBAmazeingTimer.getInstance().getMazeTimer().StopTimerOutBounds( player.getUniqueId() ); + } + } + + } + + /** + * See if the player has interacted in some way with our entry or exit place and cancel the event + * + * @param {@link PlayerInteractEvent} + */ + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + Player player = event.getPlayer(); + Location playerloc = player.getLocation(); + + MazeTrigger entryloc = OBAmazeingTimer.getInstance().getStartTimerTrigger(); + MazeTrigger exitloc = OBAmazeingTimer.getInstance().getStopTimerTrigger(); + + if ( event.getAction() == Action.PHYSICAL ) { + + Material eventplate = event.getClickedBlock().getLocation().getBlock().getType(); + + // check for entry plate event and cancel event + if (!OBAmazeingTimer.getInstance().getMazeTimer().timerRunning( player.getUniqueId() ) && + (int)playerloc.getX() == entryloc.getPadLocationX() && + (int)playerloc.getY() == entryloc.getPadLocationY() && + (int)playerloc.getZ() == entryloc.getPadLocationZ() && + eventplate.equals( OBAmazeingTimer.getInstance().getStartTimerTrigger().getPadMaterial() ) ) { + event.setCancelled(true); + } + + // check for exit plate event and cancel + if (OBAmazeingTimer.getInstance().getMazeTimer().timerRunning( player.getUniqueId() ) && + (int)playerloc.getX() == exitloc.getPadLocationX() && + (int)playerloc.getY() == exitloc.getPadLocationY() && + (int)playerloc.getZ() == exitloc.getPadLocationZ() && + eventplate.equals( OBAmazeingTimer.getInstance().getStopTimerTrigger().getPadMaterial() ) ) { + event.setCancelled(true); + } + } + } + + @EventHandler + /* + * Detect if a player has quit whilst being timed and cancel their timer and task + * + * @param {@link PlayerQuitEvent} + */ + public void onPlayerQuit( PlayerQuitEvent event ) { + + clearTaskTimer( event.getPlayer() ); + } + + /* + * Detect if a player has been kicked whilst being timed and cancel their timer and task + * + * @param {@link PlayerKickEvent} + */ + @EventHandler + public void onPlayerKick( PlayerKickEvent event ) { + + clearTaskTimer( event.getPlayer() ); + } + + /* + * Detect if a player joins and cancel any tasks that might be left over + * + * @param {@link PlayerJoinEvent} + */ + @EventHandler + public void onPlayerJoin( PlayerJoinEvent event ) { + + clearTaskTimer( event.getPlayer() ); + } + + /* + * Detect if a player respawns and cancel any timers and tasks + * + * @param {@link PlayerRespawnEvent} + */ + @EventHandler + public void onReSpawn( PlayerRespawnEvent event ) { + + clearTaskTimer( event.getPlayer() ); + } + + /* + * Detect if a player leaves the world and cancel any timers and tasks + * + * @param {@link PlayerChangedWorldEvent} + */ + @EventHandler + public void onWorldChange( PlayerChangedWorldEvent event ) { + + clearTaskTimer( event.getPlayer() ); + } + + /* + * Detect if a player dies and cancel any timers and tasks + * + * @param {@link PlayerDeathEvent} + */ + @EventHandler + public void onPlayerDeath( PlayerDeathEvent event ) { + + clearTaskTimer( event.getEntity() ); + } + + /* + * Remove a player timer and cancel any timer tasks + * + * @param {@link Player} + */ + private void clearTaskTimer( Player player ) { + + if ( OBAmazeingTimer.getInstance().getMazeTimer().timerRunning( player.getUniqueId() ) ) { + + OBAmazeingTimer.getInstance().getMazeTimer().removePlayerTimer( player.getUniqueId() ); + OBAmazeingTimer.getInstance().getMazeTimer().removePlayerTask( player.getUniqueId() ); + } + } +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/LeaderboardManager.java b/src/main/java/net/obmc/OBAmazeingTimer/LeaderboardManager.java new file mode 100644 index 0000000..ab24f1f --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/LeaderboardManager.java @@ -0,0 +1,211 @@ +package net.obmc.OBAmazeingTimer; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +public class LeaderboardManager { + + static Logger log = Logger.getLogger("Minecraft"); + + private Map leaderboard = new TreeMap(); + private String logmsgprefix = null; + + private String leaderboardfilename = "leaderboard.yml"; + private FileConfiguration leaderboardconfig = null; + + /* + * Manages the in-game leaderboard and the leaderboard configuration file + * + * Note: The in-game leaderboard is a TreeMap of time and player uuid + * whilst the leaderboard file is a hash of player uuid and time. A TreeMap + * is used so entries are always in time order for easier rendering of the + * leaderboard, whilst the file has player id key to maintain uniqueness of key + */ + public LeaderboardManager() { + + logmsgprefix = OBAmazeingTimer.getLogMsgPrefix(); + + if ( leaderboard == null ) { + leaderboard = new HashMap(); + } + + File leaderboardfile = new File( OBAmazeingTimer.getInstance().getDataFolder(), leaderboardfilename ); + if ( !leaderboardfile.exists() ) { + leaderboardfile.getParentFile().mkdirs(); + OBAmazeingTimer.getInstance().saveResource( leaderboardfilename, false ); + } + } + + /* + * Return the current leaderboard map + * + * @return the leaderboard treemap + */ + public Map getLeaderboard() { + + return leaderboard; + } + + /* + * Build leaderboard from the leaderboard config file + * Note the swapping of time and uuid for the in-game TreeMap + * + * @return true is the leaderboard was loaded and created, false if not + */ + public boolean loadLeaderboardFromConfig() { + + try { + + leaderboardconfig = YamlConfiguration.loadConfiguration( new File( OBAmazeingTimer.getInstance().getDataFolder(), leaderboardfilename ) ); + if ( leaderboardconfig.contains( "timings" ) ) { + if ( leaderboardconfig.getConfigurationSection( "timings" ).getKeys( false ).size() > 0 ) { + leaderboardconfig.getConfigurationSection( "timings" ).getKeys( false ).forEach( + key -> leaderboard.put( leaderboardconfig.getConfigurationSection( "timings" ).getLong( key ), UUID.fromString( key ) ) + ); + } + } + } catch ( Exception e ) { + + log.log(Level.SEVERE, logmsgprefix + "Failed to load leaderboard from config file " + leaderboardfilename ); + e.printStackTrace(); + return false; + } + + return true; + } + + /* + * Save player time to the leaderboard file + * + * @param time + * @param UUID of player + * @return true if the save went through without issue, false if an exception occurred + */ + public boolean saveToLeaderboardFile( Long time, UUID playerid ) { + + try { + + if ( !leaderboardconfig.contains( "timings" ) ) { + leaderboardconfig.createSection( "timings" ); + } + leaderboardconfig.getConfigurationSection( "timings" ).set( playerid.toString(), time ); + leaderboardconfig.save( new File( OBAmazeingTimer.getInstance().getDataFolder(), leaderboardfilename ) ); + + } catch ( Exception e ) { + log.log(Level.SEVERE, logmsgprefix + "Failed to save entry to the leaderboard to config file " + leaderboardfilename ); + return false; + } + + return true; + } + + /* + * Remove a player time from the leaderboard config file + * + * @param UUID of player + * @return true if we removed the entry, false if an exception was generated + */ + public boolean removeFromLeaderboardFile( UUID playerid ) { + + try { + + leaderboardconfig.getConfigurationSection( "timings" ).set( playerid.toString(), null ); + leaderboardconfig.save( new File( OBAmazeingTimer.getInstance().getDataFolder(), leaderboardfilename ) ); + + } catch ( Exception e ) { + log.log(Level.SEVERE, logmsgprefix + "Failed to remove entry from the leaderboard to config file " + leaderboardfilename ); + return false; + } + + return true; + } + + /* + * Add a players time to the leaderboard + * + * @param time + * @param UUID of player + */ + public void addTime( Long time, UUID playerid ) { + + //remove existing time from leaderboard if there is one + Long key = getPlayerExistingTime( playerid ); + if ( key != null ) { + + removeTime( key, playerid ); + } + + // put new time into leaderboard and save leaderboard config + leaderboard.put( time, playerid ); + saveToLeaderboardFile( time, playerid ); + } + + /* + * Remove a players time from the leaderboard and leaderboard file + * + * @param time + * @param player UUID + */ + public void removeTime( Long time, UUID playerid ) { + + //TODO: additional checks required? + + Iterator lbit = leaderboard.keySet().iterator(); + while ( lbit.hasNext() ) { + + Long key = lbit.next(); + if ( key.equals( time ) && leaderboard.get( key ).toString().equals( playerid.toString() ) ) { + + lbit.remove(); + } + } + + removeFromLeaderboardFile( playerid ); + } + + /* + * Retrieve a players time if one exists + * + * @param UUID of player + * @return the key if player has a time, null if not + */ + public Long getPlayerExistingTime( UUID playerid ) { + + for ( Long key : leaderboard.keySet() ) { + leaderboard.get( key ); + if ( leaderboard.get( key ).toString().equals( playerid.toString() ) ) { + return key; + } + } + + return null; + } + + /* + * See where a time would rank - more recent time wins when times are the same + * + * @param time + * @return the position in the leaderboard for the time + */ + public int getRankForTime( Long time ) { + + int rank = 1; + for ( Long key : leaderboard.keySet() ) { + if ( time <= key ) { + return rank; + } + rank++; + } + + return rank; + } +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/MazeTimer.java b/src/main/java/net/obmc/OBAmazeingTimer/MazeTimer.java new file mode 100644 index 0000000..ec869fc --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/MazeTimer.java @@ -0,0 +1,288 @@ +package net.obmc.OBAmazeingTimer; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.time.StopWatch; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.ApiStatus.Internal; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; + +public class MazeTimer { + + static Logger log = Logger.getLogger("Minecraft"); + + private Map playertimers = new HashMap(); + private Map playertasks = new HashMap(); + + private String chatmsgprefix = null; + private String logmsgprefix = null; + + /* + * Construct a new {@link MazeTimer} manager which will manage stopwatch timers for players + * + * There are two lists. One contains stop watches for players and the other the task + * ID of any scheduler tasks associated with a [running] timer + */ + + @Internal + public MazeTimer() { + + chatmsgprefix = OBAmazeingTimer.getChatMsgPrefix(); + logmsgprefix = OBAmazeingTimer.getLogMsgPrefix(); + } + + /* + * Start a timer for a player + * + * @param uuid of the player + */ + public void StartTimer( UUID playerid ) { + + Player player = Bukkit.getPlayer(playerid); + + // return if player already has a timer running + if ( playertimers.containsKey( playerid ) && playertimers.get( playerid ).isStarted() ) { + return; + } + + if ( !playertimers.containsKey( playerid ) ) { + playertimers.put( playerid, new StopWatch() ); + } + + playertimers.get( playerid ).reset(); + playertimers.get( playerid ).start(); + + player.sendMessage( OBAmazeingTimer.getChatMsgPrefix() + "Started A-Maze-Ing maze timer for " + player.getName() + "!" ); + + // run a task to update the player timer in the action bar + updatePlayerTimer( playerid ); + } + + /* + * Stop a player timer and remove from our lists + * + * @param the player UUID + */ + public void StopTimerOutBounds( UUID playerid ) { + + if ( playertimers.containsKey( playerid ) && playertimers.get( playerid ).isStarted() ) { + + //playertimers.get( playerid ).stop(); + OBAmazeingTimer.getInstance().getMazeTimer().removePlayerTimer( playerid ); + } + } + + /* + * Process events for player exiting the maze by stepping on the exit trigger + * Stops the player timer, send messages, fanfare and add to leaderboard if eligible + * and redraws the leaderboard + * + * @param UUID of player + */ + public void StopTimerOnExit( UUID playerid ) { + + Player player = Bukkit.getPlayer( playerid ); + + if ( !playertimers.containsKey( playerid ) || playertimers.get( playerid ).isStopped() ) { + return; + } + + // stop timer and get a formatted time + playertimers.get( playerid ).stop(); + Long finaltime = playertimers.get( playerid ).getTime(); + String finaltimestr = Utils.formatTime( finaltime, "final" ); + log.log(Level.INFO, logmsgprefix + "Stopped maze timer for " + player.getName() + " at " + finaltimestr + "(" + finaltime + "ms)" ); + + // send a title message with the formatted time + player.sendTitle(ChatColor.GOLD + " Congratulations!", ChatColor.YELLOW + "Your final time was " + finaltimestr, 1*20, 5*20, 2*20); + + // Get rank for time and add to the leaderboard if eligible and report if player beat their time or not + LeaderboardManager leaderboardmgr = OBAmazeingTimer.getInstance().getLeaderboardManager(); + Long playercurrenttime = leaderboardmgr.getPlayerExistingTime( playerid ); + + int timerank = leaderboardmgr.getRankForTime( finaltime ); + if ( timerank <= ( 3 * 4 ) ) { + + // set flags to determine which message to send + boolean beatexistingtime = false; + boolean hasexistingtime = playercurrenttime != null ? true : false; + if ( hasexistingtime && finaltime < playercurrenttime ) { + beatexistingtime = true; + } + boolean failedtobeat = ( hasexistingtime && !beatexistingtime ) ? true : false; + + // first place + if ( timerank == 1 ) { + + player.sendMessage( chatmsgprefix + ChatColor.GOLD + "Outstanding!! " + ChatColor.GREEN + "You made it to FIRST place on the leaderboard!! " + ChatColor.LIGHT_PURPLE + "WOW!!" ); + leaderboardmgr.addTime( finaltime, playerid ); + //TODO: mega fanfare + } + + // second to fourth place + if ( timerank > 1 && timerank <= 4 && !failedtobeat ) { + + player.sendMessage( chatmsgprefix + ChatColor.GOLD + "Brilliant!! " + ChatColor.GREEN + "You made it into the top 4" + ( beatexistingtime ? " and beat your existing time!" : "! Way to go!" ) ); + if ( beatexistingtime ) { + player.sendMessage( chatmsgprefix + ChatColor.GREEN + "You beat your old time by " + Utils.formatTime( ( playercurrenttime - finaltime ), "final" ) + ". Incredible!!" ); + } + leaderboardmgr.addTime( finaltime, playerid ); + //TODO: fanfare + } + + // somewhere on the displayed leaderboard + if ( timerank > 4 && !failedtobeat ) { + + player.sendMessage( chatmsgprefix + ChatColor.GOLD + "Great job! " + ChatColor.GREEN + "you ranked #" + timerank + " on the leaderboard" + ( beatexistingtime ? " and beat your existing time!" : "! Great job!" ) ); + + // if they beat their old time, show by how much + if ( beatexistingtime ) { + player.sendMessage( chatmsgprefix + ChatColor.GREEN + "You beat your old time by " + Utils.formatTime( ( playercurrenttime - finaltime ), "final" ) + ". Way to go!!" ); + } + leaderboardmgr.addTime( finaltime, playerid ); + } + + // failed to beat an existing time + if ( failedtobeat ) { + + player.sendMessage( chatmsgprefix + ChatColor.GOLD + "Good attempt! " + ChatColor.GREEN + "However you failed to beat your existing time." ); + // did not beat existing score - give some encouragement if close - 10% and 20% range for now + double timediff = 1.0 - playercurrenttime.doubleValue() / finaltime.doubleValue(); + if ( timediff < 0.1 ) { + player.sendMessage( chatmsgprefix + ChatColor.GREEN + "Getting very close! Keep trying as you're almost there!!" ); + } else if ( timediff < 0.2 ) { + player.sendMessage( chatmsgprefix + ChatColor.GREEN + "Getting closer! Get those neurons fired up and remember the route!!" ); + } + } + + // redraw leaderboard + OBAmazeingTimer.getInstance().getSignManager().populateSigns( OBAmazeingTimer.getInstance().getLeaderboardManager() ); + + } else { + + // not on leaderboard with this time + player.sendMessage( chatmsgprefix + ChatColor.GOLD + "Nice try! " + ChatColor.GREEN + "However you didn't make it onto the leaderboard with this time!" ); + player.sendMessage( chatmsgprefix + ChatColor.GREEN + "Better luck next try!" ); + } + + // redraw leaderboard and remove any timers and tasks + OBAmazeingTimer.getInstance().getSignManager().populateSigns( OBAmazeingTimer.getInstance().getLeaderboardManager() ); + removePlayerTimer( playerid ); + removePlayerTask( playerid ); + + } + + /* + * Check whether a player has a timer running or not + * + * @param UUID of the player + * @return true if a timer is running, false if not + */ + public boolean timerRunning( UUID playerid ) { + + return ( playertimers.containsKey( playerid ) && playertimers.get( playerid ).isStarted() ) ? true : false; + } + + /* + * Get the current timer time in milliseconds of a player timer + * + * @param UUID of the player + * @return the time in milliseconds as a string if the player has a timer, or null + */ + public String GetTime( UUID playerid ) { + + if ( playertimers.containsKey( playerid )) { + + return playertimers.get( playerid ).toString(); + } + + return null; + } + + /* + * Start a task to display the time of a player timer on the action bar + * and stop the timer and cancel the task if the timer has been stopped + * + * @param UUID of the player + */ + private void updatePlayerTimer( UUID playerid ) { + + Player player = Bukkit.getPlayer( playerid ); + + new BukkitRunnable() { + + @Override + public void run() { + + if ( playertimers.containsKey( playerid ) && !playertimers.get( player.getUniqueId() ).isStopped() ) { + + if ( !playertasks.containsKey( playerid ) ) { + playertasks.put( playerid, this.getTaskId() ); + } + + String time = Utils.formatTime( playertimers.get( playerid ).getTime(), "actionbar" ); + time = ChatColor.GREEN + "Time: " + ChatColor.YELLOW + time; + TextComponent mtime = new TextComponent( time ); + if ( player != null ) { + player.spigot().sendMessage( ChatMessageType.ACTION_BAR, mtime ); + } + + // cancel our task if our timer is stopped or removed + if ( playertimers.get( player.getUniqueId() ).isStopped() || !playertimers.containsKey( playerid ) ) { + this.cancel(); + playertasks.remove( playerid ); + } + } else { + this.cancel(); + playertasks.remove( playerid ); + } + } + }.runTaskTimer( OBAmazeingTimer.getInstance(), 1L, 1L ); + } + + /* + * Stop any timers running for a player and remove from our timer list + * + * @param UUID of the player + */ + public void removePlayerTimer( UUID playerid ) { + + if ( !playertimers.containsKey( playerid ) ) { + return; + } + + if ( playertimers.get( playerid ).isStarted() ) { + + playertimers.get( playerid ).stop(); + playertimers.get( playerid ).reset(); + playertimers.remove( playerid ); + } + } + + /* + * Remove a player timer task if running + * + * @param UUID of the player + */ + public void removePlayerTask( UUID playerid ) { + + if ( playertasks.containsKey( playerid ) ) { + + if ( Bukkit.getScheduler().isCurrentlyRunning( playertasks.get( playerid ) ) ) { + Bukkit.getScheduler().cancelTask( playertasks.get( playerid ) ); + } + + playertasks.remove( playerid ); + } + } + +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/MazeTrigger.java b/src/main/java/net/obmc/OBAmazeingTimer/MazeTrigger.java new file mode 100644 index 0000000..d1dc731 --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/MazeTrigger.java @@ -0,0 +1,93 @@ +package net.obmc.OBAmazeingTimer; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.jetbrains.annotations.NotNull; + +public class MazeTrigger { + + private Location padlocation; + private Material padmaterial; + + /** + * Construct a new {@link MazeLocation}. Represents a trigger for entering or + * exiting the maze, such as pressure plates + * + * @param padloc the location of the pad + * @param padmat the material of the pad + */ + public MazeTrigger( @NotNull Location padloc, @NotNull Material padmat ) { + this.padlocation = padloc; + this.padmaterial = padmat; + } + + + /* + * Returns the location of a trigger + * + * @return {@link Location} + */ + public Location getPadLocation() { + return this.padlocation; + } + + /* + * Returns the X coordinate of a trigger location + * + * @return X coordinate as integer + */ + public int getPadLocationX() { + return (int) this.padlocation.getX(); + } + + /* + * Returns the Y coordinate of a trigger location + * + * @return Y coordinate as integer + */ + public int getPadLocationY() { + + return (int) this.padlocation.getY(); + } + + /* + * Returns the Z coordinate of a trigger location + * + * @return Z coordinate as integer + */ + public int getPadLocationZ() { + + return (int) this.padlocation.getZ(); + } + + /* + * Sets the location of a trigger + * + * @param {@link Location} of the trigger + */ + public void setPadLocation( Location padloc ) { + + this.padlocation = padloc; + } + + /* + * Returns the material type of a trigger + * + * @return {@link Material} type + */ + public Material getPadMaterial() { + + return this.padmaterial; + } + + /* + * Sets the material type of a trigger + * + * @param {@link Material} type + */ + public void setPadMaterial( Material padmat ) { + + this.padmaterial = padmat; + } + +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/OBAmazeingTimer.java b/src/main/java/net/obmc/OBAmazeingTimer/OBAmazeingTimer.java new file mode 100644 index 0000000..e5f09b0 --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/OBAmazeingTimer.java @@ -0,0 +1,285 @@ +package net.obmc.OBAmazeingTimer; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.configuration.Configuration; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import net.md_5.bungee.api.ChatColor; + +public class OBAmazeingTimer extends JavaPlugin +{ + static Logger log = Logger.getLogger("Minecraft"); + + public static OBAmazeingTimer instance; + + private EventListener listener; + + private MazeTrigger starttimertrigger; + private MazeTrigger stoptimertrigger; + + private double mazehigh; + private double mazelow; + private Location mazecornerhigh; + private Location mazecornerlow; + + private MazeTimer timers = new MazeTimer(); + private LeaderboardManager leaderboardmanager = null; + private SignManager signmanager = null; + + private static String plugin = "OBAmazeingTimer"; + private static String pluginprefix = "[" + plugin + "]"; + private static String chatmsgprefix = ChatColor.AQUA + "" + ChatColor.BOLD + plugin + ChatColor.DARK_GRAY + ChatColor.BOLD + " » " + ChatColor.LIGHT_PURPLE + ""; + private static String logmsgprefix = pluginprefix + " » "; + + public OBAmazeingTimer() { + instance = this; + } + + /* + * Make our (public) main class methods and variables available to other classes + * + * @return this instance + */ + public static OBAmazeingTimer getInstance() { + return instance; + } + + @Override + public void onEnable() { + // Initialize Stuff + if ( !initializeStuff() ) { + log.log(Level.INFO, logmsgprefix + "Failed to initialize plugin from config"); + + } + + // Register stuff + registerStuff(); + + log.log(Level.INFO, getLogMsgPrefix() + " Plugin Version " + this.getDescription().getVersion() + " activated!"); + } + + // Plugin Stop + public void onDisable() { + + log.log(Level.INFO, getLogMsgPrefix() + " Plugin deactivated!"); + } + + /* + * Load plugin config and set up various objects we need for the timer, like the + * triggers, leaderboard, and render the leaderboard in the world + * + * @return true if everything went ok, otherwise false + */ + public boolean initializeStuff() { + + this.saveDefaultConfig(); + Configuration config = this.getConfig(); + + // setup entry location + try { + this.starttimertrigger = new MazeTrigger( + new Location( Bukkit.getWorld( config.getConfigurationSection( "maze" ).getString( "world" ) ), + config.getConfigurationSection( "starttimertrigger" ).getConfigurationSection("pad").getDouble( "x" ), + config.getConfigurationSection( "starttimertrigger" ).getConfigurationSection("pad").getDouble( "y" ), + config.getConfigurationSection( "starttimertrigger" ).getConfigurationSection("pad").getDouble( "z" ) + ), + Material.valueOf( config.getConfigurationSection( "starttimertrigger" ).getConfigurationSection( "pad" ).getString( "material" ) ) + ); + } catch( Exception e ) { + log.log( Level.SEVERE, logmsgprefix + "Failed to setup maze start timer trigger" ); + e.printStackTrace(); + return false; + } + + // setup exit location + try { + this.stoptimertrigger = new MazeTrigger( + new Location( Bukkit.getWorld( config.getConfigurationSection( "maze" ).getString( "world" ) ), + config.getConfigurationSection( "stoptimertrigger" ).getConfigurationSection( "pad" ).getDouble( "x" ), + config.getConfigurationSection( "stoptimertrigger" ).getConfigurationSection( "pad" ).getDouble( "y" ), + config.getConfigurationSection( "stoptimertrigger" ).getConfigurationSection( "pad" ).getDouble( "z" ) + ), + Material.valueOf( config.getConfigurationSection( "stoptimertrigger" ).getConfigurationSection("pad").getString( "material" ) ) + ); + } catch( Exception e ) { + log.log( Level.SEVERE, logmsgprefix + "Failed to setup maze stop timer trigger" ); + e.printStackTrace(); + return false; + } + + // read maze properties + try { + this.mazehigh = config.getConfigurationSection( "maze" ).getDouble( "high" ); + this.mazelow = config.getConfigurationSection( "maze" ).getDouble( "low" ); + this.mazecornerhigh = new Location( + Bukkit.getWorld( config.getConfigurationSection( "maze" ).getString( "world" ) ), + config.getConfigurationSection( "maze" ).getConfigurationSection( "sidehigh" ).getDouble( "x" ), + this.mazehigh, + config.getConfigurationSection( "maze" ).getConfigurationSection( "sidehigh" ).getDouble( "z" ) + ); + this.mazecornerlow = new Location( + Bukkit.getWorld( config.getConfigurationSection( "maze" ).getString( "world" ) ), + config.getConfigurationSection( "maze" ).getConfigurationSection( "sidelow" ).getDouble( "x" ), + this.mazehigh, + config.getConfigurationSection( "maze" ).getConfigurationSection( "sidelow" ).getDouble( "z" ) + ); + + } catch( Exception e ) { + log.log( Level.SEVERE, logmsgprefix + "Failed to read maze properties" ); + e.printStackTrace(); + return false; + } + + // read current leaderboard config, populate signs and render leaderboard + leaderboardmanager = new LeaderboardManager(); + leaderboardmanager.loadLeaderboardFromConfig(); + signmanager = new SignManager(); + signmanager.loadSignsFromConfig(); + signmanager.populateSigns( leaderboardmanager ); + + return true; + } + + /* + * Register event listeners + */ + public void registerStuff() { + + this.listener = new EventListener(); + this.getServer().getPluginManager().registerEvents( (Listener)this.listener, (Plugin)this ); + } + + /* + * Return the Y coordinate which represents the top of the maze + * Used for bounds checking to is player exited the maze somehow + * + * @return maze high value + */ + public double getMazeHigh() { + + return this.mazehigh; + } + + /* + * Return the Y coordinate which represents the bottom of the maze + * Used for bounds checking to is player exited the maze somehow + * + * @return maze low value + */ + public double getMazeLow() { + + return this.mazelow; + } + + /* + * Return the location which represents the high corner of the maze + * Used for bounds checking to is player exited the maze somehow + * + * @return maze high corner + */ + public Location getMazeSideHigh() { + + return this.mazecornerhigh; + } + + /* + * Return the location which represents the low corner of the maze + * Used for bounds checking to is player exited the maze somehow + * + * @return maze low corner + */ + public Location getMazeSideLow() { + + return this.mazecornerlow; + } + + /* + * Return a maze timer manager + * + * @return {@link MazeTimer} + */ + public MazeTimer getMazeTimer() { + + return this.timers; + } + + /* + * Return the maze start timer trigger + * + * @return {@link MazeTrigger} + */ + public MazeTrigger getStartTimerTrigger() { + + return starttimertrigger; + } + + /* + * Return the maze stop timer trigger + * + * @return {@link MazeTrigger} + */ + public MazeTrigger getStopTimerTrigger() { + return stoptimertrigger; + } + + /* + * Return the leaderboard manager + * + * @return {@link LeaderboardManager} + */ + public LeaderboardManager getLeaderboardManager() { + return this.leaderboardmanager; + } + + /* + * Return the sign manager + * + * @return {@link SignManager} + */ + public SignManager getSignManager() { + return this.signmanager; + } + + /* + * Return the name of the plugin + * + * @return plugin name as a string + */ + public static String getPluginName() { + return plugin; + } + + /* + * Return the prefix used by the plugin for player and log messaging + * + * @return prefix as a string + */ + public static String getPluginPrefix() { + return pluginprefix; + } + + /* + * Return the prefix used by the plugin for player messaging + * + * @return prefix as a string + */ + public static String getChatMsgPrefix() { + return chatmsgprefix; + } + + /* + * Return the prefix used by the plugin for logging + * + * @return prefix as a string + */ + public static String getLogMsgPrefix() { + return logmsgprefix; + } +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/SignManager.java b/src/main/java/net/obmc/OBAmazeingTimer/SignManager.java new file mode 100644 index 0000000..97c05a0 --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/SignManager.java @@ -0,0 +1,285 @@ +package net.obmc.OBAmazeingTimer; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import net.md_5.bungee.api.ChatColor; + +public class SignManager { + + static Logger log = Logger.getLogger("Minecraft"); + + private Map signs = new HashMap(); + + private String logmsgprefix = null; + + private String signsfilename = "signs.yml"; + private boolean redrawtitlesigns = true; + private String worldname = "world"; + private int signrows = 3; + private BlockFace signdirection = BlockFace.EAST; + private Material titlesignmaterial = null; + private Material titlebackmaterial = null; + private ChatColor titletextcolor = null; + private Material leaderboardsignmaterial = null; + private Material leaderboardbackmaterial = null; + private ChatColor leaderboardtextcolor = null; + + /* + * Draws the in-game leaderboard using signs and backing blocks + * + * Sign material, backing block material and sign text color and + * the number of rows of signs (size of the leaderboard) as well + * as the direction the leaderboard faces are set in the signs + * configuration file. + */ + public SignManager() { + + if ( signs == null ) { + signs = new HashMap(); + } + + OBAmazeingTimer.getChatMsgPrefix(); + logmsgprefix = OBAmazeingTimer.getLogMsgPrefix(); + + File signsfile = new File( OBAmazeingTimer.getInstance().getDataFolder(), signsfilename ); + if ( !signsfile.exists() ) { + signsfile.getParentFile().mkdirs(); + OBAmazeingTimer.getInstance().saveResource( signsfilename, false ); + } + } + + /* + * Return the map of sign locations + * + * @return hash of sign type and its location + */ + public Map getSigns() { + return signs; + } + + /* + * Load up our title and leaderboard sign hashes with the data from the config file + * + * @return true if no issued encountered, false if not + */ + public boolean loadSignsFromConfig() { + + FileConfiguration fileload = YamlConfiguration.loadConfiguration( new File( OBAmazeingTimer.getInstance().getDataFolder(), signsfilename ) ); + if ( fileload == null ) { + log.log( Level.SEVERE, logmsgprefix + "No signs file found in the plugin folder"); + return false; + } + + if ( !fileload.contains( "signs" ) || !fileload.contains( "signrows" ) ) { + log.log( Level.INFO, logmsgprefix + "Signs file is missing a signs section or the number of signs value"); + return false; + } + + double[] coords = { 0, 0, 0 }; + + try { + + worldname = OBAmazeingTimer.getInstance().getConfig().getConfigurationSection( "maze" ).getString( "world" ); + redrawtitlesigns = fileload.getBoolean( "redrawtitlesigns" ); + signrows = fileload.getInt( "signrows" ); + + // load title sign config from signs file + ConfigurationSection signsconfig = fileload.getConfigurationSection( "signs" ); + signdirection = BlockFace.valueOf( signsconfig.getString( "facing" ) ); + ConfigurationSection titleconfig = signsconfig.getConfigurationSection( "title" ); + titlesignmaterial = Material.valueOf( titleconfig.getString( "signmaterial" ) ); + titlebackmaterial = Material.valueOf( titleconfig.getString( "backingmaterial" ) ); + titletextcolor = ChatColor.of( titleconfig.getString( "textcolor" ) ); + coords[0] = titleconfig.getConfigurationSection( "startlocation" ).getDouble( "x" ); + coords[1] = titleconfig.getConfigurationSection( "startlocation" ).getDouble( "y" ); + coords[2] = titleconfig.getConfigurationSection( "startlocation" ).getDouble( "z" ); + + // we are fixed with 3 sign wide (rank, name and time), but it can be any number of sign rows + for ( int signnum = 0; signnum < 3; signnum++ ) { + + signs.put( "title" + signnum, new Location ( Bukkit.getWorld( worldname ), coords[0], coords[1], coords[2] ) ); + + // repeat the signs in the correct direction based on which way the signs are facing + coords = nextLocationByDirection( signdirection, coords ); + } + + // load leaderboard sign config + ConfigurationSection leaderboardconfig = signsconfig.getConfigurationSection( "leaderboard" ); + leaderboardsignmaterial = Material.valueOf( leaderboardconfig.getString( "signmaterial" ) ); + leaderboardbackmaterial = Material.valueOf( leaderboardconfig.getString( "backingmaterial" ) ); + leaderboardtextcolor = ChatColor.of( leaderboardconfig.getString( "textcolor" ) ); + coords[0] = leaderboardconfig.getConfigurationSection( "startlocation" ).getDouble( "x" ); + coords[1] = leaderboardconfig.getConfigurationSection( "startlocation" ).getDouble( "y" ); + coords[2] = leaderboardconfig.getConfigurationSection( "startlocation" ).getDouble( "z" ); + + // generate a rank, player and time sign for however many rows on the leaderboard + // we are fixed 3 signs wide - rank, name and time + // there are 4 ranks per sign, so 3 signs would give 12 ranks + for ( int signrow = 0; signrow < signrows ; signrow++ ) { + + signs.put( "ranksign" + signrow, new Location( Bukkit.getWorld( worldname ), coords[0], coords[1], coords[2] ) ); + coords = nextLocationByDirection( signdirection, coords ); + + signs.put( "namesign" + signrow, new Location( Bukkit.getWorld( worldname ), coords[0], coords[1], coords[2] ) ); + + coords = nextLocationByDirection( signdirection, coords ); + signs.put( "timesign" + signrow, new Location( Bukkit.getWorld( worldname ), coords[0], coords[1], coords[2] ) ); + + coords[0] = leaderboardconfig.getConfigurationSection( "startlocation" ).getDouble( "x" ); + coords[1]--; + coords[2] = leaderboardconfig.getConfigurationSection( "startlocation" ).getDouble( "z" ); + } + } catch ( Exception e ) { + + log.log(Level.SEVERE, logmsgprefix + "Failed to load leaderboard sign data from sign file " + signsfilename ); + e.printStackTrace(); + return false; + } + + return true; + } + + /* + * Render the leaderboard in the world. We create the backing block and the signs + * on them to represent the leaderboard title and data from the leaderboard manager + * + * @param {@link LeaderboardManager} + */ + public void populateSigns( LeaderboardManager leaderboardmanager ) { + + Block signblock = null; + + // redraw the title signs if required + if ( redrawtitlesigns ) { + for ( int signnum = 0; signnum < 3; signnum++ ) { + + signblock = Bukkit.getWorld( worldname ).getBlockAt( signs.get( "title" + signnum ) ); + setSignBlock( signblock, titlesignmaterial, titlebackmaterial ); + + Sign titlesign = (Sign) Bukkit.getWorld( worldname ).getBlockAt( signs.get( "title" + signnum ) ).getState(); + for ( int line = 0; line < 4; line++ ) { + if ( signnum == 1 && line == 1 ) { + titlesign.setLine( line, ChatColor.AQUA + "A-MAZE-ING" ); + } else if ( signnum == 1 && line == 2 ) { + titlesign.setLine( line, ChatColor.GREEN + "LEADERBOARD" ); + } else { + titlesign.setLine( line, titletextcolor + "***************" ); + } + } + titlesign.update(); + } + } + + ArrayList playertimes = new ArrayList( leaderboardmanager.getLeaderboard().keySet() ); + ArrayList playerids = new ArrayList( leaderboardmanager.getLeaderboard().values() ); + int numleaders = playerids.size(); + + // populate however many rows of signs with 4 lines each with as much data as we have and the remaining with a message + int prevsignnum = -1; + Sign ranksign = null; + Sign namesign = null; + Sign timesign = null; + for ( int ranknum = 0; ranknum < ( signrows * 4 ); ranknum++ ) { + + int signnum = (int)( ranknum / 4 ) + 1; + int signlinenum = ranknum - ((int)( ranknum / 4 ) * 4 ); + + // detect when we've moved from one row of signs to the next + // a row of signs is a rank, name and time sign. Four ranks/lines per sign set + if ( signnum != prevsignnum ) { + + signblock = Bukkit.getWorld( worldname ).getBlockAt( signs.get( "ranksign" + ( signnum - 1 ) ) ); + setSignBlock( signblock, leaderboardsignmaterial, leaderboardbackmaterial ); + ranksign = (Sign) signblock.getState(); + + signblock = Bukkit.getWorld( worldname ).getBlockAt( signs.get( "namesign" + (signnum - 1 ) ) ); + setSignBlock( signblock, leaderboardsignmaterial, leaderboardbackmaterial ); + namesign = (Sign) signblock.getState(); + + signblock = Bukkit.getWorld( worldname ).getBlockAt( signs.get( "timesign" + ( signnum - 1 ) ) ); + setSignBlock( signblock, leaderboardsignmaterial, leaderboardbackmaterial ); + timesign = (Sign) signblock.getState(); + + prevsignnum = signnum; + } + + // put text on our rank, name and time signs for this row on the leaderboard + if ( ranknum < numleaders ) { + + ranksign.setLine( signlinenum, leaderboardtextcolor + "" + ( ranknum + 1 ) + "." ); + namesign.setLine( signlinenum, leaderboardtextcolor + Bukkit.getOfflinePlayer( playerids.get( ranknum ) ).getName() ); + timesign.setLine( signlinenum, leaderboardtextcolor + Utils.formatTime( playertimes.get( ranknum ), "leaderboard" ) ); + + } else { + ranksign.setLine( signlinenum, leaderboardtextcolor + "" + ( ranknum + 1 ) + "."); + namesign.setLine( signlinenum, leaderboardtextcolor + "Your name here!" ); + timesign.setLine( signlinenum, leaderboardtextcolor + "Your time here!" ); + } + + // send players the sign update event so they see the signs updated in-game + for (Player player : Bukkit.getOnlinePlayers()) { + player.sendSignChange( signs.get( "ranksign" + ( signnum - 1 ) ), ranksign.getLines() ); + player.sendSignChange( signs.get( "namesign" + ( signnum - 1 ) ), namesign.getLines() ); + player.sendSignChange( signs.get( "timesign" + ( signnum - 1 ) ), namesign.getLines() ); + } + ranksign.update( true ); + namesign.update( true ); + timesign.update( true ); + } + } + + /* + * Sets the backing block and sign at the sign block location + * + * @param {@link Block} the block for the sign + * @param {@link Material} the sign material + * @param {@link Material} the backing block material + */ + private void setSignBlock( Block signblock, Material sign, Material backing ) { + + Block backblock = signblock.getRelative( signdirection.getOppositeFace() ); + if ( !backblock.equals( backing ) ) { + + backblock.setType( backing ); + } + + signblock.setType( sign ); + Utils.setSignFacing( signblock, signdirection ); + } + + /* + * Get next block location based on direction + * + * @param {@link BlockFace} direction + * @param x, -x, z and -z array + * @return adjusted coordinates indicating the next block location + */ + private double[] nextLocationByDirection( BlockFace direction, double[] coords ) { + if ( direction.equals( BlockFace.EAST ) ) { + coords[2]--; + } else if ( direction.equals( BlockFace.SOUTH ) ) { + coords[0]++; + } else if ( direction.equals( BlockFace.WEST ) ) { + coords[2]++; + } else { + coords[0]--; + } + return coords; + } +} diff --git a/src/main/java/net/obmc/OBAmazeingTimer/Utils.java b/src/main/java/net/obmc/OBAmazeingTimer/Utils.java new file mode 100644 index 0000000..a2e7644 --- /dev/null +++ b/src/main/java/net/obmc/OBAmazeingTimer/Utils.java @@ -0,0 +1,94 @@ +package net.obmc.OBAmazeingTimer; + +import java.util.logging.Logger; + +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import org.bukkit.block.data.type.WallSign; + +public class Utils { + + static Logger log = Logger.getLogger("Minecraft"); + + /* + * Format time into a message for the action bar, final time message or leaderboard + * + * @param time in milliseconds + * @param type of formatting - final time, action bar or leaderboard + * @return formatted time string + */ + public static String formatTime( long time, String type ) { + + //long milliseconds = ( time % 1000 ); + //int two_digit_ms = (int) (milliseconds/10); + int totalseconds = (int) ( time /1000.0 ); + int hours = (int)( totalseconds / 3600 ); + int minutes = (int)( ( totalseconds % 3600 ) / 60 ); + int seconds = totalseconds % 60; + + String hrsstr = ""; String minstr = ""; String secstr = ""; + + if ( type.equals( "final" ) ) { + + // final time formatting - when player exits the maze + hrsstr = "%1d hour" + ( hours != 1 ? "s" : "" ); + minstr = "%1d minute" + ( minutes != 1 ? "s" : "" ); + secstr = "%1d second" + ( seconds != 1 ? "s" : "" ); + if (hours > 0) { + return String.format( hrsstr + " " + minstr + " " + secstr, hours, minutes, seconds ); + } else { + if ( minutes > 0 ) { + return String.format( minstr + " " + secstr, minutes, seconds ); + } else { + return String.format( secstr, seconds ); + } + } + } else if ( type.equals( "actionbar" ) ) { + + // action bar formatting - condensed to just digits + if ( hours > 0 ) { + return String.format("%01d:%02d:%02d", hours, minutes, seconds ); + } else { + return String.format("%01d:%02d", minutes, seconds ); + } + } else { + + // leaderboard formatting - shortened wording + hrsstr = "%1d hr" + ( hours != 1 ? "s" : "" ); + minstr = "%1d min" + ( minutes != 1 ? "s" : "" ); + secstr = "%1d sec" + ( seconds != 1 ? "s" : "" ); + if ( hours > 0 ) { + return String.format( hrsstr + " " + minstr + " " + secstr, hours, minutes, seconds); + } else { + if ( minutes > 0 ) { + return String.format( minstr + " " + secstr, minutes, seconds); + } else { + return String.format( secstr, seconds); + } + } + } + } + + /* + * Set the direction of a sign at a block location + * + * @param {@link Block} block + * @param {@link BlockFace} direction + */ + public static void setSignFacing( Block block, BlockFace face ){ + + if ( block.getState() instanceof Sign ) { + + Sign sign = (Sign) block.getState(); + if ( sign.getBlockData() instanceof WallSign ) { + + WallSign signData = (WallSign) sign.getBlockData(); + signData.setFacing( face ); + sign.setBlockData( signData ); + block.setBlockData( sign.getBlockData() ); + sign.update(); + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..91daf36 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,24 @@ +starttimertrigger: + pad: + x: 618 + y: 65 + z: -85 + material: LIGHT_WEIGHTED_PRESSURE_PLATE +stoptimertrigger: + pad: + world: world + x: 536 + y: 65 + z: -83 + material: LIGHT_WEIGHTED_PRESSURE_PLATE +maze: + world: world + sidehigh: + x: 620 + z: -43 + sidelow: + x: 536 + z: -124 + high: 67 + low: 63 + \ No newline at end of file diff --git a/src/main/resources/leaderboard.yml b/src/main/resources/leaderboard.yml new file mode 100644 index 0000000..93672ba --- /dev/null +++ b/src/main/resources/leaderboard.yml @@ -0,0 +1 @@ +timings: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..1f38c0b --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: OBAmazeingTimer +version: 1.0 +description: Timer for solving A-Maze-Ing maze on the build server and recording attempt times on signs +author: F451 +api-version: 1.19 +website: https://ob-mc.net +main: net.obmc.OBAmazeingTimer.OBAmazeingTimer +depend: [] +commands: diff --git a/src/main/resources/signs.yml b/src/main/resources/signs.yml new file mode 100644 index 0000000..29c7c5e --- /dev/null +++ b/src/main/resources/signs.yml @@ -0,0 +1,20 @@ +signrows: 3 +redrawtitlesigns: false +signs: + facing: EAST + title: + startlocation: + x: 621 + y: 66 + z: -88 + signmaterial: OAK_WALL_SIGN + backingmaterial: OAK_PLANKS + textcolor: BLACK + leaderboard: + startlocation: + x: 621 + y: 67 + z: -88 + signmaterial: OAK_WALL_SIGN + backingmaterial: OAK_PLANKS + textcolor: BLACK \ No newline at end of file