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.
This commit is contained in:
Kristian S. Stangeland 2013-09-28 16:25:30 +02:00
parent 2001c15132
commit 1b1f36c5d6
7 changed files with 579 additions and 349 deletions

View File

@ -1,32 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<classpath> <classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"> <classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes> <attributes>
<attribute name="optional" value="true"/> <attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"> <classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes> <attributes>
<attribute name="optional" value="true"/> <attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"> <classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="output" path="target/classes"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View File

@ -81,12 +81,17 @@ public class ProtocolLibrary extends JavaPlugin {
/** /**
* The minimum version ProtocolLib has been tested with. * 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, * 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. * 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."); logger.warning("Version " + current + " has not yet been tested! Proceed with caution.");
} }
return current; return current;
} catch (Exception e) { } catch (Exception e) {
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e)); reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e));
} }
@ -384,7 +389,7 @@ public class ProtocolLibrary extends JavaPlugin {
// Unknown version // Unknown version
return null; return null;
} }
private void checkConflictingVersions() { private void checkConflictingVersions() {
Pattern ourPlugin = Pattern.compile("ProtocolLib-(.*)\\.jar"); Pattern ourPlugin = Pattern.compile("ProtocolLib-(.*)\\.jar");
MinecraftVersion currentVersion = new MinecraftVersion(this.getDescription().getVersion()); MinecraftVersion currentVersion = new MinecraftVersion(this.getDescription().getVersion());

View File

@ -1,305 +1,305 @@
/* /*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland * Copyright (C) 2012 Kristian S. Stangeland
* *
* This program is free software; you can redistribute it and/or modify it under the terms of the * 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 * GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version. * 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; * 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. * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. * 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; * 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 * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA * 02111-1307 USA
*/ */
package com.comphenix.protocol.async; package com.comphenix.protocol.async;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.PriorityBlockingQueue;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.PlayerLoggedOutException; import com.comphenix.protocol.injector.PlayerLoggedOutException;
import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FieldAccessException;
/** /**
* Represents packets ready to be transmitted to a client. * Represents packets ready to be transmitted to a client.
* @author Kristian * @author Kristian
*/ */
abstract class PacketSendingQueue { abstract class PacketSendingQueue {
public static final int INITIAL_CAPACITY = 10; public static final int INITIAL_CAPACITY = 10;
private PriorityBlockingQueue<PacketEventHolder> sendingQueue; private PriorityBlockingQueue<PacketEventHolder> sendingQueue;
// Asynchronous packet sending // Asynchronous packet sending
private Executor asynchronousSender; private Executor asynchronousSender;
// Whether or not packet transmission must occur on a specific thread // Whether or not packet transmission must occur on a specific thread
private final boolean notThreadSafe; private final boolean notThreadSafe;
// Whether or not we've run the cleanup procedure // Whether or not we've run the cleanup procedure
private boolean cleanedUp = false; private boolean cleanedUp = false;
/** /**
* Create a packet sending queue. * Create a packet sending queue.
* @param notThreadSafe - whether or not to synchronize with the main thread or a background thread. * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread.
*/ */
public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) { public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) {
this.sendingQueue = new PriorityBlockingQueue<PacketEventHolder>(INITIAL_CAPACITY); this.sendingQueue = new PriorityBlockingQueue<PacketEventHolder>(INITIAL_CAPACITY);
this.notThreadSafe = notThreadSafe; this.notThreadSafe = notThreadSafe;
this.asynchronousSender = asynchronousSender; this.asynchronousSender = asynchronousSender;
} }
/** /**
* Number of packet events in the queue. * Number of packet events in the queue.
* @return The number of packet events in the queue. * @return The number of packet events in the queue.
*/ */
public int size() { public int size() {
return sendingQueue.size(); return sendingQueue.size();
} }
/** /**
* Enqueue a packet for sending. * Enqueue a packet for sending.
* @param packet - packet to queue. * @param packet - packet to queue.
*/ */
public void enqueue(PacketEvent packet) { public void enqueue(PacketEvent packet) {
sendingQueue.add(new PacketEventHolder(packet)); sendingQueue.add(new PacketEventHolder(packet));
} }
/** /**
* Invoked when one of the packets have finished processing. * Invoked when one of the packets have finished processing.
* @param packetUpdated - the packet that has now been updated. * @param packetUpdated - the packet that has now been updated.
* @param onMainThread - whether or not this is occuring on the main thread. * @param onMainThread - whether or not this is occuring on the main thread.
*/ */
public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) {
AsyncMarker marker = packetUpdated.getAsyncMarker(); AsyncMarker marker = packetUpdated.getAsyncMarker();
// Should we reorder the event? // Should we reorder the event?
if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
// "Cancel" the original event // "Cancel" the original event
packetUpdated.setReadOnly(false); packetUpdated.setReadOnly(false);
packetUpdated.setCancelled(true); packetUpdated.setCancelled(true);
// Enqueue the copy with the new sending index // Enqueue the copy with the new sending index
enqueue(copy); enqueue(copy);
} }
// Mark this packet as finished // Mark this packet as finished
marker.setProcessed(true); marker.setProcessed(true);
trySendPackets(onMainThread); trySendPackets(onMainThread);
} }
/*** /***
* Invoked when a list of packet IDs are no longer associated with any listeners. * Invoked when a list of packet IDs are no longer associated with any listeners.
* @param packetsRemoved - packets that no longer have any listeners. * @param packetsRemoved - packets that no longer have any listeners.
* @param onMainThread - whether or not this is occuring on the main thread. * @param onMainThread - whether or not this is occuring on the main thread.
*/ */
public synchronized void signalPacketUpdate(List<Integer> packetsRemoved, boolean onMainThread) { public synchronized void signalPacketUpdate(List<Integer> packetsRemoved, boolean onMainThread) {
Set<Integer> lookup = new HashSet<Integer>(packetsRemoved); Set<Integer> lookup = new HashSet<Integer>(packetsRemoved);
// Note that this is O(n), so it might be expensive // Note that this is O(n), so it might be expensive
for (PacketEventHolder holder : sendingQueue) { for (PacketEventHolder holder : sendingQueue) {
PacketEvent event = holder.getEvent(); PacketEvent event = holder.getEvent();
if (lookup.contains(event.getPacketID())) { if (lookup.contains(event.getPacketID())) {
event.getAsyncMarker().setProcessed(true); event.getAsyncMarker().setProcessed(true);
} }
} }
// This is likely to have changed the situation a bit // This is likely to have changed the situation a bit
trySendPackets(onMainThread); trySendPackets(onMainThread);
} }
/** /**
* Attempt to send any remaining packets. * Attempt to send any remaining packets.
* @param onMainThread - whether or not this is occuring on the main thread. * @param onMainThread - whether or not this is occuring on the main thread.
*/ */
public void trySendPackets(boolean onMainThread) { public void trySendPackets(boolean onMainThread) {
// Whether or not to continue sending packets // Whether or not to continue sending packets
boolean sending = true; boolean sending = true;
// Transmit as many packets as we can // Transmit as many packets as we can
while (sending) { while (sending) {
PacketEventHolder holder = sendingQueue.poll(); PacketEventHolder holder = sendingQueue.poll();
if (holder != null) { if (holder != null) {
sending = processPacketHolder(onMainThread, holder); sending = processPacketHolder(onMainThread, holder);
if (!sending) { if (!sending) {
// Add it back again // Add it back again
sendingQueue.add(holder); sendingQueue.add(holder);
} }
} else { } else {
// No more packets to send // No more packets to send
sending = false; sending = false;
} }
} }
} }
/** /**
* Invoked when a packet might be ready for transmission. * Invoked when a packet might be ready for transmission.
* @param onMainThread - TRUE if we're on the main thread, FALSE otherwise. * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise.
* @param holder - packet container. * @param holder - packet container.
* @return TRUE to continue sending packets, FALSE otherwise. * @return TRUE to continue sending packets, FALSE otherwise.
*/ */
private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) { private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) {
PacketEvent current = holder.getEvent(); PacketEvent current = holder.getEvent();
AsyncMarker marker = current.getAsyncMarker(); AsyncMarker marker = current.getAsyncMarker();
boolean hasExpired = marker.hasExpired(); boolean hasExpired = marker.hasExpired();
// Guard in cause the queue is closed // Guard in cause the queue is closed
if (cleanedUp) { if (cleanedUp) {
return true; return true;
} }
// End condition? // End condition?
if (marker.isProcessed() || hasExpired) { if (marker.isProcessed() || hasExpired) {
if (hasExpired) { if (hasExpired) {
// Notify timeout listeners // Notify timeout listeners
onPacketTimeout(current); onPacketTimeout(current);
// Recompute // Recompute
marker = current.getAsyncMarker(); marker = current.getAsyncMarker();
hasExpired = marker.hasExpired(); hasExpired = marker.hasExpired();
// Could happen due to the timeout listeners // Could happen due to the timeout listeners
if (!marker.isProcessed() && !hasExpired) { if (!marker.isProcessed() && !hasExpired) {
return false; return false;
} }
} }
// Is it okay to send the packet? // Is it okay to send the packet?
if (!current.isCancelled() && !hasExpired) { if (!current.isCancelled() && !hasExpired) {
// Make sure we're on the main thread // Make sure we're on the main thread
if (notThreadSafe) { if (notThreadSafe) {
try { try {
boolean wantAsync = marker.isMinecraftAsync(current); boolean wantAsync = marker.isMinecraftAsync(current);
boolean wantSync = !wantAsync; boolean wantSync = !wantAsync;
// Wait for the next main thread heartbeat if we haven't fulfilled our promise // Wait for the next main thread heartbeat if we haven't fulfilled our promise
if (!onMainThread && wantSync) { if (!onMainThread && wantSync) {
return false; return false;
} }
// Let's give it what it wants // Let's give it what it wants
if (onMainThread && wantAsync) { if (onMainThread && wantAsync) {
asynchronousSender.execute(new Runnable() { asynchronousSender.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
// We know this isn't on the main thread // We know this isn't on the main thread
processPacketHolder(false, holder); processPacketHolder(false, holder);
} }
}); });
// Scheduler will do the rest // Scheduler will do the rest
return true; return true;
} }
} catch (FieldAccessException e) { } catch (FieldAccessException e) {
e.printStackTrace(); e.printStackTrace();
// Just drop the packet // Just drop the packet
return true; return true;
} }
} }
// Silently skip players that have logged out // Silently skip players that have logged out
if (isOnline(current.getPlayer())) { if (isOnline(current.getPlayer())) {
sendPacket(current); sendPacket(current);
} }
} }
// Drop the packet // Drop the packet
return true; return true;
} }
// Add it back and stop sending // Add it back and stop sending
return false; return false;
} }
/** /**
* Invoked when a packet has timed out. * Invoked when a packet has timed out.
* @param event - the timed out packet. * @param event - the timed out packet.
*/ */
protected abstract void onPacketTimeout(PacketEvent event); protected abstract void onPacketTimeout(PacketEvent event);
private boolean isOnline(Player player) { private boolean isOnline(Player player) {
return player != null && player.isOnline(); return player != null && player.isOnline();
} }
/** /**
* Send every packet, regardless of the processing state. * Send every packet, regardless of the processing state.
*/ */
private void forceSend() { private void forceSend() {
while (true) { while (true) {
PacketEventHolder holder = sendingQueue.poll(); PacketEventHolder holder = sendingQueue.poll();
if (holder != null) { if (holder != null) {
sendPacket(holder.getEvent()); sendPacket(holder.getEvent());
} else { } else {
break; break;
} }
} }
} }
/** /**
* Whether or not the packet transmission must synchronize with the main thread. * Whether or not the packet transmission must synchronize with the main thread.
* @return TRUE if it must, FALSE otherwise. * @return TRUE if it must, FALSE otherwise.
*/ */
public boolean isSynchronizeMain() { public boolean isSynchronizeMain() {
return notThreadSafe; return notThreadSafe;
} }
/** /**
* Transmit a packet, if it hasn't already. * Transmit a packet, if it hasn't already.
* @param event - the packet to transmit. * @param event - the packet to transmit.
*/ */
private void sendPacket(PacketEvent event) { private void sendPacket(PacketEvent event) {
AsyncMarker marker = event.getAsyncMarker(); AsyncMarker marker = event.getAsyncMarker();
try { try {
// Don't send a packet twice // Don't send a packet twice
if (marker != null && !marker.isTransmitted()) { if (marker != null && !marker.isTransmitted()) {
marker.sendPacket(event); marker.sendPacket(event);
} }
} catch (PlayerLoggedOutException e) { } catch (PlayerLoggedOutException e) {
System.out.println(String.format( System.out.println(String.format(
"[ProtocolLib] Warning: Dropped packet index %s of ID %s", "[ProtocolLib] Warning: Dropped packet index %s of ID %s",
marker.getOriginalSendingIndex(), event.getPacketID() marker.getOriginalSendingIndex(), event.getPacketID()
)); ));
} catch (IOException e) { } catch (IOException e) {
// Just print the error // Just print the error
e.printStackTrace(); e.printStackTrace();
} }
} }
/** /**
* Automatically transmits every delayed packet. * Automatically transmits every delayed packet.
*/ */
public void cleanupAll() { public void cleanupAll() {
if (!cleanedUp) { if (!cleanedUp) {
// Note that the cleanup itself will always occur on the main thread // Note that the cleanup itself will always occur on the main thread
forceSend(); forceSend();
// And we're done // And we're done
cleanedUp = true; cleanedUp = true;
} }
} }
} }

