diff --git a/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java b/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java index a62716b928..20f8b2b18b 100644 --- a/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java +++ b/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java @@ -24,6 +24,7 @@ import javax.swing.SwingUtilities; import net.rptools.clientserver.simple.MessageHandler; import net.rptools.lib.MD5Key; +import net.rptools.maptool.client.events.PlayerStatusChanged; import net.rptools.maptool.client.functions.ExecFunction; import net.rptools.maptool.client.functions.MacroLinkFunction; import net.rptools.maptool.client.ui.MapToolFrame; @@ -159,6 +160,7 @@ public void handleMessage(String id, byte[] message) { case UPDATE_GM_MACROS_MSG -> handle(msg.getUpdateGmMacrosMsg()); case UPDATE_EXPOSED_AREA_META_MSG -> handle(msg.getUpdateExposedAreaMetaMsg()); case UPDATE_TOKEN_MOVE_MSG -> handle(msg.getUpdateTokenMoveMsg()); + case UPDATE_PLAYER_STATUS_MSG -> handle(msg.getUpdatePlayerStatusMsg()); default -> log.warn(msgType + "not handled."); } log.info(id + " handled: " + msgType); @@ -999,4 +1001,27 @@ private void handle(BootPlayerMsg bootPlayerMsg) { MapTool.showInformation("You have been booted from the server."); }); } + + private void handle(UpdatePlayerStatusMsg updatePlayerStatusMsg) { + var playerName = updatePlayerStatusMsg.getPlayer(); + var zoneGUID = GUID.valueOf(updatePlayerStatusMsg.getZoneGuid()); + var loaded = updatePlayerStatusMsg.getLoaded(); + + Player player = + MapTool.getPlayerList().stream() + .filter(x -> x.getName().equals(playerName)) + .findFirst() + .orElse(null); + + if (player == null) { + log.info("UpdatePlayerStatusMsg failed. No player with name: '" + playerName + "'"); + return; + } + + player.setZoneId(zoneGUID); + player.setLoaded(loaded); + + final var eventBus = new MapToolEventBus().getMainEventBus(); + eventBus.post(new PlayerStatusChanged(player)); + } } diff --git a/src/main/java/net/rptools/maptool/client/MapTool.java b/src/main/java/net/rptools/maptool/client/MapTool.java index 7ce22c59aa..7f963d1d3d 100644 --- a/src/main/java/net/rptools/maptool/client/MapTool.java +++ b/src/main/java/net/rptools/maptool/client/MapTool.java @@ -92,6 +92,7 @@ import net.rptools.maptool.model.player.Player; import net.rptools.maptool.model.player.PlayerDatabase; import net.rptools.maptool.model.player.PlayerDatabaseFactory; +import net.rptools.maptool.model.player.PlayerZoneListener; import net.rptools.maptool.model.player.Players; import net.rptools.maptool.model.zones.TokensAdded; import net.rptools.maptool.model.zones.TokensRemoved; @@ -155,6 +156,7 @@ public class MapTool { private static List playerList; private static LocalPlayer player; + private static PlayerZoneListener playerZoneListener; private static MapToolConnection conn; private static ClientMessageHandler handler; @@ -668,6 +670,7 @@ private static void initialize() { try { player = new LocalPlayer("", Player.Role.GM, ServerConfig.getPersonalServerGMPassword()); + playerZoneListener = new PlayerZoneListener(); Campaign cmpgn = CampaignFactory.createBasicCampaign(); // This was previously being done in the server thread and didn't always get done // before the campaign was accessed by the postInitialize() method below. diff --git a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java index 74c744d286..0f83ff82f1 100644 --- a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java +++ b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java @@ -35,6 +35,7 @@ import net.rptools.maptool.model.gamedata.proto.GameDataDto; import net.rptools.maptool.model.gamedata.proto.GameDataValueDto; import net.rptools.maptool.model.library.addon.TransferableAddOnLibrary; +import net.rptools.maptool.model.player.Player; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.ServerCommand; import net.rptools.maptool.server.ServerMessageHandler; @@ -788,6 +789,16 @@ public void updateTokenProperty( TokenPropertyValueDto.newBuilder().setDoubleValue(value2.doubleValue()).build()); } + @Override + public void updatePlayerStatus(Player player) { + var msg = + UpdatePlayerStatusMsg.newBuilder() + .setPlayer(player.getName()) + .setZoneGuid(player.getZoneId().toString()) + .setLoaded(player.getLoaded()); + makeServerCall(Message.newBuilder().setUpdatePlayerStatusMsg(msg).build()); + } + /** * Some events become obsolete very quickly, such as dragging a token around. This queue always * has exactly one element, the more current version of the event. The event is then dispatched at diff --git a/src/main/java/net/rptools/maptool/client/events/PlayerStatusChanged.java b/src/main/java/net/rptools/maptool/client/events/PlayerStatusChanged.java new file mode 100644 index 0000000000..a76b45709a --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/events/PlayerStatusChanged.java @@ -0,0 +1,19 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.events; + +import net.rptools.maptool.model.player.Player; + +public record PlayerStatusChanged(Player player) {} diff --git a/src/main/java/net/rptools/maptool/client/events/ZoneLoaded.java b/src/main/java/net/rptools/maptool/client/events/ZoneLoaded.java new file mode 100644 index 0000000000..7b2178c6da --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/events/ZoneLoaded.java @@ -0,0 +1,19 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.events; + +import net.rptools.maptool.model.Zone; + +public record ZoneLoaded(Zone zone) {} diff --git a/src/main/java/net/rptools/maptool/client/events/ZoneLoading.java b/src/main/java/net/rptools/maptool/client/events/ZoneLoading.java new file mode 100644 index 0000000000..10fd977f35 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/events/ZoneLoading.java @@ -0,0 +1,19 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.events; + +import net.rptools.maptool.model.Zone; + +public record ZoneLoading(Zone zone) {} diff --git a/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java b/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java new file mode 100644 index 0000000000..70ddb6e92e --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java @@ -0,0 +1,124 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.swing; + +import com.google.common.eventbus.Subscribe; +import java.awt.Dimension; +import javax.swing.Icon; +import javax.swing.JLabel; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.events.PlayerConnected; +import net.rptools.maptool.client.events.PlayerDisconnected; +import net.rptools.maptool.client.events.PlayerStatusChanged; +import net.rptools.maptool.client.events.ServerStopped; +import net.rptools.maptool.client.ui.theme.Icons; +import net.rptools.maptool.client.ui.theme.RessourceManager; +import net.rptools.maptool.events.MapToolEventBus; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.player.Player; + +/** */ +public class PlayersLoadingStatusBar extends JLabel { + private static final Dimension minSize = new Dimension(60, 10); + private static Icon checkmarkIcon; + private static Icon loadingIcon; + + static { + checkmarkIcon = RessourceManager.getSmallIcon(Icons.STATUSBAR_PLAYERS_DONE_LOADING); + loadingIcon = RessourceManager.getSmallIcon(Icons.STATUSBAR_PLAYERS_LOADING); + } + + public PlayersLoadingStatusBar() { + refreshCount(); + new MapToolEventBus().getMainEventBus().register(this); + } + + private void refreshCount() { + var players = MapTool.getPlayerList(); + var total = players.size(); + var loaded = players.stream().filter(x -> x.getLoaded()).count(); + + var sb = + new StringBuilder(I18N.getText("ConnectionStatusPanel.playersLoadedZone", loaded, total)); + + var self = MapTool.getPlayer(); + + for (Player player : players) { + // The Player in the list is a seperate entity to the one from MapTool.getPlayer() + // So it doesn't have the correct status data. + if (player.getName().equals(self.getName())) { + player = self; + } + var zone = + player.getZoneId() == null ? null : MapTool.getCampaign().getZone(player.getZoneId()); + + var text = + I18N.getText( + player.getLoaded() ? "connections.playerIsInZone" : "connections.playerIsLoadingZone", + player.toString(), + zone == null ? null : zone.getDisplayName()); + sb.append("\n"); + sb.append(text); + } + + String text = loaded + "/" + total; + + if (total == loaded) { + setIcon(checkmarkIcon); + } else { + setIcon(loadingIcon); + } + this.setText(text); + this.setToolTipText(sb.toString()); + } + + /* + * (non-Javadoc) + * + * @see javax.swing.JComponent#getMinimumSize() + */ + public Dimension getMinimumSize() { + return minSize; + } + + /* + * (non-Javadoc) + * + * @see javax.swing.JComponent#getPreferredSize() + */ + public Dimension getPreferredSize() { + return getMinimumSize(); + } + + @Subscribe + private void onPlayerConnected(PlayerConnected event) { + refreshCount(); + } + + @Subscribe + private void onPlayerStatusChanged(PlayerStatusChanged event) { + refreshCount(); + } + + @Subscribe + private void onPlayerDisconnected(PlayerDisconnected event) { + refreshCount(); + } + + @Subscribe + private void onServerStopped(ServerStopped event) { + refreshCount(); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java index fea91d1135..f182530395 100644 --- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java @@ -44,6 +44,7 @@ import net.rptools.maptool.client.AppActions.ClientAction; import net.rptools.maptool.client.events.ZoneActivated; import net.rptools.maptool.client.events.ZoneDeactivated; +import net.rptools.maptool.client.events.ZoneLoading; import net.rptools.maptool.client.swing.AboutDialog; import net.rptools.maptool.client.swing.AppHomeDiskSpaceStatusBar; import net.rptools.maptool.client.swing.AssetCacheStatusBar; @@ -53,6 +54,7 @@ import net.rptools.maptool.client.swing.ImageCacheStatusBar; import net.rptools.maptool.client.swing.ImageChooserDialog; import net.rptools.maptool.client.swing.MemoryStatusBar; +import net.rptools.maptool.client.swing.PlayersLoadingStatusBar; import net.rptools.maptool.client.swing.PositionalLayout; import net.rptools.maptool.client.swing.ProgressStatusBar; import net.rptools.maptool.client.swing.SpacerStatusBar; @@ -166,6 +168,7 @@ public class MapToolFrame extends DefaultDockableHolder implements WindowListene private AssetCacheStatusBar assetCacheStatusBar; private ImageCacheStatusBar imageCacheStatusBar; private AppHomeDiskSpaceStatusBar appHomeDiskSpaceStatusBar; + private PlayersLoadingStatusBar playersLoadingStatusBar; private ZoomStatusBar zoomStatusBar; private JLabel chatActionLabel; private boolean fullScreenToolsShown; @@ -388,6 +391,7 @@ public MapToolFrame(JMenuBar menuBar) { statusPanel.addPanel(getAppHomeDiskSpaceStatusBar()); statusPanel.addPanel(getCoordinateStatusBar()); statusPanel.addPanel(getZoomStatusBar()); + statusPanel.addPanel(getPlayersLoadingStatusBar()); statusPanel.addPanel(MemoryStatusBar.getInstance()); // statusPanel.addPanel(progressBar); statusPanel.addPanel(connectionStatusPanel); @@ -915,6 +919,13 @@ public void showControlPanel(JPanel... panels) { visibleControlPanel = layoutPanel; } + public PlayersLoadingStatusBar getPlayersLoadingStatusBar() { + if (playersLoadingStatusBar == null) { + playersLoadingStatusBar = new PlayersLoadingStatusBar(); + } + return playersLoadingStatusBar; + } + public ZoomStatusBar getZoomStatusBar() { if (zoomStatusBar == null) { zoomStatusBar = new ZoomStatusBar(); @@ -1521,6 +1532,8 @@ private void stopTokenDrag() { public void setCurrentZoneRenderer(ZoneRenderer renderer) { // Flush first so that the new zone renderer can inject the newly needed images if (renderer != null) { + new MapToolEventBus().getMainEventBus().post(new ZoneLoading(renderer.getZone())); + ImageManager.flush(renderer.getZone().getAllAssetIds()); } else { ImageManager.flush(); diff --git a/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java b/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java index 4db2b6b98a..3a2638b04d 100644 --- a/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java @@ -32,6 +32,7 @@ import net.rptools.maptool.client.AppActions; import net.rptools.maptool.client.events.PlayerConnected; import net.rptools.maptool.client.events.PlayerDisconnected; +import net.rptools.maptool.client.events.PlayerStatusChanged; import net.rptools.maptool.client.events.ServerStopped; import net.rptools.maptool.client.swing.PopupListener; import net.rptools.maptool.events.MapToolEventBus; @@ -81,6 +82,7 @@ public ClientConnectionPanel() { listModel = new DefaultListModel<>(); list.setModel(listModel); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setCellRenderer(new PlayerListCellRenderer()); list.addMouseListener(createPopupListener()); @@ -110,6 +112,14 @@ private void onPlayerConnected(PlayerConnected event) { listModel.addElement(event.player()); } + @Subscribe + private void onPlayerStatusChanged(PlayerStatusChanged event) { + var index = listModel.indexOf(event.player()); + if (index != -1) { + listModel.set(index, event.player()); + } + } + @Subscribe private void onPlayerDisconnected(PlayerDisconnected event) { listModel.removeElement(event.player()); diff --git a/src/main/java/net/rptools/maptool/client/ui/connections/PlayerListCellRenderer.java b/src/main/java/net/rptools/maptool/client/ui/connections/PlayerListCellRenderer.java new file mode 100644 index 0000000000..65ff110126 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/connections/PlayerListCellRenderer.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.connections; + +import java.awt.Component; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.player.Player; + +public class PlayerListCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent( + JList list, Object value, int index, boolean isSelected, boolean hasFocus) { + if (value instanceof Player player) { + var zone = + player.getZoneId() == null ? null : MapTool.getCampaign().getZone(player.getZoneId()); + + String text = + I18N.getText( + player.getLoaded() ? "connections.playerIsInZone" : "connections.playerIsLoadingZone", + player.toString(), + zone == null ? null : zone.getDisplayName()); + return super.getListCellRendererComponent(list, text, index, isSelected, hasFocus); + } + return super.getListCellRendererComponent(list, value, index, isSelected, hasFocus); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java index 9d4b7bf217..5bd99e6141 100644 --- a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java +++ b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java @@ -102,6 +102,8 @@ public enum Icons { STATUSBAR_ASSET_CACHE, STATUSBAR_FREE_SPACE, STATUSBAR_IMAGE_CACHE, + STATUSBAR_PLAYERS_DONE_LOADING, + STATUSBAR_PLAYERS_LOADING, STATUSBAR_RECEIVE_OFF, STATUSBAR_RECEIVE_ON, STATUSBAR_SERVER_CONNECTED, diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java index 154397c24a..a98db2f7b3 100644 --- a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java +++ b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java @@ -130,6 +130,8 @@ public class RessourceManager { put(Icons.STATUSBAR_ASSET_CACHE, IMAGE_DIR + "asset-status.png"); put(Icons.STATUSBAR_FREE_SPACE, IMAGE_DIR + "disk-space.png"); put(Icons.STATUSBAR_IMAGE_CACHE, IMAGE_DIR + "thumbnail-status.png"); + put(Icons.STATUSBAR_PLAYERS_DONE_LOADING, IMAGE_DIR + "currentIndicator.png"); + put(Icons.STATUSBAR_PLAYERS_LOADING, IMAGE_DIR + "loading.png"); put(Icons.STATUSBAR_RECEIVE_OFF, IMAGE_DIR + "activityOff.png"); put(Icons.STATUSBAR_RECEIVE_ON, IMAGE_DIR + "receiveOn.png"); put(Icons.STATUSBAR_SERVER_CONNECTED, IMAGE_DIR + "computer_on.png"); @@ -318,6 +320,8 @@ public class RessourceManager { put(Icons.STATUSBAR_ASSET_CACHE, ROD_ICONS + "bottom/Assets Cache.svg"); put(Icons.STATUSBAR_FREE_SPACE, ROD_ICONS + "bottom/Free Space.svg"); put(Icons.STATUSBAR_IMAGE_CACHE, ROD_ICONS + "bottom/Image Thumbs Cache.svg"); + put(Icons.STATUSBAR_PLAYERS_DONE_LOADING, ROD_ICONS + "misc/Select All Tokens.svg"); + put(Icons.STATUSBAR_PLAYERS_LOADING, ROD_ICONS + "bottom/Assets Cache.svg"); put(Icons.STATUSBAR_RECEIVE_OFF, ROD_ICONS + "bottom/Receive Data - Inactive.svg"); put(Icons.STATUSBAR_RECEIVE_ON, ROD_ICONS + "bottom/Receive Data - Active.svg"); put(Icons.STATUSBAR_SERVER_CONNECTED, ROD_ICONS + "bottom/Server Status - Connected.svg"); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index b1b285502b..69a343bff8 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -40,6 +40,7 @@ import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.*; +import net.rptools.maptool.client.events.ZoneLoaded; import net.rptools.maptool.client.functions.TokenMoveFunctions; import net.rptools.maptool.client.swing.ImageBorder; import net.rptools.maptool.client.swing.ImageLabel; @@ -1959,6 +1960,8 @@ public boolean isLoading() { if (isLoaded) { // Notify the token tree that it should update MapTool.getFrame().updateTokenTree(); + + new MapToolEventBus().getMainEventBus().post(new ZoneLoaded(zone)); } return !isLoaded; } diff --git a/src/main/java/net/rptools/maptool/model/player/Player.java b/src/main/java/net/rptools/maptool/model/player/Player.java index aba313918b..7adace91e7 100644 --- a/src/main/java/net/rptools/maptool/model/player/Player.java +++ b/src/main/java/net/rptools/maptool/model/player/Player.java @@ -15,6 +15,7 @@ package net.rptools.maptool.model.player; import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.GUID; import net.rptools.maptool.server.proto.PlayerDto; import net.rptools.maptool.util.cipher.CipherUtil; @@ -45,6 +46,8 @@ public String toString() { private String name; // Primary Key private String role; + private GUID zoneId; + private boolean loaded; private transient CipherUtil.Key password; private transient Role actualRole; @@ -57,6 +60,8 @@ public Player() { this.name = name; this.role = role.name(); this.password = password; + this.zoneId = null; + this.loaded = true; } protected void setRole(Role role) { @@ -64,6 +69,22 @@ protected void setRole(Role role) { actualRole = role; } + public GUID getZoneId() { + return zoneId; + } + + public void setZoneId(GUID zoneId) { + this.zoneId = zoneId; + } + + public boolean getLoaded() { + return loaded; + } + + public void setLoaded(boolean loaded) { + this.loaded = loaded; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Player)) { @@ -122,10 +143,19 @@ public static Player fromDto(PlayerDto dto) { var player = new Player(); player.name = dto.getName(); player.role = dto.getRole(); + player.zoneId = dto.getZoneGuid().equals("") ? null : GUID.valueOf(dto.getZoneGuid()); + player.loaded = dto.getLoaded(); + return player; } public PlayerDto toDto() { - return PlayerDto.newBuilder().setName(name).setRole(role).build(); + var builder = PlayerDto.newBuilder().setName(name).setRole(role).setLoaded(loaded); + + if (zoneId != null) { + builder.setZoneGuid(zoneId.toString()); + } + + return builder.build(); } } diff --git a/src/main/java/net/rptools/maptool/model/player/PlayerZoneListener.java b/src/main/java/net/rptools/maptool/model/player/PlayerZoneListener.java new file mode 100644 index 0000000000..df701f5cee --- /dev/null +++ b/src/main/java/net/rptools/maptool/model/player/PlayerZoneListener.java @@ -0,0 +1,80 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.model.player; + +import com.google.common.eventbus.Subscribe; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.events.PlayerStatusChanged; +import net.rptools.maptool.client.events.ZoneLoaded; +import net.rptools.maptool.client.events.ZoneLoading; +import net.rptools.maptool.events.MapToolEventBus; + +public class PlayerZoneListener { + public PlayerZoneListener() { + new MapToolEventBus().getMainEventBus().register(this); + } + + @Subscribe + public void OnZoneLoading(ZoneLoading event) { + var player = MapTool.getPlayer(); + player.setLoaded(false); + player.setZoneId(event.zone().getId()); + + // To keep everything tidy we're also updating the player entry + // in the player list since they are seperate entities + var playerListPlayer = + MapTool.getPlayerList().stream() + .filter(x -> x.getName().equals(player.getName())) + .findAny() + .orElse(null); + + // On startup when we start to load the grassland zone the player list is still empty so we skip + if (playerListPlayer != null) { + playerListPlayer.setLoaded(false); + playerListPlayer.setZoneId(event.zone().getId()); + } + + final var eventBus = new MapToolEventBus().getMainEventBus(); + eventBus.post(new PlayerStatusChanged(player)); + + MapTool.serverCommand().updatePlayerStatus(player); + } + + @Subscribe + public void OnZoneLoaded(ZoneLoaded event) { + var player = MapTool.getPlayer(); + player.setLoaded(true); + player.setZoneId(event.zone().getId()); + + // To keep everything tidy we're also updating the player entry + // in the player list since they are seperate entities + var playerListPlayer = + MapTool.getPlayerList().stream() + .filter(x -> x.getName().equals(player.getName())) + .findAny() + .orElse(null); + + // On startup when we load the grassland zone the player list is still empty so we skip this + if (playerListPlayer != null) { + playerListPlayer.setLoaded(true); + playerListPlayer.setZoneId(event.zone().getId()); + } + + final var eventBus = new MapToolEventBus().getMainEventBus(); + eventBus.post(new PlayerStatusChanged(player)); + + MapTool.serverCommand().updatePlayerStatus(player); + } +} diff --git a/src/main/java/net/rptools/maptool/server/MapToolServer.java b/src/main/java/net/rptools/maptool/server/MapToolServer.java index 43da26dd9f..0d3bb8c0b7 100644 --- a/src/main/java/net/rptools/maptool/server/MapToolServer.java +++ b/src/main/java/net/rptools/maptool/server/MapToolServer.java @@ -31,6 +31,7 @@ import net.rptools.maptool.common.MapToolConstants; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Campaign; +import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.TextMessage; import net.rptools.maptool.model.player.PlayerDatabase; import net.rptools.maptool.model.player.PlayerDatabaseFactory; @@ -139,6 +140,12 @@ public boolean isPlayerConnected(String id) { return conn.getPlayer(id) != null; } + public void updatePlayerStatus(String playerName, GUID zoneId, boolean loaded) { + var player = conn.getPlayer(playerName); + player.setLoaded(loaded); + player.setZoneId(zoneId); + } + public void setCampaign(Campaign campaign) { // Don't allow null campaigns, but allow the campaign to be cleared out if (campaign == null) { diff --git a/src/main/java/net/rptools/maptool/server/ServerCommand.java b/src/main/java/net/rptools/maptool/server/ServerCommand.java index ab4d28d4cf..ae411df169 100644 --- a/src/main/java/net/rptools/maptool/server/ServerCommand.java +++ b/src/main/java/net/rptools/maptool/server/ServerCommand.java @@ -28,6 +28,7 @@ import net.rptools.maptool.model.gamedata.proto.GameDataDto; import net.rptools.maptool.model.gamedata.proto.GameDataValueDto; import net.rptools.maptool.model.library.addon.TransferableAddOnLibrary; +import net.rptools.maptool.model.player.Player; public interface ServerCommand { void bootPlayer(String player); @@ -228,4 +229,6 @@ void updateTokenProperty( void updateTokenProperty(Token token, Token.Update update, String value1, boolean value2); void updateTokenProperty(Token token, Token.Update update, String value, BigDecimal value2); + + void updatePlayerStatus(Player player); } diff --git a/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java b/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java index 6a60932eb0..8b77c721c9 100644 --- a/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java +++ b/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java @@ -255,6 +255,11 @@ public void handleMessage(String id, byte[] message) { handle(msg.getUpdateExposedAreaMetaMsg()); sendToClients(id, msg); } + case UPDATE_PLAYER_STATUS_MSG -> { + handle(id, msg.getUpdatePlayerStatusMsg()); + sendToClients(id, msg); + } + default -> log.warn(msgType + "not handled."); } log.info("from " + id + " handled: " + msgType); @@ -577,6 +582,16 @@ private void handle(BootPlayerMsg bootPlayerMsg) { server.releaseClientConnection(server.getConnectionId(bootPlayerMsg.getPlayerName())); } + private void handle(String id, UpdatePlayerStatusMsg updatePlayerStatusMsg) { + var playerName = updatePlayerStatusMsg.getPlayer(); + var zoneId = + updatePlayerStatusMsg.getZoneGuid().equals("") + ? null + : GUID.valueOf(updatePlayerStatusMsg.getZoneGuid()); + var loaded = updatePlayerStatusMsg.getLoaded(); + server.updatePlayerStatus(playerName, zoneId, loaded); + } + private void sendToClients(String excludedId, Message message) { server.getConnection().broadcastMessage(new String[] {excludedId}, message); } diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto index 6caefce8f4..d6adb4c212 100644 --- a/src/main/proto/data_transfer_objects.proto +++ b/src/main/proto/data_transfer_objects.proto @@ -443,6 +443,8 @@ message TextMessageDto { message PlayerDto { string name = 1; string role = 2; + string zone_guid = 3; + bool loaded = 4; } enum AssetDtoType { diff --git a/src/main/proto/message.proto b/src/main/proto/message.proto index 5f348d1fa6..5deea9c19c 100644 --- a/src/main/proto/message.proto +++ b/src/main/proto/message.proto @@ -88,5 +88,6 @@ message Message { RemoveDataStoreMsg remove_data_store_msg = 71; RemoveDataNamespaceMsg remove_data_namespace_msg = 72; RemoveDataMsg remove_data_msg = 73; + UpdatePlayerStatusMsg update_player_status_msg = 74; } } diff --git a/src/main/proto/message_types.proto b/src/main/proto/message_types.proto index 452764efdf..d2e4c92345 100644 --- a/src/main/proto/message_types.proto +++ b/src/main/proto/message_types.proto @@ -376,4 +376,10 @@ message RemoveDataMsg { string type = 1; string namespace = 2; string name = 3; +} + +message UpdatePlayerStatusMsg { + string player = 1; + string zone_guid = 2; + bool loaded = 3; } \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/client/image/loading.png b/src/main/resources/net/rptools/maptool/client/image/loading.png new file mode 100644 index 0000000000..6b9540e50e Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/image/loading.png differ diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 759b758640..9c1b1c93e0 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -227,9 +227,10 @@ ConnectionInfoDialog.address.external = External Address: ConnectionInfoDialog.port = Port: ConnectionInfoDialog.discovering = Discovering... -ConnectionStatusPanel.notConnected = Not connected -ConnectionStatusPanel.runningServer = Running a Server -ConnectionStatusPanel.serverConnected = Connected to Server +ConnectionStatusPanel.notConnected = Not connected +ConnectionStatusPanel.runningServer = Running a Server +ConnectionStatusPanel.playersLoadedZone = {0} out of {1} players have loaded their zone +ConnectionStatusPanel.serverConnected = Connected to Server CoordinateStatusBar.mapCoordinates = Map Coordinates @@ -2198,6 +2199,8 @@ panel.Chat.description = Dockable chat window. panel.Connections = Connections connections.tab.connected = Connected connections.tab.pending = Pending +connections.playerIsInZone = {0} is in zone {1}. +connections.playerIsLoadingZone = {0} is loading zone {1}. pendingConnection.label.playerName = Name pendingConnection.label.pin = PIN pendingConnection.column.title = Players attempting to connect