diff --git a/include/emulator.hpp b/include/emulator.hpp
index 207bef46..4ed31945 100644
--- a/include/emulator.hpp
+++ b/include/emulator.hpp
@@ -132,6 +132,7 @@ class Emulator {
 	ServiceManager& getServiceManager() { return kernel.getServiceManager(); }
 	LuaManager& getLua() { return lua; }
 	Scheduler& getScheduler() { return scheduler; }
+	Memory& getMemory() { return memory; }
 
 	RendererType getRendererType() const { return config.rendererType; }
 	Renderer* getRenderer() { return gpu.getRenderer(); }
diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp
index c5ef2465..42ce1590 100644
--- a/include/loader/ncch.hpp
+++ b/include/loader/ncch.hpp
@@ -36,6 +36,7 @@ struct NCCH {
 	};
 
 	u64 partitionIndex = 0;
+	u64 programID = 0;
 	u64 fileOffset = 0;
 
 	bool isNew3DS = false;
diff --git a/include/lua_manager.hpp b/include/lua_manager.hpp
index 50b8dd61..46fd553a 100644
--- a/include/lua_manager.hpp
+++ b/include/lua_manager.hpp
@@ -2,7 +2,6 @@
 #include <string>
 
 #include "helpers.hpp"
-#include "memory.hpp"
 
 // The kinds of events that can cause a Lua call.
 // Frame: Call program on frame end
@@ -11,6 +10,8 @@ enum class LuaEvent {
 	Frame,
 };
 
+class Emulator;
+
 #ifdef PANDA3DS_ENABLE_LUA
 extern "C" {
 #include <lauxlib.h>
@@ -30,9 +31,9 @@ class LuaManager {
   public:
 	// For Lua we must have some global pointers to our emulator objects to use them in script code via thunks. See the thunks in lua.cpp as an
 	// example
-	static Memory* g_memory;
+	static Emulator* g_emulator;
 
-	LuaManager(Memory& mem) { g_memory = &mem; }
+	LuaManager(Emulator& emulator) { g_emulator = &emulator; }
 
 	void close();
 	void initialize();
@@ -51,7 +52,7 @@ class LuaManager {
 #else  // Lua not enabled, Lua manager does nothing
 class LuaManager {
   public:
-	LuaManager(Memory& mem) {}
+	LuaManager(Emulator& emulator) {}
 
 	void close() {}
 	void initialize() {}
diff --git a/include/memory.hpp b/include/memory.hpp
index e2716e2a..1b6e622c 100644
--- a/include/memory.hpp
+++ b/include/memory.hpp
@@ -275,6 +275,8 @@ private:
 	// File handle for reading the loaded ncch
 	IOFile CXIFile;
 
+	std::optional<u64> getProgramID();
+
 	u8* getDSPMem() { return dspRam; }
 	u8* getDSPDataMem() { return &dspRam[DSP_DATA_MEMORY_OFFSET]; }
 	u8* getDSPCodeMem() { return &dspRam[DSP_CODE_MEMORY_OFFSET]; }
diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp
index 1f534fa0..3bf73e5d 100644
--- a/src/core/loader/ncch.cpp
+++ b/src/core/loader/ncch.cpp
@@ -32,7 +32,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
 	size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
 	exheaderSize = *(u32*)&header[0x180];
 
-	const u64 programID = *(u64*)&header[0x118];
+	programID = *(u64*)&header[0x118];
 
 	// Read NCCH flags
 	secondaryKeySlot = header[0x188 + 3];
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index fdc8c475..09b49eee 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -524,4 +524,14 @@ void Memory::copySharedFont(u8* pointer) {
 	auto fonts = cmrc::ConsoleFonts::get_filesystem();
 	auto font = fonts.open("CitraSharedFontUSRelocated.bin");
 	std::memcpy(pointer, font.begin(), font.size());
+}
+
+std::optional<u64> Memory::getProgramID() {
+	auto cxi = getCXI();
+
+	if (cxi) {
+		return cxi->programID;
+	}
+
+	return std::nullopt;
 }
\ No newline at end of file
diff --git a/src/emulator.cpp b/src/emulator.cpp
index dbbb0c37..4481731f 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -18,7 +18,7 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
 
 Emulator::Emulator()
 	: config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config),
-	  cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false)
+	  cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false), programRunning(false)
 #ifdef PANDA3DS_ENABLE_HTTP_SERVER
 	  ,
 	  httpServer(this)
diff --git a/src/lua.cpp b/src/lua.cpp
index 09c63173..f7b0a8a1 100644
--- a/src/lua.cpp
+++ b/src/lua.cpp
@@ -1,4 +1,5 @@
 #ifdef PANDA3DS_ENABLE_LUA
+#include "emulator.hpp"
 #include "lua_manager.hpp"
 
 #ifndef __ANDROID__
@@ -42,7 +43,7 @@ void LuaManager::loadFile(const char* path) {
 	if (!initialized) {
 		initialize();
 	}
-	
+
 	// If init failed, don't execute
 	if (!initialized) {
 		printf("Lua initialization failed, file won't run\n");
@@ -88,8 +89,8 @@ void LuaManager::loadString(const std::string& code) {
 }
 
 void LuaManager::signalEventInternal(LuaEvent e) {
-	lua_getglobal(L, "eventHandler"); // We want to call the event handler
-	lua_pushnumber(L, static_cast<int>(e)); // Push event type
+	lua_getglobal(L, "eventHandler");        // We want to call the event handler
+	lua_pushnumber(L, static_cast<int>(e));  // Push event type
 
 	// Call the function with 1 argument and 0 outputs, without an error handler
 	lua_pcall(L, 1, 0, 0);
@@ -103,28 +104,46 @@ void LuaManager::reset() {
 // Initialize C++ thunks for Lua code to call here
 // All code beyond this point is terrible and full of global state, don't judge
 
-Memory* LuaManager::g_memory = nullptr;
+Emulator* LuaManager::g_emulator = nullptr;
 
-#define MAKE_MEMORY_FUNCTIONS(size)                                 \
-	static int read##size##Thunk(lua_State* L) {                    \
-		const u32 vaddr = (u32)lua_tonumber(L, 1);                  \
-		lua_pushnumber(L, LuaManager::g_memory->read##size(vaddr)); \
-		return 1;                                                   \
-	}                                                               \
-	static int write##size##Thunk(lua_State* L) {                   \
-		const u32 vaddr = (u32)lua_tonumber(L, 1);                  \
-		const u##size value = (u##size)lua_tonumber(L, 2);          \
-		LuaManager::g_memory->write##size(vaddr, value);            \
-		return 0;                                                   \
+#define MAKE_MEMORY_FUNCTIONS(size)                                               \
+	static int read##size##Thunk(lua_State* L) {                                  \
+		const u32 vaddr = (u32)lua_tonumber(L, 1);                                \
+		lua_pushnumber(L, LuaManager::g_emulator->getMemory().read##size(vaddr)); \
+		return 1;                                                                 \
+	}                                                                             \
+	static int write##size##Thunk(lua_State* L) {                                 \
+		const u32 vaddr = (u32)lua_tonumber(L, 1);                                \
+		const u##size value = (u##size)lua_tonumber(L, 2);                        \
+		LuaManager::g_emulator->getMemory().write##size(vaddr, value);            \
+		return 0;                                                                 \
 	}
 
-
 MAKE_MEMORY_FUNCTIONS(8)
 MAKE_MEMORY_FUNCTIONS(16)
 MAKE_MEMORY_FUNCTIONS(32)
 MAKE_MEMORY_FUNCTIONS(64)
 #undef MAKE_MEMORY_FUNCTIONS
 
+static int getAppIDThunk(lua_State* L) {
+	std::optional<u64> id = LuaManager::g_emulator->getMemory().getProgramID();
+	
+	// If the app has an ID, return true + its ID
+	// Otherwise return false and 0 as the ID
+	if (id.has_value()) {
+		lua_pushboolean(L, 1);    // Return true
+		lua_pushnumber(L, u32(*id));  // Return bottom 32 bits
+		lua_pushnumber(L, u32(*id >> 32));  // Return top 32 bits
+	} else {
+		lua_pushboolean(L, 0);  // Return false
+		// Return no ID
+		lua_pushnumber(L, 0);
+		lua_pushnumber(L, 0);
+	}
+
+	return 3;
+}
+
 // clang-format off
 static constexpr luaL_Reg functions[] = {
 	{ "__read8", read8Thunk },
@@ -135,6 +154,7 @@ static constexpr luaL_Reg functions[] = {
 	{ "__write16", write16Thunk },
 	{ "__write32", write32Thunk },
 	{ "__write64", write64Thunk },
+	{ "__getAppID", getAppIDThunk },
 	{ nullptr, nullptr },
 };
 // clang-format on
@@ -150,6 +170,15 @@ void LuaManager::initializeThunks() {
 		write16 = function(addr, value) GLOBALS.__write16(addr, value) end,
 		write32 = function(addr, value) GLOBALS.__write32(addr, value) end,
 		write64 = function(addr, value) GLOBALS.__write64(addr, value) end,
+
+		getAppID = function()
+			local ffi = require("ffi")
+
+			result, low, high = GLOBALS.__getAppID()
+			id = bit.bor(ffi.cast("uint64_t", low), (bit.lshift(ffi.cast("uint64_t", high), 32)))
+			return result, id
+		end,
+
 		Frame = __Frame,
 	}
 )";