From cd3d4807fcd31b4c0d366ac61055d0cb565a6433 Mon Sep 17 00:00:00 2001
From: DorWeid <13677126+DorWeid@users.noreply.github.com>
Date: Thu, 27 Aug 2026 08:22:28 -0400
Subject: [PATCH] feat(chat): add world-wide global command
---
.../java/client/command/CommandsExecutor.java | 2 +
.../command/commands/gm0/GlobalCommand.java | 46 +++++++++++++
.../commands/gm0/GlobalCommandTest.java | 68 +++++++++++++++++++
3 files changed, 116 insertions(+)
create mode 100644 src/main/java/client/command/commands/gm0/GlobalCommand.java
create mode 100644 src/test/java/client/command/commands/gm0/GlobalCommandTest.java
diff --git a/src/main/java/client/command/CommandsExecutor.java b/src/main/java/client/command/CommandsExecutor.java
index 16de527825..a2863d3714 100644
--- a/src/main/java/client/command/CommandsExecutor.java
+++ b/src/main/java/client/command/CommandsExecutor.java
@@ -30,6 +30,7 @@
import client.command.commands.gm0.EnableAuthCommand;
import client.command.commands.gm0.EquipLvCommand;
import client.command.commands.gm0.GachaCommand;
+import client.command.commands.gm0.GlobalCommand;
import client.command.commands.gm0.GmCommand;
import client.command.commands.gm0.HelpCommand;
import client.command.commands.gm0.JoinEventCommand;
@@ -346,6 +347,7 @@ private void registerLv0Commands() {
addCommand("credits", StaffCommand.class);
addCommand("uptime", UptimeCommand.class);
addCommand("gacha", GachaCommand.class);
+ addCommand("global", GlobalCommand.class);
addCommand("dispose", DisposeCommand.class);
addCommand("changel", ChangeLanguageCommand.class);
addCommand("equiplv", EquipLvCommand.class);
diff --git a/src/main/java/client/command/commands/gm0/GlobalCommand.java b/src/main/java/client/command/commands/gm0/GlobalCommand.java
new file mode 100644
index 0000000000..7e1ad863e9
--- /dev/null
+++ b/src/main/java/client/command/commands/gm0/GlobalCommand.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of the HeavenMS MapleStory Server, commands OdinMS-based
+ Copyleft (L) 2016 - 2019 RonanLana
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation version 3 as published by
+ the Free Software Foundation. You may not use, modify or distribute
+ this program under any other version of the GNU Affero General Public
+ License.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+package client.command.commands.gm0;
+
+import client.Character;
+import client.Client;
+import client.command.Command;
+import server.ChatLogger;
+import tools.PacketCreator;
+
+public class GlobalCommand extends Command {
+ {
+ setDescription("Send a message to every player in the world.");
+ }
+
+ @Override
+ public void execute(Client client, String[] params) {
+ Character player = client.getPlayer();
+ String message = player.getLastCommandMessage();
+ if (message == null || message.isBlank()) {
+ player.yellowMessage("Usage: @global ");
+ return;
+ }
+
+ String formattedMessage = "[Global] " + player.getName() + ": " + message;
+ client.getWorldServer().broadcastPacket(PacketCreator.serverNotice(6, formattedMessage));
+ ChatLogger.log(client, "Global", message);
+ }
+}
diff --git a/src/test/java/client/command/commands/gm0/GlobalCommandTest.java b/src/test/java/client/command/commands/gm0/GlobalCommandTest.java
new file mode 100644
index 0000000000..62dfcbfdf5
--- /dev/null
+++ b/src/test/java/client/command/commands/gm0/GlobalCommandTest.java
@@ -0,0 +1,68 @@
+package client.command.commands.gm0;
+
+import client.Character;
+import client.Client;
+import io.netty.buffer.Unpooled;
+import net.packet.ByteBufInPacket;
+import net.packet.Packet;
+import net.server.world.World;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import server.ChatLogger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class GlobalCommandTest {
+ @Mock
+ private Client client;
+ @Mock
+ private Character player;
+ @Mock
+ private World world;
+
+ @Test
+ void broadcastsOriginalMessageToTheWholeWorld() {
+ when(client.getPlayer()).thenReturn(player);
+ when(client.getWorldServer()).thenReturn(world);
+ when(player.getName()).thenReturn("RustPocChar");
+ when(player.getLastCommandMessage()).thenReturn("Heading to Henesys");
+
+ try (MockedStatic loggerStatic = mockStatic(ChatLogger.class)) {
+ new GlobalCommand().execute(client, new String[]{"heading", "to", "henesys"});
+
+ var packetCaptor = org.mockito.ArgumentCaptor.forClass(Packet.class);
+ verify(world).broadcastPacket(packetCaptor.capture());
+ var packet = new ByteBufInPacket(
+ Unpooled.wrappedBuffer(packetCaptor.getValue().getBytes()));
+ assertEquals(0x0044, packet.readShort());
+ assertEquals(6, packet.readUnsignedByte());
+ assertEquals("[Global] RustPocChar: Heading to Henesys", packet.readString());
+ assertEquals(0, packet.readInt());
+ assertEquals(0, packet.available());
+ loggerStatic.verify(() -> ChatLogger.log(client, "Global", "Heading to Henesys"));
+ }
+ }
+
+ @Test
+ void rejectsBlankMessagesWithoutBroadcastingOrLogging() {
+ when(client.getPlayer()).thenReturn(player);
+ when(player.getLastCommandMessage()).thenReturn(" ");
+
+ try (MockedStatic loggerStatic = mockStatic(ChatLogger.class)) {
+ new GlobalCommand().execute(client, new String[]{});
+
+ verify(player).yellowMessage("Usage: @global ");
+ verify(world, never()).broadcastPacket(any(Packet.class));
+ loggerStatic.verifyNoInteractions();
+ }
+ }
+}