From b97c54a2fc96ceca7015b23edf4accb81e48f1bf Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Fri, 13 Mar 2020 14:21:09 -0400 Subject: [PATCH] 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 6b90c1ff0aa..42f96800f7e 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 2faa8155776..422b445b7c3 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 df109503ad6..21a82a4cea4 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 00000000000..f9d8649fff4 --- /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 00000000000..e4ca6016231 --- /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 2637e989ccf..76a953559c8 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 dd089e2fa70..49dba47cd79 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 00000000000..057c758d5ac --- /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 00000000000..88f0a446698 --- /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 00000000000..570284884a9 --- /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 00000000000..4d8c48913ca --- /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; + } +}