diff --git a/.gitignore b/.gitignore
index 96a91c3..3156dc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@ build
*.nsp
*.nso
-*.npdm
\ No newline at end of file
+*.npdm
+*.zip
+sdout/
+.DS_Store
\ No newline at end of file
diff --git a/README.MD b/README.MD
index 320bf66..90d07b4 100644
--- a/README.MD
+++ b/README.MD
@@ -31,6 +31,23 @@ You have two sections:
Order doesn’t matter.
Just add the program ID(s) under whichever environment you want them blocked in.
+### Firmware Version Conditions (Optional)
+
+Each program ID can optionally include a firmware version condition.
+When a condition is set, the program is **only blocked if the current firmware does NOT match the condition**.
+
+Supported operators: `=` (equal), `>=`, `<=`, `>`, `<`
+
+Format: ` = `
+
+Examples:
+```
+[SysNand]
+0100000000001000 ; No condition: always blocked in SysNAND
+010000000000XXXX = 16.0.0 ; Blocked unless firmware == 16.0.0
+010000000000YYYY = >=17.0.0 ; Blocked unless firmware >= 17.0.0
+```
+
## Example
For instance this config will block themes on ``SysNand``.
@@ -41,5 +58,13 @@ For instance this config will block themes on ``SysNand``.
[EmuNand]
```
+With firmware version conditions — only block a theme if firmware is not 16.0.0:
+```
+[SysNand]
+0100000000001000 = 16.0.0
+
+[EmuNand]
+```
+
## Thanks to
- Arcdelta for motivating me to actually finish this lmao.
diff --git a/sysmodule/Makefile b/sysmodule/Makefile
index e0fc730..5aa30ef 100644
--- a/sysmodule/Makefile
+++ b/sysmodule/Makefile
@@ -146,15 +146,29 @@ ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif
-.PHONY: $(BUILD) clean all
+.PHONY: $(BUILD) clean dist all
+
+TITLE_ID := 420000000000042A
+SD_OUT := sdout
#---------------------------------------------------------------------------------
-all: $(BUILD)
+# default: build + package SD card layout
+dist: $(BUILD)
+ @echo packaging SD layout ...
+ @mkdir -p $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/flags
+ @mkdir -p $(SD_OUT)/config/sys-env
+ @cp $(OUTPUT).nsp $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/exefs.nsp
+ @touch $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/flags/boot2.flag
+ @cp config.ini.template $(SD_OUT)/config/sys-env/config.ini.example
+ @cd $(SD_OUT) && zip -r ../$(TARGET).zip .
+ @echo built $(TARGET).zip
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
+all: dist
+
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@@ -163,7 +177,7 @@ ifeq ($(strip $(APP_JSON)),)
else
@rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf
endif
-
+ @rm -fr $(SD_OUT) $(TARGET).zip
#---------------------------------------------------------------------------------
else
diff --git a/sysmodule/config.ini.template b/sysmodule/config.ini.template
new file mode 100644
index 0000000..4edaf1d
--- /dev/null
+++ b/sysmodule/config.ini.template
@@ -0,0 +1,31 @@
+;
+; sys-env configuration
+; Path: sdmc:/config/sys-env/config.ini
+;
+; ========== Format ==========
+;
+; [SysNand] programs to block in SysNAND
+; [EmuNand] programs to block in EmuNAND
+;
+; Each entry: <16-char program ID>[ = ]
+;
+; Without version condition: always blocked in this environment
+; With version condition: blocked only when firmware does NOT match the condition
+;
+; ========== Version Operators ==========
+; = 16.0.0 exact match (equal)
+; >= 16.0.0 greater than or equal
+; <= 16.0.0 less than or equal
+; > 16.0.0 greater than
+; < 16.0.0 less than
+;
+; ========== Examples ==========
+
+[SysNand]
+;0100000000001000 ; Always block themes in SysNAND
+;010000000000XXXX = 16.0.0 ; Block unless firmware is 16.0.0
+;010000000000YYYY = >=17.0.0 ; Block unless firmware >= 17.0.0
+
+[EmuNand]
+;0100000000001000 ; Always block themes in EmuNAND
+;010000000000XXXX = 16.0.0 ; Block unless firmware is 16.0.0
diff --git a/sysmodule/source/fs.cpp b/sysmodule/source/fs.cpp
index 032d3d9..3daa750 100644
--- a/sysmodule/source/fs.cpp
+++ b/sysmodule/source/fs.cpp
@@ -1,13 +1,12 @@
#include
#include
#include
-#include
#include
#include
-#include
#include
#include
#include "result.hpp"
+#include "fs.hpp"
namespace fs {
@@ -36,7 +35,22 @@ namespace fs {
return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
}
- Result EditContent(std::vector &matchList, std::string &env, std::string &del) {
+ bool CheckVersion(u32 currentVer, const ProgramEntry &entry) {
+ if (!entry.hasVersionCheck) {
+ return true;
+ }
+
+ switch (entry.op) {
+ case OP_EQ: return currentVer == entry.version;
+ case OP_GE: return currentVer >= entry.version;
+ case OP_LE: return currentVer <= entry.version;
+ case OP_GT: return currentVer > entry.version;
+ case OP_LT: return currentVer < entry.version;
+ default: return false;
+ }
+ }
+
+ Result EditContent(std::vector &matchList, std::string &env, std::string &del, u32 currentVer) {
DIR *dir = opendir(CONTENTS);
if (!dir) {
return SYSENV_RC(SysEnvResult_OpenContentsFailed);
@@ -60,11 +74,45 @@ namespace fs {
modified.erase(modified.length() - del.length());
}
- bool inList = std::find(matchList.begin(), matchList.end(), name) != matchList.end() || std::find(matchList.begin(), matchList.end(), modified) != matchList.end();
+ // Find matching ProgramEntry
+ // Also try stripping the current env suffix to match previously blocked directories
+ std::string matchName = modified;
+ if (EndsWith(matchName, env)) {
+ matchName.erase(matchName.length() - env.length());
+ }
+
+ ProgramEntry *matched = nullptr;
+ for (auto &pe : matchList) {
+ if (name == pe.id || modified == pe.id || matchName == pe.id) {
+ matched = &pe;
+ break;
+ }
+ }
+
+ if (matched != nullptr) {
+ bool shouldBlock;
+ if (matched->hasVersionCheck) {
+ // Has version condition: block only when firmware doesn't match
+ shouldBlock = !CheckVersion(currentVer, *matched);
+ } else {
+ // No version condition: always block (original behavior)
+ shouldBlock = true;
+ }
- if (inList) {
- if (!EndsWith(modified, env)) {
- modified += env;
+ if (shouldBlock) {
+ if (!EndsWith(modified, env)) {
+ modified += env;
+ }
+ } else {
+ // Should not block: remove env suffix to restore loading
+ if (EndsWith(modified, env)) {
+ modified.erase(modified.length() - env.length());
+ }
+ }
+ } else {
+ // Not in matchList: clean up orphaned .bak from removed entries
+ if (EndsWith(modified, env)) {
+ modified.erase(modified.length() - env.length());
}
}
@@ -96,7 +144,98 @@ namespace fs {
return SYSENV_RC(SysEnvResult_HeaderMissing);
}
- void ReadConfigEntries(std::ifstream &file, std::vector &entries) {
+ // Parse the version condition part of a config line, e.g. "= >=16.0.0" or "= 16.0.0"
+ Result ParseVersionCondition(const std::string &condition, ProgramEntry &entry) {
+ std::string cond = condition;
+
+ // Strip leading and trailing spaces
+ size_t pos = 0;
+ while (pos < cond.size() && cond[pos] == ' ') {
+ pos++;
+ }
+ cond = cond.substr(pos);
+ while (!cond.empty() && cond.back() == ' ') {
+ cond.pop_back();
+ }
+
+ if (cond.empty()) {
+ return SYSENV_RC(SysEnvResult_InvalidVersionFormat);
+ }
+
+ // Detect operator
+ u8 op;
+ size_t verStart;
+ if (cond.compare(0, 2, ">=") == 0) {
+ op = OP_GE;
+ verStart = 2;
+ } else if (cond.compare(0, 2, "<=") == 0) {
+ op = OP_LE;
+ verStart = 2;
+ } else if (cond[0] == '>') {
+ op = OP_GT;
+ verStart = 1;
+ } else if (cond[0] == '<') {
+ op = OP_LT;
+ verStart = 1;
+ } else if (cond[0] == '=') {
+ op = OP_EQ;
+ verStart = 1;
+ } else {
+ // No operator: default to exact match (supports bare "16.0.0")
+ op = OP_EQ;
+ verStart = 0;
+ }
+
+ std::string verStr = cond.substr(verStart);
+ // Strip spaces before version number
+ pos = 0;
+ while (pos < verStr.size() && verStr[pos] == ' ') {
+ pos++;
+ }
+ verStr = verStr.substr(pos);
+
+ // Parse MAJOR.MINOR.MICRO
+ u32 major = 0, minor = 0, micro = 0;
+ int dots = 0;
+ std::string part;
+ for (char c : verStr) {
+ if (c >= '0' && c <= '9') {
+ part += c;
+ } else if (c == '.') {
+ if (part.empty()) {
+ return SYSENV_RC(SysEnvResult_InvalidVersionFormat);
+ }
+ if (dots == 0) {
+ major = std::stoul(part);
+ } else if (dots == 1) {
+ minor = std::stoul(part);
+ }
+ part.clear();
+ dots++;
+ } else {
+ return SYSENV_RC(SysEnvResult_InvalidVersionFormat);
+ }
+ }
+ if (!part.empty()) {
+ if (dots == 0) {
+ major = std::stoul(part);
+ } else {
+ micro = std::stoul(part);
+ }
+ }
+
+ if (dots > 2) {
+ return SYSENV_RC(SysEnvResult_InvalidVersionFormat);
+ }
+
+ entry.hasVersionCheck = true;
+ entry.op = op;
+ entry.version = MAKEHOSVERSION(major, minor, micro);
+
+ R_SUCCEED();
+ }
+
+ void ReadConfigEntries(std::ifstream &file, std::vector &entries) {
std::string line;
while (std::getline(file, line)) {
if (!line.empty() && line.back() == '\r') {
@@ -111,7 +250,34 @@ namespace fs {
continue;
}
- entries.push_back(line);
+ ProgramEntry entry;
+ entry.hasVersionCheck = false;
+ entry.op = OP_EQ;
+ entry.version = 0;
+
+ // Find '=' delimiter to split program ID and version condition
+ size_t eqPos = line.find('=');
+ if (eqPos != std::string::npos) {
+ entry.id = line.substr(0, eqPos);
+ // Trim trailing spaces from program ID
+ while (!entry.id.empty() && entry.id.back() == ' ') {
+ entry.id.pop_back();
+ }
+
+ std::string cond = line.substr(eqPos + 1);
+ Result rc = ParseVersionCondition(cond, entry);
+ if (R_FAILED(rc)) {
+ // Parse failed: skip this line and log
+ Log("Failed to parse version condition: %s", line.c_str());
+ continue;
+ }
+ } else {
+ entry.id = line;
+ }
+
+ if (!entry.id.empty()) {
+ entries.push_back(entry);
+ }
}
}
@@ -141,7 +307,7 @@ namespace fs {
return SYSENV_RC(SysEnvResult_EmptyConfig);
}
- Result ParseConfig(std::vector &entries, bool emuNand) {
+ Result ParseConfig(std::vector &entries, bool emuNand) {
R_TRY(EnsureConfigExists());
std::ifstream file(PATH);
diff --git a/sysmodule/source/fs.hpp b/sysmodule/source/fs.hpp
index 5610fd6..08ad8e8 100644
--- a/sysmodule/source/fs.hpp
+++ b/sysmodule/source/fs.hpp
@@ -4,8 +4,23 @@
namespace fs {
+ enum VersionOp {
+ OP_EQ = 0, // =
+ OP_GE, // >=
+ OP_LE, // <=
+ OP_GT, // >
+ OP_LT, // <
+ };
+
+ struct ProgramEntry {
+ std::string id;
+ bool hasVersionCheck; // true if firmware version condition is set
+ u8 op; // VersionOp value
+ u32 version; // MAKEHOSVERSION(major, minor, micro)
+ };
+
void Log(const char *log, ...);
- void EditContent(std::vector &matchList, std::string &env, std::string &del);
- Result ParseConfig(std::vector &entries, bool emuNand);
+ Result EditContent(std::vector &matchList, std::string &env, std::string &del, u32 currentVer);
+ Result ParseConfig(std::vector &entries, bool emuNand);
}
diff --git a/sysmodule/source/main.cpp b/sysmodule/source/main.cpp
index 84a7729..bba6165 100644
--- a/sysmodule/source/main.cpp
+++ b/sysmodule/source/main.cpp
@@ -59,8 +59,9 @@ bool IsEmuNand() {
}
int main() {
- std::vector entries;
+ std::vector entries;
bool isEmunand = IsEmuNand();
+ u32 currentVer = hosversionGet();
Result rc = fs::ParseConfig(entries, isEmunand);
if (R_FAILED(rc)) {
@@ -77,7 +78,7 @@ int main() {
del = ".emu.bak";
}
- fs::EditContent(entries, env, del);
+ fs::EditContent(entries, env, del, currentVer);
return 0;
}
diff --git a/sysmodule/source/result.hpp b/sysmodule/source/result.hpp
index 0feac01..59501b1 100644
--- a/sysmodule/source/result.hpp
+++ b/sysmodule/source/result.hpp
@@ -12,6 +12,7 @@ enum SysEnvResult {
SysEnvResult_CreateFileFailed,
SysEnvResult_OpenContentsFailed,
SysEnvResult_RenameFailed,
+ SysEnvResult_InvalidVersionFormat,
};
#define SYSENV_RC(x) MAKERESULT(SysEnvModule, x)