596 lines
18 KiB
C
596 lines
18 KiB
C
#include <3ds.h>
|
|
#include "ifile.h"
|
|
#include "utils.h" // for makeARMBranch
|
|
#include "luma_config.h"
|
|
#include "plugin.h"
|
|
#include "fmt.h"
|
|
#include "menu.h"
|
|
#include "menus.h"
|
|
#include "memory.h"
|
|
#include "sleep.h"
|
|
#include "task_runner.h"
|
|
|
|
#define PLGLDR_VERSION (SYSTEM_VERSION(1, 0, 1))
|
|
|
|
#define THREADVARS_MAGIC 0x21545624 // !TV$
|
|
|
|
static const char *g_title = "Plugin loader";
|
|
PluginLoaderContext PluginLoaderCtx;
|
|
extern u32 g_blockMenuOpen;
|
|
|
|
void IR__Patch(void);
|
|
void IR__Unpatch(void);
|
|
|
|
void PluginLoader__Init(void)
|
|
{
|
|
PluginLoaderContext *ctx = &PluginLoaderCtx;
|
|
|
|
memset(ctx, 0, sizeof(PluginLoaderContext));
|
|
|
|
s64 pluginLoaderFlags = 0;
|
|
|
|
svcGetSystemInfo(&pluginLoaderFlags, 0x10000, 0x180);
|
|
ctx->isEnabled = pluginLoaderFlags & 1;
|
|
|
|
ctx->plgEventPA = (s32 *)PA_FROM_VA_PTR(&ctx->plgEvent);
|
|
ctx->plgReplyPA = (s32 *)PA_FROM_VA_PTR(&ctx->plgReply);
|
|
|
|
ctx->pluginMemoryStrategy = PLG_STRATEGY_SWAP;
|
|
|
|
MemoryBlock__ResetSwapSettings();
|
|
|
|
assertSuccess(svcCreateAddressArbiter(&ctx->arbiter));
|
|
assertSuccess(svcCreateEvent(&ctx->kernelEvent, RESET_ONESHOT));
|
|
|
|
svcKernelSetState(0x10007, ctx->kernelEvent, 0, 0);
|
|
}
|
|
|
|
void PluginLoader__Error(const char *message, Result res)
|
|
{
|
|
DispErrMessage(g_title, message, res);
|
|
}
|
|
|
|
bool PluginLoader__IsEnabled(void)
|
|
{
|
|
return PluginLoaderCtx.isEnabled;
|
|
}
|
|
|
|
void PluginLoader__MenuCallback(void)
|
|
{
|
|
PluginLoaderCtx.isEnabled = !PluginLoaderCtx.isEnabled;
|
|
LumaConfig_RequestSaveSettings();
|
|
PluginLoader__UpdateMenu();
|
|
}
|
|
|
|
void PluginLoader__UpdateMenu(void)
|
|
{
|
|
static const char *status[2] =
|
|
{
|
|
"Plugin Loader: [Disabled]",
|
|
"Plugin Loader: [Enabled]"
|
|
};
|
|
|
|
rosalinaMenu.items[3].title = status[PluginLoaderCtx.isEnabled];
|
|
}
|
|
|
|
static ControlApplicationMemoryModeOverrideConfig g_memorymodeoverridebackup = { 0 };
|
|
Result PluginLoader__SetMode3AppMode(bool enable)
|
|
{
|
|
Handle loaderHandle;
|
|
Result res = srvGetServiceHandle(&loaderHandle, "Loader");
|
|
|
|
if (R_FAILED(res)) return res;
|
|
|
|
u32 *cmdbuf = getThreadCommandBuffer();
|
|
|
|
if (enable) {
|
|
ControlApplicationMemoryModeOverrideConfig* mode = (ControlApplicationMemoryModeOverrideConfig*)&cmdbuf[1];
|
|
|
|
memset(mode, 0, sizeof(ControlApplicationMemoryModeOverrideConfig));
|
|
mode->query = true;
|
|
cmdbuf[0] = IPC_MakeHeader(0x101, 1, 0); // ControlApplicationMemoryModeOverride
|
|
|
|
if (R_SUCCEEDED((res = svcSendSyncRequest(loaderHandle))) && R_SUCCEEDED(res = cmdbuf[1]))
|
|
{
|
|
memcpy(&g_memorymodeoverridebackup, &cmdbuf[2], sizeof(ControlApplicationMemoryModeOverrideConfig));
|
|
|
|
memset(mode, 0, sizeof(ControlApplicationMemoryModeOverrideConfig));
|
|
mode->enable_o3ds = true;
|
|
mode->o3ds_mode = SYSMODE_DEV2;
|
|
cmdbuf[0] = IPC_MakeHeader(0x101, 1, 0); // ControlApplicationMemoryModeOverride
|
|
if (R_SUCCEEDED((res = svcSendSyncRequest(loaderHandle)))) {
|
|
res = cmdbuf[1];
|
|
}
|
|
}
|
|
} else {
|
|
ControlApplicationMemoryModeOverrideConfig* mode = (ControlApplicationMemoryModeOverrideConfig*)&cmdbuf[1];
|
|
*mode = g_memorymodeoverridebackup;
|
|
cmdbuf[0] = IPC_MakeHeader(0x101, 1, 0); // ControlApplicationMemoryModeOverride
|
|
if (R_SUCCEEDED((res = svcSendSyncRequest(loaderHandle)))) {
|
|
res = cmdbuf[1];
|
|
}
|
|
}
|
|
|
|
svcCloseHandle(loaderHandle);
|
|
return res;
|
|
}
|
|
static void j_PluginLoader__SetMode3AppMode(void* arg) {(void)arg; PluginLoader__SetMode3AppMode(false);}
|
|
|
|
void CheckMemory(void);
|
|
|
|
void PLG__NotifyEvent(PLG_Event event, bool signal);
|
|
|
|
void PluginLoader__HandleCommands(void *_ctx)
|
|
{
|
|
(void)_ctx;
|
|
|
|
u32 *cmdbuf = getThreadCommandBuffer();
|
|
PluginLoaderContext *ctx = &PluginLoaderCtx;
|
|
|
|
switch (cmdbuf[0] >> 16)
|
|
{
|
|
case 1: // Load plugin
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(1, 1, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
ctx->plgEvent = PLG_OK;
|
|
svcOpenProcess(&ctx->target, cmdbuf[1]);
|
|
|
|
if (ctx->useUserLoadParameters && ctx->userLoadParameters.pluginMemoryStrategy == PLG_STRATEGY_MODE3)
|
|
TaskRunner_RunTask(j_PluginLoader__SetMode3AppMode, NULL, 0);
|
|
|
|
bool flash = !(ctx->useUserLoadParameters && ctx->userLoadParameters.noFlash);
|
|
if (ctx->isEnabled && TryToLoadPlugin(ctx->target))
|
|
{
|
|
if (flash)
|
|
{
|
|
// A little flash to notify the user that the plugin is loaded
|
|
for (u32 i = 0; i < 64; i++)
|
|
{
|
|
REG32(0x10202204) = 0x01FF9933;
|
|
svcSleepThread(5000000);
|
|
}
|
|
REG32(0x10202204) = 0;
|
|
}
|
|
//if (!ctx->userLoadParameters.noIRPatch)
|
|
// IR__Patch();
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_RUNNING);
|
|
}
|
|
else
|
|
{
|
|
svcCloseHandle(ctx->target);
|
|
ctx->target = 0;
|
|
}
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(1, 1, 0);
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 2: // Check if plugin loader is enabled
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(2, 0, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(2, 2, 0);
|
|
cmdbuf[1] = 0;
|
|
cmdbuf[2] = (u32)ctx->isEnabled;
|
|
break;
|
|
}
|
|
|
|
case 3: // Enable / Disable plugin loader
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(3, 1, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
if (cmdbuf[1] != ctx->isEnabled)
|
|
{
|
|
ctx->isEnabled = cmdbuf[1];
|
|
LumaConfig_RequestSaveSettings();
|
|
PluginLoader__UpdateMenu();
|
|
}
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(3, 1, 0);
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 4: // Define next plugin load settings
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(4, 2, 4))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
PluginLoadParameters *params = &ctx->userLoadParameters;
|
|
|
|
ctx->useUserLoadParameters = true;
|
|
params->noFlash = cmdbuf[1] & 0xFF;
|
|
params->pluginMemoryStrategy = (cmdbuf[1] >> 8) & 0xFF;
|
|
params->lowTitleId = cmdbuf[2];
|
|
|
|
strncpy(params->path, (const char *)cmdbuf[4], 255);
|
|
memcpy(params->config, (void *)cmdbuf[6], 32 * sizeof(u32));
|
|
|
|
if (params->pluginMemoryStrategy == PLG_STRATEGY_MODE3)
|
|
cmdbuf[1] = PluginLoader__SetMode3AppMode(true);
|
|
else
|
|
cmdbuf[1] = 0;
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(4, 1, 0);
|
|
break;
|
|
}
|
|
|
|
case 5: // Display menu
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(5, 1, 8))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
u32 nbItems = cmdbuf[1];
|
|
u32 states = cmdbuf[3];
|
|
DisplayPluginMenu(cmdbuf);
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(5, 1, 2);
|
|
cmdbuf[1] = 0;
|
|
cmdbuf[2] = IPC_Desc_Buffer(nbItems, IPC_BUFFER_RW);
|
|
cmdbuf[3] = states;
|
|
break;
|
|
}
|
|
|
|
case 6: // Display message
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(6, 0, 4))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
const char *title = (const char *)cmdbuf[2];
|
|
const char *body = (const char *)cmdbuf[4];
|
|
|
|
DispMessage(title, body);
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(6, 1, 0);
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 7: // Display error message
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(7, 1, 4))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
const char *title = (const char *)cmdbuf[3];
|
|
const char *body = (const char *)cmdbuf[5];
|
|
|
|
DispErrMessage(title, body, cmdbuf[1]);
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(7, 1, 0);
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 8: // Get PLGLDR Version
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(8, 0, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(8, 2, 0);
|
|
cmdbuf[1] = 0;
|
|
cmdbuf[2] = PLGLDR_VERSION;
|
|
break;
|
|
}
|
|
|
|
case 9: // Get the arbiter (events)
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(9, 0, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(9, 1, 2);
|
|
cmdbuf[1] = 0;
|
|
cmdbuf[2] = IPC_Desc_SharedHandles(1);
|
|
cmdbuf[3] = ctx->arbiter;
|
|
break;
|
|
}
|
|
|
|
case 10: // Get plugin path
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(10, 0, 2))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
char *path = (char *)cmdbuf[2];
|
|
strncpy(path, ctx->pluginPath, 255);
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(10, 1, 2);
|
|
cmdbuf[1] = 0;
|
|
cmdbuf[2] = IPC_Desc_Buffer(255, IPC_BUFFER_RW);
|
|
cmdbuf[3] = (u32)path;
|
|
|
|
break;
|
|
}
|
|
|
|
case 11: // Set rosalina menu block
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(11, 1, 0))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
|
|
g_blockMenuOpen = cmdbuf[1];
|
|
|
|
cmdbuf[0] = IPC_MakeHeader(11, 1, 0);
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 12: // Set swap settings
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(12, 2, 4))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
cmdbuf[0] = IPC_MakeHeader(12, 1, 0);
|
|
MemoryBlock__ResetSwapSettings();
|
|
if (!cmdbuf[1] || !cmdbuf[2]) {
|
|
cmdbuf[1] = MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_INVALID_ADDRESS);
|
|
break;
|
|
}
|
|
|
|
u32* remoteSavePhysAddr = (u32*)(cmdbuf[1] | (1 << 31));
|
|
u32* remoteLoadPhysAddr = (u32*)(cmdbuf[2] | (1 << 31));
|
|
|
|
Result ret = MemoryBlock__SetSwapSettings(remoteSavePhysAddr, false, (u32*)cmdbuf[4]);
|
|
if (!ret) ret = MemoryBlock__SetSwapSettings(remoteLoadPhysAddr, true, (u32*)cmdbuf[4]);
|
|
|
|
if (ret) {
|
|
cmdbuf[1] = MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_TOO_LARGE);
|
|
MemoryBlock__ResetSwapSettings();
|
|
break;
|
|
}
|
|
|
|
ctx->isSwapFunctionset = true;
|
|
|
|
if (((char*)cmdbuf[6])[0] != '\0') strncpy(g_swapFileName, (char*)cmdbuf[6], 255);
|
|
|
|
svcInvalidateEntireInstructionCache(); // Could use the range one
|
|
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
case 13: // Set plugin exe load func
|
|
{
|
|
if (cmdbuf[0] != IPC_MakeHeader(13, 1, 2))
|
|
{
|
|
error(cmdbuf, 0xD9001830);
|
|
break;
|
|
}
|
|
cmdbuf[0] = IPC_MakeHeader(13, 1, 0);
|
|
Reset_3gx_LoadParams();
|
|
if (!cmdbuf[1]) {
|
|
cmdbuf[1] = MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_INVALID_ADDRESS);
|
|
break;
|
|
}
|
|
|
|
u32* remoteLoadExeFuncAddr = (u32*)(cmdbuf[1] | (1 << 31));
|
|
Result ret = Set_3gx_LoadParams(remoteLoadExeFuncAddr, (u32*)cmdbuf[3]);
|
|
if (ret)
|
|
{
|
|
cmdbuf[1] = MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_TOO_LARGE);
|
|
Reset_3gx_LoadParams();
|
|
break;
|
|
}
|
|
|
|
ctx->isExeLoadFunctionset = true;
|
|
|
|
svcInvalidateEntireInstructionCache(); // Could use the range one
|
|
|
|
cmdbuf[1] = 0;
|
|
break;
|
|
}
|
|
|
|
default: // Unknown command
|
|
{
|
|
error(cmdbuf, 0xD900182F);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ctx->error.message)
|
|
{
|
|
PluginLoader__Error(ctx->error.message, ctx->error.code);
|
|
ctx->error.message = NULL;
|
|
ctx->error.code = 0;
|
|
}
|
|
}
|
|
|
|
static bool ThreadPredicate(u32 *kthread)
|
|
{
|
|
// Check if the thread is part of the plugin
|
|
u32 *tls = (u32 *)kthread[0x26];
|
|
|
|
return *tls == THREADVARS_MAGIC;
|
|
}
|
|
|
|
static void __strex__(s32 *addr, s32 val)
|
|
{
|
|
do
|
|
__ldrex(addr);
|
|
while (__strex(addr, val));
|
|
}
|
|
|
|
void PLG__NotifyEvent(PLG_Event event, bool signal)
|
|
{
|
|
if (!PluginLoaderCtx.plgEventPA) return;
|
|
|
|
__strex__(PluginLoaderCtx.plgEventPA, event);
|
|
if (signal)
|
|
svcArbitrateAddress(PluginLoaderCtx.arbiter, (u32)PluginLoaderCtx.plgEventPA, ARBITRATION_SIGNAL, 1, 0);
|
|
}
|
|
|
|
void PLG__WaitForReply(void)
|
|
{
|
|
__strex__(PluginLoaderCtx.plgReplyPA, PLG_WAIT);
|
|
svcArbitrateAddress(PluginLoaderCtx.arbiter, (u32)PluginLoaderCtx.plgReplyPA, ARBITRATION_WAIT_IF_LESS_THAN_TIMEOUT, PLG_OK, 10000000000ULL);
|
|
}
|
|
|
|
void PLG__SetConfigMemoryStatus(u32 status)
|
|
{
|
|
*(vu32 *)PA_FROM_VA_PTR(0x1FF800F0) = status;
|
|
}
|
|
|
|
u32 PLG__GetConfigMemoryStatus(void)
|
|
{
|
|
return (*(vu32 *)PA_FROM_VA_PTR((u32 *)0x1FF800F0)) & 0xFFFF;
|
|
}
|
|
|
|
u32 PLG__GetConfigMemoryEvent(void)
|
|
{
|
|
return (*(vu32 *)PA_FROM_VA_PTR(0x1FF800F0)) & ~0xFFFF;
|
|
}
|
|
|
|
static void WaitForProcessTerminated(void *arg)
|
|
{
|
|
(void)arg;
|
|
PluginLoaderContext *ctx = &PluginLoaderCtx;
|
|
|
|
// Wait until all threads of the process have finished (svcWaitSynchronization == 0) or 5 seconds have passed.
|
|
for (u32 i = 0; svcWaitSynchronization(ctx->target, 0) != 0 && i < 100; i++) svcSleepThread(50000000); // 50ms
|
|
|
|
// Unmap plugin's memory before closing the process
|
|
if (!ctx->pluginIsSwapped) {
|
|
MemoryBlock__UnmountFromProcess();
|
|
MemoryBlock__Free();
|
|
}
|
|
// Terminate process
|
|
svcCloseHandle(ctx->target);
|
|
// Reset plugin loader state
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_NONE);
|
|
ctx->pluginIsSwapped = false;
|
|
ctx->pluginIsHome = false;
|
|
ctx->target = 0;
|
|
ctx->isExeLoadFunctionset = false;
|
|
ctx->isSwapFunctionset = false;
|
|
ctx->pluginMemoryStrategy = PLG_STRATEGY_SWAP;
|
|
g_blockMenuOpen = 0;
|
|
MemoryBlock__ResetSwapSettings();
|
|
//if (!ctx->userLoadParameters.noIRPatch)
|
|
// IR__Unpatch();
|
|
}
|
|
|
|
void PluginLoader__HandleKernelEvent(u32 notifId)
|
|
{
|
|
(void)notifId;
|
|
if (PLG__GetConfigMemoryStatus() == PLG_CFG_EXITING)
|
|
{
|
|
srvPublishToSubscriber(0x1002, 0);
|
|
return;
|
|
}
|
|
|
|
PluginLoaderContext *ctx = &PluginLoaderCtx;
|
|
u32 event = PLG__GetConfigMemoryEvent();
|
|
|
|
if (event == PLG_CFG_EXIT_EVENT)
|
|
{
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_EXITING);
|
|
if (!ctx->pluginIsSwapped)
|
|
{
|
|
// Signal the plugin that the game is exiting
|
|
PLG__NotifyEvent(PLG_ABOUT_TO_EXIT, false);
|
|
// Wait for plugin reply
|
|
PLG__WaitForReply();
|
|
}
|
|
// Start a task to wait for process to be terminated
|
|
TaskRunner_RunTask(WaitForProcessTerminated, NULL, 0);
|
|
}
|
|
else if (event == PLG_CFG_HOME_EVENT)
|
|
{
|
|
if ((ctx->pluginMemoryStrategy == PLG_STRATEGY_SWAP) && !isN3DS) {
|
|
if (ctx->pluginIsSwapped)
|
|
{
|
|
// Reload data from swap file
|
|
MemoryBlock__IsReady();
|
|
MemoryBlock__FromSwapFile();
|
|
MemoryBlock__MountInProcess();
|
|
// Unlock plugin threads
|
|
svcControlProcess(ctx->target, PROCESSOP_SCHEDULE_THREADS, 0, (u32)ThreadPredicate);
|
|
// Resume plugin execution
|
|
PLG__NotifyEvent(PLG_OK, true);
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_RUNNING);
|
|
}
|
|
else
|
|
{
|
|
// Signal plugin that it's about to be swapped
|
|
PLG__NotifyEvent(PLG_ABOUT_TO_SWAP, false);
|
|
// Wait for plugin reply
|
|
PLG__WaitForReply();
|
|
// Lock plugin threads
|
|
svcControlProcess(ctx->target, PROCESSOP_SCHEDULE_THREADS, 1, (u32)ThreadPredicate);
|
|
// Put data into file and release memory
|
|
MemoryBlock__UnmountFromProcess();
|
|
MemoryBlock__ToSwapFile();
|
|
MemoryBlock__Free();
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_INHOME);
|
|
}
|
|
ctx->pluginIsSwapped = !ctx->pluginIsSwapped;
|
|
} else {
|
|
// Needed for compatibility with old plugins that don't expect the PLG_HOME events.
|
|
// Evades cache by using physical address.
|
|
volatile PluginHeader* mappedHeader = MemoryBlock__GetMappedPluginHeader();
|
|
mappedHeader = mappedHeader ? PA_FROM_VA_PTR(mappedHeader) : NULL;
|
|
bool doNotification = mappedHeader ? mappedHeader->notifyHomeEvent : false;
|
|
if (ctx->pluginIsHome)
|
|
{
|
|
if (doNotification) {
|
|
// Signal plugin that it's about to exit home menu
|
|
PLG__NotifyEvent(PLG_HOME_EXIT, false);
|
|
// Wait for plugin reply
|
|
PLG__WaitForReply();
|
|
}
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_RUNNING);
|
|
}
|
|
else
|
|
{
|
|
if (doNotification) {
|
|
// Signal plugin that it's about to enter home menu
|
|
PLG__NotifyEvent(PLG_HOME_ENTER, false);
|
|
// Wait for plugin reply
|
|
PLG__WaitForReply();
|
|
}
|
|
PLG__SetConfigMemoryStatus(PLG_CFG_INHOME);
|
|
}
|
|
ctx->pluginIsHome = !ctx->pluginIsHome;
|
|
}
|
|
|
|
}
|
|
srvPublishToSubscriber(0x1002, 0);
|
|
}
|