Compare commits

...

5 Commits

Author SHA1 Message Date
mike 0228d6947a removed 'runnable' functionality of GameEngine class 2023-04-15 18:56:36 -04:00
mike a4eec6ecc6 michael changes 2023-04-15 18:23:30 -04:00
Brett 49169c3bdd working client 2023-04-15 16:06:33 -04:00
Brett 6a559bc5ab working on client side 2023-04-15 15:29:19 -04:00
Brett 3b6269958b move networking into it's own package, add messages 2023-04-15 13:19:40 -04:00
11 changed files with 323 additions and 151 deletions

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -1,12 +1,12 @@
package ca.cosc3p91.a4;
import ca.cosc3p91.a4.util.Client;
import ca.cosc3p91.a4.util.network.Client;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
Client gameClient = new Client(42069);
Client gameClient = new Client("localhost");
}
}

View File

@ -13,30 +13,24 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Random;
public class GameEngine implements Runnable {
public class GameEngine {
public static final double GOLD_FACTOR = 5;
public static final double IRON_FACTOR = 1;
public static final double WOOD_FACTOR = 0.1;
private Player player;
boolean running = true;
private float pillageFactor = 0.5f;
private int currentTime;
private final Random random = new Random(System.nanoTime());
public Map map;
public GameDisplay view;
public GameEngine() {
player = new Player();
map = generateInitialMap();
}
public void attackVillage(Map map) {
public void attackVillage(Map attacking, Map defending) {
// int defenseiveCounter = 1;
// int inhabCounter = 0;
// for (Building b : map.contains)
@ -53,31 +47,31 @@ public class GameEngine implements Runnable {
// this.map.getTownHall().addWood((int) (map.getTownHall().getCurrentWood() * pillageFactor));
// this.map.getTownHall().addIron((int) (map.getTownHall().getCurrentIron() * pillageFactor));
// this.map.getTownHall().addGold((int) (map.getTownHall().getCurrentGold() * pillageFactor));
ChallengeAdapter adapter = new ChallengeAdapter(this.map);
adapter.attack(map);
ChallengeAdapter adapter = new ChallengeAdapter(attacking);
adapter.attack(defending);
}
private Map generateInitialMap(){
public Map generateInitialMap(){
return new Map(new CasaDeNarino(1, VillageHallStages.villageStages[0]), 30);
}
public Map generateMap() {
public Map generateMap(Map map) {
Map initialMap = generateInitialMap();
CasaDeNarino hall = initialMap.getTownHall();
// generate a similar town hall
int levelChange = random.nextInt(2) - 1;
int nextLevel = this.map.getTownHall().getLevel() + levelChange;
int nextLevel = map.getTownHall().getLevel() + levelChange;
// only need to change if the new village level is higher than initial
if (nextLevel > 0)
hall.upgrade(VillageHallStages.villageStages[nextLevel]);
hall.addWood(this.map.getTownHall().getCurrentWood() + random.nextInt(500) - 150);
hall.addIron(this.map.getTownHall().getCurrentIron() + random.nextInt(500) - 150);
hall.addGold(this.map.getTownHall().getCurrentGold() + random.nextInt(500) - 150);
hall.addWood(map.getTownHall().getCurrentWood() + random.nextInt(500) - 150);
hall.addIron(map.getTownHall().getCurrentIron() + random.nextInt(500) - 150);
hall.addGold(map.getTownHall().getCurrentGold() + random.nextInt(500) - 150);
int buildingCount = this.map.contains.size();
int buildingCount = map.contains.size();
int saulGoodMines = 0;
int ironMines = 0;
@ -86,7 +80,7 @@ public class GameEngine implements Runnable {
int cannons = 0;
// count buildings in our map
for (Building b : this.map.contains){
for (Building b : map.contains){
if (b instanceof SaulGoodMine)
saulGoodMines++;
else if (b instanceof IronMine)
@ -145,6 +139,35 @@ public class GameEngine implements Runnable {
return score;
}
public void updateMap(Map map) {
for (Building b : map.contains){
if ((b instanceof ResourceBuilding)) {
((ResourceBuilding) b).update(map.getTownHall());
}
}
}
public boolean build (Map map, String buildingArg) {
BuildingFactory bfactory = new BuildingFactory();
Building type = bfactory.getBuilding(buildingArg);
return map.build(new Tile(), type);
}
public boolean train (Map map, String inhabitantArgs) {
InhabitantFactory ifactory = new InhabitantFactory();
Inhabitant type = ifactory.getInhabitant(inhabitantArgs);
return map.train(type);
}
public boolean upgradeBuilding (Map map, int buildingIndex) {
return map.upgradeBuilding(buildingIndex);
}
public boolean upgradeInhabitant (Map map, int inhabitantIndex) {
return map.upgradeInhabitant(inhabitantIndex);
}
/*
@Override
public void run() {
String in;
@ -156,11 +179,6 @@ public class GameEngine implements Runnable {
Map exploringMap = null;
boolean deleteMyHeart = true;
while (running) {
for (Building b : this.map.contains){
if ((b instanceof ResourceBuilding)) {
((ResourceBuilding) b).update(this.map.getTownHall());
}
}
try {
if ((in = view.nextInput()) != null) {
String[] args = in.split(" ");
@ -249,8 +267,7 @@ public class GameEngine implements Runnable {
if (deleteMyHeart)
exploringMap = null;
}
save("test.xml", this.map);
}
} */
public void save(String file, Map map){
try (XMLEncoder encoder = new XMLEncoder(new BufferedOutputStream(Files.newOutputStream(Paths.get(file))))) {

View File

@ -56,7 +56,7 @@ public class Map implements Serializable {
public boolean upgradeBuilding(int buildingIndex) {
if (buildingIndex >= contains.size()) return false;
if (buildingIndex >= contains.size() || buildingIndex < 0) return false;
Building b = contains.get(buildingIndex);
@ -93,7 +93,7 @@ public class Map implements Serializable {
public boolean upgradeInhabitant(int inhabitantIndex) {
if (inhabitantIndex >= inhabitants.size()) return false;
if (inhabitantIndex >= inhabitants.size() || inhabitantIndex < 0) return false;
Inhabitant i = inhabitants.get(inhabitantIndex);

View File

@ -15,8 +15,8 @@ public class GameDisplay {
private BufferedReader reader;
private String input;
public GameDisplay(InputStream readFrom) {
reader = new BufferedReader(new InputStreamReader(readFrom));
public GameDisplay() {
reader = new BufferedReader(new InputStreamReader(System.in));
}
public String nextInput() throws IOException {

View File

@ -1,34 +0,0 @@
package ca.cosc3p91.a4.util;
import ca.cosc3p91.a4.userinterface.GameDisplay;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Client {
GameDisplay view = new GameDisplay(System.in);
public Client(int port) throws IOException {
DatagramSocket clientSocket = new DatagramSocket();
InetAddress IPAddress = InetAddress.getByName("localhost");
String prompt;
byte[] sendData = new byte[1024];
byte[] receiveData = new byte[1024];
while (true) {
if ((prompt = view.nextInput()) != null) {
if (!prompt.isEmpty() && prompt.charAt(0) == '6') break;
sendData = prompt.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
clientSocket.send(sendPacket);
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
String serverOutput = new String(receivePacket.getData()).trim();
System.out.println(">" + serverOutput);
view.printGameMenu();
}
}
clientSocket.close();
}
}

View File

@ -1,16 +0,0 @@
package ca.cosc3p91.a4.util;
public class PacketTable {
// packetID -> byte defined in this file
// clientID -> long
// messageID -> long
// packetID, clientID (0 if connecting to server)
public static final byte CONNECT = 0x1;
// packetID, clientID
public static final byte DISCONNECT = 0x2;
// packetID, clientID, messageID
public static final byte ACK = 0x3;
}

View File

@ -0,0 +1,90 @@
package ca.cosc3p91.a4.util.network;
import ca.cosc3p91.a4.userinterface.GameDisplay;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
public class Client implements Runnable {
private GameDisplay view = new GameDisplay();
private DatagramSocket clientSocket;
private boolean running = true;
private Thread receiveThread;
private final HashMap<Long, Message.Sent> sentMessages = new HashMap<>();
private int lastMessageID = 0;
private final InetAddress serverAddress;
public Client(String address) throws IOException {
serverAddress = InetAddress.getByName(address);
clientSocket = new DatagramSocket();
receiveThread = new Thread(this);
receiveThread.start();
sendMessage(new Message.Sent(PacketTable.CONNECT, 0, ++lastMessageID));
String prompt;
while (running) {
if ((prompt = view.nextInput()) != null) {
if (prompt.trim().isEmpty())
continue;
if (prompt.charAt(0) == '6')
break;
view.printGameMenu();
}
}
clientSocket.close();
}
public void run(){
while (running){
try {
byte[] receiveData = new byte[Server.PACKET_SIZE];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(receivePacket.getData()));
byte packetID = stream.readByte();
long clientID = stream.readLong();
long messageID = stream.readLong();
switch (packetID) {
case PacketTable.ACK:
Message.Sent message = sentMessages.get(messageID);
if (message == null)
throw new RuntimeException("Server message sync error!");
message.acknowledged();
sentMessages.remove(messageID);
System.out.println("Message acknowledged " + messageID);
break;
case PacketTable.DISCONNECT:
running = false;
break;
}
} catch (Exception e){
e.printStackTrace();
}
}
}
private void sendMessage(Message.Sent message){
sentMessages.put(message.getMessageID(), message);
byte[] data = message.getData().toByteArray();
DatagramPacket sendPacket = new DatagramPacket(data, data.length, serverAddress, Server.SERVER_PORT);
try {
clientSocket.send(sendPacket);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,105 @@
package ca.cosc3p91.a4.util.network;
import ca.cosc3p91.a4.util.Time;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
public class Message {
private final byte packetID;
private final long clientID, messageID;
public Message(byte packetID, long clientID, long messageID){
this.packetID = packetID;
this.clientID = clientID;
this.messageID = messageID;
}
public byte getPacketID() {
return packetID;
}
public long getClientID() {
return clientID;
}
public long getMessageID() {
return messageID;
}
public static class Received extends Message {
private final DataInputStream reader;
private final byte[] data;
public Received(byte packetID, long clientID, long messageID, DataInputStream reader, byte[] data) {
super(packetID, clientID, messageID);
this.reader = reader;
this.data = data;
}
public DataInputStream getReader(){
return reader;
}
public byte[] getData(){
return data;
}
}
public static class Sent extends Message {
private final DataOutputStream writer;
private final ByteArrayOutputStream data;
private boolean ack = false;
private final Time timeSent;
/**
* A message packet which will be sent to a client or the server, contains the standard message header and
* writes the header to the stream, make sure you don't write into the stream before constructing this!
*
* @param packetID type of this message
* @param clientID the client id, if this is going to the client it is unlikely to be used but should always be correct!
* @param messageID client specific message id, used to reference/acknowledge messages
* @param writer stream to write to
* @param data byte array stream which contains the byte[] used in packet construction
*/
public Sent(byte packetID, long clientID, long messageID) {
super(packetID, clientID, messageID);
this.data = new ByteArrayOutputStream();
this.writer = new DataOutputStream(this.data);
timeSent = Time.getTime();
// write the header to the stream, make sure you don't write into the stream before constructing this!
try {
writer.writeByte(packetID);
writer.writeLong(clientID);
writer.writeLong(messageID);
} catch (Exception e){
e.printStackTrace();
}
}
public void acknowledged(){
this.ack = true;
}
public boolean isAcknowledged(){
return ack;
}
public DataOutputStream getWriter(){
return writer;
}
public Time getTimeSinceSent(){
return Time.getTime().difference(timeSent);
}
public ByteArrayOutputStream getData(){
return data;
}
}
}

View File

@ -0,0 +1,22 @@
package ca.cosc3p91.a4.util.network;
public class PacketTable {
// packetID -> byte defined in this file
// clientID -> long
// messageID -> long
// messageHeader (packetID, clientID, messageID)
// messageHeader, (clientID = 0 if connecting to server)
public static final byte CONNECT = 0x1;
// messageHeader
public static final byte DISCONNECT = 0x2;
// messageHeader
public static final byte ACK = 0x3;
// messageHeader, UTF8 String with length information (use DOS.writeUTF/DIS.readUTF)
public static final byte MESSAGE = 0x4;
// messageHeader, serial packets with map info
public static final byte MAP_DATA = 0x5;
}

View File

@ -1,11 +1,15 @@
package ca.cosc3p91.a4.util;
package ca.cosc3p91.a4.util.network;
import ca.cosc3p91.a4.game.GameEngine;
import ca.cosc3p91.a4.game.Map;
import java.io.*;
import java.net.*;
import java.rmi.ServerException;
import java.util.*;
import java.util.HashMap;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
public class Server implements Runnable {
@ -16,9 +20,9 @@ public class Server implements Runnable {
private final HashMap<Long, ConnectedClient> clients = new HashMap<>();
private long clientAssignmentID = 0;
private long lastMessageID = 0;
private final DatagramSocket socket;
private final Thread ioThread;
private long lastSentMessageID = 0;
private GameEngine mainEngine;
@ -32,22 +36,25 @@ public class Server implements Runnable {
public void run(){
while (running) {
byte[] receiveData = new byte[PACKET_SIZE];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
try {
byte[] receiveData = new byte[PACKET_SIZE];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
// BLOCKING!
socket.receive(receivePacket);
// read in the message header that is associated with every message.
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(receivePacket.getData()));
byte packetID = stream.readByte();
long clientID = stream.readLong();
long messageID = stream.readLong();
ConnectedClient client = clients.get(clientID);
// the server must handle connection requests while the client's processing thread will handle all other messages
if (packetID == PacketTable.CONNECT){
clients.put(++clientAssignmentID, new ConnectedClient(socket, clientID, receivePacket.getAddress(), receivePacket.getPort()));
} else if (packetID == PacketTable.DISCONNECT){
clients.put(++clientAssignmentID, new ConnectedClient(socket, mainEngine, clientID, messageID, receivePacket.getAddress(), receivePacket.getPort()));
} else if (packetID == PacketTable.DISCONNECT) {
if (client == null)
throw new ServerException("Client disconnected with invalid client id! (" + clientID + ")");
client.halt();
@ -55,7 +62,7 @@ public class Server implements Runnable {
} else {
if (client == null)
throw new ServerException("Client message with invalid client id! (" + clientID + ")");
client.handleRequest(new ConnectedClient.ServerRequest(packetID, stream));
client.handleRequest(new Message.Received(packetID, clientID, messageID, stream, receivePacket.getData()));
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
@ -75,99 +82,90 @@ public class Server implements Runnable {
private static class ConnectedClient implements Runnable {
private final InetAddress address;
private final int port;
private final ArrayList<ServerRequest> requests = new ArrayList<>();
private final Queue<ServerRequest> pendingRequests = new PriorityQueue<>();
// could use read/write lock for some of this, as certain operations, mostly timeout check, won't modify data.
private final Queue<Message.Received> pendingRequests = new PriorityQueue<>();
private final ReentrantLock requestLock = new ReentrantLock();
private final DatagramSocket socket;
private final HashMap<Long, Message.Sent> sentMessages = new HashMap<>();
private final DatagramSocket serverSocket;
private final long clientID;
private volatile boolean running = true;
private final Thread processingThread;
private final Map clientMap;
public ConnectedClient(DatagramSocket socket, long clientID, InetAddress address, int port){
this.socket = socket;
public ConnectedClient(DatagramSocket serverSocket, GameEngine engine, long clientID, long messageID, InetAddress address, int port){
this.serverSocket = serverSocket;
this.address = address;
this.port = port;
this.clientID = clientID;
this.clientMap = engine.generateInitialMap();
processingThread = new Thread(this);
processingThread.start();
sendMessage(new Message.Sent(PacketTable.ACK, clientID, messageID));
}
public void handleRequest(ServerRequest request){
public void handleRequest(Message.Received request){
if (request.getClientID() != this.clientID)
throw new RuntimeException("Server sent us a message, yet we are not the intended recipient!");
requestLock.lock();
pendingRequests.add(request);
requestLock.unlock();
}
private void processRequest(ServerRequest request){
private void processRequest(Message.Received request){
try {
switch (request.getID()){
switch (request.getPacketID()){
case PacketTable.ACK:
long messageID = request.getDataStream().readLong();
Message.Sent message = sentMessages.get(request.getMessageID());
if (message == null)
throw new RuntimeException("A message was acknowledged but does not exist!");
message.acknowledged();
break;
case PacketTable.MESSAGE:
System.out.println(request.getReader().readUTF());
break;
}
} catch (IOException e) {
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void run(){
while (running){
// handle request processing without blocking the I/O thread
requestLock.lock();
while (pendingRequests.size() > 0) {
ServerRequest request = pendingRequests.remove();
if (!pendingRequests.isEmpty()) {
Message.Received request = pendingRequests.remove();
processRequest(request);
requests.add(request);
}
requestLock.unlock();
requests.removeIf(ServerRequest::isAck);
for (ServerRequest request : requests){
// TODO:
if (request.getTimeSinceReceived().get() > MAX_PACKET_ACK_TIME_SECONDS)
System.out.println("A packet hasn't received a ack, it might have been lost!");
for (HashMap.Entry<Long, Message.Sent> message : sentMessages.entrySet()){
if (message.getValue().getTimeSinceSent().get() > MAX_PACKET_ACK_TIME_SECONDS) {
System.out.println("The server did not process our message, did they receive it?");
// todo: resend message
}
}
}
}
public void sendMessage(Message.Sent message){
this.sentMessages.put(message.getMessageID(), message);
byte[] data = message.getData().toByteArray();
if (data.length > PACKET_SIZE)
throw new RuntimeException("Unable to send packet as it exceeds maximum packet size!");
DatagramPacket request = new DatagramPacket(data, data.length, address, port);
try {
serverSocket.send(request);
} catch (IOException e) {
e.printStackTrace();
}
}
public void halt() throws InterruptedException {
running = false;
processingThread.join();
}
private static class ServerRequest {
private final byte id;
private final Time receiveTime;
private final DataInputStream receive;
private boolean ack = false;
public ServerRequest(byte id, DataInputStream receive){
this.id = id;
this.receive = receive;
receiveTime = Time.getTime();
}
public byte getID(){
return id;
}
public void acknowledged(){
this.ack = true;
}
public boolean isAck(){
return this.ack;
}
public DataInputStream getDataStream(){
return receive;
}
public Time getTimeSinceReceived(){
return Time.getTime().difference(receiveTime);
}
}
}
}