Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Michael Rausch
2017-07-26 20:28:21 +12:00
10 changed files with 135 additions and 68 deletions
@@ -59,6 +59,7 @@ public class ClientPacketParser {
*/ */
public ClientPacketParser() { public ClientPacketParser() {
} }
/** /**
* Looks at the type of the packet then sends it to the appropriate parser to extract the * Looks at the type of the packet then sends it to the appropriate parser to extract the
* specific data associated with that packet type * specific data associated with that packet type
@@ -108,7 +109,7 @@ public class ClientPacketParser {
} }
} catch (NullPointerException e) { } catch (NullPointerException e) {
System.out.println("Error parsing packet"); System.out.println("Error parsing packet");
e.printStackTrace(); // e.printStackTrace();
} }
} }
@@ -185,7 +186,6 @@ public class ClientPacketParser {
int noBoats = payload[22]; int noBoats = payload[22];
int raceType = payload[23]; int raceType = payload[23];
clientStateBoats = ClientState.getBoats();
for (int i = 0; i < noBoats; i++) { for (int i = 0; i < noBoats; i++) {
long boatStatusSourceID = bytesToLong( long boatStatusSourceID = bytesToLong(
Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20))); Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20)));
@@ -206,7 +206,9 @@ public class ClientPacketParser {
boat.setEstimateTimeAtNextMark(estTimeAtNextMark); boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
boat.setEstimateTimeAtFinish(estTimeAtFinish); boat.setEstimateTimeAtFinish(estTimeAtFinish);
Yacht clientBoat = clientStateBoats.get((int) boatStatusSourceID); // Update Client State boats when receive race status packet.
// Potentially could replace boats in ClientPacketParser.
Yacht clientBoat = ClientState.getBoats().get((int) boatStatusSourceID);
clientBoat.setBoatStatus((boatStatus)); clientBoat.setBoatStatus((boatStatus));
setBoatLegPosition(clientBoat, boatLegNumber); setBoatLegPosition(clientBoat, boatLegNumber);
clientBoat.setPenaltiesAwarded(boatPenaltyAwarded); clientBoat.setPenaltiesAwarded(boatPenaltyAwarded);
@@ -215,9 +217,12 @@ public class ClientPacketParser {
clientBoat.setEstimateTimeAtFinish(estTimeAtFinish); clientBoat.setEstimateTimeAtFinish(estTimeAtFinish);
} }
// 3 is race started // 3 is race started.
// ClientState race started flag will be set to true if race started, else set false.
if (raceStatus == 3) { if (raceStatus == 3) {
ClientState.setRaceStarted(true); ClientState.setRaceStarted(true);
} else {
ClientState.setRaceStarted(false);
} }
} }
@@ -289,8 +294,10 @@ public class ClientPacketParser {
xmlObject.constructXML(doc, messageType); xmlObject.constructXML(doc, messageType);
if (messageType == 7) { //7 is the boat XML if (messageType == 7) { //7 is the boat XML
boats = xmlObject.getBoatXML().getCompetingBoats(); boats = xmlObject.getBoatXML().getCompetingBoats();
// Set/Update the ClientState boats after receiving new boat xml.
// Flag boatsUpdated in ClientState to true.
ClientState.setBoats(xmlObject.getBoatXML().getCompetingBoats()); ClientState.setBoats(xmlObject.getBoatXML().getCompetingBoats());
ClientState.setDirtyState(true); ClientState.setBoatsUpdated(true);
} }
if (messageType == 6) { //6 is race info xml if (messageType == 6) { //6 is race info xml
newRaceXmlReceived = true; newRaceXmlReceived = true;
@@ -1,14 +1,12 @@
package seng302.client; package seng302.client;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import seng302.models.Yacht; import seng302.models.Yacht;
/** /**
* Used by the client to store static variables to be used in game. * Used by the client to store static variables, which other threads and classes
* observer so that they can update their status accordingly.
*/ */
public class ClientState { public class ClientState {
@@ -17,7 +15,7 @@ public class ClientState {
private static Boolean raceStarted = false; private static Boolean raceStarted = false;
private static Boolean connectedToHost = false; private static Boolean connectedToHost = false;
private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>(); private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>();
private static Boolean dirtyState = true; private static Boolean boatsUpdated = true;
private static String clientSourceId = ""; private static String clientSourceId = "";
public static String getHostIp() { public static String getHostIp() {
@@ -56,12 +54,12 @@ public class ClientState {
return boats; return boats;
} }
public static Boolean isDirtyState() { public static Boolean isBoatsUpdated() {
return dirtyState; return boatsUpdated;
} }
public static void setDirtyState(Boolean dirtyState) { public static void setBoatsUpdated(Boolean boatsUpdated) {
ClientState.dirtyState = dirtyState; ClientState.boatsUpdated = boatsUpdated;
} }
public static String getClientSourceId() { public static String getClientSourceId() {
@@ -12,6 +12,12 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable
public ClientStateQueryingRunnable() {} public ClientStateQueryingRunnable() {}
/**
* Notifies observers(the lobby controller) that "game started" if ClientState
* raceStarted flag is true and terminates itself. Also, it notifies observers
* to add/remove players if ClientState boatsUpdated flag is true, then resets
* the flag to false;
*/
@Override @Override
public void run() { public void run() {
while(!terminate) { while(!terminate) {
@@ -29,14 +35,19 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable
terminate(); terminate();
} }
if (ClientState.isDirtyState()) { if (ClientState.isBoatsUpdated()) {
setChanged(); setChanged();
notifyObservers("update players"); notifyObservers("update players");
ClientState.setDirtyState(false); ClientState.setBoatsUpdated(false);
} }
} }
} }
/**
* Used to terminate the thread.
*
* Currently called by the main while loop when game started is detected.
*/
public void terminate() { public void terminate() {
terminate = true; terminate = true;
} }
@@ -15,7 +15,8 @@ import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.Message; import seng302.server.messages.Message;
/** /**
* Created by kre39 on 13/07/17. * A class describing a single connection to a Server for the purposes of sending and receiving on
* its own thread.
*/ */
public class ClientToServerThread implements Runnable { public class ClientToServerThread implements Runnable {
@@ -32,6 +33,17 @@ public class ClientToServerThread implements Runnable {
private Boolean updateClient = true; private Boolean updateClient = true;
private ByteArrayOutputStream crcBuffer; private ByteArrayOutputStream crcBuffer;
/**
* Constructor for ClientToServerThread which takes in ipAddress and portNumber and attempts to
* connect to the specified ipAddress and port.
*
* Upon successful socket connection, threeWayHandshake will be preformed and the instance will
* be put on a thread and run immediately.
*
* @param ipAddress a string of ip address to be connected to
* @param portNumber an integer port number
* @throws Exception SocketConnection if fail to connect to ip address and port number combination
*/
public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception{ public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception{
socket = new Socket(ipAddress, portNumber); socket = new Socket(ipAddress, portNumber);
is = socket.getInputStream(); is = socket.getInputStream();
@@ -40,7 +52,7 @@ public class ClientToServerThread implements Runnable {
Integer allocatedID = threeWayHandshake(); Integer allocatedID = threeWayHandshake();
if (allocatedID != null) { if (allocatedID != null) {
ourID = allocatedID; ourID = allocatedID;
clientLog("Successful handshake. Allocated ID: " + ourID, 1); clientLog("Successful handshake. Allocated ID: " + ourID, 0);
ClientState.setClientSourceId(String.valueOf(ourID)); ClientState.setClientSourceId(String.valueOf(ourID));
} else { } else {
clientLog("Unsuccessful handshake", 1); clientLog("Unsuccessful handshake", 1);
@@ -50,31 +62,31 @@ public class ClientToServerThread implements Runnable {
thread = new Thread(this); thread = new Thread(this);
thread.start(); thread.start();
} }
/**
* Prints out log messages and the time happened.
* Only perform task if log level is below LOG_LEVEL variable.
*
* @param message a string of message to be printed out
* @param logLevel an int for log level
*/
static void clientLog(String message, int logLevel){ static void clientLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){ if(logLevel <= LOG_LEVEL){
System.out.println("[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message); System.out.println("[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
} }
} }
/**
* Perform the thread loop. It exits the loop if ClientState connected to host
* variable is false.
*/
public void run() { public void run() {
int sync1; int sync1;
int sync2; int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop // TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while(ClientState.isConnectedToHost()) { while(ClientState.isConnectedToHost()) {
try { try {
//Perform a write if it is time to as delegated by the MainServerThread
if (updateClient) {
// TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream
// try {
// GameState.outputState(os);
// } catch (IOException e) {
// System.out.println("IO error in server thread upon writing to output stream");
// }
updateClient = false;
}
crcBuffer = new ByteArrayOutputStream(); crcBuffer = new ByteArrayOutputStream();
sync1 = readByte(); sync1 = readByte();
sync2 = readByte(); sync2 = readByte();
@@ -93,7 +105,6 @@ public class ClientToServerThread implements Runnable {
if (computedCrc == packetCrc) { if (computedCrc == packetCrc) {
ClientPacketParser ClientPacketParser
.parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload)); .parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload));
// TODO: 17/07/17 wmu16 - Fix this or maybe we dont need to go through the main server at all!?!?
// packetBufferDelegate.addToBuffer(new StreamPacket(type, payloadLength, timeStamp, payload)); // packetBufferDelegate.addToBuffer(new StreamPacket(type, payloadLength, timeStamp, payload));
} else { } else {
clientLog("Packet has been dropped", 1); clientLog("Packet has been dropped", 1);
@@ -101,8 +112,7 @@ public class ClientToServerThread implements Runnable {
} }
} catch (Exception e) { } catch (Exception e) {
closeSocket(); closeSocket();
System.err.println("SERVER DISCONNECTED"); clientLog("Disconnected from server", 1);
// e.printStackTrace();
return; return;
} }
} }
@@ -112,7 +122,7 @@ public class ClientToServerThread implements Runnable {
/** /**
* Listens for an allocated sourceID and returns it to the server if recieved * Listens for an allocated sourceID and returns it to the server
* @return the sourceID allocated to us by the server * @return the sourceID allocated to us by the server
*/ */
private Integer threeWayHandshake() { private Integer threeWayHandshake() {
@@ -121,14 +131,15 @@ public class ClientToServerThread implements Runnable {
try { try {
ourSourceID = is.read(); ourSourceID = is.read();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); clientLog("Three way handshake failed", 1);
} }
if (ourSourceID != null) { if (ourSourceID != null) {
try { try {
os.write(ourSourceID); os.write(ourSourceID);
return ourSourceID; return ourSourceID;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); clientLog("Three way handshake failed", 1);
return null; return null;
} }
} }
@@ -144,8 +155,7 @@ public class ClientToServerThread implements Runnable {
try { try {
os.write(boatActionMessage.getBuffer()); os.write(boatActionMessage.getBuffer());
} catch (IOException e) { } catch (IOException e) {
clientLog("COULD NOT WRITE TO SERVER", 0); clientLog("Could not write to server", 1);
e.printStackTrace();
} }
} }
@@ -154,7 +164,7 @@ public class ClientToServerThread implements Runnable {
try { try {
socket.close(); socket.close();
} catch (IOException e) { } catch (IOException e) {
clientLog("Failed to close the socket", 0); clientLog("Failed to close the socket", 1);
} }
} }
@@ -165,7 +175,7 @@ public class ClientToServerThread implements Runnable {
currentByte = is.read(); currentByte = is.read();
crcBuffer.write(currentByte); crcBuffer.write(currentByte);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); clientLog("Read byte failed", 1);
} }
if (currentByte == -1){ if (currentByte == -1){
throw new Exception(); throw new Exception();
@@ -75,7 +75,7 @@ public class CanvasController {
private List<MarkGroup> markGroups = new ArrayList<>(); private List<MarkGroup> markGroups = new ArrayList<>();
private List<BoatGroup> boatGroups = new ArrayList<>(); private List<BoatGroup> boatGroups = new ArrayList<>();
private Text FPSdisplay = new Text(); private Text FPSDisplay = new Text();
private Polygon raceBorder = new Polygon(); private Polygon raceBorder = new Polygon();
//FRAME RATE //FRAME RATE
@@ -120,10 +120,10 @@ public class CanvasController {
gc.setGlobalAlpha(0.5); gc.setGlobalAlpha(0.5);
fitMarksToCanvas(); fitMarksToCanvas();
drawGoogleMap(); drawGoogleMap();
FPSdisplay.setLayoutX(5); FPSDisplay.setLayoutX(5);
FPSdisplay.setLayoutY(20); FPSDisplay.setLayoutY(20);
FPSdisplay.setStrokeWidth(2); FPSDisplay.setStrokeWidth(2);
group.getChildren().add(FPSdisplay); group.getChildren().add(FPSDisplay);
group.getChildren().add(raceBorder); group.getChildren().add(raceBorder);
initializeMarks(); initializeMarks();
initializeBoats(); initializeBoats();
@@ -407,10 +407,10 @@ public class CanvasController {
private void drawFps(int fps){ private void drawFps(int fps){
if (raceViewController.isDisplayFps()){ if (raceViewController.isDisplayFps()){
FPSdisplay.setVisible(true); FPSDisplay.setVisible(true);
FPSdisplay.setText(String.format("%d FPS", fps)); FPSDisplay.setText(String.format("%d FPS", fps));
} else { } else {
FPSdisplay.setVisible(false); FPSDisplay.setVisible(false);
} }
} }
@@ -112,6 +112,7 @@ public class LobbyController implements Initializable, Observer{
readyButton.setDisable(true); readyButton.setDisable(true);
} }
// put all javafx objects in lists, so we can iterate though conveniently
imageViews = new ArrayList<>(); imageViews = new ArrayList<>();
Collections.addAll(imageViews, firstImageView, secondImageView, thirdImageView, fourthImageView, Collections.addAll(imageViews, firstImageView, secondImageView, thirdImageView, fourthImageView,
fifthImageView, sixthImageView, seventhImageView, eighthImageView); fifthImageView, sixthImageView, seventhImageView, eighthImageView);
@@ -134,6 +135,13 @@ public class LobbyController implements Initializable, Observer{
clientStateQueryingThread.start(); clientStateQueryingThread.start();
} }
/**
* Observers "ClientStateQueryingRunnable".
* When the clients state has been marked to "race start", the querying thread
* will notify this lobby to change the view
* @param o
* @param arg
*/
@Override @Override
public void update(Observable o, Object arg) { public void update(Observable o, Object arg) {
Platform.runLater(new Runnable() { Platform.runLater(new Runnable() {
@@ -149,6 +157,9 @@ public class LobbyController implements Initializable, Observer{
}); });
} }
/**
* Reset all ListViews and ImageViews according to the current competitors
*/
private void initialiseListView() { private void initialiseListView() {
listViews.forEach(listView -> listView.getItems().clear()); listViews.forEach(listView -> listView.getItems().clear());
imageViews.forEach(gif -> gif.setVisible(false)); imageViews.forEach(gif -> gif.setVisible(false));
@@ -162,6 +173,9 @@ public class LobbyController implements Initializable, Observer{
} }
} }
/**
* Loads preset images into imageViews
*/
private void initialiseImageView() { private void initialiseImageView() {
for (int i = 0; i < MAX_NUM_PLAYERS; i++) { for (int i = 0; i < MAX_NUM_PLAYERS; i++) {
imageViews.get(i).setImage(new Image(getClass().getResourceAsStream("/pics/sail.png"))); imageViews.get(i).setImage(new Image(getClass().getResourceAsStream("/pics/sail.png")));
@@ -195,14 +209,11 @@ public class LobbyController implements Initializable, Observer{
@FXML @FXML
public void readyButtonPressed() { public void readyButtonPressed() {
// setContentPane("/views/RaceView.fxml");
GameState.setCurrentStage(GameStages.RACING); GameState.setCurrentStage(GameStages.RACING);
mainServerThread.startGame(); mainServerThread.startGame();
} }
private void switchToRaceView() { private void switchToRaceView() {
if (!switchedPane) { if (!switchedPane) {
switchedPane = true; switchedPane = true;
@@ -1,10 +1,11 @@
package seng302.gameServer; package seng302.gameServer;
import java.util.*; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import seng302.client.ClientPacketParser; import seng302.client.ClientPacketParser;
import seng302.models.Player; import seng302.models.Player;
import seng302.models.Yacht; import seng302.models.Yacht;
import seng302.server.messages.BoatActionType; import seng302.server.messages.BoatActionType;
@@ -30,7 +31,7 @@ public class GameState implements Runnable {
public GameState(String hostIpAddress) { public GameState(String hostIpAddress) {
windDirection = 170d; windDirection = 180d;
windSpeed = 10000d; windSpeed = 10000d;
yachts = new HashMap<>(); yachts = new HashMap<>();
players = new ArrayList<>(); players = new ArrayList<>();
@@ -116,7 +117,6 @@ public class GameState implements Runnable {
case VMG: case VMG:
playerYacht.turnToVMG(); playerYacht.turnToVMG();
// System.out.println("Snapping to VMG"); // System.out.println("Snapping to VMG");
// TODO: 22/07/17 wmu16 - Add in the vmg calculation code here
break; break;
case SAILS_IN: case SAILS_IN:
playerYacht.toggleSailIn(); playerYacht.toggleSailIn();
+1 -2
View File
@@ -4,8 +4,6 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
/** /**
@@ -182,4 +180,5 @@ public final class PolarTable {
return closestAngle; return closestAngle;
} }
} }
+40 -9
View File
@@ -4,6 +4,7 @@ import static seng302.utilities.GeoUtility.getGeoCoordinate;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.HashMap;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import seng302.client.ClientPacketParser; import seng302.client.ClientPacketParser;
import seng302.controllers.RaceViewController; import seng302.controllers.RaceViewController;
@@ -136,9 +137,9 @@ public class Yacht {
if (velocity > 0d) { if (velocity > 0d) {
if (maxBoatSpeed != 0d) { if (maxBoatSpeed != 0d) {
velocity -= maxBoatSpeed / 25; velocity -= maxBoatSpeed / 600;
} else { } else {
velocity -= velocity / 25; velocity -= velocity / 100;
} }
if (velocity < 0) { if (velocity < 0) {
velocity = 0d; velocity = 0d;
@@ -164,8 +165,7 @@ public class Yacht {
} }
public void tackGybe(Double windDirection) { public void tackGybe(Double windDirection) {
Double normalizedHeading = heading - GameState.windDirection; Double normalizedHeading = normalizeHeading();
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
adjustHeading(-2 * normalizedHeading); adjustHeading(-2 * normalizedHeading);
} }
@@ -174,8 +174,7 @@ public class Yacht {
} }
public void turnUpwind() { public void turnUpwind() {
Double normalizedHeading = heading - GameState.windDirection; Double normalizedHeading = normalizeHeading();
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
if (normalizedHeading == 0) { if (normalizedHeading == 0) {
if (lastHeading < 180) { if (lastHeading < 180) {
adjustHeading(-TURN_STEP); adjustHeading(-TURN_STEP);
@@ -196,8 +195,7 @@ public class Yacht {
} }
public void turnDownwind() { public void turnDownwind() {
Double normalizedHeading = heading - GameState.windDirection; Double normalizedHeading = normalizeHeading();
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
if (normalizedHeading == 0) { if (normalizedHeading == 0) {
if (lastHeading < 180) { if (lastHeading < 180) {
adjustHeading(TURN_STEP); adjustHeading(TURN_STEP);
@@ -218,10 +216,43 @@ public class Yacht {
} }
public void turnToVMG() { public void turnToVMG() {
// TODO: 25/07/17 wmu16 - Fix this so it grabs the optimal value from the optimal Polar Double normalizedHeading = normalizeHeading();
Double optimalHeading;
HashMap<Double, Double> optimalPolarMap;
if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind
optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
} else {
optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
}
// Take optimal heading and turn into correct
optimalHeading =
optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L);
turnTowardsHeading(optimalHeading);
} }
private void turnTowardsHeading(Double newHeading) {
System.out.println(newHeading);
if (heading < 90 && newHeading > 270) {
adjustHeading(-TURN_STEP);
} else {
if (heading < newHeading) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
}
}
private Double normalizeHeading() {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
return normalizedHeading;
}
public String getBoatType() { public String getBoatType() {
return boatType; return boatType;
+1 -1
View File
@@ -1,4 +1,4 @@
Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7 Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7
4,0,0,30,4,45,8,60,9,75,10,90,10,115,10,145,10,155,10,175,4 4,0,0,30,4,45,8,60,9,75,10,90,10,115,10,145,10,155,10,175,4
8,0,0,30,7,43,10,60,11,75,11,90,11,115,12,145,12,153,12,175,10 8,0,0,30,7,43,10,60,11,75,11,90,11,115,12,145,12,153,12,175,10
12,0,0,30,11,43,14.4,60,16,75,20,90,23,115,24,145,23,153,21.6,175,14 12,0,0,30,11,43,14.4,60,16,75,20,90,23,115,24,145,23,153,21.6,175,14
1 Tws Twa0 Bsp0 Twa1 Bsp1 UpTwa UpBsp Twa2 Bsp2 Twa3 Bsp3 Twa4 Bsp4 Twa5 Bsp5 Twa6 Bsp6 DnTwa DnBsp Twa7 Bsp7
2 4 0 0 30 4 45 8 60 9 75 10 90 10 115 10 145 10 155 10 175 4
3 8 0 0 30 7 43 10 60 11 75 11 90 11 115 12 145 12 153 12 175 10
4 12 0 0 30 11 43 14.4 60 16 75 20 90 23 115 24 145 23 153 21.6 175 14