Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME block_reason COMMAND block_reason_tests)
add_executable(account_grade_tests tests/account_grade.cpp)
add_test(NAME account_grade COMMAND account_grade_tests)
add_executable(admin_result_tests tests/admin_result.cpp Net/InPacket.cpp Net/Handlers/Helpers/AdminResult.cpp)
target_include_directories(admin_result_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME admin_result COMMAND admin_result_tests)
add_executable(gm_command_tests tests/gm_command.cpp IO/GmCommand.cpp)
add_test(NAME gm_command COMMAND gm_command_tests)
endif()

include_directories(.)
Expand Down Expand Up @@ -1103,6 +1108,10 @@ add_executable(HeavenClient
IO/FamilyActions.h
IO/MarriageActions.cpp
IO/MarriageActions.h
IO/GmCommand.cpp
IO/GmCommand.h
IO/GmCommands.cpp
IO/GmCommands.h
IO/GuildCommands.cpp
IO/GuildCommands.h
IO/ChatCommand.h
Expand Down Expand Up @@ -1130,6 +1139,8 @@ add_executable(HeavenClient
Net/Handlers/Helpers/CharacterParser.h
Net/Handlers/Helpers/ItemParser.cpp
Net/Handlers/Helpers/ItemParser.h
Net/Handlers/Helpers/AdminResult.cpp
Net/Handlers/Helpers/AdminResult.h
Net/Handlers/Helpers/LoginParser.cpp
Net/Handlers/Helpers/LoginParser.h
Net/Handlers/Helpers/MovementParser.cpp
Expand Down
126 changes: 126 additions & 0 deletions HeavenClient/IO/GmCommand.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "GmCommand.h"

#include "ChatCommand.h"

#include <cstdlib>

namespace ms
{
namespace
{
struct Broadcast
{
const char* command;
bool argument;
uint8_t scope;
uint8_t type;
};

// In v83's order (0x52cf45..0x52d39c); an `x` sends no text, which
// takes the slide down.
constexpr Broadcast BROADCASTS[] = {
// StringPool 0x6e9, 0x6eb, 0x104, 0x6e7
{ "/alertch", true, GmCommand::CHANNEL_SCOPE, GmCommand::ALERT },
{ "/alertmap", true, GmCommand::MAP, GmCommand::ALERT },
{ "/alertm", true, GmCommand::MAP, GmCommand::ALERT },
{ "/alertall", true, GmCommand::WORLD, GmCommand::ALERT },
// StringPool 0x6e8, 0x6ea, 0x6ec, 0x105
{ "/noticeall", true, GmCommand::WORLD, GmCommand::NOTICE },
{ "/noticech", true, GmCommand::CHANNEL_SCOPE, GmCommand::NOTICE },
{ "/noticemap", true, GmCommand::MAP, GmCommand::NOTICE },
{ "/noticem", true, GmCommand::MAP, GmCommand::NOTICE },
// StringPool 0xaff, 0xb06, 0xb00, 0xb07, 0xb01, 0xb08
{ "/slideall", true, GmCommand::WORLD, GmCommand::SLIDE },
{ "/slideallx", false, GmCommand::WORLD, GmCommand::SLIDE },
{ "/slidech", true, GmCommand::CHANNEL_SCOPE, GmCommand::SLIDE },
{ "/slidechx", false, GmCommand::CHANNEL_SCOPE, GmCommand::SLIDE },
{ "/slidemap", true, GmCommand::MAP, GmCommand::SLIDE },
{ "/slidemapx", false, GmCommand::MAP, GmCommand::SLIDE }
};

GmCommand of(GmCommand::Kind kind)
{
GmCommand command;
command.kind = kind;
return command;
}
}

GmCommand GmCommand::parse(const std::string& input, int rank)
{
std::string rest;

// StringPool 0x6e6
if (rank <= 4 && ChatCommand::with_argument(input, "/h", rest))
{
GmCommand command = of(Kind::HIDE);
command.on = std::atoi(rest.c_str()) != 0;
return command;
}

if (rank <= 3)
{
// StringPool 0x6e4, 0xbf
if (ChatCommand::with_argument(input, "/ch", rest))
{
GmCommand command = of(Kind::CHANNEL);
command.channel = std::atoi(rest.c_str()) - 1;
return command;
}

if (ChatCommand::same_text(input, "/u"))
return of(Kind::USERS);

// StringPool 0x700: a name, a space, and the rest (FUN_004a8f57).
if (ChatCommand::with_argument(input, "/w", rest))
{
size_t space = rest.find(' ');

if (space != std::string::npos && space > 0 && space + 1 < rest.size())
{
GmCommand command = of(Kind::WARN);
command.name = rest.substr(0, space);
command.text = rest.substr(space + 1);
return command;
}
}

// StringPool 0x6e5
if (ChatCommand::with_argument(input, "/ban", rest))
{
GmCommand command = of(Kind::BAN);
command.name = rest;
return command;
}

// StringPool 0x6df
if (ChatCommand::same_text(input, "/uclip"))
return of(Kind::UCLIP);
}

if (rank <= 2)
{
for (const Broadcast& broadcast : BROADCASTS)
{
bool matched = broadcast.argument
? ChatCommand::with_argument(input, broadcast.command, rest)
: ChatCommand::same_text(input, broadcast.command);

if (!matched)
continue;

GmCommand command = of(Kind::BROADCAST);
command.scope = broadcast.scope;
command.type = broadcast.type;
command.text = broadcast.argument ? rest : "";
return command;
}

// StringPool 0xb09
if (ChatCommand::same_text(input, "/slideax"))
return of(Kind::SLIDE_AX);
}

return GmCommand();
}
}
57 changes: 57 additions & 0 deletions HeavenClient/IO/GmCommand.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#pragma once

#include <cstdint>
#include <string>

namespace ms
{
// A GM's slash command, as v83's chat bar reads it (FUN_0052c40c) before
// any of the players' commands.
struct GmCommand
{
enum class Kind
{
// Not a GM command, or not one this rank may use.
NONE,
// /h N: ADMIN_COMMAND 0x10, hidden when N isn't 0.
HIDE,
// /ch N: a change to channel `channel` (from 0), asked as the
// player's own.
CHANNEL,
// /u: ADMIN_COMMAND 0x11 0, the users on the map.
USERS,
// /w NAME MSG: ADMIN_COMMAND 0x1E, a warning.
WARN,
// /ban NAME: ADMIN_COMMAND 0x03.
BAN,
// /uclip: ADMIN_COMMAND 0x11 0x0C.
UCLIP,
// /alert*, /notice* and /slide*: ADMIN_BROADCAST, `type` to
// `scope`.
BROADCAST,
// /slideax: 0x86.
SLIDE_AX
};

// ADMIN_BROADCAST's scopes and types.
static constexpr uint8_t WORLD = 0;
static constexpr uint8_t CHANNEL_SCOPE = 1;
static constexpr uint8_t MAP = 2;
static constexpr uint8_t NOTICE = 0;
static constexpr uint8_t ALERT = 1;
static constexpr uint8_t SLIDE = 4;

Kind kind = Kind::NONE;
std::string name;
std::string text;
bool on = false;
int32_t channel = 0;
uint8_t scope = 0;
uint8_t type = 0;

// The command `input` is for an account of v83's GM `rank`
// (AccountGrade::rank). Each is matched as v83 matches it: a name
// alone, or a name, a space and more.
static GmCommand parse(const std::string& input, int rank);
};
}
60 changes: 60 additions & 0 deletions HeavenClient/IO/GmCommands.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "GmCommands.h"

#include "GmCommand.h"

#include "../Configuration.h"
#include "../Gameplay/Stage.h"
#include "../Net/Packets/GameplayPackets.h"

namespace ms
{
namespace GmCommands
{
bool run(const std::string& input)
{
const AccountGrade& grade = Configuration::get().get_grade();

// v83 reads the GM commands only for these grades (0x52c9cd).
if (!grade.slash_commands())
return false;

GmCommand command = GmCommand::parse(input, grade.rank());

switch (command.kind)
{
case GmCommand::Kind::NONE:
return false;
case GmCommand::Kind::HIDE:
AdminHidePacket(command.on).dispatch();
break;
case GmCommand::Kind::CHANNEL:
// FUN_005304af, as the channel menu asks, with no check of
// the number.
Stage::get().change_channel(static_cast<uint8_t>(command.channel));
break;
case GmCommand::Kind::USERS:
AdminEnterMapPacket(AdminEnterMapPacket::Operation::SHOW_USERS).dispatch();
break;
case GmCommand::Kind::WARN:
AdminWarnPacket(command.name, command.text).dispatch();
break;
case GmCommand::Kind::BAN:
AdminBanPacket(command.name).dispatch();
break;
case GmCommand::Kind::UCLIP:
AdminEnterMapPacket(AdminEnterMapPacket::Operation::ALERT_ADMINS).dispatch();
break;
case GmCommand::Kind::BROADCAST:
AdminBroadcastPacket(command.scope, command.type, command.text).dispatch();
break;
case GmCommand::Kind::SLIDE_AX:
GmSlidePacket().dispatch();
break;
}

// Every GM command is logged, as typed (0x52e28f).
AdminLogPacket(input).dispatch();
return true;
}
}
}
14 changes: 14 additions & 0 deletions HeavenClient/IO/GmCommands.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include <string>

namespace ms
{
// v83's GM slash commands, for an account whose grade bytes allow them.
namespace GmCommands
{
// Runs input if it is a GM command the account may use, and logs
// it to the server (ADMIN_LOG); returns whether it was one.
bool run(const std::string& input);
}
}
8 changes: 7 additions & 1 deletion HeavenClient/IO/UITypes/UIChatBar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "../Components/MapleButton.h"

#include "../BuddyCommands.h"
#include "../GmCommands.h"
#include "../GuildCommands.h"
#include "../PartyCommands.h"

Expand Down Expand Up @@ -335,8 +336,13 @@ namespace ms
if (msg[0] != '/' && chat_target == Target::WHISPER && whisper_name.empty())
send_chatline("There is no one to whisper to.", LineType::SKILL);

// A GM's commands come first, so a GM's /w warns
// (0x52c9cd).
if (msg[0] == '/' && GmCommands::run(msg))
{
}
// HC's own: v83 has no command to whisper one line.
if (whisper_result == WhisperParseResult::ValidWhisper
else if (whisper_result == WhisperParseResult::ValidWhisper
&& same_name(target, Stage::get().get_player().get_stats().get_name()))
{
send_chatline("You can't whisper to yourself.", LineType::SKILL);
Expand Down
Loading
Loading