feat(game): hand over pom-pom skins, the jukebox, the data bank and messages

This commit is contained in:
capyb2222 committed 2026-09-13 10:25:23 -07:00
1 parent a6d15b6262
commit 4dda820c44
9 files changed
+227 -6

No files matched your search

+10
View File
@@ -255,6 +255,16 @@ GetShopListCsReq
GetShopListScRsp GetShopListScRsp
BuyGoodsCsReq BuyGoodsCsReq
BuyGoodsScRsp BuyGoodsScRsp
GetPamSkinDataCsReq
GetPamSkinDataScRsp
SelectPamSkinCsReq
SelectPamSkinScRsp
GetJukeboxDataCsReq
GetJukeboxDataScRsp
PlayBackGroundMusicCsReq
PlayBackGroundMusicScRsp
GetNpcMessageGroupCsReq
GetNpcMessageGroupScRsp
UseItemCsReq UseItemCsReq
UseItemScRsp UseItemScRsp
SellItemCsReq SellItemCsReq
+152
View File
@@ -0,0 +1,152 @@
// Pom-Pom's outfits, the Jukebox, the Data Bank and phone messages. A private server
// has nothing to earn them with, so each collection is handed over whole.
#include <algorithm>
#include "core/logger.h"
#include "core/util.h"
#include "data/excel.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 {
Player* playerOf(net::Session& session, const char* what) {
Player* player = session.player();
if (player == nullptr) logging::warn("game", "{} before login", what);
return player;
}
void onGetPamSkinData(net::Session& session, const proto::GetPamSkinDataCsReq&) {
Player* player = playerOf(session, "GetPamSkinData");
if (player == nullptr) return;
proto::GetPamSkinDataScRsp rsp;
rsp.retcode = 0;
rsp.cur_skin = player->pamSkin();
rsp.unlock_skin_list = data::Tables::get().pamSkins();
session.send(cmd::GetPamSkinDataScRsp, rsp);
}
void onSelectPamSkin(net::Session& session, const proto::SelectPamSkinCsReq& req) {
Player* player = playerOf(session, "SelectPamSkin");
if (player == nullptr) return;
const std::vector<uint32_t>& skins = data::Tables::get().pamSkins();
if (std::find(skins.begin(), skins.end(), req.pam_skin) != skins.end()) {
player->setPamSkin(req.pam_skin);
player->save();
}
// Both fields carry the outfit now worn.
proto::SelectPamSkinScRsp rsp;
rsp.retcode = 0;
rsp.cur_skin = player->pamSkin();
rsp.set_skin = player->pamSkin();
session.send(cmd::SelectPamSkinScRsp, rsp);
}
void onGetJukeboxData(net::Session& session, const proto::GetJukeboxDataCsReq&) {
Player* player = playerOf(session, "GetJukeboxData");
if (player == nullptr) return;
proto::GetJukeboxDataScRsp rsp;
rsp.retcode = 0;
for (const data::MusicInfo& track : data::Tables::get().music()) {
proto::LHHGCDLCJDA entry;
entry.id = track.id;
entry.group_id = track.groupId;
entry.BIMDKNLMICG = true; // unlocked
rsp.IOKAJIBHLMP.push_back(entry);
}
// What is playing.
rsp.GFFOBALDBPM.emplace().JKNLCEEDAHJ.emplace().id = player->music();
session.send(cmd::GetJukeboxDataScRsp, rsp);
}
void onPlayBackGroundMusic(net::Session& session, const proto::PlayBackGroundMusicCsReq& req) {
Player* player = playerOf(session, "PlayBackGroundMusic");
if (player == nullptr) return;
uint32_t id = req.IKJNIKIGFLF && req.IKJNIKIGFLF->JKNLCEEDAHJ ? req.IKJNIKIGFLF->JKNLCEEDAHJ->id : 0;
proto::PlayBackGroundMusicScRsp rsp;
if (data::Tables::get().musicTrack(id) == nullptr) {
rsp.retcode = fail(proto::Retcode::RET_MUSIC_NOT_EXIST);
} else {
player->setMusic(id);
player->save();
rsp.retcode = 0;
// Echoed whole, pause state included.
rsp.GFFOBALDBPM = *req.IKJNIKIGFLF;
}
session.send(cmd::PlayBackGroundMusicScRsp, rsp);
}
void onGetArchiveData(net::Session& session, const proto::GetArchiveDataCsReq&) {
const data::Tables& tables = data::Tables::get();
proto::GetArchiveDataScRsp rsp;
rsp.retcode = 0;
auto& archive = rsp.archive_data.emplace();
for (const auto& [id, lightcone] : tables.lightcones()) archive.archive_equipment_id_list.push_back(id);
std::sort(archive.archive_equipment_id_list.begin(), archive.archive_equipment_id_list.end());
for (const data::RelicSetPiece& piece : tables.relicSetPieces()) {
proto::RelicList relic;
relic.set_id = piece.setId;
relic.type = piece.type;
archive.relic_list.push_back(relic);
}
std::vector<uint32_t> monsters;
for (const auto& [id, monster] : tables.monsters()) monsters.push_back(id);
std::sort(monsters.begin(), monsters.end());
for (uint32_t id : monsters) {
proto::MonsterList entry;
entry.monster_id = id;
entry.num = 1;
archive.kill_monster_list.push_back(entry);
}
session.send(cmd::GetArchiveDataScRsp, rsp);
}
void onGetNpcMessageGroup(net::Session& session, const proto::GetNpcMessageGroupCsReq& req) {
// Nothing tracks how far a conversation got, so each one reads as finished: where the
// story leaves them, and what unlocks gated on a finished chat look for.
const data::Tables& tables = data::Tables::get();
proto::GetNpcMessageGroupScRsp rsp;
rsp.retcode = 0;
auto refreshed = static_cast<int64_t>(util::nowSec());
for (uint32_t contact : req.BJIPBBFIBPD) { // contact ids
const std::vector<data::MessageGroupInfo>* groups = tables.messageGroups(contact);
if (groups == nullptr) continue;
for (const data::MessageGroupInfo& info : *groups) {
proto::MessageGroup group;
group.id = info.id;
group.status = proto::MessageGroupStatus::MessageGroupStatus_MessageGroupFinish;
group.refresh_time = refreshed;
for (uint32_t sectionId : info.sections) {
proto::MessageSection section;
section.id = sectionId;
section.status = proto::MessageSectionStatus::MessageSectionStatus_MessageSectionFinish;
group.message_section_list.push_back(section);
}
if (!info.sections.empty()) group.message_section_id = info.sections.back();
rsp.message_group_list.push_back(std::move(group));
}
}
session.send(cmd::GetNpcMessageGroupScRsp, rsp);
}
} // namespace
void registerCollectionHandlers() {
net::on<proto::GetPamSkinDataCsReq>(cmd::GetPamSkinDataCsReq, onGetPamSkinData);
net::on<proto::SelectPamSkinCsReq>(cmd::SelectPamSkinCsReq, onSelectPamSkin);
net::on<proto::GetJukeboxDataCsReq>(cmd::GetJukeboxDataCsReq, onGetJukeboxData);
net::on<proto::PlayBackGroundMusicCsReq>(cmd::PlayBackGroundMusicCsReq, onPlayBackGroundMusic);
net::on<proto::GetArchiveDataCsReq>(cmd::GetArchiveDataCsReq, onGetArchiveData);
net::on<proto::GetNpcMessageGroupCsReq>(cmd::GetNpcMessageGroupCsReq, onGetNpcMessageGroup);
}
} // namespace game
+1
View File
@@ -25,6 +25,7 @@ void registerTierceHandlers();
void registerPeakHandlers(); void registerPeakHandlers();
void registerGachaHandlers(); void registerGachaHandlers();
void registerShopHandlers(); void registerShopHandlers();
void registerCollectionHandlers();
void registerModuleHandlers(); void registerModuleHandlers();
void registerMiscHandlers(); void registerMiscHandlers();
+1
View File
@@ -208,6 +208,7 @@ void registerAllHandlers() {
registerPeakHandlers(); registerPeakHandlers();
registerGachaHandlers(); registerGachaHandlers();
registerShopHandlers(); registerShopHandlers();
registerCollectionHandlers();
registerModuleHandlers(); registerModuleHandlers();
registerMiscHandlers(); registerMiscHandlers();
logging::info("game", "{} packet handlers registered, {} answered empty", logging::info("game", "{} packet handlers registered, {} answered empty",
+1 -4
View File
@@ -43,14 +43,11 @@ constexpr uint16_t kSilent[] = {
cmd::ChimeraDuelGetDataCsReq, cmd::ChimeraDuelGetDataCsReq,
cmd::ChenLingGetDataCsReq, cmd::ChenLingGetDataCsReq,
cmd::GetEvolveBuildQueryInfoCsReq, cmd::GetEvolveBuildQueryInfoCsReq,
cmd::GetJukeboxDataCsReq,
// Second round, from the OS 4.5.53 client's own log: CycleScore.Sync, // Second round, from the OS 4.5.53 client's own log: CycleScore.Sync,
// PetMarbleModule._OnPetMarbleGetDataScRsp, ArchiveModule._OnGetArchiveDataScRsp // PetMarbleModule._OnPetMarbleGetDataScRsp and DrinkMakerBar._UpdateGameplayData
// (through AvatarArchiveData.Sync) and DrinkMakerBar._UpdateGameplayData all
// dereference their way through an empty body. // dereference their way through an empty body.
cmd::CycleScoreRewardGetDataCsReq, cmd::CycleScoreRewardGetDataCsReq,
cmd::PetMarbleGetDataCsReq, cmd::PetMarbleGetDataCsReq,
cmd::GetArchiveDataCsReq,
cmd::GetDrinkMakerDataCsReq, cmd::GetDrinkMakerDataCsReq,
}; };
+7
View File
@@ -216,6 +216,11 @@ public:
std::map<uint32_t, GoodsPurchase>& goodsPurchases() { return goodsPurchases_; } std::map<uint32_t, GoodsPurchase>& goodsPurchases() { return goodsPurchases_; }
const std::map<uint32_t, GoodsPurchase>& goodsPurchases() const { return goodsPurchases_; } const std::map<uint32_t, GoodsPurchase>& goodsPurchases() const { return goodsPurchases_; }
uint32_t pamSkin() const { return pamSkin_; }
void setPamSkin(uint32_t value) { pamSkin_ = value; }
uint32_t music() const { return music_; }
void setMusic(uint32_t value) { music_ = value; }
uint32_t mainCharacter() const { return mainCharacter_; } uint32_t mainCharacter() const { return mainCharacter_; }
void setMainCharacter(uint32_t value) { mainCharacter_ = value; } void setMainCharacter(uint32_t value) { mainCharacter_ = value; }
uint32_t marchType() const { return marchType_; } uint32_t marchType() const { return marchType_; }
@@ -257,6 +262,8 @@ private:
std::map<uint32_t, ChallengeRecord> challengeRecords_; std::map<uint32_t, ChallengeRecord> challengeRecords_;
std::map<uint32_t, uint64_t> challengeRewardsTaken_; std::map<uint32_t, uint64_t> challengeRewardsTaken_;
std::map<uint32_t, GoodsPurchase> goodsPurchases_; std::map<uint32_t, GoodsPurchase> goodsPurchases_;
uint32_t pamSkin_ = 252000;
uint32_t music_ = 210000;
uint32_t mainCharacter_ = 8008; uint32_t mainCharacter_ = 8008;
uint32_t marchType_ = 1224; uint32_t marchType_ = 1224;
+4
View File
@@ -244,6 +244,8 @@ bool loadPlayerState(Player& player) {
} }
} }
player.setPamSkin(u32(j, "pam_skin", player.pamSkin()));
player.setMusic(u32(j, "music", player.music()));
if (auto bought = j.find("goods_purchases"); bought != j.end() && bought->is_array()) { if (auto bought = j.find("goods_purchases"); bought != j.end() && bought->is_array()) {
for (const json& entry : *bought) { for (const json& entry : *bought) {
uint32_t goods = entry.is_object() ? u32(entry, "goods", 0) : 0; uint32_t goods = entry.is_object() ? u32(entry, "goods", 0) : 0;
@@ -369,6 +371,8 @@ std::string playerStateJson(const Player& player) {
} }
j["challenge_rewards_taken"] = taken; j["challenge_rewards_taken"] = taken;
j["pam_skin"] = player.pamSkin();
j["music"] = player.music();
json bought = json::array(); json bought = json::array();
for (const auto& [goods, purchase] : player.goodsPurchases()) { for (const auto& [goods, purchase] : player.goodsPurchases()) {
if (purchase.times == 0) continue; if (purchase.times == 0) continue;
+47 -1
View File
@@ -1122,7 +1122,7 @@ void runFlowTests() {
claimed.retcode == 0 && !claimed.taken_reward_list.empty(), claimed.retcode == 0 && !claimed.taken_reward_list.empty(),
"season rewards can be claimed"); "season rewards can be claimed");
// Shops. // Shops, Pom-Pom's outfits, the Jukebox, the Data Bank and phone messages.
proto::GetShopListCsReq shops; proto::GetShopListCsReq shops;
shops.shop_type = 1; shops.shop_type = 1;
client.send(cmd::GetShopListCsReq, shops); client.send(cmd::GetShopListCsReq, shops);
@@ -1131,6 +1131,52 @@ void runFlowTests() {
!shopList.shop_list.empty() && !shopList.shop_list[0].goods_list.empty(), !shopList.shop_list.empty() && !shopList.shop_list[0].goods_list.empty(),
"a shop lists its goods"); "a shop lists its goods");
client.sendEmpty(cmd::GetPamSkinDataCsReq);
proto::GetPamSkinDataScRsp skins;
check(client.await(cmd::GetPamSkinDataScRsp, packet) && parseBody(packet, skins) &&
skins.unlock_skin_list.size() >= 5 && skins.cur_skin == 252000,
"every Pom-Pom outfit is unlocked");
proto::SelectPamSkinCsReq outfit;
outfit.pam_skin = 252001;
client.send(cmd::SelectPamSkinCsReq, outfit);
proto::SelectPamSkinScRsp worn;
check(client.await(cmd::SelectPamSkinScRsp, packet) && parseBody(packet, worn) && worn.retcode == 0 &&
worn.cur_skin == 252001,
"and one can be worn");
client.sendEmpty(cmd::GetJukeboxDataCsReq);
proto::GetJukeboxDataScRsp jukebox;
check(client.await(cmd::GetJukeboxDataScRsp, packet) && parseBody(packet, jukebox) &&
jukebox.IOKAJIBHLMP.size() > 200 && jukebox.GFFOBALDBPM.has(),
"the Jukebox lists its tracks and what is playing");
if (jukebox.IOKAJIBHLMP.size() > 1) {
proto::PlayBackGroundMusicCsReq play;
play.IKJNIKIGFLF.emplace().JKNLCEEDAHJ.emplace().id = jukebox.IOKAJIBHLMP[1].id;
client.send(cmd::PlayBackGroundMusicCsReq, play);
proto::PlayBackGroundMusicScRsp playing;
check(client.await(cmd::PlayBackGroundMusicScRsp, packet) && parseBody(packet, playing) &&
playing.retcode == 0 && playing.GFFOBALDBPM && playing.GFFOBALDBPM->JKNLCEEDAHJ &&
playing.GFFOBALDBPM->JKNLCEEDAHJ->id == jukebox.IOKAJIBHLMP[1].id,
"and plays a pick");
}
client.sendEmpty(cmd::GetArchiveDataCsReq);
proto::GetArchiveDataScRsp archive;
check(client.await(cmd::GetArchiveDataScRsp, packet) && parseBody(packet, archive) && archive.archive_data &&
!archive.archive_data->kill_monster_list.empty() && !archive.archive_data->relic_list.empty() &&
!archive.archive_data->archive_equipment_id_list.empty(),
"the Data Bank is filled");
proto::GetNpcMessageGroupCsReq contacts;
contacts.BJIPBBFIBPD = {1002};
client.send(cmd::GetNpcMessageGroupCsReq, contacts);
proto::GetNpcMessageGroupScRsp conversations;
check(client.await(cmd::GetNpcMessageGroupScRsp, packet) && parseBody(packet, conversations) &&
!conversations.message_group_list.empty() &&
conversations.message_group_list[0].status ==
proto::MessageGroupStatus::MessageGroupStatus_MessageGroupFinish,
"phone conversations read as finished");
// 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);
+4 -1
View File
@@ -672,10 +672,13 @@ void testShop() {
std::string realPath = core::Config::get().paths.playerFile; std::string realPath = core::Config::get().paths.playerFile;
core::Config::get().paths.playerFile = "build/test-shop-player.json"; core::Config::get().paths.playerFile = "build/test-shop-player.json";
player.setPamSkin(252003);
player.setMusic(210001);
game::savePlayerState(player); game::savePlayerState(player);
game::Player loaded(1); game::Player loaded(1);
game::loadPlayerState(loaded); game::loadPlayerState(loaded);
check(loaded.goodsPurchases().count(pick->id) == 1, "purchases survive a restart"); check(loaded.goodsPurchases().count(pick->id) == 1 && loaded.pamSkin() == 252003 && loaded.music() == 210001,
"purchases, the outfit and the music survive a restart");
core::Config::get().paths.playerFile = realPath; core::Config::get().paths.playerFile = realPath;
} }