From 4dda820c44e49b5d8849babd9ab3513f3cd1c76a Mon Sep 17 00:00:00 2001 From: capyb2222 Date: Sun, 13 Sep 2026 10:25:23 -0700 Subject: [PATCH] feat(game): hand over pom-pom skins, the jukebox, the data bank and messages --- proto/seeds.txt | 10 ++ src/game/collection_handlers.cpp | 152 +++++++++++++++++++++++++++++++ src/game/handlers.h | 1 + src/game/misc_handlers.cpp | 1 + src/game/module_handlers.cpp | 5 +- src/game/player.h | 7 ++ src/game/player_store.cpp | 4 + tests/test_flow.cpp | 48 +++++++++- tests/test_game.cpp | 5 +- 9 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 src/game/collection_handlers.cpp diff --git a/proto/seeds.txt b/proto/seeds.txt index 1c49140..d8d4a5e 100644 --- a/proto/seeds.txt +++ b/proto/seeds.txt @@ -255,6 +255,16 @@ GetShopListCsReq GetShopListScRsp BuyGoodsCsReq BuyGoodsScRsp +GetPamSkinDataCsReq +GetPamSkinDataScRsp +SelectPamSkinCsReq +SelectPamSkinScRsp +GetJukeboxDataCsReq +GetJukeboxDataScRsp +PlayBackGroundMusicCsReq +PlayBackGroundMusicScRsp +GetNpcMessageGroupCsReq +GetNpcMessageGroupScRsp UseItemCsReq UseItemScRsp SellItemCsReq diff --git a/src/game/collection_handlers.cpp b/src/game/collection_handlers.cpp new file mode 100644 index 0000000..5340b0a --- /dev/null +++ b/src/game/collection_handlers.cpp @@ -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 + +#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& 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 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(util::nowSec()); + for (uint32_t contact : req.BJIPBBFIBPD) { // contact ids + const std::vector* 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(cmd::GetPamSkinDataCsReq, onGetPamSkinData); + net::on(cmd::SelectPamSkinCsReq, onSelectPamSkin); + net::on(cmd::GetJukeboxDataCsReq, onGetJukeboxData); + net::on(cmd::PlayBackGroundMusicCsReq, onPlayBackGroundMusic); + net::on(cmd::GetArchiveDataCsReq, onGetArchiveData); + net::on(cmd::GetNpcMessageGroupCsReq, onGetNpcMessageGroup); +} + +} // namespace game diff --git a/src/game/handlers.h b/src/game/handlers.h index e9d4363..2ad296e 100644 --- a/src/game/handlers.h +++ b/src/game/handlers.h @@ -25,6 +25,7 @@ void registerTierceHandlers(); void registerPeakHandlers(); void registerGachaHandlers(); void registerShopHandlers(); +void registerCollectionHandlers(); void registerModuleHandlers(); void registerMiscHandlers(); diff --git a/src/game/misc_handlers.cpp b/src/game/misc_handlers.cpp index 2761318..ef8791a 100644 --- a/src/game/misc_handlers.cpp +++ b/src/game/misc_handlers.cpp @@ -208,6 +208,7 @@ void registerAllHandlers() { registerPeakHandlers(); registerGachaHandlers(); registerShopHandlers(); + registerCollectionHandlers(); registerModuleHandlers(); registerMiscHandlers(); logging::info("game", "{} packet handlers registered, {} answered empty", diff --git a/src/game/module_handlers.cpp b/src/game/module_handlers.cpp index f14f3bb..5044a52 100644 --- a/src/game/module_handlers.cpp +++ b/src/game/module_handlers.cpp @@ -43,14 +43,11 @@ constexpr uint16_t kSilent[] = { cmd::ChimeraDuelGetDataCsReq, cmd::ChenLingGetDataCsReq, cmd::GetEvolveBuildQueryInfoCsReq, - cmd::GetJukeboxDataCsReq, // Second round, from the OS 4.5.53 client's own log: CycleScore.Sync, - // PetMarbleModule._OnPetMarbleGetDataScRsp, ArchiveModule._OnGetArchiveDataScRsp - // (through AvatarArchiveData.Sync) and DrinkMakerBar._UpdateGameplayData all + // PetMarbleModule._OnPetMarbleGetDataScRsp and DrinkMakerBar._UpdateGameplayData // dereference their way through an empty body. cmd::CycleScoreRewardGetDataCsReq, cmd::PetMarbleGetDataCsReq, - cmd::GetArchiveDataCsReq, cmd::GetDrinkMakerDataCsReq, }; diff --git a/src/game/player.h b/src/game/player.h index 984d716..e532209 100644 --- a/src/game/player.h +++ b/src/game/player.h @@ -216,6 +216,11 @@ public: std::map& goodsPurchases() { return goodsPurchases_; } const std::map& 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_; } void setMainCharacter(uint32_t value) { mainCharacter_ = value; } uint32_t marchType() const { return marchType_; } @@ -257,6 +262,8 @@ private: std::map challengeRecords_; std::map challengeRewardsTaken_; std::map goodsPurchases_; + uint32_t pamSkin_ = 252000; + uint32_t music_ = 210000; uint32_t mainCharacter_ = 8008; uint32_t marchType_ = 1224; diff --git a/src/game/player_store.cpp b/src/game/player_store.cpp index f777e36..4f5917a 100644 --- a/src/game/player_store.cpp +++ b/src/game/player_store.cpp @@ -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()) { for (const json& entry : *bought) { 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["pam_skin"] = player.pamSkin(); + j["music"] = player.music(); json bought = json::array(); for (const auto& [goods, purchase] : player.goodsPurchases()) { if (purchase.times == 0) continue; diff --git a/tests/test_flow.cpp b/tests/test_flow.cpp index 29f931e..93ebc27 100644 --- a/tests/test_flow.cpp +++ b/tests/test_flow.cpp @@ -1122,7 +1122,7 @@ void runFlowTests() { claimed.retcode == 0 && !claimed.taken_reward_list.empty(), "season rewards can be claimed"); - // Shops. + // Shops, Pom-Pom's outfits, the Jukebox, the Data Bank and phone messages. proto::GetShopListCsReq shops; shops.shop_type = 1; client.send(cmd::GetShopListCsReq, shops); @@ -1131,6 +1131,52 @@ void runFlowTests() { !shopList.shop_list.empty() && !shopList.shop_list[0].goods_list.empty(), "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 // one has no handler at all, so it exercises the name-derived fallback. client.sendEmpty(cmd::GetStarFightDataCsReq); diff --git a/tests/test_game.cpp b/tests/test_game.cpp index 3894656..a0ba2a3 100644 --- a/tests/test_game.cpp +++ b/tests/test_game.cpp @@ -672,10 +672,13 @@ void testShop() { std::string realPath = core::Config::get().paths.playerFile; core::Config::get().paths.playerFile = "build/test-shop-player.json"; + player.setPamSkin(252003); + player.setMusic(210001); game::savePlayerState(player); game::Player loaded(1); 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; }