Fix CMemoryCardSys

This commit is contained in:
Phillip Stephens 2020-04-15 04:27:06 -07:00
parent 8a974d6e5e
commit e553a9022f
Signed by: Antidote
GPG Key ID: F8BEE4C83DACA60D
6 changed files with 96 additions and 64 deletions

View File

@ -81,10 +81,10 @@ bool SpecBase::canExtract(const ExtractPassInfo& info, std::vector<ExtractReport
case ERegion::NTSC_U: case ERegion::NTSC_U:
regstr = &regE; regstr = &regE;
break; break;
case ERegion::PAL: case ERegion::NTSC_J:
regstr = &regJ; regstr = &regJ;
break; break;
case ERegion::NTSC_J: case ERegion::PAL:
regstr = &regP; regstr = &regP;
break; break;
default: default:

View File

@ -525,7 +525,10 @@ void CMemoryCardSys::CommitToDisk(kabufuda::ECardSlot port) {
kabufuda::SystemString CMemoryCardSys::CreateDolphinCard(kabufuda::ECardSlot slot) { kabufuda::SystemString CMemoryCardSys::CreateDolphinCard(kabufuda::ECardSlot slot) {
kabufuda::SystemString path = _CreateDolphinCard(slot); kabufuda::SystemString path = _CreateDolphinCard(slot);
CardProbe(slot); if (CardProbe(slot).x0_error != ECardResult::READY) {
return {};
}
MountCard(slot); MountCard(slot);
FormatCard(slot); FormatCard(slot);
kabufuda::Card& card = g_CardStates[int(slot)]; kabufuda::Card& card = g_CardStates[int(slot)];

View File

@ -71,12 +71,6 @@
#include <discord_rpc.h> #include <discord_rpc.h>
namespace hecl {
extern CVar* com_enableCheats;
extern CVar* com_developer;
extern CVar* com_cubemaps;
}; // namespace hecl
namespace urde::MP1 { namespace urde::MP1 {
namespace { namespace {
struct AudioGroupInfo { struct AudioGroupInfo {
@ -99,7 +93,7 @@ CGameArchitectureSupport::CGameArchitectureSupport(CMain& parent, boo::IAudioVoi
, x0_audioSys(voiceEngine, backend, 0, 0, 0, 0, 0) , x0_audioSys(voiceEngine, backend, 0, 0, 0, 0, 0)
, x30_inputGenerator(g_tweakPlayer->GetLeftLogicalThreshold(), g_tweakPlayer->GetRightLogicalThreshold()) , x30_inputGenerator(g_tweakPlayer->GetLeftLogicalThreshold(), g_tweakPlayer->GetRightLogicalThreshold())
, x44_guiSys(*g_ResFactory, *g_SimplePool, CGuiSys::EUsageMode::Zero) { , x44_guiSys(*g_ResFactory, *g_SimplePool, CGuiSys::EUsageMode::Zero) {
CMain* m = static_cast<CMain*>(g_Main); auto* m = static_cast<CMain*>(g_Main);
x30_inputGenerator.startScanning(); x30_inputGenerator.startScanning();
g_InputGenerator = &x30_inputGenerator; g_InputGenerator = &x30_inputGenerator;
@ -145,8 +139,9 @@ void CGameArchitectureSupport::Update(float dt) {
} }
bool CGameArchitectureSupport::LoadAudio() { bool CGameArchitectureSupport::LoadAudio() {
if (x88_audioLoadStatus == EAudioLoadStatus::Loaded) if (x88_audioLoadStatus == EAudioLoadStatus::Loaded) {
return true; return true;
}
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
TToken<CAudioGroupSet>& tok = x8c_pendingAudioGroups[i]; TToken<CAudioGroupSet>& tok = x8c_pendingAudioGroups[i];
@ -345,24 +340,27 @@ CGameGlobalObjects::~CGameGlobalObjects() {
g_TweakManager = nullptr; g_TweakManager = nullptr;
} }
void CGameGlobalObjects::PostInitialize() { void CGameGlobalObjects::PostInitialize() {
AddPaksAndFactories(); AddPaksAndFactories();
LoadTextureCache(); LoadTextureCache();
LoadStringTable(); LoadStringTable();
m_renderer.reset(AllocateRenderer(*xcc_simplePool, *x4_resFactory)); m_renderer.reset(AllocateRenderer(*xcc_simplePool, *x4_resFactory));
CEnvFxManager::Initialize(); CEnvFxManager::Initialize();
CScriptMazeNode::LoadMazeSeeds(); CScriptMazeNode::LoadMazeSeeds();
} }
void CMain::AddWorldPaks() { void CMain::AddWorldPaks() {
CResLoader* loader = g_ResFactory->GetResLoader(); CResLoader* loader = g_ResFactory->GetResLoader();
if (!loader) if (loader == nullptr) {
return; return;
}
auto pakPrefix = g_tweakGame->GetWorldPrefix(); auto pakPrefix = g_tweakGame->GetWorldPrefix();
for (int i = 0; i < 9; ++i) { for (int i = 0; i < 9; ++i) {
std::string path(pakPrefix); std::string path(pakPrefix);
if (i != 0) if (i != 0) {
path += '0' + i; path += '0' + char(i);
}
if (CDvdFile::FileExists(path + ".upak")) { if (CDvdFile::FileExists(path + ".upak")) {
loader->AddPakFileAsync(path, false, true); loader->AddPakFileAsync(path, false, true);
@ -373,8 +371,9 @@ void CMain::AddWorldPaks() {
void CMain::AddOverridePaks() { void CMain::AddOverridePaks() {
CResLoader* loader = g_ResFactory->GetResLoader(); CResLoader* loader = g_ResFactory->GetResLoader();
if (!loader) if (loader == nullptr) {
return; return;
}
/* Inversely load each pak starting at 999, to ensure proper priority order /* Inversely load each pak starting at 999, to ensure proper priority order
* the higher the number the higer the priority, e.g: Override0 has less priority than Override1 etc. * the higher the number the higer the priority, e.g: Override0 has less priority than Override1 etc.
@ -391,8 +390,9 @@ void CMain::AddOverridePaks() {
/* Attempt to load URDE.upak /* Attempt to load URDE.upak
* NOTE(phil): Should we fatal here if it's not found? * NOTE(phil): Should we fatal here if it's not found?
*/ */
if (CDvdFile::FileExists("URDE.upak")) if (CDvdFile::FileExists("URDE.upak")) {
loader->AddPakFile("URDE", false, false, true); loader->AddPakFile("URDE", false, false, true);
}
} }
void CMain::ResetGameState() { void CMain::ResetGameState() {
@ -417,7 +417,7 @@ void CMain::InitializeSubsystems() {
} }
void CMain::MemoryCardInitializePump() { void CMain::MemoryCardInitializePump() {
if (g_MemoryCardSys) { if (g_MemoryCardSys != nullptr) {
return; return;
} }
@ -438,8 +438,9 @@ void CMain::FillInAssetIDs() {
} }
bool CMain::LoadAudio() { bool CMain::LoadAudio() {
if (x164_archSupport) if (x164_archSupport) {
return x164_archSupport->LoadAudio(); return x164_archSupport->LoadAudio();
}
return true; return true;
} }
@ -449,8 +450,9 @@ void CMain::EnsureWorldPakReady(CAssetId mlvl) { /* TODO: Schedule resource list
} }
void CMain::Give(hecl::Console* console, const std::vector<std::string>& args) { void CMain::Give(hecl::Console* console, const std::vector<std::string>& args) {
if (args.size() < 1 || (!g_GameState || !g_GameState->GetPlayerState())) if (args.empty() || (g_GameState == nullptr || !g_GameState->GetPlayerState())) {
return; return;
}
std::string type = args[0]; std::string type = args[0];
athena::utility::tolower(type); athena::utility::tolower(type);
@ -475,8 +477,9 @@ void CMain::Give(hecl::Console* console, const std::vector<std::string>& args) {
pState->IncrPickup(eType, 9999); pState->IncrPickup(eType, 9999);
console->report(hecl::Console::Level::Info, console->report(hecl::Console::Level::Info,
FMT_STRING("Cheater....., Greatly increasing Metroid encounters, have fun!")); FMT_STRING("Cheater....., Greatly increasing Metroid encounters, have fun!"));
if (g_StateManager) if (g_StateManager != nullptr) {
g_StateManager->Player()->AsyncLoadSuit(*g_StateManager); g_StateManager->Player()->AsyncLoadSuit(*g_StateManager);
}
return; return;
} }
@ -493,23 +496,28 @@ void CMain::Give(hecl::Console* console, const std::vector<std::string>& args) {
if (eType == CPlayerState::EItemType::Missiles) { if (eType == CPlayerState::EItemType::Missiles) {
u32 tmp = ((u32(itemAmt) / 5) + (itemAmt % 5)) * 5; u32 tmp = ((u32(itemAmt) / 5) + (itemAmt % 5)) * 5;
pState->ReInitalizePowerUp(eType, tmp); pState->ReInitalizePowerUp(eType, tmp);
} else } else {
pState->ReInitalizePowerUp(eType, itemAmt); pState->ReInitalizePowerUp(eType, itemAmt);
}
} }
if (itemAmt > 0) if (itemAmt > 0) {
pState->IncrPickup(eType, u32(itemAmt)); pState->IncrPickup(eType, u32(itemAmt));
else } else {
pState->DecrPickup(eType, zeus::clamp(0u, u32(abs(itemAmt)), pState->GetItemAmount(eType))); pState->DecrPickup(eType, zeus::clamp(0u, u32(abs(itemAmt)), pState->GetItemAmount(eType)));
}
} }
if (g_StateManager) if (g_StateManager != nullptr) {
g_StateManager->Player()->AsyncLoadSuit(*g_StateManager); g_StateManager->Player()->AsyncLoadSuit(*g_StateManager);
console->report(hecl::Console::Level::Info, FMT_STRING("Cheater....., Greatly increasing Metroid encounters, have fun!")); }
console->report(hecl::Console::Level::Info,
FMT_STRING("Cheater....., Greatly increasing Metroid encounters, have fun!"));
} // namespace MP1 } // namespace MP1
void CMain::Remove(hecl::Console*, const std::vector<std::string>& args) { void CMain::Remove(hecl::Console*, const std::vector<std::string>& args) {
if (args.size() < 1 || (!g_GameState || !g_GameState->GetPlayerState())) if (args.empty() || (g_GameState == nullptr || !g_GameState->GetPlayerState())) {
return; return;
}
std::string type = args[0]; std::string type = args[0];
athena::utility::tolower(type); athena::utility::tolower(type);
@ -522,37 +530,42 @@ void CMain::Remove(hecl::Console*, const std::vector<std::string>& args) {
CPlayerState::EItemType eType = CPlayerState::ItemNameToType(type); CPlayerState::EItemType eType = CPlayerState::ItemNameToType(type);
if (eType != CPlayerState::EItemType::Invalid) { if (eType != CPlayerState::EItemType::Invalid) {
pState->ReInitalizePowerUp(eType, 0); pState->ReInitalizePowerUp(eType, 0);
if (g_StateManager) if (g_StateManager != nullptr) {
g_StateManager->Player()->AsyncLoadSuit(*g_StateManager); g_StateManager->Player()->AsyncLoadSuit(*g_StateManager);
}
} }
} }
} }
void CMain::God(hecl::Console* con, const std::vector<std::string>&) { void CMain::God(hecl::Console* con, const std::vector<std::string>&) {
if (g_GameState && g_GameState->GetPlayerState()) { if (g_GameState != nullptr && g_GameState->GetPlayerState()) {
g_GameState->GetPlayerState()->SetCanTakeDamage(!g_GameState->GetPlayerState()->CanTakeDamage()); g_GameState->GetPlayerState()->SetCanTakeDamage(!g_GameState->GetPlayerState()->CanTakeDamage());
if (!g_GameState->GetPlayerState()->CanTakeDamage()) if (!g_GameState->GetPlayerState()->CanTakeDamage()) {
con->report(hecl::Console::Level::Info, FMT_STRING("God Mode Enabled")); con->report(hecl::Console::Level::Info, FMT_STRING("God Mode Enabled"));
else } else {
con->report(hecl::Console::Level::Info, FMT_STRING("God Mode Disabled")); con->report(hecl::Console::Level::Info, FMT_STRING("God Mode Disabled"));
}
} }
} }
void CMain::Teleport(hecl::Console*, const std::vector<std::string>& args) { void CMain::Teleport(hecl::Console*, const std::vector<std::string>& args) {
if (!g_StateManager || args.size() < 3) if (g_StateManager == nullptr || args.size() < 3) {
return; return;
}
zeus::CVector3f loc; zeus::CVector3f loc;
for (u32 i = 0; i < 3; ++i) for (u32 i = 0; i < 3; ++i) {
loc[i] = strtof(args[i].c_str(), nullptr); loc[i] = strtof(args[i].c_str(), nullptr);
}
zeus::CTransform xf = g_StateManager->Player()->GetTransform(); zeus::CTransform xf = g_StateManager->Player()->GetTransform();
xf.origin = loc; xf.origin = loc;
if (args.size() >= 6) { if (args.size() >= 6) {
zeus::CVector3f angle; zeus::CVector3f angle;
for (u32 i = 0; i < 3; ++i) for (u32 i = 0; i < 3; ++i) {
angle[i] = zeus::degToRad(strtof(args[i + 3].c_str(), nullptr)); angle[i] = zeus::degToRad(strtof(args[i + 3].c_str(), nullptr));
}
xf.setRotation(zeus::CMatrix3f(zeus::CQuaternion(angle))); xf.setRotation(zeus::CMatrix3f(zeus::CQuaternion(angle)));
} }
g_StateManager->Player()->Teleport(xf, *g_StateManager, false); g_StateManager->Player()->Teleport(xf, *g_StateManager, false);
@ -560,41 +573,47 @@ void CMain::Teleport(hecl::Console*, const std::vector<std::string>& args) {
void CMain::ListWorlds(hecl::Console* con, const std::vector<std::string>&) { void CMain::ListWorlds(hecl::Console* con, const std::vector<std::string>&) {
if (g_ResFactory && g_ResFactory->GetResLoader()) { if (g_ResFactory != nullptr && g_ResFactory->GetResLoader() != nullptr) {
for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) {
if (pak->IsWorldPak()) { if (pak->IsWorldPak()) {
for (const auto& named : pak->GetNameList()) for (const auto& named : pak->GetNameList()) {
if (named.second.type == SBIG('MLVL')) { if (named.second.type == SBIG('MLVL')) {
con->report(hecl::Console::Level::Info, FMT_STRING("{} '{}'"), named.first, named.second.id); con->report(hecl::Console::Level::Info, FMT_STRING("{} '{}'"), named.first, named.second.id);
} }
}
} }
}
} }
} }
void CMain::Warp(hecl::Console* con, const std::vector<std::string>& args) { void CMain::Warp(hecl::Console* con, const std::vector<std::string>& args) {
if (!g_StateManager) if (g_StateManager == nullptr) {
return; return;
}
if (args.size() < 1) if (args.empty()) {
return; return;
}
TAreaId aId; TAreaId aId = 0;
std::string worldName; std::string worldName;
if (args.size() == 2) { if (args.size() == 2) {
worldName = args[0]; worldName = args[0];
athena::utility::tolower(worldName); athena::utility::tolower(worldName);
aId = strtol(args[1].c_str(), nullptr, 10); aId = strtol(args[1].c_str(), nullptr, 10);
} else } else {
aId = strtol(args[0].c_str(), nullptr, 10); aId = strtol(args[0].c_str(), nullptr, 10);
}
if (!worldName.empty() && g_ResFactory && g_ResFactory->GetResLoader()) { if (!worldName.empty() && g_ResFactory != nullptr && g_ResFactory->GetResLoader() != nullptr) {
bool found = false; bool found = false;
for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) { for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) {
if (found) if (found) {
break; break;
}
if (pak->IsWorldPak()) { if (pak->IsWorldPak()) {
for (const auto& named : pak->GetNameList()) for (const auto& named : pak->GetNameList()) {
if (named.second.type == SBIG('MLVL')) { if (named.second.type == SBIG('MLVL')) {
std::string name = named.first; std::string name = named.first;
athena::utility::tolower(name); athena::utility::tolower(name);
@ -604,14 +623,16 @@ void CMain::Warp(hecl::Console* con, const std::vector<std::string>& args) {
break; break;
} }
} }
}
} }
} }
} }
g_GameState->GetWorldTransitionManager()->DisableTransition(); g_GameState->GetWorldTransitionManager()->DisableTransition();
if (aId >= g_GameState->CurrentWorldState().GetLayerState()->GetAreaCount()) if (aId >= g_GameState->CurrentWorldState().GetLayerState()->GetAreaCount()) {
aId = 0; aId = 0;
}
g_GameState->CurrentWorldState().SetAreaId(aId); g_GameState->CurrentWorldState().SetAreaId(aId);
g_Main->SetFlowState(EFlowState::None); g_Main->SetFlowState(EFlowState::None);
@ -678,7 +699,7 @@ void CMain::UpdateDiscordPresence(CAssetId worldSTRG) {
updated = true; updated = true;
} }
if (g_GameState) { if (g_GameState != nullptr) {
if (CPlayerState* pState = g_GameState->GetPlayerState().get()) { if (CPlayerState* pState = g_GameState->GetPlayerState().get()) {
u32 itemPercent = pState->CalculateItemCollectionRate() * 100 / pState->GetPickupTotal(); u32 itemPercent = pState->CalculateItemCollectionRate() * 100 / pState->GetPickupTotal();
if (DiscordItemPercent != itemPercent) { if (DiscordItemPercent != itemPercent) {
@ -699,7 +720,9 @@ void CMain::UpdateDiscordPresence(CAssetId worldSTRG) {
} }
} }
void CMain::HandleDiscordReady(const DiscordUser* request) { DiscordLog.report(logvisor::Info, FMT_STRING("Discord Ready")); } void CMain::HandleDiscordReady(const DiscordUser* request) {
DiscordLog.report(logvisor::Info, FMT_STRING("Discord Ready"));
}
void CMain::HandleDiscordDisconnected(int errorCode, const char* message) { void CMain::HandleDiscordDisconnected(int errorCode, const char* message) {
DiscordLog.report(logvisor::Warning, FMT_STRING("Discord Disconnected: {}"), message); DiscordLog.report(logvisor::Warning, FMT_STRING("Discord Disconnected: {}"), message);
@ -758,13 +781,15 @@ void CMain::Init(const hecl::Runtime::FileStoreManager& storeMgr, hecl::CVarMana
const hecl::SystemChar* worldIdxStr = (*(it + 1)).c_str(); const hecl::SystemChar* worldIdxStr = (*(it + 1)).c_str();
const hecl::SystemChar* areaIdxStr = (*(it + 2)).c_str(); const hecl::SystemChar* areaIdxStr = (*(it + 2)).c_str();
hecl::SystemChar* endptr; hecl::SystemChar* endptr = nullptr;
m_warpWorldIdx = TAreaId(hecl::StrToUl(worldIdxStr, &endptr, 0)); m_warpWorldIdx = TAreaId(hecl::StrToUl(worldIdxStr, &endptr, 0));
if (endptr == worldIdxStr) if (endptr == worldIdxStr) {
m_warpWorldIdx = 0; m_warpWorldIdx = 0;
}
m_warpAreaId = TAreaId(hecl::StrToUl(areaIdxStr, &endptr, 0)); m_warpAreaId = TAreaId(hecl::StrToUl(areaIdxStr, &endptr, 0));
if (endptr == areaIdxStr) if (endptr == areaIdxStr) {
m_warpAreaId = 0; m_warpAreaId = 0;
}
bool found = false; bool found = false;
for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) { for (const auto& pak : g_ResFactory->GetResLoader()->GetPaks()) {
@ -787,7 +812,7 @@ void CMain::Init(const hecl::Runtime::FileStoreManager& storeMgr, hecl::CVarMana
if (*cur == _SYS_STR('1')) if (*cur == _SYS_STR('1'))
m_warpLayerBits |= u64(1) << (cur - layerStr); m_warpLayerBits |= u64(1) << (cur - layerStr);
} else if (layerStr[0] == _SYS_STR('0') && layerStr[1] == _SYS_STR('x')) { } else if (layerStr[0] == _SYS_STR('0') && layerStr[1] == _SYS_STR('x')) {
m_warpMemoryRelays.push_back(TAreaId(hecl::StrToUl(layerStr + 2, nullptr, 16))); m_warpMemoryRelays.emplace_back(TAreaId(hecl::StrToUl(layerStr + 2, nullptr, 16)));
} }
++it; ++it;
} }
@ -801,21 +826,22 @@ void CMain::Init(const hecl::Runtime::FileStoreManager& storeMgr, hecl::CVarMana
x164_archSupport = std::make_unique<CGameArchitectureSupport>(*this, voiceEngine, backend); x164_archSupport = std::make_unique<CGameArchitectureSupport>(*this, voiceEngine, backend);
g_archSupport = x164_archSupport.get(); g_archSupport = x164_archSupport.get();
x164_archSupport->PreloadAudio(); x164_archSupport->PreloadAudio();
std::srand(static_cast<unsigned int>(std::time(nullptr))); std::srand(static_cast<u32>(std::time(nullptr)));
// g_TweakManager->ReadFromMemoryCard("AudioTweaks"); // g_TweakManager->ReadFromMemoryCard("AudioTweaks");
} }
static logvisor::Module WarmupLog("ShaderWarmup"); static logvisor::Module WarmupLog("ShaderWarmup");
void CMain::WarmupShaders() { void CMain::WarmupShaders() {
if (m_warmupTags.size()) if (!m_warmupTags.empty())
return; return;
m_needsWarmupClear = true; m_needsWarmupClear = true;
size_t modelCount = 0; size_t modelCount = 0;
g_ResFactory->EnumerateResources([&](const SObjectTag& tag) { g_ResFactory->EnumerateResources([&](const SObjectTag& tag) {
if (tag.type == FOURCC('CMDL') || tag.type == FOURCC('MREA')) if (tag.type == FOURCC('CMDL') || tag.type == FOURCC('MREA')) {
++modelCount; ++modelCount;
}
return true; return true;
}); });
m_warmupTags.reserve(modelCount); m_warmupTags.reserve(modelCount);
@ -825,8 +851,9 @@ void CMain::WarmupShaders() {
g_ResFactory->EnumerateResources([&](const SObjectTag& tag) { g_ResFactory->EnumerateResources([&](const SObjectTag& tag) {
if (tag.type == FOURCC('CMDL') || tag.type == FOURCC('MREA')) { if (tag.type == FOURCC('CMDL') || tag.type == FOURCC('MREA')) {
if (addedTags.find(tag) != addedTags.end()) if (addedTags.find(tag) != addedTags.end()) {
return true; return true;
}
addedTags.insert(tag); addedTags.insert(tag);
m_warmupTags.push_back(tag); m_warmupTags.push_back(tag);
} }

View File

@ -134,7 +134,7 @@ class CGameArchitectureSupport
std::vector<TToken<CAudioGroupSet>> x8c_pendingAudioGroups; std::vector<TToken<CAudioGroupSet>> x8c_pendingAudioGroups;
boo::SWindowRect m_windowRect; boo::SWindowRect m_windowRect;
bool m_rectIsDirty; bool m_rectIsDirty = false;
void destroyed() { x4_archQueue.Push(MakeMsg::CreateRemoveAllIOWins(EArchMsgTarget::IOWinManager)); } void destroyed() { x4_archQueue.Push(MakeMsg::CreateRemoveAllIOWins(EArchMsgTarget::IOWinManager)); }

View File

@ -45,8 +45,10 @@ void CEntity::AcceptScriptMsg(EScriptObjectMessage msg, TUniqueId objId, CStateM
} }
void CEntity::SendScriptMsgs(EScriptObjectState state, CStateManager& stateMgr, EScriptObjectMessage skipMsg) { void CEntity::SendScriptMsgs(EScriptObjectState state, CStateManager& stateMgr, EScriptObjectMessage skipMsg) {
for (const SConnection& conn : x20_conns) for (const SConnection& conn : x20_conns) {
if (conn.x0_state == state && conn.x4_msg != skipMsg) if (conn.x0_state == state && conn.x4_msg != skipMsg) {
stateMgr.SendScriptMsg(x8_uid, conn.x8_objId, conn.x4_msg, state); stateMgr.SendScriptMsg(x8_uid, conn.x8_objId, conn.x4_msg, state);
}
}
} }
} // namespace urde } // namespace urde

@ -1 +1 @@
Subproject commit 6d8d389459243a38efc585dc03b877b4697cdcf2 Subproject commit 4891a9568856bb775d0889b47c3808880803423b