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
+
+
+
+
+
+
+
+ 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