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
|
GetBigDataAllRecommendCsReq
|
||||||
GetBigDataAllRecommendScRsp
|
GetBigDataAllRecommendScRsp
|
||||||
|
|
||||||
|
# ---- friends / chat: the CapySR console ----
|
||||||
|
GetFriendListInfoCsReq
|
||||||
|
GetFriendListInfoScRsp
|
||||||
|
GetFriendLoginInfoCsReq
|
||||||
|
GetFriendLoginInfoScRsp
|
||||||
|
GetChatEmojiListCsReq
|
||||||
|
GetChatEmojiListScRsp
|
||||||
|
GetChatFriendHistoryCsReq
|
||||||
|
GetChatFriendHistoryScRsp
|
||||||
|
GetLoginChatInfoCsReq
|
||||||
|
GetLoginChatInfoScRsp
|
||||||
|
GetPrivateChatHistoryCsReq
|
||||||
|
GetPrivateChatHistoryScRsp
|
||||||
|
SendMsgCsReq
|
||||||
|
SendMsgScRsp
|
||||||
|
RevcMsgScNotify
|
||||||
|
|
||||||
# ---- mission / tutorial ----
|
# ---- mission / tutorial ----
|
||||||
GetMissionStatusCsReq
|
GetMissionStatusCsReq
|
||||||
GetMissionStatusScRsp
|
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 registerGachaHandlers();
|
||||||
void registerShopHandlers();
|
void registerShopHandlers();
|
||||||
void registerCollectionHandlers();
|
void registerCollectionHandlers();
|
||||||
|
void registerChatHandlers();
|
||||||
void registerModuleHandlers();
|
void registerModuleHandlers();
|
||||||
void registerMiscHandlers();
|
void registerMiscHandlers();
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,8 @@ constexpr uint16_t kEmptyReplies[] = {
|
|||||||
cmd::GetAllServerPrefsDataCsReq,
|
cmd::GetAllServerPrefsDataCsReq,
|
||||||
cmd::GetQuestRecordCsReq,
|
cmd::GetQuestRecordCsReq,
|
||||||
cmd::GetRogueInfoCsReq,
|
cmd::GetRogueInfoCsReq,
|
||||||
cmd::GetFriendListInfoCsReq,
|
|
||||||
cmd::GetFriendApplyListInfoCsReq,
|
cmd::GetFriendApplyListInfoCsReq,
|
||||||
cmd::GetFriendLoginInfoCsReq,
|
|
||||||
cmd::GetFriendRecommendListInfoCsReq,
|
cmd::GetFriendRecommendListInfoCsReq,
|
||||||
cmd::GetChatEmojiListCsReq,
|
|
||||||
cmd::GetPrivateChatHistoryCsReq,
|
|
||||||
cmd::GetPhoneDataCsReq,
|
cmd::GetPhoneDataCsReq,
|
||||||
cmd::GetMailCsReq,
|
cmd::GetMailCsReq,
|
||||||
cmd::GetSecretKeyInfoCsReq,
|
cmd::GetSecretKeyInfoCsReq,
|
||||||
@@ -47,7 +43,6 @@ constexpr uint16_t kEmptyReplies[] = {
|
|||||||
cmd::GetRaidInfoCsReq,
|
cmd::GetRaidInfoCsReq,
|
||||||
cmd::GetMuseumInfoCsReq,
|
cmd::GetMuseumInfoCsReq,
|
||||||
cmd::TextJoinQueryCsReq,
|
cmd::TextJoinQueryCsReq,
|
||||||
cmd::GetLoginChatInfoCsReq,
|
|
||||||
cmd::QueryProductInfoCsReq,
|
cmd::QueryProductInfoCsReq,
|
||||||
cmd::GetLoginActivityCsReq,
|
cmd::GetLoginActivityCsReq,
|
||||||
cmd::GetTrialActivityDataCsReq,
|
cmd::GetTrialActivityDataCsReq,
|
||||||
@@ -209,6 +204,7 @@ void registerAllHandlers() {
|
|||||||
registerGachaHandlers();
|
registerGachaHandlers();
|
||||||
registerShopHandlers();
|
registerShopHandlers();
|
||||||
registerCollectionHandlers();
|
registerCollectionHandlers();
|
||||||
|
registerChatHandlers();
|
||||||
registerModuleHandlers();
|
registerModuleHandlers();
|
||||||
registerMiscHandlers();
|
registerMiscHandlers();
|
||||||
logging::info("game", "{} packet handlers registered, {} answered empty",
|
logging::info("game", "{} packet handlers registered, {} answered empty",
|
||||||
|
|||||||
@@ -12,12 +12,16 @@
|
|||||||
|
|
||||||
#include "core/config.h"
|
#include "core/config.h"
|
||||||
#include "core/util.h"
|
#include "core/util.h"
|
||||||
|
#include "game/command.h"
|
||||||
#include "game/handlers.h"
|
#include "game/handlers.h"
|
||||||
|
#include "game/inventory.h"
|
||||||
#include "game/lineup.h"
|
#include "game/lineup.h"
|
||||||
|
#include "game/player.h"
|
||||||
#include "net/cmd_ids.h"
|
#include "net/cmd_ids.h"
|
||||||
#include "http/http_server.h"
|
#include "http/http_server.h"
|
||||||
#include "net/gateway.h"
|
#include "net/gateway.h"
|
||||||
#include "net/packet.h"
|
#include "net/packet.h"
|
||||||
|
#include "net/session.h"
|
||||||
#include "net/socket.h"
|
#include "net/socket.h"
|
||||||
#include "proto/gen/protos.h"
|
#include "proto/gen/protos.h"
|
||||||
#include "sdk/admin.h"
|
#include "sdk/admin.h"
|
||||||
@@ -1177,6 +1181,106 @@ void runFlowTests() {
|
|||||||
proto::MessageGroupStatus::MessageGroupStatus_MessageGroupFinish,
|
proto::MessageGroupStatus::MessageGroupStatus_MessageGroupFinish,
|
||||||
"phone conversations read as finished");
|
"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
|
// 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.
|
// one has no handler at all, so it exercises the name-derived fallback.
|
||||||
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
||||||
|
|||||||
Reference in new issue
Block a user