mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-10-30 07:11:05 +00:00
Implemented achievement data verification (#161)
This commit is contained in:
parent
d443693d78
commit
c6a25f21c2
16 changed files with 350 additions and 213 deletions
|
|
@ -178,6 +178,7 @@ set(UNLEASHED_RECOMP_INSTALL_CXX_SOURCES
|
|||
|
||||
set(UNLEASHED_RECOMP_USER_CXX_SOURCES
|
||||
"user/achievement_data.cpp"
|
||||
"user/achievement_manager.cpp"
|
||||
"user/config.cpp"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
#include "Hedgehog/Universe/Engine/hhUpdateInfo.h"
|
||||
#include "Hedgehog/Universe/Engine/hhUpdateUnit.h"
|
||||
#include "Hedgehog/Universe/Thread/hhParallelJob.h"
|
||||
#include "SWA/Achievement/AchievementID.h"
|
||||
#include "SWA/Achievement/AchievementManager.h"
|
||||
#include "SWA/Achievement/AchievementTest.h"
|
||||
#include "SWA/CSD/CsdDatabaseWrapper.h"
|
||||
|
|
|
|||
55
UnleashedRecomp/api/SWA/Achievement/AchievementID.h
Normal file
55
UnleashedRecomp/api/SWA/Achievement/AchievementID.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
enum EAchievementID : uint32_t
|
||||
{
|
||||
eAchievementID_StillBroken = 24,
|
||||
eAchievementID_LookingBetter,
|
||||
eAchievementID_StillAJigsawPuzzle,
|
||||
eAchievementID_PickingUpThePieces,
|
||||
eAchievementID_AlmostThere,
|
||||
eAchievementID_OneMoreToGo,
|
||||
eAchievementID_WorldSavior = 31,
|
||||
eAchievementID_PartlyCloudy,
|
||||
eAchievementID_Sunny,
|
||||
eAchievementID_HalfMoon,
|
||||
eAchievementID_FullMoon,
|
||||
eAchievementID_BlueStreak,
|
||||
eAchievementID_PowerOverwhelming,
|
||||
eAchievementID_GettingTheHangOfThings,
|
||||
eAchievementID_CreatureOfTheNight,
|
||||
eAchievementID_HelpingHand,
|
||||
eAchievementID_LayTheSmackdown,
|
||||
eAchievementID_WallCrawler,
|
||||
eAchievementID_Airdevil,
|
||||
eAchievementID_Hyperdrive,
|
||||
eAchievementID_Basher,
|
||||
eAchievementID_Smasher,
|
||||
eAchievementID_Crasher,
|
||||
eAchievementID_Thrasher,
|
||||
eAchievementID_SocialButterfly,
|
||||
eAchievementID_HungryHungryHedgehog,
|
||||
eAchievementID_AcePilot,
|
||||
eAchievementID_DayTripper,
|
||||
eAchievementID_HardDaysNight,
|
||||
eAchievementID_GetOnTheExorciseBandwagon,
|
||||
eAchievementID_GyroWithRelish = 64,
|
||||
eAchievementID_PigInABlanket,
|
||||
eAchievementID_ExoticToppings,
|
||||
eAchievementID_SausageFriedRice,
|
||||
eAchievementID_IcedHotdog,
|
||||
eAchievementID_KebabOnABun,
|
||||
eAchievementID_KetchupAndMustard,
|
||||
eAchievementID_HardBoiled,
|
||||
eAchievementID_FriedClamRoll,
|
||||
eAchievementID_FirstTimeCustomer,
|
||||
eAchievementID_OhYouShouldntHave,
|
||||
eAchievementID_ThatsEnoughSeriously,
|
||||
eAchievementID_Hedgehunk,
|
||||
eAchievementID_IAintAfraidOfNoGhost,
|
||||
eAchievementID_BFFs,
|
||||
eAchievementID_SpeedingTicket,
|
||||
eAchievementID_ComboKing,
|
||||
eAchievementID_RingLeader,
|
||||
eAchievementID_KnockoutBrawler,
|
||||
eAchievementID_BlueMeteor
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <SWA.inl>
|
||||
#include <SWA/Achievement/AchievementID.h>
|
||||
|
||||
namespace SWA::Achievement
|
||||
{
|
||||
|
|
@ -11,7 +12,7 @@ namespace SWA::Achievement
|
|||
{
|
||||
public:
|
||||
SWA_INSERT_PADDING(0x08);
|
||||
be<uint32_t> m_AchievementID;
|
||||
be<EAchievementID> m_AchievementID;
|
||||
};
|
||||
|
||||
SWA_INSERT_PADDING(0x98);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <SWA.inl>
|
||||
#include <SWA/Achievement/AchievementID.h>
|
||||
|
||||
namespace SWA
|
||||
{
|
||||
|
|
@ -9,7 +10,7 @@ namespace SWA
|
|||
public:
|
||||
SWA_INSERT_PADDING(0x38);
|
||||
be<uint32_t> m_Unk1;
|
||||
be<uint32_t> m_AchievementID;
|
||||
be<EAchievementID> m_AchievementID;
|
||||
uint8_t m_Unk2;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ std::unordered_map<std::string, std::unordered_map<ELanguage, std::string>> g_lo
|
|||
}
|
||||
},
|
||||
{
|
||||
// Notes: message appears when the SYS-DATA is corrupted (mismatching file size).
|
||||
// Notes: message appears when SYS-DATA is corrupted (mismatching file size) upon pressing start at the title screen.
|
||||
// To make this occur, open the file in any editor and just remove a large chunk of data.
|
||||
// Do not localise this unless absolutely necessary, these strings are from the XEX.
|
||||
"Title_Message_SaveDataCorrupt",
|
||||
|
|
@ -356,6 +356,22 @@ std::unordered_map<std::string, std::unordered_map<ELanguage, std::string>> g_lo
|
|||
{ ELanguage::Italian, "I file di salvataggio sembrano danneggiati\ne non possono essere caricati." }
|
||||
}
|
||||
},
|
||||
{
|
||||
// Notes: message appears when ACH-DATA is corrupted (mismatching file size, bad signature, incorrect version or invalid checksum) upon pressing start at the title screen.
|
||||
// To make this occur, open the file in any editor and just remove a large chunk of data.
|
||||
"Title_Message_AchievementDataCorrupt",
|
||||
{
|
||||
{ ELanguage::English, "The achievement data appears to be\ncorrupted and cannot be loaded.\n\nProceeding from this point will\nclear your achievement data." }
|
||||
}
|
||||
},
|
||||
{
|
||||
// Notes: message appears when ACH-DATA cannot be loaded upon pressing start at the title screen.
|
||||
// To make this occur, lock the ACH-DATA file using an external program so that it cannot be accessed by the game.
|
||||
"Title_Message_AchievementDataIOError",
|
||||
{
|
||||
{ ELanguage::English, "The achievement data could not be loaded.\nYour achievements will not be saved." }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Common_On",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
#include <xex.h>
|
||||
#include <apu/audio.h>
|
||||
#include <hid/hid.h>
|
||||
#include <user/achievement_data.h>
|
||||
#include <user/config.h>
|
||||
#include <user/paths.h>
|
||||
#include <kernel/xdbf.h>
|
||||
|
|
@ -184,8 +183,6 @@ int main(int argc, char *argv[])
|
|||
|
||||
ModLoader::Init();
|
||||
|
||||
AchievementData::Load();
|
||||
|
||||
KiSystemStartup();
|
||||
|
||||
uint32_t entry = LdrLoadModule(modulePath);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#include <api/SWA.h>
|
||||
#include <ui/game_window.h>
|
||||
#include <user/achievement_data.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/config.h>
|
||||
|
||||
void AchievementManagerUnlockMidAsmHook(PPCRegister& id)
|
||||
{
|
||||
AchievementData::Unlock(id.u32);
|
||||
AchievementManager::Unlock(id.u32);
|
||||
}
|
||||
|
||||
bool DisableHintsMidAsmHook()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#include <user/achievement_data.h>
|
||||
#include <user/config.h>
|
||||
#include <api/SWA.h>
|
||||
#include <os/logger.h>
|
||||
#include <hid/hid.h>
|
||||
#include <os/logger.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/config.h>
|
||||
#include <app.h>
|
||||
|
||||
bool m_isSavedAchievementData = false;
|
||||
|
|
@ -93,9 +93,7 @@ PPC_FUNC(sub_824E5170)
|
|||
|
||||
if (!m_isSavedAchievementData)
|
||||
{
|
||||
LOGN("Saving achievements...");
|
||||
|
||||
AchievementData::Save();
|
||||
AchievementManager::Save();
|
||||
|
||||
m_isSavedAchievementData = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <locale/locale.h>
|
||||
#include <ui/fader.h>
|
||||
#include <ui/message_window.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/paths.h>
|
||||
#include <app.h>
|
||||
|
||||
|
|
@ -13,6 +14,9 @@ static int g_quitMessageResult = -1;
|
|||
static std::atomic<bool> g_corruptSaveMessageOpen = false;
|
||||
static int g_corruptSaveMessageResult = -1;
|
||||
|
||||
static bool g_corruptAchievementsMessageOpen = false;
|
||||
static int g_corruptAchievementsMessageResult = -1;
|
||||
|
||||
static bool ProcessQuitMessage()
|
||||
{
|
||||
if (g_corruptSaveMessageOpen)
|
||||
|
|
@ -60,7 +64,36 @@ static bool ProcessCorruptSaveMessage()
|
|||
return true;
|
||||
}
|
||||
|
||||
void StorageDevicePromptMidAsmHook() {}
|
||||
static bool ProcessCorruptAchievementsMessage()
|
||||
{
|
||||
if (!g_corruptAchievementsMessageOpen)
|
||||
return false;
|
||||
|
||||
auto message = AchievementManager::Status == EAchStatus::IOError
|
||||
? Localise("Title_Message_AchievementDataIOError")
|
||||
: Localise("Title_Message_AchievementDataCorrupt");
|
||||
|
||||
if (MessageWindow::Open(message, &g_corruptAchievementsMessageResult) == MSG_CLOSED)
|
||||
{
|
||||
// Allow user to proceed if the achievement data couldn't be loaded.
|
||||
// Restarting may fix this error, so it isn't worth clearing the data for.
|
||||
if (AchievementManager::Status != EAchStatus::IOError)
|
||||
AchievementManager::Save(true);
|
||||
|
||||
g_corruptAchievementsMessageOpen = false;
|
||||
g_corruptAchievementsMessageResult = -1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void StorageDevicePromptMidAsmHook()
|
||||
{
|
||||
AchievementManager::Load();
|
||||
|
||||
if (AchievementManager::Status != EAchStatus::Success)
|
||||
g_corruptAchievementsMessageOpen = true;
|
||||
}
|
||||
|
||||
// Save data validation hook.
|
||||
PPC_FUNC_IMPL(__imp__sub_822C55B0);
|
||||
|
|
@ -82,7 +115,7 @@ PPC_FUNC(sub_82587E50)
|
|||
{
|
||||
__imp__sub_82587E50(ctx, base);
|
||||
}
|
||||
else if (!ProcessCorruptSaveMessage())
|
||||
else if (!ProcessCorruptSaveMessage() && !ProcessCorruptAchievementsMessage())
|
||||
{
|
||||
auto pInputState = SWA::CInputState::GetInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <kernel/xdbf.h>
|
||||
#include <locale/locale.h>
|
||||
#include <ui/button_guide.h>
|
||||
#include <user/achievement_data.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/config.h>
|
||||
#include <app.h>
|
||||
#include <exports.h>
|
||||
|
|
@ -276,7 +276,7 @@ static void DrawAchievement(int rowIndex, float yOffset, Achievement& achievemen
|
|||
if (!isUnlocked)
|
||||
return;
|
||||
|
||||
auto timestamp = AchievementData::GetTimestamp(achievement.ID);
|
||||
auto timestamp = AchievementManager::GetTimestamp(achievement.ID);
|
||||
|
||||
if (!timestamp)
|
||||
return;
|
||||
|
|
@ -485,7 +485,7 @@ static void DrawAchievementTotal(ImVec2 min, ImVec2 max)
|
|||
auto uv1 = ImVec2((columnIndex + 1) * spriteSize / textureWidth, (rowIndex + 1) * spriteSize / textureHeight);
|
||||
|
||||
constexpr auto recordsHalfTotal = ACH_RECORDS / 2;
|
||||
auto records = AchievementData::GetTotalRecords();
|
||||
auto records = AchievementManager::GetTotalRecords();
|
||||
|
||||
ImVec4 colBronze = ImGui::ColorConvertU32ToFloat4(IM_COL32(198, 105, 15, 255 * alpha));
|
||||
ImVec4 colSilver = ImGui::ColorConvertU32ToFloat4(IM_COL32(220, 220, 220, 255 * alpha));
|
||||
|
|
@ -613,7 +613,7 @@ static void DrawContentContainer()
|
|||
{
|
||||
auto achievement = std::get<0>(tpl);
|
||||
|
||||
if (AchievementData::IsUnlocked(achievement.ID))
|
||||
if (AchievementManager::IsUnlocked(achievement.ID))
|
||||
DrawAchievement(rowCount++, yOffset, achievement, true);
|
||||
}
|
||||
|
||||
|
|
@ -621,7 +621,7 @@ static void DrawContentContainer()
|
|||
{
|
||||
auto achievement = std::get<0>(tpl);
|
||||
|
||||
if (!AchievementData::IsUnlocked(achievement.ID))
|
||||
if (!AchievementManager::IsUnlocked(achievement.ID))
|
||||
DrawAchievement(rowCount++, yOffset, achievement, false);
|
||||
}
|
||||
|
||||
|
|
@ -804,7 +804,7 @@ void AchievementMenu::Open()
|
|||
if (Config::Language == ELanguage::English)
|
||||
achievement.Name = xdbf::FixInvalidSequences(achievement.Name);
|
||||
|
||||
g_achievements.push_back(std::make_tuple(achievement, AchievementData::GetTimestamp(achievement.ID)));
|
||||
g_achievements.push_back(std::make_tuple(achievement, AchievementManager::GetTimestamp(achievement.ID)));
|
||||
}
|
||||
|
||||
std::sort(g_achievements.begin(), g_achievements.end(), [](const auto& a, const auto& b)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ void MessageWindow::Draw()
|
|||
|
||||
ImVec2 centre = { res.x / 2, res.y / 2 };
|
||||
|
||||
float maxWidth = Scale(640.0f);
|
||||
auto maxWidth = Scale(820);
|
||||
auto fontSize = Scale(28);
|
||||
auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, maxWidth, 5, g_text.c_str());
|
||||
auto textMarginX = Scale(37);
|
||||
|
|
|
|||
|
|
@ -1,70 +1,22 @@
|
|||
#include "achievement_data.h"
|
||||
#include <ui/achievement_overlay.h>
|
||||
#include <user/config.h>
|
||||
#include <os/logger.h>
|
||||
|
||||
#define NUM_RECORDS sizeof(Data.Records) / sizeof(Record)
|
||||
#define NUM_RECORDS sizeof(Records) / sizeof(AchRecord)
|
||||
|
||||
time_t AchievementData::GetTimestamp(uint16_t id)
|
||||
bool AchievementData::VerifySignature() const
|
||||
{
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
char sig[4] = ACH_SIGNATURE;
|
||||
|
||||
if (Data.Records[i].ID == id)
|
||||
return Data.Records[i].Timestamp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return memcmp(Signature, sig, sizeof(Signature)) == 0;
|
||||
}
|
||||
|
||||
int AchievementData::GetTotalRecords()
|
||||
bool AchievementData::VerifyVersion() const
|
||||
{
|
||||
auto result = 0;
|
||||
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
|
||||
result++;
|
||||
}
|
||||
|
||||
return result;
|
||||
return Version == AchVersion ACH_VERSION;
|
||||
}
|
||||
|
||||
bool AchievementData::IsUnlocked(uint16_t id)
|
||||
bool AchievementData::VerifyChecksum()
|
||||
{
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
|
||||
if (Data.Records[i].ID == id)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AchievementData::Unlock(uint16_t id)
|
||||
{
|
||||
if (IsUnlocked(id))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (Data.Records[i].ID == 0)
|
||||
{
|
||||
Data.Records[i].ID = id;
|
||||
Data.Records[i].Timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Config::AchievementNotifications)
|
||||
AchievementOverlay::Open(id);
|
||||
return Checksum == CalculateChecksum();
|
||||
}
|
||||
|
||||
uint32_t AchievementData::CalculateChecksum()
|
||||
|
|
@ -73,109 +25,11 @@ uint32_t AchievementData::CalculateChecksum()
|
|||
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
auto& record = Data.Records[i];
|
||||
auto& record = Records[i];
|
||||
|
||||
for (size_t j = 0; j < sizeof(Record); j++)
|
||||
for (size_t j = 0; j < sizeof(AchRecord); j++)
|
||||
result ^= ((uint8_t*)(&record))[j];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AchievementData::VerifySignature()
|
||||
{
|
||||
char sig[4] = ACH_SIGNATURE;
|
||||
|
||||
return Data.Signature[0] == sig[0] &&
|
||||
Data.Signature[1] == sig[1] &&
|
||||
Data.Signature[2] == sig[2] &&
|
||||
Data.Signature[3] == sig[3];
|
||||
}
|
||||
|
||||
bool AchievementData::VerifyVersion()
|
||||
{
|
||||
return Data.Version == Version ACH_VERSION;
|
||||
}
|
||||
|
||||
bool AchievementData::VerifyChecksum()
|
||||
{
|
||||
return Data.Checksum == CalculateChecksum();
|
||||
}
|
||||
|
||||
void AchievementData::Load()
|
||||
{
|
||||
auto dataPath = GetDataPath(true);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
{
|
||||
// Try loading base achievement data as fallback.
|
||||
dataPath = GetDataPath(false);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(dataPath, std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LOGN_ERROR("Failed to read achievement data.");
|
||||
return;
|
||||
}
|
||||
|
||||
file.read((char*)&Data.Signature, sizeof(Data.Signature));
|
||||
|
||||
if (!VerifySignature())
|
||||
{
|
||||
LOGN_ERROR("Invalid achievement data signature.");
|
||||
|
||||
char sig[4] = ACH_SIGNATURE;
|
||||
|
||||
Data.Signature[0] = sig[0];
|
||||
Data.Signature[1] = sig[1];
|
||||
Data.Signature[2] = sig[2];
|
||||
Data.Signature[3] = sig[3];
|
||||
|
||||
file.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
file.read((char*)&Data.Version, sizeof(Data.Version));
|
||||
|
||||
if (!VerifyVersion())
|
||||
{
|
||||
LOGN_ERROR("Unsupported achievement data version.");
|
||||
Data.Version = ACH_VERSION;
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
file.seekg(0);
|
||||
file.read((char*)&Data, sizeof(Data));
|
||||
|
||||
// TODO: display error message to user before wiping data?
|
||||
if (!VerifyChecksum())
|
||||
{
|
||||
LOGN_WARNING("Achievement data checksum mismatch.");
|
||||
memset(&Data.Records, 0, sizeof(Data.Records));
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
void AchievementData::Save()
|
||||
{
|
||||
std::ofstream file(GetDataPath(true), std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LOGN_ERROR("Failed to write achievement data.");
|
||||
return;
|
||||
}
|
||||
|
||||
Data.Checksum = CalculateChecksum();
|
||||
|
||||
file.write((const char*)&Data, sizeof(Data));
|
||||
file.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <user/paths.h>
|
||||
|
||||
#define ACH_FILENAME "ACH-DATA"
|
||||
#define ACH_SIGNATURE { 'A', 'C', 'H', ' ' }
|
||||
#define ACH_VERSION { 1, 0, 0 }
|
||||
#define ACH_RECORDS 50
|
||||
|
|
@ -9,23 +10,14 @@
|
|||
class AchievementData
|
||||
{
|
||||
public:
|
||||
#pragma pack(push, 1)
|
||||
struct Record
|
||||
{
|
||||
uint16_t ID;
|
||||
time_t Timestamp;
|
||||
uint16_t Reserved[3];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct Version
|
||||
struct AchVersion
|
||||
{
|
||||
uint8_t Major;
|
||||
uint8_t Minor;
|
||||
uint8_t Revision;
|
||||
uint8_t Reserved;
|
||||
|
||||
bool operator==(const Version& other) const
|
||||
bool operator==(const AchVersion& other) const
|
||||
{
|
||||
return Major == other.Major &&
|
||||
Minor == other.Minor &&
|
||||
|
|
@ -33,31 +25,23 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
class Data
|
||||
#pragma pack(push, 1)
|
||||
struct AchRecord
|
||||
{
|
||||
public:
|
||||
char Signature[4];
|
||||
Version Version{};
|
||||
uint32_t Checksum;
|
||||
uint32_t Reserved;
|
||||
Record Records[ACH_RECORDS];
|
||||
uint16_t ID;
|
||||
time_t Timestamp;
|
||||
uint16_t Reserved[3];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static inline Data Data{ ACH_SIGNATURE, ACH_VERSION };
|
||||
char Signature[4] ACH_SIGNATURE;
|
||||
AchVersion Version ACH_VERSION;
|
||||
uint32_t Checksum;
|
||||
uint32_t Reserved;
|
||||
AchRecord Records[ACH_RECORDS];
|
||||
|
||||
static std::filesystem::path GetDataPath(bool checkForMods)
|
||||
{
|
||||
return GetSavePath(checkForMods) / "ACH-DATA";
|
||||
}
|
||||
|
||||
static time_t GetTimestamp(uint16_t id);
|
||||
static int GetTotalRecords();
|
||||
static bool IsUnlocked(uint16_t id);
|
||||
static void Unlock(uint16_t id);
|
||||
static uint32_t CalculateChecksum();
|
||||
static bool VerifySignature();
|
||||
static bool VerifyVersion();
|
||||
static bool VerifyChecksum();
|
||||
static void Load();
|
||||
static void Save();
|
||||
bool VerifySignature() const;
|
||||
bool VerifyVersion() const;
|
||||
bool VerifyChecksum();
|
||||
uint32_t CalculateChecksum();
|
||||
};
|
||||
|
|
|
|||
164
UnleashedRecomp/user/achievement_manager.cpp
Normal file
164
UnleashedRecomp/user/achievement_manager.cpp
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
#include "achievement_manager.h"
|
||||
#include <os/logger.h>
|
||||
#include <ui/achievement_overlay.h>
|
||||
#include <user/config.h>
|
||||
|
||||
#define NUM_RECORDS sizeof(AchievementManager::Data.Records) / sizeof(AchievementData::AchRecord)
|
||||
|
||||
time_t AchievementManager::GetTimestamp(uint16_t id)
|
||||
{
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
|
||||
if (Data.Records[i].ID == id)
|
||||
return Data.Records[i].Timestamp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t AchievementManager::GetTotalRecords()
|
||||
{
|
||||
auto result = 0;
|
||||
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
|
||||
result++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AchievementManager::IsUnlocked(uint16_t id)
|
||||
{
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (!Data.Records[i].ID)
|
||||
break;
|
||||
|
||||
if (Data.Records[i].ID == id)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AchievementManager::Unlock(uint16_t id)
|
||||
{
|
||||
if (IsUnlocked(id))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < NUM_RECORDS; i++)
|
||||
{
|
||||
if (Data.Records[i].ID == 0)
|
||||
{
|
||||
Data.Records[i].ID = id;
|
||||
Data.Records[i].Timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Config::AchievementNotifications)
|
||||
AchievementOverlay::Open(id);
|
||||
}
|
||||
|
||||
void AchievementManager::Load()
|
||||
{
|
||||
Data = {};
|
||||
Status = EAchStatus::Success;
|
||||
|
||||
auto dataPath = GetDataPath(true);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
{
|
||||
// Try loading base achievement data as fallback.
|
||||
dataPath = GetDataPath(false);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
auto fileSize = std::filesystem::file_size(dataPath, ec);
|
||||
auto dataSize = sizeof(AchievementData);
|
||||
|
||||
if (fileSize != dataSize)
|
||||
{
|
||||
Status = EAchStatus::BadFileSize;
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(dataPath, std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
Status = EAchStatus::IOError;
|
||||
return;
|
||||
}
|
||||
|
||||
AchievementData data{};
|
||||
|
||||
file.read((char*)&data.Signature, sizeof(data.Signature));
|
||||
|
||||
if (!data.VerifySignature())
|
||||
{
|
||||
Status = EAchStatus::BadSignature;
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
file.read((char*)&data.Version, sizeof(data.Version));
|
||||
|
||||
// TODO: upgrade in future if the version changes.
|
||||
if (!data.VerifyVersion())
|
||||
{
|
||||
Status = EAchStatus::BadVersion;
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
file.seekg(0);
|
||||
file.read((char*)&data, sizeof(data));
|
||||
|
||||
if (!data.VerifyChecksum())
|
||||
{
|
||||
Status = EAchStatus::BadChecksum;
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
memcpy(&Data, &data, dataSize);
|
||||
}
|
||||
|
||||
void AchievementManager::Save(bool ignoreStatus)
|
||||
{
|
||||
if (!ignoreStatus && Status != EAchStatus::Success)
|
||||
{
|
||||
LOGN_WARNING("Achievement data will not be saved in this session!");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGN("Saving achievements...");
|
||||
|
||||
std::ofstream file(GetDataPath(true), std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LOGN_ERROR("Failed to write achievement data.");
|
||||
return;
|
||||
}
|
||||
|
||||
Data.Checksum = Data.CalculateChecksum();
|
||||
|
||||
file.write((const char*)&Data, sizeof(AchievementData));
|
||||
file.close();
|
||||
|
||||
Status = EAchStatus::Success;
|
||||
}
|
||||
32
UnleashedRecomp/user/achievement_manager.h
Normal file
32
UnleashedRecomp/user/achievement_manager.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <user/achievement_data.h>
|
||||
|
||||
enum class EAchStatus
|
||||
{
|
||||
Success,
|
||||
IOError,
|
||||
BadFileSize,
|
||||
BadSignature,
|
||||
BadVersion,
|
||||
BadChecksum
|
||||
};
|
||||
|
||||
class AchievementManager
|
||||
{
|
||||
public:
|
||||
static inline AchievementData Data{};
|
||||
static inline EAchStatus Status{};
|
||||
|
||||
static std::filesystem::path GetDataPath(bool checkForMods)
|
||||
{
|
||||
return GetSavePath(checkForMods) / ACH_FILENAME;
|
||||
}
|
||||
|
||||
static time_t GetTimestamp(uint16_t id);
|
||||
static size_t GetTotalRecords();
|
||||
static bool IsUnlocked(uint16_t id);
|
||||
static void Unlock(uint16_t id);
|
||||
static void Load();
|
||||
static void Save(bool ignoreStatus = false);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue