From 1b1f36c5d6fc247455db395db8bf9af6e60fe53a Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 28 Sep 2013 16:25:30 +0200 Subject: [PATCH] Attempt to handle snapshot versions by assuming Minecraft version. The snapshot version contains a release date, so we'll simply compare that against a known release date of a Minecraft version. It it's later, we know it is at least a minor version above, and vice versa. --- ProtocolLib/.classpath | 64 +- .../comphenix/protocol/ProtocolLibrary.java | 13 +- .../protocol/async/PacketSendingQueue.java | 610 +++++++++--------- .../protocol/utility/MinecraftVersion.java | 84 ++- .../protocol/utility/SnapshotVersion.java | 108 ++++ .../protocol/MinecraftVersionTest.java | 8 +- .../protocol/utility/SnapshotVersionTest.java | 41 ++ 7 files changed, 579 insertions(+), 349 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/SnapshotVersion.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/protocol/utility/SnapshotVersionTest.java diff --git a/ProtocolLib/.classpath b/ProtocolLib/.classpath index c5d993a4..e842aee4 100644 --- a/ProtocolLib/.classpath +++ b/ProtocolLib/.classpath @@ -1,32 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 178892d5..08b4bf1a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -81,12 +81,17 @@ public class ProtocolLibrary extends JavaPlugin { /** * The minimum version ProtocolLib has been tested with. */ - private static final String MINIMUM_MINECRAFT_VERSION = "1.0.0"; + public static final String MINIMUM_MINECRAFT_VERSION = "1.0.0"; /** * The maximum version ProtocolLib has been tested with, */ - private static final String MAXIMUM_MINECRAFT_VERSION = "1.6.2"; + public static final String MAXIMUM_MINECRAFT_VERSION = "1.6.2"; + + /** + * The date (with ISO 8601) when the most recent version was released. + */ + public static final String MINECRAFT_LAST_RELEASE_DATE = "2013-07-08"; /** * The number of milliseconds per second. @@ -376,7 +381,7 @@ public class ProtocolLibrary extends JavaPlugin { logger.warning("Version " + current + " has not yet been tested! Proceed with caution."); } return current; - + } catch (Exception e) { reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e)); } @@ -384,7 +389,7 @@ public class ProtocolLibrary extends JavaPlugin { // Unknown version return null; } - + private void checkConflictingVersions() { Pattern ourPlugin = Pattern.compile("ProtocolLib-(.*)\\.jar"); MinecraftVersion currentVersion = new MinecraftVersion(this.getDescription().getVersion()); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java index 2da2d220..39556f29 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java @@ -1,305 +1,305 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.async; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.PriorityBlockingQueue; - -import org.bukkit.entity.Player; - -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.injector.PlayerLoggedOutException; -import com.comphenix.protocol.reflect.FieldAccessException; - -/** - * Represents packets ready to be transmitted to a client. - * @author Kristian - */ -abstract class PacketSendingQueue { - - public static final int INITIAL_CAPACITY = 10; - - private PriorityBlockingQueue sendingQueue; - - // Asynchronous packet sending - private Executor asynchronousSender; - - // Whether or not packet transmission must occur on a specific thread - private final boolean notThreadSafe; - - // Whether or not we've run the cleanup procedure - private boolean cleanedUp = false; - - /** - * Create a packet sending queue. - * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. - */ - public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { - this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); - this.notThreadSafe = notThreadSafe; - this.asynchronousSender = asynchronousSender; - } - - /** - * Number of packet events in the queue. - * @return The number of packet events in the queue. - */ - public int size() { - return sendingQueue.size(); - } - - /** - * Enqueue a packet for sending. - * @param packet - packet to queue. - */ - public void enqueue(PacketEvent packet) { - sendingQueue.add(new PacketEventHolder(packet)); - } - - /** - * Invoked when one of the packets have finished processing. - * @param packetUpdated - the packet that has now been updated. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { - - AsyncMarker marker = packetUpdated.getAsyncMarker(); - - // Should we reorder the event? - if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { - PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); - - // "Cancel" the original event - packetUpdated.setReadOnly(false); - packetUpdated.setCancelled(true); - - // Enqueue the copy with the new sending index - enqueue(copy); - } - - // Mark this packet as finished - marker.setProcessed(true); - trySendPackets(onMainThread); - } - - /*** - * Invoked when a list of packet IDs are no longer associated with any listeners. - * @param packetsRemoved - packets that no longer have any listeners. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { - - Set lookup = new HashSet(packetsRemoved); - - // Note that this is O(n), so it might be expensive - for (PacketEventHolder holder : sendingQueue) { - PacketEvent event = holder.getEvent(); - - if (lookup.contains(event.getPacketID())) { - event.getAsyncMarker().setProcessed(true); - } - } - - // This is likely to have changed the situation a bit - trySendPackets(onMainThread); - } - - /** - * Attempt to send any remaining packets. - * @param onMainThread - whether or not this is occuring on the main thread. - */ - public void trySendPackets(boolean onMainThread) { - // Whether or not to continue sending packets - boolean sending = true; - - // Transmit as many packets as we can - while (sending) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sending = processPacketHolder(onMainThread, holder); - - if (!sending) { - // Add it back again - sendingQueue.add(holder); - } - - } else { - // No more packets to send - sending = false; - } - } - } - - /** - * Invoked when a packet might be ready for transmission. - * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. - * @param holder - packet container. - * @return TRUE to continue sending packets, FALSE otherwise. - */ - private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { - PacketEvent current = holder.getEvent(); - AsyncMarker marker = current.getAsyncMarker(); - boolean hasExpired = marker.hasExpired(); - - // Guard in cause the queue is closed - if (cleanedUp) { - return true; - } - - // End condition? - if (marker.isProcessed() || hasExpired) { - if (hasExpired) { - // Notify timeout listeners - onPacketTimeout(current); - - // Recompute - marker = current.getAsyncMarker(); - hasExpired = marker.hasExpired(); - - // Could happen due to the timeout listeners - if (!marker.isProcessed() && !hasExpired) { - return false; - } - } - - // Is it okay to send the packet? - if (!current.isCancelled() && !hasExpired) { - // Make sure we're on the main thread - if (notThreadSafe) { - try { - boolean wantAsync = marker.isMinecraftAsync(current); - boolean wantSync = !wantAsync; - - // Wait for the next main thread heartbeat if we haven't fulfilled our promise - if (!onMainThread && wantSync) { - return false; - } - - // Let's give it what it wants - if (onMainThread && wantAsync) { - asynchronousSender.execute(new Runnable() { - @Override - public void run() { - // We know this isn't on the main thread - processPacketHolder(false, holder); - } - }); - - // Scheduler will do the rest - return true; - } - - } catch (FieldAccessException e) { - e.printStackTrace(); - - // Just drop the packet - return true; - } - } - - // Silently skip players that have logged out - if (isOnline(current.getPlayer())) { - sendPacket(current); - } - } - - // Drop the packet - return true; - } - - // Add it back and stop sending - return false; - } - - /** - * Invoked when a packet has timed out. - * @param event - the timed out packet. - */ - protected abstract void onPacketTimeout(PacketEvent event); - - private boolean isOnline(Player player) { - return player != null && player.isOnline(); - } - - /** - * Send every packet, regardless of the processing state. - */ - private void forceSend() { - while (true) { - PacketEventHolder holder = sendingQueue.poll(); - - if (holder != null) { - sendPacket(holder.getEvent()); - } else { - break; - } - } - } - - /** - * Whether or not the packet transmission must synchronize with the main thread. - * @return TRUE if it must, FALSE otherwise. - */ - public boolean isSynchronizeMain() { - return notThreadSafe; - } - - /** - * Transmit a packet, if it hasn't already. - * @param event - the packet to transmit. - */ - private void sendPacket(PacketEvent event) { - - AsyncMarker marker = event.getAsyncMarker(); - - try { - // Don't send a packet twice - if (marker != null && !marker.isTransmitted()) { - marker.sendPacket(event); - } - - } catch (PlayerLoggedOutException e) { - System.out.println(String.format( - "[ProtocolLib] Warning: Dropped packet index %s of ID %s", - marker.getOriginalSendingIndex(), event.getPacketID() - )); - - } catch (IOException e) { - // Just print the error - e.printStackTrace(); - } - } - - /** - * Automatically transmits every delayed packet. - */ - public void cleanupAll() { - if (!cleanedUp) { - // Note that the cleanup itself will always occur on the main thread - forceSend(); - - // And we're done - cleanedUp = true; - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.async; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.PriorityBlockingQueue; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Represents packets ready to be transmitted to a client. + * @author Kristian + */ +abstract class PacketSendingQueue { + + public static final int INITIAL_CAPACITY = 10; + + private PriorityBlockingQueue sendingQueue; + + // Asynchronous packet sending + private Executor asynchronousSender; + + // Whether or not packet transmission must occur on a specific thread + private final boolean notThreadSafe; + + // Whether or not we've run the cleanup procedure + private boolean cleanedUp = false; + + /** + * Create a packet sending queue. + * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. + */ + public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { + this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY); + this.notThreadSafe = notThreadSafe; + this.asynchronousSender = asynchronousSender; + } + + /** + * Number of packet events in the queue. + * @return The number of packet events in the queue. + */ + public int size() { + return sendingQueue.size(); + } + + /** + * Enqueue a packet for sending. + * @param packet - packet to queue. + */ + public void enqueue(PacketEvent packet) { + sendingQueue.add(new PacketEventHolder(packet)); + } + + /** + * Invoked when one of the packets have finished processing. + * @param packetUpdated - the packet that has now been updated. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { + + AsyncMarker marker = packetUpdated.getAsyncMarker(); + + // Should we reorder the event? + if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { + PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); + + // "Cancel" the original event + packetUpdated.setReadOnly(false); + packetUpdated.setCancelled(true); + + // Enqueue the copy with the new sending index + enqueue(copy); + } + + // Mark this packet as finished + marker.setProcessed(true); + trySendPackets(onMainThread); + } + + /*** + * Invoked when a list of packet IDs are no longer associated with any listeners. + * @param packetsRemoved - packets that no longer have any listeners. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { + + Set lookup = new HashSet(packetsRemoved); + + // Note that this is O(n), so it might be expensive + for (PacketEventHolder holder : sendingQueue) { + PacketEvent event = holder.getEvent(); + + if (lookup.contains(event.getPacketID())) { + event.getAsyncMarker().setProcessed(true); + } + } + + // This is likely to have changed the situation a bit + trySendPackets(onMainThread); + } + + /** + * Attempt to send any remaining packets. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public void trySendPackets(boolean onMainThread) { + // Whether or not to continue sending packets + boolean sending = true; + + // Transmit as many packets as we can + while (sending) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sending = processPacketHolder(onMainThread, holder); + + if (!sending) { + // Add it back again + sendingQueue.add(holder); + } + + } else { + // No more packets to send + sending = false; + } + } + } + + /** + * Invoked when a packet might be ready for transmission. + * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. + * @param holder - packet container. + * @return TRUE to continue sending packets, FALSE otherwise. + */ + private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { + PacketEvent current = holder.getEvent(); + AsyncMarker marker = current.getAsyncMarker(); + boolean hasExpired = marker.hasExpired(); + + // Guard in cause the queue is closed + if (cleanedUp) { + return true; + } + + // End condition? + if (marker.isProcessed() || hasExpired) { + if (hasExpired) { + // Notify timeout listeners + onPacketTimeout(current); + + // Recompute + marker = current.getAsyncMarker(); + hasExpired = marker.hasExpired(); + + // Could happen due to the timeout listeners + if (!marker.isProcessed() && !hasExpired) { + return false; + } + } + + // Is it okay to send the packet? + if (!current.isCancelled() && !hasExpired) { + // Make sure we're on the main thread + if (notThreadSafe) { + try { + boolean wantAsync = marker.isMinecraftAsync(current); + boolean wantSync = !wantAsync; + + // Wait for the next main thread heartbeat if we haven't fulfilled our promise + if (!onMainThread && wantSync) { + return false; + } + + // Let's give it what it wants + if (onMainThread && wantAsync) { + asynchronousSender.execute(new Runnable() { + @Override + public void run() { + // We know this isn't on the main thread + processPacketHolder(false, holder); + } + }); + + // Scheduler will do the rest + return true; + } + + } catch (FieldAccessException e) { + e.printStackTrace(); + + // Just drop the packet + return true; + } + } + + // Silently skip players that have logged out + if (isOnline(current.getPlayer())) { + sendPacket(current); + } + } + + // Drop the packet + return true; + } + + // Add it back and stop sending + return false; + } + + /** + * Invoked when a packet has timed out. + * @param event - the timed out packet. + */ + protected abstract void onPacketTimeout(PacketEvent event); + + private boolean isOnline(Player player) { + return player != null && player.isOnline(); + } + + /** + * Send every packet, regardless of the processing state. + */ + private void forceSend() { + while (true) { + PacketEventHolder holder = sendingQueue.poll(); + + if (holder != null) { + sendPacket(holder.getEvent()); + } else { + break; + } + } + } + + /** + * Whether or not the packet transmission must synchronize with the main thread. + * @return TRUE if it must, FALSE otherwise. + */ + public boolean isSynchronizeMain() { + return notThreadSafe; + } + + /** + * Transmit a packet, if it hasn't already. + * @param event - the packet to transmit. + */ + private void sendPacket(PacketEvent event) { + + AsyncMarker marker = event.getAsyncMarker(); + + try { + // Don't send a packet twice + if (marker != null && !marker.isTransmitted()) { + marker.sendPacket(event); + } + + } catch (PlayerLoggedOutException e) { + System.out.println(String.format( + "[ProtocolLib] Warning: Dropped packet index %s of ID %s", + marker.getOriginalSendingIndex(), event.getPacketID() + )); + + } catch (IOException e) { + // Just print the error + e.printStackTrace(); + } + } + + /** + * Automatically transmits every delayed packet. + */ + public void cleanupAll() { + if (!cleanedUp) { + // Note that the cleanup itself will always occur on the main thread + forceSend(); + + // And we're done + cleanedUp = true; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java index f7dcf041..e1703e92 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java @@ -17,10 +17,13 @@ package com.comphenix.protocol.utility; +import java.text.SimpleDateFormat; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bukkit.Server; +import com.comphenix.protocol.ProtocolLibrary; import com.google.common.base.Objects; import com.google.common.collect.ComparisonChain; @@ -35,15 +38,18 @@ public class MinecraftVersion implements Comparable { /** * Regular expression used to parse version strings. */ - private static final String VERSION_PATTERN = ".*\\(.*MC.\\s*((?:\\d+\\.)*\\d)\\s*\\)"; + private static final String VERSION_PATTERN = ".*\\(.*MC.\\s*([a-zA-z0-9\\-\\.]+)\\s*\\)"; private final int major; private final int minor; private final int build; // The development stage - private final String development; - + private final String development; + + // Snapshot? + private final SnapshotVersion snapshot; + /** * Determine the current Minecraft version. * @param server - the Bukkit server that will be used to examine the MC version. @@ -53,17 +59,53 @@ public class MinecraftVersion implements Comparable { } /** - * Construct a version object from the format major.minor.build. + * Construct a version object from the format major.minor.build, or the snapshot format. * @param versionOnly - the version in text form. */ public MinecraftVersion(String versionOnly) { + this(versionOnly, true); + } + + /** + * Construct a version format from the standard release version or the snapshot verison. + * @param versionOnly - the version. + * @param parseSnapshot - TRUE to parse the snapshot, FALSE otherwise. + */ + private MinecraftVersion(String versionOnly, boolean parseSnapshot) { String[] section = versionOnly.split("-"); - int[] numbers = parseVersion(section[0]); + SnapshotVersion snapshot = null; + int[] numbers = new int[3]; + + try { + numbers = parseVersion(section[0]); + + } catch (NumberFormatException cause) { + // Skip snapshot parsing + if (!parseSnapshot) + throw cause; + + try { + // Determine if the snapshot is newer than the current release version + snapshot = new SnapshotVersion(section[0]); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + + MinecraftVersion latest = new MinecraftVersion(ProtocolLibrary.MAXIMUM_MINECRAFT_VERSION, false); + boolean newer = snapshot.getSnapshotDate().compareTo( + format.parse(ProtocolLibrary.MINECRAFT_LAST_RELEASE_DATE)) > 0; + + numbers[0] = latest.getMajor(); + numbers[1] = latest.getMinor() + (newer ? 1 : -1); + numbers[2] = 0; + } catch (Exception e) { + throw new IllegalStateException("Cannot parse " + section[0], e); + } + } this.major = numbers[0]; this.minor = numbers[1]; this.build = numbers[2]; - this.development = section.length > 1 ? section[1] : null; + this.development = section.length > 1 ? section[1] : (snapshot != null ? "snapshot" : null); + this.snapshot = snapshot; } /** @@ -88,6 +130,7 @@ public class MinecraftVersion implements Comparable { this.minor = minor; this.build = build; this.development = development; + this.snapshot = null; } private int[] parseVersion(String version) { @@ -136,6 +179,22 @@ public class MinecraftVersion implements Comparable { return development; } + /** + * Retrieve the snapshot version, or NULL if this is a release. + * @return The snapshot version. + */ + public SnapshotVersion getSnapshot() { + return snapshot; + } + + /** + * Determine if this version is a snapshot. + * @return The snapshot version. + */ + public boolean isSnapshot() { + return snapshot != null; + } + /** * Retrieve the version String (major.minor.build) only. * @return A normal version string. @@ -144,7 +203,8 @@ public class MinecraftVersion implements Comparable { if (getDevelopmentStage() == null) return String.format("%s.%s.%s", getMajor(), getMinor(), getBuild()); else - return String.format("%s.%s.%s-%s", getMajor(), getMinor(), getBuild(), getDevelopmentStage()); + return String.format("%s.%s.%s-%s%s", getMajor(), getMinor(), getBuild(), + getDevelopmentStage(), isSnapshot() ? snapshot : ""); } @Override @@ -158,6 +218,7 @@ public class MinecraftVersion implements Comparable { compare(getBuild(), o.getBuild()). // No development String means it's a release compare(getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast()). + compare(getSnapshot(), o.getSnapshot()). result(); } @@ -207,4 +268,13 @@ public class MinecraftVersion implements Comparable { throw new IllegalStateException("Cannot parse version String '" + text + "'"); } } + + /** + * Parse the given server version into a Minecraft version. + * @param serverVersion - the server version. + * @return The resulting Minecraft version. + */ + public static MinecraftVersion fromServerVersion(String serverVersion) { + return new MinecraftVersion(extractVersion(serverVersion)); + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/SnapshotVersion.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/SnapshotVersion.java new file mode 100644 index 00000000..8c1708e8 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/SnapshotVersion.java @@ -0,0 +1,108 @@ +package com.comphenix.protocol.utility; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Objects; +import com.google.common.collect.ComparisonChain; + +/** + * Used to parse a snapshot version. + * @author Kristian + */ +public class SnapshotVersion implements Comparable { + private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("(\\d{2}w\\d{2})([a-z])"); + + private final String rawString; + private final Date snapshotDate; + private final int snapshotWeekVersion; + + public SnapshotVersion(String version) { + Matcher matcher = SNAPSHOT_PATTERN.matcher(version.trim()); + + if (matcher.matches()) { + try { + this.snapshotDate = getDateFormat().parse(matcher.group(1)); + this.snapshotWeekVersion = (int)matcher.group(2).charAt(0) - (int)'a'; + this.rawString = version; + } catch (ParseException e) { + throw new IllegalArgumentException("Date implied by snapshot version is invalid.", e); + } + } else { + throw new IllegalArgumentException("Cannot parse " + version + " as a snapshot version."); + } + } + + /** + * Retrieve the snapshot date parser. + *

+ * We have to create a new instance of SimpleDateFormat every time as it is not thread safe. + * @return The date formatter. + */ + private static SimpleDateFormat getDateFormat() { + SimpleDateFormat format = new SimpleDateFormat("yy'w'ww", Locale.US); + format.setLenient(false); + return format; + } + + /** + * Retrieve the snapshot version within a week, starting at zero. + * @return The weekly version + */ + public int getSnapshotWeekVersion() { + return snapshotWeekVersion; + } + + /** + * Retrieve the week this snapshot was released. + * @return The week. + */ + public Date getSnapshotDate() { + return snapshotDate; + } + + /** + * Retrieve the raw snapshot string (yy'w'ww[a-z]). + * @return The snapshot string. + */ + public String getSnapshotString() { + return rawString; + } + + @Override + public int compareTo(SnapshotVersion o) { + if (o == null) + return 1; + + return ComparisonChain.start(). + compare(snapshotDate, o.getSnapshotDate()). + compare(snapshotWeekVersion, o.getSnapshotWeekVersion()). + result(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof SnapshotVersion) { + SnapshotVersion other = (SnapshotVersion) obj; + return Objects.equal(snapshotDate, other.getSnapshotDate()) && + snapshotWeekVersion == other.getSnapshotWeekVersion(); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(snapshotDate, snapshotWeekVersion); + } + + @Override + public String toString() { + return rawString; + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java index 2d127c23..aa3a3691 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/MinecraftVersionTest.java @@ -22,9 +22,9 @@ import static org.junit.Assert.*; import org.junit.Test; import com.comphenix.protocol.utility.MinecraftVersion; +import com.comphenix.protocol.utility.SnapshotVersion; public class MinecraftVersionTest { - @Test public void testComparision() { MinecraftVersion within = new MinecraftVersion(1, 2, 5); @@ -38,6 +38,12 @@ public class MinecraftVersionTest { assertFalse(outside.compareTo(within) < 0 && outside.compareTo(highest) < 0); } + @Test + public void testSnapshotVersion() { + MinecraftVersion version = MinecraftVersion.fromServerVersion("git-Spigot-1119 (MC: 13w39b)"); + assertEquals(version.getSnapshot(), new SnapshotVersion("13w39b")); + } + public void testParsing() { assertEquals(MinecraftVersion.extractVersion("CraftBukkit R3.0 (MC: 1.4.3)"), "1.4.3"); assertEquals(MinecraftVersion.extractVersion("CraftBukkit Test Beta 1 (MC: 1.10.01 )"), "1.10.01"); diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/utility/SnapshotVersionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/SnapshotVersionTest.java new file mode 100644 index 00000000..534a5d2d --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/SnapshotVersionTest.java @@ -0,0 +1,41 @@ +package com.comphenix.protocol.utility; + +import static org.junit.Assert.*; + +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import org.junit.Test; + +public class SnapshotVersionTest { + @Test + public void testDates() { + SnapshotVersion a = new SnapshotVersion("12w50b"); + SnapshotVersion b = new SnapshotVersion("13w05a"); + + expect(a.getSnapshotDate(), 12, 50); + expect(b.getSnapshotDate(), 13, 5); + + // Test equality + assertEquals(a, new SnapshotVersion("12w50b")); + } + + @Test(expected=IllegalArgumentException.class) + public void testDateParsingProblem() { + // This date is not valid + new SnapshotVersion("12w80a"); + } + + @Test(expected=IllegalArgumentException.class) + public void testMissingWeekVersion() { + new SnapshotVersion("13w05"); + } + + private void expect(Date date, int year, int week) { + Calendar calendar = Calendar.getInstance(Locale.US); + calendar.setTime(date); + assertEquals(year, calendar.get(Calendar.YEAR) % 100); + assertEquals(week, calendar.get(Calendar.WEEK_OF_YEAR)); + } +}