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(); + } + } +}