Skip to content

Commit bb65cc2

Browse files
Highlight server warnings in multiplayer chat (#10437)
Adds a WARNING ChatMessage.MessageType, rendered as amber foreground text on desktop (FNetOverlay) and an amber bubble background on mobile (ChatMessageBubble), distinct from the existing blue system and default player styles. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7844f25 commit bb65cc2

10 files changed

Lines changed: 71 additions & 28 deletions

File tree

forge-gui-desktop/src/main/java/forge/gui/FNetOverlay.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public FDialog getWindow() {
8181
private StyledDocument doc;
8282
private SimpleAttributeSet systemStyle;
8383
private SimpleAttributeSet playerStyle;
84+
private SimpleAttributeSet warningStyle;
8485

8586
private final FTextField txtInput = new FTextField.Builder().maxLength(255).build();
8687
private final FLabel cmdSend = new FLabel.ButtonBuilder().text(Localizer.getInstance().getMessage("lblSend")).build();
@@ -124,6 +125,7 @@ public FTextField getTxtInput(){
124125
doc = txtLog.getStyledDocument();
125126
systemStyle = new SimpleAttributeSet();
126127
playerStyle = new SimpleAttributeSet();
128+
warningStyle = new SimpleAttributeSet();
127129

128130
// Configure system message style: light blue RGB(100, 150, 255) - matches mobile implementation
129131
StyleConstants.setForeground(systemStyle, new Color(100, 150, 255));
@@ -133,6 +135,9 @@ public FTextField getTxtInput(){
133135
Color playerColor = (skinTextColor != null) ? skinTextColor.getColor() : Color.WHITE;
134136
StyleConstants.setForeground(playerStyle, playerColor);
135137

138+
// Warning style: amber/caution color, distinct from blue system / white player
139+
StyleConstants.setForeground(warningStyle, new Color(230, 160, 50));
140+
136141
window.setTitle(Localizer.getInstance().getMessage("lblChat"));
137142
window.setVisible(false);
138143
window.setBackground(FSkin.getColor(FSkin.Colors.CLR_ZEBRA));
@@ -191,8 +196,7 @@ public void onLeftDoubleClick(MouseEvent e) {
191196
if (message != null) {
192197
try {
193198
doc.remove(0, doc.getLength());
194-
SimpleAttributeSet style = message.isSystemMessage() ? systemStyle : playerStyle;
195-
doc.insertString(0, message.getFormattedMessage(), style);
199+
doc.insertString(0, message.getFormattedMessage(), styleFor(message));
196200
} catch (BadLocationException e) {
197201
// Fallback to plain text if styled insert fails
198202
txtLog.setText(message.getFormattedMessage());
@@ -257,13 +261,19 @@ else if (centerY > screenBounds.y + screenBounds.height) {
257261
@Override
258262
public void addMessage(final ChatMessage message) {
259263
try {
260-
// Choose style based on message type
261-
SimpleAttributeSet style = message.isSystemMessage() ? systemStyle : playerStyle;
262264
String text = "\n" + message.getFormattedMessage();
263-
doc.insertString(doc.getLength(), text, style);
265+
doc.insertString(doc.getLength(), text, styleFor(message));
264266
} catch (BadLocationException e) {
265267
// Fallback - should not occur in normal operation
266268
e.printStackTrace();
267269
}
268270
}
271+
272+
private SimpleAttributeSet styleFor(final ChatMessage message) {
273+
return switch (message.getType()) {
274+
case WARNING -> warningStyle;
275+
case SYSTEM -> systemStyle;
276+
default -> playerStyle;
277+
};
278+
}
269279
}

forge-gui-desktop/src/test/java/forge/net/HeadlessNetworkClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import forge.game.GameView;
44
import forge.gamemodes.net.DeltaPacket;
5+
import forge.gamemodes.net.ChatMessage;
56
import forge.util.IHasForgeLog;
67
import forge.gamemodes.match.GameLobby.GameLobbyData;
78
import forge.gamemodes.net.client.ClientGameLobby;
@@ -237,7 +238,7 @@ public void update(GameLobbyData state, int slot) {
237238
}
238239

239240
@Override
240-
public void message(String source, String message) {
241+
public void message(String source, String message, ChatMessage.MessageType type) {
241242
netLog.info("Chat: {}: {}", source, message);
242243
}
243244

forge-gui-desktop/src/test/java/forge/net/UnifiedNetworkHarness.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import forge.gamemodes.match.HostedMatch;
1212
import forge.gamemodes.match.LobbySlot;
1313
import forge.gamemodes.match.LobbySlotType;
14+
import forge.gamemodes.net.ChatMessage;
1415
import forge.util.IHasForgeLog;
1516
import forge.gamemodes.net.NetworkByteTracker;
1617
import forge.gamemodes.net.NetworkLogConfig;
@@ -496,7 +497,7 @@ public void update(GameLobbyData state, int slot) {
496497
}
497498

498499
@Override
499-
public void message(String source, String message) {
500+
public void message(String source, String message, ChatMessage.MessageType type) {
500501
netLog.info("Lobby message from {}: {}", source, message);
501502
}
502503

forge-gui-mobile/src/forge/screens/online/OnlineChatScreen.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ private static class ChatMessageBubble extends FDisplayObject {
103103
private static final FSkinFont FONT = FSkinFont.get(12);
104104
private static final FSkinColor LOCAL_COLOR = FSkinColor.get(Colors.CLR_ZEBRA);
105105
private static final FSkinColor REMOTE_COLOR = LOCAL_COLOR.getContrastColor(-20);
106+
private static final FSkinColor WARNING_COLOR = FSkinColor.getStandardColor(140, 95, 25);
106107
private static final FSkinColor MESSAGE_COLOR = FSkinColor.get(Colors.CLR_TEXT);
107108
private static final FSkinColor SOURCE_COLOR = MESSAGE_COLOR.alphaColor(0.75f);
108109
private static final FSkinColor TIMESTAMP_COLOR = MESSAGE_COLOR.alphaColor(0.5f);
@@ -142,7 +143,12 @@ public void draw(Graphics g) {
142143
float y = TEXT_INSET;
143144
float w = getWidth() - TRIANGLE_WIDTH;
144145
float h = getHeight() - TEXT_INSET;
145-
FSkinColor color = isLocal ? LOCAL_COLOR : REMOTE_COLOR;
146+
FSkinColor color;
147+
if (message.getType() == ChatMessage.MessageType.WARNING) {
148+
color = WARNING_COLOR;
149+
} else {
150+
color = isLocal ? LOCAL_COLOR : REMOTE_COLOR;
151+
}
146152
int horzAlignment = isLocal ? Align.right : Align.left;
147153
float timestampHeight = FONT.getCapHeight();
148154

forge-gui/src/main/java/forge/gamemodes/net/ChatMessage.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ public class ChatMessage {
1313

1414
public enum MessageType {
1515
PLAYER, // Regular player chat message
16-
SYSTEM // System notification (displayed in blue)
16+
SYSTEM, // System notification (displayed in blue)
17+
WARNING // Significant server warning (displayed in amber)
1718
}
1819

1920
private final String source, message, timestamp;
@@ -41,10 +42,6 @@ public boolean isLocal() {
4142
return source == null || source.equals(prefs.getPref(FPref.PLAYER_NAME));
4243
}
4344

44-
public boolean isSystemMessage() {
45-
return type == MessageType.SYSTEM;
46-
}
47-
4845
public MessageType getType() {
4946
return type;
5047
}

forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public void update(final GameLobbyData state, final int slot) {
7070
// NO-OP, lobby connected directly
7171
}
7272
@Override
73-
public void message(final String source, final String message) {
74-
chatInterface.addMessage(new ChatMessage(source, message));
73+
public void message(final String source, final String message, final ChatMessage.MessageType type) {
74+
chatInterface.addMessage(new ChatMessage(source, message, type));
7575
}
7676
@Override
7777
public void close() {
@@ -172,8 +172,8 @@ public static ChatMessage join(final String url, final IOnlineLobby onlineLobby,
172172
lobby.setListener(view);
173173
client.addLobbyListener(new ILobbyListener() {
174174
@Override
175-
public void message(final String source, final String message) {
176-
chatInterface.addMessage(new ChatMessage(source, message));
175+
public void message(final String source, final String message, final ChatMessage.MessageType type) {
176+
chatInterface.addMessage(new ChatMessage(source, message, type));
177177
}
178178
@Override
179179
public void update(final GameLobbyData state, final int slot) {

forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ private class MessageHandler extends ChannelInboundHandlerAdapter {
164164
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
165165
if (msg instanceof MessageEvent event) {
166166
for (final ILobbyListener listener : lobbyListeners) {
167-
listener.message(event.getSource(), event.getMessage());
167+
listener.message(event.getSource(), event.getMessage(), event.getType());
168168
}
169169
}
170170
super.channelRead(ctx, msg);

forge-gui/src/main/java/forge/gamemodes/net/event/MessageEvent.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
package forge.gamemodes.net.event;
22

3+
import forge.gamemodes.net.ChatMessage;
34
import forge.gamemodes.net.server.RemoteClient;
45

56
public final class MessageEvent implements NetEvent {
67
private static final long serialVersionUID = 1700060210647684186L;
78

89
private final String source, message;
10+
// String not enum — unknown enum constants cause InvalidObjectException on older clients
11+
private final String type;
12+
913
public MessageEvent(final String message) {
10-
this(null, message);
14+
this(null, message, (String) null);
1115
}
1216
public MessageEvent(final String source, final String message) {
17+
this(source, message, (String) null);
18+
}
19+
public MessageEvent(final String source, final String message, final ChatMessage.MessageType type) {
20+
this(source, message, type != null ? type.name() : null);
21+
}
22+
private MessageEvent(final String source, final String message, final String type) {
1323
this.source = source;
1424
this.message = message;
25+
this.type = type;
26+
}
27+
28+
public static MessageEvent warning(final String message) {
29+
return new MessageEvent(null, message, ChatMessage.MessageType.WARNING);
1530
}
1631

1732
@Override
@@ -26,6 +41,17 @@ public String getMessage() {
2641
return message;
2742
}
2843

44+
public ChatMessage.MessageType getType() {
45+
if (type == null) {
46+
return source == null ? ChatMessage.MessageType.SYSTEM : ChatMessage.MessageType.PLAYER;
47+
}
48+
try {
49+
return ChatMessage.MessageType.valueOf(type);
50+
} catch (IllegalArgumentException e) {
51+
return ChatMessage.MessageType.SYSTEM;
52+
}
53+
}
54+
2955
@Override
3056
public String toString() {
3157
return getMessage();

forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import forge.gamemodes.match.LobbySlot;
1010
import forge.gamemodes.match.LobbySlotType;
1111
import forge.gamemodes.match.input.InputSynchronized;
12+
import forge.gamemodes.net.ChatMessage;
1213
import forge.gamemodes.net.CompatibleObjectDecoder;
1314
import forge.gamemodes.net.CompatibleObjectEncoder;
1415
import forge.gamemodes.net.NetworkLogConfig;
@@ -253,7 +254,7 @@ public int getTotalSendErrors() {
253254

254255
public void broadcast(final NetEvent event) {
255256
if (event instanceof MessageEvent msgEvent) {
256-
lobbyListener.message(msgEvent.getSource(), msgEvent.getMessage());
257+
lobbyListener.message(msgEvent.getSource(), msgEvent.getMessage(), msgEvent.getType());
257258
}
258259
broadcastTo(event, clients.values());
259260
}
@@ -644,7 +645,7 @@ private void onUPnPResult(boolean success) {
644645
? localizer.getMessage("lblUPnPSuccess", String.valueOf(port))
645646
: localizer.getMessage("lblUPnPFailed", String.valueOf(port));
646647
if (lobbyListener != null) {
647-
broadcast(new MessageEvent(msg));
648+
broadcast(success ? new MessageEvent(msg) : MessageEvent.warning(msg));
648649
}
649650
}
650651

@@ -825,7 +826,7 @@ private void handleReconnectTimeout(final String username) {
825826
// Reset lobby slot
826827
localLobby.disconnectPlayer(client.getIndex());
827828

828-
broadcast(new MessageEvent(String.format("%s did not reconnect in time. AI has taken over.", username)));
829+
broadcast(MessageEvent.warning(String.format("%s did not reconnect in time. AI has taken over.", username)));
829830
}
830831

831832
public void convertToAI(final int slotIndex, final String username) {
@@ -959,12 +960,12 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throw
959960
final String clientVersion = event.getVersion();
960961
final String hostVersion = BuildInfo.getVersionString();
961962
if (clientVersion == null) {
962-
broadcast(new MessageEvent(String.format(
963+
broadcast(MessageEvent.warning(String.format(
963964
"Warning: Could not determine %s's Forge version. "
964965
+ "Please use the same version as the host to avoid network compatibility issues.",
965966
event.getUsername())));
966967
} else if (!clientVersion.equals(hostVersion)) {
967-
broadcast(new MessageEvent(String.format(
968+
broadcast(MessageEvent.warning(String.format(
968969
"Warning: %s is using Forge version %s (host: %s). "
969970
+ "Please use the same version as the host to avoid network compatibility issues.",
970971
event.getUsername(), clientVersion, hostVersion)));
@@ -991,7 +992,7 @@ public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt
991992
final String msg = name + " timed out after " + HEARTBEAT_TIMEOUT_SECONDS
992993
+ " seconds without a network response. Closing connection.";
993994
netLog.warn(msg);
994-
broadcast(new MessageEvent(msg));
995+
broadcast(MessageEvent.warning(msg));
995996
ctx.close();
996997
return;
997998
}
@@ -1042,9 +1043,9 @@ public void run() {
10421043
}
10431044
}, 30_000L, 30_000L);
10441045

1045-
broadcast(new MessageEvent(
1046+
broadcast(MessageEvent.warning(
10461047
String.format("%s disconnected. Waiting %s for reconnect...", username, formatTime(RECONNECT_TIMEOUT_SECONDS))));
1047-
lobbyListener.message(null, "(Host can use /skipreconnect to replace disconnected player with AI, or /skiptimeout to wait indefinitely.)");
1048+
lobbyListener.message(null, "(Host can use /skipreconnect to replace disconnected player with AI, or /skiptimeout to wait indefinitely.)", ChatMessage.MessageType.SYSTEM);
10481049
netLog.info("[Disconnect] Player disconnected mid-game: {} (slot {}). Waiting for reconnect.", username, playerIndex);
10491050
} else if (client.hasValidSlot()) {
10501051
// Peer completed registration but match isn't active (or slot was freed earlier)

forge-gui/src/main/java/forge/interfaces/ILobbyListener.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package forge.interfaces;
22

33
import forge.gamemodes.match.GameLobby.GameLobbyData;
4+
import forge.gamemodes.net.ChatMessage;
45
import forge.gamemodes.net.client.ClientGameLobby;
56

67
public interface ILobbyListener {
7-
void message(String source, String message);
8+
void message(String source, String message, ChatMessage.MessageType type);
89
void update(GameLobbyData state, int slot);
910
void close();
1011
ClientGameLobby getLobby();

0 commit comments

Comments
 (0)