View File

@ -17,10 +17,13 @@
package com.comphenix.protocol.utility; package com.comphenix.protocol.utility;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bukkit.Server; import org.bukkit.Server;
import com.comphenix.protocol.ProtocolLibrary;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.collect.ComparisonChain; import com.google.common.collect.ComparisonChain;
@ -35,15 +38,18 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
/** /**
* Regular expression used to parse version strings. * 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 major;
private final int minor; private final int minor;
private final int build; private final int build;
// The development stage // The development stage
private final String development; private final String development;
// Snapshot?
private final SnapshotVersion snapshot;
/** /**
* Determine the current Minecraft version. * Determine the current Minecraft version.
* @param server - the Bukkit server that will be used to examine the MC version. * @param server - the Bukkit server that will be used to examine the MC version.
@ -53,17 +59,53 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
} }
/** /**
* 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. * @param versionOnly - the version in text form.
*/ */
public MinecraftVersion(String versionOnly) { 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("-"); 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.major = numbers[0];
this.minor = numbers[1]; this.minor = numbers[1];
this.build = numbers[2]; 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<MinecraftVersion> {
this.minor = minor; this.minor = minor;
this.build = build; this.build = build;
this.development = development; this.development = development;
this.snapshot = null;
} }
private int[] parseVersion(String version) { private int[] parseVersion(String version) {
@ -136,6 +179,22 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
return development; 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. * Retrieve the version String (major.minor.build) only.
* @return A normal version string. * @return A normal version string.
@ -144,7 +203,8 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
if (getDevelopmentStage() == null) if (getDevelopmentStage() == null)
return String.format("%s.%s.%s", getMajor(), getMinor(), getBuild()); return String.format("%s.%s.%s", getMajor(), getMinor(), getBuild());
else 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 @Override
@ -158,6 +218,7 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
compare(getBuild(), o.getBuild()). compare(getBuild(), o.getBuild()).
// No development String means it's a release // No development String means it's a release
compare(getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast()). compare(getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast()).
compare(getSnapshot(), o.getSnapshot()).
result(); result();
} }
@ -207,4 +268,13 @@ public class MinecraftVersion implements Comparable<MinecraftVersion> {
throw new IllegalStateException("Cannot parse version String '" + text + "'"); 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));
}
} }

View File

@ -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<SnapshotVersion> {
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.
* <p>
* 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;
}
}

View File

@ -22,9 +22,9 @@ import static org.junit.Assert.*;
import org.junit.Test; import org.junit.Test;
import com.comphenix.protocol.utility.MinecraftVersion; import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.utility.SnapshotVersion;
public class MinecraftVersionTest { public class MinecraftVersionTest {
@Test @Test
public void testComparision() { public void testComparision() {
MinecraftVersion within = new MinecraftVersion(1, 2, 5); MinecraftVersion within = new MinecraftVersion(1, 2, 5);
@ -38,6 +38,12 @@ public class MinecraftVersionTest {
assertFalse(outside.compareTo(within) < 0 && outside.compareTo(highest) < 0); 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() { public void testParsing() {
assertEquals(MinecraftVersion.extractVersion("CraftBukkit R3.0 (MC: 1.4.3)"), "1.4.3"); 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"); assertEquals(MinecraftVersion.extractVersion("CraftBukkit Test Beta 1 (MC: 1.10.01 )"), "1.10.01");

View File

@ -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));
}
}