There is a bug in the JSON library which causes the initializer list constructor to inconsistently create either an array or a scalar value. This bug causes inconsistent behavoir between the PC and WASM builds. See https://json.nlohmann.me/home/faq/#known-bugs
1531 lines
56 KiB
C++
1531 lines
56 KiB
C++
/*@~@~@ C A K E F O O T <presented by> 💫dank.game💫
|
|
|~)~)~)
|
|
|\~*~*| Licensed under the zlib and CC-BY licenses. Source is available at
|
|
|\\~*~|
|
|
,~|#\\~*|~, <https://open.shampoo.ooo/shampoo/cakefoot>
|
|
: \\@\\~| :
|
|
: \\#\\| : Created with the SPACE🪐BOX engine for cross-platform PC, WebGL and mobile games
|
|
: \\@\' :
|
|
: \\/ : <https://open.shampoo.ooo/shampoo/spacebox>
|
|
`~ ~ ~`~ */
|
|
|
|
#include "test/setup.hpp"
|
|
#include "filesystem.hpp"
|
|
#include "../Cakefoot.hpp"
|
|
#include "../Replay.hpp"
|
|
|
|
nlohmann::json mock_preferences_json = R"({
|
|
"config": {
|
|
"fullscreen": true,
|
|
"use display button": false
|
|
}})"_json;
|
|
|
|
nlohmann::json mock_stats_json = R"({
|
|
"achievements": [],
|
|
"config": {},
|
|
"stats": {
|
|
"STAT_CONSECUTIVE_DAYS_PLAYED": 7.0,
|
|
"STAT_WARPED_LEVELS_BEATEN": 21
|
|
}
|
|
})"_json;
|
|
|
|
nlohmann::json mock_progress_json = R"({
|
|
"progress": {
|
|
"all time bank": "--++-+----------------",
|
|
"arcade bank": 0,
|
|
"arcade checkpoint": 0.0,
|
|
"arcade difficulty": 0,
|
|
"arcade level": 1,
|
|
"arcade max distance": 0,
|
|
"arcade time": 0.0,
|
|
"current challenge": 0,
|
|
"current difficulty": 0,
|
|
"current level": 7,
|
|
"current view": 2,
|
|
"jackpot": 0,
|
|
"max difficulty": 0,
|
|
"max level": 7,
|
|
"max view": 2,
|
|
"quest bank": "--++-+----------------",
|
|
"quest best": 0.0,
|
|
"quest checkpoint": 0,
|
|
"quest deaths": 1,
|
|
"quest difficulty": 0,
|
|
"quest level": 7,
|
|
"quest time": 9999.934000015258789,
|
|
"total time": 9999.934000015258789,
|
|
"warped": "+++++++++++++++++++++-"
|
|
}
|
|
})"_json;
|
|
|
|
nlohmann::json mock_deprecated_progress_json = R"({
|
|
"progress": {
|
|
"arcade bank": "-++++++-+-----+---++-+",
|
|
"arcade checkpoint": 0,
|
|
"arcade coin": false,
|
|
"arcade difficulty": 0,
|
|
"arcade level": 8,
|
|
"arcade max distance": 14036,
|
|
"arcade time": 91.6259937286377,
|
|
"current challenge": 0,
|
|
"current difficulty": 2,
|
|
"current level": 11,
|
|
"current view": 0,
|
|
"deaths": 3583,
|
|
"jackpot": 0,
|
|
"max challenge": 5,
|
|
"max difficulty": 2,
|
|
"max level": 11,
|
|
"max view": 0,
|
|
"quest bank": "++++++++++------------",
|
|
"quest best": 4214.365234375,
|
|
"quest checkpoint": 0,
|
|
"quest coin": false,
|
|
"quest difficulty": 2,
|
|
"quest level": 11,
|
|
"quest time": 987.0749969482422,
|
|
"total time": 26409.425424814224
|
|
}})"_json;
|
|
|
|
nlohmann::json mock_zero_arcade_bank_json = R"({
|
|
"progress": {
|
|
"arcade bank": 0,
|
|
"quest bank": "++++++++++------------"
|
|
}})"_json;
|
|
|
|
nlohmann::json mock_zero_quest_bank_json = R"({
|
|
"progress": {
|
|
"arcade bank": "-++++++-+-----+---++-+",
|
|
"quest bank": 0
|
|
}})"_json;
|
|
|
|
nlohmann::json mock_scores_json = R"([
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 14036,
|
|
"name": "ZZZ",
|
|
"time": 5.3380126953125
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 14036,
|
|
"name": "ZZZ",
|
|
"time": 305.3380126953125
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 1912,
|
|
"name": "UAA",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 1337,
|
|
"name": "UAA",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 777,
|
|
"name": "UAA",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 420,
|
|
"name": "UAA",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 49,
|
|
"name": "UWU",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:58",
|
|
"distance": 47,
|
|
"name": "BUB",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 35,
|
|
"name": "TOE",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 19,
|
|
"name": "AAA",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 19,
|
|
"name": "EGG",
|
|
"time": 0.0
|
|
},
|
|
{
|
|
"date": "2024/10/16 18:57",
|
|
"distance": 0,
|
|
"name": "AAA",
|
|
"time": 0.0
|
|
}
|
|
])"_json;
|
|
|
|
/* Use temporary directory to create mock paths for mock save-data JSON */
|
|
std::shared_ptr<fs::path> mock_preferences_path { sb::test::temp_path("cakefoot_mock_preferences.json") };
|
|
std::shared_ptr<fs::path> mock_stats_path { sb::test::temp_path("cakefoot_mock_stats.json") };
|
|
std::shared_ptr<fs::path> mock_progress_path { sb::test::temp_path("cakefoot_mock_progress.json") };
|
|
std::shared_ptr<fs::path> mock_deprecated_progress_path { sb::test::temp_path("cakefoot_mock_deprecated_progress.json") };
|
|
std::shared_ptr<fs::path> mock_scores_path { sb::test::temp_path("cakefoot_mock_scores.json") };
|
|
std::shared_ptr<fs::path> mock_replays_path { sb::test::temp_path("cakefoot_mock_replays") };
|
|
|
|
/* Mock paths for non-existing files */
|
|
std::shared_ptr<fs::path> mock_empty_stats_path { sb::test::temp_path("cakefoot_mock_empty_stats.json") };
|
|
std::shared_ptr<fs::path> mock_empty_progress_path { sb::test::temp_path("cakefoot_mock_empty_progress.json") };
|
|
std::shared_ptr<fs::path> mock_empty_scores_path { sb::test::temp_path("cakefoot_mock_empty_scores.json") };
|
|
std::shared_ptr<fs::path> mock_empty_preferences_path { sb::test::temp_path("cakefoot_mock_empty_preferences.json") };
|
|
|
|
/* Mock paths for testing bank parses */
|
|
std::shared_ptr<fs::path> mock_zero_arcade_bank_path { sb::test::temp_path("cakefoot_mock_zero_arcade_bank.json") };
|
|
std::shared_ptr<fs::path> mock_zero_quest_bank_path { sb::test::temp_path("cakefoot_mock_zero_quest_bank.json") };
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
Catch::Session session;
|
|
|
|
/* Set up the CLI and reporters */
|
|
sb::test::setup(session, argc, argv);
|
|
|
|
/* Rig the stats so the last time stamp was recent and the day will not match today */
|
|
std::chrono::time_point<std::chrono::system_clock> now { std::chrono::system_clock::now() };
|
|
mock_stats_json["timestamp"] = sb::time::epoch_minutes(now - std::chrono::hours(23));
|
|
mock_stats_json["day"] = "ฅ^•ﻌ•^ฅ";
|
|
|
|
/* Write mock data to temporary files */
|
|
sb::test::write_json(*mock_preferences_path, mock_preferences_json);
|
|
sb::test::write_json(*mock_progress_path, mock_progress_json);
|
|
sb::test::write_json(*mock_deprecated_progress_path, mock_deprecated_progress_json);
|
|
sb::test::write_json(*mock_stats_path, mock_stats_json);
|
|
sb::test::write_json(*mock_scores_path, mock_scores_json);
|
|
sb::test::write_json(*mock_zero_arcade_bank_path, mock_zero_arcade_bank_json);
|
|
sb::test::write_json(*mock_zero_quest_bank_path, mock_zero_quest_bank_json);
|
|
|
|
return session.run();
|
|
}
|
|
|
|
class MockCakefoot
|
|
{
|
|
|
|
private:
|
|
|
|
class NoClipCakefoot : public Cakefoot
|
|
{
|
|
public:
|
|
NoClipCakefoot(bool noclip, std::vector<nlohmann::json> configs) : Cakefoot(configs)
|
|
{
|
|
this->noclip = noclip;
|
|
}
|
|
|
|
template<typename Clock>
|
|
void mock_validate_date(std::chrono::time_point<Clock> time)
|
|
{
|
|
validate_date(time);
|
|
}
|
|
|
|
const MultiBGM& mock_bgm()
|
|
{
|
|
return bgm;
|
|
}
|
|
|
|
const std::map<std::string, sb::audio::Chunk>& mock_audio()
|
|
{
|
|
return audio;
|
|
}
|
|
|
|
void mock_reconfig()
|
|
{
|
|
reconfig();
|
|
}
|
|
|
|
float mock_credits()
|
|
{
|
|
return credits;
|
|
}
|
|
|
|
bool mock_operator_menu_active()
|
|
{
|
|
return operator_menu_active;
|
|
}
|
|
|
|
std::vector<cakefoot::Replay> mock_replays(int level)
|
|
{
|
|
return replays(level);
|
|
}
|
|
|
|
void mock_store_replay_recording(cakefoot::Replay& replay, int level)
|
|
{
|
|
store_replay_recording(replay, level);
|
|
}
|
|
};
|
|
|
|
public:
|
|
|
|
NoClipCakefoot cakefoot;
|
|
sb::progress::Stats stats;
|
|
sb::progress::Achievements achievements;
|
|
sb::progress::Progress stat_progress;
|
|
sb::progress::Progress progress;
|
|
sb::progress::Progress preferences;
|
|
|
|
MockCakefoot(
|
|
fs::path progress, fs::path scores, fs::path preferences, fs::path stats, nlohmann::json merge = {},
|
|
bool noclip = true
|
|
) :
|
|
cakefoot(NoClipCakefoot { noclip, { merge, nlohmann::json({
|
|
{ "display", {{ "use play button", false }} },
|
|
|
|
/* Uncomment the next line to get log output on the console */
|
|
// { "log", {{ "stdout enabled", true }, {"debug to stdout", false }} },
|
|
|
|
{ "storage", {
|
|
{"progress file", progress},
|
|
{"scores file", scores},
|
|
{"preferences file", preferences},
|
|
{"stats file", stats},
|
|
{"stats write frequency", 2.0f}}},
|
|
|
|
{ "replay", {{ "directory", *mock_replays_path }} }
|
|
})}}),
|
|
stats(sb::progress::Stats { cakefoot.configuration() }),
|
|
achievements(sb::progress::Achievements { cakefoot.configuration() })
|
|
{
|
|
refresh_progress();
|
|
}
|
|
|
|
void refresh_progress()
|
|
{
|
|
stat_progress.load(cakefoot.configuration()("storage", "stats file").get<fs::path>());
|
|
if (fs::exists(cakefoot.configuration()("storage", "progress file").get<fs::path>()))
|
|
{
|
|
progress.load(cakefoot.configuration()("storage", "progress file").get<fs::path>());
|
|
}
|
|
if (fs::exists(cakefoot.configuration()("storage", "preferences file").get<fs::path>()))
|
|
{
|
|
preferences.load(cakefoot.configuration()("storage", "preferences file").get<fs::path>());
|
|
}
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* Run a full quest followed by a full arcade run, until the end screen, by auto pressing buttons on the title screen.
|
|
* Check title screen menu values in the process. If count is 1 instead of the default of 2, only run the quest run.
|
|
*/
|
|
void mock_full_runs(MockCakefoot& mock, int count = 2)
|
|
{
|
|
bool checked_challenges = false;
|
|
bool checked_pause = false;
|
|
int resets = 0;
|
|
int downs = 0;
|
|
std::size_t toggles = 0;
|
|
std::size_t toggle_target = 0;
|
|
const nlohmann::json& challenge_list = mock.cakefoot.configuration()("challenge");
|
|
mock.cakefoot.run([&](float timestamp){
|
|
if (mock.cakefoot.configuration()("progress", "quest deaths") > 2)
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
mock.refresh_progress();
|
|
std::size_t level = mock.cakefoot.configuration()("progress", "current level");
|
|
int progress_challenge = mock.cakefoot.configuration()("progress", "current challenge");
|
|
if (!checked_challenges)
|
|
{
|
|
if (downs++ == 0)
|
|
{
|
|
/* Move to challenge increment button */
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("right");
|
|
|
|
/* Set toggle target to the amount of toggles to do a complete loop around the menu */
|
|
if (challenge_list.at(progress_challenge).at("name") == "RESUME QUEST")
|
|
{
|
|
toggle_target = challenge_list.size() - 1;
|
|
}
|
|
else if (challenge_list.at(progress_challenge).at("name") == "NEW QUEST")
|
|
{
|
|
toggle_target = challenge_list.size() - 3;
|
|
}
|
|
else
|
|
{
|
|
CAPTURE(challenge_list.at(progress_challenge).at("name"));
|
|
FAIL("Unsupported initial test conditions or menu code has failed");
|
|
}
|
|
|
|
/* Try posting the operator event to make sure it doesn't do anything. */
|
|
sb::Delegate::post("operator");
|
|
}
|
|
else if (toggles == 0)
|
|
{
|
|
/* Toggle to arcade mode, then start counting toggles */
|
|
if (challenge_list.at(progress_challenge).at("name") == "ARCADE")
|
|
{
|
|
toggles++;
|
|
}
|
|
sb::Delegate::post("any");
|
|
|
|
/* Make sure operator mode wasn't triggered */
|
|
CHECK_FALSE(mock.cakefoot.mock_operator_menu_active());
|
|
}
|
|
else if (toggles < toggle_target)
|
|
{
|
|
if (toggles == 2 || toggles == 3)
|
|
{
|
|
/* This is expected to not move the cursor to make sure the start button is disabled */
|
|
sb::Delegate::post("up");
|
|
}
|
|
|
|
/* Toggle until reaching arcade mode */
|
|
if (challenge_list.at(progress_challenge).at("name") != "ARCADE")
|
|
{
|
|
toggles++;
|
|
sb::Delegate::post("any");
|
|
}
|
|
}
|
|
else if (toggles == toggle_target)
|
|
{
|
|
/* One full loop exactly around the menu should have ended at arcade mode */
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "ARCADE");
|
|
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
|
CHECK_FALSE(mock.cakefoot.mock_bgm().playing());
|
|
|
|
/* Do five toggles to wrap around to quest mode */
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
toggles += 5;
|
|
}
|
|
else
|
|
{
|
|
/* Check if quest mode is active */
|
|
if (toggle_target == challenge_list.size() - 1)
|
|
{
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME QUEST");
|
|
}
|
|
else if (toggle_target == challenge_list.size() - 3)
|
|
{
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "NEW QUEST");
|
|
}
|
|
|
|
/* Start the quest */
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("any");
|
|
checked_challenges = true;
|
|
toggles = 0;
|
|
downs = 0;
|
|
}
|
|
}
|
|
|
|
/* Check if a run is happening on cake difficulty */
|
|
else if (mock.cakefoot.configuration()("progress", "max difficulty") < 1)
|
|
{
|
|
/* Skip levels until the final level */
|
|
if (level < mock.cakefoot.configuration()("levels").size() - 2)
|
|
{
|
|
const nlohmann::json& world = mock.cakefoot.configuration()("world");
|
|
for (std::size_t ii = 0; ii < world.size(); ii++)
|
|
{
|
|
if (ii == world.size() - 1 || level < world[ii + 1].at("start"))
|
|
{
|
|
CHECK(mock.cakefoot.mock_bgm().playing());
|
|
CHECK(mock.cakefoot.mock_bgm().index() == ii);
|
|
break;
|
|
}
|
|
}
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME QUEST");
|
|
CHECK(mock.cakefoot.configuration()("progress", "quest level") == level);
|
|
sb::Delegate::post("skip forward");
|
|
}
|
|
|
|
/* Post a pause event to make sure it doesn't crash the game. The "any" event will close the pause menu
|
|
* if it is working correctly. */
|
|
else if (!checked_pause)
|
|
{
|
|
sb::Delegate::post("pause");
|
|
checked_pause = true;
|
|
}
|
|
|
|
/* Accelerate character */
|
|
else
|
|
{
|
|
sb::Delegate::post("any");
|
|
}
|
|
}
|
|
else if (resets < 1)
|
|
{
|
|
if (count < 2)
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
sb::Delegate::post("reset");
|
|
resets++;
|
|
}
|
|
}
|
|
else if (downs++ == 0)
|
|
{
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "NEW QUEST");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("right");
|
|
}
|
|
else if (toggles++ < 2)
|
|
{
|
|
sb::Delegate::post("any");
|
|
if (toggles == 2)
|
|
{
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("any");
|
|
}
|
|
}
|
|
else if (!mock.stat_progress.achievement_unlocked(mock.achievements["ACH_HOTCAKES"]))
|
|
{
|
|
if (mock.cakefoot.configuration()("progress", "arcade level") <
|
|
mock.cakefoot.configuration()("levels").size() - 2)
|
|
{
|
|
if (level > 0)
|
|
{
|
|
CHECK(challenge_list.at(progress_challenge).at("name") == "RESUME ARCADE");
|
|
CHECK(mock.cakefoot.configuration()("progress", "arcade level") == level);
|
|
}
|
|
sb::Delegate::post("skip forward");
|
|
}
|
|
else
|
|
{
|
|
sb::Delegate::post("any");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
}
|
|
mock.cakefoot.draw(timestamp);
|
|
});
|
|
}
|
|
|
|
TEST_CASE("Empty storage")
|
|
{
|
|
/* Create a game with no progress or existing stats. */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path
|
|
};
|
|
|
|
/* Confirm that only one stat and achievement were written */
|
|
CHECK(mock.stat_progress.read("stats").size() == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_CONSECUTIVE_DAYS_PLAYED"]) == 1);
|
|
CHECK(mock.stat_progress.achievement_count() >= 1);
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EMPTY_STOMACH"]));
|
|
|
|
/* Force unlock date-based achievements */
|
|
std::tm birthday_tm;
|
|
std::istringstream ss("2024-05-10 00:00:00");
|
|
std::string format = "%Y-%m-%d %H:%M:%S";
|
|
ss >> std::get_time(&birthday_tm, format.c_str());
|
|
birthday_tm.tm_isdst = true;
|
|
std::chrono::time_point<std::chrono::system_clock> birthday {
|
|
std::chrono::system_clock::from_time_t(std::mktime(&birthday_tm))
|
|
};
|
|
std::tm cake_day_tm;
|
|
ss = std::istringstream("2024-11-26 00:00:00");
|
|
ss >> std::get_time(&cake_day_tm, format.c_str());
|
|
std::chrono::time_point<std::chrono::system_clock> cake_day {
|
|
std::chrono::system_clock::from_time_t(std::mktime(&cake_day_tm))
|
|
};
|
|
mock.cakefoot.mock_validate_date(birthday);
|
|
mock.cakefoot.mock_validate_date(cake_day);
|
|
|
|
#if !defined(__EMSCRIPTEN__)
|
|
/* Launch a game */
|
|
mock_full_runs(mock);
|
|
|
|
/* Stats are written when quit is called */
|
|
mock.cakefoot.quit();
|
|
mock.refresh_progress();
|
|
|
|
/* Check achievements */
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_PIECE_OF_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EASY_AS_PIE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_TAKES_THE_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKEWALK"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_MOIST"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_KNEAD_FOR_SPEED"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKEFEAT"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_HOTCAKES"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_SLICE_OF_LIFE"]));
|
|
CHECK_FALSE(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_WHISKED"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_BIRTHDAY_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKE_MY_DAY"]));
|
|
|
|
/* Check stats */
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_FASTEST_QUEST_TIME"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FASTEST_QUEST_TIME"]) > 10.0f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FASTEST_QUEST_TIME"]) < 30.0f);
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_BEST_ARCADE_CLOCK"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_BEST_ARCADE_CLOCK"]) > 700.0f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_BEST_ARCADE_CLOCK"]) < 750.0f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_CHECKPOINTS_REACHED"]) == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_ARCADE_RUNS"]) == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FARTHEST_ARCADE_DISTANCE"]) == 14'036);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FARTHEST_DISTANCE_REACHED"]) == 14'036);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_QUESTS_COMPLETED"]) == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_LEVELS_UNLOCKED"]) == 22);
|
|
CHECK_FALSE(mock.stat_progress.stat_exists(mock.stats["STAT_WARPED_LEVELS_BEATEN"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_ARCADE_RUNS_STARTED"]) == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_GAMES_STARTED"]) == 2);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_POWER_ON_TIME"]) > 10.0f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_PLAY_TIME"]) > 15.0f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_POWER_ON_TIME"]) >
|
|
mock.stat_progress.stat_value(mock.stats["STAT_PLAY_TIME"]));
|
|
|
|
/* Check progress */
|
|
CHECK(mock.progress.read("progress", "quest deaths").get<int>() == 0);
|
|
CHECK(mock.progress.read("progress", "arcade deaths").get<int>() == 0);
|
|
|
|
/* Check replay data */
|
|
CHECK(fs::exists(*mock_replays_path));
|
|
CHECK_FALSE(fs::is_empty(*mock_replays_path));
|
|
std::size_t count = 0;
|
|
for (const fs::directory_entry& entry : fs::directory_iterator(*mock_replays_path))
|
|
{
|
|
/* Make sure only one file is in the directory */
|
|
CHECK(count++ < 1);
|
|
|
|
/* Open the replay as JSON and make sure the first key frame is located at the start of level 22 */
|
|
CHECK(entry.is_regular_file());
|
|
nlohmann::json json = sb::json_from_file(entry.path());
|
|
CHECK(json.contains("key frames"));
|
|
CHECK(json.at("key frames").at(0).at(2) == 5'637);
|
|
}
|
|
#endif
|
|
|
|
/* Delete progress files, so they won't affect the next test */
|
|
fs::remove(*mock_empty_stats_path);
|
|
fs::remove(*mock_empty_progress_path);
|
|
fs::remove(*mock_empty_preferences_path);
|
|
fs::remove(*mock_empty_scores_path);
|
|
fs::remove_all(*mock_replays_path);
|
|
}
|
|
|
|
#if !defined(__EMSCRIPTEN__)
|
|
|
|
TEST_CASE("D'ohnut")
|
|
{
|
|
/* Create a game with no progress or existing stats, and override the config. */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path,
|
|
{},
|
|
false
|
|
};
|
|
|
|
/* Mock the D'ohnut achievement so it happens at any point after the beginning of the line */
|
|
mock.cakefoot.configuration()["achievements"][4]["goal"] = 0.01f;
|
|
mock.cakefoot.mock_reconfig();
|
|
|
|
/* Force another birthday unlock to check a date after May 10th as well */
|
|
std::tm birthday_tm;
|
|
std::istringstream ss("2024-05-31 00:00:00");
|
|
std::string format = "%Y-%m-%d %H:%M:%S";
|
|
ss >> std::get_time(&birthday_tm, format.c_str());
|
|
birthday_tm.tm_isdst = true;
|
|
std::chrono::time_point<std::chrono::system_clock> birthday {
|
|
std::chrono::system_clock::from_time_t(std::mktime(&birthday_tm))
|
|
};
|
|
mock.cakefoot.mock_validate_date(birthday);
|
|
|
|
/* Run until three deaths */
|
|
mock_full_runs(mock, 1);
|
|
|
|
/* Quit game and write save progress */
|
|
mock.cakefoot.quit();
|
|
|
|
/* Check results */
|
|
mock.refresh_progress();
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_BIRTHDAY_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_DOHNUT"]));
|
|
CHECK(mock.progress.read("progress", "quest deaths").get<int>() == 3);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_TOTAL_DEATHS"], mock.stats) == 3);
|
|
|
|
/* Delete progress files, so they won't affect the next test */
|
|
fs::remove(*mock_empty_stats_path);
|
|
fs::remove(*mock_empty_progress_path);
|
|
}
|
|
|
|
#endif
|
|
|
|
TEST_CASE("In-progress")
|
|
{
|
|
/* Create a game with an in-progress quest. */
|
|
MockCakefoot mock {
|
|
*mock_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_stats_path
|
|
};
|
|
|
|
/* Check initial stats after construction */
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EMPTY_STOMACH"]));
|
|
|
|
#if !defined(__EMSCRIPTEN__)
|
|
/* One full quest run with existing progress */
|
|
mock_full_runs(mock, 1);
|
|
|
|
/* Stats are written when quit is called */
|
|
mock.cakefoot.quit();
|
|
mock.refresh_progress();
|
|
|
|
/* Check achievements */
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_PIECE_OF_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EASY_AS_PIE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_TAKES_THE_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKEWALK"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_MOIST"]));
|
|
CHECK_FALSE(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_SLICE_OF_LIFE"]));
|
|
CHECK_FALSE(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_KNEAD_FOR_SPEED"]));
|
|
CHECK_FALSE(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKEFEAT"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_WHISKED"]));
|
|
|
|
/* Check stats */
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FASTEST_QUEST_TIME"]) > 9999.9f);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FARTHEST_DISTANCE_REACHED"]) == 14'036);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_QUESTS_COMPLETED"]) == 1);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_LEVELS_UNLOCKED"]) == 22);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_WARPED_LEVELS_BEATEN"]) == 22);
|
|
|
|
/* Check progress */
|
|
CHECK(mock.progress.read("progress", "warped").get<std::string>() == "++++++++++++++++++++++");
|
|
#endif
|
|
|
|
/* Reset progress files, so they won't affect the next test */
|
|
fs::remove(*mock_stats_path);
|
|
sb::test::write_json(*mock_stats_path, mock_stats_json);
|
|
fs::remove(*mock_empty_progress_path);
|
|
}
|
|
|
|
TEST_CASE("Zero arcade bank")
|
|
{
|
|
/* Create a game with coins saved in the quest bank and an empty arcade bank. */
|
|
MockCakefoot mock {
|
|
*mock_zero_arcade_bank_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path
|
|
};
|
|
|
|
/* Check the contents */
|
|
CHECK(mock.stat_progress.read("stats").size() == 3);
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_COINS_COLLECTED"]));
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_COINS_UNLOCKED"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_COLLECTED"]) == 10);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_UNLOCKED"]) == 10);
|
|
CHECK(mock.cakefoot.configuration()("progress", "all time bank") == "++++++++++------------");
|
|
|
|
/* Quit so another game can launch */
|
|
mock.cakefoot.quit();
|
|
|
|
/* Delete stats file, so it won't affect the next test */
|
|
fs::remove(*mock_empty_stats_path);
|
|
}
|
|
|
|
TEST_CASE("Zero quest bank")
|
|
{
|
|
/* Create a game with coins saved in the arcade bank and an empty quest bank. */
|
|
MockCakefoot mock {
|
|
*mock_zero_quest_bank_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path
|
|
};
|
|
|
|
/* Check the contents */
|
|
CHECK(mock.stat_progress.read("stats").size() == 3);
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_COINS_COLLECTED"]));
|
|
CHECK(mock.stat_progress.stat_exists(mock.stats["STAT_COINS_UNLOCKED"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_COLLECTED"]) == 11);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_UNLOCKED"]) == 11);
|
|
CHECK(mock.cakefoot.configuration()("progress", "all time bank") == "-++++++-+-----+---++-+");
|
|
|
|
/* Quit so another game can launch */
|
|
mock.cakefoot.quit();
|
|
|
|
/* Delete stats file, so it won't affect the next test */
|
|
fs::remove(*mock_empty_stats_path);
|
|
}
|
|
|
|
TEST_CASE("Estimate stats")
|
|
{
|
|
/* Create a game with deprecated progress file and no existing stats file. */
|
|
MockCakefoot mock {
|
|
*mock_deprecated_progress_path,
|
|
*mock_scores_path,
|
|
*mock_preferences_path,
|
|
*mock_empty_stats_path
|
|
};
|
|
|
|
/* Check the contents */
|
|
REQUIRE(mock.stat_progress.stat_exists(mock.stats["STAT_SLICER_DEATHS"]));
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_SLICER_DEATHS"]) == 895);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_TOTAL_DEATHS"], mock.stats) == 4 * 895);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_DISTANCE_TRAVELED"]) == 2 * 20'000 + 10 * 800 + 32'687);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_ARCADE_RUNS"]) == 12);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FARTHEST_ARCADE_DISTANCE"]) == 14'036);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FARTHEST_DISTANCE_REACHED"]) == 14'036);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_QUESTS_COMPLETED"]) == 2);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_CAKES_UNLOCKED"]) == 2);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_FASTEST_QUEST_TIME"]) == 4'214.365234375);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_BEST_ARCADE_CLOCK"]) == 305.3380126953125);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_LEVELS_UNLOCKED"]) == 22);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_COLLECTED"]) == 21);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_COINS_UNLOCKED"]) == 14);
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_CONSECUTIVE_DAYS_PLAYED"]) == 1);
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EMPTY_STOMACH"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_LAYER_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_FRESHLY_BAKED"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_FORKED_UP"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_OVERCOOKED"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_BAKERS_DOZEN"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_HOTCAKES"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_CAKEWALK"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_MOIST"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_SUPER_MOIST"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_PIECE_OF_CAKE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_EASY_AS_PIE"]));
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_TAKES_THE_CAKE"]));
|
|
CHECK(mock.cakefoot.configuration()("progress", "all time bank") == "++++++++++----+---++-+");
|
|
|
|
/* Quit so another game can launch */
|
|
mock.cakefoot.quit();
|
|
}
|
|
|
|
TEST_CASE("Existing stats")
|
|
{
|
|
/* Create a game with existing stats. */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_scores_path,
|
|
*mock_preferences_path,
|
|
*mock_stats_path
|
|
};
|
|
|
|
/* Launch a game */
|
|
mock.cakefoot.run([&](float timestamp){
|
|
if (timestamp > 10.0f)
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
mock.cakefoot.draw(timestamp);
|
|
}
|
|
});
|
|
|
|
/* Check that a consecutive day was added */
|
|
mock.refresh_progress();
|
|
CHECK(mock.stat_progress.stat_value(mock.stats["STAT_CONSECUTIVE_DAYS_PLAYED"]) == 8);
|
|
CHECK(mock.stat_progress.achievement_unlocked(mock.achievements["ACH_C8KE"]));
|
|
|
|
/* Quit so another game can launch */
|
|
mock.cakefoot.quit();
|
|
}
|
|
|
|
TEST_CASE("Idle timeout")
|
|
{
|
|
/* Mock demo and arcade-only modes */
|
|
for (const std::string mode : {"demo", "arcade"})
|
|
{
|
|
/* Fresh installation. */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path
|
|
};
|
|
|
|
/* Mock a quick idle timeout. */
|
|
mock.cakefoot.configuration()[mode]["idle timeout"] = 2.0f;
|
|
mock.cakefoot.configuration()[mode]["countdown display timeout"] = 2.0f;
|
|
if (mode == "demo")
|
|
{
|
|
mock.cakefoot.configuration()["demo"]["active"] = true;
|
|
}
|
|
else
|
|
{
|
|
mock.cakefoot.configuration()["arcade"]["arcade only"] = true;
|
|
}
|
|
|
|
/* Launch */
|
|
bool started = false;
|
|
bool button_released = false;
|
|
mock.cakefoot.run([&](float timestamp){
|
|
/* Automatically fail if running for more than 15 seconds */
|
|
if (timestamp > 15.0f)
|
|
{
|
|
CAPTURE(mode);
|
|
FAIL("Idle timeout test failed");
|
|
}
|
|
else
|
|
{
|
|
if (!started)
|
|
{
|
|
/* Start game */
|
|
sb::Delegate::post("any");
|
|
started = true;
|
|
}
|
|
else if (!button_released)
|
|
{
|
|
/* Stop moving */
|
|
sb::Delegate::post("any", true);
|
|
button_released = true;
|
|
}
|
|
|
|
/* Use the currently playing audio to determine if the main menu has been returned to, confirming idle
|
|
* reset. */
|
|
else if (mock.cakefoot.mock_audio().at("menu").playing() && !mock.cakefoot.mock_bgm().playing())
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
mock.cakefoot.draw(timestamp);
|
|
}
|
|
});
|
|
mock.cakefoot.quit();
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Credits")
|
|
{
|
|
/* Test five different configurations for credits */
|
|
for (const nlohmann::json& params : nlohmann::json({
|
|
{ "four", true, 4.0f, 1.0f, 0.0f },
|
|
{ "one", true, 1.0f, 1.0f, 0.0f },
|
|
{ "double", true, 50.5f, 25.25f, 0.0f },
|
|
{ "max", true, 2.0f, 1.0f, 3.0f },
|
|
{ "none", false, 4.0f, 1.0f, 0.0f } }))
|
|
{
|
|
/* Mock arcade cabinet version requiring credits to play (other than the last configuration). */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path,
|
|
{{ "arcade",
|
|
{
|
|
{ "arcade only", true },
|
|
{ "credits enabled", params[1] },
|
|
{ "idle timeout", 2.0f },
|
|
{ "countdown display timeout", 2.0f },
|
|
{ "credits required", params[2] },
|
|
{ "credit increase per event", params[3] },
|
|
{ "max credits", params[4] }
|
|
} } }
|
|
};
|
|
|
|
/* Launch */
|
|
bool running = false;
|
|
std::optional<float> run_start;
|
|
int try_count = 0;
|
|
mock.cakefoot.run([&](float timestamp){
|
|
/* Automatically fail if running for more than 10 seconds */
|
|
if (running && timestamp - *run_start > 10.0f)
|
|
{
|
|
CAPTURE(params);
|
|
FAIL("Credits test failed");
|
|
}
|
|
else if (!running)
|
|
{
|
|
running = true;
|
|
run_start = timestamp;
|
|
}
|
|
else
|
|
{
|
|
if (try_count == 0)
|
|
{
|
|
/* Always start by trying to start the game */
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
try_count++;
|
|
}
|
|
else if (try_count == 1)
|
|
{
|
|
if (params[0] == "none")
|
|
{
|
|
/* If there was no credit requirement, the game should have begun. */
|
|
CHECK(mock.cakefoot.mock_bgm().playing());
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
/* Make sure the game didn't start */
|
|
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
|
|
|
/* Try to start game again, then add a credit. */
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
sb::Delegate::post("add credit");
|
|
}
|
|
try_count++;
|
|
}
|
|
else if (try_count == 2)
|
|
{
|
|
/* Check the credit account */
|
|
if (params[0] != "double")
|
|
{
|
|
CHECK(mock.cakefoot.mock_credits() == 1.0f);
|
|
}
|
|
else
|
|
{
|
|
CHECK(mock.cakefoot.mock_credits() == 25.25f);
|
|
}
|
|
|
|
/* Make sure the game didn't start */
|
|
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
|
|
|
/* Try to start game again, then add another credit. */
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
sb::Delegate::post("add credit");
|
|
try_count++;
|
|
}
|
|
else if (try_count == 3)
|
|
{
|
|
if (params[0] == "one")
|
|
{
|
|
/* Only one credit was required */
|
|
CHECK(mock.cakefoot.mock_bgm().playing());
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
/* Check the credit account */
|
|
if (params[0] != "double")
|
|
{
|
|
CHECK(mock.cakefoot.mock_credits() == 2.0f);
|
|
}
|
|
else
|
|
{
|
|
CHECK(mock.cakefoot.mock_credits() == 50.5f);
|
|
}
|
|
|
|
/* Make sure the game didn't start */
|
|
CHECK(mock.cakefoot.mock_audio().at("menu").playing());
|
|
|
|
/* Try starting again */
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
}
|
|
try_count++;
|
|
}
|
|
else if (try_count == 4)
|
|
{
|
|
if (params[0] == "double" || params[0] == "max")
|
|
{
|
|
/* Credit requirement should be satisfied */
|
|
CHECK(mock.cakefoot.mock_bgm().playing());
|
|
|
|
if (params[0] == "max")
|
|
{
|
|
/* Test the max credits feature */
|
|
sb::Delegate::post("add credit");
|
|
sb::Delegate::post("add credit");
|
|
}
|
|
else
|
|
{
|
|
/* End test */
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
}
|
|
|
|
/* Add two more credits for final test and try again */
|
|
sb::Delegate::post("add credit");
|
|
sb::Delegate::post("add credit");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
try_count++;
|
|
}
|
|
else if (try_count == 5)
|
|
{
|
|
if (params[0] == "max")
|
|
{
|
|
/* Make sure credits didn't go over the max */
|
|
CHECK(mock.cakefoot.mock_credits() == 3.0f);
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
/* Make sure credits were removed from account and game started. */
|
|
CHECK(mock.cakefoot.mock_credits() == 0.0f);
|
|
CHECK(mock.cakefoot.mock_bgm().playing());
|
|
}
|
|
try_count++;
|
|
}
|
|
else if (try_count == 6)
|
|
{
|
|
/* Wait for arcade run to reset, then try another run */
|
|
if (mock.cakefoot.mock_audio().at("menu").playing())
|
|
{
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any", true);
|
|
try_count++;
|
|
}
|
|
}
|
|
else if (try_count == 7)
|
|
{
|
|
/* Make sure the game didn't start */
|
|
CHECK_FALSE(mock.cakefoot.mock_bgm().playing());
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
mock.cakefoot.draw(timestamp);
|
|
}
|
|
});
|
|
|
|
/* Make sure the amount of tries matches the parameters */
|
|
if (params[0] == "four")
|
|
{
|
|
CHECK(try_count == 7);
|
|
}
|
|
else if (params[0] == "one")
|
|
{
|
|
CHECK(try_count == 4);
|
|
}
|
|
else if (params[0] == "double")
|
|
{
|
|
CHECK(try_count == 5);
|
|
}
|
|
else if (params[0] == "max")
|
|
{
|
|
CHECK(try_count == 6);
|
|
}
|
|
else if (params[0] == "none")
|
|
{
|
|
CHECK(try_count == 2);
|
|
}
|
|
|
|
mock.cakefoot.quit();
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Operator menu")
|
|
{
|
|
/* Mock arcade cabinet version */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path,
|
|
{{ "operator",
|
|
{
|
|
{ "enabled", true }
|
|
} },
|
|
{ "arcade",
|
|
{
|
|
{ "arcade only", true },
|
|
{ "credits enabled", false },
|
|
{ "idle timeout", 2.0f },
|
|
{ "countdown display timeout", 2.0f },
|
|
{ "credits required", 1.0f },
|
|
{ "credit increase per event", 1.0f },
|
|
{ "max credits", 0.0f }
|
|
} } }
|
|
};
|
|
|
|
/* Launch */
|
|
bool running = false;
|
|
std::optional<float> run_start;
|
|
int toggles = 0;
|
|
int downs = 0;
|
|
int ups = 0;
|
|
mock.cakefoot.run([&](float timestamp){
|
|
/* Automatically fail if running for more than 10 seconds */
|
|
if (running && timestamp - *run_start > 10.0f)
|
|
{
|
|
FAIL("Operator menu test timed out");
|
|
}
|
|
else if (!running)
|
|
{
|
|
running = true;
|
|
run_start = timestamp;
|
|
}
|
|
else if (toggles == 0)
|
|
{
|
|
if (!mock.cakefoot.mock_operator_menu_active())
|
|
{
|
|
/* Open the operator menu */
|
|
sb::Delegate::post("operator");
|
|
}
|
|
else
|
|
{
|
|
/* Toggle credits enabled on */
|
|
sb::Delegate::post("any");
|
|
toggles++;
|
|
}
|
|
}
|
|
else if (downs == 0)
|
|
{
|
|
/* Setting should not have been applied */
|
|
CHECK_FALSE(mock.cakefoot.configuration()("arcade", "credits enabled"));
|
|
|
|
/* Move down to exit */
|
|
for (int count = 0; count < 7; count++) sb::Delegate::post("down");
|
|
downs = 7;
|
|
}
|
|
else if (ups == 0)
|
|
{
|
|
/* Try exiting which should require a confirmation press */
|
|
sb::Delegate::post("any");
|
|
|
|
/* Move up to apply & save */
|
|
sb::Delegate::post("up");
|
|
ups++;
|
|
}
|
|
else if (downs == 7)
|
|
{
|
|
/* Try moving to credit increase, deleting all characters, and changing value */
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("up");
|
|
sb::Delegate::post("any");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("any");
|
|
ups += 4;
|
|
|
|
/* Try the apply and save button */
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("any");
|
|
|
|
/* Move down to exit and press the button */
|
|
sb::Delegate::post("down");
|
|
sb::Delegate::post("any");
|
|
downs += 5;
|
|
}
|
|
else
|
|
{
|
|
mock.cakefoot.flag_to_end();
|
|
}
|
|
});
|
|
|
|
/* Check if the menu exited, all buttons were pressed, and the settings were applied */
|
|
CHECK_FALSE(mock.cakefoot.mock_operator_menu_active());
|
|
CHECK(toggles == 1);
|
|
CHECK(ups == 5);
|
|
CHECK(downs == 12);
|
|
CHECK(mock.cakefoot.configuration()("arcade", "credits enabled"));
|
|
CHECK(mock.cakefoot.configuration()("arcade", "credit increase per event") == 9.0f);
|
|
|
|
/* Quit the game and check if the setting was saved to file */
|
|
mock.cakefoot.quit();
|
|
mock.refresh_progress();
|
|
CHECK(mock.preferences.read("config", "arcade", "credits enabled"));
|
|
}
|
|
|
|
TEST_CASE("HTTP")
|
|
{
|
|
/* Only on Linux builds with cURL enabled, test the arcade build's remote logger call after auto-updates. */
|
|
#if defined(__LINUX__) && defined(HTTP_ENABLED)
|
|
|
|
/* Mock arcade cabinet version */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path,
|
|
{{ "operator",
|
|
{
|
|
{ "enabled", true }
|
|
} },
|
|
{ "arcade",
|
|
{
|
|
{ "arcade only", true },
|
|
{ "credits enabled", false },
|
|
{ "idle timeout", 2.0f },
|
|
{ "countdown display timeout", 2.0f },
|
|
{ "credits required", 1.0f },
|
|
{ "credit increase per event", 1.0f },
|
|
{ "max credits", 0.0f }
|
|
} } }
|
|
};
|
|
|
|
/* Try a non-existent update script to make sure the game still runs. */
|
|
|
|
/* Try an update script that runs and exits with an error. */
|
|
|
|
/* Try an update script that runs and exits normally. */
|
|
|
|
/* Try a non-existent remote logger. */
|
|
|
|
/* Try a remote logger that returns an error code. */
|
|
|
|
/* Try a remote logger that returns a 200 status. */
|
|
|
|
#endif
|
|
}
|
|
|
|
float lower_precision(float higher_precision)
|
|
{
|
|
return float(int(cakefoot::Replay::time_precision * higher_precision)) / cakefoot::Replay::time_precision;
|
|
}
|
|
|
|
TEST_CASE("Replay")
|
|
{
|
|
using namespace cakefoot;
|
|
|
|
/* Mock replay is able to get public access to the JSON path. */
|
|
class MockReplay : public Replay
|
|
{
|
|
public:
|
|
const fs::path& mock_path()
|
|
{
|
|
return most_recent_path;
|
|
}
|
|
};
|
|
|
|
/* Create a 0.1s long replay with two frames */
|
|
MockReplay replay;
|
|
CHECK(replay.empty());
|
|
Replay::KeyFrame second_frame { Replay::KeyFrame{2.2f, glm::vec2 {0.1f, 0.0f}, true} };
|
|
replay.record(Replay::KeyFrame{2.1f, glm::vec2 {0.0f, 0.0f}});
|
|
CHECK_FALSE(replay.empty());
|
|
replay.record(second_frame);
|
|
|
|
/* The replay lengths are affected by the conversion to lower precision values */
|
|
CHECK(replay.length() == lower_precision(2.2f) - lower_precision(2.1f));
|
|
CHECK(replay.elapsed(second_frame) == 2.2f - lower_precision(2.1f));
|
|
|
|
/* Read the first frame */
|
|
std::vector<Replay::KeyFrame> unread { replay.unread(0.0f) };
|
|
CHECK(unread.size() == 1);
|
|
CHECK(unread.front().time == lower_precision(2.1f));
|
|
CHECK(replay.elapsed(unread.front()) == 0.0f);
|
|
CHECK(unread.front().translation == glm::vec2 {0.0f, 0.0f});
|
|
CHECK_FALSE(unread.front().mirrored);
|
|
|
|
/* Try reading a frame before a new frame is ready */
|
|
unread = replay.unread(0.05f);
|
|
CHECK(unread.size() == 0);
|
|
|
|
/* Read the next frame */
|
|
unread = replay.unread(0.11f);
|
|
CHECK(unread.size() == 1);
|
|
CHECK(unread.front().time == lower_precision(2.2f));
|
|
CHECK(replay.elapsed(unread.front()) == lower_precision(2.2f) - lower_precision(2.1f));
|
|
CHECK(unread.front().translation == glm::vec2 {0.1f, 0.0f});
|
|
CHECK(unread.front().mirrored);
|
|
|
|
/* Try reading another frame after all frames have been read */
|
|
unread = replay.unread(0.5f);
|
|
CHECK(unread.size() == 0);
|
|
|
|
/* Read all frames at once */
|
|
replay.reset();
|
|
unread = replay.unread(0.11f);
|
|
CHECK(unread.size() == 2);
|
|
CHECK_FALSE(replay.coin_taken);
|
|
CHECK_FALSE(replay.coin_collected);
|
|
|
|
/* Add coin events */
|
|
replay.record(Replay::KeyFrame{2.3f, glm::vec2 {0.2f, 0.0f}, true, Replay::coin});
|
|
replay.record(Replay::KeyFrame{2.4f, glm::vec2 {0.3f, 0.0f}, true, Replay::collision});
|
|
replay.record(Replay::KeyFrame{2.5f, glm::vec2 {0.1f, 0.0f}, true});
|
|
replay.record(Replay::KeyFrame{2.6f, glm::vec2 {0.2f, 0.0f}, true, Replay::coin});
|
|
replay.record(Replay::KeyFrame{2.7f, glm::vec2 {0.3f, 0.0f}, true});
|
|
replay.record(Replay::KeyFrame{2.8f, glm::vec2 {0.4f, 0.0f}, true, Replay::collect});
|
|
replay.record(Replay::KeyFrame{2.8f, glm::vec2 {0.4f, 0.0f}, true, Replay::checkpoint});
|
|
replay.record(Replay::KeyFrame{3.5f, glm::vec2 {0.8f, 0.0f}, true});
|
|
replay.record(Replay::KeyFrame{3.5f, glm::vec2 {0.8f, 0.0f}, true, Replay::end});
|
|
|
|
/* Check coin and checkpoint states */
|
|
replay.reset();
|
|
unread = replay.unread(0.25f);
|
|
CHECK(unread.size() == 3);
|
|
CHECK(replay.coin_taken);
|
|
CHECK_FALSE(replay.coin_collected);
|
|
CHECK(replay.checkpoints == 0);
|
|
unread = replay.unread(0.35f);
|
|
CHECK(unread.size() == 1);
|
|
CHECK_FALSE(replay.coin_taken);
|
|
CHECK_FALSE(replay.coin_collected);
|
|
CHECK(replay.checkpoints == 0);
|
|
unread = replay.unread(0.75f);
|
|
CHECK(unread.size() == 5);
|
|
CHECK(replay.coin_taken);
|
|
CHECK(replay.coin_collected);
|
|
CHECK(replay.checkpoints == 1);
|
|
CHECK_FALSE(replay.ended());
|
|
|
|
/* Read the end event */
|
|
CHECK(replay.unread(1.401f).size() == 2);
|
|
CHECK(replay.ended());
|
|
|
|
/* Write the replay and check the contents */
|
|
CHECK(replay.mock_path().empty());
|
|
replay.save(1, {}, fs::temp_directory_path());
|
|
std::chrono::time_point<std::chrono::system_clock> start_time {
|
|
std::chrono::system_clock::now() - std::chrono::seconds(static_cast<int>(replay.length()))
|
|
};
|
|
std::string start_timestamp { sb::time::minute_stamp(start_time) };
|
|
CHECK(fs::exists(replay.mock_path()));
|
|
std::ostringstream contents;
|
|
contents << "{\"_id\":\"" << replay.id() << "\",\"key frames\":[[209999,0,0],[220000,1000,0,1]," <<
|
|
"[230000,2000,0,1,3],[240000,3000,0,1,1],[250000,1000,0,1],[259999,2000,0,1,3],[270000,3000,0,1]," <<
|
|
"[280000,4000,0,1,5],[280000,4000,0,1,4],[350000,8000,0,1],[350000,8000,0,1,6]],\"metadata\":" <<
|
|
"{\"length\":\"1.4\",\"level\":1},\"start\":\"" << start_timestamp << "\"}\n";
|
|
CHECK(sb::file_to_string(replay.mock_path()) == contents.str());
|
|
fs::path path = replay.mock_path();
|
|
replay.remove();
|
|
CHECK_FALSE(fs::exists(path));
|
|
|
|
/* Clear data from a replay object */
|
|
replay.clear();
|
|
CHECK(replay.empty());
|
|
CHECK(replay.unread(1.0f).empty());
|
|
CHECK(replay.length() == 0.0f);
|
|
|
|
/* Check roundtrip of metadata */
|
|
replay.record(Replay::KeyFrame{3.14159f, glm::vec2 {2.71828f, 1.61803f}});
|
|
nlohmann::json metadata = nlohmann::json({{"Commander", "Waluigi"},{"Time Zone", "Andromeda"}});
|
|
replay.save(999'999, metadata, fs::temp_directory_path());
|
|
Replay roundtrip;
|
|
roundtrip.load(replay.mock_path());
|
|
metadata["level"] = 999'999;
|
|
metadata["length"] = "0";
|
|
CHECK(roundtrip.metadata() == metadata);
|
|
|
|
/* Mock three replays */
|
|
std::shared_ptr<fs::path> base_replay_path { sb::test::temp_path("cakefoot_base_replay.json") };
|
|
std::ofstream(*base_replay_path) << contents.str();
|
|
std::shared_ptr<fs::path> faster_replay_path { sb::test::temp_path("cakefoot_faster_replay.json") };
|
|
std::ofstream(*faster_replay_path) << "{\"key frames\":[[209999,0,0],[220000,1000,0,1],[230000,2000,0,1,3]," <<
|
|
"[240000,3000,0,1,1],[250000,1000,0,1],[259999,2000,0,1,3],[270000,3000,0,1]]}";
|
|
std::shared_ptr<fs::path> slower_replay_path { sb::test::temp_path("cakefoot_slower_replay.json") };
|
|
std::ofstream(*slower_replay_path) << "{\"key frames\":[[209999,0,0],[220000,1000,0,1],[230000,2000,0,1,3]," <<
|
|
"[240000,3000,0,1,1],[250000,1000,0,1],[259999,2000,0,1,3],[270000,3000,0,1],[290000,4000,0,1,5]]}";
|
|
Replay base_replay;
|
|
base_replay.load(*base_replay_path);
|
|
Replay faster_replay;
|
|
faster_replay.load(*faster_replay_path);
|
|
Replay slower_replay;
|
|
slower_replay.load(*slower_replay_path);
|
|
|
|
/* Test five different configurations for replays */
|
|
for (const nlohmann::json& params : nlohmann::json({
|
|
{ "default", 60.0f, 8, 1, "slowest" },
|
|
{ "oldest", 60.0f, 8, 1, "oldest" },
|
|
{ "impossible", 0.0f, 1, 1, "slowest" },
|
|
{ "level one", 60.0f, 1, 1, "slowest" },
|
|
{ "multiple", 60.0f, 8, 3, "slowest" } }))
|
|
{
|
|
/* Start with an empty replays directory */
|
|
fs::remove_all(*mock_replays_path);
|
|
|
|
/* Mock games with different replay configuration. */
|
|
MockCakefoot mock {
|
|
*mock_empty_progress_path,
|
|
*mock_empty_scores_path,
|
|
*mock_empty_preferences_path,
|
|
*mock_empty_stats_path,
|
|
{{ "replay",
|
|
{
|
|
{ "max length", params[1] },
|
|
{ "min level", params[2] },
|
|
{ "max files per level", params[3] },
|
|
{ "replace", params[4] }
|
|
} } }
|
|
};
|
|
|
|
for (int level : { 1, 10 })
|
|
{
|
|
mock.cakefoot.mock_store_replay_recording(base_replay, level);
|
|
}
|
|
if (params[0] == "impossible")
|
|
{
|
|
CHECK_FALSE(fs::exists(*mock_replays_path));
|
|
}
|
|
else
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(1).size() == (params[0] == "level one"));
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 1);
|
|
}
|
|
|
|
for (int level : { 1, 10 })
|
|
{
|
|
mock.cakefoot.mock_store_replay_recording(faster_replay, level);
|
|
}
|
|
if (params[0] == "impossible")
|
|
{
|
|
CHECK_FALSE(fs::exists(*mock_replays_path));
|
|
}
|
|
else if (params[0] == "multiple")
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 2);
|
|
}
|
|
else
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(1).size() == (params[0] == "level one"));
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 1);
|
|
CHECK(mock.cakefoot.mock_replays(10)[0].id() == faster_replay.id());
|
|
}
|
|
|
|
for (int level : { 1, 10 })
|
|
{
|
|
mock.cakefoot.mock_store_replay_recording(slower_replay, level);
|
|
}
|
|
if (params[0] == "impossible")
|
|
{
|
|
CHECK_FALSE(fs::exists(*mock_replays_path));
|
|
}
|
|
else if (params[0] == "multiple")
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 3);
|
|
}
|
|
else if (params[0] == "oldest")
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 1);
|
|
CHECK(mock.cakefoot.mock_replays(10)[0].id() == slower_replay.id());
|
|
}
|
|
else
|
|
{
|
|
CHECK(mock.cakefoot.mock_replays(1).size() == (params[0] == "level one"));
|
|
CHECK(mock.cakefoot.mock_replays(10).size() == 1);
|
|
CHECK(mock.cakefoot.mock_replays(10)[0].id() == faster_replay.id());
|
|
}
|
|
}
|
|
}
|
|
|
|
/* TODO: Test reading, sorting, and adding scores */
|
|
|
|
/* TODO: Test missing sound device */
|