Skip to content

Commit

Permalink
Automatic Bot Moderators (#12758)
Browse files Browse the repository at this point in the history
Updates to give the host of a network game 'mod' powers. If the game is a bot, then also give the 'oldest' player 'mod' powers.

Mod powers allow:

    disconnect players
    ban players (effective until restart of bot)

The bot case is interesting since the 'oldest' player can change. For example, two players enter a bot (to the staging screen), first is mod & then leaves. Now the second player needs to become moderator.

To achieve this we add a "moderator promoted" message which is sent by the server to inform players of the new moderator.
  • Loading branch information
DanVanAtta authored Jul 24, 2024
1 parent 09fa340 commit 31479d3
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class ChatParticipant implements Serializable {
@Nonnull private final String playerChatId;

/** True if the player has moderator privileges. */
private final boolean isModerator;
@Setter private boolean isModerator;

/** Status is custom text set by players, eg: "AFK" or "Looking for a game". */
@Setter @Nullable private String status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.google.common.collect.EvictingQueue;
import com.google.common.collect.Sets;
import games.strategy.net.IMessageListener;
import games.strategy.net.Messengers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -56,6 +58,14 @@ public Chat(final ChatTransmitter chatTransmitter) {
updateConnections();
}

public void addMessengersListener(IMessageListener messageListener) {
if (chatTransmitter instanceof MessengersChatTransmitter) {
((MessengersChatTransmitter) chatTransmitter)
.getMessengers()
.addMessageListener(messageListener);
}
}

private void updateConnections() {
final List<ChatParticipant> playerNames =
chatters.stream()
Expand Down Expand Up @@ -178,4 +188,12 @@ boolean isIgnored(final UserName userName) {
Collection<UserName> getOnlinePlayers() {
return chatters.stream().map(ChatParticipant::getUserName).collect(Collectors.toSet());
}

public Messengers getMessengers() {
if (!(chatTransmitter instanceof MessengersChatTransmitter)) {
throw new UnsupportedOperationException(
"getMessengers is to support legacy 'messengers' communication only");
}
return ((MessengersChatTransmitter) chatTransmitter).getMessengers();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package games.strategy.engine.chat;

import com.google.common.base.Strings;
import games.strategy.engine.framework.GameRunner;
import games.strategy.engine.message.MessageContext;
import games.strategy.engine.message.RemoteName;
import games.strategy.net.IConnectionChangeListener;
import games.strategy.net.INode;
import games.strategy.net.Messengers;
import games.strategy.net.ServerMessenger;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.triplea.domain.data.ChatParticipant;
Expand All @@ -26,21 +25,11 @@ public class ChatController implements IChatController {
private static final String CHAT_REMOTE = "_ChatRemote_";
private static final String CHAT_CHANNEL = "_ChatControl_";
private final Messengers messengers;
private final ServerMessenger serverMessenger;

private final String chatName;
private final Map<INode, Tag> chatters = new HashMap<>();

private final Predicate<INode> isModerator =
node -> {
if (chatters.isEmpty() && !GameRunner.headless()) {
return true;
} else if (GameRunner.headless() && chatters.size() == 1) {
return true;
} else {
return false;
}
};

private final Map<INode, PlayerChatId> chatterIds = new HashMap<>();
private final Map<UserName, String> chatterStatus = new HashMap<>();

Expand All @@ -62,9 +51,11 @@ public void connectionRemoved(final INode to) {
}
};

public ChatController(final String name, final Messengers messengers) {
public ChatController(
final String name, final Messengers messengers, ServerMessenger serverMessenger) {
chatName = name;
this.messengers = messengers;
this.serverMessenger = serverMessenger;
chatChannel = getChatChannelName(name);
messengers.registerRemote(this, getChatControllerRemoteName(name));
messengers.addConnectionChangeListener(connectionChangeListener);
Expand Down Expand Up @@ -116,7 +107,7 @@ private IChatChannel getChatBroadcaster() {
public Collection<ChatParticipant> joinChat() {
final INode node = MessageContext.getSender();
log.info("Chatter:" + node + " is joining chat:" + chatName);
final Tag tag = isModerator.test(node) ? Tag.MODERATOR : Tag.NONE;
final Tag tag = Tag.NONE;
synchronized (mutex) {
final PlayerChatId id = PlayerChatId.newId();
chatterIds.put(node, id);
Expand All @@ -126,14 +117,14 @@ public Collection<ChatParticipant> joinChat() {
ChatParticipant.builder()
.userName(node.getPlayerName().getValue())
.playerChatId(id.getValue())
.isModerator(tag == Tag.MODERATOR)
.isModerator(serverMessenger.isModerator(node))
.build());

return chatters.entrySet().stream()
.map(
entry ->
ChatParticipant.builder()
.isModerator(entry.getValue() == Tag.MODERATOR)
.isModerator(serverMessenger.isModerator(entry.getKey()))
.userName(entry.getKey().getPlayerName().getValue())
.playerChatId(chatterIds.get(entry.getKey()).getValue())
.status(chatterStatus.get(entry.getKey().getPlayerName()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package games.strategy.engine.chat;

import games.strategy.engine.framework.startup.mc.messages.ModeratorPromoted;
import games.strategy.net.IMessageListener;
import games.strategy.net.INode;
import games.strategy.triplea.settings.ClientSetting;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Insets;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.Serializable;
import javax.swing.Action;
import javax.swing.BoundedRangeModel;
import javax.swing.InputMap;
Expand Down Expand Up @@ -80,10 +84,21 @@ public enum ChatSoundProfile {

public ChatMessagePanel(
final Chat chat, final ChatSoundProfile chatSoundProfile, final ClipPlayer clipPlayer) {

this.chatSoundProfile = chatSoundProfile;
this.clipPlayer = clipPlayer;
init();
setChat(chat);

chat.addMessengersListener(
new IMessageListener() {
@Override
public void messageReceived(Serializable msg, INode from) {
if (msg instanceof ModeratorPromoted) {
addGenericMessage("Moderator Promoted: " + ((ModeratorPromoted) msg).getPlayerName());
}
}
});
}

private void init() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package games.strategy.engine.chat;

import games.strategy.engine.framework.startup.mc.messages.ModeratorMessage;
import games.strategy.engine.framework.startup.mc.messages.ModeratorPromoted;
import games.strategy.net.IMessageListener;
import games.strategy.net.INode;
import games.strategy.triplea.EngineImageLoader;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
Expand All @@ -26,6 +31,7 @@
import org.triplea.domain.data.ChatParticipant;
import org.triplea.domain.data.UserName;
import org.triplea.java.StringUtils;
import org.triplea.swing.EventThreadJOptionPane;
import org.triplea.swing.SwingAction;

/** A UI component that displays the players participating in a chat. */
Expand Down Expand Up @@ -53,6 +59,21 @@ public ChatPlayerPanel(final Chat chat) {
layoutComponents();
setupListeners();
setChat(chat);
chat.addMessengersListener(
new IMessageListener() {
@Override
public void messageReceived(Serializable msg, INode from) {
if (msg instanceof ModeratorPromoted) {
String newModerator = ((ModeratorPromoted) msg).getPlayerName();
for (int i = 0; i < listModel.getSize(); i++) {
if (listModel.get(i).getUserName().toString().equals(newModerator)) {
listModel.get(i).setModerator(true);
break;
}
}
}
}
});
}

/** Sets the chat whose players will be displayed in this panel. */
Expand Down Expand Up @@ -138,6 +159,7 @@ public void mouseReleased(final MouseEvent e) {
mouseOnPlayersList(e);
}
});
final JPanel panelReference = this;
actionFactories.add(
clickedOn -> {
// you can't slap or ignore yourself
Expand All @@ -155,7 +177,54 @@ public void mouseReleased(final MouseEvent e) {
final Action slap =
SwingAction.of(
"Slap " + clickedOn.getUserName(), e -> chat.sendSlap(clickedOn.getUserName()));
return List.of(slap, ignore);

// TODO: add check for if we are moderator
final Action disconnect =
SwingAction.of(
"Disconnect " + clickedOn.getUserName(),
e -> {
if (EventThreadJOptionPane.showConfirmDialog(
panelReference,
"Disconnect " + clickedOn.getUserName() + "?",
"Confirm Disconnect",
EventThreadJOptionPane.ConfirmDialogType.YES_NO)) {
chat.getMessengers()
.sendToServer(
ModeratorMessage.newDisconnect(clickedOn.getUserName().getValue()));
}
});
// TODO: add check for if we are moderator

final Action ban =
SwingAction.of(
"Ban " + clickedOn.getUserName(),
e -> {
if (EventThreadJOptionPane.showConfirmDialog(
panelReference,
"Ban " + clickedOn.getUserName() + "?",
"Confirm Ban",
EventThreadJOptionPane.ConfirmDialogType.YES_NO)) {
chat.getMessengers()
.sendToServer(
ModeratorMessage.newBan(clickedOn.getUserName().getValue()));
}
});

List<Action> availableActions = new ArrayList<>(List.of(slap, ignore));

boolean currentPlayerIsModerator = false;
for (int i = 0, n = listModel.getSize(); i < n; i++) {
var participant = listModel.get(i);
if (chat.getLocalUserName().equals(participant.getUserName())) {
currentPlayerIsModerator = participant.isModerator();
break;
}
}

if (currentPlayerIsModerator && !clickedOn.isModerator()) {
availableActions.addAll(List.of(disconnect, ban));
}
return availableActions;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import games.strategy.net.websocket.ClientNetworkBridge;
import games.strategy.triplea.settings.ClientSetting;
import java.util.Collection;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.triplea.domain.data.ChatParticipant;
import org.triplea.domain.data.UserName;
Expand All @@ -15,7 +16,7 @@
@Slf4j
public class MessengersChatTransmitter implements ChatTransmitter {
private final UserName userName;
private final Messengers messengers;
@Getter private final Messengers messengers;

private IChatChannel chatChannelSubscriber;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import games.strategy.engine.framework.startup.LobbyWatcherThread;
import games.strategy.engine.framework.startup.launcher.LaunchAction;
import games.strategy.engine.framework.startup.launcher.ServerLauncher;
import games.strategy.engine.framework.startup.mc.messages.ModeratorMessage;
import games.strategy.engine.framework.startup.ui.InGameLobbyWatcherWrapper;
import games.strategy.engine.framework.startup.ui.PlayerTypes;
import games.strategy.engine.framework.startup.ui.panels.main.game.selector.GameSelectorModel;
Expand Down Expand Up @@ -215,6 +216,24 @@ private GameHostingResponse createServerMessenger(final ServerConnectionProps pr
new ServerMessenger(props.getName(), props.getPort(), objectStreamFactory);
serverMessenger.addConnectionChangeListener(this);

// add moderator action handlers (eg: ban/disconnect)
serverMessenger.addMessageListener(
(msg, from) -> {
// check that message is from a moderator
if (msg instanceof ModeratorMessage) {
if (!serverMessenger.isModerator(from)) {
return;
}

ModeratorMessage moderatorMessage = (ModeratorMessage) msg;
if (moderatorMessage.isBan()) {
serverMessenger.banPlayer(moderatorMessage.getPlayerName());
} else if (moderatorMessage.isDisconnect()) {
serverMessenger.removeConnection(moderatorMessage.getPlayerName());
}
}
});

messengers = new Messengers(serverMessenger);
messengers.registerRemote(
launchAction.getStartupRemote(new DefaultServerModelView()), SERVER_REMOTE_NAME);
Expand Down Expand Up @@ -257,7 +276,7 @@ private GameHostingResponse createServerMessenger(final ServerConnectionProps pr
gameHostingResponse = null;
}

chatController = new ChatController(CHAT_NAME, messengers);
chatController = new ChatController(CHAT_NAME, messengers, serverMessenger);

// TODO: Project#4 Change no-op network sender to a real network bridge
chatModel =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package games.strategy.engine.framework.startup.mc.messages;

import java.io.Serializable;
import javax.annotation.Nonnull;
import lombok.Value;

/** Represents a message sent to the server, from a moderator */
@Value
public class ModeratorMessage implements Serializable {
private static final long serialVersionUID = 1L;

@Nonnull String action;
@Nonnull String playerName;

public static ModeratorMessage newDisconnect(String playerName) {
return new ModeratorMessage("disconnect", playerName);
}

public static ModeratorMessage newBan(String playerName) {
return new ModeratorMessage("ban", playerName);
}

public boolean isBan() {
return "ban".equalsIgnoreCase(action);
}

public boolean isDisconnect() {
return "disconnect".equalsIgnoreCase(action);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package games.strategy.engine.framework.startup.mc.messages;

import java.io.Serializable;
import lombok.Value;

/**
* This is a message sent to all players to indicate moderator player has changed. For example, in a
* bot game, if there are 3 players - then the bot and oldest joined will be moderators. If the
* oldest joined leaves, then the remaining player become smoderator.
*/
@Value
public class ModeratorPromoted implements Serializable {
/** The name of the player who is now a moderator. */
String playerName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@ public class MessageHeader implements Serializable {
// from can be null if the sending node doesnt know its own address
@Nullable private final INode from;
private final Serializable message;

/** Indicates if the message is intended for everyone (true). */
public boolean isBroadcast() {
return to == null;
}

public boolean isAddressedTo(INode target) {
return target.equals(to);
}
}
Loading

0 comments on commit 31479d3

Please sign in to comment.