mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-04-28 13:27:58 +03:00
Fix DLC info message always being displayed (#1393)
* Fix DLC info message always being displayed * Use uint32_t for version numbers * Make AchievementManager follow the same naming convention as PersistentStorageManager * persistent_data: remove header size field * Make status success by default or on file not existing
This commit is contained in:
parent
4b4c439709
commit
1dd5ba7fcd
15 changed files with 281 additions and 60 deletions
|
@ -183,6 +183,8 @@ set(UNLEASHED_RECOMP_USER_CXX_SOURCES
|
|||
"user/config.cpp"
|
||||
"user/registry.cpp"
|
||||
"user/paths.cpp"
|
||||
"user/persistent_data.cpp"
|
||||
"user/persistent_storage_manager.cpp"
|
||||
)
|
||||
|
||||
set(UNLEASHED_RECOMP_MOD_CXX_SOURCES
|
||||
|
|
|
@ -80,9 +80,13 @@ namespace SWA
|
|||
boost::shared_ptr<Hedgehog::Mirage::CRenderScene> m_spRenderScene;
|
||||
SWA_INSERT_PADDING(0x04);
|
||||
boost::shared_ptr<CGameParameter> m_spGameParameter;
|
||||
SWA_INSERT_PADDING(0x78);
|
||||
SWA_INSERT_PADDING(0x0C);
|
||||
boost::anonymous_shared_ptr m_spItemParamManager;
|
||||
SWA_INSERT_PADDING(0x64);
|
||||
boost::shared_ptr<Hedgehog::Base::CCriticalSection> m_spCriticalSection;
|
||||
SWA_INSERT_PADDING(0x20);
|
||||
SWA_INSERT_PADDING(0x14);
|
||||
bool m_ShowDLCInfo;
|
||||
SWA_INSERT_PADDING(0x08);
|
||||
};
|
||||
|
||||
// TODO: Hedgehog::Base::TSynchronizedPtr<CApplicationDocument>
|
||||
|
@ -111,7 +115,9 @@ namespace SWA
|
|||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_Field10C, 0x10C);
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spRenderScene, 0x12C);
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spGameParameter, 0x138);
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spItemParamManager, 0x14C);
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spCriticalSection, 0x1B8);
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_ShowDLCInfo, 0x1D4);
|
||||
SWA_ASSERT_SIZEOF(CApplicationDocument::CMember, 0x1E0);
|
||||
|
||||
SWA_ASSERT_OFFSETOF(CApplicationDocument, m_pMember, 0x04);
|
||||
|
|
|
@ -7,11 +7,19 @@ namespace SWA
|
|||
class CGameParameter // : public Hedgehog::Universe::CMessageActor
|
||||
{
|
||||
public:
|
||||
struct SSaveData;
|
||||
struct SSaveData
|
||||
{
|
||||
SWA_INSERT_PADDING(0x8600);
|
||||
be<uint32_t> DLCFlags[8];
|
||||
SWA_INSERT_PADDING(0x15C);
|
||||
};
|
||||
|
||||
struct SStageParameter;
|
||||
|
||||
SWA_INSERT_PADDING(0x94);
|
||||
xpointer<SSaveData> m_pSaveData;
|
||||
xpointer<SStageParameter> m_pStageParameter;
|
||||
};
|
||||
|
||||
SWA_ASSERT_OFFSETOF(CGameParameter::SSaveData, DLCFlags, 0x8600);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <hid/hid.h>
|
||||
#include <user/config.h>
|
||||
#include <user/paths.h>
|
||||
#include <user/persistent_storage_manager.h>
|
||||
#include <user/registry.h>
|
||||
#include <kernel/xdbf.h>
|
||||
#include <install/installer.h>
|
||||
|
@ -225,6 +226,9 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
|
||||
Config::Load();
|
||||
|
||||
if (!PersistentStorageManager::LoadBinary())
|
||||
LOGFN_ERROR("Failed to load persistent storage binary... (status code {})", (int)PersistentStorageManager::BinStatus);
|
||||
|
||||
#if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12)
|
||||
for (auto& dll : g_D3D12RequiredModules)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include <api/SWA.h>
|
||||
#include <install/update_checker.h>
|
||||
#include <locale/locale.h>
|
||||
#include <os/logger.h>
|
||||
#include <ui/fader.h>
|
||||
#include <ui/message_window.h>
|
||||
#include <user/achievement_manager.h>
|
||||
|
@ -64,16 +65,16 @@ static bool ProcessCorruptAchievementsMessage()
|
|||
if (!g_corruptAchievementsMessageOpen)
|
||||
return false;
|
||||
|
||||
auto message = AchievementManager::Status == EAchStatus::IOError
|
||||
auto message = AchievementManager::BinStatus == EAchBinStatus::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);
|
||||
// Create a new save file if the file was successfully loaded and failed validation.
|
||||
// If the file couldn't be opened, restarting may fix this error, so it isn't worth clearing the data for.
|
||||
if (AchievementManager::BinStatus != EAchBinStatus::IOError)
|
||||
AchievementManager::SaveBinary(true);
|
||||
|
||||
g_corruptAchievementsMessageOpen = false;
|
||||
g_corruptAchievementsMessageOpen.notify_one();
|
||||
|
@ -135,9 +136,10 @@ void PressStartSaveLoadThreadMidAsmHook()
|
|||
g_faderBegun.wait(true);
|
||||
}
|
||||
|
||||
AchievementManager::Load();
|
||||
if (!AchievementManager::LoadBinary())
|
||||
LOGFN_ERROR("Failed to load achievement data... (status code {})", (int)AchievementManager::BinStatus);
|
||||
|
||||
if (AchievementManager::Status != EAchStatus::Success)
|
||||
if (AchievementManager::BinStatus != EAchBinStatus::Success)
|
||||
{
|
||||
g_corruptAchievementsMessageOpen = true;
|
||||
g_corruptAchievementsMessageOpen.wait(true);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include <api/SWA.h>
|
||||
#include <ui/game_window.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/persistent_storage_manager.h>
|
||||
#include <user/config.h>
|
||||
|
||||
void AchievementManagerUnlockMidAsmHook(PPCRegister& id)
|
||||
|
@ -172,3 +173,23 @@ PPC_FUNC(sub_82B4DB48)
|
|||
|
||||
__imp__sub_82B4DB48(ctx, base);
|
||||
}
|
||||
|
||||
// DLC save data flag check.
|
||||
//
|
||||
// The DLC checks are fundamentally broken in this game, resulting in this method always
|
||||
// returning true and displaying the DLC info message when it shouldn't be.
|
||||
//
|
||||
// The original intent here seems to have been to display the message every time new DLC
|
||||
// content is installed, but the flags in the save data never get written to properly,
|
||||
// causing this function to always pass in some way.
|
||||
//
|
||||
// We bypass the save data completely and write to external persistent storage to store
|
||||
// whether we've seen the DLC info message instead. This way we can retain the original
|
||||
// broken game behaviour, whilst also providing a fix for this issue that is safe.
|
||||
PPC_FUNC_IMPL(__imp__sub_824EE620);
|
||||
PPC_FUNC(sub_824EE620)
|
||||
{
|
||||
__imp__sub_824EE620(ctx, base);
|
||||
|
||||
ctx.r3.u32 = PersistentStorageManager::ShouldDisplayDLCMessage(true);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
#include <hid/hid.h>
|
||||
#include <os/logger.h>
|
||||
#include <user/achievement_manager.h>
|
||||
#include <user/persistent_storage_manager.h>
|
||||
#include <user/config.h>
|
||||
#include <app.h>
|
||||
|
||||
bool m_isSavedAchievementData = false;
|
||||
|
||||
// SWA::Message::MsgRequestStartLoading::Impl
|
||||
PPC_FUNC_IMPL(__imp__sub_824DCF38);
|
||||
PPC_FUNC(sub_824DCF38)
|
||||
|
@ -99,20 +98,23 @@ PPC_FUNC(sub_824E5170)
|
|||
|
||||
App::s_isSaving = pSaveIcon->m_IsVisible;
|
||||
|
||||
static bool isSavedExtraData = false;
|
||||
|
||||
if (pSaveIcon->m_IsVisible)
|
||||
{
|
||||
App::s_isSaveDataCorrupt = false;
|
||||
|
||||
if (!m_isSavedAchievementData)
|
||||
if (!isSavedExtraData)
|
||||
{
|
||||
AchievementManager::Save();
|
||||
AchievementManager::SaveBinary();
|
||||
PersistentStorageManager::SaveBinary();
|
||||
|
||||
m_isSavedAchievementData = true;
|
||||
isSavedExtraData = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_isSavedAchievementData = false;
|
||||
isSavedExtraData = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ bool AchievementData::VerifySignature() const
|
|||
|
||||
bool AchievementData::VerifyVersion() const
|
||||
{
|
||||
return Version == AchVersion ACH_VERSION;
|
||||
return Version <= ACH_VERSION;
|
||||
}
|
||||
|
||||
bool AchievementData::VerifyChecksum()
|
||||
|
|
|
@ -4,27 +4,12 @@
|
|||
|
||||
#define ACH_FILENAME "ACH-DATA"
|
||||
#define ACH_SIGNATURE { 'A', 'C', 'H', ' ' }
|
||||
#define ACH_VERSION { 1, 0, 0 }
|
||||
#define ACH_VERSION 1
|
||||
#define ACH_RECORDS 50
|
||||
|
||||
class AchievementData
|
||||
{
|
||||
public:
|
||||
struct AchVersion
|
||||
{
|
||||
uint8_t Major;
|
||||
uint8_t Minor;
|
||||
uint8_t Revision;
|
||||
uint8_t Reserved;
|
||||
|
||||
bool operator==(const AchVersion& other) const
|
||||
{
|
||||
return Major == other.Major &&
|
||||
Minor == other.Minor &&
|
||||
Revision == other.Revision;
|
||||
}
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct AchRecord
|
||||
{
|
||||
|
@ -35,10 +20,10 @@ public:
|
|||
#pragma pack(pop)
|
||||
|
||||
char Signature[4] ACH_SIGNATURE;
|
||||
AchVersion Version ACH_VERSION;
|
||||
uint32_t Checksum;
|
||||
uint32_t Reserved;
|
||||
AchRecord Records[ACH_RECORDS];
|
||||
uint32_t Version{ ACH_VERSION };
|
||||
uint32_t Checksum{};
|
||||
uint32_t Reserved{};
|
||||
AchRecord Records[ACH_RECORDS]{};
|
||||
|
||||
bool VerifySignature() const;
|
||||
bool VerifyVersion() const;
|
||||
|
|
|
@ -105,11 +105,11 @@ void AchievementManager::Reset()
|
|||
*(bool*)g_memory.Translate(0x833647C4) = false;
|
||||
}
|
||||
|
||||
void AchievementManager::Load()
|
||||
bool AchievementManager::LoadBinary()
|
||||
{
|
||||
AchievementManager::Reset();
|
||||
|
||||
Status = EAchStatus::Success;
|
||||
BinStatus = EAchBinStatus::Success;
|
||||
|
||||
auto dataPath = GetDataPath(true);
|
||||
|
||||
|
@ -119,7 +119,7 @@ void AchievementManager::Load()
|
|||
dataPath = GetDataPath(false);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
|
@ -128,16 +128,16 @@ void AchievementManager::Load()
|
|||
|
||||
if (fileSize != dataSize)
|
||||
{
|
||||
Status = EAchStatus::BadFileSize;
|
||||
return;
|
||||
BinStatus = EAchBinStatus::BadFileSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(dataPath, std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
Status = EAchStatus::IOError;
|
||||
return;
|
||||
BinStatus = EAchBinStatus::IOError;
|
||||
return false;
|
||||
}
|
||||
|
||||
AchievementData data{};
|
||||
|
@ -146,19 +146,18 @@ void AchievementManager::Load()
|
|||
|
||||
if (!data.VerifySignature())
|
||||
{
|
||||
Status = EAchStatus::BadSignature;
|
||||
BinStatus = EAchBinStatus::BadSignature;
|
||||
file.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.read((char*)&data.Version, sizeof(data.Version));
|
||||
|
||||
// TODO: upgrade in future if the version changes.
|
||||
if (!data.VerifyVersion())
|
||||
{
|
||||
Status = EAchStatus::BadVersion;
|
||||
BinStatus = EAchBinStatus::BadVersion;
|
||||
file.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seekg(0);
|
||||
|
@ -166,22 +165,24 @@ void AchievementManager::Load()
|
|||
|
||||
if (!data.VerifyChecksum())
|
||||
{
|
||||
Status = EAchStatus::BadChecksum;
|
||||
BinStatus = EAchBinStatus::BadChecksum;
|
||||
file.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
memcpy(&Data, &data, dataSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AchievementManager::Save(bool ignoreStatus)
|
||||
bool AchievementManager::SaveBinary(bool ignoreStatus)
|
||||
{
|
||||
if (!ignoreStatus && Status != EAchStatus::Success)
|
||||
if (!ignoreStatus && BinStatus != EAchBinStatus::Success)
|
||||
{
|
||||
LOGN_WARNING("Achievement data will not be saved in this session!");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGN("Saving achievements...");
|
||||
|
@ -191,7 +192,7 @@ void AchievementManager::Save(bool ignoreStatus)
|
|||
if (!file)
|
||||
{
|
||||
LOGN_ERROR("Failed to write achievement data.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Data.Checksum = Data.CalculateChecksum();
|
||||
|
@ -199,5 +200,7 @@ void AchievementManager::Save(bool ignoreStatus)
|
|||
file.write((const char*)&Data, sizeof(AchievementData));
|
||||
file.close();
|
||||
|
||||
Status = EAchStatus::Success;
|
||||
BinStatus = EAchBinStatus::Success;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include <user/achievement_data.h>
|
||||
|
||||
enum class EAchStatus
|
||||
enum class EAchBinStatus
|
||||
{
|
||||
Success,
|
||||
IOError,
|
||||
|
@ -16,7 +16,7 @@ class AchievementManager
|
|||
{
|
||||
public:
|
||||
static inline AchievementData Data{};
|
||||
static inline EAchStatus Status{};
|
||||
static inline EAchBinStatus BinStatus{ EAchBinStatus::Success };
|
||||
|
||||
static std::filesystem::path GetDataPath(bool checkForMods)
|
||||
{
|
||||
|
@ -29,6 +29,6 @@ public:
|
|||
static void Unlock(uint16_t id);
|
||||
static void UnlockAll();
|
||||
static void Reset();
|
||||
static void Load();
|
||||
static void Save(bool ignoreStatus = false);
|
||||
static bool LoadBinary();
|
||||
static bool SaveBinary(bool ignoreStatus = false);
|
||||
};
|
||||
|
|
13
UnleashedRecomp/user/persistent_data.cpp
Normal file
13
UnleashedRecomp/user/persistent_data.cpp
Normal file
|
@ -0,0 +1,13 @@
|
|||
#include "persistent_data.h"
|
||||
|
||||
bool PersistentData::VerifySignature() const
|
||||
{
|
||||
char sig[4] = EXT_SIGNATURE;
|
||||
|
||||
return memcmp(Signature, sig, sizeof(Signature)) == 0;
|
||||
}
|
||||
|
||||
bool PersistentData::VerifyVersion() const
|
||||
{
|
||||
return Version <= EXT_VERSION;
|
||||
}
|
30
UnleashedRecomp/user/persistent_data.h
Normal file
30
UnleashedRecomp/user/persistent_data.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <user/paths.h>
|
||||
|
||||
#define EXT_FILENAME "EXT-DATA"
|
||||
#define EXT_SIGNATURE { 'E', 'X', 'T', ' ' }
|
||||
#define EXT_VERSION 1
|
||||
|
||||
enum class EDLCFlag
|
||||
{
|
||||
ApotosAndShamar,
|
||||
Spagonia,
|
||||
Chunnan,
|
||||
Mazuri,
|
||||
Holoska,
|
||||
EmpireCityAndAdabat,
|
||||
Count
|
||||
};
|
||||
|
||||
class PersistentData
|
||||
{
|
||||
public:
|
||||
char Signature[4] EXT_SIGNATURE;
|
||||
uint32_t Version{ EXT_VERSION };
|
||||
uint64_t Reserved{};
|
||||
bool DLCFlags[6]{};
|
||||
|
||||
bool VerifySignature() const;
|
||||
bool VerifyVersion() const;
|
||||
};
|
117
UnleashedRecomp/user/persistent_storage_manager.cpp
Normal file
117
UnleashedRecomp/user/persistent_storage_manager.cpp
Normal file
|
@ -0,0 +1,117 @@
|
|||
#include "persistent_storage_manager.h"
|
||||
#include <install/installer.h>
|
||||
#include <os/logger.h>
|
||||
#include <user/paths.h>
|
||||
|
||||
bool PersistentStorageManager::ShouldDisplayDLCMessage(bool setOffendingDLCFlag)
|
||||
{
|
||||
if (BinStatus != EExtBinStatus::Success)
|
||||
return true;
|
||||
|
||||
static std::unordered_map<EDLCFlag, DLC> flags =
|
||||
{
|
||||
{ EDLCFlag::ApotosAndShamar, DLC::ApotosShamar },
|
||||
{ EDLCFlag::Spagonia, DLC::Spagonia },
|
||||
{ EDLCFlag::Chunnan, DLC::Chunnan },
|
||||
{ EDLCFlag::Mazuri, DLC::Mazuri },
|
||||
{ EDLCFlag::Holoska, DLC::Holoska },
|
||||
{ EDLCFlag::EmpireCityAndAdabat, DLC::EmpireCityAdabat }
|
||||
};
|
||||
|
||||
auto result = false;
|
||||
|
||||
for (auto& pair : flags)
|
||||
{
|
||||
if (!Data.DLCFlags[(int)pair.first] && Installer::checkDLCInstall(GetGamePath(), pair.second))
|
||||
{
|
||||
if (setOffendingDLCFlag)
|
||||
Data.DLCFlags[(int)pair.first] = true;
|
||||
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool PersistentStorageManager::LoadBinary()
|
||||
{
|
||||
BinStatus = EExtBinStatus::Success;
|
||||
|
||||
auto dataPath = GetDataPath(true);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
{
|
||||
// Try loading base persistent data as fallback.
|
||||
dataPath = GetDataPath(false);
|
||||
|
||||
if (!std::filesystem::exists(dataPath))
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
auto fileSize = std::filesystem::file_size(dataPath, ec);
|
||||
auto dataSize = sizeof(PersistentData);
|
||||
|
||||
if (fileSize != dataSize)
|
||||
{
|
||||
BinStatus = EExtBinStatus::BadFileSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(dataPath, std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
BinStatus = EExtBinStatus::IOError;
|
||||
return false;
|
||||
}
|
||||
|
||||
PersistentData data{};
|
||||
|
||||
file.read((char*)&data.Signature, sizeof(data.Signature));
|
||||
|
||||
if (!data.VerifySignature())
|
||||
{
|
||||
BinStatus = EExtBinStatus::BadSignature;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
file.read((char*)&data.Version, sizeof(data.Version));
|
||||
|
||||
if (!data.VerifyVersion())
|
||||
{
|
||||
BinStatus = EExtBinStatus::BadVersion;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seekg(0);
|
||||
file.read((char*)&data, sizeof(data));
|
||||
file.close();
|
||||
|
||||
memcpy(&Data, &data, dataSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PersistentStorageManager::SaveBinary()
|
||||
{
|
||||
LOGN("Saving persistent storage binary...");
|
||||
|
||||
std::ofstream file(GetDataPath(true), std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LOGN_ERROR("Failed to write persistent storage binary.");
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write((const char*)&Data, sizeof(PersistentData));
|
||||
file.close();
|
||||
|
||||
BinStatus = EExtBinStatus::Success;
|
||||
|
||||
return true;
|
||||
}
|
28
UnleashedRecomp/user/persistent_storage_manager.h
Normal file
28
UnleashedRecomp/user/persistent_storage_manager.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <user/persistent_data.h>
|
||||
|
||||
enum class EExtBinStatus
|
||||
{
|
||||
Success,
|
||||
IOError,
|
||||
BadFileSize,
|
||||
BadSignature,
|
||||
BadVersion
|
||||
};
|
||||
|
||||
class PersistentStorageManager
|
||||
{
|
||||
public:
|
||||
static inline PersistentData Data{};
|
||||
static inline EExtBinStatus BinStatus{ EExtBinStatus::Success };
|
||||
|
||||
static std::filesystem::path GetDataPath(bool checkForMods)
|
||||
{
|
||||
return GetSavePath(checkForMods) / EXT_FILENAME;
|
||||
}
|
||||
|
||||
static bool ShouldDisplayDLCMessage(bool setOffendingDLCFlag);
|
||||
static bool LoadBinary();
|
||||
static bool SaveBinary();
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue