feat(game): add the new challenge modes
This commit is contained in:
1 parent
e77d7600ae
commit
9dd37f8087
31 files changed
+2536
-70
No files matched your search
+12
-3
@@ -1,8 +1,16 @@
|
||||
{
|
||||
"server": {
|
||||
"name": "CapySR",
|
||||
"http": { "bind": "0.0.0.0", "port": 21000, "public_host": "127.0.0.1" },
|
||||
"game": { "bind": "0.0.0.0", "port": 23301, "public_host": "127.0.0.1" }
|
||||
"http": {
|
||||
"bind": "0.0.0.0",
|
||||
"port": 21000,
|
||||
"public_host": "127.0.0.1"
|
||||
},
|
||||
"game": {
|
||||
"bind": "0.0.0.0",
|
||||
"port": 23301,
|
||||
"public_host": "127.0.0.1"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
@@ -35,7 +43,8 @@
|
||||
"global_buffs": true,
|
||||
"unlock_all_challenges": true,
|
||||
"main_character": 8008,
|
||||
"march_type": 1224
|
||||
"march_type": 1224,
|
||||
"skip_missions": []
|
||||
},
|
||||
"paths": {
|
||||
"srtools_file": "data/freesr-data.json",
|
||||
|
||||
@@ -140,6 +140,12 @@ UpdateGroupPropertyCsReq
|
||||
UpdateGroupPropertyScRsp
|
||||
GetFirstTalkNpcCsReq
|
||||
GetFirstTalkNpcScRsp
|
||||
GetFirstTalkByPerformanceNpcCsReq
|
||||
GetFirstTalkByPerformanceNpcScRsp
|
||||
FinishFirstTalkNpcCsReq
|
||||
FinishFirstTalkNpcScRsp
|
||||
FinishFirstTalkByPerformanceNpcCsReq
|
||||
FinishFirstTalkByPerformanceNpcScRsp
|
||||
GetNpcTakenRewardCsReq
|
||||
GetNpcTakenRewardScRsp
|
||||
ActiveFarmElementCsReq
|
||||
@@ -195,6 +201,47 @@ TakeChallengeRewardScRsp
|
||||
ChallengeLineupNotify
|
||||
ChallengeSettleNotify
|
||||
|
||||
# ---- three-node challenge floors (memory of chaos and its siblings, 4.6) ----
|
||||
GetChallengeTierceDataCsReq
|
||||
GetChallengeTierceDataScRsp
|
||||
GetChallengeTierceControllerCsReq
|
||||
GetChallengeTierceControllerScRsp
|
||||
SetChallengeTierceLineupCsReq
|
||||
SetChallengeTierceLineupScRsp
|
||||
StartChallengeTierceCsReq
|
||||
StartChallengeTierceScRsp
|
||||
StartNextChallengeTierceCsReq
|
||||
StartNextChallengeTierceScRsp
|
||||
RestartChallengeTierceCsReq
|
||||
RestartChallengeTierceScRsp
|
||||
ConfirmChallengeTierceStageSettleCsReq
|
||||
ConfirmChallengeTierceStageSettleScRsp
|
||||
LeaveChallengeTierceCsReq
|
||||
LeaveChallengeTierceScRsp
|
||||
ChallengeTierceSyncNotify
|
||||
|
||||
# ---- anomaly arbitration ----
|
||||
GetChallengePeakDataCsReq
|
||||
GetChallengePeakDataScRsp
|
||||
GetCurChallengePeakCsReq
|
||||
GetCurChallengePeakScRsp
|
||||
SetChallengePeakMobLineupAvatarCsReq
|
||||
SetChallengePeakMobLineupAvatarScRsp
|
||||
SetChallengePeakBossHardModeCsReq
|
||||
SetChallengePeakBossHardModeScRsp
|
||||
StartChallengePeakCsReq
|
||||
StartChallengePeakScRsp
|
||||
ReStartChallengePeakCsReq
|
||||
ReStartChallengePeakScRsp
|
||||
LeaveChallengePeakCsReq
|
||||
LeaveChallengePeakScRsp
|
||||
ConfirmChallengePeakSettleCsReq
|
||||
ConfirmChallengePeakSettleScRsp
|
||||
TakeChallengePeakRewardCsReq
|
||||
TakeChallengePeakRewardScRsp
|
||||
ChallengePeakGroupDataUpdateScNotify
|
||||
ChallengePeakSettleScNotify
|
||||
|
||||
# ---- inventory ----
|
||||
GetBagCsReq
|
||||
GetBagScRsp
|
||||
|
||||
@@ -104,6 +104,7 @@ bool Config::load(const std::string& path) {
|
||||
pick(*it, "unlock_all_challenges", gameplay.unlockAllChallenges);
|
||||
pick(*it, "main_character", gameplay.mainCharacter);
|
||||
pick(*it, "march_type", gameplay.marchType);
|
||||
pick(*it, "skip_missions", gameplay.skipMissions);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+13
-2
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -58,8 +59,8 @@ struct PathsConfig {
|
||||
};
|
||||
|
||||
struct GameplayConfig {
|
||||
// auto = a srtools build replaces overworld fights; srtools = it replaces every
|
||||
// fight; stage = never, always use the stage the client asked for.
|
||||
// auto = a srtools build replaces calyx fights and nothing else; srtools = it
|
||||
// replaces every fight; stage = never, always use the stage the client asked for.
|
||||
std::string battleSource = "auto";
|
||||
bool globalBuffs = true;
|
||||
// Report every challenge floor as a full clear. A floor with no stars locks the one
|
||||
@@ -67,6 +68,16 @@ struct GameplayConfig {
|
||||
bool unlockAllChallenges = true;
|
||||
uint32_t mainCharacter = 8008;
|
||||
uint32_t marchType = 1224;
|
||||
// Missions never reported as finished. A scene group gated on one of these makes
|
||||
// the 4.5.54 client throw inside MissionModule._CheckVerseByMainMission while it
|
||||
// works out which groups to activate, which kills the whole map load: a hang on the
|
||||
// loading screen when entering at login, a fade that rolls straight back otherwise.
|
||||
// Main and sub mission ids both go here.
|
||||
std::vector<uint32_t> skipMissions;
|
||||
|
||||
bool missionSkipped(uint32_t id) const {
|
||||
return std::find(skipMissions.begin(), skipMissions.end(), id) != skipMissions.end();
|
||||
}
|
||||
};
|
||||
|
||||
struct Config {
|
||||
|
||||
+126
-2
@@ -226,6 +226,23 @@ const T* findById(const std::vector<T>& sorted, uint32_t id) {
|
||||
|
||||
const ChallengeInfo* Tables::challenge(uint32_t id) const { return findById(challenges_, id); }
|
||||
|
||||
const PeakGroupInfo* Tables::peakGroup(uint32_t id) const { return findById(peakGroups_, id); }
|
||||
|
||||
const PeakInfo* Tables::peak(uint32_t id) const {
|
||||
auto it = peaks_.find(id);
|
||||
return it == peaks_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const std::vector<uint32_t>* Tables::peakRewards(uint32_t rewardGroupId) const {
|
||||
auto it = peakRewards_.find(rewardGroupId);
|
||||
return it == peakRewards_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const BattleTargetInfo* Tables::battleTarget(uint32_t id) const {
|
||||
auto it = battleTargets_.find(id);
|
||||
return it == battleTargets_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const ChallengeGroupInfo* Tables::challengeGroup(uint32_t groupId) const {
|
||||
return findById(challengeGroups_, groupId);
|
||||
}
|
||||
@@ -235,6 +252,18 @@ const ChallengeTarget* Tables::challengeTarget(uint32_t id) const {
|
||||
return it == challengeTargets_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const ChallengeTierceInfo* Tables::challengeTierce(uint32_t id) const {
|
||||
auto it = challengeTierces_.find(id);
|
||||
return it == challengeTierces_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const ChallengeTierceInfo* Tables::challengeTierceFor(uint32_t challengeId) const {
|
||||
for (const auto& [id, info] : challengeTierces_) {
|
||||
if (info.preChallengeId == challengeId) return &info;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint64_t Tables::challengeRewardStars(uint32_t rewardLineGroupId) const {
|
||||
auto it = challengeRewardStars_.find(rewardLineGroupId);
|
||||
return it == challengeRewardStars_.end() ? 0 : it->second;
|
||||
@@ -545,6 +574,31 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
||||
challengeFloors("ChallengeStoryMazeConfig.json", ChallengeKind::Story);
|
||||
challengeFloors("ChallengeBossMazeConfig.json", ChallengeKind::Boss);
|
||||
|
||||
// The third node, beta-only so far. GNOOAGPBNLD is the floor-wide cycle limit;
|
||||
// only the Memory rows carry one.
|
||||
for (const json* row : table("ChallengeMazeTierceConfig.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
if (id == 0 || challengeTierces_.count(id) != 0) continue;
|
||||
ChallengeTierceInfo info;
|
||||
info.id = id;
|
||||
info.preChallengeId = u32(*row, "PreChallengeMazeID");
|
||||
info.mapEntranceId = u32(*row, "MapEntranceID");
|
||||
info.mazeGroupId = u32(*row, "MazeGroupID");
|
||||
info.roundLimit = u32(*row, "GNOOAGPBNLD");
|
||||
info.targetIds = u32List(*row, "ChallengeTargetID");
|
||||
std::vector<uint32_t> configs = u32List(*row, "ConfigList");
|
||||
std::vector<uint32_t> npcs = u32List(*row, "NpcMonsterIDList");
|
||||
std::vector<uint32_t> events = u32List(*row, "EventIDList");
|
||||
for (size_t i = 0; i < configs.size(); ++i) {
|
||||
ChallengeMonster monster;
|
||||
monster.configId = configs[i];
|
||||
monster.npcMonsterId = i < npcs.size() ? npcs[i] : 0;
|
||||
monster.eventId = i < events.size() ? events[i] : 0;
|
||||
info.monsters.push_back(monster);
|
||||
}
|
||||
if (info.preChallengeId != 0) challengeTierces_.emplace(id, std::move(info));
|
||||
}
|
||||
|
||||
// Pure Fiction keeps its turn limit and its score targets one table over.
|
||||
for (const json* row : table("ChallengeStoryMazeExtra.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
@@ -594,6 +648,61 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
||||
challengeRewards("ChallengeStoryRewardLine.json");
|
||||
challengeRewards("ChallengeBossRewardLine.json");
|
||||
|
||||
// Anomaly Arbitration. Only the beta dump places the fights -- production rows
|
||||
// have no arena columns -- so a later source fills what an earlier one left
|
||||
// empty rather than losing the row to it. The last entry of each list is the
|
||||
// encounter itself.
|
||||
for (const json* row : table("ChallengePeakConfig.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
if (id == 0) continue;
|
||||
PeakInfo& peak = peaks_[id];
|
||||
peak.id = id;
|
||||
if (peak.mapEntranceId == 0) peak.mapEntranceId = u32(*row, "MapEntranceID");
|
||||
if (peak.mazeGroupId == 0) peak.mazeGroupId = u32(*row, "MazeGroupID");
|
||||
if (peak.npcMonsterId == 0) {
|
||||
std::vector<uint32_t> npcs = u32List(*row, "NpcMonsterIDList");
|
||||
if (!npcs.empty()) peak.npcMonsterId = npcs.back();
|
||||
}
|
||||
if (peak.eventId == 0) {
|
||||
std::vector<uint32_t> events = u32List(*row, "EventIDList");
|
||||
if (!events.empty()) peak.eventId = events.back();
|
||||
}
|
||||
if (peak.targetIds.empty()) peak.targetIds = u32List(*row, "NormalTargetList");
|
||||
if (peak.tagBuffs.empty()) peak.tagBuffs = u32List(*row, "TagList");
|
||||
}
|
||||
for (const json* row : table("ChallengePeakBossConfig.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
if (id == 0) continue;
|
||||
PeakInfo& peak = peaks_[id];
|
||||
if (peak.boss) continue;
|
||||
peak.id = id;
|
||||
peak.boss = true;
|
||||
peak.bossBuffs = u32List(*row, "BuffList");
|
||||
peak.hardTarget = u32(*row, "HardTarget");
|
||||
std::vector<uint32_t> events = u32List(*row, "HardEventIDList");
|
||||
if (!events.empty()) peak.hardEventId = events.back();
|
||||
peak.hardTagBuffs = u32List(*row, "HardTagList");
|
||||
}
|
||||
for (const json* row : table("ChallengePeakGroupConfig.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
if (id == 0) continue;
|
||||
peakGroups_.push_back({id, u32List(*row, "PreLevelIDList"), u32(*row, "BossLevelID"),
|
||||
u32(*row, "RewardGroupID")});
|
||||
}
|
||||
for (const json* row : table("ChallengePeakReward.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
uint32_t group = u32(*row, "RewardGroupID");
|
||||
if (id == 0 || group == 0) continue;
|
||||
std::vector<uint32_t>& ids = peakRewards_[group];
|
||||
if (std::find(ids.begin(), ids.end(), id) == ids.end()) ids.push_back(id);
|
||||
}
|
||||
for (const json* row : table("BattleTargetConfig.json")) {
|
||||
uint32_t id = u32(*row, "ID");
|
||||
if (id == 0 || battleTargets_.count(id) != 0) continue;
|
||||
bool deaths = str(*row, "AbilityName").find("DeathCount") != std::string::npos;
|
||||
battleTargets_.emplace(id, BattleTargetInfo{id, u32(*row, "TargetParam"), deaths});
|
||||
}
|
||||
|
||||
for (const json* row : table("InteractConfig.json")) {
|
||||
uint32_t id = u32(*row, "InteractID");
|
||||
if (id == 0 || interacts_.count(id) != 0) continue;
|
||||
@@ -634,6 +743,21 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
||||
};
|
||||
sortById(challenges_);
|
||||
sortById(challengeGroups_);
|
||||
sortById(peakGroups_);
|
||||
|
||||
// Each fight learns its season, and plants its one monster over the arena's marker.
|
||||
for (const PeakGroupInfo& group : peakGroups_) {
|
||||
for (uint32_t id : group.mobIds) {
|
||||
if (auto it = peaks_.find(id); it != peaks_.end()) it->second.groupId = group.id;
|
||||
}
|
||||
if (auto it = peaks_.find(group.bossId); it != peaks_.end()) it->second.groupId = group.id;
|
||||
}
|
||||
for (auto& [id, peak] : peaks_) {
|
||||
if (peak.eventId != 0) peak.monsters = {{kPeakMarkerId, peak.npcMonsterId, peak.eventId}};
|
||||
if (peak.hardEventId != 0) {
|
||||
peak.hardMonsters = {{kPeakMarkerId, peak.npcMonsterId, peak.hardEventId}};
|
||||
}
|
||||
}
|
||||
|
||||
for (ChallengeInfo& floor : challenges_) {
|
||||
auto extra = challengeExtras_.find(floor.id);
|
||||
@@ -653,8 +777,8 @@ bool Tables::load(const std::vector<std::string>& sources) {
|
||||
"{} plane events from {} source(s) in {} ms",
|
||||
avatars_.size(), skillPointCount_, stages_.size(), monsters_.size(),
|
||||
entrances_.size(), planeEvents_.size(), used, util::nowMs() - started);
|
||||
logging::info("data", "{} challenge floors in {} seasons",
|
||||
challenges_.size(), challengeGroups_.size());
|
||||
logging::info("data", "{} challenge floors in {} seasons, {} of them with a third node",
|
||||
challenges_.size(), challengeGroups_.size(), challengeTierces_.size());
|
||||
logging::info("data", "{} main missions, {} tutorials, {} guides, {} quests",
|
||||
mainMissions_.size(), tutorials_.size(), tutorialGuides_.size(),
|
||||
quests_.size());
|
||||
|
||||
@@ -127,6 +127,20 @@ struct ChallengeInfo {
|
||||
ChallengeStage stages[2];
|
||||
};
|
||||
|
||||
// The third node 4.6 bolts onto a challenge floor. It brings its own arena, monster and
|
||||
// star targets; the buff, the mode and the first two nodes come from the floor it
|
||||
// extends. The client starts the whole floor through this once a row exists for it.
|
||||
struct ChallengeTierceInfo {
|
||||
uint32_t id = 0;
|
||||
uint32_t preChallengeId = 0; // PreChallengeMazeID, the floor this extends
|
||||
uint32_t mapEntranceId = 0;
|
||||
uint32_t mazeGroupId = 0;
|
||||
// Cycles for the whole floor, MoC only; the other two keep the floor's own limit.
|
||||
uint32_t roundLimit = 0;
|
||||
std::vector<uint32_t> targetIds;
|
||||
std::vector<ChallengeMonster> monsters;
|
||||
};
|
||||
|
||||
// How one star is earned. MoC counts cycles left and deaths; the other two only score.
|
||||
struct ChallengeTarget {
|
||||
uint32_t id = 0;
|
||||
@@ -142,6 +156,47 @@ struct ChallengeGroupInfo {
|
||||
ChallengeKind kind = ChallengeKind::Memory;
|
||||
};
|
||||
|
||||
// Every Anomaly Arbitration arena keeps its one monster at this instance id.
|
||||
constexpr uint32_t kPeakMarkerId = 200001;
|
||||
|
||||
// One Anomaly Arbitration fight: a knight, or the season's boss. Like the other
|
||||
// challenges it is fought on an ordinary floor cut down to one maze group, where a
|
||||
// single monster starts the stage its event names.
|
||||
struct PeakInfo {
|
||||
uint32_t id = 0;
|
||||
uint32_t groupId = 0;
|
||||
uint32_t mapEntranceId = 0;
|
||||
uint32_t mazeGroupId = 0;
|
||||
uint32_t npcMonsterId = 0;
|
||||
uint32_t eventId = 0;
|
||||
std::vector<uint32_t> targetIds; // NormalTargetList
|
||||
std::vector<uint32_t> tagBuffs; // the enemy tags, TagList
|
||||
std::vector<ChallengeMonster> monsters;
|
||||
// The boss only: the buffs the player picks one of, and what hard mode swaps in.
|
||||
bool boss = false;
|
||||
std::vector<uint32_t> bossBuffs;
|
||||
uint32_t hardTarget = 0;
|
||||
uint32_t hardEventId = 0;
|
||||
std::vector<uint32_t> hardTagBuffs;
|
||||
std::vector<ChallengeMonster> hardMonsters;
|
||||
};
|
||||
|
||||
// One Anomaly Arbitration season: three knights and a boss.
|
||||
struct PeakGroupInfo {
|
||||
uint32_t id = 0;
|
||||
std::vector<uint32_t> mobIds;
|
||||
uint32_t bossId = 0;
|
||||
uint32_t rewardGroupId = 0;
|
||||
};
|
||||
|
||||
// BattleTargetConfig, as far as a result is judged by it: a ceiling on the cycles used,
|
||||
// or on the avatars lost.
|
||||
struct BattleTargetInfo {
|
||||
uint32_t id = 0;
|
||||
uint32_t param = 0;
|
||||
bool countsDeaths = false;
|
||||
};
|
||||
|
||||
// MazePlane.PlaneType strings, in the numbering SceneInfo.game_mode_type uses.
|
||||
uint32_t planeTypeFromName(const std::string& name);
|
||||
|
||||
@@ -205,6 +260,9 @@ public:
|
||||
|
||||
// Every floor and season of the three challenge modes, in id order.
|
||||
const std::vector<ChallengeInfo>& challenges() const { return challenges_; }
|
||||
// Keyed by the tierce's own id, and by the floor it extends.
|
||||
const ChallengeTierceInfo* challengeTierce(uint32_t id) const;
|
||||
const ChallengeTierceInfo* challengeTierceFor(uint32_t challengeId) const;
|
||||
const std::vector<ChallengeGroupInfo>& challengeGroups() const { return challengeGroups_; }
|
||||
const ChallengeInfo* challenge(uint32_t id) const;
|
||||
const ChallengeGroupInfo* challengeGroup(uint32_t groupId) const;
|
||||
@@ -212,6 +270,14 @@ public:
|
||||
// Bit n set for every star count the reward line of that group pays out at.
|
||||
uint64_t challengeRewardStars(uint32_t rewardLineGroupId) const;
|
||||
|
||||
// Anomaly Arbitration seasons in id order, and the fights they are made of.
|
||||
const std::vector<PeakGroupInfo>& peakGroups() const { return peakGroups_; }
|
||||
const PeakGroupInfo* peakGroup(uint32_t id) const;
|
||||
const PeakInfo* peak(uint32_t id) const;
|
||||
// ChallengePeakReward rows paid out by one reward group.
|
||||
const std::vector<uint32_t>* peakRewards(uint32_t rewardGroupId) const;
|
||||
const BattleTargetInfo* battleTarget(uint32_t id) const;
|
||||
|
||||
const std::unordered_map<uint32_t, AvatarInfo>& avatars() const { return avatars_; }
|
||||
const std::unordered_map<uint32_t, EntranceInfo>& entrances() const { return entrances_; }
|
||||
|
||||
@@ -240,6 +306,7 @@ private:
|
||||
std::vector<uint32_t> tutorialGuides_;
|
||||
std::vector<uint32_t> quests_;
|
||||
std::vector<ChallengeInfo> challenges_;
|
||||
std::unordered_map<uint32_t, ChallengeTierceInfo> challengeTierces_;
|
||||
std::vector<ChallengeGroupInfo> challengeGroups_;
|
||||
std::unordered_map<uint32_t, ChallengeTarget> challengeTargets_;
|
||||
// ChallengeStoryMazeExtra, held only until load() folds it into the floors.
|
||||
@@ -249,6 +316,10 @@ private:
|
||||
};
|
||||
std::unordered_map<uint32_t, ChallengeExtra> challengeExtras_;
|
||||
std::unordered_map<uint32_t, uint64_t> challengeRewardStars_;
|
||||
std::vector<PeakGroupInfo> peakGroups_;
|
||||
std::unordered_map<uint32_t, PeakInfo> peaks_;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> peakRewards_;
|
||||
std::unordered_map<uint32_t, BattleTargetInfo> battleTargets_;
|
||||
size_t skillPointCount_ = 0;
|
||||
bool loaded_ = false;
|
||||
};
|
||||
|
||||
+26
-9
@@ -56,7 +56,7 @@ bool useSrToolsBattle(const BattleConfig& config, bool allowOverride) {
|
||||
const std::string& source = core::Config::get().gameplay.battleSource;
|
||||
if (source == "stage") return false;
|
||||
if (source == "srtools") return true;
|
||||
return allowOverride; // "auto"
|
||||
return allowOverride; // "auto": the calyx only
|
||||
}
|
||||
|
||||
// Battle types that need a win condition of their own, or the client hangs on the
|
||||
@@ -82,6 +82,17 @@ void addBattleTargets(proto::SceneBattleInfo& info, const std::string& battleTyp
|
||||
}
|
||||
} else if (battleType == "AS") {
|
||||
target(1, 90005);
|
||||
} else if (battleType == "AA") {
|
||||
// Every slot present; the fight's own targets go in the fifth, each with the
|
||||
// ceiling it is judged against.
|
||||
for (uint32_t key = 1; key <= 4; ++key) info.battle_target_info[key];
|
||||
for (uint32_t id : targets) {
|
||||
const data::BattleTargetInfo* config = data::Tables::get().battleTarget(id);
|
||||
proto::BattleTarget entry;
|
||||
entry.id = id;
|
||||
entry.total_progress = config != nullptr ? config->param : 0;
|
||||
info.battle_target_info[5].battle_target_list.push_back(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,16 +176,19 @@ proto::SceneBattleInfo create(Player& player, const BattleRequest& request) {
|
||||
if (useSrToolsBattle(config, request.allowSrToolsOverride)) {
|
||||
info.stage_id = config.stageId;
|
||||
info.rounds_limit = config.cycleCount;
|
||||
for (const std::vector<BattleMonster>& wave : config.waves) {
|
||||
std::vector<uint32_t> ids;
|
||||
uint32_t level = 1;
|
||||
for (const BattleMonster& monster : wave) {
|
||||
for (uint32_t n = 0; n < std::max(1u, monster.amount); ++n) {
|
||||
ids.push_back(monster.monsterId);
|
||||
// A calyx sweep buys several runs, one fight each, so the build's waves repeat.
|
||||
for (uint32_t run = 0; run < std::max(1u, request.wave); ++run) {
|
||||
for (const std::vector<BattleMonster>& wave : config.waves) {
|
||||
std::vector<uint32_t> ids;
|
||||
uint32_t level = 1;
|
||||
for (const BattleMonster& monster : wave) {
|
||||
for (uint32_t n = 0; n < std::max(1u, monster.amount); ++n) {
|
||||
ids.push_back(monster.monsterId);
|
||||
}
|
||||
level = std::max(level, monster.level);
|
||||
}
|
||||
level = std::max(level, monster.level);
|
||||
addWave(info, config.stageId, ids, level);
|
||||
}
|
||||
addWave(info, config.stageId, ids, level);
|
||||
}
|
||||
for (const BattleBuff& blessing : config.blessings) {
|
||||
proto::BattleBuff buff = makeBuff(blessing.id, blessing.level, kNoOwner);
|
||||
@@ -230,6 +244,9 @@ proto::SceneBattleInfo create(Player& player, const BattleRequest& request) {
|
||||
for (uint32_t buffId : {request.mazeBuffId, request.stageBuffId}) {
|
||||
if (buffId != 0) info.buff_list.push_back(makeBuff(buffId, 1, kNoOwner));
|
||||
}
|
||||
for (uint32_t buffId : request.floorBuffIds) {
|
||||
if (buffId != 0) info.buff_list.push_back(makeBuff(buffId, 1, kNoOwner));
|
||||
}
|
||||
if (!request.battleType.empty()) {
|
||||
addBattleTargets(info, request.battleType, request.battleTargetIds, request.scoreSoFar);
|
||||
}
|
||||
|
||||
+7
-4
@@ -20,8 +20,9 @@ struct BattleRequest {
|
||||
std::vector<uint32_t> stageIds;
|
||||
uint32_t cocoonId = 0;
|
||||
uint32_t wave = 0;
|
||||
// Calyx and challenges keep their own stage even when a srtools build is loaded.
|
||||
bool allowSrToolsOverride = true;
|
||||
// Only a calyx hands its fight over to a srtools build; everything else keeps the
|
||||
// stage it named. See gameplay.battle_source.
|
||||
bool allowSrToolsOverride = false;
|
||||
|
||||
// ---- challenges ----------------------------------------------------------
|
||||
// The team fighting this node, when it is not the squad the player walks around
|
||||
@@ -30,10 +31,12 @@ struct BattleRequest {
|
||||
// The floor's own maze buff, and the one the player picked for this half.
|
||||
uint32_t mazeBuffId = 0;
|
||||
uint32_t stageBuffId = 0;
|
||||
// Anomaly Arbitration's enemy tags and the boss buff the player picked.
|
||||
std::vector<uint32_t> floorBuffIds;
|
||||
// MoC's cycle limit, counted down across the whole floor.
|
||||
uint32_t roundsLimit = 0;
|
||||
// "PF" or "AS": the two scored modes need a win condition of their own or the
|
||||
// client hangs on the last turn with nothing to settle.
|
||||
// "PF", "AS" or "AA": modes that need a win condition of their own, or the client
|
||||
// hangs on the last turn with nothing to settle.
|
||||
std::string battleType;
|
||||
// Score carried in from the first half, and Pure Fiction's per-wave targets.
|
||||
uint32_t scoreSoFar = 0;
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "game/challenge.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/notify.h"
|
||||
#include "game/peak.h"
|
||||
#include "game/player.h"
|
||||
#include "game/scene.h"
|
||||
#include "game/tierce.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/handler.h"
|
||||
#include "net/session.h"
|
||||
@@ -30,7 +32,7 @@ bool clearsFodder(const Player& player, uint32_t skillIndex, uint32_t casterEnti
|
||||
if (skillIndex == 0 || casterEntityId == 0) return false;
|
||||
// A challenge node is not cleared by walking through it, and the caster would be
|
||||
// looked up in the overworld squad rather than the challenge team.
|
||||
if (player.challenge().active) return false;
|
||||
if (player.challenge().active || player.peak().active || player.tierce().active) return false;
|
||||
std::vector<uint32_t> party = player.lineups().curMembers();
|
||||
if (casterEntityId > party.size()) return false;
|
||||
uint32_t avatarId = party[casterEntityId - 1];
|
||||
@@ -62,7 +64,6 @@ void applyChallenge(const Player& player, BattleRequest& request) {
|
||||
request.roundsLimit = run.roundsLeft;
|
||||
request.scoreSoFar = run.totalScore();
|
||||
request.battleTargetIds = config->battleTargetIds;
|
||||
request.allowSrToolsOverride = false;
|
||||
switch (config->kind) {
|
||||
case data::ChallengeKind::Story:
|
||||
request.battleType = "PF";
|
||||
@@ -153,6 +154,8 @@ void onSceneCastSkill(net::Session& session, const proto::SceneCastSkillCsReq& r
|
||||
request.skillIndex = req.skill_index;
|
||||
request.monsterEntityIds = monsters;
|
||||
applyChallenge(*player, request);
|
||||
tierce::prepareBattle(*player, request);
|
||||
peak::prepareBattle(*player, request);
|
||||
rsp.battle_info = battle::create(*player, request);
|
||||
report(monsters, proto::MonsterBattleType::MONSTER_BATTLE_TYPE_TRIGGER_BATTLE);
|
||||
session.send(cmd::SceneCastSkillScRsp, rsp);
|
||||
@@ -178,14 +181,14 @@ void onSceneCastSkillCostMp(net::Session& session, const proto::SceneCastSkillCo
|
||||
session.send(cmd::SceneCastSkillCostMpScRsp, rsp);
|
||||
}
|
||||
|
||||
// Calyx and the other farm stages: the cocoon names the stage, so a srtools build
|
||||
// must not replace it. `wave` is how many runs were bought, one stage each.
|
||||
// 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.
|
||||
proto::SceneBattleInfo cocoonBattle(Player& player, uint32_t cocoonId, uint32_t wave,
|
||||
uint32_t worldLevel) {
|
||||
BattleRequest request;
|
||||
request.cocoonId = cocoonId;
|
||||
request.wave = std::max(1u, wave);
|
||||
request.allowSrToolsOverride = false;
|
||||
request.allowSrToolsOverride = true;
|
||||
|
||||
const data::CocoonInfo* cocoon = data::Tables::get().cocoon(cocoonId, worldLevel);
|
||||
if (cocoon == nullptr || cocoon->stageIds.empty()) {
|
||||
@@ -230,7 +233,7 @@ void onQuickStartCocoonStage(net::Session& session,
|
||||
}
|
||||
|
||||
// A Stagnant Shadow started from the Survival Index. PAOFHFLFFHD is the shadow's stage,
|
||||
// the key FarmElementSweep uses too; like a calyx, a srtools build must not replace it.
|
||||
// the key FarmElementSweep uses too.
|
||||
void onQuickStartFarmElement(net::Session& session,
|
||||
const proto::QuickStartFarmElementCsReq& req) {
|
||||
Player* player = playerOf(session, "QuickStartFarmElement");
|
||||
@@ -238,7 +241,6 @@ void onQuickStartFarmElement(net::Session& session,
|
||||
|
||||
uint32_t worldLevel = req.world_level != 0 ? req.world_level : player->worldLevel();
|
||||
BattleRequest request;
|
||||
request.allowSrToolsOverride = false;
|
||||
uint32_t stageId = data::Tables::get().farmElementStage(req.PAOFHFLFFHD, worldLevel);
|
||||
if (stageId != 0) request.stageIds.push_back(stageId);
|
||||
|
||||
@@ -318,10 +320,8 @@ void onReEnterLastElementStage(net::Session& session,
|
||||
Player* player = playerOf(session, "ReEnterLastElementStage");
|
||||
if (player == nullptr) return;
|
||||
|
||||
// The retry button names the stage it wants replayed, so a srtools build must not
|
||||
// substitute its own -- same rule as a calyx.
|
||||
// The retry button names the stage it wants replayed.
|
||||
BattleRequest request;
|
||||
request.allowSrToolsOverride = false;
|
||||
if (req.stage_id != 0) request.stageIds.push_back(req.stage_id);
|
||||
|
||||
proto::ReEnterLastElementStageScRsp rsp;
|
||||
@@ -350,6 +350,8 @@ void onPveBattleResult(net::Session& session, const proto::PVEBattleResultCsReq&
|
||||
// The monsters are gone from the scene by now, which is how the run tells whether
|
||||
// the node it was fighting is finished.
|
||||
challenge::battleFinished(session, *player, req);
|
||||
tierce::battleFinished(session, *player, req);
|
||||
peak::battleFinished(session, *player, req);
|
||||
|
||||
// The fight was simulated on the client; the server only confirms the outcome.
|
||||
proto::PVEBattleResultScRsp rsp;
|
||||
|
||||
+23
-15
@@ -81,15 +81,15 @@ proto::GetChallengeScRsp history() {
|
||||
return rsp;
|
||||
}
|
||||
|
||||
proto::LineupInfo lineup(const Player& player, uint32_t stage) {
|
||||
const std::vector<uint32_t>& party = player.challenge().party[half(stage)];
|
||||
proto::LineupInfo extraLineup(const Player& player, const std::vector<uint32_t>& party,
|
||||
proto::ExtraLineupType type) {
|
||||
Roster roster = player.roster();
|
||||
|
||||
proto::LineupInfo info;
|
||||
info.extra_lineup_type = lineupTypeOf(stage);
|
||||
info.extra_lineup_type = type;
|
||||
// An extra lineup is addressed as its type plus ten, which is how the client keeps
|
||||
// it out of the six ordinary squads.
|
||||
info.index = static_cast<uint32_t>(info.extra_lineup_type) + 10;
|
||||
info.index = static_cast<uint32_t>(type) + 10;
|
||||
info.max_mp = maxMp(party);
|
||||
info.mp = info.max_mp;
|
||||
info.leader_slot = 0;
|
||||
@@ -101,6 +101,10 @@ proto::LineupInfo lineup(const Player& player, uint32_t stage) {
|
||||
return info;
|
||||
}
|
||||
|
||||
proto::LineupInfo lineup(const Player& player, uint32_t stage) {
|
||||
return extraLineup(player, player.challenge().party[half(stage)], lineupTypeOf(stage));
|
||||
}
|
||||
|
||||
proto::CurChallenge current(const Player& player) {
|
||||
const ChallengeRun& run = player.challenge();
|
||||
const data::ChallengeInfo* config = configOf(player);
|
||||
@@ -124,9 +128,10 @@ proto::CurChallenge current(const Player& player) {
|
||||
const ChallengeArena* arena(const Player& player, ChallengeArena& storage) {
|
||||
const data::ChallengeInfo* config = configOf(player);
|
||||
if (config == nullptr) return nullptr;
|
||||
const data::ChallengeStage& stage = config->stages[half(player.challenge().stage)];
|
||||
storage.mazeGroupId = stage.mazeGroupId;
|
||||
storage.monsters = &stage.monsters;
|
||||
uint32_t stage = player.challenge().stage;
|
||||
storage.mazeGroupId = config->stages[half(stage)].mazeGroupId;
|
||||
storage.monsters = &config->stages[half(stage)].monsters;
|
||||
storage.party = &player.challenge().party[half(stage)];
|
||||
return &storage;
|
||||
}
|
||||
|
||||
@@ -288,14 +293,7 @@ bool nextPhase(net::Session& session, Player& player, proto::SceneInfo& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void leave(net::Session& session, Player& player) {
|
||||
ChallengeRun& run = player.challenge();
|
||||
if (!run.active) return;
|
||||
|
||||
uint32_t entryId = run.origin.entryId;
|
||||
Position at = run.originPos;
|
||||
run = ChallengeRun{};
|
||||
|
||||
void putBack(net::Session& session, Player& player, uint32_t entryId, const Position& at) {
|
||||
proto::SceneInfo scene;
|
||||
if (!scene::load(player, entryId, 0, true, scene)) {
|
||||
logging::warn("challenge", "cannot put the player back at entry {}", entryId);
|
||||
@@ -315,5 +313,15 @@ void leave(net::Session& session, Player& player) {
|
||||
player.saveNow();
|
||||
}
|
||||
|
||||
void leave(net::Session& session, Player& player) {
|
||||
ChallengeRun& run = player.challenge();
|
||||
if (!run.active) return;
|
||||
|
||||
uint32_t entryId = run.origin.entryId;
|
||||
Position at = run.originPos;
|
||||
run = ChallengeRun{};
|
||||
putBack(session, player, entryId, at);
|
||||
}
|
||||
|
||||
} // namespace challenge
|
||||
} // namespace game
|
||||
@@ -49,5 +49,11 @@ bool nextPhase(net::Session& session, Player& player, proto::SceneInfo& out);
|
||||
// Ends the run and puts the player back where they started.
|
||||
void leave(net::Session& session, Player& player);
|
||||
|
||||
// Shared with Anomaly Arbitration, which fights with the same kind of team and leaves
|
||||
// the same way.
|
||||
proto::LineupInfo extraLineup(const Player& player, const std::vector<uint32_t>& party,
|
||||
proto::ExtraLineupType type);
|
||||
void putBack(net::Session& session, Player& player, uint32_t entryId, const Position& at);
|
||||
|
||||
} // namespace challenge
|
||||
} // namespace game
|
||||
@@ -19,7 +19,10 @@ void registerLineupHandlers();
|
||||
void registerSceneHandlers();
|
||||
void registerBattleHandlers();
|
||||
void registerMissionHandlers();
|
||||
void registerTalkHandlers();
|
||||
void registerChallengeHandlers();
|
||||
void registerTierceHandlers();
|
||||
void registerPeakHandlers();
|
||||
void registerModuleHandlers();
|
||||
void registerMiscHandlers();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "net/handler.h"
|
||||
#include "net/session.h"
|
||||
#include "proto/gen/protos.h"
|
||||
#include "sdk/routes.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
@@ -123,16 +124,17 @@ void onGetActivityScheduleConfig(net::Session& session,
|
||||
session.send(cmd::GetActivityScheduleConfigScRsp, rsp);
|
||||
}
|
||||
|
||||
// A "CapySR" tag on the client's version label: Lua in ClientDownloadDataScNotify that
|
||||
// rewrites the VersionText object. Only the name -- the native watermark (gateway field
|
||||
// 1660) stays off, it prints account data.
|
||||
// A "CapySR" tag on the client's version label, with the client version after it: Lua in
|
||||
// ClientDownloadDataScNotify that rewrites the VersionText object. The native watermark
|
||||
// (gateway field 1660) stays off, it prints account data.
|
||||
constexpr std::string_view kWatermarkText = "CapySR";
|
||||
constexpr uint32_t kWatermarkFrom = 0xD08C4F; // caramel
|
||||
constexpr uint32_t kWatermarkTo = 0xF6DDA0; // sand
|
||||
|
||||
std::string watermarkLua() {
|
||||
// Caramel to sand across `text`, one colour tag per character.
|
||||
std::string gradient(std::string_view text) {
|
||||
std::string rich;
|
||||
size_t n = kWatermarkText.size();
|
||||
size_t n = text.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double t = n > 1 ? static_cast<double>(i) / static_cast<double>(n - 1) : 0.0;
|
||||
auto channel = [&](int shift) {
|
||||
@@ -144,9 +146,29 @@ std::string watermarkLua() {
|
||||
std::snprintf(tag, sizeof(tag), "<color=#%02X%02X%02X>", channel(16), channel(8),
|
||||
channel(0));
|
||||
rich += tag;
|
||||
rich += kWatermarkText[i];
|
||||
rich += text[i];
|
||||
rich += "</color>";
|
||||
}
|
||||
return rich;
|
||||
}
|
||||
|
||||
// The version comes off a query string, so only what a version is made of gets into
|
||||
// the Lua.
|
||||
std::string cleanVersion(std::string_view version) {
|
||||
std::string clean;
|
||||
for (char c : version) {
|
||||
bool keep = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
c == '.' || c == '_' || c == '-';
|
||||
if (keep) clean += c;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
std::string watermarkLua(std::string_view version) {
|
||||
// Name and version drawn alike, side by side.
|
||||
std::string rich = gradient(kWatermarkText);
|
||||
std::string clean = cleanVersion(version);
|
||||
if (!clean.empty()) rich += " " + gradient("|") + " " + gradient(clean);
|
||||
return "pcall(function()\n"
|
||||
" local go = CS.UnityEngine.GameObject.Find(\"VersionText\")\n"
|
||||
" if go == nil then return end\n"
|
||||
@@ -166,7 +188,7 @@ void sendWatermark(net::Session& session) {
|
||||
auto& download = notify.download_data.emplace();
|
||||
download.version = ++version;
|
||||
download.time = static_cast<int64_t>(util::nowMs() / 1000);
|
||||
download.data = watermarkLua();
|
||||
download.data = watermarkLua(sdk::lastClientVersion());
|
||||
session.send(cmd::ClientDownloadDataScNotify, notify);
|
||||
}
|
||||
|
||||
@@ -200,7 +222,10 @@ void registerAllHandlers() {
|
||||
registerSceneHandlers();
|
||||
registerBattleHandlers();
|
||||
registerMissionHandlers();
|
||||
registerTalkHandlers();
|
||||
registerChallengeHandlers();
|
||||
registerTierceHandlers();
|
||||
registerPeakHandlers();
|
||||
registerModuleHandlers();
|
||||
registerMiscHandlers();
|
||||
logging::info("game", "{} packet handlers registered, {} answered empty",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
// This is also what unlocks the world: the client gates map regions on finished main
|
||||
// missions.
|
||||
#include "core/config.h"
|
||||
#include "core/logger.h"
|
||||
#include "data/excel.h"
|
||||
#include "game/handlers.h"
|
||||
@@ -18,9 +19,20 @@
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
void onGetMissionStatus(net::Session& session, const proto::GetMissionStatusCsReq& req) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
// Everything finished, bar the handful config names -- see gameplay.skip_missions.
|
||||
std::vector<uint32_t> finishedMainMissions() {
|
||||
const core::GameplayConfig& gameplay = core::Config::get().gameplay;
|
||||
const std::vector<uint32_t>& all = data::Tables::get().mainMissions();
|
||||
if (gameplay.skipMissions.empty()) return all;
|
||||
std::vector<uint32_t> out;
|
||||
out.reserve(all.size());
|
||||
for (uint32_t id : all) {
|
||||
if (!gameplay.missionSkipped(id)) out.push_back(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void onGetMissionStatus(net::Session& session, const proto::GetMissionStatusCsReq& req) {
|
||||
proto::GetMissionStatusScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
// Whatever it asked about is finished. There are ~15k sub-missions in the tables,
|
||||
@@ -32,9 +44,9 @@ void onGetMissionStatus(net::Session& session, const proto::GetMissionStatusCsRe
|
||||
mission.progress = 1;
|
||||
rsp.sub_mission_status_list.push_back(mission);
|
||||
}
|
||||
rsp.finished_main_mission_id_list = tables.mainMissions();
|
||||
rsp.finished_main_mission_id_list = finishedMainMissions();
|
||||
// The client cross-checks this against the version it is running.
|
||||
rsp.curversion_finished_main_mission_id_list = tables.mainMissions();
|
||||
rsp.curversion_finished_main_mission_id_list = rsp.finished_main_mission_id_list;
|
||||
session.send(cmd::GetMissionStatusScRsp, rsp);
|
||||
}
|
||||
|
||||
@@ -43,7 +55,7 @@ void onGetMissionStatus(net::Session& session, const proto::GetMissionStatusCsRe
|
||||
void onGetMissionData(net::Session& session, const proto::GetMissionDataCsReq&) {
|
||||
proto::GetMissionDataScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.finished_main_mission_id_list = data::Tables::get().mainMissions();
|
||||
rsp.finished_main_mission_id_list = finishedMainMissions();
|
||||
session.send(cmd::GetMissionDataScRsp, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
#include "game/peak.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "core/config.h"
|
||||
#include "core/logger.h"
|
||||
#include "data/excel.h"
|
||||
#include "data/scene_res.h"
|
||||
#include "game/battle.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/player.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/session.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
// Season one's arena. A season newer than the scene dump fights here instead: the
|
||||
// floor is only a backdrop for one monster, and the stage comes from the fight.
|
||||
constexpr uint32_t kFallbackEntrance = 3013501;
|
||||
constexpr uint32_t kFallbackMazeGroup = 8;
|
||||
|
||||
// The battle target slot a fight's own targets go in.
|
||||
constexpr uint32_t kTargetSlot = 5;
|
||||
|
||||
bool cleared() { return core::Config::get().gameplay.unlockAllChallenges; }
|
||||
|
||||
const data::PeakInfo* configOf(const Player& player) {
|
||||
const PeakRun& run = player.peak();
|
||||
if (!run.active) return nullptr;
|
||||
return data::Tables::get().peak(run.peakId);
|
||||
}
|
||||
|
||||
// Hard mode judges the boss by one target of its own.
|
||||
std::vector<uint32_t> targetsOf(const data::PeakInfo& config, bool hard) {
|
||||
if (hard && config.hardTarget != 0) return {config.hardTarget};
|
||||
return config.targetIds;
|
||||
}
|
||||
|
||||
// The most cycles a clear can take and still meet a target.
|
||||
uint32_t turnBudget(const std::vector<uint32_t>& targets) {
|
||||
uint32_t budget = 0;
|
||||
for (uint32_t id : targets) {
|
||||
const data::BattleTargetInfo* target = data::Tables::get().battleTarget(id);
|
||||
if (target != nullptr && !target->countsDeaths) budget = std::max(budget, target->param);
|
||||
}
|
||||
return budget;
|
||||
}
|
||||
|
||||
const std::vector<uint32_t>& teamOf(const PeakProgress& progress, uint32_t peakId) {
|
||||
static const std::vector<uint32_t> kNone;
|
||||
auto it = progress.teams.find(peakId);
|
||||
return it == progress.teams.end() ? kNone : it->second;
|
||||
}
|
||||
|
||||
// The boss buff last picked, or the first on offer so the slot is never empty.
|
||||
uint32_t bossBuffOf(const PeakProgress& progress, const data::PeakInfo& boss) {
|
||||
auto it = progress.bossBuffs.find(boss.id);
|
||||
if (it != progress.bossBuffs.end() && it->second != 0) return it->second;
|
||||
return boss.bossBuffs.empty() ? 0 : boss.bossBuffs.front();
|
||||
}
|
||||
|
||||
std::vector<proto::ChallengePeakRecordAvatar> recordAvatars(const std::vector<uint32_t>& team) {
|
||||
std::vector<proto::ChallengePeakRecordAvatar> out;
|
||||
for (uint32_t avatarId : team) {
|
||||
proto::ChallengePeakRecordAvatar entry;
|
||||
entry.avatar_id = avatarId;
|
||||
out.push_back(entry);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
proto::ChallengePeakBossClearance clearance(const PeakProgress& progress,
|
||||
const data::PeakInfo& boss, bool hard) {
|
||||
proto::ChallengePeakBossClearance out;
|
||||
const PeakRecord* record = progress.record(boss.id, hard);
|
||||
out.has_passed = record != nullptr || cleared();
|
||||
// Both difficulties show the same team and buff; the client fills its slots from them.
|
||||
out.peak_avatar_id_list = teamOf(progress, boss.id);
|
||||
out.buff_id = bossBuffOf(progress, boss);
|
||||
if (record != nullptr) {
|
||||
uint32_t budget = turnBudget(targetsOf(boss, hard));
|
||||
out.best_cycle_count = record->cycles;
|
||||
out.best_record_buff_id = record->buffId;
|
||||
out.max_left_turn = budget > record->cycles ? budget - record->cycles : 0;
|
||||
out.best_record_avatar_list = recordAvatars(record->team);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool enterArena(Player& player, proto::SceneInfo& out) {
|
||||
const data::PeakInfo* config = configOf(player);
|
||||
if (config == nullptr) return false;
|
||||
|
||||
PeakRun& run = player.peak();
|
||||
run.entryId = config->mapEntranceId;
|
||||
run.mazeGroupId = config->mazeGroupId;
|
||||
if (run.entryId == 0 || data::SceneRes::get().byEntry(run.entryId) == nullptr) {
|
||||
logging::info("peak", "fight {}: arena {} is not in the scene dump, using {}",
|
||||
config->id, run.entryId, kFallbackEntrance);
|
||||
run.entryId = kFallbackEntrance;
|
||||
run.mazeGroupId = kFallbackMazeGroup;
|
||||
}
|
||||
|
||||
// Every attempt starts at the entrance, not wherever the last fight ended.
|
||||
SceneLocation before = player.location();
|
||||
player.location().floorId = 0;
|
||||
ChallengeArena storage;
|
||||
if (scene::load(player, run.entryId, 0, true, out, peak::arena(player, storage))) return true;
|
||||
player.location() = before;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace peak {
|
||||
|
||||
proto::ChallengePeakGroup group(const Player& player, uint32_t groupId) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
const PeakProgress& progress = player.peakProgress();
|
||||
|
||||
proto::ChallengePeakGroup out;
|
||||
out.peak_group_id = groupId;
|
||||
// Read the other way round by the client: set means the boss is on normal.
|
||||
out.disable_hard_mode = progress.hardGroups.count(groupId) == 0;
|
||||
|
||||
const data::PeakGroupInfo* info = tables.peakGroup(groupId);
|
||||
if (info == nullptr) return out;
|
||||
|
||||
uint32_t stars = 0;
|
||||
proto::ChallengePeakBestRecordList best;
|
||||
for (uint32_t mobId : info->mobIds) {
|
||||
const data::PeakInfo* mob = tables.peak(mobId);
|
||||
if (mob == nullptr) continue;
|
||||
const PeakRecord* record = progress.record(mobId, false);
|
||||
|
||||
proto::ChallengePeak entry;
|
||||
entry.peak_id = mobId;
|
||||
entry.has_passed = record != nullptr || cleared();
|
||||
entry.peak_avatar_id_list = teamOf(progress, mobId);
|
||||
if (record != nullptr) {
|
||||
entry.finished_target_list = record->targets;
|
||||
entry.cycles_used = record->cycles;
|
||||
} else if (cleared()) {
|
||||
entry.finished_target_list = mob->targetIds;
|
||||
}
|
||||
stars += static_cast<uint32_t>(entry.finished_target_list.size());
|
||||
|
||||
if (entry.has_passed) {
|
||||
proto::ChallengePeakBestRecord clear;
|
||||
clear.peak_id = mobId;
|
||||
clear.cycles_used = entry.cycles_used;
|
||||
clear.finished_target_list = entry.finished_target_list;
|
||||
clear.peak_avatar_id_list =
|
||||
recordAvatars(record != nullptr ? record->team : entry.peak_avatar_id_list);
|
||||
best.peaks.push_back(std::move(clear));
|
||||
}
|
||||
out.peaks.push_back(std::move(entry));
|
||||
}
|
||||
out.count_of_peaks = static_cast<uint32_t>(out.peaks.size());
|
||||
// A full clear is three stars a knight, whatever the records say.
|
||||
out.obtained_stars = cleared() ? std::max(stars, out.count_of_peaks * 3) : stars;
|
||||
if (!best.peaks.empty()) out.best_record_list = std::move(best);
|
||||
if (cleared()) {
|
||||
// Reported as taken: there is no inventory to pay them into.
|
||||
if (const std::vector<uint32_t>* rewards = tables.peakRewards(info->rewardGroupId)) {
|
||||
out.taken_star_rewards = *rewards;
|
||||
}
|
||||
}
|
||||
|
||||
const data::PeakInfo* boss = tables.peak(info->bossId);
|
||||
if (boss == nullptr) return out;
|
||||
const PeakRecord* easy = progress.record(boss->id, false);
|
||||
auto& bossInfo = out.peak_boss.emplace();
|
||||
bossInfo.hard_mode_has_passed = progress.record(boss->id, true) != nullptr || cleared();
|
||||
bossInfo.easy_mode = clearance(progress, *boss, false);
|
||||
bossInfo.hard_mode = clearance(progress, *boss, true);
|
||||
if (easy != nullptr) {
|
||||
bossInfo.finished_target_list = easy->targets;
|
||||
} else if (cleared()) {
|
||||
bossInfo.finished_target_list = boss->targetIds;
|
||||
if (boss->hardTarget != 0) bossInfo.finished_target_list.push_back(boss->hardTarget);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
proto::GetChallengePeakDataScRsp overview(const Player& player) {
|
||||
proto::GetChallengePeakDataScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
for (const data::PeakGroupInfo& info : data::Tables::get().peakGroups()) {
|
||||
rsp.challenge_peak_groups.push_back(group(player, info.id));
|
||||
// The newest season is the one on show.
|
||||
rsp.current_peak_group_id = info.id;
|
||||
}
|
||||
return rsp;
|
||||
}
|
||||
|
||||
void pushGroup(net::Session& session, const Player& player, uint32_t groupId) {
|
||||
proto::ChallengePeakGroupDataUpdateScNotify notify;
|
||||
notify.challenge_peak_group = group(player, groupId);
|
||||
session.send(cmd::ChallengePeakGroupDataUpdateScNotify, notify);
|
||||
}
|
||||
|
||||
uint32_t start(Player& player, uint32_t peakId, uint32_t buffId, std::vector<uint32_t> team,
|
||||
proto::SceneInfo& out) {
|
||||
const data::PeakInfo* config = data::Tables::get().peak(peakId);
|
||||
if (config == nullptr || config->monsters.empty()) {
|
||||
return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
}
|
||||
|
||||
PeakProgress& progress = player.peakProgress();
|
||||
if (team.empty()) team = teamOf(progress, peakId);
|
||||
if (team.empty()) return fail(proto::Retcode::RET_CHALLENGE_LINEUP_EMPTY);
|
||||
if (config->boss && buffId == 0) buffId = bossBuffOf(progress, *config);
|
||||
progress.teams[peakId] = team;
|
||||
if (config->boss) progress.bossBuffs[peakId] = buffId;
|
||||
|
||||
PeakRun run;
|
||||
run.active = true;
|
||||
run.peakId = peakId;
|
||||
run.groupId = config->groupId;
|
||||
run.hard = config->boss && progress.hardGroups.count(config->groupId) != 0;
|
||||
run.buffId = config->boss ? buffId : 0;
|
||||
run.party = std::move(team);
|
||||
// A retry keeps the spot the first attempt left from.
|
||||
const PeakRun& previous = player.peak();
|
||||
run.origin = previous.active ? previous.origin : player.location();
|
||||
run.originPos = previous.active ? previous.originPos : player.position();
|
||||
|
||||
PeakRun saved = player.peak();
|
||||
player.peak() = std::move(run);
|
||||
if (!enterArena(player, out)) {
|
||||
player.peak() = std::move(saved);
|
||||
return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
}
|
||||
|
||||
logging::info("peak", "fight {} of season {} started{}, stage {}", peakId, config->groupId,
|
||||
player.peak().hard ? " on hard" : "",
|
||||
player.peak().hard && config->hardEventId != 0 ? config->hardEventId
|
||||
: config->eventId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
proto::LineupInfo lineup(const Player& player) {
|
||||
return challenge::extraLineup(player, player.peak().party,
|
||||
proto::ExtraLineupType::ExtraLineupType_LineupChallenge);
|
||||
}
|
||||
|
||||
const ChallengeArena* arena(const Player& player, ChallengeArena& storage) {
|
||||
const data::PeakInfo* config = configOf(player);
|
||||
if (config == nullptr) return nullptr;
|
||||
const PeakRun& run = player.peak();
|
||||
storage.mazeGroupId = run.mazeGroupId;
|
||||
storage.monsters = run.hard && !config->hardMonsters.empty() ? &config->hardMonsters
|
||||
: &config->monsters;
|
||||
storage.party = &run.party;
|
||||
return &storage;
|
||||
}
|
||||
|
||||
void prepareBattle(const Player& player, BattleRequest& request) {
|
||||
const data::PeakInfo* config = configOf(player);
|
||||
if (config == nullptr) return;
|
||||
const PeakRun& run = player.peak();
|
||||
|
||||
request.party = run.party;
|
||||
// The enemy tags are not sent back by the client; they come from the fight's config.
|
||||
request.floorBuffIds = run.hard ? config->hardTagBuffs : config->tagBuffs;
|
||||
if (run.buffId != 0) request.floorBuffIds.push_back(run.buffId);
|
||||
request.battleType = "AA";
|
||||
request.battleTargetIds = targetsOf(*config, run.hard);
|
||||
}
|
||||
|
||||
bool battleFinished(net::Session& session, Player& player,
|
||||
const proto::PVEBattleResultCsReq& req) {
|
||||
const data::PeakInfo* config = configOf(player);
|
||||
if (config == nullptr) return false;
|
||||
const PeakRun& run = player.peak();
|
||||
|
||||
// Backing out of the fight is not losing it; the client stays in the arena.
|
||||
if (req.end_status == proto::BattleEndStatus::BATTLE_END_QUIT) return true;
|
||||
|
||||
proto::ChallengePeakSettleScNotify notify;
|
||||
notify.peak_id = run.peakId;
|
||||
notify.is_win = req.end_status == proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
if (!notify.is_win) {
|
||||
session.send(cmd::ChallengePeakSettleScNotify, notify);
|
||||
logging::info("peak", "fight {} lost", run.peakId);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t cycles = req.stt ? req.stt->round_cnt : 0;
|
||||
uint32_t deaths = 0;
|
||||
const proto::BattleTargetList* reported = nullptr;
|
||||
if (req.stt) {
|
||||
for (const proto::AvatarBattleInfo& avatar : req.stt->battle_avatar_list) {
|
||||
if (avatar.avatar_status && avatar.avatar_status->left_hp == 0) ++deaths;
|
||||
}
|
||||
auto it = req.stt->battle_target_info.find(kTargetSlot);
|
||||
if (it != req.stt->battle_target_info.end()) reported = &it->second;
|
||||
}
|
||||
|
||||
// The client's own count wins; without one, judge by cycles used and avatars lost.
|
||||
std::vector<uint32_t> targets = targetsOf(*config, run.hard);
|
||||
std::vector<uint32_t> finished;
|
||||
for (uint32_t id : targets) {
|
||||
const data::BattleTargetInfo* target = data::Tables::get().battleTarget(id);
|
||||
if (target == nullptr) continue;
|
||||
uint32_t progress = target->countsDeaths ? deaths : cycles;
|
||||
if (reported != nullptr) {
|
||||
for (const proto::BattleTarget& entry : reported->battle_target_list) {
|
||||
if (entry.id == id) progress = entry.progress;
|
||||
}
|
||||
}
|
||||
if (progress <= target->param) finished.push_back(id);
|
||||
}
|
||||
|
||||
PeakProgress& progress = player.peakProgress();
|
||||
notify.cycles_used = cycles;
|
||||
notify.is_first_pass = progress.record(run.peakId, run.hard) == nullptr;
|
||||
if (config->boss && run.hard) {
|
||||
// A hard clear has a result screen of its own, with no targets on it.
|
||||
notify.hard_mode_has_passed = true;
|
||||
} else {
|
||||
notify.finished_target_list = finished;
|
||||
if (config->boss) {
|
||||
uint32_t budget = turnBudget(config->targetIds);
|
||||
notify.turn_left = budget > cycles ? budget - cycles : 0;
|
||||
}
|
||||
}
|
||||
session.send(cmd::ChallengePeakSettleScNotify, notify);
|
||||
|
||||
// Keep the better clear: more targets, then fewer cycles.
|
||||
uint32_t key = PeakProgress::key(run.peakId, run.hard);
|
||||
auto it = progress.records.find(key);
|
||||
if (it == progress.records.end() || finished.size() > it->second.targets.size() ||
|
||||
(finished.size() == it->second.targets.size() && cycles < it->second.cycles)) {
|
||||
progress.records[key] = PeakRecord{cycles, finished, run.party, run.buffId};
|
||||
}
|
||||
pushGroup(session, player, run.groupId);
|
||||
|
||||
logging::info("peak", "fight {} cleared{} in {} cycle(s), {} of {} target(s)", run.peakId,
|
||||
run.hard ? " on hard" : "", cycles, finished.size(), targets.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void leave(net::Session& session, Player& player) {
|
||||
PeakRun& run = player.peak();
|
||||
if (!run.active) return;
|
||||
|
||||
uint32_t entryId = run.origin.entryId;
|
||||
Position at = run.originPos;
|
||||
run = PeakRun{};
|
||||
challenge::putBack(session, player, entryId, at);
|
||||
}
|
||||
|
||||
} // namespace peak
|
||||
} // namespace game
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "game/scene.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
namespace net {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace game {
|
||||
|
||||
class Player;
|
||||
struct BattleRequest;
|
||||
|
||||
// Anomaly Arbitration: three knights and a boss a season, each one fight with its own
|
||||
// team. Hard mode is a per-season toggle set before the boss is started; it swaps in
|
||||
// the boss's hard stage, tags and target.
|
||||
namespace peak {
|
||||
|
||||
// Every season, cleared when gameplay.unlock_all_challenges is set.
|
||||
proto::GetChallengePeakDataScRsp overview(const Player& player);
|
||||
// One season as the overview draws it.
|
||||
proto::ChallengePeakGroup group(const Player& player, uint32_t groupId);
|
||||
// Re-sends one season, after anything that changes what the overview shows.
|
||||
void pushGroup(net::Session& session, const Player& player, uint32_t groupId);
|
||||
|
||||
// Starts a fight and enters its arena. Returns the retcode; on success `out` holds the
|
||||
// arena. An empty team or a zero buff reuses the last ones picked for that fight.
|
||||
uint32_t start(Player& player, uint32_t peakId, uint32_t buffId, std::vector<uint32_t> team,
|
||||
proto::SceneInfo& out);
|
||||
|
||||
// The run's team, as the extra lineup the arena is fought with.
|
||||
proto::LineupInfo lineup(const Player& player);
|
||||
|
||||
// Same as challenge::arena, for a fight in progress.
|
||||
const ChallengeArena* arena(const Player& player, ChallengeArena& storage);
|
||||
|
||||
// What a fight inside the run brings: its team, the enemy tags, the boss buff, targets.
|
||||
void prepareBattle(const Player& player, BattleRequest& request);
|
||||
|
||||
// The fight has ended. Returns false when no run is going.
|
||||
bool battleFinished(net::Session& session, Player& player,
|
||||
const proto::PVEBattleResultCsReq& req);
|
||||
|
||||
// Ends the run and puts the player back where they started.
|
||||
void leave(net::Session& session, Player& player);
|
||||
|
||||
} // namespace peak
|
||||
} // namespace game
|
||||
@@ -0,0 +1,193 @@
|
||||
// Anomaly Arbitration.
|
||||
//
|
||||
// The overview is one GetChallengePeakData: every season, its three knights and its
|
||||
// boss, with the last team used on each. Starting a fight answers with a bare retcode --
|
||||
// the arena has to go out first, as EnterSceneByServerScNotify, or the client is left
|
||||
// on a black screen. The result is a settle notify off the battle result, and a season
|
||||
// is sent again after anything that changes it.
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/logger.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/peak.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;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> nonZero(const std::vector<uint32_t>& ids) {
|
||||
std::vector<uint32_t> out;
|
||||
for (uint32_t id : ids) {
|
||||
if (id != 0) out.push_back(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The arena, then the team fighting in it, then the season with that team in its slot.
|
||||
void pushArena(net::Session& session, Player& player, proto::SceneInfo& scene) {
|
||||
proto::SyncLineupNotify sync;
|
||||
sync.lineup = peak::lineup(player);
|
||||
session.send(cmd::SyncLineupNotify, sync);
|
||||
|
||||
proto::EnterSceneByServerScNotify notify;
|
||||
notify.scene = std::move(scene);
|
||||
notify.lineup = peak::lineup(player);
|
||||
session.send(cmd::EnterSceneByServerScNotify, notify);
|
||||
|
||||
peak::pushGroup(session, player, player.peak().groupId);
|
||||
}
|
||||
|
||||
void onGetChallengePeakData(net::Session& session, const proto::GetChallengePeakDataCsReq&) {
|
||||
Player* player = playerOf(session, "GetChallengePeakData");
|
||||
if (player == nullptr) return;
|
||||
session.send(cmd::GetChallengePeakDataScRsp, peak::overview(*player));
|
||||
}
|
||||
|
||||
void onGetCurChallengePeak(net::Session& session, const proto::GetCurChallengePeakCsReq&) {
|
||||
Player* player = playerOf(session, "GetCurChallengePeak");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::GetCurChallengePeakScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
const PeakRun& run = player->peak();
|
||||
if (run.active) {
|
||||
rsp.peak_id = run.peakId;
|
||||
rsp.boss_buff_id = run.buffId;
|
||||
rsp.has_passed = true;
|
||||
}
|
||||
session.send(cmd::GetCurChallengePeakScRsp, rsp);
|
||||
}
|
||||
|
||||
void onSetChallengePeakMobLineupAvatar(net::Session& session,
|
||||
const proto::SetChallengePeakMobLineupAvatarCsReq& req) {
|
||||
Player* player = playerOf(session, "SetChallengePeakMobLineupAvatar");
|
||||
if (player == nullptr) return;
|
||||
|
||||
PeakProgress& progress = player->peakProgress();
|
||||
for (const proto::ChallengePeakLineup& lineup : req.lineup_list) {
|
||||
if (lineup.peak_id != 0) progress.teams[lineup.peak_id] = nonZero(lineup.peak_avatar_id_list);
|
||||
}
|
||||
player->saveNow();
|
||||
peak::pushGroup(session, *player, req.peak_group_id);
|
||||
|
||||
proto::SetChallengePeakMobLineupAvatarScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::SetChallengePeakMobLineupAvatarScRsp, rsp);
|
||||
}
|
||||
|
||||
void onSetChallengePeakBossHardMode(net::Session& session,
|
||||
const proto::SetChallengePeakBossHardModeCsReq& req) {
|
||||
Player* player = playerOf(session, "SetChallengePeakBossHardMode");
|
||||
if (player == nullptr) return;
|
||||
|
||||
// Kept per season and across restarts: the toggle is set before the boss starts.
|
||||
std::set<uint32_t>& hard = player->peakProgress().hardGroups;
|
||||
if (req.is_hard_mode) {
|
||||
hard.insert(req.peak_group_id);
|
||||
} else {
|
||||
hard.erase(req.peak_group_id);
|
||||
}
|
||||
player->saveNow();
|
||||
|
||||
proto::SetChallengePeakBossHardModeScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.peak_group_id = req.peak_group_id;
|
||||
rsp.is_hard_mode = req.is_hard_mode;
|
||||
session.send(cmd::SetChallengePeakBossHardModeScRsp, rsp);
|
||||
}
|
||||
|
||||
void onStartChallengePeak(net::Session& session, const proto::StartChallengePeakCsReq& req) {
|
||||
Player* player = playerOf(session, "StartChallengePeak");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::SceneInfo scene;
|
||||
proto::StartChallengePeakScRsp rsp;
|
||||
rsp.retcode = peak::start(*player, req.peak_id, req.boss_buff_id,
|
||||
nonZero(req.peak_avatar_id_list), scene);
|
||||
if (rsp.retcode == 0) pushArena(session, *player, scene);
|
||||
session.send(cmd::StartChallengePeakScRsp, rsp);
|
||||
}
|
||||
|
||||
void onReStartChallengePeak(net::Session& session, const proto::ReStartChallengePeakCsReq&) {
|
||||
Player* player = playerOf(session, "ReStartChallengePeak");
|
||||
if (player == nullptr) return;
|
||||
|
||||
// The result screen's retry: the same fight, team and buff, from the entrance.
|
||||
proto::ReStartChallengePeakScRsp rsp;
|
||||
const PeakRun& run = player->peak();
|
||||
if (!run.active) {
|
||||
rsp.retcode = fail(proto::Retcode::RET_CHALLENGE_NOT_DOING);
|
||||
} else {
|
||||
proto::SceneInfo scene;
|
||||
rsp.retcode = peak::start(*player, run.peakId, run.buffId, run.party, scene);
|
||||
if (rsp.retcode == 0) pushArena(session, *player, scene);
|
||||
}
|
||||
session.send(cmd::ReStartChallengePeakScRsp, rsp);
|
||||
}
|
||||
|
||||
void onLeaveChallengePeak(net::Session& session, const proto::LeaveChallengePeakCsReq&) {
|
||||
Player* player = playerOf(session, "LeaveChallengePeak");
|
||||
if (player == nullptr) return;
|
||||
|
||||
bool wasRunning = player->peak().active;
|
||||
uint32_t groupId = player->peak().groupId;
|
||||
peak::leave(session, *player);
|
||||
|
||||
proto::LeaveChallengePeakScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::LeaveChallengePeakScRsp, rsp);
|
||||
|
||||
// Back on the overview, which refills its team slots from the season.
|
||||
if (wasRunning) peak::pushGroup(session, *player, groupId);
|
||||
}
|
||||
|
||||
void onConfirmChallengePeakSettle(net::Session& session,
|
||||
const proto::ConfirmChallengePeakSettleCsReq& req) {
|
||||
proto::ConfirmChallengePeakSettleScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.peak_id = req.peak_id;
|
||||
session.send(cmd::ConfirmChallengePeakSettleScRsp, rsp);
|
||||
}
|
||||
|
||||
void onTakeChallengePeakReward(net::Session& session,
|
||||
const proto::TakeChallengePeakRewardCsReq& req) {
|
||||
// A cleared season already reports every reward as taken, but the request still has
|
||||
// to complete or the screen hangs.
|
||||
proto::TakeChallengePeakRewardScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.peak_group_id = req.peak_group_id;
|
||||
session.send(cmd::TakeChallengePeakRewardScRsp, rsp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerPeakHandlers() {
|
||||
net::on<proto::GetChallengePeakDataCsReq>(cmd::GetChallengePeakDataCsReq,
|
||||
onGetChallengePeakData);
|
||||
net::on<proto::GetCurChallengePeakCsReq>(cmd::GetCurChallengePeakCsReq, onGetCurChallengePeak);
|
||||
net::on<proto::SetChallengePeakMobLineupAvatarCsReq>(cmd::SetChallengePeakMobLineupAvatarCsReq,
|
||||
onSetChallengePeakMobLineupAvatar);
|
||||
net::on<proto::SetChallengePeakBossHardModeCsReq>(cmd::SetChallengePeakBossHardModeCsReq,
|
||||
onSetChallengePeakBossHardMode);
|
||||
net::on<proto::StartChallengePeakCsReq>(cmd::StartChallengePeakCsReq, onStartChallengePeak);
|
||||
net::on<proto::ReStartChallengePeakCsReq>(cmd::ReStartChallengePeakCsReq,
|
||||
onReStartChallengePeak);
|
||||
net::on<proto::LeaveChallengePeakCsReq>(cmd::LeaveChallengePeakCsReq, onLeaveChallengePeak);
|
||||
net::on<proto::ConfirmChallengePeakSettleCsReq>(cmd::ConfirmChallengePeakSettleCsReq,
|
||||
onConfirmChallengePeakSettle);
|
||||
net::on<proto::TakeChallengePeakRewardCsReq>(cmd::TakeChallengePeakRewardCsReq,
|
||||
onTakeChallengePeakReward);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -47,6 +49,81 @@ struct ChallengeRun {
|
||||
const std::vector<uint32_t>& curParty() const { return party[stage > 1 ? 1 : 0]; }
|
||||
};
|
||||
|
||||
// An Anomaly Arbitration fight in progress: one team, one monster, no second node.
|
||||
struct PeakRun {
|
||||
bool active = false;
|
||||
uint32_t peakId = 0;
|
||||
uint32_t groupId = 0;
|
||||
bool hard = false;
|
||||
uint32_t buffId = 0; // the boss buff the player picked
|
||||
std::vector<uint32_t> party;
|
||||
// The arena actually entered; a season newer than the scene dump borrows another.
|
||||
uint32_t entryId = 0;
|
||||
uint32_t mazeGroupId = 0;
|
||||
SceneLocation origin;
|
||||
Position originPos;
|
||||
};
|
||||
|
||||
// The three-node run 4.6 replaced Memory of Chaos's two with. Each node brings its own
|
||||
// team and its own buff, and the cycle pool is spent across all three.
|
||||
struct TierceRun {
|
||||
bool active = false;
|
||||
uint32_t tierceId = 0;
|
||||
uint32_t stage = 0; // 0..2
|
||||
// Replaying one cleared node rather than walking the whole floor.
|
||||
bool singleStage = false;
|
||||
bool passed = false;
|
||||
uint32_t roundsLeft = 0;
|
||||
uint32_t deaths[3] = {0, 0, 0};
|
||||
uint32_t cycles[3] = {0, 0, 0};
|
||||
uint32_t scores[3] = {0, 0, 0};
|
||||
// proto::BattleEndStatus per node, as the result list reports it.
|
||||
uint32_t endStatus[3] = {0, 0, 0};
|
||||
uint32_t buffs[3] = {0, 0, 0};
|
||||
std::vector<uint32_t> party[3];
|
||||
SceneLocation origin;
|
||||
Position originPos;
|
||||
|
||||
uint32_t totalScore() const { return scores[0] + scores[1] + scores[2]; }
|
||||
const std::vector<uint32_t>& curParty() const { return party[stage < 3 ? stage : 2]; }
|
||||
};
|
||||
|
||||
// What the three-node history remembers between runs: the teams and buffs the player
|
||||
// last set per floor, and how far they got.
|
||||
struct TierceProgress {
|
||||
std::vector<uint32_t> party[3];
|
||||
uint32_t buffs[3] = {0, 0, 0};
|
||||
uint32_t scores[3] = {0, 0, 0};
|
||||
uint32_t cycles[3] = {0, 0, 0};
|
||||
uint32_t deaths[3] = {0, 0, 0};
|
||||
bool cleared[3] = {false, false, false};
|
||||
bool passed = false;
|
||||
std::vector<uint32_t> targets;
|
||||
};
|
||||
|
||||
// A cleared Anomaly Arbitration fight, as the overview shows it.
|
||||
struct PeakRecord {
|
||||
uint32_t cycles = 0;
|
||||
std::vector<uint32_t> targets;
|
||||
std::vector<uint32_t> team;
|
||||
uint32_t buffId = 0;
|
||||
};
|
||||
|
||||
// What the Anomaly Arbitration overview remembers between fights and restarts: the
|
||||
// last team and boss buff per fight, the seasons on hard, and the best clears.
|
||||
struct PeakProgress {
|
||||
std::map<uint32_t, std::vector<uint32_t>> teams;
|
||||
std::map<uint32_t, uint32_t> bossBuffs;
|
||||
std::set<uint32_t> hardGroups;
|
||||
std::map<uint32_t, PeakRecord> records; // by key()
|
||||
|
||||
static uint32_t key(uint32_t peakId, bool hard) { return peakId * 2 + (hard ? 1 : 0); }
|
||||
const PeakRecord* record(uint32_t peakId, bool hard) const {
|
||||
auto it = records.find(key(peakId, hard));
|
||||
return it == records.end() ? nullptr : &it->second;
|
||||
}
|
||||
};
|
||||
|
||||
// One logged-in account: identity, party, position and the current fight.
|
||||
class Player {
|
||||
public:
|
||||
@@ -83,6 +160,14 @@ public:
|
||||
const BattleContext& battle() const { return battle_; }
|
||||
ChallengeRun& challenge() { return challenge_; }
|
||||
const ChallengeRun& challenge() const { return challenge_; }
|
||||
PeakRun& peak() { return peak_; }
|
||||
const PeakRun& peak() const { return peak_; }
|
||||
TierceRun& tierce() { return tierce_; }
|
||||
const TierceRun& tierce() const { return tierce_; }
|
||||
std::map<uint32_t, TierceProgress>& tierceHistory() { return tierceHistory_; }
|
||||
const std::map<uint32_t, TierceProgress>& tierceHistory() const { return tierceHistory_; }
|
||||
PeakProgress& peakProgress() { return peakProgress_; }
|
||||
const PeakProgress& peakProgress() const { return peakProgress_; }
|
||||
|
||||
uint32_t mainCharacter() const { return mainCharacter_; }
|
||||
void setMainCharacter(uint32_t value) { mainCharacter_ = value; }
|
||||
@@ -120,6 +205,10 @@ private:
|
||||
SceneState sceneState_;
|
||||
BattleContext battle_;
|
||||
ChallengeRun challenge_;
|
||||
PeakRun peak_;
|
||||
PeakProgress peakProgress_;
|
||||
TierceRun tierce_;
|
||||
std::map<uint32_t, TierceProgress> tierceHistory_;
|
||||
|
||||
uint32_t mainCharacter_ = 8008;
|
||||
uint32_t marchType_ = 1224;
|
||||
|
||||
@@ -27,6 +27,16 @@ bool boolOr(const json& j, const char* key, bool fallback) {
|
||||
return (it != j.end() && it->is_boolean()) ? it->get<bool>() : fallback;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> u32Array(const json& j, const char* key) {
|
||||
std::vector<uint32_t> out;
|
||||
auto it = j.find(key);
|
||||
if (it == j.end() || !it->is_array()) return out;
|
||||
for (const json& entry : *it) {
|
||||
if (entry.is_number()) out.push_back(entry.get<uint32_t>());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool loadPlayerState(Player& player) {
|
||||
@@ -104,6 +114,63 @@ bool loadPlayerState(Player& player) {
|
||||
}
|
||||
book.setCurIndex(u32(j, "cur_squad", book.curIndex()));
|
||||
book.setMp(u32(j, "mp", book.maxMp()));
|
||||
|
||||
if (auto peak = j.find("peak"); peak != j.end() && peak->is_object()) {
|
||||
PeakProgress& progress = player.peakProgress();
|
||||
auto ids = [](const json& entry, const char* key) {
|
||||
std::vector<uint32_t> out;
|
||||
auto it = entry.find(key);
|
||||
if (it == entry.end() || !it->is_array()) return out;
|
||||
for (const json& id : *it) {
|
||||
if (id.is_number()) out.push_back(id.get<uint32_t>());
|
||||
}
|
||||
return out;
|
||||
};
|
||||
for (uint32_t groupId : ids(*peak, "hard")) progress.hardGroups.insert(groupId);
|
||||
if (auto fights = peak->find("fights"); fights != peak->end() && fights->is_array()) {
|
||||
for (const json& fight : *fights) {
|
||||
uint32_t id = fight.is_object() ? u32(fight, "id", 0) : 0;
|
||||
if (id == 0) continue;
|
||||
progress.teams[id] = ids(fight, "team");
|
||||
if (uint32_t buff = u32(fight, "buff", 0); buff != 0) progress.bossBuffs[id] = buff;
|
||||
}
|
||||
}
|
||||
if (auto records = peak->find("records"); records != peak->end() && records->is_array()) {
|
||||
for (const json& entry : *records) {
|
||||
uint32_t id = entry.is_object() ? u32(entry, "id", 0) : 0;
|
||||
if (id == 0) continue;
|
||||
PeakRecord& record =
|
||||
progress.records[PeakProgress::key(id, boolOr(entry, "hard", false))];
|
||||
record.cycles = u32(entry, "cycles", 0);
|
||||
record.targets = ids(entry, "targets");
|
||||
record.team = ids(entry, "team");
|
||||
record.buffId = u32(entry, "buff", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The three-node floors: what the team editor last held, and how far each got.
|
||||
if (auto tierce = j.find("tierce"); tierce != j.end() && tierce->is_array()) {
|
||||
for (const json& entry : *tierce) {
|
||||
uint32_t id = entry.is_object() ? u32(entry, "id", 0) : 0;
|
||||
if (id == 0) continue;
|
||||
TierceProgress& progress = player.tierceHistory()[id];
|
||||
progress.passed = boolOr(entry, "passed", false);
|
||||
progress.targets = u32Array(entry, "targets");
|
||||
auto nodes = entry.find("nodes");
|
||||
if (nodes == entry.end() || !nodes->is_array()) continue;
|
||||
for (size_t stage = 0; stage < nodes->size() && stage < 3; ++stage) {
|
||||
const json& node = (*nodes)[stage];
|
||||
if (!node.is_object()) continue;
|
||||
progress.party[stage] = u32Array(node, "team");
|
||||
progress.buffs[stage] = u32(node, "buff", 0);
|
||||
progress.scores[stage] = u32(node, "score", 0);
|
||||
progress.cycles[stage] = u32(node, "cycles", 0);
|
||||
progress.deaths[stage] = u32(node, "deaths", 0);
|
||||
progress.cleared[stage] = boolOr(node, "cleared", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,6 +207,43 @@ bool savePlayerState(const Player& player) {
|
||||
{"entry_id", player.location().entryId}}},
|
||||
};
|
||||
|
||||
const PeakProgress& progress = player.peakProgress();
|
||||
json fights = json::array();
|
||||
for (const auto& [id, team] : progress.teams) {
|
||||
auto buff = progress.bossBuffs.find(id);
|
||||
fights.push_back({{"id", id},
|
||||
{"team", team},
|
||||
{"buff", buff != progress.bossBuffs.end() ? buff->second : 0u}});
|
||||
}
|
||||
json records = json::array();
|
||||
for (const auto& [key, record] : progress.records) {
|
||||
records.push_back({{"id", key / 2},
|
||||
{"hard", key % 2 == 1},
|
||||
{"cycles", record.cycles},
|
||||
{"targets", record.targets},
|
||||
{"team", record.team},
|
||||
{"buff", record.buffId}});
|
||||
}
|
||||
j["peak"] = {{"hard", progress.hardGroups}, {"fights", fights}, {"records", records}};
|
||||
|
||||
json tierce = json::array();
|
||||
for (const auto& [id, floor] : player.tierceHistory()) {
|
||||
json nodes = json::array();
|
||||
for (uint32_t stage = 0; stage < 3; ++stage) {
|
||||
nodes.push_back({{"team", floor.party[stage]},
|
||||
{"buff", floor.buffs[stage]},
|
||||
{"score", floor.scores[stage]},
|
||||
{"cycles", floor.cycles[stage]},
|
||||
{"deaths", floor.deaths[stage]},
|
||||
{"cleared", floor.cleared[stage]}});
|
||||
}
|
||||
tierce.push_back({{"id", id},
|
||||
{"passed", floor.passed},
|
||||
{"targets", floor.targets},
|
||||
{"nodes", nodes}});
|
||||
}
|
||||
j["tierce"] = tierce;
|
||||
|
||||
const std::string& path = core::Config::get().paths.playerFile;
|
||||
if (!util::writeFile(path, j.dump(2))) {
|
||||
logging::warn("player", "could not write {}", path);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
#include "core/logger.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/peak.h"
|
||||
#include "game/player.h"
|
||||
#include "game/scene.h"
|
||||
#include "game/tierce.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/session.h"
|
||||
#include "proto/gen/protos.h"
|
||||
@@ -37,10 +39,30 @@ std::string unstick(net::Session& session, uint32_t entryId) {
|
||||
logging::info("rescue", "uid {}: {}", player->uid(), did);
|
||||
return did;
|
||||
}
|
||||
if (player->peak().active && entryId == 0) {
|
||||
peak::leave(session, *player);
|
||||
note("left the arbitration");
|
||||
logging::info("rescue", "uid {}: {}", player->uid(), did);
|
||||
return did;
|
||||
}
|
||||
if (player->tierce().active && entryId == 0) {
|
||||
tierce::leave(session, *player);
|
||||
note("left the challenge floor");
|
||||
logging::info("rescue", "uid {}: {}", player->uid(), did);
|
||||
return did;
|
||||
}
|
||||
if (player->challenge().active) {
|
||||
player->challenge() = ChallengeRun{};
|
||||
note("abandoned the challenge");
|
||||
}
|
||||
if (player->peak().active) {
|
||||
player->peak() = PeakRun{};
|
||||
note("abandoned the arbitration");
|
||||
}
|
||||
if (player->tierce().active) {
|
||||
player->tierce() = TierceRun{};
|
||||
note("abandoned the challenge floor");
|
||||
}
|
||||
|
||||
uint32_t target = entryId != 0 ? entryId : player->location().entryId;
|
||||
proto::SceneInfo scene;
|
||||
|
||||
+22
-4
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/config.h"
|
||||
#include "core/logger.h"
|
||||
#include "data/excel.h"
|
||||
#include "data/scene_res.h"
|
||||
@@ -105,13 +106,18 @@ proto::LineupInfo lineupInfo(const Player& player) {
|
||||
}
|
||||
|
||||
proto::SceneEntityGroupInfo actorGroup(Player& player, const Position& at) {
|
||||
return actorGroup(player, at, player.lineups().curMembers());
|
||||
}
|
||||
|
||||
proto::SceneEntityGroupInfo actorGroup(Player& player, const Position& at,
|
||||
const std::vector<uint32_t>& members) {
|
||||
proto::MotionInfo motion = toMotion(at);
|
||||
|
||||
proto::SceneEntityGroupInfo group;
|
||||
group.group_id = 0;
|
||||
|
||||
uint32_t slot = 0;
|
||||
for (uint32_t avatarId : player.lineups().curMembers()) {
|
||||
for (uint32_t avatarId : members) {
|
||||
SceneEntity entity;
|
||||
entity.entityId = slot + 1;
|
||||
entity.kind = EntityKind::Actor;
|
||||
@@ -146,7 +152,12 @@ bool load(Player& player, uint32_t entryId, uint32_t teleportId, bool commit,
|
||||
// entrance's own anchor, then any pad on the floor.
|
||||
Position spawn = player.position();
|
||||
const data::ResTeleport* anchor = teleportId != 0 ? floor->teleport(teleportId) : nullptr;
|
||||
if (anchor == nullptr && floor->floorId != player.location().floorId) {
|
||||
if (teleportId != 0 && anchor == nullptr) {
|
||||
logging::warn("scene", "entry {} has no teleport {}, landing on its entrance", entryId,
|
||||
teleportId);
|
||||
}
|
||||
// A teleport that asked to go somewhere always moves the player, even on this floor.
|
||||
if (anchor == nullptr && (teleportId != 0 || floor->floorId != player.location().floorId)) {
|
||||
const data::EntranceInfo* entrance = data::Tables::get().entrance(entryId);
|
||||
uint32_t anchorId = entrance != nullptr ? entrance->startAnchorId : 0;
|
||||
anchor = data::SceneRes::get().anchor(entryId, anchorId);
|
||||
@@ -172,7 +183,10 @@ bool load(Player& player, uint32_t entryId, uint32_t teleportId, bool commit,
|
||||
scene.game_mode_type = floor->planeType;
|
||||
scene.world_id = fixWorldId(floor->worldId);
|
||||
scene.lighten_section_list = floor->sections;
|
||||
scene.leader_entity_id = player.lineups().leaderSlot(player.lineups().curIndex()) + 1;
|
||||
// An arena team is led from its first slot.
|
||||
bool arenaTeam = arena != nullptr && arena->party != nullptr;
|
||||
scene.leader_entity_id =
|
||||
arenaTeam ? 1 : player.lineups().leaderSlot(player.lineups().curIndex()) + 1;
|
||||
scene.scene_identifier.emplace().floor_id = floor->floorId;
|
||||
for (const auto& [name, value] : floor->savedValues) scene.floor_saved_data[name] = value;
|
||||
|
||||
@@ -274,10 +288,13 @@ bool load(Player& player, uint32_t entryId, uint32_t teleportId, bool commit,
|
||||
}
|
||||
|
||||
for (uint32_t chest : group.chests) scene.opened_chests_list.push_back(chest);
|
||||
const core::GameplayConfig& gameplay = core::Config::get().gameplay;
|
||||
for (uint32_t mission : group.finishedMainMissions) {
|
||||
if (gameplay.missionSkipped(mission)) continue;
|
||||
missions.finished_main_mission_id_list.push_back(mission);
|
||||
}
|
||||
for (uint32_t mission : group.finishedSubMissions) {
|
||||
if (gameplay.missionSkipped(mission)) continue;
|
||||
proto::Mission entry;
|
||||
entry.id = mission;
|
||||
entry.status = proto::MissionStatus::MissionStatus_MissionFinish;
|
||||
@@ -292,7 +309,8 @@ bool load(Player& player, uint32_t entryId, uint32_t teleportId, bool commit,
|
||||
|
||||
// The party is group 0; its entity ids are slot + 1, which is what
|
||||
// SceneCastSkill and leader_entity_id refer to.
|
||||
scene.entity_group_list.push_back(actorGroup(player, spawn));
|
||||
scene.entity_group_list.push_back(arenaTeam ? actorGroup(player, spawn, *arena->party)
|
||||
: actorGroup(player, spawn));
|
||||
|
||||
out = std::move(scene);
|
||||
return true;
|
||||
|
||||
+13
-8
@@ -12,17 +12,18 @@ class Player;
|
||||
|
||||
// Positions are quantised to 1/1000 of a unit, the way the client sends them.
|
||||
struct Position {
|
||||
int32_t x = -26968;
|
||||
int32_t y = 78953;
|
||||
int32_t z = 14457;
|
||||
int32_t rotY = 11858;
|
||||
int32_t x = 42548;
|
||||
int32_t y = 3716;
|
||||
int32_t z = -38422;
|
||||
int32_t rotY = 325717;
|
||||
};
|
||||
|
||||
// The Astral Express parlour: somewhere every account can stand.
|
||||
// Jarilo-VI's Outlying Snow Plains, on the space anchor beside a calyx, so a new
|
||||
// account has a fight to hand.
|
||||
struct SceneLocation {
|
||||
uint32_t planeId = 20411;
|
||||
uint32_t floorId = 20411001;
|
||||
uint32_t entryId = 2041101;
|
||||
uint32_t planeId = 20101;
|
||||
uint32_t floorId = 20101001;
|
||||
uint32_t entryId = 2010101;
|
||||
};
|
||||
|
||||
enum class EntityKind : uint8_t { Actor, Npc, Monster, Prop };
|
||||
@@ -79,6 +80,8 @@ proto::MotionInfo toMotion(const Position& position);
|
||||
struct ChallengeArena {
|
||||
uint32_t mazeGroupId = 0;
|
||||
const std::vector<data::ChallengeMonster>* monsters = nullptr;
|
||||
// The team standing in it, which is not the squad the player walks around with.
|
||||
const std::vector<uint32_t>* party = nullptr;
|
||||
};
|
||||
|
||||
// Assembles SceneInfo out of the scene dump plus the current party.
|
||||
@@ -98,6 +101,8 @@ proto::SceneMapInfo mapInfo(const Player& player, uint32_t floorId);
|
||||
|
||||
// The party actors as their own group, which is how a lineup edit is pushed.
|
||||
proto::SceneEntityGroupInfo actorGroup(Player& player, const Position& at);
|
||||
proto::SceneEntityGroupInfo actorGroup(Player& player, const Position& at,
|
||||
const std::vector<uint32_t>& members);
|
||||
|
||||
} // namespace scene
|
||||
} // namespace game
|
||||
@@ -3,8 +3,10 @@
|
||||
#include "data/scene_res.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/peak.h"
|
||||
#include "game/player.h"
|
||||
#include "game/scene.h"
|
||||
#include "game/tierce.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/handler.h"
|
||||
#include "net/session.h"
|
||||
@@ -32,6 +34,8 @@ void onGetCurSceneInfo(net::Session& session, const proto::GetCurSceneInfoCsReq&
|
||||
// every monster the dump has in it.
|
||||
ChallengeArena storage;
|
||||
const ChallengeArena* arena = challenge::arena(*player, storage);
|
||||
if (arena == nullptr) arena = tierce::arena(*player, storage);
|
||||
if (arena == nullptr) arena = peak::arena(*player, storage);
|
||||
if (scene::load(*player, player->location().entryId, 0, false, info, arena)) {
|
||||
rsp.scene = std::move(info);
|
||||
} else {
|
||||
@@ -49,6 +53,8 @@ void onGetCurSceneInfo(net::Session& session, const proto::GetCurSceneInfoCsReq&
|
||||
void onEnterScene(net::Session& session, const proto::EnterSceneCsReq& req) {
|
||||
Player* player = playerOf(session, "EnterScene");
|
||||
if (player == nullptr) return;
|
||||
logging::debug("scene", "enter {} teleport {} interact {} from entry {}", req.entry_id,
|
||||
req.teleport_id, req.interact_id, player->location().entryId);
|
||||
|
||||
proto::SceneInfo info;
|
||||
if (!scene::load(*player, req.entry_id, req.teleport_id, true, info)) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// NPC talk state.
|
||||
//
|
||||
// Before it will offer a talk option the client asks whether the party has already met
|
||||
// the npcs standing around it, and each of these replies has to carry back the id it
|
||||
// was asked about -- an empty reply reads as "npc 0", so every npc stays unmet and the
|
||||
// interact prompt comes up with nothing in it. Everyone is met; nobody has taken a
|
||||
// talk reward.
|
||||
#include "game/handlers.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/handler.h"
|
||||
#include "net/session.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
void onGetFirstTalkNpc(net::Session& session, const proto::GetFirstTalkNpcCsReq& req) {
|
||||
proto::GetFirstTalkNpcScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
for (uint32_t npcId : req.npc_id_list) {
|
||||
proto::FirstNpcTalkInfo info;
|
||||
info.npc_id = npcId;
|
||||
info.is_meet = true;
|
||||
rsp.npc_meet_status_list.push_back(info);
|
||||
}
|
||||
session.send(cmd::GetFirstTalkNpcScRsp, rsp);
|
||||
}
|
||||
|
||||
// The same question for the npcs a cutscene introduces. Left unmet, as the official
|
||||
// server does -- claiming otherwise skips the first-meet performance.
|
||||
void onGetFirstTalkByPerformanceNpc(net::Session& session,
|
||||
const proto::GetFirstTalkByPerformanceNpcCsReq& req) {
|
||||
proto::GetFirstTalkByPerformanceNpcScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
for (uint32_t performanceId : req.performance_id_list) {
|
||||
proto::NpcMeetByPerformanceStatus status;
|
||||
status.performance_id = performanceId;
|
||||
rsp.npc_meet_status_list.push_back(status);
|
||||
}
|
||||
session.send(cmd::GetFirstTalkByPerformanceNpcScRsp, rsp);
|
||||
}
|
||||
|
||||
void onFinishFirstTalkNpc(net::Session& session, const proto::FinishFirstTalkNpcCsReq& req) {
|
||||
proto::FinishFirstTalkNpcScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.npc_id = req.npc_id;
|
||||
session.send(cmd::FinishFirstTalkNpcScRsp, rsp);
|
||||
}
|
||||
|
||||
void onFinishFirstTalkByPerformanceNpc(net::Session& session,
|
||||
const proto::FinishFirstTalkByPerformanceNpcCsReq& req) {
|
||||
proto::FinishFirstTalkByPerformanceNpcScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.performance_id = req.performance_id;
|
||||
// Present but empty: there is no inventory to pay a first-meet reward into.
|
||||
rsp.reward.emplace();
|
||||
session.send(cmd::FinishFirstTalkByPerformanceNpcScRsp, rsp);
|
||||
}
|
||||
|
||||
void onGetNpcTakenReward(net::Session& session, const proto::GetNpcTakenRewardCsReq& req) {
|
||||
proto::GetNpcTakenRewardScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.npc_id = req.npc_id;
|
||||
session.send(cmd::GetNpcTakenRewardScRsp, rsp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerTalkHandlers() {
|
||||
net::on<proto::GetFirstTalkNpcCsReq>(cmd::GetFirstTalkNpcCsReq, onGetFirstTalkNpc);
|
||||
net::on<proto::GetFirstTalkByPerformanceNpcCsReq>(cmd::GetFirstTalkByPerformanceNpcCsReq,
|
||||
onGetFirstTalkByPerformanceNpc);
|
||||
net::on<proto::FinishFirstTalkNpcCsReq>(cmd::FinishFirstTalkNpcCsReq, onFinishFirstTalkNpc);
|
||||
net::on<proto::FinishFirstTalkByPerformanceNpcCsReq>(cmd::FinishFirstTalkByPerformanceNpcCsReq,
|
||||
onFinishFirstTalkByPerformanceNpc);
|
||||
net::on<proto::GetNpcTakenRewardCsReq>(cmd::GetNpcTakenRewardCsReq, onGetNpcTakenReward);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,506 @@
|
||||
#include "game/tierce.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "core/logger.h"
|
||||
#include "data/excel.h"
|
||||
#include "data/scene_res.h"
|
||||
#include "game/battle.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/player.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/session.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kNodes = 3;
|
||||
|
||||
const data::ChallengeTierceInfo* configOf(const Player& player) {
|
||||
const TierceRun& run = player.tierce();
|
||||
if (!run.active) return nullptr;
|
||||
return data::Tables::get().challengeTierce(run.tierceId);
|
||||
}
|
||||
|
||||
// The floor a tierce row extends; everything but the third node comes from it.
|
||||
const data::ChallengeInfo* floorOf(const data::ChallengeTierceInfo& config) {
|
||||
return data::Tables::get().challenge(config.preChallengeId);
|
||||
}
|
||||
|
||||
const data::ChallengeInfo* floorOf(const Player& player) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
return config == nullptr ? nullptr : floorOf(*config);
|
||||
}
|
||||
|
||||
proto::ExtraLineupType lineupTypeOf(uint32_t stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
return proto::ExtraLineupType::ExtraLineupType_LineupChallenge;
|
||||
case 1:
|
||||
return proto::ExtraLineupType::ExtraLineupType_LineupChallenge2;
|
||||
default:
|
||||
return proto::ExtraLineupType::ExtraLineupType_LineupChallenge3;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t entranceOf(const data::ChallengeTierceInfo& config, const data::ChallengeInfo& floor,
|
||||
uint32_t stage) {
|
||||
if (stage >= 2) return config.mapEntranceId;
|
||||
if (stage == 1 && floor.mapEntranceId2 != 0) return floor.mapEntranceId2;
|
||||
return floor.mapEntranceId;
|
||||
}
|
||||
|
||||
// Memory of Chaos spends one pool of cycles across all three nodes; the other two keep
|
||||
// the floor's own per-node limit.
|
||||
bool poolsCycles(const data::ChallengeInfo& floor) {
|
||||
return floor.kind == data::ChallengeKind::Memory;
|
||||
}
|
||||
|
||||
std::string battleTypeOf(const data::ChallengeInfo& floor) {
|
||||
switch (floor.kind) {
|
||||
case data::ChallengeKind::Story:
|
||||
return "PF";
|
||||
case data::ChallengeKind::Boss:
|
||||
return "AS";
|
||||
case data::ChallengeKind::Memory:
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Which of the floor's star targets the run has met, as a flat list of target ids --
|
||||
// this mode reports the targets themselves rather than a bitmask.
|
||||
std::vector<uint32_t> finishedTargets(const Player& player) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
if (config == nullptr) return {};
|
||||
const TierceRun& run = player.tierce();
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
|
||||
std::vector<uint32_t> met;
|
||||
for (uint32_t id : config->targetIds) {
|
||||
const data::ChallengeTarget* target = tables.challengeTarget(id);
|
||||
if (target == nullptr) continue;
|
||||
bool ok = false;
|
||||
switch (target->kind) {
|
||||
case data::ChallengeTarget::Kind::RoundsLeft:
|
||||
ok = run.roundsLeft >= target->param;
|
||||
break;
|
||||
case data::ChallengeTarget::Kind::DeadAvatar:
|
||||
ok = run.deaths[0] + run.deaths[1] + run.deaths[2] == 0;
|
||||
break;
|
||||
case data::ChallengeTarget::Kind::TotalScore:
|
||||
ok = run.totalScore() >= target->param;
|
||||
break;
|
||||
}
|
||||
if (ok) met.push_back(id);
|
||||
}
|
||||
return met;
|
||||
}
|
||||
|
||||
bool enterArena(Player& player, proto::SceneInfo& out) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
if (config == nullptr) return false;
|
||||
const data::ChallengeInfo* floor = floorOf(*config);
|
||||
if (floor == nullptr) return false;
|
||||
|
||||
uint32_t entryId = entranceOf(*config, *floor, player.tierce().stage);
|
||||
ChallengeArena storage;
|
||||
// Every node starts at its entrance, not wherever the last one ended.
|
||||
SceneLocation before = player.location();
|
||||
player.location().floorId = 0;
|
||||
if (scene::load(player, entryId, 0, true, out, tierce::arena(player, storage))) return true;
|
||||
player.location() = before;
|
||||
logging::warn("tierce", "floor {} node {} names entrance {}, which is not in the dump",
|
||||
config->id, player.tierce().stage + 1, entryId);
|
||||
return false;
|
||||
}
|
||||
|
||||
proto::ChallengeTierceStageData stageData(const TierceRun& run, uint32_t stage) {
|
||||
proto::ChallengeTierceStageData data;
|
||||
data.stage_index = stage;
|
||||
data.end_status = static_cast<proto::BattleEndStatus>(run.endStatus[stage]);
|
||||
data.score_id = run.scores[stage];
|
||||
data.INLLMKEDGLC = run.cycles[stage];
|
||||
data.IBECKONIICF = run.deaths[stage];
|
||||
return data;
|
||||
}
|
||||
|
||||
// One node cleared, or the whole floor. The client redraws the node list off these.
|
||||
void syncStage(net::Session& session, const Player& player, uint32_t stage) {
|
||||
const TierceRun& run = player.tierce();
|
||||
proto::ChallengeTierceSyncNotify notify;
|
||||
auto& single = notify.IANBCDJPDJF.emplace();
|
||||
single.stage_data = stageData(run, stage);
|
||||
single.is_passed = run.passed;
|
||||
single.finished_target_list = finishedTargets(player);
|
||||
notify.MMCGBGDJIPN_case = proto::ChallengeTierceSyncNotify::k_IANBCDJPDJF;
|
||||
session.send(cmd::ChallengeTierceSyncNotify, notify);
|
||||
}
|
||||
|
||||
void syncFloor(net::Session& session, const Player& player) {
|
||||
const TierceRun& run = player.tierce();
|
||||
proto::ChallengeTierceSyncNotify notify;
|
||||
auto& all = notify.DFNNKENBOEN.emplace();
|
||||
all.challenge_id = run.tierceId;
|
||||
all.is_passed = run.passed;
|
||||
all.finished_target_list = finishedTargets(player);
|
||||
for (uint32_t stage = 0; stage < kNodes; ++stage) {
|
||||
all.GBMBAHHDONN.push_back(stageData(run, stage));
|
||||
}
|
||||
auto& record = all.EGEHLOPFELK.emplace();
|
||||
record.used_cycle_count = run.cycles[0] + run.cycles[1] + run.cycles[2];
|
||||
record.total_score = run.totalScore();
|
||||
notify.MMCGBGDJIPN_case = proto::ChallengeTierceSyncNotify::k_DFNNKENBOEN;
|
||||
session.send(cmd::ChallengeTierceSyncNotify, notify);
|
||||
}
|
||||
|
||||
void syncMedal(net::Session& session, uint32_t groupId) {
|
||||
proto::ChallengeTierceSyncNotify notify;
|
||||
notify.medal.emplace().group_id = groupId;
|
||||
notify.MMCGBGDJIPN_case = proto::ChallengeTierceSyncNotify::k_medal;
|
||||
session.send(cmd::ChallengeTierceSyncNotify, notify);
|
||||
}
|
||||
|
||||
// Folds the run into what the floor's history shows from now on.
|
||||
void record(Player& player) {
|
||||
const TierceRun& run = player.tierce();
|
||||
TierceProgress& progress = player.tierceHistory()[run.tierceId];
|
||||
for (uint32_t stage = 0; stage < kNodes; ++stage) {
|
||||
if (!run.party[stage].empty()) progress.party[stage] = run.party[stage];
|
||||
progress.buffs[stage] = run.buffs[stage];
|
||||
if (run.endStatus[stage] ==
|
||||
static_cast<uint32_t>(proto::BattleEndStatus::BATTLE_END_WIN)) {
|
||||
progress.cleared[stage] = true;
|
||||
progress.scores[stage] = run.scores[stage];
|
||||
progress.cycles[stage] = run.cycles[stage];
|
||||
progress.deaths[stage] = run.deaths[stage];
|
||||
}
|
||||
}
|
||||
if (run.passed) progress.passed = true;
|
||||
std::vector<uint32_t> met = finishedTargets(player);
|
||||
for (uint32_t id : met) {
|
||||
if (std::find(progress.targets.begin(), progress.targets.end(), id) ==
|
||||
progress.targets.end()) {
|
||||
progress.targets.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The per-node teams a start request carries, or the ones the floor remembers when it
|
||||
// sends none.
|
||||
void adoptLineups(Player& player, uint32_t tierceId,
|
||||
const std::vector<proto::ChallengeTierceStageLineupInfo>& stages) {
|
||||
TierceRun& run = player.tierce();
|
||||
auto it = player.tierceHistory().find(tierceId);
|
||||
const TierceProgress* progress =
|
||||
it == player.tierceHistory().end() ? nullptr : &it->second;
|
||||
|
||||
for (uint32_t stage = 0; stage < kNodes; ++stage) {
|
||||
std::vector<uint32_t> party;
|
||||
if (stage < stages.size()) {
|
||||
for (const proto::AvatarIdentifier& entry : stages[stage].lineup) {
|
||||
if (entry.id != 0) party.push_back(entry.id);
|
||||
}
|
||||
run.buffs[stage] = stages[stage].buff_id;
|
||||
}
|
||||
if (party.empty() && progress != nullptr) party = progress->party[stage];
|
||||
if (stage >= stages.size() && progress != nullptr) run.buffs[stage] = progress->buffs[stage];
|
||||
run.party[stage] = std::move(party);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace tierce {
|
||||
|
||||
proto::GetChallengeTierceDataScRsp history(const Player& player) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
|
||||
proto::GetChallengeTierceDataScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
for (const data::ChallengeInfo& floor : tables.challenges()) {
|
||||
const data::ChallengeTierceInfo* config = tables.challengeTierceFor(floor.id);
|
||||
if (config == nullptr) continue;
|
||||
|
||||
auto it = player.tierceHistory().find(config->id);
|
||||
const TierceProgress* progress =
|
||||
it == player.tierceHistory().end() ? nullptr : &it->second;
|
||||
|
||||
proto::ChallengeTierceData data;
|
||||
data.challenge_id = config->id;
|
||||
// Unlocking floors is GetChallenge's job; this only says what has been played.
|
||||
data.is_passed = progress != nullptr && progress->passed;
|
||||
if (progress != nullptr) data.finished_target_list = progress->targets;
|
||||
auto& summary = data.record.emplace();
|
||||
for (uint32_t stage = 0; stage < kNodes; ++stage) {
|
||||
proto::ChallengeTierceStageInfo info;
|
||||
info.stage_index = stage;
|
||||
if (progress != nullptr) {
|
||||
info.buff_id = progress->buffs[stage];
|
||||
for (uint32_t avatarId : progress->party[stage]) {
|
||||
proto::AvatarIdentifier entry;
|
||||
entry.id = avatarId;
|
||||
info.lineup.push_back(entry);
|
||||
}
|
||||
summary.used_cycle_count += progress->cycles[stage];
|
||||
summary.total_score += progress->scores[stage];
|
||||
|
||||
proto::ChallengeTierceStageData result;
|
||||
result.stage_index = stage;
|
||||
result.end_status = progress->cleared[stage]
|
||||
? proto::BattleEndStatus::BATTLE_END_WIN
|
||||
: proto::BattleEndStatus::BATTLE_END_NONE;
|
||||
result.score_id = progress->scores[stage];
|
||||
result.INLLMKEDGLC = progress->cycles[stage];
|
||||
result.IBECKONIICF = progress->deaths[stage];
|
||||
data.result_list.push_back(std::move(result));
|
||||
}
|
||||
data.stage_info_list.push_back(std::move(info));
|
||||
}
|
||||
rsp.challenge_info_list.push_back(std::move(data));
|
||||
}
|
||||
return rsp;
|
||||
}
|
||||
|
||||
proto::ChallengeTierceChallengeInfo current(const Player& player) {
|
||||
const TierceRun& run = player.tierce();
|
||||
|
||||
proto::ChallengeTierceChallengeInfo info;
|
||||
info.challenge_id = run.tierceId;
|
||||
info.stage_index = run.stage;
|
||||
info.is_single_stage = run.singleStage;
|
||||
for (uint32_t stage = 0; stage < kNodes; ++stage) {
|
||||
info.lineup_list.push_back(lineup(player, stage));
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
proto::LineupInfo lineup(const Player& player, uint32_t stage) {
|
||||
uint32_t index = std::min(stage, kNodes - 1);
|
||||
return challenge::extraLineup(player, player.tierce().party[index], lineupTypeOf(index));
|
||||
}
|
||||
|
||||
void setLineups(Player& player, uint32_t tierceId,
|
||||
const std::vector<proto::ChallengeTierceStageLineupInfo>& stages) {
|
||||
if (data::Tables::get().challengeTierce(tierceId) == nullptr) return;
|
||||
TierceProgress& progress = player.tierceHistory()[tierceId];
|
||||
for (uint32_t stage = 0; stage < kNodes && stage < stages.size(); ++stage) {
|
||||
std::vector<uint32_t> party;
|
||||
for (const proto::AvatarIdentifier& entry : stages[stage].lineup) {
|
||||
if (entry.id != 0) party.push_back(entry.id);
|
||||
}
|
||||
if (!party.empty()) progress.party[stage] = std::move(party);
|
||||
progress.buffs[stage] = stages[stage].buff_id;
|
||||
}
|
||||
player.save();
|
||||
}
|
||||
|
||||
uint32_t start(Player& player, uint32_t tierceId, bool single, uint32_t stageIndex,
|
||||
const std::vector<proto::ChallengeTierceStageLineupInfo>& stages,
|
||||
proto::SceneInfo& out) {
|
||||
const data::ChallengeTierceInfo* config = data::Tables::get().challengeTierce(tierceId);
|
||||
if (config == nullptr) return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
const data::ChallengeInfo* floor = floorOf(*config);
|
||||
if (floor == nullptr) return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
if (stageIndex >= kNodes) return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
|
||||
TierceRun saved = player.tierce();
|
||||
|
||||
TierceRun run;
|
||||
run.active = true;
|
||||
run.tierceId = tierceId;
|
||||
run.singleStage = single;
|
||||
run.stage = single ? stageIndex : 0;
|
||||
run.roundsLeft = poolsCycles(*floor) ? config->roundLimit : floor->roundLimit;
|
||||
// A retry keeps the spot the first attempt left from.
|
||||
run.origin = saved.active ? saved.origin : player.location();
|
||||
run.originPos = saved.active ? saved.originPos : player.position();
|
||||
|
||||
player.tierce() = std::move(run);
|
||||
adoptLineups(player, tierceId, stages);
|
||||
if (player.tierce().curParty().empty()) {
|
||||
player.tierce() = std::move(saved);
|
||||
return fail(proto::Retcode::RET_CHALLENGE_LINEUP_EMPTY);
|
||||
}
|
||||
if (!enterArena(player, out)) {
|
||||
player.tierce() = std::move(saved);
|
||||
return fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
}
|
||||
|
||||
// Both cannot be going at once, and the older packet's run is what the client would
|
||||
// otherwise still think it is in.
|
||||
player.challenge() = ChallengeRun{};
|
||||
record(player);
|
||||
player.saveNow();
|
||||
|
||||
logging::info("tierce", "floor {} started at node {}{}, {} cycle(s)", tierceId,
|
||||
player.tierce().stage + 1, single ? " on its own" : "",
|
||||
player.tierce().roundsLeft);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ChallengeArena* arena(const Player& player, ChallengeArena& storage) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
if (config == nullptr) return nullptr;
|
||||
const data::ChallengeInfo* floor = floorOf(*config);
|
||||
if (floor == nullptr) return nullptr;
|
||||
|
||||
const TierceRun& run = player.tierce();
|
||||
if (run.stage >= 2) {
|
||||
storage.mazeGroupId = config->mazeGroupId;
|
||||
storage.monsters = &config->monsters;
|
||||
} else {
|
||||
storage.mazeGroupId = floor->stages[run.stage].mazeGroupId;
|
||||
storage.monsters = &floor->stages[run.stage].monsters;
|
||||
}
|
||||
storage.party = &run.curParty();
|
||||
return &storage;
|
||||
}
|
||||
|
||||
void prepareBattle(const Player& player, BattleRequest& request) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
if (config == nullptr) return;
|
||||
const data::ChallengeInfo* floor = floorOf(*config);
|
||||
if (floor == nullptr) return;
|
||||
const TierceRun& run = player.tierce();
|
||||
|
||||
request.party = run.curParty();
|
||||
request.mazeBuffId = floor->mazeBuffId;
|
||||
request.stageBuffId = run.buffs[run.stage < kNodes ? run.stage : kNodes - 1];
|
||||
request.roundsLimit = run.roundsLeft;
|
||||
request.scoreSoFar = run.totalScore();
|
||||
request.battleType = battleTypeOf(*floor);
|
||||
request.battleTargetIds = floor->battleTargetIds;
|
||||
}
|
||||
|
||||
bool nextStage(net::Session& session, Player& player, proto::SceneInfo& out) {
|
||||
TierceRun& run = player.tierce();
|
||||
if (!run.active || run.singleStage || run.stage + 1 >= kNodes) return false;
|
||||
|
||||
++run.stage;
|
||||
if (run.party[run.stage].empty()) {
|
||||
--run.stage;
|
||||
return false;
|
||||
}
|
||||
if (!enterArena(player, out)) {
|
||||
--run.stage;
|
||||
return false;
|
||||
}
|
||||
|
||||
proto::ChallengeLineupNotify swap;
|
||||
swap.extra_lineup_type = lineupTypeOf(run.stage);
|
||||
session.send(cmd::ChallengeLineupNotify, swap);
|
||||
|
||||
proto::SyncLineupNotify sync;
|
||||
sync.lineup = lineup(player, run.stage);
|
||||
session.send(cmd::SyncLineupNotify, sync);
|
||||
player.saveNow();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool restart(net::Session& session, Player& player, proto::SceneInfo& out) {
|
||||
TierceRun& run = player.tierce();
|
||||
if (!run.active) return false;
|
||||
|
||||
// The node is fought again from scratch; what it already banked is dropped.
|
||||
run.endStatus[run.stage] = 0;
|
||||
run.scores[run.stage] = 0;
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
const data::ChallengeInfo* floor = config == nullptr ? nullptr : floorOf(*config);
|
||||
if (floor != nullptr && poolsCycles(*floor)) {
|
||||
run.roundsLeft += run.cycles[run.stage];
|
||||
}
|
||||
run.cycles[run.stage] = 0;
|
||||
run.deaths[run.stage] = 0;
|
||||
|
||||
if (!enterArena(player, out)) return false;
|
||||
|
||||
proto::SyncLineupNotify sync;
|
||||
sync.lineup = lineup(player, run.stage);
|
||||
session.send(cmd::SyncLineupNotify, sync);
|
||||
player.saveNow();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool battleFinished(net::Session& session, Player& player,
|
||||
const proto::PVEBattleResultCsReq& req) {
|
||||
const data::ChallengeTierceInfo* config = configOf(player);
|
||||
if (config == nullptr) return false;
|
||||
const data::ChallengeInfo* floor = floorOf(*config);
|
||||
if (floor == nullptr) return false;
|
||||
|
||||
TierceRun& run = player.tierce();
|
||||
uint32_t stage = std::min(run.stage, kNodes - 1);
|
||||
|
||||
// Backing out of a node is not losing the floor; the client stays put.
|
||||
if (req.end_status == proto::BattleEndStatus::BATTLE_END_QUIT) return true;
|
||||
|
||||
if (req.stt) {
|
||||
// Pure Fiction and Apocalyptic Shadow report a running total, so this node's
|
||||
// share is whatever is above what the earlier ones banked.
|
||||
uint32_t total = req.stt->challenge_score;
|
||||
uint32_t before = run.totalScore() - run.scores[stage];
|
||||
run.scores[stage] = total > before ? total - before : 0;
|
||||
|
||||
for (const proto::AvatarBattleInfo& avatar : req.stt->battle_avatar_list) {
|
||||
if (avatar.avatar_status && avatar.avatar_status->left_hp == 0) ++run.deaths[stage];
|
||||
}
|
||||
run.cycles[stage] = std::max(run.cycles[stage], req.stt->round_cnt);
|
||||
if (poolsCycles(*floor)) {
|
||||
uint32_t used = run.cycles[0] + run.cycles[1] + run.cycles[2];
|
||||
uint32_t limit = config->roundLimit;
|
||||
run.roundsLeft = limit > used ? limit - used : 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool win = req.end_status == proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
if (!win) {
|
||||
run.endStatus[stage] = static_cast<uint32_t>(proto::BattleEndStatus::BATTLE_END_LOSE);
|
||||
syncFloor(session, player);
|
||||
record(player);
|
||||
player.saveNow();
|
||||
logging::info("tierce", "floor {} failed at node {}", run.tierceId, stage + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Monsters still standing means this node is not over.
|
||||
for (const SceneEntity& entity : player.sceneState().entities()) {
|
||||
if (entity.kind == EntityKind::Monster) return true;
|
||||
}
|
||||
|
||||
run.endStatus[stage] = static_cast<uint32_t>(proto::BattleEndStatus::BATTLE_END_WIN);
|
||||
// Replaying one node ends there; walking the floor ends after the third, and only
|
||||
// the third clears the floor either way.
|
||||
bool last = run.singleStage || stage + 1 >= kNodes;
|
||||
if (stage + 1 >= kNodes) run.passed = true;
|
||||
record(player);
|
||||
player.saveNow();
|
||||
|
||||
logging::info("tierce", "floor {} node {} cleared in {} cycle(s), score {}", run.tierceId,
|
||||
stage + 1, run.cycles[stage], run.scores[stage]);
|
||||
|
||||
if (last) {
|
||||
syncFloor(session, player);
|
||||
if (run.passed) syncMedal(session, floor->groupId);
|
||||
} else {
|
||||
// The client asks for the next node itself once it has read this.
|
||||
syncStage(session, player, stage);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void leave(net::Session& session, Player& player) {
|
||||
TierceRun& run = player.tierce();
|
||||
if (!run.active) return;
|
||||
|
||||
record(player);
|
||||
uint32_t entryId = run.origin.entryId;
|
||||
Position at = run.originPos;
|
||||
run = TierceRun{};
|
||||
challenge::putBack(session, player, entryId, at);
|
||||
}
|
||||
|
||||
} // namespace tierce
|
||||
} // namespace game
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "game/scene.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
namespace net {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace game {
|
||||
|
||||
class Player;
|
||||
struct BattleRequest;
|
||||
|
||||
// The three-node floor 4.6 replaced the two-node one with. A ChallengeMazeTierce row
|
||||
// bolts a third node onto a Memory of Chaos, Pure Fiction or Apocalyptic Shadow floor,
|
||||
// and from then on the client starts that whole floor through here rather than through
|
||||
// StartChallenge -- so a floor with a tierce row never sees the older packet at all.
|
||||
//
|
||||
// The first two nodes are the ones the floor already had; the third brings its own
|
||||
// arena and monster. Each node fights with its own team and its own picked buff.
|
||||
namespace tierce {
|
||||
|
||||
// Every floor that has a third node, with whatever the player has done on it.
|
||||
proto::GetChallengeTierceDataScRsp history(const Player& player);
|
||||
// The run in progress, as the client models it. Meaningless when none is.
|
||||
proto::ChallengeTierceChallengeInfo current(const Player& player);
|
||||
// The team fighting node `stage` (0..2), as an extra lineup rather than a squad.
|
||||
proto::LineupInfo lineup(const Player& player, uint32_t stage);
|
||||
|
||||
// Remembers what the team editor set without starting anything, so the per-node UI
|
||||
// keeps showing it.
|
||||
void setLineups(Player& player, uint32_t tierceId,
|
||||
const std::vector<proto::ChallengeTierceStageLineupInfo>& stages);
|
||||
|
||||
// Starts a floor, entering the arena for its first node -- or for `stageIndex` alone
|
||||
// when the player is replaying one. Returns the retcode; on success `out` holds the
|
||||
// arena.
|
||||
uint32_t start(Player& player, uint32_t tierceId, bool single, uint32_t stageIndex,
|
||||
const std::vector<proto::ChallengeTierceStageLineupInfo>& stages,
|
||||
proto::SceneInfo& out);
|
||||
|
||||
// Walks on to the next node; false when the run is on its last.
|
||||
bool nextStage(net::Session& session, Player& player, proto::SceneInfo& out);
|
||||
// Re-enters the node in progress, with its counters rolled back.
|
||||
bool restart(net::Session& session, Player& player, proto::SceneInfo& out);
|
||||
|
||||
// Same as challenge::arena, for the node in progress.
|
||||
const ChallengeArena* arena(const Player& player, ChallengeArena& storage);
|
||||
|
||||
// What the node brings to a fight: its team, the floor's maze buff, the buff the player
|
||||
// picked for it, the cycles left and the mode's win condition.
|
||||
void prepareBattle(const Player& player, BattleRequest& request);
|
||||
|
||||
// A fight inside a run has ended. False when no run is going.
|
||||
bool battleFinished(net::Session& session, Player& player,
|
||||
const proto::PVEBattleResultCsReq& req);
|
||||
|
||||
// Ends the run and puts the player back where they started.
|
||||
void leave(net::Session& session, Player& player);
|
||||
|
||||
} // namespace tierce
|
||||
} // namespace game
|
||||
@@ -0,0 +1,169 @@
|
||||
// The three-node challenge floor.
|
||||
//
|
||||
// StartChallengeTierce answers with the run and the arena in one response -- unlike
|
||||
// Anomaly Arbitration, this one carries its SceneInfo in the reply rather than in a
|
||||
// notify, and the same goes for the next node and a restart. Clearing a node is
|
||||
// reported with ChallengeTierceSyncNotify; the client then asks for the node after it.
|
||||
#include <utility>
|
||||
|
||||
#include "core/logger.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/player.h"
|
||||
#include "game/tierce.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;
|
||||
}
|
||||
|
||||
// The team for the node being entered, so the client swaps to it before the scene.
|
||||
void pushLineup(net::Session& session, const Player& player) {
|
||||
uint32_t stage = player.tierce().stage;
|
||||
|
||||
proto::ChallengeLineupNotify swap;
|
||||
swap.extra_lineup_type = stage >= 2
|
||||
? proto::ExtraLineupType::ExtraLineupType_LineupChallenge3
|
||||
: stage == 1
|
||||
? proto::ExtraLineupType::ExtraLineupType_LineupChallenge2
|
||||
: proto::ExtraLineupType::ExtraLineupType_LineupChallenge;
|
||||
session.send(cmd::ChallengeLineupNotify, swap);
|
||||
|
||||
proto::SyncLineupNotify sync;
|
||||
sync.lineup = tierce::lineup(player, stage);
|
||||
session.send(cmd::SyncLineupNotify, sync);
|
||||
}
|
||||
|
||||
void onGetChallengeTierceData(net::Session& session, const proto::GetChallengeTierceDataCsReq&) {
|
||||
Player* player = playerOf(session, "GetChallengeTierceData");
|
||||
if (player == nullptr) return;
|
||||
session.send(cmd::GetChallengeTierceDataScRsp, tierce::history(*player));
|
||||
}
|
||||
|
||||
// Asked every login. Carries the run in progress so a reconnect lands back in it.
|
||||
void onGetChallengeTierceController(net::Session& session,
|
||||
const proto::GetChallengeTierceControllerCsReq&) {
|
||||
Player* player = playerOf(session, "GetChallengeTierceController");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::GetChallengeTierceControllerScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
if (player->tierce().active) {
|
||||
rsp.challenge_tierce_info = tierce::current(*player);
|
||||
}
|
||||
session.send(cmd::GetChallengeTierceControllerScRsp, rsp);
|
||||
}
|
||||
|
||||
void onSetChallengeTierceLineup(net::Session& session,
|
||||
const proto::SetChallengeTierceLineupCsReq& req) {
|
||||
Player* player = playerOf(session, "SetChallengeTierceLineup");
|
||||
if (player == nullptr) return;
|
||||
|
||||
tierce::setLineups(*player, req.challenge_id, req.stage_info_list);
|
||||
|
||||
proto::SetChallengeTierceLineupScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::SetChallengeTierceLineupScRsp, rsp);
|
||||
}
|
||||
|
||||
void onStartChallengeTierce(net::Session& session, const proto::StartChallengeTierceCsReq& req) {
|
||||
Player* player = playerOf(session, "StartChallengeTierce");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::StartChallengeTierceScRsp rsp;
|
||||
proto::SceneInfo scene;
|
||||
rsp.retcode = tierce::start(*player, req.challenge_id, req.is_single_stage, req.stage_index,
|
||||
req.stage_info_list, scene);
|
||||
if (rsp.retcode != 0) {
|
||||
session.send(cmd::StartChallengeTierceScRsp, rsp);
|
||||
return;
|
||||
}
|
||||
|
||||
pushLineup(session, *player);
|
||||
rsp.challenge_tierce_info = tierce::current(*player);
|
||||
rsp.scene = std::move(scene);
|
||||
session.send(cmd::StartChallengeTierceScRsp, rsp);
|
||||
}
|
||||
|
||||
void onStartNextChallengeTierce(net::Session& session,
|
||||
const proto::StartNextChallengeTierceCsReq&) {
|
||||
Player* player = playerOf(session, "StartNextChallengeTierce");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::StartNextChallengeTierceScRsp rsp;
|
||||
proto::SceneInfo scene;
|
||||
if (!tierce::nextStage(session, *player, scene)) {
|
||||
rsp.retcode = fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
session.send(cmd::StartNextChallengeTierceScRsp, rsp);
|
||||
return;
|
||||
}
|
||||
rsp.retcode = 0;
|
||||
rsp.challenge_tierce_info = tierce::current(*player);
|
||||
rsp.scene = std::move(scene);
|
||||
session.send(cmd::StartNextChallengeTierceScRsp, rsp);
|
||||
}
|
||||
|
||||
void onRestartChallengeTierce(net::Session& session, const proto::RestartChallengeTierceCsReq&) {
|
||||
Player* player = playerOf(session, "RestartChallengeTierce");
|
||||
if (player == nullptr) return;
|
||||
|
||||
proto::RestartChallengeTierceScRsp rsp;
|
||||
proto::SceneInfo scene;
|
||||
if (!tierce::restart(session, *player, scene)) {
|
||||
rsp.retcode = fail(proto::Retcode::RET_CHALLENGE_NOT_EXIST);
|
||||
session.send(cmd::RestartChallengeTierceScRsp, rsp);
|
||||
return;
|
||||
}
|
||||
rsp.retcode = 0;
|
||||
rsp.GFACJIPNIAN = std::move(scene);
|
||||
session.send(cmd::RestartChallengeTierceScRsp, rsp);
|
||||
}
|
||||
|
||||
// The settle screen closing. Nothing to hand over -- rewards are already taken.
|
||||
void onConfirmChallengeTierceStageSettle(
|
||||
net::Session& session, const proto::ConfirmChallengeTierceStageSettleCsReq&) {
|
||||
proto::ConfirmChallengeTierceStageSettleScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::ConfirmChallengeTierceStageSettleScRsp, rsp);
|
||||
}
|
||||
|
||||
void onLeaveChallengeTierce(net::Session& session, const proto::LeaveChallengeTierceCsReq&) {
|
||||
Player* player = playerOf(session, "LeaveChallengeTierce");
|
||||
if (player == nullptr) return;
|
||||
|
||||
tierce::leave(session, *player);
|
||||
|
||||
proto::LeaveChallengeTierceScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
session.send(cmd::LeaveChallengeTierceScRsp, rsp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerTierceHandlers() {
|
||||
net::on<proto::GetChallengeTierceDataCsReq>(cmd::GetChallengeTierceDataCsReq,
|
||||
onGetChallengeTierceData);
|
||||
net::on<proto::GetChallengeTierceControllerCsReq>(cmd::GetChallengeTierceControllerCsReq,
|
||||
onGetChallengeTierceController);
|
||||
net::on<proto::SetChallengeTierceLineupCsReq>(cmd::SetChallengeTierceLineupCsReq,
|
||||
onSetChallengeTierceLineup);
|
||||
net::on<proto::StartChallengeTierceCsReq>(cmd::StartChallengeTierceCsReq,
|
||||
onStartChallengeTierce);
|
||||
net::on<proto::StartNextChallengeTierceCsReq>(cmd::StartNextChallengeTierceCsReq,
|
||||
onStartNextChallengeTierce);
|
||||
net::on<proto::RestartChallengeTierceCsReq>(cmd::RestartChallengeTierceCsReq,
|
||||
onRestartChallengeTierce);
|
||||
net::on<proto::ConfirmChallengeTierceStageSettleCsReq>(
|
||||
cmd::ConfirmChallengeTierceStageSettleCsReq, onConfirmChallengeTierceStageSettle);
|
||||
net::on<proto::LeaveChallengeTierceCsReq>(cmd::LeaveChallengeTierceCsReq,
|
||||
onLeaveChallengeTierce);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "sdk/routes.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "core/config.h"
|
||||
@@ -14,6 +17,9 @@ using json = nlohmann::json;
|
||||
namespace sdk {
|
||||
namespace {
|
||||
|
||||
std::mutex versionMutex;
|
||||
std::string lastVersion;
|
||||
|
||||
json accountBlock(const std::string& uid, const std::string& token) {
|
||||
return json{{"uid", uid},
|
||||
{"name", ""},
|
||||
@@ -101,6 +107,11 @@ std::string gatewayBody(const std::string& version) {
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string lastClientVersion() {
|
||||
std::lock_guard<std::mutex> lock(versionMutex);
|
||||
return lastVersion;
|
||||
}
|
||||
|
||||
void registerRoutes(http::Server& server) {
|
||||
server.get("/query_dispatch", [](const http::Request&, http::Response& res) {
|
||||
res.text(dispatchBody());
|
||||
@@ -108,6 +119,10 @@ void registerRoutes(http::Server& server) {
|
||||
|
||||
server.get("/query_gateway", [](const http::Request& req, http::Response& res) {
|
||||
std::string version = req.queryValue("version");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(versionMutex);
|
||||
lastVersion = version;
|
||||
}
|
||||
logging::info("dispatch", "query_gateway version={} seed={}", version,
|
||||
req.queryValue("dispatch_seed"));
|
||||
res.text(gatewayBody(version));
|
||||
|
||||
@@ -6,4 +6,8 @@ namespace sdk {
|
||||
|
||||
void registerRoutes(http::Server& server);
|
||||
|
||||
// The client version the last gateway query named, such as "CNBETAWin4.5.54"; empty
|
||||
// until one has. The game connection that follows carries nothing that says.
|
||||
std::string lastClientVersion();
|
||||
|
||||
} // namespace sdk
|
||||
@@ -439,6 +439,35 @@ void runFlowTests() {
|
||||
}
|
||||
}
|
||||
|
||||
// Every npc the client asks about comes back as already met, under the id it asked
|
||||
// about: an empty reply is what leaves an npc with no talk option.
|
||||
{
|
||||
proto::GetFirstTalkNpcCsReq talk;
|
||||
talk.npc_id_list = {1001, 1002, 1003};
|
||||
client.send(cmd::GetFirstTalkNpcCsReq, talk);
|
||||
check(client.await(cmd::GetFirstTalkNpcScRsp, packet), "the npc meet query answers");
|
||||
|
||||
proto::GetFirstTalkNpcScRsp talkRsp;
|
||||
check(parseBody(packet, talkRsp), "the npc meet query parses");
|
||||
check(talkRsp.npc_meet_status_list.size() == 3, "one status per npc asked about");
|
||||
bool echoed = talkRsp.npc_meet_status_list.size() == 3;
|
||||
for (size_t i = 0; i < talkRsp.npc_meet_status_list.size(); ++i) {
|
||||
if (talkRsp.npc_meet_status_list[i].npc_id != talk.npc_id_list[i] ||
|
||||
!talkRsp.npc_meet_status_list[i].is_meet) {
|
||||
echoed = false;
|
||||
}
|
||||
}
|
||||
check(echoed, "each carries its own id and is already met");
|
||||
|
||||
proto::GetNpcTakenRewardCsReq reward;
|
||||
reward.npc_id = 1003;
|
||||
client.send(cmd::GetNpcTakenRewardCsReq, reward);
|
||||
check(client.await(cmd::GetNpcTakenRewardScRsp, packet), "the npc reward query answers");
|
||||
proto::GetNpcTakenRewardScRsp rewardRsp;
|
||||
check(parseBody(packet, rewardRsp), "the npc reward query parses");
|
||||
check(rewardRsp.npc_id == 1003, "about the npc that was asked for");
|
||||
}
|
||||
|
||||
// A calyx, the way the Survival Index starts one: no prop to walk up to, just a
|
||||
// cocoon id and how many runs were bought.
|
||||
{
|
||||
@@ -618,6 +647,282 @@ void runFlowTests() {
|
||||
check(!curRsp.cur_challenge.has(), "and there is no longer one going");
|
||||
}
|
||||
|
||||
// The three-node floor: the packet 4.6 starts Memory of Chaos with. Unlike the older
|
||||
// one it carries its arena in the reply rather than in a notify.
|
||||
{
|
||||
proto::StartChallengeTierceCsReq start;
|
||||
start.challenge_id = 5213;
|
||||
for (uint32_t stage = 0; stage < 3; ++stage) {
|
||||
proto::ChallengeTierceStageLineupInfo info;
|
||||
proto::AvatarIdentifier one;
|
||||
one.id = 8001;
|
||||
proto::AvatarIdentifier two;
|
||||
two.id = 1001;
|
||||
info.lineup.push_back(one);
|
||||
info.lineup.push_back(two);
|
||||
start.stage_info_list.push_back(std::move(info));
|
||||
}
|
||||
client.send(cmd::StartChallengeTierceCsReq, start);
|
||||
check(client.await(cmd::StartChallengeTierceScRsp, packet), "starting the floor answers");
|
||||
|
||||
proto::StartChallengeTierceScRsp startRsp;
|
||||
check(parseBody(packet, startRsp), "the start parses");
|
||||
check(startRsp.retcode == 0, "and was accepted");
|
||||
check(startRsp.scene.has(), "with the arena in the reply itself");
|
||||
check(startRsp.challenge_tierce_info.has(), "and the run");
|
||||
if (startRsp.challenge_tierce_info) {
|
||||
check(startRsp.challenge_tierce_info->challenge_id == 5213, "for the floor asked for");
|
||||
check(startRsp.challenge_tierce_info->stage_index == 0, "starting on its first node");
|
||||
check(startRsp.challenge_tierce_info->lineup_list.size() == 3, "with all three teams");
|
||||
}
|
||||
|
||||
uint32_t nodeMonster = 0;
|
||||
size_t nodeMonsters = 0;
|
||||
if (startRsp.scene) {
|
||||
check(startRsp.scene->entry_id == 3014002, "on the floor's own map entrance");
|
||||
for (const proto::SceneEntityGroupInfo& group : startRsp.scene->entity_group_list) {
|
||||
for (const proto::SceneEntityInfo& entity : group.entity_list) {
|
||||
if (!entity.npc_monster) continue;
|
||||
++nodeMonsters;
|
||||
nodeMonster = entity.entity_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(nodeMonsters == 1, "holding only the node's own monster");
|
||||
|
||||
if (nodeMonster != 0) {
|
||||
proto::SceneCastSkillCsReq cast;
|
||||
cast.cast_entity_id = 1;
|
||||
cast.hit_target_entity_id_list.push_back(nodeMonster);
|
||||
client.send(cmd::SceneCastSkillCsReq, cast);
|
||||
check(client.await(cmd::SceneCastSkillScRsp, packet), "the node's fight answers");
|
||||
|
||||
proto::SceneCastSkillScRsp castRsp;
|
||||
check(parseBody(packet, castRsp), "the node's fight parses");
|
||||
check(castRsp.battle_info.has(), "a fight was started");
|
||||
if (castRsp.battle_info) {
|
||||
// The cycle pool is the tierce row's 45, not the floor's own 30.
|
||||
check(castRsp.battle_info->rounds_limit == 45, "with the floor-wide cycle pool");
|
||||
bool mazeBuff = false;
|
||||
for (const proto::BattleBuff& buff : castRsp.battle_info->buff_list) {
|
||||
if (buff.id == 3030146) mazeBuff = true;
|
||||
}
|
||||
check(mazeBuff, "and the maze buff of the floor it extends");
|
||||
|
||||
proto::PVEBattleResultCsReq result;
|
||||
result.battle_id = castRsp.battle_info->battle_id;
|
||||
result.stage_id = castRsp.battle_info->stage_id;
|
||||
result.end_status = proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
result.stt.emplace().round_cnt = 5;
|
||||
client.send(cmd::PVEBattleResultCsReq, result);
|
||||
check(client.await(cmd::PVEBattleResultScRsp, packet), "the result answers");
|
||||
|
||||
net::Packet syncPacket;
|
||||
check(client.await(cmd::ChallengeTierceSyncNotify, syncPacket),
|
||||
"clearing a node reports it");
|
||||
}
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::StartNextChallengeTierceCsReq);
|
||||
check(client.await(cmd::StartNextChallengeTierceScRsp, packet), "the next node answers");
|
||||
proto::StartNextChallengeTierceScRsp nextRsp;
|
||||
check(parseBody(packet, nextRsp), "the next node parses");
|
||||
check(nextRsp.retcode == 0, "and was accepted");
|
||||
check(nextRsp.scene.has(), "carrying its arena too");
|
||||
check(nextRsp.challenge_tierce_info.has() &&
|
||||
nextRsp.challenge_tierce_info->stage_index == 1,
|
||||
"on the second node now");
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengeTierceCsReq);
|
||||
check(client.await(cmd::LeaveChallengeTierceScRsp, packet), "leaving answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
client.sendEmpty(cmd::GetChallengeTierceDataCsReq);
|
||||
check(client.await(cmd::GetChallengeTierceDataScRsp, packet), "the history answers");
|
||||
proto::GetChallengeTierceDataScRsp dataRsp;
|
||||
check(parseBody(packet, dataRsp), "the history parses");
|
||||
bool remembered = false;
|
||||
for (const proto::ChallengeTierceData& entry : dataRsp.challenge_info_list) {
|
||||
if (entry.challenge_id != 5213) continue;
|
||||
remembered = true;
|
||||
check(!entry.result_list.empty(), "with what the run did on it");
|
||||
}
|
||||
check(remembered, "and the floor is in it");
|
||||
}
|
||||
|
||||
// Anomaly Arbitration: a knight on its own arena -- team, start, fight, settle, retry,
|
||||
// leave -- then season ten's boss on hard, whose arena is newer than the scene dump.
|
||||
{
|
||||
client.sendEmpty(cmd::GetChallengePeakDataCsReq);
|
||||
check(client.await(cmd::GetChallengePeakDataScRsp, packet), "arbitration data answered");
|
||||
proto::GetChallengePeakDataScRsp peakData;
|
||||
check(parseBody(packet, peakData), "arbitration data parses");
|
||||
check(peakData.challenge_peak_groups.size() >= 10, "with every season in it");
|
||||
check(peakData.current_peak_group_id == 10, "and the newest on show");
|
||||
|
||||
proto::SetChallengePeakMobLineupAvatarCsReq teams;
|
||||
teams.peak_group_id = 9;
|
||||
proto::ChallengePeakLineup knightTeam;
|
||||
knightTeam.peak_id = 901;
|
||||
knightTeam.peak_avatar_id_list = {8001, 1001};
|
||||
teams.lineup_list.push_back(knightTeam);
|
||||
client.send(cmd::SetChallengePeakMobLineupAvatarCsReq, teams);
|
||||
check(client.await(cmd::SetChallengePeakMobLineupAvatarScRsp, packet),
|
||||
"setting a knight's team answers");
|
||||
net::Packet groupPacket;
|
||||
check(client.await(cmd::ChallengePeakGroupDataUpdateScNotify, groupPacket),
|
||||
"and re-sends the season");
|
||||
proto::ChallengePeakGroupDataUpdateScNotify groupUpdate;
|
||||
check(parseBody(groupPacket, groupUpdate), "the season parses");
|
||||
bool teamKept = false;
|
||||
if (groupUpdate.challenge_peak_group) {
|
||||
for (const proto::ChallengePeak& peak : groupUpdate.challenge_peak_group->peaks) {
|
||||
if (peak.peak_id == 901 && peak.peak_avatar_id_list.size() == 2) teamKept = true;
|
||||
}
|
||||
}
|
||||
check(teamKept, "with the team in the knight's slot");
|
||||
|
||||
// Sent without a team, the start falls back on the one just set.
|
||||
proto::StartChallengePeakCsReq start;
|
||||
start.peak_id = 901;
|
||||
client.send(cmd::StartChallengePeakCsReq, start);
|
||||
check(client.await(cmd::StartChallengePeakScRsp, packet), "starting a knight answers");
|
||||
proto::StartChallengePeakScRsp startRsp;
|
||||
check(parseBody(packet, startRsp), "the start parses");
|
||||
check(startRsp.retcode == 0, "and was accepted");
|
||||
|
||||
auto arenaOf = [&](const char* what, uint32_t& monster, size_t& monsters, size_t& actors,
|
||||
uint32_t& entryId) {
|
||||
net::Packet arenaPacket;
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, arenaPacket), what);
|
||||
proto::EnterSceneByServerScNotify arena;
|
||||
check(parseBody(arenaPacket, arena), "the arena parses");
|
||||
monster = 0;
|
||||
monsters = actors = 0;
|
||||
entryId = arena.scene ? arena.scene->entry_id : 0;
|
||||
if (!arena.scene) return;
|
||||
for (const proto::SceneEntityGroupInfo& group : arena.scene->entity_group_list) {
|
||||
for (const proto::SceneEntityInfo& entity : group.entity_list) {
|
||||
if (entity.actor) ++actors;
|
||||
if (!entity.npc_monster) continue;
|
||||
++monsters;
|
||||
monster = entity.entity_id;
|
||||
}
|
||||
}
|
||||
};
|
||||
uint32_t knight = 0;
|
||||
size_t monsters = 0;
|
||||
size_t actors = 0;
|
||||
uint32_t entryId = 0;
|
||||
arenaOf("the arena went out before the response", knight, monsters, actors, entryId);
|
||||
check(entryId == 3014501, "on the season's own arena");
|
||||
check(monsters == 1, "with the knight and nothing else in it");
|
||||
check(actors == 2, "and the fight's team standing in it, not the squad");
|
||||
|
||||
auto fight = [&](uint32_t monster, uint32_t rounds, proto::SceneBattleInfo& battle) {
|
||||
proto::SceneCastSkillCsReq cast;
|
||||
cast.cast_entity_id = 1;
|
||||
cast.hit_target_entity_id_list.push_back(monster);
|
||||
client.send(cmd::SceneCastSkillCsReq, cast);
|
||||
check(client.await(cmd::SceneCastSkillScRsp, packet), "the fight answers");
|
||||
proto::SceneCastSkillScRsp castRsp;
|
||||
check(parseBody(packet, castRsp), "the fight parses");
|
||||
check(castRsp.battle_info.has(), "a fight was started");
|
||||
if (!castRsp.battle_info) return false;
|
||||
battle = *castRsp.battle_info;
|
||||
|
||||
proto::PVEBattleResultCsReq result;
|
||||
result.battle_id = battle.battle_id;
|
||||
result.stage_id = battle.stage_id;
|
||||
result.end_status = proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
result.stt.emplace().round_cnt = rounds;
|
||||
client.send(cmd::PVEBattleResultCsReq, result);
|
||||
check(client.await(cmd::PVEBattleResultScRsp, packet), "the result answers");
|
||||
return true;
|
||||
};
|
||||
auto targetsIn = [](const proto::SceneBattleInfo& battle) {
|
||||
auto slot = battle.battle_target_info.find(5);
|
||||
return slot == battle.battle_target_info.end() ? size_t{0}
|
||||
: slot->second.battle_target_list.size();
|
||||
};
|
||||
|
||||
proto::SceneBattleInfo battle;
|
||||
if (knight != 0 && fight(knight, 3, battle)) {
|
||||
check(battle.stage_id == 30509011, "against the knight's own stage");
|
||||
check(battle.battle_avatar_list.size() == 2, "with the knight's team");
|
||||
check(targetsIn(battle) == 3, "and its three targets");
|
||||
|
||||
net::Packet settlePacket;
|
||||
check(client.await(cmd::ChallengePeakSettleScNotify, settlePacket),
|
||||
"winning settles the knight");
|
||||
proto::ChallengePeakSettleScNotify settle;
|
||||
check(parseBody(settlePacket, settle), "the settle parses");
|
||||
check(settle.is_win && settle.peak_id == 901, "as a win for that knight");
|
||||
// Four cycles or fewer, two or fewer, and nobody lost: three cycles meets two.
|
||||
check(settle.finished_target_list.size() == 2, "three cycles meets two targets of three");
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::GetCurChallengePeakCsReq);
|
||||
check(client.await(cmd::GetCurChallengePeakScRsp, packet), "the fight is queryable");
|
||||
proto::GetCurChallengePeakScRsp cur;
|
||||
check(parseBody(packet, cur), "the fight parses");
|
||||
check(cur.peak_id == 901, "and it is the knight");
|
||||
|
||||
client.sendEmpty(cmd::ReStartChallengePeakCsReq);
|
||||
check(client.await(cmd::ReStartChallengePeakScRsp, packet), "retrying answers");
|
||||
proto::ReStartChallengePeakScRsp restart;
|
||||
check(parseBody(packet, restart), "the retry parses");
|
||||
check(restart.retcode == 0, "and was accepted");
|
||||
arenaOf("the arena went out again", knight, monsters, actors, entryId);
|
||||
check(monsters == 1, "with the knight standing again");
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengePeakCsReq);
|
||||
check(client.await(cmd::LeaveChallengePeakScRsp, packet), "leaving answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
proto::SetChallengePeakBossHardModeCsReq hardMode;
|
||||
hardMode.peak_group_id = 10;
|
||||
hardMode.is_hard_mode = true;
|
||||
client.send(cmd::SetChallengePeakBossHardModeCsReq, hardMode);
|
||||
check(client.await(cmd::SetChallengePeakBossHardModeScRsp, packet), "hard mode answers");
|
||||
proto::SetChallengePeakBossHardModeScRsp hardRsp;
|
||||
check(parseBody(packet, hardRsp), "hard mode parses");
|
||||
check(hardRsp.is_hard_mode && hardRsp.peak_group_id == 10, "and is echoed");
|
||||
|
||||
proto::StartChallengePeakCsReq bossStart;
|
||||
bossStart.peak_id = 1004;
|
||||
bossStart.peak_avatar_id_list = {8001, 1001};
|
||||
client.send(cmd::StartChallengePeakCsReq, bossStart);
|
||||
check(client.await(cmd::StartChallengePeakScRsp, packet), "starting the boss answers");
|
||||
check(parseBody(packet, startRsp) && startRsp.retcode == 0, "and was accepted");
|
||||
uint32_t boss = 0;
|
||||
arenaOf("the boss arena went out", boss, monsters, actors, entryId);
|
||||
check(entryId == 3013501, "borrowing season one's arena");
|
||||
check(monsters == 1, "with the boss alone in it");
|
||||
|
||||
if (boss != 0 && fight(boss, 1, battle)) {
|
||||
check(battle.stage_id == 30510022, "against the boss's hard stage");
|
||||
check(targetsIn(battle) == 1, "judged by the hard target alone");
|
||||
|
||||
net::Packet settlePacket;
|
||||
check(client.await(cmd::ChallengePeakSettleScNotify, settlePacket),
|
||||
"winning settles the boss");
|
||||
proto::ChallengePeakSettleScNotify settle;
|
||||
check(parseBody(settlePacket, settle), "the settle parses");
|
||||
check(settle.is_win && settle.hard_mode_has_passed, "as a hard clear");
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengePeakCsReq);
|
||||
check(client.await(cmd::LeaveChallengePeakScRsp, packet), "leaving the boss answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
client.sendEmpty(cmd::GetCurChallengePeakCsReq);
|
||||
check(client.await(cmd::GetCurChallengePeakScRsp, packet), "the run is queryable");
|
||||
proto::GetCurChallengePeakScRsp after;
|
||||
check(parseBody(packet, after), "the run parses");
|
||||
check(after.peak_id == 0, "and there is no longer one going");
|
||||
}
|
||||
|
||||
// Every way out of a fight answers. None of these did before, which left the
|
||||
// client sitting on the defeat screen or on a half-switched party.
|
||||
client.sendEmpty(cmd::QuitBattleCsReq);
|
||||
|
||||
+136
-1
@@ -18,6 +18,7 @@
|
||||
#include "game/roster.h"
|
||||
#include "game/scene.h"
|
||||
#include "game/srtools.h"
|
||||
#include "game/tierce.h"
|
||||
#include "net/packet.h"
|
||||
#include "tests/harness.h"
|
||||
|
||||
@@ -213,6 +214,27 @@ void testChallengeHistory() {
|
||||
check(rsp.max_level_list.size() == 3, "one max level per mode");
|
||||
check(rsp.serialize().size() + net::kPacketOverhead < net::kMaxKcpMessage,
|
||||
"the challenge history fits in one kcp message");
|
||||
|
||||
// Anomaly Arbitration. Production rows have no arena, so the beta dump fills it in,
|
||||
// and places season ten, which production does not have at all.
|
||||
check(tables.peakGroups().size() >= 10, "every arbitration season loaded");
|
||||
const data::PeakInfo* knight = tables.peak(101);
|
||||
check(knight != nullptr && knight->mapEntranceId == 3013501 && knight->eventId == 30501011,
|
||||
"a production knight gets its arena from the beta dump");
|
||||
check(knight != nullptr && knight->monsters.size() == 1 &&
|
||||
knight->monsters[0].configId == data::kPeakMarkerId,
|
||||
"and plants one monster over the arena's marker");
|
||||
const data::PeakInfo* boss = tables.peak(1004);
|
||||
check(boss != nullptr && boss->boss && boss->groupId == 10, "1004 is season ten's boss");
|
||||
check(boss != nullptr && boss->mapEntranceId == 3014601 && boss->mazeGroupId == 7,
|
||||
"on its own arena");
|
||||
check(boss != nullptr && boss->hardEventId == 30510022 && boss->hardTarget == 3007,
|
||||
"with a hard stage and target of its own");
|
||||
const data::BattleTargetInfo* turns = tables.battleTarget(3001);
|
||||
check(turns != nullptr && turns->param == 4 && !turns->countsDeaths,
|
||||
"target 3001 is four cycles or fewer");
|
||||
const data::BattleTargetInfo* deaths = tables.battleTarget(3000);
|
||||
check(deaths != nullptr && deaths->countsDeaths, "target 3000 counts avatars lost");
|
||||
}
|
||||
|
||||
void testSceneRes() {
|
||||
@@ -500,7 +522,6 @@ void testBattleBuild() {
|
||||
request.casterEntityId = 1;
|
||||
request.skillIndex = 0;
|
||||
request.monsterEntityIds = {monsterEntity};
|
||||
request.allowSrToolsOverride = false;
|
||||
|
||||
proto::SceneBattleInfo info = game::battle::create(player, request);
|
||||
check(info.battle_id != 0, "the fight got an id");
|
||||
@@ -565,6 +586,115 @@ void testBattleBuild() {
|
||||
check(!stillThere, "and can be switched off");
|
||||
}
|
||||
|
||||
// The third node 4.6 bolts onto a floor: its own arena and monster, everything else
|
||||
// borrowed from the floor it extends.
|
||||
void testTierceRun() {
|
||||
if (!data::SceneRes::get().loaded() || !data::Tables::get().loaded()) return;
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
|
||||
const data::ChallengeTierceInfo* config = tables.challengeTierce(5213);
|
||||
check(config != nullptr, "the tierce table is loaded");
|
||||
if (config == nullptr) return;
|
||||
check(config->preChallengeId == 5212, "5213 extends Memory of Chaos floor 5212");
|
||||
check(config->roundLimit == 45, "with a cycle pool for the whole floor");
|
||||
check(config->mazeGroupId == 11, "its node is scene group 11");
|
||||
check(config->monsters.size() == 1, "holding one planted monster");
|
||||
check(config->targetIds.size() == 3, "and three star targets");
|
||||
check(tables.challengeTierceFor(5212) == config, "the floor finds it the other way too");
|
||||
|
||||
const data::ChallengeInfo* floor = tables.challenge(config->preChallengeId);
|
||||
check(floor != nullptr, "the floor it extends is loaded");
|
||||
if (floor == nullptr) return;
|
||||
|
||||
game::Player player = makeTestPlayer();
|
||||
std::vector<proto::ChallengeTierceStageLineupInfo> stages(3);
|
||||
for (uint32_t stage = 0; stage < 3; ++stage) {
|
||||
proto::AvatarIdentifier one;
|
||||
one.id = 8001 + stage;
|
||||
stages[stage].lineup.push_back(one);
|
||||
stages[stage].buff_id = 1000 + stage;
|
||||
}
|
||||
|
||||
proto::SceneInfo scene;
|
||||
uint32_t retcode = game::tierce::start(player, 5213, false, 0, stages, scene);
|
||||
check(retcode == 0, "the floor starts");
|
||||
if (retcode != 0) return;
|
||||
check(player.tierce().active, "the run is recorded on the player");
|
||||
check(player.tierce().stage == 0, "on its first node");
|
||||
check(player.tierce().roundsLeft == 45, "with the whole cycle pool");
|
||||
check(player.location().entryId == floor->mapEntranceId,
|
||||
"and the first node is the floor's own arena");
|
||||
|
||||
// The third node is the one the tierce row plants, not one of the floor's two.
|
||||
player.tierce().stage = 2;
|
||||
game::ChallengeArena storage;
|
||||
const game::ChallengeArena* arena = game::tierce::arena(player, storage);
|
||||
check(arena != nullptr && arena->mazeGroupId == config->mazeGroupId,
|
||||
"the last node fights in the tierce's own group");
|
||||
check(arena != nullptr && arena->monsters == &config->monsters,
|
||||
"with the tierce's own monster");
|
||||
player.tierce().stage = 0;
|
||||
|
||||
// What the node brings to a fight comes from both rows.
|
||||
game::BattleRequest request;
|
||||
game::tierce::prepareBattle(player, request);
|
||||
check(request.party.size() == 1, "the node fights with its own team");
|
||||
check(request.mazeBuffId == floor->mazeBuffId, "under the floor's maze buff");
|
||||
check(request.stageBuffId == 1000, "and the buff picked for this node");
|
||||
check(request.roundsLimit == 45, "with the cycles left in the pool");
|
||||
|
||||
// Starting again keeps the spot the first attempt left from.
|
||||
game::SceneLocation origin = player.tierce().origin;
|
||||
proto::SceneInfo again;
|
||||
check(game::tierce::start(player, 5213, false, 0, stages, again) == 0, "a retry starts");
|
||||
check(player.tierce().origin.entryId == origin.entryId, "and goes back to the same place");
|
||||
|
||||
// Every floor with a third node shows up in the history, under the tierce's id.
|
||||
proto::GetChallengeTierceDataScRsp history = game::tierce::history(player);
|
||||
bool found = false;
|
||||
for (const proto::ChallengeTierceData& entry : history.challenge_info_list) {
|
||||
if (entry.challenge_id != 5213) continue;
|
||||
found = true;
|
||||
check(entry.stage_info_list.size() == 3, "with a slot for each of its three nodes");
|
||||
}
|
||||
check(found, "the floor is in the history");
|
||||
}
|
||||
|
||||
// "auto" hands a calyx over to the srtools build and leaves every other fight on the
|
||||
// stage the client named. See gameplay.battle_source.
|
||||
void testBattleSourceSplit() {
|
||||
if (!data::Tables::get().loaded()) return;
|
||||
if (core::Config::get().gameplay.battleSource != "auto") return;
|
||||
|
||||
game::Player player = makeTestPlayer();
|
||||
game::Roster roster = player.roster();
|
||||
const game::BattleConfig& build = roster.data().battle;
|
||||
if (build.waves.empty() || build.stageId == 0) {
|
||||
std::printf("SKIP battle source split (the build on disk names no fight)\n");
|
||||
return;
|
||||
}
|
||||
const data::CocoonInfo* cocoon = data::Tables::get().cocoon(1001, player.worldLevel());
|
||||
if (cocoon == nullptr || cocoon->stageIds.empty()) {
|
||||
std::printf("SKIP battle source split (calyx 1001 is not in the tables)\n");
|
||||
return;
|
||||
}
|
||||
uint32_t calyxStage = cocoon->stageIds.front();
|
||||
check(calyxStage != build.stageId, "the calyx and the build name different stages");
|
||||
|
||||
game::BattleRequest calyx;
|
||||
calyx.cocoonId = 1001;
|
||||
calyx.wave = 1;
|
||||
calyx.stageIds.push_back(calyxStage);
|
||||
calyx.allowSrToolsOverride = true;
|
||||
proto::SceneBattleInfo run = game::battle::create(player, calyx);
|
||||
check(run.stage_id == build.stageId, "a calyx fights the srtools build");
|
||||
|
||||
game::BattleRequest shadow;
|
||||
shadow.stageIds.push_back(calyxStage);
|
||||
proto::SceneBattleInfo other = game::battle::create(player, shadow);
|
||||
check(other.stage_id == calyxStage, "every other fight keeps its own stage");
|
||||
}
|
||||
|
||||
// Writing an equip change back must not drop the parts of the build we do not
|
||||
// model -- the loadout presets are the user's, not ours.
|
||||
void testSrToolsRoundTrip() {
|
||||
@@ -672,6 +802,9 @@ void runGameTests() {
|
||||
std::string root = util::findRootDir("config/config.json");
|
||||
if (!root.empty()) util::setCurrentDir(root);
|
||||
core::Config::get().load("config/config.json");
|
||||
// Anything that saves goes to a scratch file: a test must never write the save a
|
||||
// real client is using.
|
||||
core::Config::get().paths.playerFile = "build/test-player.json";
|
||||
data::Tables::get().load(core::Config::get().paths.dataSources);
|
||||
data::SceneRes::get().load(core::Config::get().paths.sceneRes);
|
||||
data::SceneRes::get().loadAnchors(core::Config::get().paths.anchors);
|
||||
@@ -688,8 +821,10 @@ void runGameTests() {
|
||||
testRosterProtos();
|
||||
testSceneBuild();
|
||||
testChallengeRun();
|
||||
testTierceRun();
|
||||
testEveryScenePacketFits();
|
||||
testBattleBuild();
|
||||
testBattleSourceSplit();
|
||||
testPropInteraction();
|
||||
testSrToolsRoundTrip();
|
||||
}
|
||||
Reference in new issue
Block a user