From 4b903306ef9177aa9cc871a9b1dfa5300f417058 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Sun, 8 Mar 2020 16:35:28 -0400 Subject: [PATCH 01/20] Add protobuf to the mix --- MekHQ/.classpath | 13 +++++++++++++ MekHQ/build.gradle | 28 +++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/MekHQ/.classpath b/MekHQ/.classpath index b616e82356..9805dad7f2 100644 --- a/MekHQ/.classpath +++ b/MekHQ/.classpath @@ -16,6 +16,19 @@ + + + + + + + + + + + + + diff --git a/MekHQ/build.gradle b/MekHQ/build.gradle index 2eb416994a..6536a7aa52 100644 --- a/MekHQ/build.gradle +++ b/MekHQ/build.gradle @@ -4,12 +4,17 @@ plugins { id 'application' id 'maven-publish' id 'edu.sc.seis.launch4j' version '2.4.4' + id 'com.google.protobuf' version '0.8.8' } sourceSets { main { java { - srcDirs = ['src'] + srcDirs = [ + 'src', + 'build/generated/source/proto/main/grpc', + 'build/generated/source/proto/main/java' + ] } resources { srcDirs = ['resources'] @@ -61,12 +66,33 @@ dependencies { runtimeOnly 'org.glassfish.jaxb:jaxb-core:2.3.0' runtimeOnly 'com.sun.activation:javax.activation:1.2.0' + // gRPC for Distributed MekHQ + implementation 'io.grpc:grpc-netty-shaded:1.27.1' + implementation 'io.grpc:grpc-protobuf:1.27.1' + implementation 'io.grpc:grpc-stub:1.27.1' + jarbundler 'com.ultramixer.jarbundler:jarbundler-core:3.3.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.20.1' } +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.11.0" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.27.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + mainClassName = 'mekhq.MekHQ' ext { From 2faeaa8433ed3e46b1be94cfdcd24696b1f7f16c Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Sun, 8 Mar 2020 16:35:42 -0400 Subject: [PATCH 02/20] Add the initial service definition --- MekHQ/src/main/proto/Service.proto | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 MekHQ/src/main/proto/Service.proto diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto new file mode 100644 index 0000000000..cde90422d9 --- /dev/null +++ b/MekHQ/src/main/proto/Service.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "mekhq.online"; +option java_outer_classname = "DistributedMekHQ"; + +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; + +package mekhqonline; + +service MekHQHost { + rpc Connect(ConnectionRequest) returns (ConnectionResponse) {} + + rpc Disconnect(DisconnectionRequest) returns (DisconnectionResponse) {} + + rpc MessageBus(stream ClientMessage) returns (stream ServerMessage) {} +} + +// The request message containing the user's name. +message ConnectionRequest { + string id = 1; + string version = 2; + string name = 3; + string date = 4; + string location = 5; + oneof password_option { + string password = 6; + } +} + +// The response message containing the greetings +message ConnectionResponse { + string id = 1; + string version = 2; + string name = 3; + string date = 4; + string location = 5; + repeated Campaign campaigns = 6; +} + +message DisconnectionRequest { + string id = 1; +} + +message DisconnectionResponse { + string id = 1; +} + +message Campaign { + string id = 1; + string name = 2; + string date = 3; + int32 team = 4; + string location = 5; +} + +message ClientMessage { + google.protobuf.Timestamp timestamp = 1; + string id = 2; + map metadata = 3; + google.protobuf.Any message = 4; +} + +message ServerMessage { + google.protobuf.Timestamp timestamp = 1; + string id = 2; + map metadata = 3; + google.protobuf.Any message = 4; +} + +message Ping { + string date = 1; + string location = 2; +} + +message Pong { + string date = 1; + string location = 2; + repeated Campaign campaigns = 3; +} + +message CampaignDateChanged { + string date = 1; +} + +message GMModeChanged { + bool value = 1; +} + +message TeamAssignmentChanged { + repeated TeamAssignment teams = 1; +} + +message TeamAssignment { + string id = 1; + int32 team = 2; +} + +message LocationChanged { + string location = 1; + bool is_gm_movement = 2; +} From 61f2f1dcac75224fb112d1a1244b6e19117a7266 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Sun, 8 Mar 2020 16:38:26 -0400 Subject: [PATCH 03/20] Add initial gRPC work towards distributed MekHQ --- MekHQ/src/mekhq/MekHQ.java | 63 +++- .../mekhq/campaign/CampaignController.java | 55 +++- MekHQ/src/mekhq/campaign/RemoteCampaign.java | 38 +++ MekHQ/src/mekhq/online/MekHQClient.java | 209 +++++++++++++ MekHQ/src/mekhq/online/MekHQServer.java | 296 ++++++++++++++++++ 5 files changed, 657 insertions(+), 4 deletions(-) create mode 100644 MekHQ/src/mekhq/campaign/RemoteCampaign.java create mode 100644 MekHQ/src/mekhq/online/MekHQClient.java create mode 100644 MekHQ/src/mekhq/online/MekHQServer.java diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java index 67dbc37cc5..0b964b2b86 100644 --- a/MekHQ/src/mekhq/MekHQ.java +++ b/MekHQ/src/mekhq/MekHQ.java @@ -34,6 +34,9 @@ import javax.swing.*; import javax.swing.text.DefaultEditorKit; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + import megamek.MegaMek; import megamek.client.Client; import megamek.common.event.EventBus; @@ -80,6 +83,8 @@ import mekhq.gui.dialog.RetirementDefectionDialog; import mekhq.gui.preferences.StringPreference; import mekhq.gui.utilities.ObservableString; +import mekhq.online.MekHQClient; +import mekhq.online.MekHQServer; import mekhq.preferences.MekHqPreferences; import mekhq.preferences.PreferencesNode; import mekhq.service.AutosaveService; @@ -130,6 +135,14 @@ public class MekHQ implements GameListener { private final IAutosaveService autosaveService; + private final ResourceBundle resourceBundle = ResourceBundle.getBundle("mekhq.resources.MekHQ"); + + private boolean isHost; + private boolean isRemote; + private MekHQServer onlineServer; + private MekHQClient onlineClient; + private ManagedChannel channel; + /** * Converts the MekHQ {@link #VERBOSITY_LEVEL} to {@link LogLevel}. * @@ -285,12 +298,21 @@ private MekHQ() { /** * At startup create and show the main frame of the application. */ - protected void startup() { + protected void startup(String[] args) { UIManager.installLookAndFeel("Flat Light", "com.formdev.flatlaf.FlatLightLaf"); UIManager.installLookAndFeel("Flat IntelliJ", "com.formdev.flatlaf.FlatIntelliJLaf"); UIManager.installLookAndFeel("Flat Dark", "com.formdev.flatlaf.FlatDarkLaf"); UIManager.installLookAndFeel("Flat Darcula", "com.formdev.flatlaf.FlatDarculaLaf"); + for (String arg : args) { + if ("--host".equalsIgnoreCase(arg)) { + setIsHosting(true); + } + else if ("--connect".equalsIgnoreCase(arg)) { + setIsConnecting(true); + } + } + showInfo(); //Setup user preferences @@ -303,7 +325,27 @@ protected void startup() { sud.setVisible(true); } - private void setUserPreferences() { + public String getVersion() { + return resourceBundle.getString("Application.version"); + } + + private void setIsConnecting(boolean b) { + isRemote = b; + } + + public boolean isRemote() { + return isRemote; + } + + private void setIsHosting(boolean b) { + isHost = b; + } + + public boolean isHosting() { + return isHost; + } + + private void setUserPreferences() { PreferencesNode preferences = getPreferences().forClass(MekHQ.class); selectedTheme = new ObservableString("selectedTheme", UIManager.getLookAndFeel().getClass().getName()); @@ -354,6 +396,21 @@ public void exit() { } public void showNewView() { + try { + if (isHosting()) { + onlineServer = new MekHQServer(3000, campaignController); + + onlineServer.start(); + } else if (isRemote()) { + channel = ManagedChannelBuilder.forTarget("localhost:3000").usePlaintext().build(); + onlineClient = new MekHQClient(channel, campaignController); + + onlineClient.connect(); + } + } catch (IOException ex) { + MekHQ.getLogger().error(MekHQ.class, "showNewView()", "Could not connect to server", ex); + } + campaigngui = new CampaignGUI(this); campaigngui.showOverviewTab(getCampaign().isOverviewLoadingValue()); } @@ -371,7 +428,7 @@ public static void main(String[] args) { // redirect output to log file redirectOutput(logFileName); // Deprecated call required for MegaMek usage - SwingUtilities.invokeLater(() -> MekHQ.getInstance().startup()); + SwingUtilities.invokeLater(() -> MekHQ.getInstance().startup(args)); } private void showInfo() { diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 3946f97b05..995faae1a7 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -18,15 +18,28 @@ */ package mekhq.campaign; +import java.util.Collection; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.joda.time.DateTime; + +import mekhq.campaign.universe.PlanetarySystem; +import mekhq.campaign.universe.Systems; /** * Manages the timeline of a {@link Campaign}. */ public class CampaignController { private final Campaign localCampaign; + private final Map remoteCampaigns = new ConcurrentHashMap<>(); + private final Map activeCampaigns = new ConcurrentHashMap<>(); private boolean isHost; private UUID host; + private DateTime hostDate; + private String hostName; + private PlanetarySystem hostLocation; /** * Creates a new {@code CampaignController} for @@ -74,6 +87,39 @@ public boolean isHost() { return isHost; } + public void setHostDate(DateTime date) { + hostDate = date; + } + + public DateTime getHostDate() { + return hostDate; + } + + public void setHostName(String name) { + hostName = name; + } + + public void setHostLocation(String planetarySystemId) { + hostLocation = Systems.getInstance().getSystemById(planetarySystemId); + } + + public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId) { + PlanetarySystem planetarySystem = Systems.getInstance().getSystemById(locationId); + remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem)); + } + + public Collection getRemoteCampaigns() { + return remoteCampaigns.values(); + } + + public void addActiveCampaign(UUID id) { + activeCampaigns.put(id, id); + } + + public void removeActiveCampaign(UUID id) { + activeCampaigns.remove(id); + } + /** * Advances the local {@link Campaign} to the next day. */ @@ -83,7 +129,14 @@ public void advanceDay() { // TODO: notifyDayChangedEvent(); } } else { - // TODO: requestNewDay(); + if (getLocalCampaign().getDateTime().isBefore(getHostDate())) { + if (getLocalCampaign().newDay()) { + // TODO: notifyDayChangedEvent + } + } + else { + // TODO: requestNewDay(); + } } } } diff --git a/MekHQ/src/mekhq/campaign/RemoteCampaign.java b/MekHQ/src/mekhq/campaign/RemoteCampaign.java new file mode 100644 index 0000000000..b1864fb803 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/RemoteCampaign.java @@ -0,0 +1,38 @@ +package mekhq.campaign; + +import java.util.UUID; + +import org.joda.time.DateTime; + +import mekhq.campaign.universe.PlanetarySystem; + +public class RemoteCampaign { + + private final UUID id; + private final String name; + private final DateTime date; + private final PlanetarySystem location; + + public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location) { + this.id = id; + this.name = name; + this.date = date; + this.location = location; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public DateTime getDate() { + return date; + } + + public PlanetarySystem getLocation() { + return location; + } +} diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java new file mode 100644 index 0000000000..5208020b23 --- /dev/null +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -0,0 +1,209 @@ +package mekhq.online; + +import java.util.ResourceBundle; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.Timestamp; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import io.grpc.Channel; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import megamek.common.event.Subscribe; +import mekhq.MekHQ; +import mekhq.campaign.CampaignController; +import mekhq.campaign.event.NewDayEvent; +import mekhq.online.MekHQHostGrpc.MekHQHostBlockingStub; +import mekhq.online.MekHQHostGrpc.MekHQHostStub; + +public class MekHQClient { + private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); + private final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.MekHQ"); + + private final MekHQHostBlockingStub blockingStub; + private final MekHQHostStub asyncStub; + private final CampaignController controller; + + private final CountDownLatch finishLatch = new CountDownLatch(1); + private StreamObserver messageBus; + private ScheduledExecutorService pingExecutor; + private ScheduledFuture pings; + + public MekHQClient(Channel channel, CampaignController controller) { + blockingStub = MekHQHostGrpc.newBlockingStub(channel); + asyncStub = MekHQHostGrpc.newStub(channel); + this.controller = controller; + } + + protected mekhq.campaign.Campaign getCampaign() { + return controller.getLocalCampaign(); + } + + public void connect() { + ConnectionRequest request = ConnectionRequest.newBuilder().setId(getCampaign().getId().toString()) + .setName(getCampaign().getName()).setVersion(resourceMap.getString("Application.version")) + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + + ConnectionResponse response; + try { + response = blockingStub.connect(request); + } catch (StatusRuntimeException e) { + MekHQ.getLogger().warning(MekHQClient.class, "connect()", "RPC failed: " + e.getStatus()); + return; + } + + MekHQ.getLogger().info(MekHQClient.class, "connect()", + "Connected to Campaign: " + response.getId() + " " + response.getDate()); + + controller.setHost(UUID.fromString(response.getId())); + controller.setHostName(response.getName()); + controller.setHostDate(DateTime.parse(response.getDate(), dateFormatter)); + controller.setHostLocation(response.getLocation()); + + createMessageBus(); + + pingExecutor = Executors.newSingleThreadScheduledExecutor(); + pings = pingExecutor.scheduleAtFixedRate(() -> sendPing(), 0, 15, TimeUnit.SECONDS); + + MekHQ.registerHandler(this); + } + + public void disconnect() { + DisconnectionRequest request = DisconnectionRequest.newBuilder().setId(getCampaign().getId().toString()) + .build(); + + try { + DisconnectionResponse response = blockingStub.disconnect(request); + MekHQ.getLogger().info(MekHQClient.class, "disconnect()", + "Disconnected from Campaign: " + response.getId()); + } catch (StatusRuntimeException e) { + MekHQ.getLogger().warning(MekHQClient.class, "disconnect()", "RPC failed: " + e.getStatus()); + } + + controller.setHost(getCampaign().getId()); + } + + private void createMessageBus() { + messageBus = asyncStub.messageBus(new StreamObserver() { + @Override + public void onNext(ServerMessage message) { + UUID id = UUID.fromString(message.getId()); + Any payload = message.getMessage(); + try { + if (payload.is(Ping.class)) { + handlePing(id, payload.unpack(Ping.class)); + } else if (payload.is(Pong.class)) { + handlePong(id, payload.unpack(Pong.class)); + } else if (payload.is(CampaignDateChanged.class)) { + handleCampaignDateChanged(id, payload.unpack(CampaignDateChanged.class)); + } else if (payload.is(GMModeChanged.class)) { + + } else if (payload.is(LocationChanged.class)) { + + } + } catch (InvalidProtocolBufferException e) { + MekHQ.getLogger().error(MekHQClient.class, "messageBus::onNext()", "RPC protocol error: " + e.getMessage(), e); + handleRpcException(e); + } + } + + @Override + public void onError(Throwable t) { + MekHQ.getLogger().error(MekHQClient.class, "messageBus::onNext()", "RPC protocol error: " + t.getMessage(), t); + finishLatch.countDown(); + } + + @Override + public void onCompleted() { + finishLatch.countDown(); + } + }); + } + + protected void handleCampaignDateChanged(UUID id, CampaignDateChanged dateChanged) { + String date = dateChanged.getDate(); + controller.setHostDate(dateFormatter.parseDateTime(date)); + MekHQ.getLogger().info(MekHQClient.class, "handleCampaignDateChanged()", String.format("<- HOST CampaignDateChanged: %s", date)); + } + + protected void handleRpcException(Throwable t) { + messageBus.onError(Status.INTERNAL + .withDescription(t.getMessage()) + .augmentDescription("messageBus()") + .withCause(t) // This can be attached to the Status locally, but NOT transmitted to the client! + .asRuntimeException()); + } + + private void sendPing() { + Ping ping = Ping.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .build(); + messageBus.onNext(buildMessage(ping)); + } + + protected void handlePing(UUID id, Ping ping) { + Pong pong = Pong.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .build(); + + MekHQ.getLogger().info(MekHQClient.class, "handlePing()", String.format("-> PING: %s %s %s", id, ping.getDate(), ping.getLocation())); + + messageBus.onNext(buildMessage(pong)); + } + + protected void handlePong(UUID id, Pong pong) { + String date = pong.getDate(); + DateTime hostDate = dateFormatter.parseDateTime(date); + String locationId = pong.getLocation(); + + MekHQ.getLogger().info(MekHQClient.class, "handlePong()", String.format("-> PONG: %s %s %s (%d connected)", id, hostDate, locationId, pong.getCampaignsCount())); + + controller.setHostDate(hostDate); + controller.setHostLocation(locationId); + + for (Campaign campaign : pong.getCampaignsList()) { + controller.addRemoteCampaign(UUID.fromString(campaign.getId()), campaign.getName(), + dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation()); + } + } + + @Subscribe + public void handle(NewDayEvent evt) { + CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .build(); + + messageBus.onNext(buildMessage(dateChanged)); + } + + private ClientMessage buildMessage(Message payload) { + return ClientMessage.newBuilder() + .setTimestamp(getTimestamp()) + .setId(getCampaign().getId().toString()) + .setMessage(Any.pack(payload)) + .build(); + } + + private static Timestamp getTimestamp() { + long millis = System.currentTimeMillis(); + return Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)) + .build(); + } +} diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java new file mode 100644 index 0000000000..ddd2f7e908 --- /dev/null +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -0,0 +1,296 @@ +package mekhq.online; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.Timestamp; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import megamek.common.event.Subscribe; +import mekhq.MekHQ; +import mekhq.campaign.CampaignController; +import mekhq.campaign.RemoteCampaign; +import mekhq.campaign.event.NewDayEvent; +import mekhq.online.MekHQHostGrpc; + +public class MekHQServer { + private final int port; + private final MekHQHostService service; + private final Server server; + + private ScheduledExecutorService pingExecutor; + private ScheduledFuture pings; + + public MekHQServer(int port, CampaignController controller) throws IOException { + this(ServerBuilder.forPort(port), port, controller); + } + + public MekHQServer(ServerBuilder serverBuilder, int port, CampaignController controller) { + this.port = port; + service = new MekHQHostService(controller); + server = serverBuilder.addService(service).build(); + } + + /** Start serving requests. */ + public void start() throws IOException { + server.start(); + + MekHQ.registerHandler(this); + + pingExecutor = Executors.newSingleThreadScheduledExecutor(); + pings = pingExecutor.scheduleAtFixedRate(() -> service.sendPings(), 1, 15, TimeUnit.SECONDS); + + MekHQ.getLogger().info(MekHQServer.class, "start()", "Server started, listening on " + port); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + // Use stderr here since the logger may have been reset by its JVM shutdown + // hook. + System.err.println("*** shutting down gRPC server since JVM is shutting down"); + try { + MekHQServer.this.stop(); + } catch (InterruptedException e) { + e.printStackTrace(System.err); + } + System.err.println("*** server shut down"); + } + }); + } + + /** Stop serving requests and shutdown resources. */ + public void stop() throws InterruptedException { + if (server != null) { + server.shutdown().awaitTermination(30, TimeUnit.SECONDS); + } + } + + /** + * Await termination on the main thread since the grpc library uses daemon + * threads. + */ + private void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } + + @Subscribe + public void handle(NewDayEvent evt) { + service.notifyDayChanged(); + } + + private static class MekHQHostService extends MekHQHostGrpc.MekHQHostImplBase { + private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); + private final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.MekHQ"); + + private final CampaignController controller; + + private final ConcurrentMap outstandingPings = new ConcurrentHashMap<>(); + private final ConcurrentMap> messageBus = new ConcurrentHashMap<>(); + + MekHQHostService(CampaignController controller) { + this.controller = controller; + } + + private mekhq.campaign.Campaign getCampaign() { + return controller.getLocalCampaign(); + } + + @Override + public void connect(ConnectionRequest request, StreamObserver responseObserver) { + String version = resourceMap.getString("Application.version"); + if (!version.equalsIgnoreCase(request.getVersion())) { + responseObserver.onError(Status.INVALID_ARGUMENT + .withDescription(String.format("Version mismatch. Host %s. Client %s.", version, request.getVersion())) + .augmentDescription("connect()") + .asRuntimeException()); + return; + } + + UUID id = UUID.fromString(request.getId()); + if (getCampaign().getId().equals(id)) { + responseObserver.onError(Status.INVALID_ARGUMENT + .withDescription(String.format("Campaign %s cannot both HOST and CONNECT", id)) + .augmentDescription("connect()") + .asRuntimeException()); + return; + } + + ConnectionResponse response = ConnectionResponse.newBuilder().setVersion(version) + .setId(getCampaign().getId().toString()).setName(getCampaign().getName()) + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .addAllCampaigns(convert(controller.getRemoteCampaigns())).build(); + responseObserver.onNext(response); + + controller.addActiveCampaign(id); + controller.addRemoteCampaign(id, request.getName(), DateTime.parse(request.getDate()), + request.getLocation()); + + responseObserver.onCompleted(); + } + + @Override + public void disconnect(DisconnectionRequest request, StreamObserver responseObserver) { + UUID id = UUID.fromString(request.getId()); + + controller.removeActiveCampaign(id); + + DisconnectionResponse response = DisconnectionResponse.newBuilder().setId(getCampaign().getId().toString()) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + @Override + public StreamObserver messageBus(StreamObserver responseObserver) { + return new StreamObserver() { + private UUID clientId; + + @Override + public void onNext(ClientMessage message) { + clientId = UUID.fromString(message.getId()); + Any payload = message.getMessage(); + try { + if (payload.is(Ping.class)) { + handlePing(responseObserver, clientId, payload.unpack(Ping.class)); + } else if (payload.is(Pong.class)) { + handlePong(responseObserver, clientId, payload.unpack(Pong.class)); + } else if (payload.is(CampaignDateChanged.class)) { + + } else if (payload.is(GMModeChanged.class)) { + + } else if (payload.is(LocationChanged.class)) { + + } + } catch (InvalidProtocolBufferException e) { + MekHQ.getLogger().error(MekHQHostService.class, "messageBus::onNext()", "RPC protocol error: " + e.getMessage(), e); + responseObserver.onError(Status.INTERNAL + .withDescription(e.getMessage()) + .augmentDescription("messageBus()") + .withCause(e) // This can be attached to the Status locally, but NOT transmitted to the client! + .asRuntimeException()); + } + } + + @Override + public void onError(Throwable t) { + MekHQ.getLogger().error(MekHQHostService.class, "messageBus::onError()", + String.format("RPC protocol error from client %s: %s", clientId, t.getMessage()), t); + removeMessageBus(clientId, responseObserver); + } + + @Override + public void onCompleted() { + removeMessageBus(clientId, responseObserver); + responseObserver.onCompleted(); + } + }; + } + + private void addMessageBus(UUID campaignId, StreamObserver responseObserver) { + messageBus.putIfAbsent(Objects.requireNonNull(campaignId), responseObserver); + } + + private void removeMessageBus(UUID campaignId, StreamObserver responseObserver) { + messageBus.remove(campaignId); + } + + public void sendPings() { + Ping ping = Ping.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .build(); + for (Map.Entry> client : messageBus.entrySet()) { + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", "<- PING: " + client.getKey()); + client.getValue().onNext(buildResponse(Ping.newBuilder(ping).build())); + } + } + + private void handlePing(StreamObserver responseObserver, UUID campaignId, Ping ping) { + addMessageBus(campaignId, responseObserver); + + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PING: %s %s %s", campaignId, ping.getDate(), ping.getLocation())); + + Pong pong = Pong.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .addAllCampaigns(convert(controller.getRemoteCampaigns())) + .build(); + responseObserver.onNext(buildResponse(pong)); + } + + private void handlePong(StreamObserver responseObserver, UUID campaignId, Pong pong) { + outstandingPings.remove(campaignId); + + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, pong.getDate(), pong.getLocation())); + } + + public void notifyDayChanged() { + CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .build(); + + sendToAll(dateChanged); + } + + private void sendToAll(Message message) { + for (Map.Entry> client : messageBus.entrySet()) { + client.getValue().onNext(buildResponse(message)); + } + } + + private ServerMessage buildResponse(Message payload) { + return ServerMessage.newBuilder() + .setTimestamp(getTimestamp()) + .setId(getCampaign().getId().toString()) + .setMessage(Any.pack(payload)) + .build(); + } + + private static Timestamp getTimestamp() { + long millis = System.currentTimeMillis(); + return Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)) + .build(); + } + + private Collection convert(Collection remoteCampaigns) { + List converted = new ArrayList<>(); + for (RemoteCampaign remoteCampaign : remoteCampaigns) { + converted.add( + Campaign.newBuilder() + .setId(remoteCampaign.getId().toString()) + .setName(remoteCampaign.getName()) + .setDate(dateFormatter.print(remoteCampaign.getDate())) + .setLocation(remoteCampaign.getLocation().getId()) + .build()); + } + return converted; + } + } +} From 45abc47267a7ddd117c059fd8522aa1cdb88893e Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Sun, 8 Mar 2020 20:38:53 -0400 Subject: [PATCH 04/20] Add GM Mode Changed and Location Changed handling --- MekHQ/src/main/proto/Service.proto | 22 ++++-- .../mekhq/campaign/CampaignController.java | 66 ++++++++++++---- MekHQ/src/mekhq/campaign/RemoteCampaign.java | 22 ++++++ MekHQ/src/mekhq/online/MekHQClient.java | 73 +++++++++++++++--- MekHQ/src/mekhq/online/MekHQServer.java | 77 +++++++++++++++++-- .../events/WaitingToAdvanceDayEvent.java | 7 ++ 6 files changed, 230 insertions(+), 37 deletions(-) create mode 100644 MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index cde90422d9..83c7928a05 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -24,8 +24,9 @@ message ConnectionRequest { string name = 3; string date = 4; string location = 5; + bool isGMMode = 6; oneof password_option { - string password = 6; + string password = 7; } } @@ -36,7 +37,8 @@ message ConnectionResponse { string name = 3; string date = 4; string location = 5; - repeated Campaign campaigns = 6; + bool isGMMode = 6; + repeated Campaign campaigns = 7; } message DisconnectionRequest { @@ -53,6 +55,7 @@ message Campaign { string date = 3; int32 team = 4; string location = 5; + bool isGMMode = 6; } message ClientMessage { @@ -72,20 +75,24 @@ message ServerMessage { message Ping { string date = 1; string location = 2; + bool isGMMode = 3; } message Pong { string date = 1; string location = 2; - repeated Campaign campaigns = 3; + bool isGMMode = 3; + repeated Campaign campaigns = 4; } message CampaignDateChanged { - string date = 1; + string id = 1; + string date = 2; } message GMModeChanged { - bool value = 1; + string id = 1; + bool value = 2; } message TeamAssignmentChanged { @@ -98,6 +105,7 @@ message TeamAssignment { } message LocationChanged { - string location = 1; - bool is_gm_movement = 2; + string id = 1; + string location = 2; + bool is_gm_movement = 3; } diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 995faae1a7..178dfe2e9a 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -19,27 +19,30 @@ package mekhq.campaign; import java.util.Collection; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.joda.time.DateTime; +import mekhq.MekHQ; import mekhq.campaign.universe.PlanetarySystem; import mekhq.campaign.universe.Systems; +import mekhq.online.events.WaitingToAdvanceDayEvent; /** * Manages the timeline of a {@link Campaign}. */ public class CampaignController { private final Campaign localCampaign; - private final Map remoteCampaigns = new ConcurrentHashMap<>(); - private final Map activeCampaigns = new ConcurrentHashMap<>(); + private final ConcurrentHashMap remoteCampaigns = new ConcurrentHashMap<>(); + private final ConcurrentHashMap activeCampaigns = new ConcurrentHashMap<>(); + private boolean isHost; private UUID host; private DateTime hostDate; private String hostName; private PlanetarySystem hostLocation; + private boolean hostIsGM; /** * Creates a new {@code CampaignController} for @@ -99,19 +102,55 @@ public void setHostName(String name) { hostName = name; } + public String getHostName() { + return hostName; + } + public void setHostLocation(String planetarySystemId) { hostLocation = Systems.getInstance().getSystemById(planetarySystemId); - } + } + + public PlanetarySystem getHostLocation() { + return hostLocation; + } + + public void setHostIsGMMode(boolean isGMMode) { + hostIsGM = isGMMode; + } + + public boolean getHostIsGMMode() { + return hostIsGM; + } - public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId) { + public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId, boolean isGMMode) { PlanetarySystem planetarySystem = Systems.getInstance().getSystemById(locationId); - remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem)); - } + remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem, isGMMode)); + } public Collection getRemoteCampaigns() { return remoteCampaigns.values(); } + public void setRemoteCampaignDate(UUID campaignId, DateTime campaignDate) { + // We only update this if the remote campaign is actually present + // otherwise the next PING-PONG will catch us up. + remoteCampaigns.computeIfPresent(campaignId, (key, remoteCampaign) -> remoteCampaign.withDate(campaignDate)); + } + + public void setRemoteCampaignLocation(UUID campaignId, String locationId) { + PlanetarySystem system = Systems.getInstance().getSystemById(locationId); + + // We only update this if the remote campaign is actually present + // otherwise the next PING-PONG will catch us up. + remoteCampaigns.computeIfPresent(campaignId, (key, remoteCampaign) -> remoteCampaign.withLocation(system)); + } + + public void setRemoteCampaignGMMode(UUID campaignId, boolean isGMMode) { + // We only update this if the remote campaign is actually present + // otherwise the next PING-PONG will catch us up. + remoteCampaigns.computeIfPresent(campaignId, (key, remoteCampaign) -> remoteCampaign.withGMMode(isGMMode)); + } + public void addActiveCampaign(UUID id) { activeCampaigns.put(id, id); } @@ -123,19 +162,16 @@ public void removeActiveCampaign(UUID id) { /** * Advances the local {@link Campaign} to the next day. */ - public void advanceDay() { + public boolean advanceDay() { if (isHost) { - if (getLocalCampaign().newDay()) { - // TODO: notifyDayChangedEvent(); - } + return getLocalCampaign().newDay(); } else { if (getLocalCampaign().getDateTime().isBefore(getHostDate())) { - if (getLocalCampaign().newDay()) { - // TODO: notifyDayChangedEvent - } + return getLocalCampaign().newDay(); } else { - // TODO: requestNewDay(); + MekHQ.triggerEvent(new WaitingToAdvanceDayEvent()); + return false; } } } diff --git a/MekHQ/src/mekhq/campaign/RemoteCampaign.java b/MekHQ/src/mekhq/campaign/RemoteCampaign.java index b1864fb803..0afb3c6316 100644 --- a/MekHQ/src/mekhq/campaign/RemoteCampaign.java +++ b/MekHQ/src/mekhq/campaign/RemoteCampaign.java @@ -12,12 +12,18 @@ public class RemoteCampaign { private final String name; private final DateTime date; private final PlanetarySystem location; + private final boolean isGMMode; public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location) { + this(id, name, date, location, false); + } + + public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location, boolean isGMMode) { this.id = id; this.name = name; this.date = date; this.location = location; + this.isGMMode = isGMMode; } public UUID getId() { @@ -35,4 +41,20 @@ public DateTime getDate() { public PlanetarySystem getLocation() { return location; } + + public boolean isGMMode() { + return isGMMode; + } + + public RemoteCampaign withDate(DateTime newDate) { + return new RemoteCampaign(id, name, newDate, location); + } + + public RemoteCampaign withLocation(PlanetarySystem newLocation) { + return new RemoteCampaign(id, name, date, newLocation); + } + + public RemoteCampaign withGMMode(boolean isGMMode) { + return new RemoteCampaign(id, name, date, location, isGMMode); + } } diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index 5208020b23..ec439d898a 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -3,7 +3,6 @@ import java.util.ResourceBundle; import java.util.UUID; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -25,6 +24,8 @@ import megamek.common.event.Subscribe; import mekhq.MekHQ; import mekhq.campaign.CampaignController; +import mekhq.campaign.event.GMModeEvent; +import mekhq.campaign.event.LocationChangedEvent; import mekhq.campaign.event.NewDayEvent; import mekhq.online.MekHQHostGrpc.MekHQHostBlockingStub; import mekhq.online.MekHQHostGrpc.MekHQHostStub; @@ -111,9 +112,9 @@ public void onNext(ServerMessage message) { } else if (payload.is(CampaignDateChanged.class)) { handleCampaignDateChanged(id, payload.unpack(CampaignDateChanged.class)); } else if (payload.is(GMModeChanged.class)) { - + handleGMModeChanged(id, payload.unpack(GMModeChanged.class)); } else if (payload.is(LocationChanged.class)) { - + handleLocationChanged(id, payload.unpack(LocationChanged.class)); } } catch (InvalidProtocolBufferException e) { MekHQ.getLogger().error(MekHQClient.class, "messageBus::onNext()", "RPC protocol error: " + e.getMessage(), e); @@ -134,12 +135,6 @@ public void onCompleted() { }); } - protected void handleCampaignDateChanged(UUID id, CampaignDateChanged dateChanged) { - String date = dateChanged.getDate(); - controller.setHostDate(dateFormatter.parseDateTime(date)); - MekHQ.getLogger().info(MekHQClient.class, "handleCampaignDateChanged()", String.format("<- HOST CampaignDateChanged: %s", date)); - } - protected void handleRpcException(Throwable t) { messageBus.onError(Status.INTERNAL .withDescription(t.getMessage()) @@ -176,22 +171,80 @@ protected void handlePong(UUID id, Pong pong) { controller.setHostDate(hostDate); controller.setHostLocation(locationId); + controller.setHostIsGMMode(pong.getIsGMMode()); for (Campaign campaign : pong.getCampaignsList()) { controller.addRemoteCampaign(UUID.fromString(campaign.getId()), campaign.getName(), - dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation()); + dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); + } + } + + protected void handleCampaignDateChanged(UUID hostId, CampaignDateChanged dateChanged) { + String date = dateChanged.getDate(); + DateTime campaignDate = dateFormatter.parseDateTime(date); + UUID campaignId = UUID.fromString(dateChanged.getId()); + if (hostId.equals(campaignId)) { + controller.setHostDate(campaignDate); + MekHQ.getLogger().info(MekHQClient.class, "handleCampaignDateChanged()", String.format("<- HOST CampaignDateChanged: %s", date)); + } else { + controller.setRemoteCampaignDate(campaignId, campaignDate); + } + } + + protected void handleGMModeChanged(UUID hostId, GMModeChanged gmModeChanged) { + boolean isGMMode = gmModeChanged.getValue(); + UUID campaignId = UUID.fromString(gmModeChanged.getId()); + if (hostId.equals(campaignId)) { + controller.setHostIsGMMode(isGMMode); + MekHQ.getLogger().info(MekHQClient.class, "handleGMModeChanged()", String.format("<- HOST GMModeChanged: %s", isGMMode ? "ON" : "OFF")); + } else { + controller.setRemoteCampaignGMMode(campaignId, isGMMode); + } + } + + protected void handleLocationChanged(UUID hostId, LocationChanged locationChanged) { + String locationId = locationChanged.getLocation(); + boolean isGM = locationChanged.getIsGmMovement(); + UUID campaignId = UUID.fromString(locationChanged.getId()); + if (hostId.equals(campaignId)) { + controller.setHostLocation(locationId); + MekHQ.getLogger().info(MekHQClient.class, "handleLocationChanged()", String.format("<- HOST LocationChanged: %s (%s)", locationId, isGM ? "GM Moved" : "KF Jump")); + } else { + controller.setRemoteCampaignLocation(campaignId, locationId); } } @Subscribe public void handle(NewDayEvent evt) { CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() + .setId(getCampaign().getId().toString()) .setDate(dateFormatter.print(getCampaign().getDateTime())) .build(); messageBus.onNext(buildMessage(dateChanged)); } + @Subscribe + public void handle(GMModeEvent evt) { + GMModeChanged gmModeChanged = GMModeChanged.newBuilder() + .setId(getCampaign().getId().toString()) + .setValue(evt.isGMMode()) + .build(); + + messageBus.onNext(buildMessage(gmModeChanged)); + } + + @Subscribe + public void handle(LocationChangedEvent evt) { + LocationChanged locationChanged = LocationChanged.newBuilder() + .setId(getCampaign().getId().toString()) + .setLocation(evt.getLocation().getCurrentSystem().getId()) + .setIsGmMovement(!evt.isKFJump()) + .build(); + + messageBus.onNext(buildMessage(locationChanged)); + } + private ClientMessage buildMessage(Message payload) { return ClientMessage.newBuilder() .setTimestamp(getTimestamp()) diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index ddd2f7e908..d3cd52504f 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -21,7 +21,6 @@ import com.google.protobuf.Timestamp; import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; @@ -33,6 +32,8 @@ import mekhq.MekHQ; import mekhq.campaign.CampaignController; import mekhq.campaign.RemoteCampaign; +import mekhq.campaign.event.GMModeEvent; +import mekhq.campaign.event.LocationChangedEvent; import mekhq.campaign.event.NewDayEvent; import mekhq.online.MekHQHostGrpc; @@ -102,6 +103,16 @@ public void handle(NewDayEvent evt) { service.notifyDayChanged(); } + @Subscribe + public void handle(GMModeEvent evt) { + service.notifyGMModeChanged(evt.isGMMode()); + } + + @Subscribe + public void handle(LocationChangedEvent evt) { + service.notifyLocationChanged(evt.getLocation().getCurrentSystem().getId(), !evt.isKFJump()); + } + private static class MekHQHostService extends MekHQHostGrpc.MekHQHostImplBase { private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); private final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.MekHQ"); @@ -143,12 +154,13 @@ public void connect(ConnectionRequest request, StreamObserver responseObserver, UUID cam MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, pong.getDate(), pong.getLocation())); } + protected void handleCampaignDateChanged(StreamObserver responseObserver, UUID clientId, + CampaignDateChanged campaignDateChanged) { + + controller.setRemoteCampaignDate(clientId, dateFormatter.parseDateTime(campaignDateChanged.getDate())); + + sendToAllExcept(clientId, campaignDateChanged); + } + + protected void handleGMModeChanged( + StreamObserver responseObserver, + UUID clientId, + GMModeChanged gmModeChanged) { + + controller.setRemoteCampaignGMMode(clientId, gmModeChanged.getValue()); + + sendToAllExcept(clientId, gmModeChanged); + } + + protected void handleLocationChanged(StreamObserver responseObserver, + UUID clientId, + LocationChanged locationChanged) { + + controller.setRemoteCampaignLocation(clientId, locationChanged.getLocation()); + + sendToAllExcept(clientId, locationChanged); + } + public void notifyDayChanged() { CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() + .setId(getCampaign().getId().toString()) .setDate(dateFormatter.print(getCampaign().getDateTime())) .build(); sendToAll(dateChanged); } + public void notifyGMModeChanged(boolean gmMode) { + GMModeChanged gmModeChanged = GMModeChanged.newBuilder() + .setId(getCampaign().getId().toString()) + .setValue(gmMode) + .build(); + + sendToAll(gmModeChanged); + } + + public void notifyLocationChanged(String locationId, boolean isGMMovement) { + LocationChanged locationChanged = LocationChanged.newBuilder() + .setId(getCampaign().getId().toString()) + .setLocation(locationId) + .setIsGmMovement(isGMMovement) + .build(); + + sendToAll(locationChanged); + } + private void sendToAll(Message message) { for (Map.Entry> client : messageBus.entrySet()) { client.getValue().onNext(buildResponse(message)); } } + private void sendToAllExcept(UUID exceptId, Message message) { + for (Map.Entry> client : messageBus.entrySet()) { + if (!client.getKey().equals(exceptId)) { + client.getValue().onNext(buildResponse(message)); + } + } + } + private ServerMessage buildResponse(Message payload) { return ServerMessage.newBuilder() .setTimestamp(getTimestamp()) diff --git a/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java b/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java new file mode 100644 index 0000000000..3051a1a488 --- /dev/null +++ b/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java @@ -0,0 +1,7 @@ +package mekhq.online.events; + +import megamek.common.event.MMEvent; + +public class WaitingToAdvanceDayEvent extends MMEvent { + +} From 728590a5a9e59270df0187697cf3af33bddd5134 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Mon, 9 Mar 2020 14:56:38 -0400 Subject: [PATCH 05/20] Add missing copyright headers and documentation --- MekHQ/src/main/proto/Service.proto | 185 +++++++++++++++++- MekHQ/src/mekhq/campaign/RemoteCampaign.java | 18 ++ MekHQ/src/mekhq/online/MekHQClient.java | 18 ++ MekHQ/src/mekhq/online/MekHQServer.java | 18 ++ .../events/WaitingToAdvanceDayEvent.java | 18 ++ 5 files changed, 255 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 83c7928a05..59d1fbbc5a 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ syntax = "proto3"; option java_multiple_files = true; @@ -9,103 +27,266 @@ import "google/protobuf/timestamp.proto"; package mekhqonline; +// The MekHQHost Service handles distributed MekHQ play +// between a Host Campaign and one or more Client Campaigns. service MekHQHost { + // + // Initiates a connection to the Host Campaign. + // rpc Connect(ConnectionRequest) returns (ConnectionResponse) {} + // + // Advises the Host Campaign that a Client Campaign + // is no longer participating in the session. + // rpc Disconnect(DisconnectionRequest) returns (DisconnectionResponse) {} + // + // Initiates a Message Bus between the Client Campaign + // and Host Campaign. + // rpc MessageBus(stream ClientMessage) returns (stream ServerMessage) {} } -// The request message containing the user's name. +// Represents a request to initiate a connection to a host +// campaign. message ConnectionRequest { + // The unique identifier of the Client Campaign. string id = 1; + + // The version of MekHQ on the Client machine. string version = 2; + + // The name of the Client Campaign. string name = 3; + + // The date of the Client Campaign (ISO8601). string date = 4; + + // The location of the Client Campaign. string location = 5; + + // A value indicating whether or not the Client Campaign + // is operating under GM Mode. bool isGMMode = 6; + + // An optional password to use during connection attempts. oneof password_option { + // The password required to initiate the connection. string password = 7; } } -// The response message containing the greetings +// Represents a response to a successful connection attempt +// containing details of the Host Campaign. message ConnectionResponse { + // The unique identifier of the Host Campaign. string id = 1; + + // The version of MekHQ on the Host machine. string version = 2; + + // The name of the Host Campaign. string name = 3; + + // The date of the Host Campaign (ISO8601). string date = 4; + + // The location of the Host Campaign. string location = 5; + + // A value indicating whether or not the Host Campaign + // is operating under GM Mode. bool isGMMode = 6; + + // Zero or more Campaign messages representing the + // Client Campaigns involved in the session. + // + // These campaigns may or may not be active. repeated Campaign campaigns = 7; } +// Represents a request to disconnect from the Host Campaign. message DisconnectionRequest { + // The unique identifier of the Client Campaign. string id = 1; } +// Represents a response to a successful disconnection attempt. message DisconnectionResponse { + // The unique identifier of the Host Campaign. string id = 1; } +// Represents the details of a specific Campaign involved +// in the session. message Campaign { + // The unique identifier of the Campaign. string id = 1; + + // The name of the Campaign. string name = 2; + + // The date of the Campaign (ISO8601) string date = 3; + + // The team number of the Campaign. + // NOTE: The Host Campaign is always team 1. int32 team = 4; + + // The location of the Campaign. string location = 5; + + // A value indicating whether or not the Campaign + // is operating under GM Mode. bool isGMMode = 6; } +// Represents a message from a Client Campaign +// to the Host Campaign over the message bus. message ClientMessage { + // The time when the message was generated. google.protobuf.Timestamp timestamp = 1; + + // The unique identifier of the Client Campaign. string id = 2; + + // An optional map of metadata associated + // with this message. map metadata = 3; + + // The eveloped message being sent to the Host Campaign. google.protobuf.Any message = 4; } +// Represents a message from the Host Campaign +// to a Client Campaign over the message bus. message ServerMessage { + // The time when the message was generated. google.protobuf.Timestamp timestamp = 1; + + // The unique identifier of the Host Campaign. string id = 2; + + // An optional map of metadata associated + // with this message. map metadata = 3; + + // The enveloped message being sent to a Client Campaign. google.protobuf.Any message = 4; } +// Represents a PING being sent to keep-alive +// the connection between a Host and Client Campaign. +// +// This message can be sent by either the Host or +// a Client Campaign. +// +// Any Campaign which receives a Ping message should +// respond as soon as possible with a Pong message. message Ping { + // The date of the Campaign sending the message (ISO8601). string date = 1; + + // The location of the Campaign. string location = 2; + + // A value indicating whether or not the Campaign + // is operating under GM Mode. bool isGMMode = 3; } +// Represents a PONG being sent in response to +// a PING message. +// +// This message can be sent by either the Host or +// a Client Campaign, but only in response to a +// previous PING message. +// +// Only the Host Campaign sends along data about +// the other campaigns in the session, and should +// ignore any data in the `campaigns` field. message Pong { + // The date of the Campaign sending the message (ISO8601). string date = 1; + + // The location of the Campaign. string location = 2; + + // A value indicating whether or not the Campaign + // is operating under GM Mode. bool isGMMode = 3; + + // Zero or more Campaign messages representing the + // Client Campaigns involved in the session. + // + // These campaigns may or may not be active. repeated Campaign campaigns = 4; } +// Represents a message sent when a Campaign changes +// dates. +// +// This message may be sent by any Campaign, and will +// be forwarded by the Host Campaign to everyone in +// the current session. message CampaignDateChanged { + // The unique identifier of the Campaign. string id = 1; + + // The new date of the Campaign (ISO8601). string date = 2; } +// Represents a message sent when a Campaign changes +// its GM Mode. +// +// This message may be sent by any Campaign, and will +// be forwarded by the Host Campaign to everyone in +// the current session. message GMModeChanged { + // The unique identifier of the Campaign. string id = 1; + + // A value indicating whether or not the Campaign + // is operating under GM Mode. bool value = 2; } +// Represents a message sent when Team Assignments change. +// +// This message is only sent by the Host Campaign to the +// connected Client Campaigns. message TeamAssignmentChanged { + // Zero or more TeamAssignment messages detailing + // which team each Campaign is assigned to. + // + // If no TeamAssignment messages are sent, then every + // Campaign is on its own team. repeated TeamAssignment teams = 1; } +// Represents a message detailing a specific Team Assignment. message TeamAssignment { + // The unique identifier of the Campaign. string id = 1; + + // The team number of the Campaign. + // + // NOTE: The Host Campaign is always on team 1. int32 team = 2; } +// Represents a message sent when a Campaign changes locations, +// either due to a KF Jump or due to GM movement. message LocationChanged { + // The unique identifier of the Campaign. string id = 1; + + // The new location of the Campaign. string location = 2; + + // A value indicating whether or not the movement + // was due to a GM action. bool is_gm_movement = 3; } diff --git a/MekHQ/src/mekhq/campaign/RemoteCampaign.java b/MekHQ/src/mekhq/campaign/RemoteCampaign.java index 0afb3c6316..8571bea470 100644 --- a/MekHQ/src/mekhq/campaign/RemoteCampaign.java +++ b/MekHQ/src/mekhq/campaign/RemoteCampaign.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ package mekhq.campaign; import java.util.UUID; diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index ec439d898a..da6016257c 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ package mekhq.online; import java.util.ResourceBundle; diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index d3cd52504f..ab8240674b 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ package mekhq.online; import java.io.IOException; diff --git a/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java b/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java index 3051a1a488..7248647c31 100644 --- a/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java +++ b/MekHQ/src/mekhq/online/events/WaitingToAdvanceDayEvent.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ package mekhq.online.events; import megamek.common.event.MMEvent; From 1c65538ff8c93cceb63e2e9b0c63f7334b8eae6a Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Mon, 9 Mar 2020 15:12:57 -0400 Subject: [PATCH 06/20] Simplify the protocol w.r.t. sending campaign details --- MekHQ/src/main/proto/Service.proto | 74 ++++++------------------- MekHQ/src/mekhq/online/MekHQClient.java | 49 ++++++++++------ MekHQ/src/mekhq/online/MekHQServer.java | 41 ++++++++------ 3 files changed, 74 insertions(+), 90 deletions(-) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 59d1fbbc5a..d4d110f032 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -51,59 +51,33 @@ service MekHQHost { // Represents a request to initiate a connection to a host // campaign. message ConnectionRequest { - // The unique identifier of the Client Campaign. - string id = 1; - // The version of MekHQ on the Client machine. - string version = 2; - - // The name of the Client Campaign. - string name = 3; + string version = 1; - // The date of the Client Campaign (ISO8601). - string date = 4; - - // The location of the Client Campaign. - string location = 5; - - // A value indicating whether or not the Client Campaign - // is operating under GM Mode. - bool isGMMode = 6; + // A CampaignDetails message with details of the Client Campaign. + CampaignDetails client = 2; // An optional password to use during connection attempts. oneof password_option { // The password required to initiate the connection. - string password = 7; + string password = 3; } } // Represents a response to a successful connection attempt // containing details of the Host Campaign. message ConnectionResponse { - // The unique identifier of the Host Campaign. - string id = 1; - // The version of MekHQ on the Host machine. - string version = 2; - - // The name of the Host Campaign. - string name = 3; - - // The date of the Host Campaign (ISO8601). - string date = 4; - - // The location of the Host Campaign. - string location = 5; + string version = 1; - // A value indicating whether or not the Host Campaign - // is operating under GM Mode. - bool isGMMode = 6; + // A CampaignDetails message with details of the Host Campaign. + CampaignDetails host = 2; - // Zero or more Campaign messages representing the + // Zero or more CampaignDetails messages representing the // Client Campaigns involved in the session. // // These campaigns may or may not be active. - repeated Campaign campaigns = 7; + repeated CampaignDetails campaigns = 3; } // Represents a request to disconnect from the Host Campaign. @@ -120,7 +94,7 @@ message DisconnectionResponse { // Represents the details of a specific Campaign involved // in the session. -message Campaign { +message CampaignDetails { // The unique identifier of the Campaign. string id = 1; @@ -185,15 +159,9 @@ message ServerMessage { // Any Campaign which receives a Ping message should // respond as soon as possible with a Pong message. message Ping { - // The date of the Campaign sending the message (ISO8601). - string date = 1; - - // The location of the Campaign. - string location = 2; - - // A value indicating whether or not the Campaign - // is operating under GM Mode. - bool isGMMode = 3; + // A CampaignDetails message which contains details + // from the sending campaign. + CampaignDetails campaign = 1; } // Represents a PONG being sent in response to @@ -207,21 +175,15 @@ message Ping { // the other campaigns in the session, and should // ignore any data in the `campaigns` field. message Pong { - // The date of the Campaign sending the message (ISO8601). - string date = 1; - - // The location of the Campaign. - string location = 2; - - // A value indicating whether or not the Campaign - // is operating under GM Mode. - bool isGMMode = 3; + // A CampaignDetails message which contains details + // from the sending campaign. + CampaignDetails campaign = 1; - // Zero or more Campaign messages representing the + // Zero or more CampaignDetails messages representing the // Client Campaigns involved in the session. // // These campaigns may or may not be active. - repeated Campaign campaigns = 4; + repeated CampaignDetails campaigns = 2; } // Represents a message sent when a Campaign changes diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index da6016257c..c5b96226ae 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -71,11 +71,20 @@ protected mekhq.campaign.Campaign getCampaign() { return controller.getLocalCampaign(); } + protected CampaignDetails getCampaignDetails() { + return CampaignDetails.newBuilder() + .setId(getCampaign().getId().toString()) + .setName(getCampaign().getName()) + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + } + public void connect() { - ConnectionRequest request = ConnectionRequest.newBuilder().setId(getCampaign().getId().toString()) - .setName(getCampaign().getName()).setVersion(resourceMap.getString("Application.version")) - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + + ConnectionRequest request = ConnectionRequest.newBuilder() + .setVersion(resourceMap.getString("Application.version")) + .setClient(getCampaignDetails()) + .build(); ConnectionResponse response; try { @@ -85,13 +94,15 @@ public void connect() { return; } + CampaignDetails hostCampaign = response.getHost(); + MekHQ.getLogger().info(MekHQClient.class, "connect()", - "Connected to Campaign: " + response.getId() + " " + response.getDate()); + "Connected to Campaign: " + hostCampaign.getId() + " " + hostCampaign.getDate()); - controller.setHost(UUID.fromString(response.getId())); - controller.setHostName(response.getName()); - controller.setHostDate(DateTime.parse(response.getDate(), dateFormatter)); - controller.setHostLocation(response.getLocation()); + controller.setHost(UUID.fromString(hostCampaign.getId())); + controller.setHostName(hostCampaign.getName()); + controller.setHostDate(DateTime.parse(hostCampaign.getDate(), dateFormatter)); + controller.setHostLocation(hostCampaign.getLocation()); createMessageBus(); @@ -163,35 +174,37 @@ protected void handleRpcException(Throwable t) { private void sendPing() { Ping ping = Ping.newBuilder() - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setCampaign(getCampaignDetails()) .build(); messageBus.onNext(buildMessage(ping)); } protected void handlePing(UUID id, Ping ping) { Pong pong = Pong.newBuilder() - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setCampaign(getCampaignDetails()) .build(); - MekHQ.getLogger().info(MekHQClient.class, "handlePing()", String.format("-> PING: %s %s %s", id, ping.getDate(), ping.getLocation())); + CampaignDetails hostCampaign = ping.getCampaign(); + + MekHQ.getLogger().info(MekHQClient.class, "handlePing()", String.format("-> PING: %s %s %s", id, hostCampaign.getDate(), hostCampaign.getLocation())); messageBus.onNext(buildMessage(pong)); } protected void handlePong(UUID id, Pong pong) { - String date = pong.getDate(); + CampaignDetails hostCampaign = pong.getCampaign(); + + String date = hostCampaign.getDate(); DateTime hostDate = dateFormatter.parseDateTime(date); - String locationId = pong.getLocation(); + String locationId = hostCampaign.getLocation(); MekHQ.getLogger().info(MekHQClient.class, "handlePong()", String.format("-> PONG: %s %s %s (%d connected)", id, hostDate, locationId, pong.getCampaignsCount())); controller.setHostDate(hostDate); controller.setHostLocation(locationId); - controller.setHostIsGMMode(pong.getIsGMMode()); + controller.setHostIsGMMode(hostCampaign.getIsGMMode()); - for (Campaign campaign : pong.getCampaignsList()) { + for (CampaignDetails campaign : pong.getCampaignsList()) { controller.addRemoteCampaign(UUID.fromString(campaign.getId()), campaign.getName(), dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); } diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index ab8240674b..479f5c40ce 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -148,6 +148,14 @@ private mekhq.campaign.Campaign getCampaign() { return controller.getLocalCampaign(); } + private CampaignDetails getCampaignDetails() { + return CampaignDetails.newBuilder() + .setId(getCampaign().getId().toString()) + .setName(getCampaign().getName()) + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + } + @Override public void connect(ConnectionRequest request, StreamObserver responseObserver) { String version = resourceMap.getString("Application.version"); @@ -159,7 +167,9 @@ public void connect(ConnectionRequest request, StreamObserver res public void sendPings() { Ping ping = Ping.newBuilder() - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setCampaign(getCampaignDetails()) .build(); for (Map.Entry> client : messageBus.entrySet()) { MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", "<- PING: " + client.getKey()); @@ -264,11 +270,12 @@ public void sendPings() { private void handlePing(StreamObserver responseObserver, UUID campaignId, Ping ping) { addMessageBus(campaignId, responseObserver); - MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PING: %s %s %s", campaignId, ping.getDate(), ping.getLocation())); + CampaignDetails clientCampaign = ping.getCampaign(); + + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PING: %s %s %s", campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); Pong pong = Pong.newBuilder() - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setCampaign(getCampaignDetails()) .addAllCampaigns(convert(controller.getRemoteCampaigns())) .build(); responseObserver.onNext(buildResponse(pong)); @@ -277,7 +284,9 @@ private void handlePing(StreamObserver responseObserver, UUID cam private void handlePong(StreamObserver responseObserver, UUID campaignId, Pong pong) { outstandingPings.remove(campaignId); - MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, pong.getDate(), pong.getLocation())); + CampaignDetails clientCampaign = pong.getCampaign(); + + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); } protected void handleCampaignDateChanged(StreamObserver responseObserver, UUID clientId, @@ -364,11 +373,11 @@ private static Timestamp getTimestamp() { .build(); } - private Collection convert(Collection remoteCampaigns) { - List converted = new ArrayList<>(); + private Collection convert(Collection remoteCampaigns) { + List converted = new ArrayList<>(); for (RemoteCampaign remoteCampaign : remoteCampaigns) { converted.add( - Campaign.newBuilder() + CampaignDetails.newBuilder() .setId(remoteCampaign.getId().toString()) .setName(remoteCampaign.getName()) .setDate(dateFormatter.print(remoteCampaign.getDate())) From e1aae03facd7312cf846bab88a2b3fda43c2f981 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Mon, 9 Mar 2020 15:22:26 -0400 Subject: [PATCH 07/20] Allow changing the hosting port and host address --- MekHQ/src/mekhq/MekHQ.java | 56 +++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java index 0b964b2b86..d1802ce70d 100644 --- a/MekHQ/src/mekhq/MekHQ.java +++ b/MekHQ/src/mekhq/MekHQ.java @@ -39,6 +39,7 @@ import megamek.MegaMek; import megamek.client.Client; +import megamek.common.annotations.Nullable; import megamek.common.event.EventBus; import megamek.common.event.GameBoardChangeEvent; import megamek.common.event.GameBoardNewEvent; @@ -137,8 +138,13 @@ public class MekHQ implements GameListener { private final ResourceBundle resourceBundle = ResourceBundle.getBundle("mekhq.resources.MekHQ"); + /** A value indicating whether or not MekHQ is hosting an online session. */ private boolean isHost; - private boolean isRemote; + /** The port to use when hosting the online session. */ + private int hostPort = 3000; + /** The address of the MekHQ instance to connect to (i.e. hostname:port) */ + private String remoteHost; + private MekHQServer onlineServer; private MekHQClient onlineClient; private ManagedChannel channel; @@ -305,11 +311,21 @@ protected void startup(String[] args) { UIManager.installLookAndFeel("Flat Darcula", "com.formdev.flatlaf.FlatDarculaLaf"); for (String arg : args) { - if ("--host".equalsIgnoreCase(arg)) { + if (arg.startsWith("--host")) { setIsHosting(true); - } - else if ("--connect".equalsIgnoreCase(arg)) { - setIsConnecting(true); + int equals = arg.indexOf('='); + if (equals > 0) { + setHostPort(Integer.parseInt(arg.substring(equals + 1))); + } + } else if (arg.startsWith("--connect")) { + int equals = arg.indexOf('='); + if (equals < 0) { + // for testing we're defaulting to localhost:3000 + // this will go away outside of prototyping + setHostLocation("localhost:3000"); + } else { + setHostLocation(arg.substring(equals + 1)); + } } } @@ -329,12 +345,12 @@ public String getVersion() { return resourceBundle.getString("Application.version"); } - private void setIsConnecting(boolean b) { - isRemote = b; + private void setHostLocation(@Nullable String host) { + remoteHost = host; } public boolean isRemote() { - return isRemote; + return remoteHost != null; } private void setIsHosting(boolean b) { @@ -345,6 +361,14 @@ public boolean isHosting() { return isHost; } + public void setHostPort(int port) { + hostPort = port; + } + + public int getHostPort() { + return hostPort; + } + private void setUserPreferences() { PreferencesNode preferences = getPreferences().forClass(MekHQ.class); @@ -397,16 +421,16 @@ public void exit() { public void showNewView() { try { - if (isHosting()) { - onlineServer = new MekHQServer(3000, campaignController); + if (isHosting()) { + onlineServer = new MekHQServer(hostPort, campaignController); - onlineServer.start(); - } else if (isRemote()) { - channel = ManagedChannelBuilder.forTarget("localhost:3000").usePlaintext().build(); - onlineClient = new MekHQClient(channel, campaignController); + onlineServer.start(); + } else if (isRemote()) { + channel = ManagedChannelBuilder.forTarget(remoteHost).usePlaintext().build(); + onlineClient = new MekHQClient(channel, campaignController); - onlineClient.connect(); - } + onlineClient.connect(); + } } catch (IOException ex) { MekHQ.getLogger().error(MekHQ.class, "showNewView()", "Could not connect to server", ex); } From 107ebf0b5b908ee7b75443c9b0167614af353123 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Mon, 9 Mar 2020 15:32:41 -0400 Subject: [PATCH 08/20] Disconnect existing sessions when loading a campaign --- MekHQ/src/mekhq/MekHQ.java | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java index d1802ce70d..0646d3dc30 100644 --- a/MekHQ/src/mekhq/MekHQ.java +++ b/MekHQ/src/mekhq/MekHQ.java @@ -420,6 +420,20 @@ public void exit() { } public void showNewView() { + + // Start up the host server or connect to a remote host + // if indicated. + connectDistributedFeatures(); + + campaigngui = new CampaignGUI(this); + campaigngui.showOverviewTab(getCampaign().isOverviewLoadingValue()); + } + + private void connectDistributedFeatures() { + + // Close any existing connections. + disconnectDistributedFeatures(); + try { if (isHosting()) { onlineServer = new MekHQServer(hostPort, campaignController); @@ -432,11 +446,27 @@ public void showNewView() { onlineClient.connect(); } } catch (IOException ex) { - MekHQ.getLogger().error(MekHQ.class, "showNewView()", "Could not connect to server", ex); + MekHQ.getLogger().error(MekHQ.class, "showNewView()", "Could not connect to server.", ex); + + disconnectDistributedFeatures(); } + } - campaigngui = new CampaignGUI(this); - campaigngui.showOverviewTab(getCampaign().isOverviewLoadingValue()); + private void disconnectDistributedFeatures() { + try { + if (onlineServer != null) { + onlineServer.stop(); + } else if (channel != null) { + try { + onlineClient.disconnect(); + } catch (Exception ex) { + MekHQ.getLogger().error(MekHQ.class, "disconnectDistributedFeatures()", "Could not disconnect from server.", ex); + } + channel.shutdownNow(); + } + } catch (Exception ex) { + MekHQ.getLogger().error(MekHQ.class, "disconnectDistributedFeatures()", "Could not shut down existing online features.", ex); + } } /** From f783493b508a61ef3121bb4b305fa3511b430a6a Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Tue, 10 Mar 2020 22:36:42 -0400 Subject: [PATCH 09/20] Identify actively playing campaigns --- MekHQ/src/main/proto/Service.proto | 4 ++ .../mekhq/campaign/CampaignController.java | 11 +++ MekHQ/src/mekhq/gui/CampaignGUI.java | 7 ++ MekHQ/src/mekhq/online/MekHQClient.java | 32 ++++++++- MekHQ/src/mekhq/online/MekHQServer.java | 68 +++++++++++++++++-- .../online/events/CampaignConnectedEvent.java | 17 +++++ .../events/CampaignDisconnectedEvent.java | 17 +++++ .../events/CampaignListUpdatedEvent.java | 6 ++ 8 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 MekHQ/src/mekhq/online/events/CampaignConnectedEvent.java create mode 100644 MekHQ/src/mekhq/online/events/CampaignDisconnectedEvent.java create mode 100644 MekHQ/src/mekhq/online/events/CampaignListUpdatedEvent.java diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index d4d110f032..423f5c3d2a 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -114,6 +114,10 @@ message CampaignDetails { // A value indicating whether or not the Campaign // is operating under GM Mode. bool isGMMode = 6; + + // A value indicating whether or not the Campaign + // is actively playing in this session. + bool is_active = 7; } // Represents a message from a Client Campaign diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 178dfe2e9a..96487eeaa9 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -19,6 +19,9 @@ package mekhq.campaign; import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -159,6 +162,14 @@ public void removeActiveCampaign(UUID id) { activeCampaigns.remove(id); } + public boolean isCampaignActive(UUID id) { + return Objects.equals(getHost(), id) || activeCampaigns.containsKey(id); + } + + public Set getActiveCampaigns() { + return Collections.unmodifiableSet(activeCampaigns.keySet()); + } + /** * Advances the local {@link Campaign} to the next day. */ diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index d5475770cd..e44f326d26 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -107,6 +107,7 @@ import mekhq.campaign.universe.RandomFactionGenerator; import mekhq.gui.model.PartsTableModel; import mekhq.io.FileType; +import mekhq.online.events.CampaignListUpdatedEvent; /** * The application's main frame. @@ -2630,6 +2631,12 @@ public void handleLocationChanged(LocationChangedEvent ev) { refreshLocation(); } + @Subscribe + public void handle(CampaignListUpdatedEvent ev) { + MekHQ.getLogger().info(CampaignGUI.class, "handle(CampaignListUpdatedEvent)", + "There are " + (getCampaignController().getRemoteCampaigns().size() + 1) + " campaigns playing, " + (getCampaignController().getActiveCampaigns().size() + 1) + " are active"); + } + public void refreshLocation() { lblLocation.setText(getCampaign().getLocation().getReport( getCampaign().getCalendar().getTime())); diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index c5b96226ae..147e1a1e41 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -18,7 +18,9 @@ */ package mekhq.online; +import java.util.HashSet; import java.util.ResourceBundle; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -47,6 +49,7 @@ import mekhq.campaign.event.NewDayEvent; import mekhq.online.MekHQHostGrpc.MekHQHostBlockingStub; import mekhq.online.MekHQHostGrpc.MekHQHostStub; +import mekhq.online.events.CampaignListUpdatedEvent; public class MekHQClient { private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); @@ -61,6 +64,8 @@ public class MekHQClient { private ScheduledExecutorService pingExecutor; private ScheduledFuture pings; + private final Set knownCampaigns = new HashSet<>(); + public MekHQClient(Channel channel, CampaignController controller) { blockingStub = MekHQHostGrpc.newBlockingStub(channel); asyncStub = MekHQHostGrpc.newStub(channel); @@ -101,9 +106,20 @@ public void connect() { controller.setHost(UUID.fromString(hostCampaign.getId())); controller.setHostName(hostCampaign.getName()); - controller.setHostDate(DateTime.parse(hostCampaign.getDate(), dateFormatter)); + controller.setHostDate(dateFormatter.parseDateTime(hostCampaign.getDate())); controller.setHostLocation(hostCampaign.getLocation()); + knownCampaigns.add(controller.getHost()); + for (CampaignDetails details : response.getCampaignsList()) { + UUID clientId = UUID.fromString(details.getId()); + + knownCampaigns.add(clientId); + + controller.addRemoteCampaign(clientId, details.getName(), + dateFormatter.parseDateTime(details.getDate()), details.getLocation(), details.getIsGMMode()); + } + + createMessageBus(); pingExecutor = Executors.newSingleThreadScheduledExecutor(); @@ -204,10 +220,22 @@ protected void handlePong(UUID id, Pong pong) { controller.setHostLocation(locationId); controller.setHostIsGMMode(hostCampaign.getIsGMMode()); + Set foundCampaigns = new HashSet<>(); + foundCampaigns.add(id); + for (CampaignDetails campaign : pong.getCampaignsList()) { - controller.addRemoteCampaign(UUID.fromString(campaign.getId()), campaign.getName(), + UUID clientId = UUID.fromString(campaign.getId()); + + foundCampaigns.add(clientId); + + controller.addRemoteCampaign(clientId, campaign.getName(), dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); } + + // Only kick off a notification if we found any new campaigns + if (!knownCampaigns.equals(foundCampaigns)) { + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); + } } protected void handleCampaignDateChanged(UUID hostId, CampaignDateChanged dateChanged) { diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index 479f5c40ce..7c468a49d6 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -54,6 +54,9 @@ import mekhq.campaign.event.LocationChangedEvent; import mekhq.campaign.event.NewDayEvent; import mekhq.online.MekHQHostGrpc; +import mekhq.online.events.CampaignConnectedEvent; +import mekhq.online.events.CampaignDisconnectedEvent; +import mekhq.online.events.CampaignListUpdatedEvent; public class MekHQServer { private final int port; @@ -153,7 +156,9 @@ private CampaignDetails getCampaignDetails() { .setId(getCampaign().getId().toString()) .setName(getCampaign().getName()) .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setIsActive(true) + .build(); } @Override @@ -188,6 +193,9 @@ public void connect(ConnectionRequest request, StreamObserver respon messageBus.putIfAbsent(Objects.requireNonNull(campaignId), responseObserver); } - private void removeMessageBus(UUID campaignId, StreamObserver responseObserver) { - messageBus.remove(campaignId); + private void handleDisconnection(UUID campaignId) { + StreamObserver existingMessageBus = messageBus.remove(campaignId); + if (existingMessageBus != null) { + existingMessageBus.onCompleted(); + } + + MekHQ.triggerEvent(new CampaignDisconnectedEvent(campaignId)); } public void sendPings() { Ping ping = Ping.newBuilder() .setCampaign(getCampaignDetails()) .build(); + + List toRemove = new ArrayList<>(); for (Map.Entry> client : messageBus.entrySet()) { - MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", "<- PING: " + client.getKey()); - client.getValue().onNext(buildResponse(Ping.newBuilder(ping).build())); + UUID clientId = client.getKey(); + + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", "<- PING: " + clientId); + + // If we have a PING without a PONG, this client is no longer active. + if (null != outstandingPings.put(clientId, clientId)) { + controller.removeActiveCampaign(clientId); + } + + try { + client.getValue().onNext(buildResponse(Ping.newBuilder(ping).build())); + } catch (Exception e) { + MekHQ.getLogger().error(MekHQHostService.class, "handlePing()", "Failed to ping campaign " + clientId, e); + toRemove.add(clientId); + } + } + + for (UUID disconnectedCampaignId : toRemove) { + handleDisconnection(disconnectedCampaignId); + } + + if (!toRemove.isEmpty()) { + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } } @@ -382,6 +434,8 @@ private Collection convert(Collection remoteCam .setName(remoteCampaign.getName()) .setDate(dateFormatter.print(remoteCampaign.getDate())) .setLocation(remoteCampaign.getLocation().getId()) + .setIsGMMode(remoteCampaign.isGMMode()) + .setIsActive(controller.isCampaignActive(remoteCampaign.getId())) .build()); } return converted; diff --git a/MekHQ/src/mekhq/online/events/CampaignConnectedEvent.java b/MekHQ/src/mekhq/online/events/CampaignConnectedEvent.java new file mode 100644 index 0000000000..59e23cb068 --- /dev/null +++ b/MekHQ/src/mekhq/online/events/CampaignConnectedEvent.java @@ -0,0 +1,17 @@ +package mekhq.online.events; + +import java.util.UUID; + +import megamek.common.event.MMEvent; + +public class CampaignConnectedEvent extends MMEvent { + private final UUID id; + + public CampaignConnectedEvent(UUID id) { + this.id = id; + } + + public UUID getCampaignId() { + return id; + } +} diff --git a/MekHQ/src/mekhq/online/events/CampaignDisconnectedEvent.java b/MekHQ/src/mekhq/online/events/CampaignDisconnectedEvent.java new file mode 100644 index 0000000000..451da3982c --- /dev/null +++ b/MekHQ/src/mekhq/online/events/CampaignDisconnectedEvent.java @@ -0,0 +1,17 @@ +package mekhq.online.events; + +import java.util.UUID; + +import megamek.common.event.MMEvent; + +public class CampaignDisconnectedEvent extends MMEvent { + private final UUID id; + + public CampaignDisconnectedEvent(UUID id) { + this.id = id; + } + + public UUID getCampaignId() { + return id; + } +} diff --git a/MekHQ/src/mekhq/online/events/CampaignListUpdatedEvent.java b/MekHQ/src/mekhq/online/events/CampaignListUpdatedEvent.java new file mode 100644 index 0000000000..2c257bfbd4 --- /dev/null +++ b/MekHQ/src/mekhq/online/events/CampaignListUpdatedEvent.java @@ -0,0 +1,6 @@ +package mekhq.online.events; + +import megamek.common.event.MMEvent; + +public class CampaignListUpdatedEvent extends MMEvent { +} From cc99a0528b837b051adbbd86d54c271bb31b9656 Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 11 Mar 2020 11:03:19 -0400 Subject: [PATCH 10/20] Add the initial Online Session tab --- .../mekhq/resources/CampaignGUI.properties | 3 + MekHQ/src/mekhq/gui/CampaignGUI.java | 16 ++ MekHQ/src/mekhq/gui/GuiTabType.java | 5 +- MekHQ/src/mekhq/gui/OnlineTab.java | 150 ++++++++++++++++++ .../gui/model/OnlineCampaignsTableModel.java | 102 ++++++++++++ MekHQ/src/mekhq/online/MekHQClient.java | 17 +- MekHQ/src/mekhq/online/MekHQServer.java | 14 ++ 7 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 MekHQ/src/mekhq/gui/OnlineTab.java create mode 100644 MekHQ/src/mekhq/gui/model/OnlineCampaignsTableModel.java diff --git a/MekHQ/resources/mekhq/resources/CampaignGUI.properties b/MekHQ/resources/mekhq/resources/CampaignGUI.properties index 0746d1ea73..b3072db007 100644 --- a/MekHQ/resources/mekhq/resources/CampaignGUI.properties +++ b/MekHQ/resources/mekhq/resources/CampaignGUI.properties @@ -274,3 +274,6 @@ tabOverview.toolTipText=A general overview of the unit, includes multiple breakd bottomRating.DragoonsRating=Dragoons Rating: %s bottomRating.CampaignOpsRating=Campaign Ops Rating: %s + +# Online Tab +panOnline.TabConstraints.tabTitle=Online Session diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index e44f326d26..ffc1f7ed41 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -240,6 +240,14 @@ public void showOverviewTab(boolean show) { } } + public void showOnlineTab(boolean show) { + if (show) { + addStandardTab(GuiTabType.ONLINE); + } else { + removeStandardTab(GuiTabType.ONLINE); + } + } + public void showGMToolsDialog() { GMToolsDialog gmTools = new GMToolsDialog(getFrame(), this); gmTools.setVisible(true); @@ -308,6 +316,14 @@ private void initComponents() { addStandardTab(GuiTabType.FINANCES); addStandardTab(GuiTabType.OVERVIEW); + // If we're hosting a MekHQ session, or if we're connected to + // a MekHQ session, or if we've had remote campaigns previously, + // then we should display the ONLINE tab. + if (app.isHosting() || app.isRemote() + || !getCampaignController().getRemoteCampaigns().isEmpty()) { + addStandardTab(GuiTabType.ONLINE); + } + initMain(); initTopButtons(); initStatusBar(); diff --git a/MekHQ/src/mekhq/gui/GuiTabType.java b/MekHQ/src/mekhq/gui/GuiTabType.java index cc39ad1c56..900e7d53e3 100644 --- a/MekHQ/src/mekhq/gui/GuiTabType.java +++ b/MekHQ/src/mekhq/gui/GuiTabType.java @@ -41,7 +41,8 @@ public enum GuiTabType { MEKLAB(8, "panMekLab.TabConstraints.tabTitle"), //$NON-NLS-1$ FINANCES(9, "panFinances.TabConstraints.tabTitle"), //$NON-NLS-1$ OVERVIEW(10, "panOverview.TabConstraints.tabTitle"), //$NON-NLS-1$ - CUSTOM(11, null); + ONLINE(11, "panOnline.TabConstraints.tabTitle"), //$NON-NLS-1$ + CUSTOM(16, null); private int defaultPos; private String name; @@ -88,6 +89,8 @@ public CampaignGuiTab createTab(CampaignGUI gui) { return new FinancesTab(gui, name); case OVERVIEW: return new OverviewTab(gui, name); + case ONLINE: + return new OnlineTab(gui, name); default: return null; diff --git a/MekHQ/src/mekhq/gui/OnlineTab.java b/MekHQ/src/mekhq/gui/OnlineTab.java new file mode 100644 index 0000000000..190d4e6b43 --- /dev/null +++ b/MekHQ/src/mekhq/gui/OnlineTab.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.gui; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTable; + +import megamek.common.event.Subscribe; +import megamek.common.util.EncodeControl; +import mekhq.MekHQ; +import mekhq.campaign.CampaignController; +import mekhq.campaign.RemoteCampaign; +import mekhq.gui.model.OnlineCampaignsTableModel; +import mekhq.online.events.CampaignListUpdatedEvent; +import mekhq.preferences.PreferencesNode; + +public final class OnlineTab extends CampaignGuiTab implements ActionListener { + + private static final long serialVersionUID = 4133071018441878778L; + + private ResourceBundle resourceMap; + + private OnlineCampaignsTableModel hostCampaignTableModel; + private JTable hostCampaignTable; + private OnlineCampaignsTableModel campaignsTableModel; + private JTable campaignsTable; + + OnlineTab(CampaignGUI gui, String name) { + super(gui, name); + MekHQ.registerHandler(this); + setUserPreferences(); + } + + private void setUserPreferences() { + PreferencesNode preferences = MekHQ.getPreferences().forClass(OnlineTab.class); + + // TODO: manage preferences + } + + private CampaignController getCampaignController() { + return getCampaignGui().getCampaignController(); + } + + @Override + public void initTab() { + resourceMap = ResourceBundle.getBundle("mekhq.resources.CampaignGUI", //$NON-NLS-1$ + new EncodeControl()); + + setName("panelOnline"); //$NON-NLS-1$ + setLayout(new GridBagLayout()); + + hostCampaignTableModel = new OnlineCampaignsTableModel(getHostCampaignAsList()); + hostCampaignTable = new JTable(hostCampaignTableModel); + + JScrollPane hostCampaignScrollPane = new JScrollPane(hostCampaignTable); + JPanel hostCampaignTablePanel = new JPanel(new GridLayout(0, 1)); + hostCampaignTablePanel.setBorder(BorderFactory.createTitledBorder("Host Campaign")); + hostCampaignTablePanel.add(hostCampaignScrollPane); + + campaignsTableModel = new OnlineCampaignsTableModel(getCampaignController().getRemoteCampaigns()); + campaignsTable = new JTable(campaignsTableModel); + + JScrollPane campaignsTableScrollPane = new JScrollPane(campaignsTable); + JPanel campaignsTablePanel = new JPanel(new GridLayout(0, 1)); + campaignsTablePanel.setBorder(BorderFactory.createTitledBorder("Remote Campaigns")); + campaignsTablePanel.add(campaignsTableScrollPane); + + JPanel topPanel = new JPanel(new GridLayout(0, 1)); + if (!getCampaignController().isHost()) { + topPanel.add(hostCampaignTablePanel); + } + topPanel.add(campaignsTablePanel); + + JPanel campaignDetailsPanel = new JPanel(); + + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topPanel, + campaignDetailsPanel); + splitPane.setOneTouchExpandable(true); + splitPane.setResizeWeight(1.0); + + GridBagConstraints gbc= new java.awt.GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.gridwidth = 6; + gbc.fill = java.awt.GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + add(splitPane, gbc); + } + + @Override + public void refreshAll() { + hostCampaignTableModel.setData(getHostCampaignAsList()); + campaignsTableModel.setData(new ArrayList<>(getCampaignController().getRemoteCampaigns())); + } + + private List getHostCampaignAsList() { + List list = new ArrayList<>(); + + list.add(new RemoteCampaign(getCampaignController().getHost(), + getCampaignController().getHostName(), getCampaignController().getHostDate(), + getCampaignController().getHostLocation(), getCampaignController().getHostIsGMMode())); + + return list; + } + + @Override + public GuiTabType tabType() { + return GuiTabType.ONLINE; + } + + @Override + public void actionPerformed(ActionEvent e) { + // TODO Auto-generated method stub + + } + + @Subscribe + public void handle(CampaignListUpdatedEvent e) { + refreshAll(); + } +} diff --git a/MekHQ/src/mekhq/gui/model/OnlineCampaignsTableModel.java b/MekHQ/src/mekhq/gui/model/OnlineCampaignsTableModel.java new file mode 100644 index 0000000000..27a0406de8 --- /dev/null +++ b/MekHQ/src/mekhq/gui/model/OnlineCampaignsTableModel.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.gui.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import mekhq.campaign.RemoteCampaign; + +public class OnlineCampaignsTableModel extends DataTableModel { + + private static final long serialVersionUID = 5792881778901299604L; + + public final static int COL_ID = 0; + public final static int COL_NAME = 1; + public final static int COL_DATE = 2; + public final static int COL_LOCATION = 3; + public final static int COL_IS_GM = 4; + public final static int COL_IS_ACTIVE = 5; + public final static int N_COL = 6; + + private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); + + public OnlineCampaignsTableModel() { + setData(Collections.emptyList()); + } + + public OnlineCampaignsTableModel(Collection remoteCampaigns) { + setData(new ArrayList<>(remoteCampaigns)); + } + + public int getRowCount() { + return data.size(); + } + + @Override + public int getColumnCount() { + return N_COL; + } + + @Override + public String getColumnName(int column) { + switch(column) { + case COL_ID: + return "ID"; + case COL_NAME: + return "Name"; + case COL_DATE: + return "Date"; + case COL_LOCATION: + return "Current Location"; + case COL_IS_GM: + return "GM?"; + case COL_IS_ACTIVE: + return "Active?"; + default: + return "?"; + } + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + RemoteCampaign row = (RemoteCampaign)data.get(rowIndex); + switch (columnIndex) + { + case COL_ID: + return row.getId(); + case COL_NAME: + return row.getName(); + case COL_DATE: + return dateFormatter.print(row.getDate()); + case COL_LOCATION: + return row.getLocation().getName(row.getDate()); + case COL_IS_GM: + return row.isGMMode() ? "Yes" : "No"; + case COL_IS_ACTIVE: + return "Yes"; + default: + return null; + } + } +} diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index 147e1a1e41..bc5b1aaeae 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -81,7 +81,9 @@ protected CampaignDetails getCampaignDetails() { .setId(getCampaign().getId().toString()) .setName(getCampaign().getName()) .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()).build(); + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setIsGMMode(getCampaign().isGM()) + .build(); } public void connect() { @@ -108,6 +110,7 @@ public void connect() { controller.setHostName(hostCampaign.getName()); controller.setHostDate(dateFormatter.parseDateTime(hostCampaign.getDate())); controller.setHostLocation(hostCampaign.getLocation()); + controller.setHostIsGMMode(hostCampaign.getIsGMMode()); knownCampaigns.add(controller.getHost()); for (CampaignDetails details : response.getCampaignsList()) { @@ -119,7 +122,6 @@ public void connect() { dateFormatter.parseDateTime(details.getDate()), details.getLocation(), details.getIsGMMode()); } - createMessageBus(); pingExecutor = Executors.newSingleThreadScheduledExecutor(); @@ -232,10 +234,7 @@ protected void handlePong(UUID id, Pong pong) { dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); } - // Only kick off a notification if we found any new campaigns - if (!knownCampaigns.equals(foundCampaigns)) { - MekHQ.triggerEvent(new CampaignListUpdatedEvent()); - } + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } protected void handleCampaignDateChanged(UUID hostId, CampaignDateChanged dateChanged) { @@ -248,6 +247,8 @@ protected void handleCampaignDateChanged(UUID hostId, CampaignDateChanged dateCh } else { controller.setRemoteCampaignDate(campaignId, campaignDate); } + + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } protected void handleGMModeChanged(UUID hostId, GMModeChanged gmModeChanged) { @@ -259,6 +260,8 @@ protected void handleGMModeChanged(UUID hostId, GMModeChanged gmModeChanged) { } else { controller.setRemoteCampaignGMMode(campaignId, isGMMode); } + + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } protected void handleLocationChanged(UUID hostId, LocationChanged locationChanged) { @@ -271,6 +274,8 @@ protected void handleLocationChanged(UUID hostId, LocationChanged locationChange } else { controller.setRemoteCampaignLocation(campaignId, locationId); } + + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } @Subscribe diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index 7c468a49d6..afebb89f1c 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -157,6 +157,7 @@ private CampaignDetails getCampaignDetails() { .setName(getCampaign().getName()) .setDate(dateFormatter.print(getCampaign().getDateTime())) .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setIsGMMode(getCampaign().isGM()) .setIsActive(true) .build(); } @@ -337,6 +338,13 @@ private void handlePong(StreamObserver responseObserver, UUID cam outstandingPings.remove(campaignId); CampaignDetails clientCampaign = pong.getCampaign(); + UUID clientId = UUID.fromString(clientCampaign.getId()); + controller.addRemoteCampaign(clientId, clientCampaign.getName(), + dateFormatter.parseDateTime(clientCampaign.getDate()), clientCampaign.getLocation(), + clientCampaign.getIsGMMode()); + controller.addActiveCampaign(clientId); + + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); } @@ -346,6 +354,8 @@ protected void handleCampaignDateChanged(StreamObserver responseO controller.setRemoteCampaignDate(clientId, dateFormatter.parseDateTime(campaignDateChanged.getDate())); + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); + sendToAllExcept(clientId, campaignDateChanged); } @@ -356,6 +366,8 @@ protected void handleGMModeChanged( controller.setRemoteCampaignGMMode(clientId, gmModeChanged.getValue()); + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); + sendToAllExcept(clientId, gmModeChanged); } @@ -365,6 +377,8 @@ protected void handleLocationChanged(StreamObserver responseObser controller.setRemoteCampaignLocation(clientId, locationChanged.getLocation()); + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); + sendToAllExcept(clientId, locationChanged); } From 9483b051545f3eb8fbe3a9ec543db92e2097193d Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 11 Mar 2020 11:17:23 -0400 Subject: [PATCH 11/20] Add status bar and title info if using online play --- MekHQ/src/mekhq/gui/CampaignGUI.java | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index ffc1f7ed41..4dc559f9b0 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -147,6 +147,7 @@ public class CampaignGUI extends JPanel { /* Components for the status panel */ private JPanel statusPanel; + private JLabel lblOnline; private JLabel lblLocation; private JLabel lblRating; private JLabel lblFunds; @@ -343,6 +344,7 @@ private void initComponents() { refreshLocation(); refreshTempAstechs(); refreshTempMedics(); + refreshOnlineStatus(); Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); @@ -986,11 +988,13 @@ private void initMain() { private void initStatusBar() { statusPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 20, 4)); + lblOnline = new JLabel(); lblRating = new JLabel(); lblFunds = new JLabel(); lblTempAstechs = new JLabel(); lblTempMedics = new JLabel(); + statusPanel.add(lblOnline); statusPanel.add(lblRating); statusPanel.add(lblFunds); statusPanel.add(lblTempAstechs); @@ -2486,7 +2490,12 @@ public void refreshLab() { } public void refreshCalendar() { - getFrame().setTitle(getCampaign().getTitle()); + if (app.isHosting() || app.isRemote()) { + getFrame().setTitle(String.format( + "%s - %s", app.isHosting() ? "HOSTING" : "CONNECTED", getCampaign().getTitle())); + } else { + getFrame().setTitle(getCampaign().getTitle()); + } } synchronized private void refreshReport() { @@ -2545,6 +2554,20 @@ private void refreshTempMedics() { lblTempMedics.setText(text); } + private void refreshOnlineStatus() { + if (app.isHosting()) { + lblOnline.setText( + String.format("HOSTING (%d connected)", + getCampaignController().getRemoteCampaigns().size())); + } else if (app.isRemote()) { + lblOnline.setText( + String.format("CONNECTED (%d playing)", + getCampaignController().getActiveCampaigns().size() + 1 /*Host*/)); + } else { + lblOnline.setVisible(false); + } + } + private ActionScheduler fundsScheduler = new ActionScheduler(this::refreshFunds); private ActionScheduler ratingScheduler = new ActionScheduler(this::refreshRating); @@ -2649,6 +2672,8 @@ public void handleLocationChanged(LocationChangedEvent ev) { @Subscribe public void handle(CampaignListUpdatedEvent ev) { + refreshOnlineStatus(); + MekHQ.getLogger().info(CampaignGUI.class, "handle(CampaignListUpdatedEvent)", "There are " + (getCampaignController().getRemoteCampaigns().size() + 1) + " campaigns playing, " + (getCampaignController().getActiveCampaigns().size() + 1) + " are active"); } From 4a39b3ae6c2d97f123a8b317acf622c9b7409b37 Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 11 Mar 2020 11:29:19 -0400 Subject: [PATCH 12/20] Correctly track active campaigns from a client --- MekHQ/src/mekhq/campaign/CampaignController.java | 6 ++++++ MekHQ/src/mekhq/gui/CampaignGUI.java | 2 +- MekHQ/src/mekhq/online/MekHQClient.java | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 96487eeaa9..d6745b73cd 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -162,6 +162,12 @@ public void removeActiveCampaign(UUID id) { activeCampaigns.remove(id); } + public void setActiveCampaigns(Collection campaignIds) { + // TODO: evaluate if we can make this atomic somehow. + activeCampaigns.clear(); + campaignIds.forEach(id -> activeCampaigns.put(id, id)); + } + public boolean isCampaignActive(UUID id) { return Objects.equals(getHost(), id) || activeCampaigns.containsKey(id); } diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index 4dc559f9b0..a4c8f1cf71 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -2562,7 +2562,7 @@ private void refreshOnlineStatus() { } else if (app.isRemote()) { lblOnline.setText( String.format("CONNECTED (%d playing)", - getCampaignController().getActiveCampaigns().size() + 1 /*Host*/)); + getCampaignController().getActiveCampaigns().size())); } else { lblOnline.setVisible(false); } diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index bc5b1aaeae..0521e637bc 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -234,6 +234,8 @@ protected void handlePong(UUID id, Pong pong) { dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); } + controller.setActiveCampaigns(foundCampaigns); + MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } From 325c4895792579095521ae2939e867d17fcc833e Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Wed, 11 Mar 2020 20:19:38 -0400 Subject: [PATCH 13/20] Make the online tab look okay on clients --- MekHQ/src/mekhq/gui/OnlineTab.java | 42 +++++++++++++++++++----------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/MekHQ/src/mekhq/gui/OnlineTab.java b/MekHQ/src/mekhq/gui/OnlineTab.java index 190d4e6b43..58a2e4184b 100644 --- a/MekHQ/src/mekhq/gui/OnlineTab.java +++ b/MekHQ/src/mekhq/gui/OnlineTab.java @@ -18,6 +18,7 @@ */ package mekhq.gui; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; @@ -32,6 +33,7 @@ import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; +import javax.swing.SwingUtilities; import megamek.common.event.Subscribe; import megamek.common.util.EncodeControl; @@ -45,7 +47,7 @@ public final class OnlineTab extends CampaignGuiTab implements ActionListener { private static final long serialVersionUID = 4133071018441878778L; - + private ResourceBundle resourceMap; private OnlineCampaignsTableModel hostCampaignTableModel; @@ -74,8 +76,10 @@ public void initTab() { resourceMap = ResourceBundle.getBundle("mekhq.resources.CampaignGUI", //$NON-NLS-1$ new EncodeControl()); + GridBagConstraints gbc; + setName("panelOnline"); //$NON-NLS-1$ - setLayout(new GridBagLayout()); + setLayout(new GridLayout(0, 1)); hostCampaignTableModel = new OnlineCampaignsTableModel(getHostCampaignAsList()); hostCampaignTable = new JTable(hostCampaignTableModel); @@ -84,6 +88,7 @@ public void initTab() { JPanel hostCampaignTablePanel = new JPanel(new GridLayout(0, 1)); hostCampaignTablePanel.setBorder(BorderFactory.createTitledBorder("Host Campaign")); hostCampaignTablePanel.add(hostCampaignScrollPane); + hostCampaignTablePanel.setMinimumSize(new Dimension(400, 120)); campaignsTableModel = new OnlineCampaignsTableModel(getCampaignController().getRemoteCampaigns()); campaignsTable = new JTable(campaignsTableModel); @@ -92,28 +97,35 @@ public void initTab() { JPanel campaignsTablePanel = new JPanel(new GridLayout(0, 1)); campaignsTablePanel.setBorder(BorderFactory.createTitledBorder("Remote Campaigns")); campaignsTablePanel.add(campaignsTableScrollPane); + campaignsTablePanel.setMinimumSize(new Dimension(400, 300)); - JPanel topPanel = new JPanel(new GridLayout(0, 1)); + JPanel topPanel = new JPanel(new GridBagLayout()); if (!getCampaignController().isHost()) { - topPanel.add(hostCampaignTablePanel); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + topPanel.add(hostCampaignTablePanel, gbc); } - topPanel.add(campaignsTablePanel); + + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + topPanel.add(campaignsTablePanel, gbc); JPanel campaignDetailsPanel = new JPanel(); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topPanel, campaignDetailsPanel); splitPane.setOneTouchExpandable(true); - splitPane.setResizeWeight(1.0); + splitPane.setResizeWeight(0.5); - GridBagConstraints gbc= new java.awt.GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 6; - gbc.fill = java.awt.GridBagConstraints.BOTH; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - add(splitPane, gbc); + add(splitPane); } @Override @@ -125,7 +137,7 @@ public void refreshAll() { private List getHostCampaignAsList() { List list = new ArrayList<>(); - list.add(new RemoteCampaign(getCampaignController().getHost(), + list.add(new RemoteCampaign(getCampaignController().getHost(), getCampaignController().getHostName(), getCampaignController().getHostDate(), getCampaignController().getHostLocation(), getCampaignController().getHostIsGMMode())); From ca469f872a56f22c5c6828e9653a5cc56365fad7 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Thu, 12 Mar 2020 08:28:02 -0400 Subject: [PATCH 14/20] Try to improve the atomicity of activity tracking --- MekHQ/src/main/proto/Service.proto | 6 ++++ .../mekhq/campaign/CampaignController.java | 32 +++++++------------ MekHQ/src/mekhq/campaign/RemoteCampaign.java | 22 ++++++++----- MekHQ/src/mekhq/gui/CampaignGUI.java | 6 ++-- MekHQ/src/mekhq/gui/OnlineTab.java | 3 +- MekHQ/src/mekhq/online/MekHQClient.java | 2 +- MekHQ/src/mekhq/online/MekHQServer.java | 4 +-- 7 files changed, 39 insertions(+), 36 deletions(-) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 423f5c3d2a..83f91135d0 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -117,6 +117,12 @@ message CampaignDetails { // A value indicating whether or not the Campaign // is actively playing in this session. + // + // Client Campaigns should not set this value + // when sending a CampaignDetails message. + // + // Host Campaigns should not read this value in + // messages from Client Campaigns. bool is_active = 7; } diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index d6745b73cd..1202c4f83f 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -19,8 +19,6 @@ package mekhq.campaign; import java.util.Collection; -import java.util.Collections; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -38,7 +36,6 @@ public class CampaignController { private final Campaign localCampaign; private final ConcurrentHashMap remoteCampaigns = new ConcurrentHashMap<>(); - private final ConcurrentHashMap activeCampaigns = new ConcurrentHashMap<>(); private boolean isHost; private UUID host; @@ -127,7 +124,7 @@ public boolean getHostIsGMMode() { public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId, boolean isGMMode) { PlanetarySystem planetarySystem = Systems.getInstance().getSystemById(locationId); - remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem, isGMMode)); + remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem, isGMMode, true)); } public Collection getRemoteCampaigns() { @@ -154,27 +151,22 @@ public void setRemoteCampaignGMMode(UUID campaignId, boolean isGMMode) { remoteCampaigns.computeIfPresent(campaignId, (key, remoteCampaign) -> remoteCampaign.withGMMode(isGMMode)); } - public void addActiveCampaign(UUID id) { - activeCampaigns.put(id, id); - } - public void removeActiveCampaign(UUID id) { - activeCampaigns.remove(id); + remoteCampaigns.computeIfPresent(id, (key, remoteCampaign) -> remoteCampaign.withActive(false)); } - public void setActiveCampaigns(Collection campaignIds) { - // TODO: evaluate if we can make this atomic somehow. - activeCampaigns.clear(); - campaignIds.forEach(id -> activeCampaigns.put(id, id)); - } - - public boolean isCampaignActive(UUID id) { - return Objects.equals(getHost(), id) || activeCampaigns.containsKey(id); + public int getActiveCampaignCount() { + return remoteCampaigns.reduceValuesToInt(Integer.MAX_VALUE, rc -> rc.isActive() ? 1 : 0, 0, (x, acc) -> x + acc); } - public Set getActiveCampaigns() { - return Collections.unmodifiableSet(activeCampaigns.keySet()); - } + /** + * Computes the set of inactive campaigns from a set of active campaign IDs. + */ + public void computeInactiveCampaigns(Set activeCampaigns) { + for (UUID clientId : remoteCampaigns.keySet()) { + remoteCampaigns.computeIfPresent(clientId, (key, rc) -> activeCampaigns.contains(clientId) ? rc : rc.withActive(false)); + } + } /** * Advances the local {@link Campaign} to the next day. diff --git a/MekHQ/src/mekhq/campaign/RemoteCampaign.java b/MekHQ/src/mekhq/campaign/RemoteCampaign.java index 8571bea470..6b6c8a7788 100644 --- a/MekHQ/src/mekhq/campaign/RemoteCampaign.java +++ b/MekHQ/src/mekhq/campaign/RemoteCampaign.java @@ -31,17 +31,15 @@ public class RemoteCampaign { private final DateTime date; private final PlanetarySystem location; private final boolean isGMMode; + private final boolean isActive; - public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location) { - this(id, name, date, location, false); - } - - public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location, boolean isGMMode) { + public RemoteCampaign(UUID id, String name, DateTime date, PlanetarySystem location, boolean isGMMode, boolean isActive) { this.id = id; this.name = name; this.date = date; this.location = location; this.isGMMode = isGMMode; + this.isActive = isActive; } public UUID getId() { @@ -64,15 +62,23 @@ public boolean isGMMode() { return isGMMode; } + public boolean isActive() { + return isActive; + } + public RemoteCampaign withDate(DateTime newDate) { - return new RemoteCampaign(id, name, newDate, location); + return new RemoteCampaign(id, name, newDate, location, isGMMode, isActive); } public RemoteCampaign withLocation(PlanetarySystem newLocation) { - return new RemoteCampaign(id, name, date, newLocation); + return new RemoteCampaign(id, name, date, newLocation, isGMMode, isActive); } public RemoteCampaign withGMMode(boolean isGMMode) { - return new RemoteCampaign(id, name, date, location, isGMMode); + return new RemoteCampaign(id, name, date, location, isGMMode, isActive); + } + + public RemoteCampaign withActive(boolean isActive) { + return new RemoteCampaign(id, name, date, location, isGMMode, isActive); } } diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index a4c8f1cf71..6764722f67 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -320,7 +320,7 @@ private void initComponents() { // If we're hosting a MekHQ session, or if we're connected to // a MekHQ session, or if we've had remote campaigns previously, // then we should display the ONLINE tab. - if (app.isHosting() || app.isRemote() + if (app.isHosting() || app.isRemote() || !getCampaignController().getRemoteCampaigns().isEmpty()) { addStandardTab(GuiTabType.ONLINE); } @@ -2562,7 +2562,7 @@ private void refreshOnlineStatus() { } else if (app.isRemote()) { lblOnline.setText( String.format("CONNECTED (%d playing)", - getCampaignController().getActiveCampaigns().size())); + getCampaignController().getActiveCampaignCount() + 1 /*host*/)); } else { lblOnline.setVisible(false); } @@ -2675,7 +2675,7 @@ public void handle(CampaignListUpdatedEvent ev) { refreshOnlineStatus(); MekHQ.getLogger().info(CampaignGUI.class, "handle(CampaignListUpdatedEvent)", - "There are " + (getCampaignController().getRemoteCampaigns().size() + 1) + " campaigns playing, " + (getCampaignController().getActiveCampaigns().size() + 1) + " are active"); + "There are " + (getCampaignController().getRemoteCampaigns().size() + 1) + " campaigns playing, " + (getCampaignController().getActiveCampaignCount() + 1) + " are active"); } public void refreshLocation() { diff --git a/MekHQ/src/mekhq/gui/OnlineTab.java b/MekHQ/src/mekhq/gui/OnlineTab.java index 58a2e4184b..df109503ad 100644 --- a/MekHQ/src/mekhq/gui/OnlineTab.java +++ b/MekHQ/src/mekhq/gui/OnlineTab.java @@ -139,7 +139,8 @@ private List getHostCampaignAsList() { list.add(new RemoteCampaign(getCampaignController().getHost(), getCampaignController().getHostName(), getCampaignController().getHostDate(), - getCampaignController().getHostLocation(), getCampaignController().getHostIsGMMode())); + getCampaignController().getHostLocation(), getCampaignController().getHostIsGMMode(), + true/*isActive*/)); return list; } diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index 0521e637bc..5f1c7c6023 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -234,7 +234,7 @@ protected void handlePong(UUID id, Pong pong) { dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); } - controller.setActiveCampaigns(foundCampaigns); + controller.computeInactiveCampaigns(foundCampaigns); MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index afebb89f1c..0d6dcde70d 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -189,7 +189,6 @@ public void connect(ConnectionRequest request, StreamObserver responseObserver, UUID cam controller.addRemoteCampaign(clientId, clientCampaign.getName(), dateFormatter.parseDateTime(clientCampaign.getDate()), clientCampaign.getLocation(), clientCampaign.getIsGMMode()); - controller.addActiveCampaign(clientId); MekHQ.triggerEvent(new CampaignListUpdatedEvent()); @@ -449,7 +447,7 @@ private Collection convert(Collection remoteCam .setDate(dateFormatter.print(remoteCampaign.getDate())) .setLocation(remoteCampaign.getLocation().getId()) .setIsGMMode(remoteCampaign.isGMMode()) - .setIsActive(controller.isCampaignActive(remoteCampaign.getId())) + .setIsActive(remoteCampaign.isActive()) .build()); } return converted; From e10f23db737317be41e42d1b467835f835a8b40b Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Thu, 12 Mar 2020 13:12:45 -0400 Subject: [PATCH 15/20] Improve handling of missing vs inactive campaigns --- .../mekhq/campaign/CampaignController.java | 15 +++++++++--- MekHQ/src/mekhq/online/MekHQClient.java | 23 ++++++++++++++----- MekHQ/src/mekhq/online/MekHQServer.java | 10 ++++++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 1202c4f83f..e0cda011ec 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -19,6 +19,7 @@ package mekhq.campaign; import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -122,15 +123,23 @@ public boolean getHostIsGMMode() { return hostIsGM; } - public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId, boolean isGMMode) { + public void addActiveRemoteCampaign(UUID id, String name, DateTime date, String locationId, boolean isGMMode) { + addRemoteCampaign(id, name, date, locationId, isGMMode, /*isActive:*/ true); + } + + public void addRemoteCampaign(UUID id, String name, DateTime date, String locationId, boolean isGMMode, boolean isActive) { PlanetarySystem planetarySystem = Systems.getInstance().getSystemById(locationId); - remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem, isGMMode, true)); + remoteCampaigns.put(id, new RemoteCampaign(id, name, date, planetarySystem, isGMMode, isActive)); } public Collection getRemoteCampaigns() { - return remoteCampaigns.values(); + return Collections.unmodifiableCollection(remoteCampaigns.values()); } + public Collection getRemoteCampaignIds() { + return Collections.unmodifiableSet(remoteCampaigns.keySet()); + } + public void setRemoteCampaignDate(UUID campaignId, DateTime campaignDate) { // We only update this if the remote campaign is actually present // otherwise the next PING-PONG will catch us up. diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index 5f1c7c6023..2637e989cc 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -119,7 +119,8 @@ public void connect() { knownCampaigns.add(clientId); controller.addRemoteCampaign(clientId, details.getName(), - dateFormatter.parseDateTime(details.getDate()), details.getLocation(), details.getIsGMMode()); + dateFormatter.parseDateTime(details.getDate()), details.getLocation(), details.getIsGMMode(), + details.getIsActive()); } createMessageBus(); @@ -222,19 +223,29 @@ protected void handlePong(UUID id, Pong pong) { controller.setHostLocation(locationId); controller.setHostIsGMMode(hostCampaign.getIsGMMode()); - Set foundCampaigns = new HashSet<>(); - foundCampaigns.add(id); + // gather the IDs of all of the campaigns we know about + Set allCampaigns = new HashSet<>(controller.getRemoteCampaignIds()); for (CampaignDetails campaign : pong.getCampaignsList()) { UUID clientId = UUID.fromString(campaign.getId()); - foundCampaigns.add(clientId); + // remove this campaign from the list we know about + // so that when this ends we'll be left with a set + // of campaigns that our host doesn't know about + // or isn't active anymore. We take into account the + // ACTIVE status from the Host campaign below. + allCampaigns.remove(clientId); controller.addRemoteCampaign(clientId, campaign.getName(), - dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode()); + dateFormatter.parseDateTime(campaign.getDate()), campaign.getLocation(), campaign.getIsGMMode(), + campaign.getIsActive()); } - controller.computeInactiveCampaigns(foundCampaigns); + // Account for any missing Campaigns on this Host + // they are all automatically considered INACTIVE. + for (UUID missingCampaignId : allCampaigns) { + controller.removeActiveCampaign(missingCampaignId); + } MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index 0d6dcde70d..dd089e2fa7 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -189,7 +189,7 @@ public void connect(ConnectionRequest request, StreamObserver responseObserver, UUID cam CampaignDetails clientCampaign = pong.getCampaign(); UUID clientId = UUID.fromString(clientCampaign.getId()); - controller.addRemoteCampaign(clientId, clientCampaign.getName(), + controller.addActiveRemoteCampaign(clientId, clientCampaign.getName(), dateFormatter.parseDateTime(clientCampaign.getDate()), clientCampaign.getLocation(), clientCampaign.getIsGMMode()); From e5138108433beb6f18b7898d032100f2e2d6fd96 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Thu, 12 Mar 2020 20:39:03 -0400 Subject: [PATCH 16/20] Remove unused computeInactiveCampaigns method --- MekHQ/src/mekhq/campaign/CampaignController.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index e0cda011ec..2faa815577 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -20,7 +20,6 @@ import java.util.Collection; import java.util.Collections; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -168,15 +167,6 @@ public int getActiveCampaignCount() { return remoteCampaigns.reduceValuesToInt(Integer.MAX_VALUE, rc -> rc.isActive() ? 1 : 0, 0, (x, acc) -> x + acc); } - /** - * Computes the set of inactive campaigns from a set of active campaign IDs. - */ - public void computeInactiveCampaigns(Set activeCampaigns) { - for (UUID clientId : remoteCampaigns.keySet()) { - remoteCampaigns.computeIfPresent(clientId, (key, rc) -> activeCampaigns.contains(clientId) ? rc : rc.withActive(false)); - } - } - /** * Advances the local {@link Campaign} to the next day. */ From 3a608089b55b7942e527fbe7b302fa43c447ea5f Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Thu, 12 Mar 2020 21:00:30 -0400 Subject: [PATCH 17/20] Add daily log related messages - Hold off on prototype until HTML escaping can be figured out. --- MekHQ/src/main/proto/Service.proto | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 83f91135d0..6b90c1ff0a 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -262,3 +262,24 @@ message LocationChanged { // was due to a GM action. bool is_gm_movement = 3; } + +// Represents a message sent when a Campaign updates its daily +// log. Zero or more of these messages may be sent for the same +// date. +message LogUpdated { + // The unique identifier of the Campaign. + string id = 1; + + // The date that this log entry corresponds to. + string date = 2; + + // Zero or more LogEntry messages containing the + // log entries for that date. + repeated LogEntry entries = 3; +} + +// Represents an entry in the daily log. +message LogEntry { + // The daily log entry. + string entry = 1; +} From c501d3e851618613ced81a5fe0a9ae131285c931 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Fri, 13 Mar 2020 14:21:09 -0400 Subject: [PATCH 18/20] Add transmission of TOEs between campaigns --- MekHQ/src/main/proto/Service.proto | 47 +++++ .../mekhq/campaign/CampaignController.java | 11 + MekHQ/src/mekhq/gui/OnlineTab.java | 23 +++ MekHQ/src/mekhq/gui/RemoteForceRenderer.java | 72 +++++++ .../mekhq/gui/model/RemoteOrgTreeModel.java | 95 +++++++++ MekHQ/src/mekhq/online/MekHQClient.java | 67 ++++++ MekHQ/src/mekhq/online/MekHQServer.java | 195 +++++++++++------- .../events/CampaignDetailsUpdatedEvent.java | 35 ++++ .../src/mekhq/online/forces/RemoteForce.java | 80 +++++++ MekHQ/src/mekhq/online/forces/RemoteTOE.java | 33 +++ MekHQ/src/mekhq/online/forces/RemoteUnit.java | 61 ++++++ 11 files changed, 646 insertions(+), 73 deletions(-) create mode 100644 MekHQ/src/mekhq/gui/RemoteForceRenderer.java create mode 100644 MekHQ/src/mekhq/gui/model/RemoteOrgTreeModel.java create mode 100644 MekHQ/src/mekhq/online/events/CampaignDetailsUpdatedEvent.java create mode 100644 MekHQ/src/mekhq/online/forces/RemoteForce.java create mode 100644 MekHQ/src/mekhq/online/forces/RemoteTOE.java create mode 100644 MekHQ/src/mekhq/online/forces/RemoteUnit.java diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 6b90c1ff0a..42f96800f7 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -283,3 +283,50 @@ message LogEntry { // The daily log entry. string entry = 1; } + +// Represents a message sent when the TOE for a Campaign +// is updated. +// +// This message may be sent by any Campaign, and will +// be forwarded by the Host Campaign to everyone in +// the current session. +// +// This message may be restricted to campaigns on the +// same team. +message TOEUpdated { + // The unique identifier of the Campaign. + string id = 1; + + // The top level Force in the Campaign. + Force force = 2; +} + +// Represents a Force within a Campaign's TOE. +message Force { + // The unique identifier of the Force. + int32 id = 1; + + // The name of the force. + string name = 2; + + // Zero or more sub-forces assigned + // to this Force. + repeated Force sub_forces = 3; + + // Zero or more units attached directly + // to this Force. + repeated ForceUnit units = 4; +} + +// Represents a unit within a Force in a Campaign's +// TOE. +message ForceUnit { + // The unique identifier of the Unit. + string id = 1; + + // The name of the Unit. + string name = 2; + + // The name of the Unit's commander. + string commander = 3; +} diff --git a/MekHQ/src/mekhq/campaign/CampaignController.java b/MekHQ/src/mekhq/campaign/CampaignController.java index 2faa815577..422b445b7c 100644 --- a/MekHQ/src/mekhq/campaign/CampaignController.java +++ b/MekHQ/src/mekhq/campaign/CampaignController.java @@ -29,6 +29,8 @@ import mekhq.campaign.universe.PlanetarySystem; import mekhq.campaign.universe.Systems; import mekhq.online.events.WaitingToAdvanceDayEvent; +import mekhq.online.forces.RemoteForce; +import mekhq.online.forces.RemoteTOE; /** * Manages the timeline of a {@link Campaign}. @@ -36,6 +38,7 @@ public class CampaignController { private final Campaign localCampaign; private final ConcurrentHashMap remoteCampaigns = new ConcurrentHashMap<>(); + private final ConcurrentHashMap remoteTOEs = new ConcurrentHashMap<>(); private boolean isHost; private UUID host; @@ -183,4 +186,12 @@ public boolean advanceDay() { } } } + + public void updateTOE(UUID campaignId, RemoteForce forces) { + remoteTOEs.put(campaignId, new RemoteTOE(forces)); + } + + public RemoteTOE getTOE(UUID campaignId) { + return remoteTOEs.get(campaignId); + } } diff --git a/MekHQ/src/mekhq/gui/OnlineTab.java b/MekHQ/src/mekhq/gui/OnlineTab.java index df109503ad..21a82a4cea 100644 --- a/MekHQ/src/mekhq/gui/OnlineTab.java +++ b/MekHQ/src/mekhq/gui/OnlineTab.java @@ -27,12 +27,14 @@ import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; +import java.util.UUID; import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; +import javax.swing.JTree; import javax.swing.SwingUtilities; import megamek.common.event.Subscribe; @@ -41,7 +43,9 @@ import mekhq.campaign.CampaignController; import mekhq.campaign.RemoteCampaign; import mekhq.gui.model.OnlineCampaignsTableModel; +import mekhq.gui.model.RemoteOrgTreeModel; import mekhq.online.events.CampaignListUpdatedEvent; +import mekhq.online.forces.RemoteTOE; import mekhq.preferences.PreferencesNode; public final class OnlineTab extends CampaignGuiTab implements ActionListener { @@ -55,6 +59,8 @@ public final class OnlineTab extends CampaignGuiTab implements ActionListener { private OnlineCampaignsTableModel campaignsTableModel; private JTable campaignsTable; + private JTree selectedCampaignToeTree; + OnlineTab(CampaignGUI gui, String name) { super(gui, name); MekHQ.registerHandler(this); @@ -83,6 +89,10 @@ public void initTab() { hostCampaignTableModel = new OnlineCampaignsTableModel(getHostCampaignAsList()); hostCampaignTable = new JTable(hostCampaignTableModel); + hostCampaignTable.getSelectionModel().addListSelectionListener(e -> { + RemoteTOE toe = getCampaignController().getTOE(getCampaignController().getHost()); + selectedCampaignToeTree.setModel(new RemoteOrgTreeModel(toe != null ? toe : RemoteTOE.EMPTY_TOE)); + }); JScrollPane hostCampaignScrollPane = new JScrollPane(hostCampaignTable); JPanel hostCampaignTablePanel = new JPanel(new GridLayout(0, 1)); @@ -92,6 +102,14 @@ public void initTab() { campaignsTableModel = new OnlineCampaignsTableModel(getCampaignController().getRemoteCampaigns()); campaignsTable = new JTable(campaignsTableModel); + campaignsTable.getSelectionModel().addListSelectionListener(e -> { + int row = e.getFirstIndex(); + if (row >= 0) { + UUID id = (UUID)campaignsTableModel.getValueAt(row, OnlineCampaignsTableModel.COL_ID); + RemoteTOE toe = getCampaignController().getTOE(id); + selectedCampaignToeTree.setModel(new RemoteOrgTreeModel(toe != null ? toe : RemoteTOE.EMPTY_TOE)); + } + }); JScrollPane campaignsTableScrollPane = new JScrollPane(campaignsTable); JPanel campaignsTablePanel = new JPanel(new GridLayout(0, 1)); @@ -120,6 +138,11 @@ public void initTab() { JPanel campaignDetailsPanel = new JPanel(); + selectedCampaignToeTree = new JTree(); + selectedCampaignToeTree.setCellRenderer(new RemoteForceRenderer()); + + campaignDetailsPanel.add(selectedCampaignToeTree); + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topPanel, campaignDetailsPanel); splitPane.setOneTouchExpandable(true); diff --git a/MekHQ/src/mekhq/gui/RemoteForceRenderer.java b/MekHQ/src/mekhq/gui/RemoteForceRenderer.java new file mode 100644 index 0000000000..f9d8649fff --- /dev/null +++ b/MekHQ/src/mekhq/gui/RemoteForceRenderer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.gui; + +import java.awt.Component; + +import javax.swing.JTree; +import javax.swing.UIManager; +import javax.swing.tree.DefaultTreeCellRenderer; + +import mekhq.online.forces.RemoteForce; +import mekhq.online.forces.RemoteUnit; + +public class RemoteForceRenderer extends DefaultTreeCellRenderer { + + private static final long serialVersionUID = 1L; + + public Component getTreeCellRendererComponent( + JTree tree, + Object value, + boolean sel, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + + super.getTreeCellRendererComponent( + tree, value, sel, + expanded, leaf, row, + hasFocus); + setBackground(UIManager.getColor("Tree.background")); + setForeground(UIManager.getColor("Tree.textForeground")); + if (sel) { + setBackground(UIManager.getColor("Tree.selectionBackground")); + setForeground(UIManager.getColor("Tree.selectionForeground")); + } + + if (value instanceof RemoteUnit) { + RemoteUnit u = (RemoteUnit)value; + String name = u.getCommander(); + if (name == null) { + name = "No Crew"; + } + String uname = "" + u.getName() + ""; + + setText("" + name + ", " + uname + ""); + } + if (value instanceof RemoteForce) { + RemoteForce f = (RemoteForce)value; + + setText("" + f.getName() + ""); + } + + return this; + } +} diff --git a/MekHQ/src/mekhq/gui/model/RemoteOrgTreeModel.java b/MekHQ/src/mekhq/gui/model/RemoteOrgTreeModel.java new file mode 100644 index 0000000000..e4ca601623 --- /dev/null +++ b/MekHQ/src/mekhq/gui/model/RemoteOrgTreeModel.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.gui.model; + +import java.util.Vector; + +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import mekhq.online.forces.RemoteForce; +import mekhq.online.forces.RemoteTOE; +import mekhq.online.forces.RemoteUnit; + +public class RemoteOrgTreeModel implements TreeModel { + + private RemoteForce rootForce; + private Vector listeners = new Vector(); + + public RemoteOrgTreeModel() { + rootForce = RemoteForce.emptyForce(); + } + + public RemoteOrgTreeModel(RemoteTOE toe) { + rootForce = toe.getForces(); + } + + @Override + public Object getChild(Object parent, int index) { + if(parent instanceof RemoteForce) { + return ((RemoteForce)parent).getAllChildren().get(index); + } + return null; + } + + @Override + public int getChildCount(Object parent) { + if(parent instanceof RemoteForce) { + return ((RemoteForce)parent).getAllChildren().size(); + } + return 0; + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + if(parent instanceof RemoteForce) { + return ((RemoteForce)parent).getAllChildren().indexOf(child); + } + return 0; + } + + @Override + public Object getRoot() { + return rootForce; + } + + @Override + public boolean isLeaf(Object node) { + return node instanceof RemoteUnit + || (node instanceof RemoteForce && ((RemoteForce)node).getAllChildren().size() == 0); + } + + @Override + public void valueForPathChanged(TreePath arg0, Object arg1) { + // TODO Auto-generated method stub + } + + public void addTreeModelListener( TreeModelListener listener ) { + if ( listener != null && !listeners.contains( listener ) ) { + listeners.addElement( listener ); + } + } + + public void removeTreeModelListener( TreeModelListener listener ) { + if ( listener != null ) { + listeners.removeElement( listener ); + } + } +} diff --git a/MekHQ/src/mekhq/online/MekHQClient.java b/MekHQ/src/mekhq/online/MekHQClient.java index 2637e989cc..76a953559c 100644 --- a/MekHQ/src/mekhq/online/MekHQClient.java +++ b/MekHQ/src/mekhq/online/MekHQClient.java @@ -47,9 +47,12 @@ import mekhq.campaign.event.GMModeEvent; import mekhq.campaign.event.LocationChangedEvent; import mekhq.campaign.event.NewDayEvent; +import mekhq.campaign.event.OrganizationChangedEvent; import mekhq.online.MekHQHostGrpc.MekHQHostBlockingStub; import mekhq.online.MekHQHostGrpc.MekHQHostStub; +import mekhq.online.events.CampaignDetailsUpdatedEvent; import mekhq.online.events.CampaignListUpdatedEvent; +import mekhq.online.forces.RemoteForce; public class MekHQClient { private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); @@ -125,6 +128,8 @@ public void connect() { createMessageBus(); + sendMessagesForInitialConnection(); + pingExecutor = Executors.newSingleThreadScheduledExecutor(); pings = pingExecutor.scheduleAtFixedRate(() -> sendPing(), 0, 15, TimeUnit.SECONDS); @@ -163,6 +168,8 @@ public void onNext(ServerMessage message) { handleGMModeChanged(id, payload.unpack(GMModeChanged.class)); } else if (payload.is(LocationChanged.class)) { handleLocationChanged(id, payload.unpack(LocationChanged.class)); + } else if (payload.is(TOEUpdated.class)) { + handleTOEUpdated(id, payload.unpack(TOEUpdated.class)); } } catch (InvalidProtocolBufferException e) { MekHQ.getLogger().error(MekHQClient.class, "messageBus::onNext()", "RPC protocol error: " + e.getMessage(), e); @@ -183,6 +190,14 @@ public void onCompleted() { }); } + /** + * Sends messages to the host campaign upon its initial connection to the message bus. + */ + private void sendMessagesForInitialConnection() { + TOEUpdated toeUpdated = buildTOEUpdated(); + messageBus.onNext(buildMessage(toeUpdated)); + } + protected void handleRpcException(Throwable t) { messageBus.onError(Status.INTERNAL .withDescription(t.getMessage()) @@ -291,6 +306,16 @@ protected void handleLocationChanged(UUID hostId, LocationChanged locationChange MekHQ.triggerEvent(new CampaignListUpdatedEvent()); } + protected void handleTOEUpdated(UUID hostId, TOEUpdated toeUpdated) { + + UUID campaignId = UUID.fromString(toeUpdated.getId()); + controller.updateTOE(campaignId, RemoteForce.build(toeUpdated.getForce())); + + MekHQ.triggerEvent(new CampaignDetailsUpdatedEvent(campaignId)); + + MekHQ.getLogger().info(MekHQClient.class, "handleTOEUpdated()", String.format("TOE Updated: %s ", campaignId)); + } + @Subscribe public void handle(NewDayEvent evt) { CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() @@ -322,6 +347,13 @@ public void handle(LocationChangedEvent evt) { messageBus.onNext(buildMessage(locationChanged)); } + @Subscribe + public void handle(OrganizationChangedEvent evt) { + TOEUpdated toeUpdated = buildTOEUpdated(); + + messageBus.onNext(buildMessage(toeUpdated)); + } + private ClientMessage buildMessage(Message payload) { return ClientMessage.newBuilder() .setTimestamp(getTimestamp()) @@ -336,4 +368,39 @@ private static Timestamp getTimestamp() { .setNanos((int) ((millis % 1000) * 1000000)) .build(); } + + private TOEUpdated buildTOEUpdated() { + return TOEUpdated.newBuilder().setId(getCampaign().getId().toString()) + .setForce(buildTOEForces(getCampaign().getForces())) + .build(); + } + + private Force buildTOEForces(mekhq.campaign.force.Force forces) { + Force.Builder forceBuilder = Force.newBuilder() + .setId(forces.getId()) + .setName(forces.getName()); + + for (Object object : forces.getAllChildren(getCampaign())) { + if (object instanceof mekhq.campaign.force.Force) { + forceBuilder.addSubForces(buildTOEForces((mekhq.campaign.force.Force) object)); + } else if (object instanceof mekhq.campaign.unit.Unit) { + forceBuilder.addUnits(buildForceUnit((mekhq.campaign.unit.Unit) object)); + } + } + + return forceBuilder.build(); + } + + private ForceUnit buildForceUnit(mekhq.campaign.unit.Unit unit) { + ForceUnit.Builder forceUnitBuilder = ForceUnit.newBuilder() + .setId(unit.getId().toString()) + .setName(unit.getName()); + + mekhq.campaign.personnel.Person commander = unit.getCommander(); + if (commander != null) { + forceUnitBuilder.setCommander(commander.getName()); + } + + return forceUnitBuilder.build(); + } } diff --git a/MekHQ/src/mekhq/online/MekHQServer.java b/MekHQ/src/mekhq/online/MekHQServer.java index dd089e2fa7..49dba47cd7 100644 --- a/MekHQ/src/mekhq/online/MekHQServer.java +++ b/MekHQ/src/mekhq/online/MekHQServer.java @@ -53,10 +53,13 @@ import mekhq.campaign.event.GMModeEvent; import mekhq.campaign.event.LocationChangedEvent; import mekhq.campaign.event.NewDayEvent; +import mekhq.campaign.event.OrganizationChangedEvent; import mekhq.online.MekHQHostGrpc; import mekhq.online.events.CampaignConnectedEvent; +import mekhq.online.events.CampaignDetailsUpdatedEvent; import mekhq.online.events.CampaignDisconnectedEvent; import mekhq.online.events.CampaignListUpdatedEvent; +import mekhq.online.forces.RemoteForce; public class MekHQServer { private final int port; @@ -134,6 +137,11 @@ public void handle(LocationChangedEvent evt) { service.notifyLocationChanged(evt.getLocation().getCurrentSystem().getId(), !evt.isKFJump()); } + @Subscribe + public void handle(OrganizationChangedEvent evt) { + service.notifyTOEChanged(); + } + private static class MekHQHostService extends MekHQHostGrpc.MekHQHostImplBase { private final DateTimeFormatter dateFormatter = ISODateTimeFormat.date(); private final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.MekHQ"); @@ -152,14 +160,10 @@ private mekhq.campaign.Campaign getCampaign() { } private CampaignDetails getCampaignDetails() { - return CampaignDetails.newBuilder() - .setId(getCampaign().getId().toString()) - .setName(getCampaign().getName()) - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) - .setIsGMMode(getCampaign().isGM()) - .setIsActive(true) - .build(); + return CampaignDetails.newBuilder().setId(getCampaign().getId().toString()).setName(getCampaign().getName()) + .setDate(dateFormatter.print(getCampaign().getDateTime())) + .setLocation(getCampaign().getLocation().getCurrentSystem().getId()) + .setIsGMMode(getCampaign().isGM()).setIsActive(true).build(); } @Override @@ -167,9 +171,9 @@ public void connect(ConnectionRequest request, StreamObserver messageBus(StreamObserver responseObserver) { - return new StreamObserver() { + StreamObserver clientMessages = new StreamObserver() { private UUID clientId; @Override @@ -231,19 +233,23 @@ public void onNext(ClientMessage message) { } else if (payload.is(Pong.class)) { handlePong(responseObserver, clientId, payload.unpack(Pong.class)); } else if (payload.is(CampaignDateChanged.class)) { - handleCampaignDateChanged(responseObserver, clientId, payload.unpack(CampaignDateChanged.class)); + handleCampaignDateChanged(responseObserver, clientId, + payload.unpack(CampaignDateChanged.class)); } else if (payload.is(GMModeChanged.class)) { handleGMModeChanged(responseObserver, clientId, payload.unpack(GMModeChanged.class)); } else if (payload.is(LocationChanged.class)) { handleLocationChanged(responseObserver, clientId, payload.unpack(LocationChanged.class)); + } else if (payload.is(TOEUpdated.class)) { + handleTOEUpdated(responseObserver, clientId, payload.unpack(TOEUpdated.class)); } } catch (InvalidProtocolBufferException e) { - MekHQ.getLogger().error(MekHQHostService.class, "messageBus::onNext()", "RPC protocol error: " + e.getMessage(), e); - responseObserver.onError(Status.INTERNAL - .withDescription(e.getMessage()) - .augmentDescription("messageBus()") - .withCause(e) // This can be attached to the Status locally, but NOT transmitted to the client! - .asRuntimeException()); + MekHQ.getLogger().error(MekHQHostService.class, "messageBus::onNext()", + "RPC protocol error: " + e.getMessage(), e); + responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()) + .augmentDescription("messageBus()").withCause(e) // This can be attached to the Status + // locally, but NOT transmitted to the + // client! + .asRuntimeException()); controller.removeActiveCampaign(clientId); @@ -254,7 +260,7 @@ public void onNext(ClientMessage message) { @Override public void onError(Throwable t) { MekHQ.getLogger().error(MekHQHostService.class, "messageBus::onError()", - String.format("RPC protocol error from client %s: %s", clientId, t.getMessage()), t); + String.format("RPC protocol error from client %s: %s", clientId, t.getMessage()), t); messageBus.remove(clientId); @@ -277,6 +283,10 @@ public void onCompleted() { } } }; + + sendMessagesForInitialConnection(responseObserver); + + return clientMessages; } private void addMessageBus(UUID campaignId, StreamObserver responseObserver) { @@ -293,9 +303,7 @@ private void handleDisconnection(UUID campaignId) { } public void sendPings() { - Ping ping = Ping.newBuilder() - .setCampaign(getCampaignDetails()) - .build(); + Ping ping = Ping.newBuilder().setCampaign(getCampaignDetails()).build(); List toRemove = new ArrayList<>(); for (Map.Entry> client : messageBus.entrySet()) { @@ -311,7 +319,8 @@ public void sendPings() { try { client.getValue().onNext(buildResponse(Ping.newBuilder(ping).build())); } catch (Exception e) { - MekHQ.getLogger().error(MekHQHostService.class, "handlePing()", "Failed to ping campaign " + clientId, e); + MekHQ.getLogger().error(MekHQHostService.class, "handlePing()", + "Failed to ping campaign " + clientId, e); toRemove.add(clientId); } } @@ -330,12 +339,11 @@ private void handlePing(StreamObserver responseObserver, UUID cam CampaignDetails clientCampaign = ping.getCampaign(); - MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PING: %s %s %s", campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PING: %s %s %s", + campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); - Pong pong = Pong.newBuilder() - .setCampaign(getCampaignDetails()) - .addAllCampaigns(convert(controller.getRemoteCampaigns())) - .build(); + Pong pong = Pong.newBuilder().setCampaign(getCampaignDetails()) + .addAllCampaigns(convert(controller.getRemoteCampaigns())).build(); responseObserver.onNext(buildResponse(pong)); } @@ -345,12 +353,13 @@ private void handlePong(StreamObserver responseObserver, UUID cam CampaignDetails clientCampaign = pong.getCampaign(); UUID clientId = UUID.fromString(clientCampaign.getId()); controller.addActiveRemoteCampaign(clientId, clientCampaign.getName(), - dateFormatter.parseDateTime(clientCampaign.getDate()), clientCampaign.getLocation(), - clientCampaign.getIsGMMode()); + dateFormatter.parseDateTime(clientCampaign.getDate()), clientCampaign.getLocation(), + clientCampaign.getIsGMMode()); MekHQ.triggerEvent(new CampaignListUpdatedEvent()); - MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); + MekHQ.getLogger().info(MekHQHostService.class, "handlePing()", String.format("-> PONG: %s %s %s", + campaignId, clientCampaign.getDate(), clientCampaign.getLocation())); } protected void handleCampaignDateChanged(StreamObserver responseObserver, UUID clientId, @@ -363,9 +372,7 @@ protected void handleCampaignDateChanged(StreamObserver responseO sendToAllExcept(clientId, campaignDateChanged); } - protected void handleGMModeChanged( - StreamObserver responseObserver, - UUID clientId, + protected void handleGMModeChanged(StreamObserver responseObserver, UUID clientId, GMModeChanged gmModeChanged) { controller.setRemoteCampaignGMMode(clientId, gmModeChanged.getValue()); @@ -375,8 +382,7 @@ protected void handleGMModeChanged( sendToAllExcept(clientId, gmModeChanged); } - protected void handleLocationChanged(StreamObserver responseObserver, - UUID clientId, + protected void handleLocationChanged(StreamObserver responseObserver, UUID clientId, LocationChanged locationChanged) { controller.setRemoteCampaignLocation(clientId, locationChanged.getLocation()); @@ -386,34 +392,58 @@ protected void handleLocationChanged(StreamObserver responseObser sendToAllExcept(clientId, locationChanged); } + protected void handleTOEUpdated(StreamObserver responseObserver, UUID clientId, + TOEUpdated toeUpdated) { + + UUID campaignId = UUID.fromString(toeUpdated.getId()); + controller.updateTOE(campaignId, RemoteForce.build(toeUpdated.getForce())); + + MekHQ.triggerEvent(new CampaignDetailsUpdatedEvent(campaignId)); + + MekHQ.getLogger().info(MekHQHostService.class, "handleTOEUpdated()", String.format("TOE Updated: %s ", campaignId)); + + sendToAllExcept(clientId, toeUpdated); + } + + /** + * Sends messages to a client campaign upon its initial connection to the message bus. + */ + protected void sendMessagesForInitialConnection(StreamObserver responseObserver) { + TOEUpdated toeUpdated = buildTOEUpdated(); + responseObserver.onNext(buildResponse(toeUpdated)); + } + public void notifyDayChanged() { - CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder() - .setId(getCampaign().getId().toString()) - .setDate(dateFormatter.print(getCampaign().getDateTime())) - .build(); + CampaignDateChanged dateChanged = CampaignDateChanged.newBuilder().setId(getCampaign().getId().toString()) + .setDate(dateFormatter.print(getCampaign().getDateTime())).build(); sendToAll(dateChanged); } public void notifyGMModeChanged(boolean gmMode) { - GMModeChanged gmModeChanged = GMModeChanged.newBuilder() - .setId(getCampaign().getId().toString()) - .setValue(gmMode) - .build(); + GMModeChanged gmModeChanged = GMModeChanged.newBuilder().setId(getCampaign().getId().toString()) + .setValue(gmMode).build(); sendToAll(gmModeChanged); } public void notifyLocationChanged(String locationId, boolean isGMMovement) { - LocationChanged locationChanged = LocationChanged.newBuilder() - .setId(getCampaign().getId().toString()) - .setLocation(locationId) - .setIsGmMovement(isGMMovement) - .build(); + LocationChanged locationChanged = LocationChanged.newBuilder().setId(getCampaign().getId().toString()) + .setLocation(locationId).setIsGmMovement(isGMMovement).build(); sendToAll(locationChanged); } + public void notifyTOEChanged() { + TOEUpdated toeUpdated = buildTOEUpdated(); + sendToAll(toeUpdated); + } + + private TOEUpdated buildTOEUpdated() { + return TOEUpdated.newBuilder().setId(getCampaign().getId().toString()) + .setForce(buildTOEForces(getCampaign().getForces())).build(); + } + private void sendToAll(Message message) { for (Map.Entry> client : messageBus.entrySet()) { client.getValue().onNext(buildResponse(message)); @@ -429,34 +459,53 @@ private void sendToAllExcept(UUID exceptId, Message message) { } private ServerMessage buildResponse(Message payload) { - return ServerMessage.newBuilder() - .setTimestamp(getTimestamp()) - .setId(getCampaign().getId().toString()) - .setMessage(Any.pack(payload)) - .build(); + return ServerMessage.newBuilder().setTimestamp(getTimestamp()).setId(getCampaign().getId().toString()) + .setMessage(Any.pack(payload)).build(); } private static Timestamp getTimestamp() { long millis = System.currentTimeMillis(); - return Timestamp.newBuilder().setSeconds(millis / 1000) - .setNanos((int) ((millis % 1000) * 1000000)) - .build(); + return Timestamp.newBuilder().setSeconds(millis / 1000).setNanos((int) ((millis % 1000) * 1000000)).build(); } private Collection convert(Collection remoteCampaigns) { List converted = new ArrayList<>(); for (RemoteCampaign remoteCampaign : remoteCampaigns) { - converted.add( - CampaignDetails.newBuilder() - .setId(remoteCampaign.getId().toString()) - .setName(remoteCampaign.getName()) - .setDate(dateFormatter.print(remoteCampaign.getDate())) - .setLocation(remoteCampaign.getLocation().getId()) - .setIsGMMode(remoteCampaign.isGMMode()) - .setIsActive(remoteCampaign.isActive()) - .build()); + converted.add(CampaignDetails.newBuilder().setId(remoteCampaign.getId().toString()) + .setName(remoteCampaign.getName()).setDate(dateFormatter.print(remoteCampaign.getDate())) + .setLocation(remoteCampaign.getLocation().getId()).setIsGMMode(remoteCampaign.isGMMode()) + .setIsActive(remoteCampaign.isActive()).build()); } return converted; } + + private Force buildTOEForces(mekhq.campaign.force.Force forces) { + Force.Builder forceBuilder = Force.newBuilder() + .setId(forces.getId()) + .setName(forces.getName()); + + for (Object object : forces.getAllChildren(getCampaign())) { + if (object instanceof mekhq.campaign.force.Force) { + forceBuilder.addSubForces(buildTOEForces((mekhq.campaign.force.Force) object)); + } else if (object instanceof mekhq.campaign.unit.Unit) { + forceBuilder.addUnits(buildForceUnit((mekhq.campaign.unit.Unit) object)); + } + } + + return forceBuilder.build(); + } + + private ForceUnit buildForceUnit(mekhq.campaign.unit.Unit unit) { + ForceUnit.Builder forceUnitBuilder = ForceUnit.newBuilder() + .setId(unit.getId().toString()) + .setName(unit.getName()); + + mekhq.campaign.personnel.Person commander = unit.getCommander(); + if (commander != null) { + forceUnitBuilder.setCommander(commander.getName()); + } + + return forceUnitBuilder.build(); + } } } diff --git a/MekHQ/src/mekhq/online/events/CampaignDetailsUpdatedEvent.java b/MekHQ/src/mekhq/online/events/CampaignDetailsUpdatedEvent.java new file mode 100644 index 0000000000..057c758d5a --- /dev/null +++ b/MekHQ/src/mekhq/online/events/CampaignDetailsUpdatedEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.online.events; + +import java.util.UUID; + +import megamek.common.event.MMEvent; + +public class CampaignDetailsUpdatedEvent extends MMEvent { + private final UUID id; + + public CampaignDetailsUpdatedEvent(UUID id) { + this.id = id; + } + + public UUID getId() { + return id; + } +} diff --git a/MekHQ/src/mekhq/online/forces/RemoteForce.java b/MekHQ/src/mekhq/online/forces/RemoteForce.java new file mode 100644 index 0000000000..88f0a44669 --- /dev/null +++ b/MekHQ/src/mekhq/online/forces/RemoteForce.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.online.forces; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import mekhq.online.Force; + +public class RemoteForce { + private static final RemoteForce EMPTY_FORCE = new RemoteForce(-1, "", Collections.emptyList(), Collections.emptyList()); + + private final int id; + private final String name; + private final List subForces; + private final List units; + + public RemoteForce(int id, String name, List subForces, List units) { + this.id = id; + this.name = name; + this.subForces = Collections.unmodifiableList(subForces); + this.units = Collections.unmodifiableList(units); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public List getSubForces() { + return subForces; + } + + public List getUnits() { + return units; + } + + public static RemoteForce build(Force force) { + return new RemoteForce(force.getId(), force.getName(), build(force.getSubForcesList()), RemoteUnit.build(force.getUnitsList())); + } + + public static List build(List subForces) { + List remoteForces = new ArrayList<>(subForces.size()); + for (Force force : subForces) { + remoteForces.add(build(force)); + } + return remoteForces; + } + + public List getAllChildren() { + List children = new ArrayList<>(subForces.size() + units.size()); + children.addAll(subForces); + children.addAll(units); + return children; + } + + public static RemoteForce emptyForce() { + return EMPTY_FORCE; + } +} diff --git a/MekHQ/src/mekhq/online/forces/RemoteTOE.java b/MekHQ/src/mekhq/online/forces/RemoteTOE.java new file mode 100644 index 0000000000..570284884a --- /dev/null +++ b/MekHQ/src/mekhq/online/forces/RemoteTOE.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.online.forces; + +public class RemoteTOE { + public static final RemoteTOE EMPTY_TOE = new RemoteTOE(RemoteForce.emptyForce()); + + private final RemoteForce forces; + + public RemoteTOE(RemoteForce forces) { + this.forces = forces; + } + + public RemoteForce getForces() { + return forces; + } +} diff --git a/MekHQ/src/mekhq/online/forces/RemoteUnit.java b/MekHQ/src/mekhq/online/forces/RemoteUnit.java new file mode 100644 index 0000000000..4d8c48913c --- /dev/null +++ b/MekHQ/src/mekhq/online/forces/RemoteUnit.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 The MegaMek Team. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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 MekHQ. If not, see . + */ +package mekhq.online.forces; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import mekhq.online.ForceUnit; + +public class RemoteUnit { + private final UUID id; + private final String name; + private final String commander; + + public RemoteUnit(UUID id, String name, String commander) { + this.id = id; + this.name = name; + this.commander = commander; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCommander() { + return commander; + } + + public static RemoteUnit build(ForceUnit unit) { + return new RemoteUnit(UUID.fromString(unit.getId()), unit.getName(), unit.getCommander()); + } + + public static List build(List units) { + List remoteUnits = new ArrayList<>(units.size()); + for (ForceUnit unit : units) { + remoteUnits.add(build(unit)); + } + return remoteUnits; + } +} From 701e1812a8b7cd6f97be25e99a43fde7995ffecd Mon Sep 17 00:00:00 2001 From: Christopher Date: Thu, 19 Mar 2020 22:39:45 -0400 Subject: [PATCH 19/20] Add initial work to define playing a scenario together --- MekHQ/src/main/proto/Service.proto | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index 42f96800f7..d4b6e19ffe 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -330,3 +330,119 @@ message ForceUnit { // The name of the Unit's commander. string commander = 3; } + +// Represents a Scenario within a Campaign. +message ScenarioIdentifier { + // The unique identifier of the Campaign + // who owns the scenario + string source_id = 1; + + // The unique identifier of the Contract + // for the source campaign + int32 contract_id = 2; + + // The unique identifier of the Scenario + // within the Contract + int32 scenario_id = 3; +} + +// Represents a Force assigned to a Scenario +message ScenarioForce { + // The unique identifier of the Campaign + // which owns this force, or null if + // the owner is a bot. + string owner_id = 1; + + // The name of the force's owner. + string owner_name = 2; + + // The team number of the force's owner. + int32 team = 3; + + // An optional Force assigned to the Scenario. + Force force = 4; + + // Zero-or-more units assigned to the Scenario. + repeated ForceUnit units = 5; +} + +// Represents the details of a Scenario. +message ScenarioDetails { + // The unique identifier the Scenario. + ScenarioIdentifier scenario = 1; + + // The date of the scenario (ISO 8601). + string date = 2; + + // The name of the Scenario. + string name = 3; + + // A description of the Scenario. + string description = 4; + + // Zero or more objectives for the Scenario. + repeated ScenarioObjective objectives = 5; + + // The forces assigned to the Scenario + repeated ScenarioForce forces = 6; +} + +// Represents an invitation to join a Scenario. +message ScenarioInvitation { + // The details of the Scenario. + ScenarioDetails scenario = 1; + + // The unique identifier of the Campaign + // who is receiving the invitation + string target_id = 2; +} + +// Represents a message sent when the source or +// target campaign would no longer like to participate +// in a scenario together. +message ScenarioInvitationCancelled { + // The unique identifier the Scenario + ScenarioIdentifier scenario_id = 1; + + // The unique identifier of the Campaign + // who recieved the invitation + string target_id = 2; +} + +// Represents a message sent when the target +// campaign accepts a request to participate +// in a scenario. +message ScenarioInvitationAcceptance { + // The unique identifier the Scenario + ScenarioIdentifier scenario_id = 1; + + // The unique identifier of the Campaign + // who recieved the invitation + string target_id = 2; +} + +// Represents a message sent when the details +// of a scenario are updated. +message ScenarioDetailsUpdated { + // The details of the Scenario. + ScenarioDetails scenario = 1; + + // The unique identifier of the Campaign + // who should receive the update. + string target_id = 2; +} + +// Represents a message sent when the target +// campaign updates their units deployed to +// the scenario. +message ScenarioUnitsUpdated { + // The unique identifier the Scenario + ScenarioIdentifier scenario_id = 1; + + // The unique identifier of the Campaign + // who recieved the invitation + string target_id = 2; + + // Zero or more forces attached to the Scenario. + repeated ScenarioForce attached_forces = 3; +} From b14b6a935fe7c8158bc05147033414a9a55486a0 Mon Sep 17 00:00:00 2001 From: Christopher Date: Thu, 19 Mar 2020 23:07:02 -0400 Subject: [PATCH 20/20] Add force deployment and scenario objectives --- MekHQ/src/main/proto/Service.proto | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/MekHQ/src/main/proto/Service.proto b/MekHQ/src/main/proto/Service.proto index d4b6e19ffe..273be9ef03 100644 --- a/MekHQ/src/main/proto/Service.proto +++ b/MekHQ/src/main/proto/Service.proto @@ -346,6 +346,18 @@ message ScenarioIdentifier { int32 scenario_id = 3; } +// Represents the deployment details of a Force +// within a Scenario. +message ForceDeployment { + // The number of rounds the force will be delayed + // upon deployment. + int32 deployment_delay = 1; + + // The edge of the board the force will be deployed + // to. + int32 board_start = 2; +} + // Represents a Force assigned to a Scenario message ScenarioForce { // The unique identifier of the Campaign @@ -364,6 +376,24 @@ message ScenarioForce { // Zero-or-more units assigned to the Scenario. repeated ForceUnit units = 5; + + // Details of how the force will be deployed + // in the Scenario. + // + // This is sent by the Host Campaign only. + ForceDeployment deployment = 6; +} + +// Represents an objective within a Scenario. +message ScenarioObjective { + // A description of the objective. + string description = 1; + + // Zero or more forces affected by the objective. + repeated string affected_forces = 2; + + // Zero or more units affected by the objective. + repeated string affected_units = 3; } // Represents the details of a Scenario.