diff --git a/src/game/battle_handlers.cpp b/src/game/battle_handlers.cpp index 5fddc63..5d52356 100644 --- a/src/game/battle_handlers.cpp +++ b/src/game/battle_handlers.cpp @@ -184,6 +184,22 @@ void onSceneCastSkillCostMp(net::Session& session, const proto::SceneCastSkillCo int64_t now() { return static_cast(util::nowSec()); } +// The monsters a won fight removes. A Stagnant Shadow stays, to be farmed again. +std::vector defeated(const Player& player, const std::vector& entityIds) { + const data::Tables& tables = data::Tables::get(); + std::vector out; + for (uint32_t entityId : entityIds) { + const SceneEntity* entity = player.sceneState().find(entityId); + if (entity == nullptr || entity->kind != EntityKind::Monster) continue; + uint32_t stage = entity->stageId != 0 + ? entity->stageId + : tables.stageForEvent(entity->eventId, player.worldLevel()); + if (stage != 0 && tables.farmElement(stage) != nullptr) continue; + out.push_back(entityId); + } + return out; +} + // Calyx: the one fight a srtools build is allowed to take over, monsters, blessings and // all. `wave` is how many runs were bought, one stage each. BattleRequest cocoonRequest(uint32_t cocoonId, uint32_t wave, uint32_t worldLevel) { @@ -453,7 +469,7 @@ void onPveBattleResult(net::Session& session, const proto::PVEBattleResultCsReq& BattleContext& context = player->battle(); bool won = req.end_status == proto::BattleEndStatus::BATTLE_END_WIN && context.active && context.battleId == req.battle_id; - if (won) notify::monstersRemoved(session, *player, context.monsterEntityIds); + if (won) notify::monstersRemoved(session, *player, defeated(*player, context.monsterEntityIds)); // A farming run costs and pays only when it is won; losing or fleeing is free. std::vector drops; if (won && (context.staminaCost != 0 || context.mappingInfoId != 0)) { diff --git a/src/game/notify.cpp b/src/game/notify.cpp index d5b65f6..9d6f8c6 100644 --- a/src/game/notify.cpp +++ b/src/game/notify.cpp @@ -85,7 +85,9 @@ void monstersRemoved(net::Session& session, Player& player, for (uint32_t groupId : groups) { auto& group = refresh.group_refresh_list.emplace_back(); group.group_id = groupId; - group.refresh_type = proto::SceneGroupRefreshType::SCENE_GROUP_REFRESH_TYPE_LOADED; + // Deletes go out as type 3. LOADED sends the client down a legacy path that + // throws, leaving the monster alive on its side but gone from ours. + group.refresh_type = proto::SceneGroupRefreshType::SCENE_GROUP_REFRESH_TYPE_AFIBFMAFNCC; for (uint32_t entityId : entityIds) { const SceneEntity* entity = player.sceneState().find(entityId); if (entity == nullptr || entity->groupId != groupId) continue; diff --git a/tests/test_flow.cpp b/tests/test_flow.cpp index 7429b3e..d5053be 100644 --- a/tests/test_flow.cpp +++ b/tests/test_flow.cpp @@ -402,6 +402,24 @@ void runFlowTests() { check(resultRsp.battle_id == result.battle_id, "battle id echoed"); check(resultRsp.end_status == proto::BattleEndStatus::BATTLE_END_WIN, "end status echoed"); + + // LOADED here throws in the client and leaves the monster standing. + // Earlier refreshes (the lineup edit's) are still queued, so skip to + // the one that deletes. + bool deleted = false; + net::Packet refreshPacket; + while (!deleted && client.await(cmd::SceneGroupRefreshScNotify, refreshPacket, 500)) { + proto::SceneGroupRefreshScNotify refresh; + if (!parseBody(refreshPacket, refresh)) break; + for (const proto::GroupRefreshInfo& group : refresh.group_refresh_list) { + for (const proto::SceneEntityRefreshInfo& change : group.refresh_entity) { + if (change.delete_entity != monsters[0]) continue; + deleted = group.refresh_type == + proto::SceneGroupRefreshType::SCENE_GROUP_REFRESH_TYPE_AFIBFMAFNCC; + } + } + } + check(deleted, "the beaten monster is deleted the way the client accepts"); } }