feat(game): add a chat console
This commit is contained in:
1 parent
49a9a7d30d
commit
530def48df
7 files changed
+504
-5
No files matched your search
@@ -274,6 +274,23 @@ GetMarkItemListScRsp
|
||||
GetBigDataAllRecommendCsReq
|
||||
GetBigDataAllRecommendScRsp
|
||||
|
||||
# ---- friends / chat: the CapySR console ----
|
||||
GetFriendListInfoCsReq
|
||||
GetFriendListInfoScRsp
|
||||
GetFriendLoginInfoCsReq
|
||||
GetFriendLoginInfoScRsp
|
||||
GetChatEmojiListCsReq
|
||||
GetChatEmojiListScRsp
|
||||
GetChatFriendHistoryCsReq
|
||||
GetChatFriendHistoryScRsp
|
||||
GetLoginChatInfoCsReq
|
||||
GetLoginChatInfoScRsp
|
||||
GetPrivateChatHistoryCsReq
|
||||
GetPrivateChatHistoryScRsp
|
||||
SendMsgCsReq
|
||||
SendMsgScRsp
|
||||
RevcMsgScNotify
|
||||
|
||||
# ---- mission / tutorial ----
|
||||
GetMissionStatusCsReq
|
||||
GetMissionStatusScRsp
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// The CapySR console, as a friend. There is no GM panel on a private server, so the
|
||||
// server puts itself in the friend list and reads whatever is typed at it: a line
|
||||
// starting with '/' runs as a command, anything else is echoed back with a nudge.
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/config.h"
|
||||
#include "core/logger.h"
|
||||
#include "core/util.h"
|
||||
#include "game/command.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/player.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/handler.h"
|
||||
#include "net/session.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kConsoleUid = command::kConsoleUid;
|
||||
|
||||
// One line of chat, attributed to `senderUid`.
|
||||
proto::ChatMessageData chatLine(const std::string& text, uint32_t senderUid) {
|
||||
proto::ChatMessageData message;
|
||||
message.create_time = static_cast<uint64_t>(util::nowSec());
|
||||
proto::MessageChatData& data = message.message_datas.emplace_back();
|
||||
data.message_type = proto::MsgType::MsgType_CustomText;
|
||||
// A oneof scalar is only written when its case is set; without this the line goes
|
||||
// out empty and the client shows a blank bubble.
|
||||
proto::ChatData& chat = data.chat_data.emplace();
|
||||
chat.message_text = text;
|
||||
chat.PGBLIFDKBHK_case = proto::ChatData::k_message_text;
|
||||
|
||||
// Two unnamed fields carry the same {role type, uid} pair and both have to name the
|
||||
// sender, in history and in a live push alike. Filling one renders nothing at all.
|
||||
proto::EKNABKLPEEL side;
|
||||
side.KPOBMNLKLOK = proto::HCMEILLLKBD::HCMEILLLKBD_JDOAIPKBIPE;
|
||||
side.role_id = senderUid;
|
||||
message.CKHPFFENOBE = side;
|
||||
message.BKOALKHDLOB = side;
|
||||
return message;
|
||||
}
|
||||
|
||||
void say(net::Session& session, const std::string& text) {
|
||||
const Player* player = session.player();
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::RevcMsgScNotify notify;
|
||||
notify.chat_type = proto::ChatType::ChatType_Private;
|
||||
// Named `source_uid` in the dump, but it is the side the message is *for*. With the
|
||||
// sender's uid in it the client has no thread to file the line under, and shows
|
||||
// nothing.
|
||||
notify.source_uid = player->uid();
|
||||
notify.recv_message_data = chatLine(text, kConsoleUid);
|
||||
session.send(cmd::RevcMsgScNotify, notify);
|
||||
}
|
||||
|
||||
// The client does not draw the line it just sent -- it waits to be handed it back. So
|
||||
// the player's own message goes home the same way, from the player, addressed to the
|
||||
// console.
|
||||
void echo(net::Session& session, const std::string& text) {
|
||||
const Player* player = session.player();
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::RevcMsgScNotify notify;
|
||||
notify.chat_type = proto::ChatType::ChatType_Private;
|
||||
notify.source_uid = kConsoleUid;
|
||||
notify.recv_message_data = chatLine(text, player->uid());
|
||||
session.send(cmd::RevcMsgScNotify, notify);
|
||||
}
|
||||
|
||||
void onGetFriendListInfo(net::Session& session, const proto::GetFriendListInfoCsReq&) {
|
||||
const core::PlayerDefaults& defaults = core::Config::get().player;
|
||||
proto::GetFriendListInfoScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
|
||||
proto::FriendSimpleInfo& friendInfo = rsp.friend_list.emplace_back();
|
||||
friendInfo.remark_name = "CapySR";
|
||||
friendInfo.is_marked = true;
|
||||
friendInfo.create_time = 0;
|
||||
friendInfo.playing_state = proto::PlayingState::PlayingState_None;
|
||||
proto::PlayerSimpleInfo& info = friendInfo.player_info.emplace();
|
||||
info.uid = kConsoleUid;
|
||||
info.nickname = "CapySR";
|
||||
info.signature = "type /help for the command list";
|
||||
info.level = defaults.level;
|
||||
info.head_icon = defaults.headIcon;
|
||||
info.online_status = proto::FriendOnlineStatus::FRIEND_ONLINE_STATUS_ONLINE;
|
||||
info.last_active_time = static_cast<int64_t>(util::nowSec());
|
||||
// Without these two the client drops the entry from its own list the next time the
|
||||
// friend screen opens, and never asks for the list again.
|
||||
info.platform = proto::PlatformType::PC;
|
||||
info.chat_bubble_id = 220005;
|
||||
|
||||
session.send(cmd::GetFriendListInfoScRsp, rsp);
|
||||
}
|
||||
|
||||
void onGetFriendLoginInfo(net::Session& session, const proto::GetFriendLoginInfoCsReq&) {
|
||||
proto::GetFriendLoginInfoScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp._friend_uid_list.push_back(kConsoleUid);
|
||||
rsp.is_allow_other_apply = true;
|
||||
session.send(cmd::GetFriendLoginInfoScRsp, rsp);
|
||||
}
|
||||
|
||||
void onGetChatEmojiList(net::Session& session, const proto::GetChatEmojiListCsReq&) {
|
||||
proto::GetChatEmojiListScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::GetChatEmojiListScRsp, rsp);
|
||||
}
|
||||
|
||||
// The chat tab's conversation list. Without the console in it the thread exists but
|
||||
// there is nothing to tap to open it.
|
||||
void onGetChatFriendHistory(net::Session& session, const proto::GetChatFriendHistoryCsReq&) {
|
||||
proto::GetChatFriendHistoryScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
proto::FriendHistoryInfo& entry = rsp.friend_history_info.emplace_back();
|
||||
entry.contact_side = kConsoleUid;
|
||||
entry.last_send_time = static_cast<int64_t>(util::nowSec());
|
||||
session.send(cmd::GetChatFriendHistoryScRsp, rsp);
|
||||
}
|
||||
|
||||
void onGetLoginChatInfo(net::Session& session, const proto::GetLoginChatInfoCsReq&) {
|
||||
proto::GetLoginChatInfoScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.contact_id_list.push_back(kConsoleUid);
|
||||
session.send(cmd::GetLoginChatInfoScRsp, rsp);
|
||||
}
|
||||
|
||||
void onGetPrivateChatHistory(net::Session& session, const proto::GetPrivateChatHistoryCsReq& req) {
|
||||
proto::GetPrivateChatHistoryScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.target_side = req.target_side;
|
||||
rsp.contact_side = kConsoleUid;
|
||||
if (req.contact_side == kConsoleUid || req.target_side == kConsoleUid) {
|
||||
for (const char* line : {"CapySR console.", "Type /help for the command list."}) {
|
||||
rsp.chat_message_list.push_back(chatLine(line, kConsoleUid));
|
||||
}
|
||||
}
|
||||
session.send(cmd::GetPrivateChatHistoryScRsp, rsp);
|
||||
}
|
||||
|
||||
void onSendMsg(net::Session& session, const proto::SendMsgCsReq& req) {
|
||||
std::string text;
|
||||
if (req.message_datas && req.message_datas->chat_data) {
|
||||
text = req.message_datas->chat_data->message_text;
|
||||
}
|
||||
bool toConsole = req.target_list.empty() ||
|
||||
std::find(req.target_list.begin(), req.target_list.end(), kConsoleUid) !=
|
||||
req.target_list.end();
|
||||
|
||||
// The replies go out before the ScRsp: the client stops listening for the thread
|
||||
// once the request it sent has been answered.
|
||||
if (toConsole && !text.empty()) {
|
||||
echo(session, text);
|
||||
if (text.front() == '/') {
|
||||
for (const std::string& line : command::run(session, text)) say(session, line);
|
||||
} else {
|
||||
say(session, "type /help for the command list");
|
||||
}
|
||||
}
|
||||
|
||||
proto::SendMsgScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::SendMsgScRsp, rsp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerChatHandlers() {
|
||||
net::on<proto::GetFriendListInfoCsReq>(cmd::GetFriendListInfoCsReq, onGetFriendListInfo);
|
||||
net::on<proto::GetFriendLoginInfoCsReq>(cmd::GetFriendLoginInfoCsReq, onGetFriendLoginInfo);
|
||||
net::on<proto::GetChatEmojiListCsReq>(cmd::GetChatEmojiListCsReq, onGetChatEmojiList);
|
||||
net::on<proto::GetChatFriendHistoryCsReq>(cmd::GetChatFriendHistoryCsReq, onGetChatFriendHistory);
|
||||
net::on<proto::GetLoginChatInfoCsReq>(cmd::GetLoginChatInfoCsReq, onGetLoginChatInfo);
|
||||
net::on<proto::GetPrivateChatHistoryCsReq>(cmd::GetPrivateChatHistoryCsReq, onGetPrivateChatHistory);
|
||||
net::on<proto::SendMsgCsReq>(cmd::SendMsgCsReq, onSendMsg);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "game/command.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
|
||||
#include "core/logger.h"
|
||||
#include "core/util.h"
|
||||
#include "data/excel.h"
|
||||
#include "game/inventory.h"
|
||||
#include "game/player.h"
|
||||
#include "game/rescue.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/session.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
namespace game::command {
|
||||
namespace {
|
||||
|
||||
using Args = std::vector<std::string_view>;
|
||||
using Reply = std::vector<std::string>;
|
||||
|
||||
Args split(std::string_view line) {
|
||||
Args out;
|
||||
size_t i = 0;
|
||||
while (i < line.size()) {
|
||||
while (i < line.size() && line[i] == ' ') ++i;
|
||||
size_t start = i;
|
||||
while (i < line.size() && line[i] != ' ') ++i;
|
||||
if (i > start) out.push_back(line.substr(start, i - start));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Parses a whole number, or nothing at all: a command must not act on a half-read id.
|
||||
bool number(std::string_view text, uint32_t& out) {
|
||||
if (text.empty()) return false;
|
||||
uint32_t value = 0;
|
||||
auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (ec != std::errc{} || end != text.data() + text.size()) return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reply onHelp(net::Session&, Player&, const Args&) {
|
||||
Reply reply{"CapySR console. Commands:"};
|
||||
for (const Entry& entry : list()) {
|
||||
std::string line = "/";
|
||||
line += entry.name;
|
||||
if (!entry.usage.empty()) {
|
||||
line += ' ';
|
||||
line += entry.usage;
|
||||
}
|
||||
line += " - ";
|
||||
line += entry.help;
|
||||
reply.push_back(std::move(line));
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
Reply onGive(net::Session& session, Player& player, const Args& args) {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t count = 1;
|
||||
if (args.empty() || !number(args[0], itemId)) return {"usage: /give <item id> [count]"};
|
||||
if (args.size() > 1 && !number(args[1], count)) return {"that count is not a number"};
|
||||
if (count == 0) return {"nothing to give"};
|
||||
|
||||
const data::ItemInfo* item = data::Tables::get().item(itemId);
|
||||
bool wallet = itemId == inventory::kStellarJade || itemId == inventory::kCredit ||
|
||||
itemId == inventory::kOneiricShard;
|
||||
if (item == nullptr && !wallet) return {"no item " + std::to_string(itemId)};
|
||||
|
||||
std::vector<data::ItemStack> items{{itemId, count}};
|
||||
inventory::grant(player, items);
|
||||
session.send(cmd::PlayerSyncScNotify, inventory::sync(player, items));
|
||||
player.saveNow();
|
||||
|
||||
std::string held = std::to_string(inventory::held(player, itemId));
|
||||
return {"gave " + std::to_string(count) + " x " + std::to_string(itemId) + ", you now hold " + held};
|
||||
}
|
||||
|
||||
Reply onRefill(net::Session& session, Player& player, const Args&) {
|
||||
const data::StaminaRules& rules = data::Tables::get().staminaRules();
|
||||
Inventory& bag = player.inventory();
|
||||
if (bag.stamina >= rules.max) return {"Trailblaze Power is already full"};
|
||||
|
||||
bag.stamina = rules.max;
|
||||
bag.staminaUpdatedAt = static_cast<int64_t>(util::nowSec());
|
||||
session.send(cmd::StaminaInfoScNotify, inventory::staminaInfo(player));
|
||||
session.send(cmd::PlayerSyncScNotify, inventory::sync(player, {}));
|
||||
player.saveNow();
|
||||
return {"Trailblaze Power refilled to " + std::to_string(rules.max)};
|
||||
}
|
||||
|
||||
Reply onUnstuck(net::Session& session, Player&, const Args&) {
|
||||
return {rescue::unstick(session, 0)};
|
||||
}
|
||||
|
||||
Reply onTp(net::Session& session, Player&, const Args& args) {
|
||||
uint32_t entryId = 0;
|
||||
if (args.empty() || !number(args[0], entryId)) return {"usage: /tp <entrance id>"};
|
||||
if (data::Tables::get().entrances().count(entryId) == 0) {
|
||||
return {"no entrance " + std::to_string(entryId)};
|
||||
}
|
||||
return {rescue::unstick(session, entryId)};
|
||||
}
|
||||
|
||||
Reply onWhere(net::Session&, Player& player, const Args&) {
|
||||
const SceneLocation& at = player.location();
|
||||
const Position& pos = player.position();
|
||||
return {"uid " + std::to_string(player.uid()) + ", world level " + std::to_string(player.worldLevel()),
|
||||
"entry " + std::to_string(at.entryId) + ", plane " + std::to_string(at.planeId) + ", floor " +
|
||||
std::to_string(at.floorId),
|
||||
"at " + std::to_string(pos.x / 1000) + " " + std::to_string(pos.y / 1000) + " " +
|
||||
std::to_string(pos.z / 1000)};
|
||||
}
|
||||
|
||||
using Handler = Reply (*)(net::Session&, Player&, const Args&);
|
||||
|
||||
struct Command {
|
||||
Entry entry;
|
||||
Handler run;
|
||||
};
|
||||
|
||||
const Command kCommands[] = {
|
||||
{{"help", "", "this list"}, onHelp},
|
||||
{{"give", "<item id> [count]", "put items in the bag"}, onGive},
|
||||
{{"refill", "", "Trailblaze Power back to full"}, onRefill},
|
||||
{{"unstuck", "", "end whatever the client is waiting on and redraw the scene"}, onUnstuck},
|
||||
{{"tp", "<entrance id>", "move to an entrance"}, onTp},
|
||||
{{"where", "", "uid, world level and where you are standing"}, onWhere},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<Entry>& list() {
|
||||
static const std::vector<Entry> entries = [] {
|
||||
std::vector<Entry> out;
|
||||
for (const Command& command : kCommands) out.push_back(command.entry);
|
||||
return out;
|
||||
}();
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::vector<std::string> run(net::Session& session, std::string_view line) {
|
||||
Player* player = session.player();
|
||||
if (player == nullptr) return {"not logged in yet"};
|
||||
|
||||
if (!line.empty() && line.front() == '/') line.remove_prefix(1);
|
||||
Args args = split(line);
|
||||
if (args.empty()) return {"type /help for the command list"};
|
||||
|
||||
std::string_view name = args.front();
|
||||
args.erase(args.begin());
|
||||
for (const Command& command : kCommands) {
|
||||
if (command.entry.name != name) continue;
|
||||
logging::info("command", "uid {} ran /{}", player->uid(), name);
|
||||
return command.run(session, *player, args);
|
||||
}
|
||||
return {"no command /" + std::string(name) + ", type /help for the list"};
|
||||
}
|
||||
|
||||
} // namespace game::command
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace net {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace game {
|
||||
|
||||
// The chat console. A private server has no GM panel, so the one place a player can
|
||||
// reach the server with a keyboard is the chat box: `src/game/chat_handlers.cpp` puts a
|
||||
// friend named CapySR in the friend list and routes whatever is typed at it through
|
||||
// here.
|
||||
namespace command {
|
||||
|
||||
// The console's own uid. Anything but the player's, so the client files the thread
|
||||
// under a contact of its own.
|
||||
constexpr uint32_t kConsoleUid = 2000;
|
||||
|
||||
struct Entry {
|
||||
std::string_view name;
|
||||
std::string_view usage; // arguments only, empty for a command that takes none
|
||||
std::string_view help;
|
||||
};
|
||||
|
||||
// Every command, in the order /help lists them.
|
||||
const std::vector<Entry>& list();
|
||||
|
||||
// Runs one typed line, with or without its leading '/', and answers with the lines to
|
||||
// send back. Never empty: an unknown command answers with the complaint to show.
|
||||
std::vector<std::string> run(net::Session& session, std::string_view line);
|
||||
|
||||
} // namespace command
|
||||
} // namespace game
|
||||
@@ -26,6 +26,7 @@ void registerPeakHandlers();
|
||||
void registerGachaHandlers();
|
||||
void registerShopHandlers();
|
||||
void registerCollectionHandlers();
|
||||
void registerChatHandlers();
|
||||
void registerModuleHandlers();
|
||||
void registerMiscHandlers();
|
||||
|
||||
|
||||
@@ -25,12 +25,8 @@ constexpr uint16_t kEmptyReplies[] = {
|
||||
cmd::GetAllServerPrefsDataCsReq,
|
||||
cmd::GetQuestRecordCsReq,
|
||||
cmd::GetRogueInfoCsReq,
|
||||
cmd::GetFriendListInfoCsReq,
|
||||
cmd::GetFriendApplyListInfoCsReq,
|
||||
cmd::GetFriendLoginInfoCsReq,
|
||||
cmd::GetFriendRecommendListInfoCsReq,
|
||||
cmd::GetChatEmojiListCsReq,
|
||||
cmd::GetPrivateChatHistoryCsReq,
|
||||
cmd::GetPhoneDataCsReq,
|
||||
cmd::GetMailCsReq,
|
||||
cmd::GetSecretKeyInfoCsReq,
|
||||
@@ -47,7 +43,6 @@ constexpr uint16_t kEmptyReplies[] = {
|
||||
cmd::GetRaidInfoCsReq,
|
||||
cmd::GetMuseumInfoCsReq,
|
||||
cmd::TextJoinQueryCsReq,
|
||||
cmd::GetLoginChatInfoCsReq,
|
||||
cmd::QueryProductInfoCsReq,
|
||||
cmd::GetLoginActivityCsReq,
|
||||
cmd::GetTrialActivityDataCsReq,
|
||||
@@ -209,6 +204,7 @@ void registerAllHandlers() {
|
||||
registerGachaHandlers();
|
||||
registerShopHandlers();
|
||||
registerCollectionHandlers();
|
||||
registerChatHandlers();
|
||||
registerModuleHandlers();
|
||||
registerMiscHandlers();
|
||||
logging::info("game", "{} packet handlers registered, {} answered empty",
|
||||
|
||||
@@ -12,12 +12,16 @@
|
||||
|
||||
#include "core/config.h"
|
||||
#include "core/util.h"
|
||||
#include "game/command.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/inventory.h"
|
||||
#include "game/lineup.h"
|
||||
#include "game/player.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "http/http_server.h"
|
||||
#include "net/gateway.h"
|
||||
#include "net/packet.h"
|
||||
#include "net/session.h"
|
||||
#include "net/socket.h"
|
||||
#include "proto/gen/protos.h"
|
||||
#include "sdk/admin.h"
|
||||
@@ -1177,6 +1181,106 @@ void runFlowTests() {
|
||||
proto::MessageGroupStatus::MessageGroupStatus_MessageGroupFinish,
|
||||
"phone conversations read as finished");
|
||||
|
||||
// The CapySR console: a friend to type commands at.
|
||||
client.sendEmpty(cmd::GetFriendListInfoCsReq);
|
||||
proto::GetFriendListInfoScRsp friends;
|
||||
check(client.await(cmd::GetFriendListInfoScRsp, packet) && parseBody(packet, friends) &&
|
||||
friends.friend_list.size() == 1 && friends.friend_list[0].player_info &&
|
||||
friends.friend_list[0].player_info->uid == game::command::kConsoleUid &&
|
||||
friends.friend_list[0].player_info->nickname == "CapySR",
|
||||
"the CapySR console is in the friend list");
|
||||
|
||||
proto::GetPrivateChatHistoryCsReq history;
|
||||
history.contact_side = game::command::kConsoleUid;
|
||||
client.send(cmd::GetPrivateChatHistoryCsReq, history);
|
||||
proto::GetPrivateChatHistoryScRsp opened;
|
||||
check(client.await(cmd::GetPrivateChatHistoryScRsp, packet) && parseBody(packet, opened) &&
|
||||
opened.retcode == 0 && !opened.chat_message_list.empty(),
|
||||
"and opening the thread shows how to use it");
|
||||
|
||||
// Says what the console answered, one line per RevcMsgScNotify. `lastSaid` keeps the
|
||||
// envelope of the final one so the routing fields can be checked too.
|
||||
proto::RevcMsgScNotify lastSaid;
|
||||
proto::RevcMsgScNotify echoed;
|
||||
auto typed = [&](const std::string& text) {
|
||||
proto::SendMsgCsReq msg;
|
||||
msg.target_list.push_back(game::command::kConsoleUid);
|
||||
msg.chat_type = proto::ChatType::ChatType_Private;
|
||||
proto::MessageChatData& data = msg.message_datas.emplace();
|
||||
data.message_type = proto::MsgType::MsgType_CustomText;
|
||||
proto::ChatData& chat = data.chat_data.emplace();
|
||||
chat.message_text = text;
|
||||
chat.PGBLIFDKBHK_case = proto::ChatData::k_message_text;
|
||||
client.send(cmd::SendMsgCsReq, msg);
|
||||
|
||||
std::vector<std::string> lines;
|
||||
net::Packet reply;
|
||||
proto::SendMsgScRsp sent;
|
||||
if (!client.await(cmd::SendMsgScRsp, reply) || !parseBody(reply, sent) || sent.retcode != 0) {
|
||||
return lines;
|
||||
}
|
||||
while (client.sawNotify(cmd::RevcMsgScNotify)) {
|
||||
proto::RevcMsgScNotify said;
|
||||
if (!client.await(cmd::RevcMsgScNotify, reply) || !parseBody(reply, said)) break;
|
||||
if (said.recv_message_data && !said.recv_message_data->message_datas.empty() &&
|
||||
said.recv_message_data->message_datas[0].chat_data) {
|
||||
// The player's own line comes back too, so the client can draw it; only
|
||||
// the console's own answers are counted here.
|
||||
bool fromConsole = said.recv_message_data->BKOALKHDLOB &&
|
||||
said.recv_message_data->BKOALKHDLOB->role_id == game::command::kConsoleUid;
|
||||
if (fromConsole) {
|
||||
lines.push_back(said.recv_message_data->message_datas[0].chat_data->message_text);
|
||||
lastSaid = said;
|
||||
} else {
|
||||
echoed = said;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
std::vector<std::string> helped = typed("/help");
|
||||
check(helped.size() == game::command::list().size() + 1 &&
|
||||
helped[0].find("CapySR console") != std::string::npos,
|
||||
"/help answers with every command");
|
||||
|
||||
// The three fields that decide whether the client shows the line at all. `source_uid`
|
||||
// is the side the message is for -- the player -- and both unnamed role fields have
|
||||
// to name the sender. Get any of them wrong and the reply arrives and renders
|
||||
// nothing, which is exactly what happened in game while this test still passed.
|
||||
check(lastSaid.source_uid == core::Config::get().player.uid && lastSaid.recv_message_data &&
|
||||
lastSaid.recv_message_data->CKHPFFENOBE && lastSaid.recv_message_data->BKOALKHDLOB &&
|
||||
lastSaid.recv_message_data->CKHPFFENOBE->role_id == game::command::kConsoleUid &&
|
||||
lastSaid.recv_message_data->BKOALKHDLOB->role_id == game::command::kConsoleUid,
|
||||
"and addresses them to the player, from the console");
|
||||
|
||||
check(echoed.source_uid == game::command::kConsoleUid && echoed.recv_message_data &&
|
||||
echoed.recv_message_data->BKOALKHDLOB &&
|
||||
echoed.recv_message_data->BKOALKHDLOB->role_id == core::Config::get().player.uid &&
|
||||
!echoed.recv_message_data->message_datas.empty() &&
|
||||
echoed.recv_message_data->message_datas[0].chat_data &&
|
||||
echoed.recv_message_data->message_datas[0].chat_data->message_text == "/help",
|
||||
"the player's own line is handed back so the client can draw it");
|
||||
|
||||
auto bagCount = [&](uint32_t itemId) -> uint32_t {
|
||||
std::vector<std::shared_ptr<net::Session>> live = gateway.sessions();
|
||||
if (live.empty() || live[0]->player() == nullptr) return 0;
|
||||
return static_cast<uint32_t>(game::inventory::held(*live[0]->player(), itemId));
|
||||
};
|
||||
uint32_t heldBefore = bagCount(101);
|
||||
std::vector<std::string> gave = typed("/give 101 7");
|
||||
check(gave.size() == 1 && gave[0].find("gave 7") != std::string::npos &&
|
||||
bagCount(101) == heldBefore + 7,
|
||||
"/give puts the items in the bag");
|
||||
|
||||
std::vector<std::string> nowhere = typed("/nope");
|
||||
check(nowhere.size() == 1 && nowhere[0].find("no command") != std::string::npos,
|
||||
"and an unknown command says so instead of going quiet");
|
||||
|
||||
std::vector<std::string> chatter = typed("hello");
|
||||
check(chatter.size() == 1 && chatter[0].find("/help") != std::string::npos,
|
||||
"plain chat is nudged towards /help");
|
||||
|
||||
// An unimplemented request still has to complete, or the client hangs on it. This
|
||||
// one has no handler at all, so it exercises the name-derived fallback.
|
||||
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
||||
|
||||
Reference in new issue
Block a user