Compare commits

...
48 Commits
Author SHA1 Message Date
niklascfw 414d2f2c24 Version bump to 1.0.10
Build / Build (push) Successful in 14s
2026-10-10 18:50:18 +02:00
niklascfw eef6bbaa33 ci: remove .net setup
Build / Build (push) Successful in 12s
Build Desktop Installer / Build (win-arm64) (push) Successful in 30s
Build Desktop Installer / Build (win-x64) (push) Successful in 8s
2026-09-13 23:02:27 +02:00
niklascfw c5b74c52d3 Show pack versions and a Nintendo-server block notice on confirm.
Build / Build (push) Successful in 29s
Build Desktop Installer / Build (win-arm64) (push) Has been cancelled
Build Desktop Installer / Build (win-x64) (push) Has been cancelled
The installer now displays current vs incoming OmniNX version and states that Nintendo server connections are suppressed by default.
2026-09-13 22:08:08 +02:00
niklascfw d573f6cca4 Add a Windows desktop installer for PC-based OmniNX installs.
This lets mass deployments and modchip setups apply the same install/update flow from a PC, with generated deletion lists kept in sync with the payload headers.
2026-09-13 21:10:58 +02:00
niklascfw c87fe1f1e8 Version bump to 1.0.3
Build / Build (push) Successful in 14s
2026-08-22 15:28:45 +02:00
niklascfw 73c2789114 Preserve Checkpoint saves across clean install backup/restore.
Back up and restore sd:/switch/Checkpoint with CyberFoil and DBI settings during clean installs.
2026-08-22 15:26:02 +02:00
niklascfw 405bed7291 Version bump to 1.0.2
Build / Build (push) Successful in 23s
2026-07-21 13:52:23 +02:00
niklascfw 3a2fb4fb2e Migrate legacy ram_config.ini into config.ini on extras post-copy.
Port [Ram] from ram_config.ini when present, preserve existing HekateAms settings, then remove the legacy file.
2026-07-21 13:50:33 +02:00
niklascfw eb18bf8c72 Preserve Checkpoint across update installs.
Build / Build (push) Successful in 13s
Skip deleting sd:/switch/checkpoint so user saves under saves/ are not wiped.
2026-07-17 21:37:20 +02:00
niklascfw 05926f7828 Show Install/Update completion based on detected mode.
Build / Build (push) Successful in 14s
Drop the version header from the final summary and label the result as Update or Installation.
2026-07-17 13:43:00 +02:00
niklascfw 7b85d428a8 Add missing pack apps to update-mode deletion lists.
Include Benchmark-Toolbox, Furmark-NX, and other switch apps from the current OmniNX pack so updates replace them cleanly.
2026-07-17 13:42:42 +02:00
niklascfw dce83bc3be Update FatFs NORTC fixed timestamp to 2026-07-17. 2026-07-17 13:42:24 +02:00
niklascfw a26531c702 Add extras post-copy flow for RAM and Hekate-Pro selection.
Build / Build (push) Successful in 14s
Copy sd:/extras from the pack, prompt for RAM and Hekate/AMS choices, persist them in config.ini, apply Pro/8GB payloads, then remove extras/. Use safe config.ini I/O and Joy-Con input clearing between menus.
2026-07-16 10:01:42 +02:00
niklascfw c0741beda3 Preserve CyberFoil across clean install backup/restore.
Build / Build (push) Successful in 13s
Back up cyberfoil.nro, config.json, and remotes/ when sd:/switch/CyberFoil exists, then restore them after the wipe.
2026-06-14 17:31:46 +02:00
niklascfw d6ec345cce only check UHS on clean install
Build / Build (push) Successful in 12s
2026-06-07 23:16:04 +02:00
niklascfw 9990dfb2ba install: shortend finish delay to 1,5 seconds
Build / Build (push) Successful in 13s
2026-06-04 22:02:22 +02:00
niklascfw c7ad761592 Added NiklasCFW Pack 1.6.2 Marker 2026-06-04 21:52:52 +02:00
niklascfw 59f103874c Preserve.ini for update cleanup, Hekate UI strings, docs
Build / Build (push) Successful in 17s
- Load sd:/config/omninx/preserve.ini during update mode cleanup to skip
  delete paths that overlap user-preserved prefixes (heap-backed path list).
- Document preserve.ini in PRESERVE_INI.md, README, and INSTALLATION_PROCESS.
- Prefer Hekate wording in main.c user messages for bootloader launch/errors.
- Extend update deletion list for exefs_patches (audio_mastervolume, SaltyNX_Fixes, logo).
2026-05-17 11:21:14 +02:00
niklascfw dce11538d1 docs: README and install process for debug.ini and backup
Build / Build (push) Successful in 17s
- Link DEBUG_INI.md; describe clean install backup (dbi.config, prod.keys)
- Update INSTALLATION_PROCESS backup/restore steps

Made-with: Cursor
2026-04-12 16:22:45 +02:00
niklascfw 21f8e0e38a refactor: clean install backup — dbi.config and prod.keys only
- Backup/restore DBI settings file instead of whole DBI folder
- Remove Tinfoil folder backup/restore
- Adjust deletion_lists_clean.h comments; UI strings in install_clean.c

Made-with: Cursor
2026-04-12 16:22:43 +02:00
niklascfw 0ad2c63123 feat: optional debug.ini to skip install steps
Read sd:/config/omninx/debug.ini [Debug] with skip_* flags for clean/update
modes and hekate post-copy. Documented in DEBUG_INI.md.

Made-with: Cursor
2026-04-12 16:22:41 +02:00
niklascfw 6780936d2d chore: normalize line endings in fs.c and fs.h
Made-with: Cursor
2026-04-12 16:22:39 +02:00
niklascfw 82639a7a86 Clean install: align deletions with NiklasCFW pack script
Build / Build (push) Successful in 17s
- Remove entire sd:/atmosphere and sd:/switch (lists simplified)
- Add sd:/SaltySD/patches; drop nyx.ini and config/quickntp to match script
- Delete sd:/switch/tinfoil/db after backup restore via clean_post_restore_dirs_to_delete

Made-with: Cursor
2026-04-12 00:34:14 +02:00
niklascfw bff8bc1910 Clean install: remove sd:/TegraExplorer entirely
Build / Build (push) Failing after 14s
Delete the TegraExplorer directory in misc dirs instead of individual
script files under scripts/.

Made-with: Cursor
2026-04-11 19:44:47 +02:00
niklascfw c9bcb29f50 Exclude source/TegraExplorer from Makefile object list
The vendored tree under source/TegraExplorer is optional reference-only;
building it fails without extra usb/ headers. The payload uses bdk/ at repo
root instead.

Made-with: Cursor
2026-04-11 00:14:41 +02:00
niklascfw dc97677e72 Delete sd:/themes/systemData on update and clean installs
Removes cached qlaunch/FW theme extracts so theme-patches apply after
firmware updates; clearing atmosphere/contents alone is insufficient.

Made-with: Cursor
2026-04-11 00:12:45 +02:00
niklascfw 4535c4508f Added TegraExplorer 2026-04-10 22:32:01 +02:00
niklascfw 726ef77194 update: delete atmosphere kips loader.kip and hoc.kip on OmniNX update
Build / Build (push) Successful in 15s
Made-with: Cursor
2026-04-09 22:11:31 +02:00
niklascfw 13c35400a1 install: backup and restore HorizonOC config.ini on OC pack update
- Copy sd:/config/horizon-oc/config.ini to sd:/.omninx_oc_update before
  update cleanup/copy; restore after Hekate step, then remove the scratch dir.
- Only when INSTALL_MODE_UPDATE and VARIANT_OC.

Made-with: Cursor
2026-04-09 22:11:30 +02:00
niklascfw f71b6ab629 install: clarify Hekate RAM menu copy (4GB/8GB labels and warnings)
Build / Build (push) Successful in 16s
Made-with: Cursor
2026-04-09 20:48:50 +02:00
niklascfw 9b559eb7e6 clean: delete TegraExplorer repair .te scripts on clean install
Build / Build (push) Successful in 16s
Made-with: Cursor
2026-04-07 23:54:54 +02:00
niklascfw d3735d17ee install: RAM config ini, selection menu, and silent Hekate 8GB step
- Read sd:/config/omninx/ram_config.ini [Ram] 8gb=0|1 when present;
  otherwise show 4 GB / 8 GB menu, write ini, same input as variant menu.
- Replace fuse-based DRAM autodetect for hekate_8gb.bin handling.
- Reprint installer top bar after RAM menu confirm.

Made-with: Cursor
2026-04-07 23:54:53 +02:00
niklascfw db7d83304f main: add installer_launch_hekate_payload (update.bin or reboot)
Made-with: Cursor
2026-04-07 23:54:51 +02:00
niklascfw 113542f7fb install: declare installer_launch_hekate_payload for sub-menu exit
Made-with: Cursor
2026-04-07 23:54:50 +02:00
niklascfw 72a2083d76 Updated Deletion list
Build / Build (push) Successful in 16s
2026-04-05 15:38:32 +02:00
niklascfw 3f6bbc0752 Input: console Vol+/- combo only before install; Joy-Con +/- exit removed
Build / Build (push) Successful in 16s
- cancel_combo_pressed uses only hardware volume buttons
- After installation summary and payload-missing screen: A or Power only

Made-with: Cursor
2026-03-31 13:18:06 +02:00
niklascfw d1fc24dae8 Install: Hekate 8GB after copy (fuse >4GiB), dram_fuse helper, RAM test target
Build / Build (push) Successful in 14s
- Add dram_fuse.c/h for shared fuse-to-MiB mapping
- After successful update/clean install copy: if hekate_8gb.bin exists and
  fuse reports more than 4 GiB, copy to sd:/payload.bin and
  sd:/bootloader/update.bin, then remove source; otherwise remove helper only
- Makefile: make ram-test builds output/RAM-Test.bin (tools/ram_test_main.c)

Made-with: Cursor
2026-03-31 00:05:30 +02:00
niklascfw ef104bce41 Update deletions: keep Switchfin, remove swr-ini-tool nro
Made-with: Cursor
2026-03-30 20:27:39 +02:00
niklascfw 0015c7e8ac Install: copy all staging root files via directory iteration
Build / Build (push) Successful in 18s
Made-with: Cursor
2026-03-30 17:49:52 +02:00
niklascfw 6dde73f39e Install: copy themes/ in stage 2 (update and clean)
Build / Build (push) Successful in 11s
Made-with: Cursor
2026-03-06 23:46:40 +01:00
niklascfw 97e8c3d965 Remove outdated UltraHand comment from deletion_lists_update.h
Build / Build (push) Successful in 13s
Made-with: Cursor
2026-03-06 22:10:32 +01:00
niklascfw 586afc2f56 Revert "Update: preserve UltraHand .offload state (overlays/packages)"
This reverts commit e71fb13bfc.
2026-03-05 17:24:37 +01:00
niklascfw f943887b39 Revert "Clean install: use normal switch copy, not .offload-aware"
This reverts commit 335ea03b05.
2026-03-05 17:24:26 +01:00
niklascfw 335ea03b05 Clean install: use normal switch copy, not .offload-aware
Build / Build (push) Successful in 14s
- update_mode_install(variant, offload_aware_switch): true = update (preserve .offload), false = clean
- Update path calls with true; clean_mode_install calls with false so switch/ is copied normally on fresh install

Made-with: Cursor
2026-03-01 15:15:45 +01:00
niklascfw b0523ada6c nx_sd: add sd_get_card_mounted() for screenshot and other callers
Made-with: Cursor
2026-03-01 14:44:37 +01:00
niklascfw 9a2307a8ee Add screenshot capture via Joy-Con Capture button
- source/screenshot.c, screenshot.h: take_screenshot() saves framebuffer as BMP
- Path: sd:/switch/screenshot/omninx_installer_YYYYMMDD_HHMMSS.bmp (RTC timestamp)
- 3s cooldown, backlight flash on capture; TegraExplorer-style BGR flip
- main.c: call take_screenshot() when jc->cap in all joycon poll loops

Made-with: Cursor
2026-03-01 14:43:58 +01:00
niklascfwandCursor e71fb13bfc Update: preserve UltraHand .offload state (overlays/packages)
Build / Build (push) Successful in 10s
- Add folder_copy_switch_update_offload_aware() for update-only switch copy
- If item exists in .overlays/.offload or .packages/.offload, update in place; else copy to main
- Remove sd:/switch/.overlays from update delete list so .offload survives
- Fresh install unchanged (no offload logic)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 21:01:36 +01:00
niklascfwandCursor 5bcd3987a2 UHS warning: recommend Samsung EVO Plus, EVO Select, PRO Plus
Build / Build (push) Successful in 11s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 20:28:15 +01:00
47 changed files with 4186 additions and 374 deletions

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
name: Build Desktop Installer
on:
push:
paths:
- 'desktop/**'
- 'source/deletion_lists_*.h'
- 'tools/generate_deletion_lists.py'
- '.github/workflows/build-desktop.yml'
workflow_dispatch:
jobs:
build:
name: Build (${{ matrix.rid }})
runs-on: windows-latest
strategy:
matrix:
rid: [win-x64, win-arm64]
steps:
- uses: actions/checkout@v5
- name: Generate deletion lists
run: python tools/generate_deletion_lists.py
- name: Publish
run: >
dotnet publish desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj
-c Release
-r ${{ matrix.rid }}
-p:SelfContained=false
-p:PublishSingleFile=true
-o desktop/out/${{ matrix.rid }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: OmniNX-Desktop-Installer-${{ matrix.rid }}
path: desktop/out/${{ matrix.rid }}/OmniNX-Desktop-Installer.exe
+8
View File
@@ -20,3 +20,11 @@ Thumbs.db
# Version file is tracked, ignore local edits # Version file is tracked, ignore local edits
# VERSION # VERSION
# Vendored TegraExplorer tree
source/TegraExplorer/
# Desktop installer (.NET)
desktop/**/bin/
desktop/**/obj/
desktop/out/
+90
View File
@@ -0,0 +1,90 @@
# Debug configuration (`debug.ini`)
Optional file to **skip parts of the install** for testing (e.g. run only deletion and inspect the SD card). If the file is **missing**, the installer ignores debug entirely and runs the full flow.
## Location
```
sd:/config/omninx/debug.ini
```
Create the folder `sd:/config/omninx/` on the SD card if it does not exist. Same parent directory as `manifest.ini` and `config.ini`.
## Format
- Section name must be exactly **`[Debug]`** (case-sensitive in the parser).
- One key per line: `name=value`.
- **Enabling a skip:** set the value so it **starts** with one of: `1`, `t`, `T`, `y`, `Y`
Examples: `1`, `true`, `yes`, `True` — all enable that skip.
- **Disabling:** `0`, `false`, `no`, or omit the key.
## Parameters
### Clean install mode (`INSTALL_MODE_CLEAN`)
| Key | What is skipped |
|-----|-----------------|
| `skip_clean_backup` | Step 1 — backup of `sd:/switch/DBI/dbi.config`, `sd:/switch/prod.keys` to `sd:/temp_backup` |
| `skip_clean_wipe` | Step 2 — all cleanup from `deletion_lists_clean.h` (deletion / wipe) |
| `skip_clean_restore` | Step 3 — restore from `sd:/temp_backup` and post-restore paths (e.g. delete `sd:/switch/tinfoil/db`) |
| `skip_clean_install` | Step 4 — copy files from the OmniNX staging folder to the SD root |
| `skip_clean_staging_cleanup` | After install: do not remove the staging directory or other variant staging folders |
### Update mode (`INSTALL_MODE_UPDATE`)
| Key | What is skipped |
|-----|-----------------|
| `skip_update_cleanup` | Step 1 — selective deletion from `deletion_lists_update.h` |
| `skip_update_install` | Step 2 — copy from staging to SD |
| `skip_update_staging_cleanup` | End: do not remove staging / other variant folders |
| `skip_update_horizon_oc` | **OC variant only:** HorizonOC config backup before update, restore and cleanup after (entire HorizonOC update side path) |
### Both modes
| Key | What is skipped |
|-----|-----------------|
| `skip_hekate_8gb_post_copy` | After a successful copy: the optional `hekate_8GB` / RAM menu and related file moves (see `install_hekate_8gb_post_copy` in `install.c`) |
## Behaviour
- On startup, if `debug.ini` exists and parses successfully, the payload prints a **DEBUG-MODUS** banner listing which skips are active.
- If the file is absent, unreadable, or has no `[Debug]` section, **no** skips apply — behaviour matches a release install.
- Skipping steps can leave the SD in an inconsistent state; this is **for developers/testers only**.
## Examples
### Wipe only (test deletion, no backup/restore/copy)
Inspect leftovers after the clean-install wipe, without backing up, restoring, installing, or cleaning staging:
```ini
[Debug]
skip_clean_backup=1
skip_clean_restore=1
skip_clean_install=1
skip_clean_staging_cleanup=1
skip_hekate_8gb_post_copy=1
```
Do **not** set `skip_clean_wipe` (leave unset or `0`).
### Update: deletion only
```ini
[Debug]
skip_update_install=1
skip_update_staging_cleanup=1
skip_hekate_8gb_post_copy=1
```
### OC update without touching HorizonOC backup/restore
```ini
[Debug]
skip_update_horizon_oc=1
```
## Implementation
- Parsed in `install.c`: `install_debug_load()`, `perform_installation()`.
- Uses the same INI parser as `config.ini` (`ini_parse` from BDK).
+21 -32
View File
@@ -2,6 +2,8 @@
This document provides a detailed, step-by-step breakdown of everything that is checked and done during the OmniNX installation/update process. This document provides a detailed, step-by-step breakdown of everything that is checked and done during the OmniNX installation/update process.
For **optional debug step skips** (`sd:/config/omninx/debug.ini`), see **[DEBUG_INI.md](DEBUG_INI.md)**.
## Overview ## Overview
The OmniNX Installer Payload operates in two modes: The OmniNX Installer Payload operates in two modes:
@@ -88,7 +90,9 @@ The OmniNX Installer Payload operates in two modes:
**Trigger**: `INSTALL_MODE_UPDATE` (OmniNX already installed) **Trigger**: `INSTALL_MODE_UPDATE` (OmniNX already installed)
### Step 1: Cleanup (Selective Deletion) ### Step 1: Cleanup (Selective Deletion)
**Location**: `install.c:321-380` **Location**: `install_update.c` (`update_mode_cleanup`), `install.c` (`delete_path_lists_grouped`)
Before deletion, the payload reads optional **`sd:/config/omninx/preserve.ini`** (section `[Preserve]`). Paths enabled there are skipped, as are delete targets that would remove them (parent/child overlap). See **[PRESERVE_INI.md](PRESERVE_INI.md)**. Clean install does not use this file.
#### 10. Clean Atmosphere Subdirectories #### 10. Clean Atmosphere Subdirectories
**Location**: `install.c:325-327`, `deletion_lists.h:9-34` **Location**: `install.c:325-327`, `deletion_lists.h:9-34`
@@ -305,17 +309,12 @@ channel_pack={variant} # Same as current_pack
#### 32. Create Backup Directory #### 32. Create Backup Directory
- Creates `sd:/temp_backup/` directory - Creates `sd:/temp_backup/` directory
#### 33. Backup DBI #### 33. Backup DBI settings
- **Source**: `sd:/switch/DBI/` - **Source**: `sd:/switch/DBI/dbi.config` (file only, not the whole `DBI` tree)
- **Destination**: `sd:/temp_backup/DBI/` - **Destination**: `sd:/temp_backup/dbi.config`
- Only if source exists - Only if the source file exists
#### 34. Backup Tinfoil #### 34. Backup prod.keys
- **Source**: `sd:/switch/tinfoil/`
- **Destination**: `sd:/temp_backup/tinfoil/`
- Only if source exists
#### 35. Backup prod.keys
- **Source**: `sd:/switch/prod.keys` - **Source**: `sd:/switch/prod.keys`
- **Destination**: `sd:/temp_backup/prod.keys` - **Destination**: `sd:/temp_backup/prod.keys`
- Only if source exists - Only if source exists
@@ -325,22 +324,22 @@ channel_pack={variant} # Same as current_pack
### Step 2: Wipe Directories ### Step 2: Wipe Directories
**Location**: `install.c:543-602` **Location**: `install.c:543-602`
#### 36. Delete Entire Directories #### 35. Delete Entire Directories
Recursively deletes (if they exist): Recursively deletes (if they exist):
- `sd:/atmosphere/` (entire directory tree) - `sd:/atmosphere/` (entire directory tree)
- `sd:/bootloader/` (entire directory tree) - `sd:/bootloader/` (entire directory tree)
- `sd:/config/` (entire directory tree) - `sd:/config/` (entire directory tree)
- `sd:/switch/` (entire directory tree) - `sd:/switch/` (entire directory tree)
#### 37. Delete Root Files #### 36. Delete Root Files
- Uses same deletion list as update mode (Step 19) - Uses same deletion list as update mode (Step 19)
- Deletes `boot.dat`, `boot.ini`, `exosphere.ini`, etc. - Deletes `boot.dat`, `boot.ini`, `exosphere.ini`, etc.
#### 38. Delete Miscellaneous Items #### 37. Delete Miscellaneous Items
- Uses same deletion lists as update mode (Steps 20-21) - Uses same deletion lists as update mode (Steps 20-21)
- Deletes `argon/`, `games/`, `SaltySD/`, etc. - Deletes `argon/`, `games/`, `SaltySD/`, etc.
#### 39. Recreate Switch Directory #### 38. Recreate Switch Directory
- Creates empty `sd:/switch/` directory for restoration - Creates empty `sd:/switch/` directory for restoration
--- ---
@@ -348,27 +347,17 @@ Recursively deletes (if they exist):
### Step 3: Restore User Data ### Step 3: Restore User Data
**Location**: `backup.c:55-95` **Location**: `backup.c:55-95`
#### 40. Restore DBI #### 39. Restore DBI settings
- **Source**: `sd:/temp_backup/DBI/` - **Source**: `sd:/temp_backup/dbi.config`
- **Destination**: `sd:/switch/DBI/` - **Destination**: `sd:/switch/DBI/dbi.config` (creates `sd:/switch/DBI/` if needed)
- **Cleanup**: After restoration, deletes old DBI NRO files: - No longer copies the full `DBI` folder; old `.nro` cleanup is unnecessary
- `sd:/switch/DBI/DBI_810_EN.nro`
- `sd:/switch/DBI/DBI_810_DE.nro`
- `sd:/switch/DBI/DBI_845_EN.nro`
- `sd:/switch/DBI/DBI_845_DE.nro`
- `sd:/switch/DBI/DBI.nro`
#### 41. Restore Tinfoil #### 40. Restore prod.keys
- **Source**: `sd:/temp_backup/tinfoil/`
- **Destination**: `sd:/switch/tinfoil/`
- **Cleanup**: After restoration, deletes `sd:/switch/tinfoil/tinfoil.nro`
#### 42. Restore prod.keys
- **Source**: `sd:/temp_backup/prod.keys` - **Source**: `sd:/temp_backup/prod.keys`
- **Destination**: `sd:/switch/prod.keys` - **Destination**: `sd:/switch/prod.keys`
#### 43. Cleanup Backup Directory #### 41. Cleanup Backup Directory
**Location**: `backup.c:98-103` **Location**: `backup.c` (`cleanup_backup`)
- Deletes `sd:/temp_backup/` directory after restoration - Deletes `sd:/temp_backup/` directory after restoration
--- ---
+23 -1
View File
@@ -31,6 +31,9 @@ OBJS += $(patsubst $(BDKDIR)/%.S, $(BUILDDIR)/$(TARGET)/%.o, \
$(patsubst $(BDKDIR)/%.c, $(BUILDDIR)/$(TARGET)/%.o, \ $(patsubst $(BDKDIR)/%.c, $(BUILDDIR)/$(TARGET)/%.o, \
$(call rwildcard, $(BDKDIR), *.S *.c))) $(call rwildcard, $(BDKDIR), *.S *.c)))
# Optional vendored tree under source/TegraExplorer/ is not part of this payload build.
OBJS := $(filter-out $(BUILDDIR)/$(TARGET)/TegraExplorer/%,$(OBJS))
GFX_INC := '"../$(SOURCEDIR)/gfx.h"' GFX_INC := '"../$(SOURCEDIR)/gfx.h"'
FFCFG_INC := '"../$(SOURCEDIR)/libs/fatfs/ffconf.h"' FFCFG_INC := '"../$(SOURCEDIR)/libs/fatfs/ffconf.h"'
@@ -46,7 +49,11 @@ LDFLAGS = $(ARCH) -nostartfiles -lgcc -Wl,--nmagic,--gc-sections -Xlinker --defs
################################################################################ ################################################################################
.PHONY: all clean release RAMTEST_BIN := $(OUTPUTDIR)/RAM-Test.bin
OBJS_RAMTEST := $(filter-out $(BUILDDIR)/$(TARGET)/main.o,$(OBJS)) \
$(BUILDDIR)/$(TARGET)/ram_test_main.o
.PHONY: all clean release ram-test
all: $(OUTPUTDIR)/$(OUTPUT_NAME) all: $(OUTPUTDIR)/$(OUTPUT_NAME)
$(eval BIN_SIZE = $(shell wc -c < $(OUTPUTDIR)/$(OUTPUT_NAME))) $(eval BIN_SIZE = $(shell wc -c < $(OUTPUTDIR)/$(OUTPUT_NAME)))
@@ -56,6 +63,7 @@ all: $(OUTPUTDIR)/$(OUTPUT_NAME)
clean: clean:
@rm -rf $(BUILDDIR) @rm -rf $(BUILDDIR)
@rm -rf $(OUTPUTDIR) @rm -rf $(OUTPUTDIR)
@rm -f $(RAMTEST_BIN)
@rm -rf release @rm -rf release
@rm -f $(TARGET)-*.zip @rm -f $(TARGET)-*.zip
@@ -67,6 +75,20 @@ $(OUTPUTDIR)/$(OUTPUT_NAME): $(BUILDDIR)/$(TARGET)/$(TARGET).elf
$(BUILDDIR)/$(TARGET)/$(TARGET).elf: $(OBJS) $(BUILDDIR)/$(TARGET)/$(TARGET).elf: $(OBJS)
$(CC) $(LDFLAGS) -T $(SOURCEDIR)/link.ld $^ -o $@ $(CC) $(LDFLAGS) -T $(SOURCEDIR)/link.ld $^ -o $@
$(BUILDDIR)/$(TARGET)/ram_test_main.o: tools/ram_test_main.c
@mkdir -p "$(@D)"
$(CC) $(CFLAGS) $(BDKINC) -I$(SOURCEDIR) -c $< -o $@
$(BUILDDIR)/$(TARGET)/ram_test.elf: $(OBJS_RAMTEST)
$(CC) $(LDFLAGS) -T $(SOURCEDIR)/link.ld $^ -o $@
$(RAMTEST_BIN): $(BUILDDIR)/$(TARGET)/ram_test.elf
@mkdir -p "$(@D)"
$(OBJCOPY) -S -O binary $< $@
@echo "RAM-Test payload: $(RAMTEST_BIN) ($$(wc -c < $@) bytes)"
ram-test: $(RAMTEST_BIN)
$(BUILDDIR)/$(TARGET)/%.o: $(SOURCEDIR)/%.c $(BUILDDIR)/$(TARGET)/%.o: $(SOURCEDIR)/%.c
@mkdir -p "$(@D)" @mkdir -p "$(@D)"
$(CC) $(CFLAGS) $(BDKINC) -I$(SOURCEDIR) -c $< -o $@ $(CC) $(CFLAGS) $(BDKINC) -I$(SOURCEDIR) -c $< -o $@
+59
View File
@@ -0,0 +1,59 @@
# User preserve list (`preserve.ini`)
**Update mode only.** During OmniNX update cleanup (selective deletion), any path listed here with a truthy value is **not** deleted. Paths that would remove a preserved path (parent directory) are skipped as well.
- **File:** `sd:/config/omninx/preserve.ini`
- If missing or invalid, behavior is unchanged (full update deletion list applies).
## Format
Section **`[Preserve]`** (case-sensitive). Each line is **path as key**, **enable flag as value**:
```ini
[Preserve]
'sd:/ROMs'=1
sd:/switch/MyHomebrewFolder=0
sd:/backup/tools=1
```
### Path (key)
- Must start with **`sd:/`** (after optional quoting).
- **Optional** matching **single** or **double** quotes around the key are stripped (helps Ultrahand `set-ini-val` and readability).
- Trailing slashes are normalized away (except the volume root).
- **`sd:/` alone** is rejected (too broad).
- Minimum useful length after `sd:/` is enforced (at least one more character).
### Value (enable)
**On** if the value starts with `1`, `t`, `T`, `y`, or `Y` (e.g. `1`, `true`, `yes`). Anything else is **off** (path ignored).
### Limits
- Up to **96** preserved paths; longer keys truncated internally (~224 bytes per path).
- Duplicate paths after normalization are skipped.
- The list is allocated on the **heap** only while update cleanup runs (not a large static buffer in IPL IRAM), to avoid crowding hardware-adjacent memory used by Joy-Con UART and similar peripherals.
## Overlap rules
For each delete candidate from `deletion_lists_update.h`, deletion is skipped if it **overlaps** any enabled preserve path:
- Same path, or
- Delete target is **inside** a preserved tree (preserved path is a prefix), or
- Delete target is an **ancestor** of a preserved path (deleting it would remove the preserved data).
So preserving `sd:/ROMs` also blocks deleting `sd:/ROMs/title` if that were on the delete list, and blocks deleting `sd:/` if `sd:/ROMs` is preserved.
## Ultrahand
You can toggle entries with:
```text
set-ini-val sd:/config/omninx/preserve.ini Preserve 'sd:/ROMs' 1
```
and `0` to disable. Use the same quoted path in the key argument as in your INI.
## Clean install
**Not used** in clean install mode. `preserve.ini` is only read for **update** cleanup (`update_mode_cleanup`).
+5 -3
View File
@@ -12,7 +12,7 @@ Based on [HATS-Installer-Payload](https://github.com/sthetix/HATS-Installer-Payl
- **Automatic Variant Detection**: Detects which OmniNX pack variant is present (Standard/Light/OC) - **Automatic Variant Detection**: Detects which OmniNX pack variant is present (Standard/Light/OC)
- **Smart Installation Modes**: - **Smart Installation Modes**:
- **Update Mode**: Selective deletion when OmniNX is already installed - **Update Mode**: Selective deletion when OmniNX is already installed
- **Clean Install**: Full wipe with backup/restore of user data (DBI, Tinfoil, prod.keys) - **Clean Install**: Full wipe with backup/restore of user data (DBI `dbi.config`, prod.keys)
- **Version Detection**: Detects installed OmniNX version via marker files (`1.0.0s`, `1.0.0l`, `1.0.0oc`) - **Version Detection**: Detects installed OmniNX version via marker files (`1.0.0s`, `1.0.0l`, `1.0.0oc`)
- **Progress Display**: Visual status messages during installation - **Progress Display**: Visual status messages during installation
- **Error Handling**: Detailed error reporting on screen - **Error Handling**: Detailed error reporting on screen
@@ -22,12 +22,15 @@ Based on [HATS-Installer-Payload](https://github.com/sthetix/HATS-Installer-Payl
For detailed information about the installation process, see: For detailed information about the installation process, see:
- **[INSTALLATION_PROCESS.md](INSTALLATION_PROCESS.md)** - Complete step-by-step breakdown of everything checked and done during installation/update - **[INSTALLATION_PROCESS.md](INSTALLATION_PROCESS.md)** - Complete step-by-step breakdown of everything checked and done during installation/update
- **[DEBUG_INI.md](DEBUG_INI.md)** - Optional `sd:/config/omninx/debug.ini` parameters to skip install steps (for testing)
- **[PRESERVE_INI.md](PRESERVE_INI.md)** - Optional `sd:/config/omninx/preserve.ini` to skip deleting chosen paths in **update** mode
## Installation Modes ## Installation Modes
### Update Mode (OmniNX Detected) ### Update Mode (OmniNX Detected)
- Detected when version marker files (`1.0.0s`, `1.0.0l`, or `1.0.0oc`) are found - Detected when version marker files (`1.0.0s`, `1.0.0l`, or `1.0.0oc`) are found
- Performs selective deletion of specific directories/files - Performs selective deletion of specific directories/files
- Optional **`sd:/config/omninx/preserve.ini`**: user-listed `sd:/…` paths (with `=1`) are excluded from that cleanup (see [PRESERVE_INI.md](PRESERVE_INI.md))
- Preserves user data, savegames, and installed games - Preserves user data, savegames, and installed games
- Updates only necessary CFW components - Updates only necessary CFW components
@@ -35,8 +38,7 @@ For detailed information about the installation process, see:
- Detected when no version marker files are found - Detected when no version marker files are found
- Performs full wipe of `/atmosphere`, `/bootloader`, `/config`, and `/switch` - Performs full wipe of `/atmosphere`, `/bootloader`, `/config`, and `/switch`
- **Backs up and restores**: - **Backs up and restores**:
- `sd:/switch/DBI` → preserved - `sd:/switch/DBI/dbi.config` → preserved (not the whole `DBI` folder)
- `sd:/switch/tinfoil` → preserved
- `sd:/switch/prod.keys` → preserved - `sd:/switch/prod.keys` → preserved
- Fresh installation of all CFW components - Fresh installation of all CFW components
+1 -1
View File
@@ -1 +1 @@
1.0.0 1.0.10
+6
View File
@@ -721,6 +721,12 @@ void jc_deinit()
clock_disable_uart(UART_C); clock_disable_uart(UART_C);
} }
/* Drop current button state without power-cycling the Joy-Cons. */
void jc_clear_input(void)
{
jc_gamepad.buttons = 0;
}
static void jc_init_conn(joycon_ctxt_t *jc) static void jc_init_conn(joycon_ctxt_t *jc)
{ {
if (((u32)get_tmr_ms() - jc->last_received_time) > 1000) if (((u32)get_tmr_ms() - jc->last_received_time) > 1000)
+1
View File
@@ -92,6 +92,7 @@ typedef struct _jc_gamepad_rpt_t
void jc_power_supply(u8 uart, bool enable); void jc_power_supply(u8 uart, bool enable);
void jc_init_hw(); void jc_init_hw();
void jc_deinit(); void jc_deinit();
void jc_clear_input(void);
jc_gamepad_rpt_t *joycon_poll(); jc_gamepad_rpt_t *joycon_poll();
jc_gamepad_rpt_t *jc_get_bt_pairing_info(bool *is_l_hos, bool *is_r_hos); jc_gamepad_rpt_t *jc_get_bt_pairing_info(bool *is_l_hos, bool *is_r_hos);
+18
View File
@@ -0,0 +1,18 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OmniNX.DesktopInstaller", "OmniNX.DesktopInstaller\OmniNX.DesktopInstaller.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,410 @@
// <auto-generated />
// Generated by tools/generate_deletion_lists.py — do not edit manually.
namespace OmniNX.DesktopInstaller.Data;
public static partial class DeletionLists
{
public static class Clean
{
public static readonly string[] clean_atmosphere_dirs_to_delete =
{
@"atmosphere",
};
public static readonly string[] clean_atmosphere_contents_dirs_to_delete =
{
};
public static readonly string[] clean_atmosphere_files_to_delete =
{
};
public static readonly string[] clean_bootloader_dirs_to_delete =
{
@"bootloader\boot",
@"bootloader\bootlogo",
@"bootloader\ini2",
@"bootloader\payloads",
@"bootloader\reboot",
@"bootloader\res",
@"bootloader\sys",
};
public static readonly string[] clean_bootloader_files_to_delete =
{
@"bootloader\ArgonNX.bin",
@"bootloader\bootlogo.bmp",
@"bootloader\hekate_ipl.ini",
@"bootloader\patches.ini",
@"bootloader\update.bin",
@"bootloader\ini\EmuMMC ohne Mods.ini",
};
public static readonly string[] clean_config_dirs_to_delete =
{
@"config\aio-switch-updater",
@"config\blue_pack_updater",
@"config\kefir-updater",
@"config\nx-hbmenu",
@"config\sys-con",
@"config\sys-patch",
@"config\uberhand",
@"config\ultrahand",
};
public static readonly string[] clean_switch_dirs_to_delete =
{
@"switch",
};
public static readonly string[] clean_switch_files_to_delete =
{
};
public static readonly string[] clean_root_files_to_delete =
{
@"boot.dat",
@"boot.ini",
@"exosphere.bin",
@"exosphere.ini",
@"hbmenu.nro",
@"install.bat",
@"license",
@"loader.bin",
@"mc-mitm.log",
@"payload.bin",
@"update.bin",
@"version",
};
public static readonly string[] clean_misc_dirs_to_delete =
{
@"argon",
@"games",
@"NSPs (Tools)",
@"Patched Apps",
@"SaltySD\flags",
@"SaltySD\patches",
@"scripts",
@"TegraExplorer",
@"themes\systemData",
@"tools",
@"warmboot_mariko",
};
public static readonly string[] clean_misc_files_to_delete =
{
@"fusee-primary.bin",
@"fusee.bin",
@"SaltySD\exceptions.txt",
@"SaltySD\saltysd_bootstrap.elf",
@"SaltySD\saltysd_bootstrap32_3k.elf",
@"SaltySD\saltysd_bootstrap32_5k.elf",
@"SaltySD\saltysd_core.elf",
@"SaltySD\saltysd_core32.elf",
};
public static readonly string[] clean_post_restore_dirs_to_delete =
{
@"switch\tinfoil\db",
};
public static readonly string[] old_version_files_to_delete =
{
@"1.4.0-pre",
@"1.4.0-pre-c",
@"1.4.0-pre-d",
@"1.4.1",
@"1.5.0",
@"1.6.0",
@"1.6.1",
@"1.6.2",
};
}
public static class Update
{
public static readonly string[] atmosphere_dirs_to_delete =
{
@"atmosphere\config",
@"atmosphere\crash_reports",
@"atmosphere\erpt_reports",
@"atmosphere\exefs_patches\audio_mastervolume",
@"atmosphere\exefs_patches\CrunchPatch",
@"atmosphere\exefs_patches\SaltyNX_Fixes",
@"atmosphere\exefs_patches\Crunchyroll Patch 1.10.0",
@"atmosphere\exefs_patches\bluetooth_patches",
@"atmosphere\exefs_patches\bootlogo",
@"atmosphere\exefs_patches\btm_patches",
@"atmosphere\exefs_patches\es_patches",
@"atmosphere\exefs_patches\hid_patches",
@"atmosphere\exefs_patches\logo",
@"atmosphere\exefs_patches\nfim_ctest",
@"atmosphere\exefs_patches\nim_ctest",
@"atmosphere\exefs_patches\nvnflinger_cmu",
@"atmosphere\extrazz",
@"atmosphere\fatal_errors",
@"atmosphere\fatal_reports",
@"atmosphere\flags",
@"atmosphere\hbl_html",
@"atmosphere\hosts",
@"atmosphere\kips",
@"atmosphere\kip1",
@"atmosphere\kip_patches",
@"atmosphere\logs",
};
public static readonly string[] atmosphere_contents_dirs_to_delete =
{
@"atmosphere\contents\0000000000534C56",
@"atmosphere\contents\00FF0000B378D640",
@"atmosphere\contents\00FF0000636C6BFF",
@"atmosphere\contents\00FF0000A53BB665",
@"atmosphere\contents\0100000000000008",
@"atmosphere\contents\010000000000000D",
@"atmosphere\contents\010000000000002B",
@"atmosphere\contents\0100000000000032",
@"atmosphere\contents\0100000000000034",
@"atmosphere\contents\0100000000000036",
@"atmosphere\contents\0100000000000037",
@"atmosphere\contents\010000000000003C",
@"atmosphere\contents\0100000000000042",
@"atmosphere\contents\0100000000000895",
@"atmosphere\contents\0100000000000F12",
@"atmosphere\contents\0100000000001000",
@"atmosphere\contents\0100000000001007",
@"atmosphere\contents\0100000000001013",
@"atmosphere\contents\010000000000DA7A",
@"atmosphere\contents\010000000000bd00",
@"atmosphere\contents\01006a800016e000",
@"atmosphere\contents\01009D901BC56000",
@"atmosphere\contents\0100A3900C3E2000",
@"atmosphere\contents\0100F43008C44000",
@"atmosphere\contents\050000BADDAD0000",
@"atmosphere\contents\4200000000000000",
@"atmosphere\contents\420000000000000B",
@"atmosphere\contents\420000000000000E",
@"atmosphere\contents\4200000000000010",
@"atmosphere\contents\4200000000000FFF",
@"atmosphere\contents\420000000007E51A",
@"atmosphere\contents\420000000007E51B",
@"atmosphere\contents\690000000000000D",
};
public static readonly string[] atmosphere_files_to_delete =
{
@"atmosphere\config\exosphere.ini",
@"atmosphere\config\stratosphere.ini",
@"atmosphere\hbl.nsp",
@"atmosphere\package3",
@"atmosphere\reboot_payload.bin",
@"atmosphere\stratosphere.romfs",
@"atmosphere\kips\loader.kip",
@"atmosphere\kips\hoc.kip",
};
public static readonly string[] bootloader_dirs_to_delete =
{
@"bootloader\boot",
@"bootloader\bootlogo",
@"bootloader\ini2",
@"bootloader\payloads",
@"bootloader\reboot",
@"bootloader\res",
@"bootloader\sys",
};
public static readonly string[] bootloader_files_to_delete =
{
@"bootloader\ArgonNX.bin",
@"bootloader\bootlogo.bmp",
@"bootloader\hekate_ipl.ini",
@"bootloader\nyx.ini",
@"bootloader\patches.ini",
@"bootloader\update.bin",
@"bootloader\ini\EmuMMC ohne Mods.ini",
};
public static readonly string[] config_dirs_to_delete =
{
@"config\aio-switch-updater",
@"config\blue_pack_updater",
@"config\kefir-updater",
@"config\nx-hbmenu",
@"config\quickntp",
@"config\sys-con",
@"config\sys-patch",
@"config\uberhand",
@"config\ultrahand",
};
public static readonly string[] switch_dirs_to_delete =
{
@"switch\.packages",
@"switch\.overlays",
@"switch\90DNS_tester",
@"switch\aio-switch-updater",
@"switch\amsPLUS-downloader",
@"switch\appstore",
@"switch\AtmoXL-Titel-Installer",
@"switch\Benchmark-Toolbox",
@"switch\breeze",
@"switch\cheats-updater",
@"switch\chiaki",
@"switch\ChoiDujourNX",
@"switch\crash_ams",
@"switch\Daybreak",
@"switch\DNS-Block_Tester",
@"switch\DNS_mitm Tester",
@"switch\EdiZon",
@"switch\Fizeau",
@"switch\FTPD",
@"switch\Furmark-NX",
@"switch\fw-downloader",
@"switch\gamecard_installer",
@"switch\Goldleaf",
@"switch\haze",
@"switch\JKSV",
@"switch\kefir-updater",
@"switch\ldnmitm_config",
@"switch\Linkalho",
@"switch\Moonlight-Switch",
@"switch\Neumann",
@"switch\NX-Activity-Log",
@"switch\NX-Save-Sync",
@"switch\NX-Shell",
@"switch\NX-Update-Checker ",
@"switch\NXGallery",
@"switch\NXRemoteLauncher",
@"switch\NXThemesInstaller",
@"switch\nxdumptool",
@"switch\nxmtp",
@"switch\Payload_launcher",
@"switch\Reboot",
@"switch\reboot_to_argonNX",
@"switch\reboot_to_hekate",
@"switch\Shutdown_System",
@"switch\SimpleModAlchemist",
@"switch\SimpleModDownloader",
@"switch\SimpleModManager",
@"switch\sphaira",
@"switch\studious-pancake",
@"switch\Switch-Time",
@"switch\SwitchIdent",
@"switch\Switch_themes_Installer",
@"switch\swr-ini-tool",
@"switch\Sys-Clk Manager",
@"switch\Sys-Con",
@"switch\sys-clk-manager",
@"switch\themezer-nx",
@"switch\ThemezerNX",
@"switch\themezernx",
@"switch\tinwoo",
@"switch\Ultrahand-Reload",
};
public static readonly string[] switch_files_to_delete =
{
@"switch\90DNS_tester\90DNS_tester.nro",
@"switch\Benchmark-Toolbox\Benchmark-Toolbox.nro",
@"switch\breeze.nro",
@"switch\cheats-updater.nro",
@"switch\chiaki.nro",
@"switch\ChoiDujourNX.nro",
@"switch\daybreak.nro",
@"switch\DBI.nro",
@"switch\DBI\DBI.nro",
@"switch\DBI\DBI_810_DE.nro",
@"switch\DBI\DBI_810_EN.nro",
@"switch\DBI\DBI_845_DE.nro",
@"switch\DBI\DBI_845_EN.nro",
@"switch\DBI\DBI_849_DE.nro",
@"switch\DBI\DBI_849_EN.nro",
@"switch\DBI_810_DE\DBI_810.nro",
@"switch\DBI_810_DE\DBI_810_DE.nro",
@"switch\DBI_810_EN\DBI_810_EN.nro",
@"switch\DBI_RU\DBI_RU.nro",
@"switch\DBI\DBI_EN.nro",
@"switch\DBI_DE\DBI_DE.nro",
@"switch\DNS-Block_Tester\dns_tester.nro",
@"switch\DNS_mitm Tester.nro",
@"switch\EdiZon.nro",
@"switch\Fizeau.nro",
@"switch\Furmark-NX\Furmark-NX.nro",
@"switch\Goldleaf.nro",
@"switch\haze.nro",
@"switch\JKSV.nro",
@"switch\ldnmitm_config.nro",
@"switch\linkalho.nro",
@"switch\Moonlight-Switch.nro",
@"switch\Neumann.nro",
@"switch\NX-Shell.nro",
@"switch\NXGallery.nro",
@"switch\NXThemesInstaller.nro",
@"switch\nxdumptool.nro",
@"switch\nxtc.bin",
@"switch\reboot_to_payload.nro",
@"switch\SimpleModAlchemist\Simple_Mod_Alchemist.nro",
@"switch\SimpleModDownloader.nro",
@"switch\SimpleModManager.nro",
@"switch\sphaira.nro",
@"switch\swr-ini-tool\swr-ini-tool.nro",
@"switch\SwitchIdent.nro",
@"switch\Switch_themes_Installer\NXThemesInstaller.nro",
@"switch\Sys-Clk Manager\sys-clk-manager.nro",
@"switch\Sys-Con.nro",
@"switch\sys-clk-manager.nro",
@"switch\ThemezerNX\themezer-nx.nro",
@"switch\tinfoil.nro",
@"switch\tinfoil\tinfoil.nro",
@"switch\tinwoo.nro",
@"switch\tinwoo\tinwoo.nro",
@"switch\Ultrahand-Reload\Ultrahand-Reload.nro",
};
public static readonly string[] root_files_to_delete =
{
@"boot.dat",
@"boot.ini",
@"exosphere.bin",
@"exosphere.ini",
@"hbmenu.nro",
@"install.bat",
@"license",
@"loader.bin",
@"mc-mitm.log",
@"payload.bin",
@"update.bin",
@"version",
};
public static readonly string[] misc_dirs_to_delete =
{
@"argon",
@"games",
@"NSPs (Tools)",
@"Patched Apps",
@"SaltySD\flags",
@"scripts",
@"switch\tinfoil\db",
@"themes\systemData",
@"tools",
@"warmboot_mariko",
};
public static readonly string[] misc_files_to_delete =
{
@"fusee-primary.bin",
@"fusee.bin",
@"SaltySD\exceptions.txt",
@"SaltySD\saltysd_bootstrap.elf",
@"SaltySD\saltysd_bootstrap32_3k.elf",
@"SaltySD\saltysd_bootstrap32_5k.elf",
@"SaltySD\saltysd_core.elf",
@"SaltySD\saltysd_core32.elf",
};
}
}
+581
View File
@@ -0,0 +1,581 @@
using OmniNX.DesktopInstaller.Models;
using OmniNX.DesktopInstaller.Services;
using System.Runtime.InteropServices;
namespace OmniNX.DesktopInstaller;
public sealed class MainForm : Form
{
private readonly GiteaClient _gitea = new();
private readonly VersionDetectService _versionDetect = new();
private readonly InstallEngine _installEngine = new();
private ComboBox _driveCombo = null!;
private Button _refreshDrivesButton = null!;
private Label _statusLabel = null!;
private Label _releaseLabel = null!;
private Button _checkReleaseButton = null!;
private RadioButton _variantLight = null!;
private RadioButton _variantStandard = null!;
private RadioButton _variantOc = null!;
private RadioButton _ram4Gb = null!;
private RadioButton _ram8Gb = null!;
private RadioButton _hekateStandard = null!;
private RadioButton _hekatePro = null!;
private CheckBox _forceCleanCheck = null!;
private Button _installButton = null!;
private ProgressBar _progressBar = null!;
private TextBox _logBox = null!;
private ReleaseInfo? _latestRelease;
private CancellationTokenSource? _operationCts;
public MainForm()
{
Text = "OmniNX Desktop Installer";
MinimumSize = new Size(820, 640);
Size = new Size(900, 700);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9F);
Icon = LoadFormIcon() ?? Icon.ExtractAssociatedIcon(Application.ExecutablePath);
BuildLayout();
RefreshDrives();
_ = LoadLatestReleaseAsync();
}
private void BuildLayout()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 8,
Padding = new Padding(12),
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.Controls.Add(BuildHeaderPanel(), 0, 0);
root.Controls.Add(BuildDrivePanel(), 0, 1);
root.Controls.Add(BuildVariantPanel(), 0, 2);
root.Controls.Add(BuildRamPanel(), 0, 3);
root.Controls.Add(BuildHekatePanel(), 0, 4);
root.Controls.Add(BuildOptionsPanel(), 0, 5);
root.Controls.Add(BuildActionPanel(), 0, 6);
root.Controls.Add(BuildLogPanel(), 0, 7);
Controls.Add(root);
}
private Control BuildHeaderPanel()
{
var panel = new TableLayoutPanel
{
AutoSize = true,
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2,
};
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var title = new Label
{
Text = "OmniNX Desktop Installer",
Font = new Font(Font.FontFamily, 14F, FontStyle.Bold),
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 16, 4),
};
_releaseLabel = new Label
{
AutoSize = true,
Text = "Release: checking...",
Margin = new Padding(0, 0, 8, 8),
Anchor = AnchorStyles.Left,
};
_checkReleaseButton = new Button { Text = "Refresh release", AutoSize = true, Margin = new Padding(0, 0, 0, 8) };
_checkReleaseButton.Click += async (_, _) => await LoadLatestReleaseAsync();
var releaseRow = new FlowLayoutPanel
{
AutoSize = true,
WrapContents = false,
Dock = DockStyle.Fill,
Margin = new Padding(0),
};
releaseRow.Controls.Add(_releaseLabel);
releaseRow.Controls.Add(_checkReleaseButton);
panel.Controls.Add(title, 0, 0);
panel.Controls.Add(releaseRow, 0, 1);
return panel;
}
private static Image? LoadEmbeddedImage(string resourceName)
{
var stream = typeof(MainForm).Assembly.GetManifestResourceStream(resourceName);
return stream is null ? null : Image.FromStream(stream);
}
private static Icon? LoadFormIcon()
{
using var image = LoadEmbeddedImage("OmniNX.DesktopInstaller.Assets.logo-128.png");
if (image is null)
return null;
using var bitmap = new Bitmap(image, new Size(32, 32));
var handle = bitmap.GetHicon();
try
{
using var temp = Icon.FromHandle(handle);
return (Icon)temp.Clone();
}
finally
{
DestroyIcon(handle);
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
private Control BuildDrivePanel()
{
var group = new GroupBox { Text = "SD Card / Target Drive", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) };
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 3, AutoSize = true };
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
_driveCombo = new ComboBox { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList };
_driveCombo.SelectedIndexChanged += (_, _) => UpdateInstallationStatus();
_refreshDrivesButton = new Button { Text = "Refresh", AutoSize = true };
_refreshDrivesButton.Click += (_, _) => RefreshDrives();
_statusLabel = new Label
{
Dock = DockStyle.Fill,
AutoSize = true,
ForeColor = Color.DimGray,
Text = "Select a removable drive (SD card reader).",
};
layout.Controls.Add(new Label { Text = "Drive:", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0);
layout.Controls.Add(_driveCombo, 1, 0);
layout.Controls.Add(_refreshDrivesButton, 2, 0);
layout.SetColumnSpan(_statusLabel, 3);
layout.Controls.Add(_statusLabel, 0, 1);
group.Controls.Add(layout);
return group;
}
private Control BuildVariantPanel()
{
var group = new GroupBox { Text = "OmniNX Pack Variant", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) };
var flow = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true };
_variantLight = new RadioButton { Text = "Light", AutoSize = true, Margin = new Padding(0, 0, 24, 0) };
_variantStandard = new RadioButton { Text = "Standard", AutoSize = true, Checked = true, Margin = new Padding(0, 0, 24, 0) };
_variantOc = new RadioButton { Text = "OC", AutoSize = true };
flow.Controls.AddRange([_variantLight, _variantStandard, _variantOc]);
group.Controls.Add(flow);
return group;
}
private Control BuildRamPanel()
{
var group = new GroupBox { Text = "RAM / Hekate Payload", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) };
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 1 };
_ram4Gb = new RadioButton
{
Text = "4 GB console (default)",
AutoSize = true,
Checked = true,
};
_ram8Gb = new RadioButton
{
Text = "8 GB console (RAM mod) — copies 8GB hekate payload to payload.bin and bootloader/update.bin",
AutoSize = true,
};
var warn = new Label
{
Text = "Warning: Only select 8 GB if the console has an 8 GB RAM mod. Wrong selection can cause boot issues.",
AutoSize = true,
ForeColor = Color.DarkOrange,
MaximumSize = new Size(820, 0),
Margin = new Padding(0, 6, 0, 0),
};
layout.Controls.Add(_ram4Gb);
layout.Controls.Add(_ram8Gb);
layout.Controls.Add(warn);
group.Controls.Add(layout);
return group;
}
private Control BuildHekatePanel()
{
var group = new GroupBox { Text = "Hekate / Atmosphere", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) };
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 1 };
_hekateStandard = new RadioButton
{
Text = "Standard Hekate / Atmosphere",
AutoSize = true,
Checked = true,
};
_hekatePro = new RadioButton
{
Text = "Hekate-Pro / Atmosphere-Pro (only for emuMMC on eMMC)",
AutoSize = true,
};
var hint = new Label
{
Text = "Pro variant replaces bootloader/atmosphere files from extras/hekate-ams-pro/.",
AutoSize = true,
ForeColor = Color.DimGray,
Margin = new Padding(0, 6, 0, 0),
};
layout.Controls.Add(_hekateStandard);
layout.Controls.Add(_hekatePro);
layout.Controls.Add(hint);
group.Controls.Add(layout);
return group;
}
private Control BuildOptionsPanel()
{
_forceCleanCheck = new CheckBox
{
Text = "Force clean install (full wipe with backup/restore) even if OmniNX is already installed",
AutoSize = true,
MaximumSize = new Size(820, 0),
};
return _forceCleanCheck;
}
private Control BuildActionPanel()
{
var panel = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, WrapContents = false };
_installButton = new Button
{
Text = "Download && Install",
AutoSize = true,
Height = 36,
Font = new Font(Font.FontFamily, 10F, FontStyle.Bold),
};
_installButton.Click += async (_, _) => await RunInstallAsync();
_progressBar = new ProgressBar { Width = 500, Height = 24, Margin = new Padding(16, 6, 0, 0) };
panel.Controls.Add(_installButton);
panel.Controls.Add(_progressBar);
return panel;
}
private Control BuildLogPanel()
{
var group = new GroupBox { Text = "Log", Dock = DockStyle.Fill, Padding = new Padding(10) };
_logBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ScrollBars = ScrollBars.Vertical,
ReadOnly = true,
Font = new Font("Consolas", 9F),
BackColor = Color.FromArgb(24, 24, 24),
ForeColor = Color.Gainsboro,
};
group.Controls.Add(_logBox);
return group;
}
private void RefreshDrives()
{
DriveService.DriveItem? selected = _driveCombo.SelectedItem as DriveService.DriveItem;
_driveCombo.Items.Clear();
foreach (var drive in DriveService.GetCandidateDrives())
_driveCombo.Items.Add(drive);
if (selected is not null)
{
for (var i = 0; i < _driveCombo.Items.Count; i++)
{
if (_driveCombo.Items[i] is DriveService.DriveItem d &&
d.Drive.Name.Equals(selected.Drive.Name, StringComparison.OrdinalIgnoreCase))
{
_driveCombo.SelectedIndex = i;
return;
}
}
}
if (_driveCombo.Items.Count > 0)
_driveCombo.SelectedIndex = 0;
else
UpdateInstallationStatus();
}
private DriveInfo? SelectedDrive =>
(_driveCombo.SelectedItem as DriveService.DriveItem)?.Drive;
private OmniNxVariant SelectedVariant
{
get
{
if (_variantOc.Checked) return OmniNxVariant.Oc;
if (_variantLight.Checked) return OmniNxVariant.Light;
return OmniNxVariant.Standard;
}
}
private void UpdateInstallationStatus()
{
var drive = SelectedDrive;
if (drive is null)
{
_statusLabel.Text = "No drive selected.";
_statusLabel.ForeColor = Color.DimGray;
return;
}
try
{
var status = _versionDetect.DetectInstallation(drive.RootDirectory.FullName);
if (status.IsInstalled)
{
_statusLabel.Text =
$"OmniNX detected: {VariantHelper.GetDisplayName(status.Variant)} v{status.Version} — " +
(_forceCleanCheck.Checked ? "will clean install" : "will update");
_statusLabel.ForeColor = Color.ForestGreen;
}
else
{
_statusLabel.Text = "No OmniNX installation detected — will perform clean install.";
_statusLabel.ForeColor = Color.SteelBlue;
}
}
catch (Exception ex)
{
_statusLabel.Text = $"Could not read drive: {ex.Message}";
_statusLabel.ForeColor = Color.Firebrick;
}
}
private async Task LoadLatestReleaseAsync()
{
try
{
SetUiEnabled(false);
_releaseLabel.Text = "Release: fetching...";
_latestRelease = await _gitea.GetLatestReleaseAsync();
_releaseLabel.Text = $"Latest release: {_latestRelease.TagName} ({_latestRelease.Name})";
Log($"Latest release: {_latestRelease.TagName}");
}
catch (Exception ex)
{
_releaseLabel.Text = "Release: fetch failed";
Log($"Failed to fetch release: {ex.Message}", error: true);
}
finally
{
SetUiEnabled(true);
}
}
private async Task RunInstallAsync()
{
var drive = SelectedDrive;
if (drive is null)
{
MessageBox.Show(this, "Please select a target drive.", "OmniNX Installer",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (_latestRelease is null)
{
MessageBox.Show(this, "Release information is not loaded yet. Click 'Refresh release' first.",
"OmniNX Installer", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var variant = SelectedVariant;
var asset = _gitea.FindPackAsset(_latestRelease, variant);
if (asset is null)
{
MessageBox.Show(this,
$"No zip asset found for {VariantHelper.GetDisplayName(variant)} in release {_latestRelease.TagName}.",
"OmniNX Installer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var driveRoot = drive.RootDirectory.FullName;
var status = _versionDetect.DetectInstallation(driveRoot);
var mode = _versionDetect.DetermineMode(status, _forceCleanCheck.Checked);
var modeText = mode == InstallMode.Update ? "UPDATE" : "CLEAN INSTALL";
var confirm = MessageBox.Show(this,
$"Target drive: {drive.Name}\n" +
$"Variant: {VariantHelper.GetDisplayName(variant)}\n" +
$"Mode: {modeText}\n" +
$"Release: {_latestRelease.TagName}\n" +
$"RAM: {(_ram8Gb.Checked ? "8 GB" : "4 GB")}\n" +
$"Hekate/AMS: {(_hekatePro.Checked ? "Pro" : "Standard")}\n\n" +
"All data on paths targeted by the installer may be modified or deleted.\n" +
"Continue?",
"Confirm installation",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (confirm != DialogResult.Yes)
return;
_operationCts?.Cancel();
_operationCts = new CancellationTokenSource();
var token = _operationCts.Token;
try
{
SetUiEnabled(false);
_progressBar.Value = 0;
_logBox.Clear();
var cacheDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OmniNX-Desktop-Installer",
"cache");
PathUtils.EnsureDirectory(cacheDir);
var zipPath = Path.Combine(cacheDir, asset.Name);
if (!File.Exists(zipPath))
{
Log($"Downloading {asset.Name} ({asset.Size / (1024 * 1024)} MB)...");
await _gitea.DownloadAssetAsync(asset, zipPath, new Progress<(long, long)>(p =>
{
var (downloaded, total) = p;
if (total > 0)
{
var pct = (int)(downloaded * 100 / total);
_progressBar.Value = Math.Clamp(pct, 0, 100);
}
}), token);
Log("Download complete.");
}
else
{
Log($"Using cached pack: {zipPath}");
}
var extractRoot = Path.Combine(cacheDir, "extract", _latestRelease.TagName);
var staging = ZipExtractor.ExtractPack(zipPath, extractRoot, new Progress<string>(msg => Log(msg)));
Log($"Pack staging: {staging}");
var options = new InstallOptions
{
DriveRoot = driveRoot,
Variant = variant,
Mode = mode,
Use8GbRam = _ram8Gb.Checked,
UseHekateAmsPro = _hekatePro.Checked,
ForceCleanInstall = _forceCleanCheck.Checked,
StagingPath = staging,
};
var progress = new Progress<InstallProgressReport>(report =>
{
Log(report.Message, report.IsError);
if (report.Percent is int pct)
_progressBar.Value = Math.Clamp(pct, 0, 100);
});
await _installEngine.RunAsync(options, progress, token);
_progressBar.Value = 100;
MessageBox.Show(this,
mode == InstallMode.Update
? "OmniNX update completed successfully."
: "OmniNX installation completed successfully.",
"OmniNX Installer",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
UpdateInstallationStatus();
}
catch (OperationCanceledException)
{
Log("Operation cancelled.", error: true);
}
catch (Exception ex)
{
Log($"Installation failed: {ex.Message}", error: true);
MessageBox.Show(this, ex.Message, "Installation failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetUiEnabled(true);
}
}
private void Log(string message, bool error = false)
{
if (InvokeRequired)
{
BeginInvoke(() => Log(message, error));
return;
}
var prefix = $"[{DateTime.Now:HH:mm:ss}] ";
_logBox.AppendText(prefix + message + Environment.NewLine);
if (error)
_statusLabel.ForeColor = Color.Firebrick;
}
private void SetUiEnabled(bool enabled)
{
if (InvokeRequired)
{
BeginInvoke(() => SetUiEnabled(enabled));
return;
}
_installButton.Enabled = enabled;
_checkReleaseButton.Enabled = enabled;
_refreshDrivesButton.Enabled = enabled;
_driveCombo.Enabled = enabled;
_variantLight.Enabled = enabled;
_variantStandard.Enabled = enabled;
_variantOc.Enabled = enabled;
_ram4Gb.Enabled = enabled;
_ram8Gb.Enabled = enabled;
_hekateStandard.Enabled = enabled;
_hekatePro.Enabled = enabled;
_forceCleanCheck.Enabled = enabled;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
_forceCleanCheck.CheckedChanged += (_, _) => UpdateInstallationStatus();
}
}
@@ -0,0 +1,54 @@
namespace OmniNX.DesktopInstaller.Models;
public enum OmniNxVariant
{
None = 0,
Light,
Standard,
Oc,
}
public enum InstallMode
{
Clean,
Update,
}
public sealed class InstallOptions
{
public required string DriveRoot { get; init; }
public required OmniNxVariant Variant { get; init; }
public InstallMode Mode { get; init; }
public bool Use8GbRam { get; init; }
public bool UseHekateAmsPro { get; init; }
public bool ForceCleanInstall { get; init; }
public string? StagingPath { get; init; }
}
public sealed class OmniNxStatus
{
public bool IsInstalled { get; init; }
public OmniNxVariant Variant { get; init; }
public string Version { get; init; } = string.Empty;
}
public sealed class ReleaseInfo
{
public required string TagName { get; init; }
public required string Name { get; init; }
public required IReadOnlyList<ReleaseAsset> Assets { get; init; }
}
public sealed class ReleaseAsset
{
public required string Name { get; init; }
public required string DownloadUrl { get; init; }
public long Size { get; init; }
}
public sealed class InstallProgressReport
{
public required string Message { get; init; }
public int? Percent { get; init; }
public bool IsError { get; init; }
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\app.ico</ApplicationIcon>
<AssemblyName>OmniNX-Desktop-Installer</AssemblyName>
<RootNamespace>OmniNX.DesktopInstaller</RootNamespace>
<Version>1.0.0</Version>
<Authors>OmniNX</Authors>
<Description>PC-based OmniNX installer for mass deployments and modchip installers</Description>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\logo-128.png" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
namespace OmniNX.DesktopInstaller;
static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
@@ -0,0 +1,118 @@
namespace OmniNX.DesktopInstaller.Services;
public sealed class BackupService
{
private const string TempBackup = @"temp_backup";
private const string DbiConfig = @"switch\DBI\dbi.config";
private const string ProdKeys = @"switch\prod.keys";
private const string CyberFoilDir = @"switch\CyberFoil";
private const string CheckpointDir = @"switch\Checkpoint";
private const string HorizonOcConfig = @"config\horizon-oc\config.ini";
private const string HorizonOcBackupDir = @".omninx_oc_update";
public void BackupUserData(string driveRoot, IProgress<string>? progress = null)
{
var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup);
PathUtils.EnsureDirectory(backupRoot);
progress?.Report("Backing up CyberFoil, Checkpoint, DBI (dbi.config), prod.keys...");
CopyIfExists(PathUtils.ToDrivePath(driveRoot, DbiConfig),
Path.Combine(backupRoot, "dbi.config"));
CopyIfExists(PathUtils.ToDrivePath(driveRoot, ProdKeys),
Path.Combine(backupRoot, "prod.keys"));
var cyberfoil = PathUtils.ToDrivePath(driveRoot, CyberFoilDir);
if (Directory.Exists(cyberfoil))
{
var dst = Path.Combine(backupRoot, "CyberFoil");
PathUtils.EnsureDirectory(dst);
CopyIfExists(Path.Combine(cyberfoil, "cyberfoil.nro"), Path.Combine(dst, "cyberfoil.nro"));
CopyIfExists(Path.Combine(cyberfoil, "config.json"), Path.Combine(dst, "config.json"));
var remotes = Path.Combine(cyberfoil, "remotes");
if (Directory.Exists(remotes))
PathUtils.CopyDirectory(remotes, Path.Combine(dst, "remotes"));
}
var checkpoint = PathUtils.ToDrivePath(driveRoot, CheckpointDir);
if (Directory.Exists(checkpoint))
PathUtils.CopyDirectory(checkpoint, Path.Combine(backupRoot, "Checkpoint"));
}
public void RestoreUserData(string driveRoot, IProgress<string>? progress = null)
{
var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup);
if (!Directory.Exists(backupRoot))
return;
progress?.Report("Restoring CyberFoil, Checkpoint, DBI (dbi.config), prod.keys...");
PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"switch"));
CopyIfExists(Path.Combine(backupRoot, "dbi.config"),
PathUtils.ToDrivePath(driveRoot, DbiConfig));
CopyIfExists(Path.Combine(backupRoot, "prod.keys"),
PathUtils.ToDrivePath(driveRoot, ProdKeys));
var cyberBackup = Path.Combine(backupRoot, "CyberFoil");
if (Directory.Exists(cyberBackup))
{
var dst = PathUtils.ToDrivePath(driveRoot, CyberFoilDir);
PathUtils.EnsureDirectory(dst);
CopyIfExists(Path.Combine(cyberBackup, "cyberfoil.nro"), Path.Combine(dst, "cyberfoil.nro"));
CopyIfExists(Path.Combine(cyberBackup, "config.json"), Path.Combine(dst, "config.json"));
var remotes = Path.Combine(cyberBackup, "remotes");
if (Directory.Exists(remotes))
PathUtils.CopyDirectory(remotes, Path.Combine(dst, "remotes"));
}
var checkpointBackup = Path.Combine(backupRoot, "Checkpoint");
if (Directory.Exists(checkpointBackup))
PathUtils.CopyDirectory(checkpointBackup, PathUtils.ToDrivePath(driveRoot, @"switch"));
}
public void CleanupBackup(string driveRoot)
{
var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup);
if (Directory.Exists(backupRoot))
PathUtils.DeletePath(backupRoot);
}
public void BackupHorizonOcConfig(string driveRoot, IProgress<string>? progress = null)
{
var src = PathUtils.ToDrivePath(driveRoot, HorizonOcConfig);
if (!File.Exists(src))
return;
progress?.Report("HorizonOC: backing up config.ini...");
var dstDir = PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir);
PathUtils.EnsureDirectory(dstDir);
PathUtils.CopyFile(src, Path.Combine(dstDir, "config.ini"));
}
public void RestoreHorizonOcConfig(string driveRoot, IProgress<string>? progress = null)
{
var src = Path.Combine(PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir), "config.ini");
if (!File.Exists(src))
return;
progress?.Report("HorizonOC: restoring config.ini...");
PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"config\horizon-oc"));
PathUtils.CopyFile(src, PathUtils.ToDrivePath(driveRoot, HorizonOcConfig));
}
public void CleanupHorizonOcBackup(string driveRoot)
{
var dir = PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir);
if (Directory.Exists(dir))
PathUtils.DeletePath(dir);
}
private static void CopyIfExists(string src, string dst)
{
if (!File.Exists(src))
return;
PathUtils.CopyFile(src, dst);
}
}
@@ -0,0 +1,132 @@
namespace OmniNX.DesktopInstaller.Services;
public sealed class ConfigIniService
{
private const string ConfigIniRelative = @"config\omninx\config.ini";
private const string LegacyRamConfigRelative = @"config\omninx\ram_config.ini";
public bool TryReadBool(string driveRoot, string section, string key, out bool value)
{
value = false;
var path = PathUtils.ToDrivePath(driveRoot, ConfigIniRelative);
if (!File.Exists(path))
return false;
var inSection = false;
foreach (var rawLine in File.ReadLines(path))
{
var line = rawLine.Trim();
if (line.Length == 0)
continue;
if (line.StartsWith('[') && line.EndsWith(']'))
{
inSection = line[1..^1].Equals(section, StringComparison.Ordinal);
continue;
}
if (!inSection)
continue;
var eq = line.IndexOf('=');
if (eq <= 0)
continue;
var k = line[..eq].Trim();
if (!k.Equals(key, StringComparison.Ordinal))
continue;
var val = line[(eq + 1)..].Trim();
if (val == "1")
{
value = true;
return true;
}
if (val == "0")
{
value = false;
return true;
}
return false;
}
return false;
}
public void MigrateLegacyRamConfig(string driveRoot)
{
var legacy = PathUtils.ToDrivePath(driveRoot, LegacyRamConfigRelative);
if (!File.Exists(legacy))
return;
if (TryReadLegacyBool(legacy, "Ram", "8gb", out var use8Gb))
{
var havePro = TryReadBool(driveRoot, "HekateAms", "pro", out var usePro);
WriteExtras(driveRoot, needRam: true, use8Gb, needHekateAms: havePro, havePro ? usePro : false);
}
PathUtils.DeletePath(legacy);
}
public void WriteExtras(string driveRoot, bool needRam, bool use8Gb, bool needHekateAms, bool usePro)
{
var path = PathUtils.ToDrivePath(driveRoot, ConfigIniRelative);
PathUtils.EnsureDirectory(Path.GetDirectoryName(path)!);
using var writer = new StreamWriter(path, append: false);
if (needRam)
{
writer.WriteLine("[Ram]");
writer.WriteLine($"8gb={(use8Gb ? 1 : 0)}");
writer.WriteLine();
}
if (needHekateAms)
{
writer.WriteLine("[HekateAms]");
writer.WriteLine($"pro={(usePro ? 1 : 0)}");
}
}
private static bool TryReadLegacyBool(string path, string section, string key, out bool value)
{
value = false;
var inSection = false;
foreach (var rawLine in File.ReadLines(path))
{
var line = rawLine.Trim();
if (line.StartsWith('[') && line.EndsWith(']'))
{
inSection = line[1..^1].Equals(section, StringComparison.Ordinal);
continue;
}
if (!inSection)
continue;
var eq = line.IndexOf('=');
if (eq <= 0)
continue;
if (!line[..eq].Trim().Equals(key, StringComparison.Ordinal))
continue;
var val = line[(eq + 1)..].Trim();
if (val == "1")
{
value = true;
return true;
}
if (val == "0")
{
value = false;
return true;
}
}
return false;
}
}
@@ -0,0 +1,187 @@
using CleanLists = OmniNX.DesktopInstaller.Data.DeletionLists.Clean;
using UpdateLists = OmniNX.DesktopInstaller.Data.DeletionLists.Update;
namespace OmniNX.DesktopInstaller.Services;
public sealed class DeletionService
{
private readonly PreserveService _preserve = new();
public void RunCleanDeletion(string driveRoot, IProgress<string>? progress = null)
{
DeleteGroup(driveRoot, "atmosphere", CleanLists.clean_atmosphere_dirs_to_delete, progress);
DeleteGroup(driveRoot, "bootloader", CleanLists.clean_bootloader_dirs_to_delete, progress);
DeleteFiles(driveRoot, CleanLists.clean_bootloader_files_to_delete, progress);
DeleteGroup(driveRoot, "config", CleanLists.clean_config_dirs_to_delete, progress);
DeleteGroup(driveRoot, "switch", CleanLists.clean_switch_dirs_to_delete, progress);
DeleteFiles(driveRoot, CleanLists.clean_root_files_to_delete, progress);
DeleteGroup(driveRoot, "misc", CleanLists.clean_misc_dirs_to_delete, progress);
DeleteFiles(driveRoot, CleanLists.clean_misc_files_to_delete, progress);
DeleteFiles(driveRoot, CleanLists.old_version_files_to_delete, progress);
PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"switch"));
}
public void RunUpdateDeletion(string driveRoot, IProgress<string>? progress = null)
{
_preserve.Load(driveRoot, progress);
DeleteGroup(driveRoot, "atmosphere", UpdateLists.atmosphere_dirs_to_delete, progress, _preserve);
DeleteGroup(driveRoot, "atmosphere/contents", UpdateLists.atmosphere_contents_dirs_to_delete, progress, _preserve);
DeleteFiles(driveRoot, UpdateLists.atmosphere_files_to_delete, progress, _preserve);
DeleteGroup(driveRoot, "bootloader", UpdateLists.bootloader_dirs_to_delete, progress, _preserve);
DeleteFiles(driveRoot, UpdateLists.bootloader_files_to_delete, progress, _preserve);
DeleteGroup(driveRoot, "config", UpdateLists.config_dirs_to_delete, progress, _preserve);
DeleteGroup(driveRoot, "switch", UpdateLists.switch_dirs_to_delete, progress, _preserve);
DeleteFiles(driveRoot, UpdateLists.switch_files_to_delete, progress, _preserve);
DeleteFiles(driveRoot, UpdateLists.root_files_to_delete, progress, _preserve);
DeleteGroup(driveRoot, "misc", UpdateLists.misc_dirs_to_delete, progress, _preserve);
DeleteFiles(driveRoot, UpdateLists.misc_files_to_delete, progress, _preserve);
_preserve.Clear();
}
public void RunPostRestoreCleanup(string driveRoot)
{
DeleteGroup(driveRoot, "switch (post-restore)", CleanLists.clean_post_restore_dirs_to_delete, null);
}
private static void DeleteGroup(
string driveRoot,
string label,
string[] paths,
IProgress<string>? progress,
PreserveService? preserve = null)
{
if (paths.Length == 0)
return;
progress?.Report($"Deleting {label}...");
foreach (var rel in paths)
{
var full = PathUtils.ToDrivePath(driveRoot, rel);
if (!PathUtils.PathExists(full))
continue;
if (preserve?.IsBlocked(full, driveRoot) == true)
{
progress?.Report($" Skipped (preserve): {rel}");
continue;
}
progress?.Report($" Delete: {rel}");
PathUtils.DeletePath(full);
}
}
private static void DeleteFiles(
string driveRoot,
string[] paths,
IProgress<string>? progress,
PreserveService? preserve = null)
{
foreach (var rel in paths)
{
var full = PathUtils.ToDrivePath(driveRoot, rel);
if (!File.Exists(full))
continue;
if (preserve?.IsBlocked(full, driveRoot) == true)
{
progress?.Report($" Skipped (preserve): {rel}");
continue;
}
progress?.Report($" Delete: {rel}");
PathUtils.DeletePath(full);
}
}
}
public sealed class PreserveService
{
private readonly List<string> _paths = new();
public void Load(string driveRoot, IProgress<string>? progress = null)
{
_paths.Clear();
var iniPath = PathUtils.ToDrivePath(driveRoot, @"config\omninx\preserve.ini");
if (!File.Exists(iniPath))
return;
var inSection = false;
foreach (var rawLine in File.ReadLines(iniPath))
{
var line = rawLine.Trim();
if (line.StartsWith('[') && line.EndsWith(']'))
{
inSection = line[1..^1].Equals("Preserve", StringComparison.Ordinal);
continue;
}
if (!inSection || !line.Contains('='))
continue;
var eq = line.IndexOf('=');
var key = line[..eq].Trim().Trim('\'', '"');
var val = line[(eq + 1)..].Trim();
if (!IsTruthy(val))
continue;
var normalized = NormalizePreservePath(key, driveRoot);
if (normalized is null)
continue;
if (!_paths.Contains(normalized, StringComparer.OrdinalIgnoreCase))
_paths.Add(normalized);
}
if (_paths.Count > 0)
progress?.Report($"preserve.ini: {_paths.Count} path(s) excluded from update cleanup.");
}
public bool IsBlocked(string deleteFullPath, string driveRoot)
{
if (_paths.Count == 0)
return false;
var normalizedDelete = Path.GetFullPath(deleteFullPath).TrimEnd(Path.DirectorySeparatorChar);
foreach (var preserve in _paths)
{
var normalizedPreserve = Path.GetFullPath(preserve).TrimEnd(Path.DirectorySeparatorChar);
if (normalizedDelete.Equals(normalizedPreserve, StringComparison.OrdinalIgnoreCase))
return true;
if (IsPrefix(normalizedPreserve, normalizedDelete))
return true;
if (IsPrefix(normalizedDelete, normalizedPreserve))
return true;
}
return false;
}
public void Clear() => _paths.Clear();
private static string? NormalizePreservePath(string key, string driveRoot)
{
var path = key.Replace("sd:/", string.Empty).Replace("sd:\\", string.Empty)
.Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar);
if (path.Length < 2)
return null;
return PathUtils.ToDrivePath(driveRoot, path);
}
private static bool IsPrefix(string prefix, string path)
{
if (!path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
return false;
return path.Length == prefix.Length ||
path[prefix.Length] == Path.DirectorySeparatorChar;
}
private static bool IsTruthy(string val)
{
if (string.IsNullOrEmpty(val))
return false;
var c = char.ToUpperInvariant(val[0]);
return c is '1' or 'T' or 'Y';
}
}
@@ -0,0 +1,97 @@
namespace OmniNX.DesktopInstaller.Services;
public sealed class ExtrasService
{
private readonly ConfigIniService _config = new();
public (bool needRam, bool needHekateAms) DetectExtras(string driveRoot)
{
var extras = PathUtils.ToDrivePath(driveRoot, "extras");
var needRam = File.Exists(Path.Combine(extras, "hekate_8gb.bin")) ||
File.Exists(Path.Combine(extras, "hekate-pro_8gb.bin"));
var needHekateAms = Directory.Exists(Path.Combine(extras, "hekate-ams-pro"));
return (needRam, needHekateAms);
}
public void ApplyExtras(
string driveRoot,
bool use8GbRam,
bool useHekateAmsPro,
IProgress<string>? progress = null)
{
_config.MigrateLegacyRamConfig(driveRoot);
var extras = PathUtils.ToDrivePath(driveRoot, "extras");
if (!Directory.Exists(extras))
return;
var (needRam, needHekateAms) = DetectExtras(driveRoot);
if (!needRam && !needHekateAms)
return;
progress?.Report("Saving RAM / Hekate-AMS selection to config.ini...");
_config.WriteExtras(driveRoot, needRam, use8GbRam, needHekateAms, useHekateAmsPro);
if (useHekateAmsPro && needHekateAms)
{
progress?.Report("Installing Hekate-Pro / Atmosphere-Pro...");
ApplyHekateAmsPro(extras, driveRoot, progress);
}
else if (needHekateAms)
{
progress?.Report("Keeping standard Hekate / Atmosphere.");
}
if (needRam)
{
progress?.Report("Applying RAM payload setting...");
Apply8GbPayload(extras, driveRoot, use8GbRam, useHekateAmsPro);
}
progress?.Report("Removing extras/...");
PathUtils.DeletePath(extras);
}
private static void ApplyHekateAmsPro(string extrasDir, string driveRoot, IProgress<string>? progress)
{
var src = Path.Combine(extrasDir, "hekate-ams-pro");
if (!Directory.Exists(src))
return;
foreach (var entry in Directory.GetFileSystemEntries(src))
{
var name = Path.GetFileName(entry);
var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), name);
if (Directory.Exists(entry))
PathUtils.CopyDirectory(entry, dst);
else
PathUtils.CopyFile(entry, dst);
progress?.Report($" Copied: {name}");
}
}
private static void Apply8GbPayload(string extrasDir, string driveRoot, bool use8Gb, bool usePro)
{
var hekate8 = Path.Combine(extrasDir, "hekate_8gb.bin");
var hekatePro8 = Path.Combine(extrasDir, "hekate-pro_8gb.bin");
if (!use8Gb)
{
if (File.Exists(hekate8)) File.Delete(hekate8);
if (File.Exists(hekatePro8)) File.Delete(hekatePro8);
return;
}
var src = usePro ? hekatePro8 : hekate8;
if (!File.Exists(src))
return;
var payloadDst = PathUtils.ToDrivePath(driveRoot, "payload.bin");
var updateDst = PathUtils.ToDrivePath(driveRoot, @"bootloader\update.bin");
PathUtils.CopyFile(src, payloadDst);
PathUtils.CopyFile(src, updateDst);
if (File.Exists(hekate8)) File.Delete(hekate8);
if (File.Exists(hekatePro8)) File.Delete(hekatePro8);
}
}
@@ -0,0 +1,100 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using OmniNX.DesktopInstaller.Models;
namespace OmniNX.DesktopInstaller.Services;
public sealed class GiteaClient
{
public const string DefaultBaseUrl = "https://git.niklascfw.de";
private readonly HttpClient _http;
private readonly string _baseUrl;
public GiteaClient(HttpClient? http = null, string? baseUrl = null)
{
_http = http ?? new HttpClient();
_http.DefaultRequestHeaders.UserAgent.ParseAdd("OmniNX-Desktop-Installer/1.0");
_baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/');
}
public async Task<ReleaseInfo> GetLatestReleaseAsync(CancellationToken cancellationToken = default)
{
var url = $"{_baseUrl}/api/v1/repos/OmniNX/OmniNX/releases/latest";
var response = await _http.GetFromJsonAsync<GiteaReleaseDto>(url, cancellationToken)
?? throw new InvalidOperationException("Empty release response from Gitea.");
return MapRelease(response);
}
public async Task<string> DownloadAssetAsync(
ReleaseAsset asset,
string destinationPath,
IProgress<(long downloaded, long total)>? progress = null,
CancellationToken cancellationToken = default)
{
PathUtils.EnsureDirectory(Path.GetDirectoryName(destinationPath)!);
using var response = await _http.GetAsync(asset.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength ?? asset.Size;
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var output = File.Create(destinationPath);
var buffer = new byte[81920];
long downloaded = 0;
int read;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
downloaded += read;
progress?.Report((downloaded, total));
}
return destinationPath;
}
public ReleaseAsset? FindPackAsset(ReleaseInfo release, OmniNxVariant variant)
{
var prefix = VariantHelper.GetZipAssetPrefix(variant);
return release.Assets.FirstOrDefault(a =>
a.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
}
private static ReleaseInfo MapRelease(GiteaReleaseDto dto) => new()
{
TagName = dto.TagName ?? string.Empty,
Name = dto.Name ?? dto.TagName ?? string.Empty,
Assets = dto.Assets?.Select(a => new ReleaseAsset
{
Name = a.Name ?? string.Empty,
DownloadUrl = a.BrowserDownloadUrl ?? string.Empty,
Size = a.Size,
}).ToList() ?? [],
};
private sealed class GiteaReleaseDto
{
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("assets")]
public List<GiteaAssetDto>? Assets { get; set; }
}
private sealed class GiteaAssetDto
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("browser_download_url")]
public string? BrowserDownloadUrl { get; set; }
[JsonPropertyName("size")]
public long Size { get; set; }
}
}
@@ -0,0 +1,186 @@
using OmniNX.DesktopInstaller.Models;
namespace OmniNX.DesktopInstaller.Services;
public sealed class InstallEngine
{
private readonly DeletionService _deletion = new();
private readonly BackupService _backup = new();
private readonly ExtrasService _extras = new();
public async Task RunAsync(
InstallOptions options,
IProgress<InstallProgressReport> progress,
CancellationToken cancellationToken = default)
{
var driveRoot = PathUtils.NormalizeDriveRoot(options.DriveRoot);
var staging = options.StagingPath
?? throw new InvalidOperationException("Staging path is required.");
if (!Directory.Exists(staging))
throw new DirectoryNotFoundException($"Staging path not found: {staging}");
Report(progress, $"Install mode: {options.Mode}");
Report(progress, $"Pack variant: {VariantHelper.GetDisplayName(options.Variant)}");
if (options.Mode == InstallMode.Update)
{
await RunUpdateAsync(driveRoot, staging, options, progress, cancellationToken);
}
else
{
await RunCleanAsync(driveRoot, staging, options, progress, cancellationToken);
}
Report(progress, options.Mode == InstallMode.Update
? "Update completed successfully."
: "Installation completed successfully.");
}
private async Task RunUpdateAsync(
string driveRoot,
string staging,
InstallOptions options,
IProgress<InstallProgressReport> progress,
CancellationToken cancellationToken)
{
if (options.Variant == OmniNxVariant.Oc)
{
Report(progress, "HorizonOC: backing up config.ini...");
_backup.BackupHorizonOcConfig(driveRoot, StringProgress(progress));
}
Report(progress, "Step 1: Cleanup...");
await Task.Run(() => _deletion.RunUpdateDeletion(driveRoot, StringProgress(progress)), cancellationToken);
Report(progress, "Step 2: Copying files...");
await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken);
Report(progress, "Applying RAM / Hekate-AMS extras...");
await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro,
StringProgress(progress)), cancellationToken);
if (options.Variant == OmniNxVariant.Oc)
{
Report(progress, "HorizonOC: restoring config.ini...");
await Task.Run(() =>
{
_backup.RestoreHorizonOcConfig(driveRoot, StringProgress(progress));
_backup.CleanupHorizonOcBackup(driveRoot);
}, cancellationToken);
}
}
private async Task RunCleanAsync(
string driveRoot,
string staging,
InstallOptions options,
IProgress<InstallProgressReport> progress,
CancellationToken cancellationToken)
{
Report(progress, "Step 1: Backing up user data...");
await Task.Run(() => _backup.BackupUserData(driveRoot, StringProgress(progress)), cancellationToken);
Report(progress, "Step 2: Cleaning old installation...");
await Task.Run(() => _deletion.RunCleanDeletion(driveRoot, StringProgress(progress)), cancellationToken);
Report(progress, "Step 3: Restoring user data...");
await Task.Run(() =>
{
_backup.RestoreUserData(driveRoot, StringProgress(progress));
_deletion.RunPostRestoreCleanup(driveRoot);
_backup.CleanupBackup(driveRoot);
}, cancellationToken);
Report(progress, "Step 4: Copying files...");
await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken);
Report(progress, "Applying RAM / Hekate-AMS extras...");
await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro,
StringProgress(progress)), cancellationToken);
}
private static void CopyPackFiles(
string staging,
string driveRoot,
OmniNxVariant variant,
IProgress<InstallProgressReport> progress)
{
CopyFolderIfExists(staging, driveRoot, "atmosphere", progress);
CopyFolderIfExists(staging, driveRoot, "bootloader", progress);
CopyFolderIfExists(staging, driveRoot, "config", progress);
CopyFolderIfExists(staging, driveRoot, "switch", progress);
CopyFolderIfExists(staging, driveRoot, "themes", progress);
CopyFolderIfExists(staging, driveRoot, "warmboot_mariko", progress);
if (variant == OmniNxVariant.Oc)
CopyFolderIfExists(staging, driveRoot, "SaltySD", progress);
CopyFolderIfExists(staging, driveRoot, "extras", progress);
CopyRootFiles(staging, driveRoot, progress);
}
private static void CopyFolderIfExists(
string staging,
string driveRoot,
string folder,
IProgress<InstallProgressReport> progress)
{
var src = Path.Combine(staging, folder);
if (!Directory.Exists(src))
return;
var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), folder);
Report(progress, $"Copying {folder}/...");
PathUtils.CopyDirectory(src, dst, new Progress<(int current, int total, string file)>(copyProgress =>
{
var (current, total, file) = copyProgress;
var pct = total > 0 ? current * 100 / total : 0;
progress.Report(new InstallProgressReport
{
Message = $" {folder}/ {file}",
Percent = pct,
});
}));
}
private static void CopyRootFiles(
string staging,
string driveRoot,
IProgress<InstallProgressReport> progress)
{
Report(progress, "Copying root files...");
var skipDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"atmosphere", "bootloader", "config", "switch", "themes",
"warmboot_mariko", "SaltySD", "extras",
};
foreach (var file in Directory.GetFiles(staging))
{
var name = Path.GetFileName(file);
if (name.StartsWith('.'))
continue;
var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), name);
PathUtils.CopyFile(file, dst);
Report(progress, $" {name}");
}
foreach (var dir in Directory.GetDirectories(staging))
{
var name = Path.GetFileName(dir);
if (skipDirs.Contains(name))
continue;
Report(progress, $"Skipping unexpected directory at pack root: {name}");
}
}
private static void Report(IProgress<InstallProgressReport> progress, string message, bool isError = false)
{
progress.Report(new InstallProgressReport { Message = message, IsError = isError });
}
private static IProgress<string> StringProgress(IProgress<InstallProgressReport> progress) =>
new Progress<string>(msg => Report(progress, msg));
}
@@ -0,0 +1,133 @@
using OmniNX.DesktopInstaller.Models;
namespace OmniNX.DesktopInstaller.Services;
public static class VariantHelper
{
public static string GetDisplayName(OmniNxVariant variant) => variant switch
{
OmniNxVariant.Light => "Light",
OmniNxVariant.Standard => "Standard",
OmniNxVariant.Oc => "OC",
_ => "None",
};
public static string GetStagingFolderName(OmniNxVariant variant) => variant switch
{
OmniNxVariant.Light => "OmniNX Light",
OmniNxVariant.Standard => "OmniNX Standard",
OmniNxVariant.Oc => "OmniNX OC",
_ => string.Empty,
};
public static string GetZipAssetPrefix(OmniNxVariant variant) => variant switch
{
OmniNxVariant.Light => "OmniNX-Light",
OmniNxVariant.Standard => "OmniNX-Standard",
OmniNxVariant.Oc => "OmniNX-OC",
_ => string.Empty,
};
public static OmniNxVariant ParsePackName(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return OmniNxVariant.None;
return value.Trim() switch
{
"Light" => OmniNxVariant.Light,
"Standard" => OmniNxVariant.Standard,
"OC" => OmniNxVariant.Oc,
_ => OmniNxVariant.None,
};
}
}
public static class PathUtils
{
public static string NormalizeDriveRoot(string driveRoot)
{
var root = driveRoot.Trim();
if (!root.EndsWith(Path.DirectorySeparatorChar))
root += Path.DirectorySeparatorChar;
return root;
}
public static string ToDrivePath(string driveRoot, string sdRelative)
{
var rel = sdRelative.Replace("sd:/", string.Empty).Replace("sd:\\", string.Empty)
.Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(NormalizeDriveRoot(driveRoot), rel);
}
public static string RelativeFromDrive(string driveRoot, string fullPath)
{
var root = NormalizeDriveRoot(driveRoot);
if (fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
return fullPath[root.Length..];
return fullPath;
}
public static bool PathExists(string path)
{
return Directory.Exists(path) || File.Exists(path);
}
public static void EnsureDirectory(string path)
{
Directory.CreateDirectory(path);
}
public static void CopyFile(string src, string dst, bool overwrite = true)
{
var dir = Path.GetDirectoryName(dst);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
File.Copy(src, dst, overwrite);
}
public static void CopyDirectory(string src, string dst, IProgress<(int current, int total, string file)>? progress = null)
{
if (!Directory.Exists(src))
return;
var files = Directory.GetFiles(src, "*", SearchOption.AllDirectories);
var total = files.Length;
var current = 0;
foreach (var file in files)
{
var rel = Path.GetRelativePath(src, file);
var target = Path.Combine(dst, rel);
CopyFile(file, target);
current++;
progress?.Report((current, total, rel));
}
}
public static void DeletePath(string path)
{
if (File.Exists(path))
{
File.SetAttributes(path, FileAttributes.Normal);
File.Delete(path);
return;
}
if (Directory.Exists(path))
Directory.Delete(path, recursive: true);
}
public static int CountItemsRecursive(string path)
{
if (File.Exists(path))
return 1;
if (!Directory.Exists(path))
return 0;
var count = 1;
foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories))
count++;
return count;
}
}
@@ -0,0 +1,79 @@
using OmniNX.DesktopInstaller.Models;
namespace OmniNX.DesktopInstaller.Services;
public sealed class VersionDetectService
{
private const string ManifestRelative = @"config\omninx\manifest.ini";
public OmniNxStatus DetectInstallation(string driveRoot)
{
var manifest = PathUtils.ToDrivePath(driveRoot, ManifestRelative);
if (!File.Exists(manifest))
return new OmniNxStatus { IsInstalled = false, Variant = OmniNxVariant.None };
var variant = OmniNxVariant.None;
var version = string.Empty;
var inSection = false;
foreach (var rawLine in File.ReadLines(manifest))
{
var line = rawLine.Trim();
if (line.StartsWith('[') && line.EndsWith(']'))
{
inSection = line[1..^1].Equals("OmniNX", StringComparison.Ordinal);
continue;
}
if (!inSection || !line.Contains('='))
continue;
var eq = line.IndexOf('=');
var key = line[..eq].Trim();
var val = line[(eq + 1)..].Trim();
if (key.Equals("current_pack", StringComparison.Ordinal))
variant = VariantHelper.ParsePackName(val);
else if (key.Equals("version", StringComparison.Ordinal))
version = val;
}
return new OmniNxStatus
{
IsInstalled = variant != OmniNxVariant.None,
Variant = variant,
Version = version,
};
}
public InstallMode DetermineMode(OmniNxStatus status, bool forceClean)
{
if (forceClean)
return InstallMode.Clean;
return status.IsInstalled ? InstallMode.Update : InstallMode.Clean;
}
}
public static class DriveService
{
public sealed record DriveItem(DriveInfo Drive)
{
public override string ToString() => FormatDriveLabel(Drive);
}
public static IReadOnlyList<DriveItem> GetCandidateDrives()
{
return DriveInfo.GetDrives()
.Where(d => d.IsReady && d.DriveType == DriveType.Removable)
.OrderBy(d => d.Name)
.Select(d => new DriveItem(d))
.ToList();
}
public static string FormatDriveLabel(DriveInfo drive)
{
var label = string.IsNullOrWhiteSpace(drive.VolumeLabel) ? "No Label" : drive.VolumeLabel;
var sizeGb = drive.TotalSize / (1024.0 * 1024 * 1024);
return $"{drive.Name} [{label}] ({sizeGb:F1} GB, {drive.DriveFormat})";
}
}
@@ -0,0 +1,40 @@
using System.IO.Compression;
using OmniNX.DesktopInstaller.Models;
namespace OmniNX.DesktopInstaller.Services;
public static class ZipExtractor
{
public static string ExtractPack(string zipPath, string extractRoot, IProgress<string>? progress = null)
{
PathUtils.EnsureDirectory(extractRoot);
var targetDir = Path.Combine(extractRoot, Path.GetFileNameWithoutExtension(zipPath));
if (Directory.Exists(targetDir))
Directory.Delete(targetDir, recursive: true);
progress?.Report($"Extracting {Path.GetFileName(zipPath)}...");
ZipFile.ExtractToDirectory(zipPath, targetDir, overwriteFiles: true);
return ResolveStagingPath(targetDir);
}
public static string ResolveStagingPath(string extractRoot)
{
foreach (var variant in new[] { OmniNxVariant.Standard, OmniNxVariant.Light, OmniNxVariant.Oc })
{
var candidate = Path.Combine(extractRoot, VariantHelper.GetStagingFolderName(variant));
if (Directory.Exists(candidate))
return candidate;
}
if (Directory.Exists(Path.Combine(extractRoot, "atmosphere")) ||
Directory.Exists(Path.Combine(extractRoot, "bootloader")))
return extractRoot;
var subdirs = Directory.GetDirectories(extractRoot);
if (subdirs.Length == 1)
return ResolveStagingPath(subdirs[0]);
throw new InvalidOperationException(
"Could not locate OmniNX pack root in extracted archive. Expected 'OmniNX Light', 'OmniNX Standard', or 'OmniNX OC' folder.");
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="OmniNX.DesktopInstaller"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
+116 -29
View File
@@ -24,17 +24,9 @@ int backup_user_data(void) {
return res; return res;
} }
// Backup DBI if it exists // Backup DBI settings file only (not .nro installers)
if (path_exists("sd:/switch/DBI")) { if (path_exists(DBI_CONFIG_PATH)) {
res = folder_copy("sd:/switch/DBI", TEMP_BACKUP_PATH); res = file_copy(DBI_CONFIG_PATH, TEMP_BACKUP_DBI_CONFIG);
if (res != FR_OK) {
return res;
}
}
// Backup Tinfoil if it exists
if (path_exists("sd:/switch/tinfoil")) {
res = folder_copy("sd:/switch/tinfoil", TEMP_BACKUP_PATH);
if (res != FR_OK) { if (res != FR_OK) {
return res; return res;
} }
@@ -48,6 +40,43 @@ int backup_user_data(void) {
} }
} }
// Backup CyberFoil (nro, config, remotes) if installed
if (path_exists(CYBERFOIL_DIR)) {
res = f_mkdir(TEMP_BACKUP_CYBERFOIL);
if (res != FR_OK && res != FR_EXIST) {
return res;
}
if (path_exists(CYBERFOIL_NRO)) {
res = file_copy(CYBERFOIL_NRO, TEMP_BACKUP_CYBERFOIL_NRO);
if (res != FR_OK) {
return res;
}
}
if (path_exists(CYBERFOIL_CONFIG)) {
res = file_copy(CYBERFOIL_CONFIG, TEMP_BACKUP_CYBERFOIL_CONFIG);
if (res != FR_OK) {
return res;
}
}
if (path_exists(CYBERFOIL_REMOTES)) {
res = folder_copy(CYBERFOIL_REMOTES, TEMP_BACKUP_CYBERFOIL);
if (res != FR_OK) {
return res;
}
}
}
// Backup Checkpoint (saves and app data) if installed
if (path_exists(CHECKPOINT_DIR)) {
res = folder_copy(CHECKPOINT_DIR, TEMP_BACKUP_PATH);
if (res != FR_OK) {
return res;
}
}
return FR_OK; return FR_OK;
} }
@@ -61,25 +90,15 @@ int restore_user_data(void) {
return res; return res;
} }
// Restore DBI if backup exists // Restore DBI settings if backed up
if (path_exists("sd:/temp_backup/DBI")) { if (path_exists(TEMP_BACKUP_DBI_CONFIG)) {
res = folder_copy("sd:/temp_backup/DBI", "sd:/switch"); res = f_mkdir("sd:/switch/DBI");
if (res == FR_OK) { if (res != FR_OK && res != FR_EXIST) {
// Delete old DBI .nro files return res;
f_unlink("sd:/switch/DBI/DBI_810_EN.nro");
f_unlink("sd:/switch/DBI/DBI_810_DE.nro");
f_unlink("sd:/switch/DBI/DBI_845_EN.nro");
f_unlink("sd:/switch/DBI/DBI_845_DE.nro");
f_unlink("sd:/switch/DBI/DBI.nro");
} }
} res = file_copy(TEMP_BACKUP_DBI_CONFIG, DBI_CONFIG_PATH);
if (res != FR_OK) {
// Restore Tinfoil if backup exists return res;
if (path_exists("sd:/temp_backup/tinfoil")) {
res = folder_copy("sd:/temp_backup/tinfoil", "sd:/switch");
if (res == FR_OK) {
// Delete old tinfoil.nro
f_unlink("sd:/switch/tinfoil/tinfoil.nro");
} }
} }
@@ -91,6 +110,43 @@ int restore_user_data(void) {
} }
} }
// Restore CyberFoil if backed up
if (path_exists(TEMP_BACKUP_CYBERFOIL)) {
res = f_mkdir(CYBERFOIL_DIR);
if (res != FR_OK && res != FR_EXIST) {
return res;
}
if (path_exists(TEMP_BACKUP_CYBERFOIL_NRO)) {
res = file_copy(TEMP_BACKUP_CYBERFOIL_NRO, CYBERFOIL_NRO);
if (res != FR_OK) {
return res;
}
}
if (path_exists(TEMP_BACKUP_CYBERFOIL_CONFIG)) {
res = file_copy(TEMP_BACKUP_CYBERFOIL_CONFIG, CYBERFOIL_CONFIG);
if (res != FR_OK) {
return res;
}
}
if (path_exists(TEMP_BACKUP_CYBERFOIL_REMOTES)) {
res = folder_copy(TEMP_BACKUP_CYBERFOIL_REMOTES, CYBERFOIL_DIR);
if (res != FR_OK) {
return res;
}
}
}
// Restore Checkpoint if backed up
if (path_exists(TEMP_BACKUP_CHECKPOINT)) {
res = folder_copy(TEMP_BACKUP_CHECKPOINT, "sd:/switch");
if (res != FR_OK) {
return res;
}
}
return FR_OK; return FR_OK;
} }
@@ -101,3 +157,34 @@ int cleanup_backup(void) {
} }
return FR_OK; return FR_OK;
} }
int backup_horizon_oc_config_for_oc_update(void) {
if (!path_exists(HORIZON_OC_CONFIG_PATH))
return FR_OK;
int res = f_mkdir(HORIZON_OC_UPDATE_BACKUP_DIR);
if (res != FR_OK && res != FR_EXIST)
return res;
return file_copy(HORIZON_OC_CONFIG_PATH, HORIZON_OC_UPDATE_BACKUP_INI);
}
int restore_horizon_oc_config_after_oc_update(void) {
if (!path_exists(HORIZON_OC_UPDATE_BACKUP_INI))
return FR_OK;
int res = f_mkdir("sd:/config");
if (res != FR_OK && res != FR_EXIST)
return res;
res = f_mkdir("sd:/config/horizon-oc");
if (res != FR_OK && res != FR_EXIST)
return res;
return file_copy(HORIZON_OC_UPDATE_BACKUP_INI, HORIZON_OC_CONFIG_PATH);
}
int cleanup_horizon_oc_update_backup(void) {
if (!path_exists(HORIZON_OC_UPDATE_BACKUP_DIR))
return FR_OK;
return folder_delete(HORIZON_OC_UPDATE_BACKUP_DIR);
}
+31 -1
View File
@@ -7,8 +7,31 @@
#include <utils/types.h> #include <utils/types.h>
#define TEMP_BACKUP_PATH "sd:/temp_backup" #define TEMP_BACKUP_PATH "sd:/temp_backup"
/** DBI settings; preserved across clean install (not whole DBI folder). */
#define DBI_CONFIG_PATH "sd:/switch/DBI/dbi.config"
#define TEMP_BACKUP_DBI_CONFIG TEMP_BACKUP_PATH "/dbi.config"
// Backup user data (DBI, Tinfoil, prod.keys) before clean install /** CyberFoil homebrew; preserved across clean install (nro, config, remotes only). */
#define CYBERFOIL_DIR "sd:/switch/CyberFoil"
#define CYBERFOIL_NRO CYBERFOIL_DIR "/cyberfoil.nro"
#define CYBERFOIL_CONFIG CYBERFOIL_DIR "/config.json"
#define CYBERFOIL_REMOTES CYBERFOIL_DIR "/remotes"
#define TEMP_BACKUP_CYBERFOIL TEMP_BACKUP_PATH "/CyberFoil"
#define TEMP_BACKUP_CYBERFOIL_NRO TEMP_BACKUP_CYBERFOIL "/cyberfoil.nro"
#define TEMP_BACKUP_CYBERFOIL_CONFIG TEMP_BACKUP_CYBERFOIL "/config.json"
#define TEMP_BACKUP_CYBERFOIL_REMOTES TEMP_BACKUP_CYBERFOIL "/remotes"
/** Checkpoint save manager; preserved across clean install (full folder). */
#define CHECKPOINT_DIR "sd:/switch/Checkpoint"
#define TEMP_BACKUP_CHECKPOINT TEMP_BACKUP_PATH "/Checkpoint"
/** Live HorizonOC settings (Overclock tool). */
#define HORIZON_OC_CONFIG_PATH "sd:/config/horizon-oc/config.ini"
/** Scratch dir during OC-variant update only; removed after successful restore. */
#define HORIZON_OC_UPDATE_BACKUP_DIR "sd:/.omninx_oc_update"
#define HORIZON_OC_UPDATE_BACKUP_INI HORIZON_OC_UPDATE_BACKUP_DIR "/config.ini"
// Backup user data (CyberFoil, Checkpoint, DBI dbi.config, prod.keys) before clean install
int backup_user_data(void); int backup_user_data(void);
// Restore user data after clean install // Restore user data after clean install
@@ -16,3 +39,10 @@ int restore_user_data(void);
// Clean up temporary backup directory // Clean up temporary backup directory
int cleanup_backup(void); int cleanup_backup(void);
// HorizonOC: backup config.ini before OC pack update (update mode only); no-op if missing
int backup_horizon_oc_config_for_oc_update(void);
// Restore after file copy; no-op if no backup from this run
int restore_horizon_oc_config_after_oc_update(void);
// Delete sd:/.omninx_oc_update after successful restore
int cleanup_horizon_oc_update_backup(void);
+19 -181
View File
@@ -1,86 +1,23 @@
/* /*
* OmniNX Installer - Deletion Lists for Clean Install Mode * OmniNX Installer - Deletion Lists for Clean Install Mode
* Selective deletion when no OmniNX install was found. * Deletion policy aligned with NiklasCFW pack clean install (TegraExplorer script):
* Only listed paths are removed; does not wipe whole card or user configs. * full sd:/atmosphere, bootloader/config subsets, sd:/switch, root + misc, then
* post-restore paths (e.g. tinfoil db). CyberFoil/Checkpoint/DBI/prod.keys backup is in backup.c.
*/ */
#pragma once #pragma once
// Atmosphere subdirectories to delete // Entire atmosphere/ tree (NiklasCFW: deldir("sd:/atmosphere"))
static const char* clean_atmosphere_dirs_to_delete[] = { static const char* clean_atmosphere_dirs_to_delete[] = {
"sd:/atmosphere/config", "sd:/atmosphere",
"sd:/atmosphere/crash_reports",
"sd:/atmosphere/erpt_reports",
"sd:/atmosphere/exefs_patches/CrunchPatch",
"sd:/atmosphere/exefs_patches/Crunchyroll Patch 1.10.0",
"sd:/atmosphere/exefs_patches/bluetooth_patches",
"sd:/atmosphere/exefs_patches/bootlogo",
"sd:/atmosphere/exefs_patches/btm_patches",
"sd:/atmosphere/exefs_patches/es_patches",
"sd:/atmosphere/exefs_patches/hid_patches",
"sd:/atmosphere/exefs_patches/logo1",
"sd:/atmosphere/exefs_patches/nfim_ctest",
"sd:/atmosphere/exefs_patches/nim_ctest",
"sd:/atmosphere/exefs_patches/nvnflinger_cmu",
"sd:/atmosphere/extrazz",
"sd:/atmosphere/fatal_errors",
"sd:/atmosphere/fatal_reports",
"sd:/atmosphere/flags",
"sd:/atmosphere/hbl_html",
"sd:/atmosphere/hosts",
"sd:/atmosphere/kips",
"sd:/atmosphere/kip1",
"sd:/atmosphere/kip_patches",
"sd:/atmosphere/logs",
NULL NULL
}; };
// Atmosphere contents directories (title IDs; only under atmosphere/contents/)
static const char* clean_atmosphere_contents_dirs_to_delete[] = { static const char* clean_atmosphere_contents_dirs_to_delete[] = {
"sd:/atmosphere/contents/0000000000534C56",
"sd:/atmosphere/contents/00FF0000B378D640",
"sd:/atmosphere/contents/00FF0000636C6BFF",
"sd:/atmosphere/contents/00FF0000A53BB665",
"sd:/atmosphere/contents/0100000000000008",
"sd:/atmosphere/contents/010000000000000D",
"sd:/atmosphere/contents/010000000000002B",
"sd:/atmosphere/contents/0100000000000032",
"sd:/atmosphere/contents/0100000000000034",
"sd:/atmosphere/contents/0100000000000036",
"sd:/atmosphere/contents/0100000000000037",
"sd:/atmosphere/contents/010000000000003C",
"sd:/atmosphere/contents/0100000000000042",
"sd:/atmosphere/contents/0100000000000895",
"sd:/atmosphere/contents/0100000000000F12",
"sd:/atmosphere/contents/0100000000001000",
"sd:/atmosphere/contents/0100000000001007",
"sd:/atmosphere/contents/0100000000001013",
"sd:/atmosphere/contents/010000000000DA7A",
"sd:/atmosphere/contents/010000000000bd00",
"sd:/atmosphere/contents/01006a800016e000",
"sd:/atmosphere/contents/01009D901BC56000",
"sd:/atmosphere/contents/0100A3900C3E2000",
"sd:/atmosphere/contents/0100F43008C44000",
"sd:/atmosphere/contents/050000BADDAD0000",
"sd:/atmosphere/contents/4200000000000000",
"sd:/atmosphere/contents/420000000000000B",
"sd:/atmosphere/contents/420000000000000E",
"sd:/atmosphere/contents/4200000000000010",
"sd:/atmosphere/contents/4200000000000FFF",
"sd:/atmosphere/contents/420000000007E51A",
"sd:/atmosphere/contents/420000000007E51B",
"sd:/atmosphere/contents/690000000000000D",
NULL NULL
}; };
// Atmosphere files to delete
static const char* clean_atmosphere_files_to_delete[] = { static const char* clean_atmosphere_files_to_delete[] = {
"sd:/atmosphere/config/exosphere.ini",
"sd:/atmosphere/config/stratosphere.ini",
"sd:/atmosphere/hbl.nsp",
"sd:/atmosphere/package3",
"sd:/atmosphere/reboot_payload.bin",
"sd:/atmosphere/stratosphere.romfs",
NULL NULL
}; };
@@ -101,7 +38,6 @@ static const char* clean_bootloader_files_to_delete[] = {
"sd:/bootloader/ArgonNX.bin", "sd:/bootloader/ArgonNX.bin",
"sd:/bootloader/bootlogo.bmp", "sd:/bootloader/bootlogo.bmp",
"sd:/bootloader/hekate_ipl.ini", "sd:/bootloader/hekate_ipl.ini",
"sd:/bootloader/nyx.ini",
"sd:/bootloader/patches.ini", "sd:/bootloader/patches.ini",
"sd:/bootloader/update.bin", "sd:/bootloader/update.bin",
"sd:/bootloader/ini/EmuMMC ohne Mods.ini", "sd:/bootloader/ini/EmuMMC ohne Mods.ini",
@@ -114,7 +50,6 @@ static const char* clean_config_dirs_to_delete[] = {
"sd:/config/blue_pack_updater", "sd:/config/blue_pack_updater",
"sd:/config/kefir-updater", "sd:/config/kefir-updater",
"sd:/config/nx-hbmenu", "sd:/config/nx-hbmenu",
"sd:/config/quickntp",
"sd:/config/sys-con", "sd:/config/sys-con",
"sd:/config/sys-patch", "sd:/config/sys-patch",
"sd:/config/uberhand", "sd:/config/uberhand",
@@ -122,118 +57,13 @@ static const char* clean_config_dirs_to_delete[] = {
NULL NULL
}; };
// Switch directories to delete // Entire switch/ tree (clean install recreates sd:/switch after wipe)
static const char* clean_switch_dirs_to_delete[] = { static const char* clean_switch_dirs_to_delete[] = {
"sd:/switch/.overlays", "sd:/switch",
"sd:/switch/.packages",
"sd:/switch/90DNS_tester",
"sd:/switch/aio-switch-updater",
"sd:/switch/amsPLUS-downloader",
"sd:/switch/appstore",
"sd:/switch/AtmoXL-Titel-Installer",
"sd:/switch/breeze",
"sd:/switch/checkpoint",
"sd:/switch/cheats-updater",
"sd:/switch/chiaki",
"sd:/switch/ChoiDujourNX",
"sd:/switch/crash_ams",
"sd:/switch/Daybreak",
"sd:/switch/DNS_mitm Tester",
"sd:/switch/EdiZon",
"sd:/switch/Fizeau",
"sd:/switch/FTPD",
"sd:/switch/fw-downloader",
"sd:/switch/gamecard_installer",
"sd:/switch/Goldleaf",
"sd:/switch/haze",
"sd:/switch/JKSV",
"sd:/switch/kefir-updater",
"sd:/switch/ldnmitm_config",
"sd:/switch/Linkalho",
"sd:/switch/Moonlight-Switch",
"sd:/switch/Neumann",
"sd:/switch/NX-Activity-Log",
"sd:/switch/NX-Save-Sync",
"sd:/switch/NX-Shell",
"sd:/switch/NX-Update-Checker ",
"sd:/switch/NXGallery",
"sd:/switch/NXRemoteLauncher",
"sd:/switch/NXThemesInstaller",
"sd:/switch/nxdumptool",
"sd:/switch/nxmtp",
"sd:/switch/Payload_launcher",
"sd:/switch/Reboot",
"sd:/switch/reboot_to_argonNX",
"sd:/switch/reboot_to_hekate",
"sd:/switch/Shutdown_System",
"sd:/switch/SimpleModDownloader",
"sd:/switch/SimpleModManager",
"sd:/switch/sphaira",
"sd:/switch/studious-pancake",
"sd:/switch/Switch-Time",
"sd:/switch/SwitchIdent",
"sd:/switch/Switch_themes_Installer",
"sd:/switch/Switchfin",
"sd:/switch/Sys-Clk Manager",
"sd:/switch/Sys-Con",
"sd:/switch/sys-clk-manager",
"sd:/switch/themezer-nx",
"sd:/switch/themezernx",
"sd:/switch/tinwoo",
NULL NULL
}; };
// Switch files (NRO) to delete
static const char* clean_switch_files_to_delete[] = { static const char* clean_switch_files_to_delete[] = {
"sd:/switch/90DNS_tester/90DNS_tester.nro",
"sd:/switch/breeze.nro",
"sd:/switch/cheats-updater.nro",
"sd:/switch/chiaki.nro",
"sd:/switch/ChoiDujourNX.nro",
"sd:/switch/daybreak.nro",
"sd:/switch/DBI.nro",
"sd:/switch/DBI/DBI.nro",
"sd:/switch/DBI/DBI_810_DE.nro",
"sd:/switch/DBI/DBI_810_EN.nro",
"sd:/switch/DBI/DBI_845_DE.nro",
"sd:/switch/DBI/DBI_845_EN.nro",
"sd:/switch/DBI/DBI_849_DE.nro",
"sd:/switch/DBI/DBI_849_EN.nro",
"sd:/switch/DBI_810_DE/DBI_810.nro",
"sd:/switch/DBI_810_DE/DBI_810_DE.nro",
"sd:/switch/DBI_810_EN/DBI_810_EN.nro",
"sd:/switch/DBI_RU/DBI_RU.nro",
"sd:/switch/DBI/DBI_EN.nro",
"sd:/switch/DBI_DE/DBI_DE.nro",
"sd:/switch/DNS_mitm Tester.nro",
"sd:/switch/EdiZon.nro",
"sd:/switch/Fizeau.nro",
"sd:/switch/Goldleaf.nro",
"sd:/switch/haze.nro",
"sd:/switch/JKSV.nro",
"sd:/switch/ldnmitm_config.nro",
"sd:/switch/linkalho.nro",
"sd:/switch/Moonlight-Switch.nro",
"sd:/switch/Neumann.nro",
"sd:/switch/NX-Shell.nro",
"sd:/switch/NXGallery.nro",
"sd:/switch/NXThemesInstaller.nro",
"sd:/switch/nxdumptool.nro",
"sd:/switch/nxtc.bin",
"sd:/switch/reboot_to_payload.nro",
"sd:/switch/SimpleModDownloader.nro",
"sd:/switch/SimpleModManager.nro",
"sd:/switch/sphaira.nro",
"sd:/switch/SwitchIdent.nro",
"sd:/switch/Switch_themes_Installer/NXThemesInstaller.nro",
"sd:/switch/Switchfin.nro",
"sd:/switch/Sys-Clk Manager/sys-clk-manager.nro",
"sd:/switch/Sys-Con.nro",
"sd:/switch/sys-clk-manager.nro",
"sd:/switch/tinfoil.nro",
"sd:/switch/tinfoil/tinfoil.nro",
"sd:/switch/tinwoo.nro",
"sd:/switch/tinwoo/tinwoo.nro",
NULL NULL
}; };
@@ -261,8 +91,10 @@ static const char* clean_misc_dirs_to_delete[] = {
"sd:/NSPs (Tools)", "sd:/NSPs (Tools)",
"sd:/Patched Apps", "sd:/Patched Apps",
"sd:/SaltySD/flags", "sd:/SaltySD/flags",
"sd:/SaltySD/patches",
"sd:/scripts", "sd:/scripts",
"sd:/switch/tinfoil/db", "sd:/TegraExplorer",
"sd:/themes/systemData",
"sd:/tools", "sd:/tools",
"sd:/warmboot_mariko", "sd:/warmboot_mariko",
NULL NULL
@@ -281,15 +113,21 @@ static const char* clean_misc_files_to_delete[] = {
NULL NULL
}; };
// After DBI/prod.keys restore (NiklasCFW: deldir("sd:/switch/tinfoil/db"))
static const char* clean_post_restore_dirs_to_delete[] = {
"sd:/switch/tinfoil/db",
NULL
};
// Old version marker files to delete (clean install only) // Old version marker files to delete (clean install only)
static const char* old_version_files_to_delete[] = { static const char* old_version_files_to_delete[] = {
"sd:/1.0.0l",
"sd:/1.0.0s",
"sd:/1.0.0oc",
"sd:/1.4.0-pre", "sd:/1.4.0-pre",
"sd:/1.4.0-pre-c", "sd:/1.4.0-pre-c",
"sd:/1.4.0-pre-d", "sd:/1.4.0-pre-d",
"sd:/1.4.1", "sd:/1.4.1",
"sd:/1.5.0", "sd:/1.5.0",
"sd:/1.6.0",
"sd:/1.6.1",
"sd:/1.6.2",
NULL NULL
}; };
+21 -5
View File
@@ -10,14 +10,16 @@ static const char* atmosphere_dirs_to_delete[] = {
"sd:/atmosphere/config", "sd:/atmosphere/config",
"sd:/atmosphere/crash_reports", "sd:/atmosphere/crash_reports",
"sd:/atmosphere/erpt_reports", "sd:/atmosphere/erpt_reports",
"sd:/atmosphere/exefs_patches/audio_mastervolume",
"sd:/atmosphere/exefs_patches/CrunchPatch", "sd:/atmosphere/exefs_patches/CrunchPatch",
"sd:/atmosphere/exefs_patches/SaltyNX_Fixes",
"sd:/atmosphere/exefs_patches/Crunchyroll Patch 1.10.0", "sd:/atmosphere/exefs_patches/Crunchyroll Patch 1.10.0",
"sd:/atmosphere/exefs_patches/bluetooth_patches", "sd:/atmosphere/exefs_patches/bluetooth_patches",
"sd:/atmosphere/exefs_patches/bootlogo", "sd:/atmosphere/exefs_patches/bootlogo",
"sd:/atmosphere/exefs_patches/btm_patches", "sd:/atmosphere/exefs_patches/btm_patches",
"sd:/atmosphere/exefs_patches/es_patches", "sd:/atmosphere/exefs_patches/es_patches",
"sd:/atmosphere/exefs_patches/hid_patches", "sd:/atmosphere/exefs_patches/hid_patches",
"sd:/atmosphere/exefs_patches/logo1", "sd:/atmosphere/exefs_patches/logo",
"sd:/atmosphere/exefs_patches/nfim_ctest", "sd:/atmosphere/exefs_patches/nfim_ctest",
"sd:/atmosphere/exefs_patches/nim_ctest", "sd:/atmosphere/exefs_patches/nim_ctest",
"sd:/atmosphere/exefs_patches/nvnflinger_cmu", "sd:/atmosphere/exefs_patches/nvnflinger_cmu",
@@ -80,6 +82,8 @@ static const char* atmosphere_files_to_delete[] = {
"sd:/atmosphere/package3", "sd:/atmosphere/package3",
"sd:/atmosphere/reboot_payload.bin", "sd:/atmosphere/reboot_payload.bin",
"sd:/atmosphere/stratosphere.romfs", "sd:/atmosphere/stratosphere.romfs",
"sd:/atmosphere/kips/loader.kip",
"sd:/atmosphere/kips/hoc.kip",
NULL NULL
}; };
@@ -122,25 +126,27 @@ static const char* config_dirs_to_delete[] = {
}; };
// Switch directories to delete // Switch directories to delete
// NOTE: .packages is intentionally excluded - UltraHand package cache, preserve during updates
static const char* switch_dirs_to_delete[] = { static const char* switch_dirs_to_delete[] = {
"sd:/switch/.packages",
"sd:/switch/.overlays", "sd:/switch/.overlays",
"sd:/switch/90DNS_tester", "sd:/switch/90DNS_tester",
"sd:/switch/aio-switch-updater", "sd:/switch/aio-switch-updater",
"sd:/switch/amsPLUS-downloader", "sd:/switch/amsPLUS-downloader",
"sd:/switch/appstore", "sd:/switch/appstore",
"sd:/switch/AtmoXL-Titel-Installer", "sd:/switch/AtmoXL-Titel-Installer",
"sd:/switch/Benchmark-Toolbox",
"sd:/switch/breeze", "sd:/switch/breeze",
"sd:/switch/checkpoint",
"sd:/switch/cheats-updater", "sd:/switch/cheats-updater",
"sd:/switch/chiaki", "sd:/switch/chiaki",
"sd:/switch/ChoiDujourNX", "sd:/switch/ChoiDujourNX",
"sd:/switch/crash_ams", "sd:/switch/crash_ams",
"sd:/switch/Daybreak", "sd:/switch/Daybreak",
"sd:/switch/DNS-Block_Tester",
"sd:/switch/DNS_mitm Tester", "sd:/switch/DNS_mitm Tester",
"sd:/switch/EdiZon", "sd:/switch/EdiZon",
"sd:/switch/Fizeau", "sd:/switch/Fizeau",
"sd:/switch/FTPD", "sd:/switch/FTPD",
"sd:/switch/Furmark-NX",
"sd:/switch/fw-downloader", "sd:/switch/fw-downloader",
"sd:/switch/gamecard_installer", "sd:/switch/gamecard_installer",
"sd:/switch/Goldleaf", "sd:/switch/Goldleaf",
@@ -165,6 +171,7 @@ static const char* switch_dirs_to_delete[] = {
"sd:/switch/reboot_to_argonNX", "sd:/switch/reboot_to_argonNX",
"sd:/switch/reboot_to_hekate", "sd:/switch/reboot_to_hekate",
"sd:/switch/Shutdown_System", "sd:/switch/Shutdown_System",
"sd:/switch/SimpleModAlchemist",
"sd:/switch/SimpleModDownloader", "sd:/switch/SimpleModDownloader",
"sd:/switch/SimpleModManager", "sd:/switch/SimpleModManager",
"sd:/switch/sphaira", "sd:/switch/sphaira",
@@ -172,19 +179,22 @@ static const char* switch_dirs_to_delete[] = {
"sd:/switch/Switch-Time", "sd:/switch/Switch-Time",
"sd:/switch/SwitchIdent", "sd:/switch/SwitchIdent",
"sd:/switch/Switch_themes_Installer", "sd:/switch/Switch_themes_Installer",
"sd:/switch/Switchfin", "sd:/switch/swr-ini-tool",
"sd:/switch/Sys-Clk Manager", "sd:/switch/Sys-Clk Manager",
"sd:/switch/Sys-Con", "sd:/switch/Sys-Con",
"sd:/switch/sys-clk-manager", "sd:/switch/sys-clk-manager",
"sd:/switch/themezer-nx", "sd:/switch/themezer-nx",
"sd:/switch/ThemezerNX",
"sd:/switch/themezernx", "sd:/switch/themezernx",
"sd:/switch/tinwoo", "sd:/switch/tinwoo",
"sd:/switch/Ultrahand-Reload",
NULL NULL
}; };
// Switch files (NRO) to delete // Switch files (NRO) to delete
static const char* switch_files_to_delete[] = { static const char* switch_files_to_delete[] = {
"sd:/switch/90DNS_tester/90DNS_tester.nro", "sd:/switch/90DNS_tester/90DNS_tester.nro",
"sd:/switch/Benchmark-Toolbox/Benchmark-Toolbox.nro",
"sd:/switch/breeze.nro", "sd:/switch/breeze.nro",
"sd:/switch/cheats-updater.nro", "sd:/switch/cheats-updater.nro",
"sd:/switch/chiaki.nro", "sd:/switch/chiaki.nro",
@@ -204,9 +214,11 @@ static const char* switch_files_to_delete[] = {
"sd:/switch/DBI_RU/DBI_RU.nro", "sd:/switch/DBI_RU/DBI_RU.nro",
"sd:/switch/DBI/DBI_EN.nro", "sd:/switch/DBI/DBI_EN.nro",
"sd:/switch/DBI_DE/DBI_DE.nro", "sd:/switch/DBI_DE/DBI_DE.nro",
"sd:/switch/DNS-Block_Tester/dns_tester.nro",
"sd:/switch/DNS_mitm Tester.nro", "sd:/switch/DNS_mitm Tester.nro",
"sd:/switch/EdiZon.nro", "sd:/switch/EdiZon.nro",
"sd:/switch/Fizeau.nro", "sd:/switch/Fizeau.nro",
"sd:/switch/Furmark-NX/Furmark-NX.nro",
"sd:/switch/Goldleaf.nro", "sd:/switch/Goldleaf.nro",
"sd:/switch/haze.nro", "sd:/switch/haze.nro",
"sd:/switch/JKSV.nro", "sd:/switch/JKSV.nro",
@@ -220,19 +232,22 @@ static const char* switch_files_to_delete[] = {
"sd:/switch/nxdumptool.nro", "sd:/switch/nxdumptool.nro",
"sd:/switch/nxtc.bin", "sd:/switch/nxtc.bin",
"sd:/switch/reboot_to_payload.nro", "sd:/switch/reboot_to_payload.nro",
"sd:/switch/SimpleModAlchemist/Simple_Mod_Alchemist.nro",
"sd:/switch/SimpleModDownloader.nro", "sd:/switch/SimpleModDownloader.nro",
"sd:/switch/SimpleModManager.nro", "sd:/switch/SimpleModManager.nro",
"sd:/switch/sphaira.nro", "sd:/switch/sphaira.nro",
"sd:/switch/swr-ini-tool/swr-ini-tool.nro",
"sd:/switch/SwitchIdent.nro", "sd:/switch/SwitchIdent.nro",
"sd:/switch/Switch_themes_Installer/NXThemesInstaller.nro", "sd:/switch/Switch_themes_Installer/NXThemesInstaller.nro",
"sd:/switch/Switchfin.nro",
"sd:/switch/Sys-Clk Manager/sys-clk-manager.nro", "sd:/switch/Sys-Clk Manager/sys-clk-manager.nro",
"sd:/switch/Sys-Con.nro", "sd:/switch/Sys-Con.nro",
"sd:/switch/sys-clk-manager.nro", "sd:/switch/sys-clk-manager.nro",
"sd:/switch/ThemezerNX/themezer-nx.nro",
"sd:/switch/tinfoil.nro", "sd:/switch/tinfoil.nro",
"sd:/switch/tinfoil/tinfoil.nro", "sd:/switch/tinfoil/tinfoil.nro",
"sd:/switch/tinwoo.nro", "sd:/switch/tinwoo.nro",
"sd:/switch/tinwoo/tinwoo.nro", "sd:/switch/tinwoo/tinwoo.nro",
"sd:/switch/Ultrahand-Reload/Ultrahand-Reload.nro",
NULL NULL
}; };
@@ -262,6 +277,7 @@ static const char* misc_dirs_to_delete[] = {
"sd:/SaltySD/flags", "sd:/SaltySD/flags",
"sd:/scripts", "sd:/scripts",
"sd:/switch/tinfoil/db", "sd:/switch/tinfoil/db",
"sd:/themes/systemData",
"sd:/tools", "sd:/tools",
"sd:/warmboot_mariko", "sd:/warmboot_mariko",
NULL NULL
+46
View File
@@ -0,0 +1,46 @@
/*
* DRAM capacity from fuse (SKU), not physical probe.
*/
#include "dram_fuse.h"
#include <soc/fuse.h>
#include <soc/hw_init.h>
#include <soc/t210.h>
static int mariko_dram_mib(u32 dram_id)
{
switch (dram_id) {
case 9:
case 13:
case 18:
case 21:
case 23:
case 28:
return 8192;
default:
if (dram_id >= 3 && dram_id <= 28)
return 4096;
return -1;
}
}
static int erista_dram_mib(u32 dram_id)
{
if (dram_id == 4)
return 6144;
if (dram_id <= 6)
return 4096;
return -1;
}
int dram_capacity_mib_from_fuse(void)
{
u32 nid = fuse_read_dramid(false);
u32 chip = hw_get_chip_id();
if (chip == GP_HIDREV_MAJOR_T210)
return erista_dram_mib(nid);
if (chip == GP_HIDREV_MAJOR_T210B01)
return mariko_dram_mib(nid);
return -1;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <utils/types.h>
/* DRAM capacity in MiB from fuse DRAM ID + SoC (same table as RAM test payload). */
int dram_capacity_mib_from_fuse(void);
+1004 -43
View File
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -13,7 +13,10 @@ typedef enum {
INSTALL_MODE_CLEAN // No OmniNX - selective deletion from clean list INSTALL_MODE_CLEAN // No OmniNX - selective deletion from clean list
} install_mode_t; } install_mode_t;
// Main installation function // If a sub-menu aborts to Hekate / update.bin (does not return if launch succeeds)
void installer_launch_hekate_payload(void);
// Main installation function (optional debug: see DEBUG_INI.md in repo root)
int perform_installation(omninx_variant_t pack_variant, install_mode_t mode); int perform_installation(omninx_variant_t pack_variant, install_mode_t mode);
// Update mode operations (install_update.c) // Update mode operations (install_update.c)
@@ -29,12 +32,16 @@ int clean_mode_wipe(void);
int clean_mode_restore(void); int clean_mode_restore(void);
int clean_mode_install(omninx_variant_t variant); int clean_mode_install(omninx_variant_t variant);
void install_preserve_update_cleanup_begin(void);
void install_preserve_update_cleanup_end(void);
// Shared helpers (install.c) - used by install_update.c and install_clean.c // Shared helpers (install.c) - used by install_update.c and install_clean.c
void install_set_color(u32 color); void install_set_color(u32 color);
void install_check_and_clear_screen_if_needed(void); void install_check_and_clear_screen_if_needed(void);
bool install_path_exists(const char *path); bool install_path_exists(const char *path);
int install_count_directory_items(const char *path); int install_count_directory_items(const char *path);
void install_combine_path(char *result, size_t size, const char *base, const char *add); void install_combine_path(char *result, size_t size, const char *base, const char *add);
int install_copy_staging_root_files(const char *staging, const char *dst_root);
int delete_path_list(const char* paths[], const char* description); int delete_path_list(const char* paths[], const char* description);
int delete_path_lists_grouped(const char *folder_display_name, ...); int delete_path_lists_grouped(const char *folder_display_name, ...);
int folder_delete_single_with_progress(const char *path, const char *display_name); int folder_delete_single_with_progress(const char *path, const char *display_name);
+3 -2
View File
@@ -34,7 +34,7 @@
// Clean mode: Backup user data // Clean mode: Backup user data
int clean_mode_backup(void) { int clean_mode_backup(void) {
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf(" Sichere: DBI, Tinfoil, prod.keys\n"); gfx_printf(" Sichere: CyberFoil, Checkpoint, DBI (dbi.config), prod.keys\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
int res = backup_user_data(); int res = backup_user_data();
if (res == FR_OK) { if (res == FR_OK) {
@@ -95,13 +95,14 @@ int clean_mode_wipe(void) {
// Clean mode: Restore user data // Clean mode: Restore user data
int clean_mode_restore(void) { int clean_mode_restore(void) {
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf(" Stelle wieder her: DBI, Tinfoil, prod.keys\n"); gfx_printf(" Stelle wieder her: CyberFoil, Checkpoint, DBI (dbi.config), prod.keys\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
int res = restore_user_data(); int res = restore_user_data();
if (res == FR_OK) { if (res == FR_OK) {
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf(" [OK] Wiederherstellung abgeschlossen\n"); gfx_printf(" [OK] Wiederherstellung abgeschlossen\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
delete_path_list(clean_post_restore_dirs_to_delete, "switch/ (nach Restore)");
} }
cleanup_backup(); cleanup_backup();
return res; return res;
+15 -28
View File
@@ -12,10 +12,6 @@
#include <string.h> #include <string.h>
#include <utils/sprintf.h> #include <utils/sprintf.h>
#ifndef VERSION
#define VERSION "1.0.0"
#endif
#undef COLOR_CYAN #undef COLOR_CYAN
#undef COLOR_WHITE #undef COLOR_WHITE
#undef COLOR_GREEN #undef COLOR_GREEN
@@ -37,6 +33,8 @@
// Update mode: Cleanup specific directories/files // Update mode: Cleanup specific directories/files
int update_mode_cleanup(omninx_variant_t variant) { int update_mode_cleanup(omninx_variant_t variant) {
(void)variant; (void)variant;
install_preserve_update_cleanup_begin();
check_and_clear_screen_if_needed(); check_and_clear_screen_if_needed();
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
@@ -66,6 +64,8 @@ int update_mode_cleanup(omninx_variant_t variant) {
misc_files_to_delete, misc_files_to_delete,
NULL); NULL);
install_preserve_update_cleanup_end();
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf(" Bereinigung abgeschlossen!\n"); gfx_printf(" Bereinigung abgeschlossen!\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
@@ -78,7 +78,6 @@ int update_mode_install(omninx_variant_t variant) {
int res; int res;
const char* staging = get_staging_path(variant); const char* staging = get_staging_path(variant);
char src_path[256]; char src_path[256];
char dst_path[256];
if (!staging) { if (!staging) {
return FR_INVALID_PARAMETER; return FR_INVALID_PARAMETER;
@@ -106,6 +105,10 @@ int update_mode_install(omninx_variant_t variant) {
res = folder_copy_with_progress_v2(src_path, "sd:/", "switch/"); res = folder_copy_with_progress_v2(src_path, "sd:/", "switch/");
if (res != FR_OK && res != FR_NO_FILE) return res; if (res != FR_OK && res != FR_NO_FILE) return res;
s_printf(src_path, "%s/themes", staging);
res = folder_copy_with_progress_v2(src_path, "sd:/", "themes/");
if (res != FR_OK && res != FR_NO_FILE) return res;
s_printf(src_path, "%s/warmboot_mariko", staging); s_printf(src_path, "%s/warmboot_mariko", staging);
res = folder_copy_with_progress_v2(src_path, "sd:/", "warmboot_mariko/"); res = folder_copy_with_progress_v2(src_path, "sd:/", "warmboot_mariko/");
if (res != FR_OK && res != FR_NO_FILE) return res; if (res != FR_OK && res != FR_NO_FILE) return res;
@@ -116,33 +119,17 @@ int update_mode_install(omninx_variant_t variant) {
if (res != FR_OK && res != FR_NO_FILE) return res; if (res != FR_OK && res != FR_NO_FILE) return res;
} }
s_printf(src_path, "%s/extras", staging);
res = folder_copy_with_progress_v2(src_path, "sd:/", "extras/");
if (res != FR_OK && res != FR_NO_FILE) return res;
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf(" Kopiere Root-Dateien...\n"); gfx_printf(" Kopiere Root-Dateien...\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
s_printf(src_path, "%s/boot.dat", staging); res = install_copy_staging_root_files(staging, "sd:/");
s_printf(dst_path, "sd:/boot.dat"); if (res != FR_OK)
if (path_exists(src_path)) file_copy(src_path, dst_path); return res;
s_printf(src_path, "%s/boot.ini", staging);
s_printf(dst_path, "sd:/boot.ini");
if (path_exists(src_path)) file_copy(src_path, dst_path);
s_printf(src_path, "%s/exosphere.ini", staging);
s_printf(dst_path, "sd:/exosphere.ini");
if (path_exists(src_path)) file_copy(src_path, dst_path);
s_printf(src_path, "%s/hbmenu.nro", staging);
s_printf(dst_path, "sd:/hbmenu.nro");
if (path_exists(src_path)) file_copy(src_path, dst_path);
s_printf(src_path, "%s/loader.bin", staging);
s_printf(dst_path, "sd:/loader.bin");
if (path_exists(src_path)) file_copy(src_path, dst_path);
s_printf(src_path, "%s/payload.bin", staging);
s_printf(dst_path, "sd:/payload.bin");
if (path_exists(src_path)) file_copy(src_path, dst_path);
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf(" Kopie abgeschlossen!\n"); gfx_printf(" Kopie abgeschlossen!\n");
+3 -3
View File
@@ -243,9 +243,9 @@
#define FF_FS_NORTC 1 #define FF_FS_NORTC 1
#define FF_NORTC_MON 1 #define FF_NORTC_MON 10
#define FF_NORTC_MDAY 1 #define FF_NORTC_MDAY 10
#define FF_NORTC_YEAR 2020 #define FF_NORTC_YEAR 2026
/* The option FF_FS_NORTC switches timestamp function. If the system does not have /* The option FF_FS_NORTC switches timestamp function. If the system does not have
/ any RTC function or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable / any RTC function or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable
/ the timestamp function. Every object modified by FatFs will have a fixed timestamp / the timestamp function. Every object modified by FatFs will have a fixed timestamp
+190 -43
View File
@@ -6,10 +6,6 @@
* Based on TegraExplorer/hekate by CTCaer, naehrwert, shchmue * Based on TegraExplorer/hekate by CTCaer, naehrwert, shchmue
*/ */
#ifndef VERSION
#define VERSION "1.0.0"
#endif
#include <string.h> #include <string.h>
#include <display/di.h> #include <display/di.h>
@@ -35,6 +31,7 @@
#include "fs.h" #include "fs.h"
#include "version.h" #include "version.h"
#include "install.h" #include "install.h"
#include "screenshot.h"
// Configuration // Configuration
#define PAYLOAD_PATH "sd:/bootloader/update.bin" #define PAYLOAD_PATH "sd:/bootloader/update.bin"
@@ -109,12 +106,18 @@ static void print_header(void) {
gfx_clear_grey(0x1B); gfx_clear_grey(0x1B);
gfx_con_setpos(0, 0); gfx_con_setpos(0, 0);
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("===================================\n"); gfx_printf("====================================\n");
gfx_printf(" OmniNX Installer Payload v%s\n", VERSION); gfx_printf(" OmniNX Installer Payload v%s\n", VERSION);
gfx_printf("===================================\n\n"); gfx_printf("====================================\n\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
} }
/* Console Vol+ and Vol- together (exit / cancel where documented) */
static inline bool cancel_combo_pressed(u8 btn)
{
return (btn & (BTN_VOL_UP | BTN_VOL_DOWN)) == (BTN_VOL_UP | BTN_VOL_DOWN);
}
void reloc_patcher(u32 payload_dst, u32 payload_src, u32 payload_size) { void reloc_patcher(u32 payload_dst, u32 payload_src, u32 payload_size) {
memcpy((u8 *)payload_src, (u8 *)IPL_LOAD_ADDR, PATCHED_RELOC_SZ); memcpy((u8 *)payload_src, (u8 *)IPL_LOAD_ADDR, PATCHED_RELOC_SZ);
@@ -138,7 +141,7 @@ static int launch_payload(const char *path) {
if (sd_mount()) { if (sd_mount()) {
FIL fp; FIL fp;
if (f_open(&fp, path, FA_READ)) { if (f_open(&fp, path, FA_READ)) {
gfx_printf("Payload nicht gefunden: %s\n", path); gfx_printf("Hekate nicht gefunden: %s\n", path);
return 1; return 1;
} }
@@ -182,6 +185,12 @@ static int file_exists(const char *path) {
return (f_stat(path, &fno) == FR_OK); return (f_stat(path, &fno) == FR_OK);
} }
void installer_launch_hekate_payload(void) {
if (file_exists(PAYLOAD_PATH))
launch_payload(PAYLOAD_PATH);
power_set_state(POWER_OFF_REBOOT);
}
extern void pivot_stack(u32 stack_top); extern void pivot_stack(u32 stack_top);
void ipl_main(void) { void ipl_main(void) {
@@ -243,11 +252,13 @@ void ipl_main(void) {
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n");
gfx_printf("um Hekate zu starten...\n"); gfx_printf("um Hekate zu starten...\n");
gfx_printf("Oder Vol+ und Vol- (Konsole) gleichzeitig.\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
} else { } else {
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n");
gfx_printf("um den Neustart zu starten...\n"); gfx_printf("um den Neustart zu starten...\n");
gfx_printf("Oder Vol+ und Vol- (Konsole) gleichzeitig.\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
} }
@@ -264,9 +275,17 @@ void ipl_main(void) {
break; break;
} }
// Check joycon A button // Check joycon A button; Capture = screenshot
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
if (jc && jc->a) { if (jc) {
if (jc->cap)
take_screenshot();
if (jc->a) {
button_pressed = true;
break;
}
}
if (cancel_combo_pressed(btn_state)) {
button_pressed = true; button_pressed = true;
break; break;
} }
@@ -278,7 +297,7 @@ void ipl_main(void) {
if (file_exists(PAYLOAD_PATH)) { if (file_exists(PAYLOAD_PATH)) {
gfx_printf("\n"); gfx_printf("\n");
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("Payload wird gestartet...\n"); gfx_printf("Hekate wird gestartet...\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
msleep(500); msleep(500);
launch_payload(PAYLOAD_PATH); launch_payload(PAYLOAD_PATH);
@@ -324,12 +343,14 @@ void ipl_main(void) {
gfx_printf("\n"); gfx_printf("\n");
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("D-Pad / Vol+/-: Auswahl | A oder Power: Bestaetigen\n"); gfx_printf("D-Pad / Vol+/-: Auswahl | A oder Power: Bestaetigen\n");
gfx_printf("Vol+ und Vol- gleichzeitig: Abbrechen (Hekate)\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
// Edge detection: only move on press (not while held) // Edge detection: only move on press (not while held)
bool prev_up = false, prev_down = false; bool prev_up = false, prev_down = false;
bool menu_aborted = false;
while (!confirmed) { while (!confirmed && !menu_aborted) {
// On selection change: redraw only the two affected lines (no full clear) // On selection change: redraw only the two affected lines (no full clear)
if (selected != prev_selected) { if (selected != prev_selected) {
gfx_con_setpos(menu_x, menu_variant_start_y + (u32)prev_selected * 16); gfx_con_setpos(menu_x, menu_variant_start_y + (u32)prev_selected * 16);
@@ -345,6 +366,14 @@ void ipl_main(void) {
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
u8 btn = btn_read(); u8 btn = btn_read();
if (jc && jc->cap)
take_screenshot();
if (cancel_combo_pressed(btn)) {
menu_aborted = true;
break;
}
// D-pad or Vol+ / Vol- for selection (Vol+ = up, Vol- = down) // D-pad or Vol+ / Vol- for selection (Vol+ = up, Vol- = down)
bool cur_up = false, cur_down = false; bool cur_up = false, cur_down = false;
if (jc) { if (jc) {
@@ -370,6 +399,20 @@ void ipl_main(void) {
msleep(50); msleep(50);
} }
if (menu_aborted) {
gfx_printf("\n");
set_color(COLOR_YELLOW);
gfx_printf("Abgebrochen. Starte Hekate...\n");
set_color(COLOR_WHITE);
msleep(500);
if (file_exists(PAYLOAD_PATH)) {
launch_payload(PAYLOAD_PATH);
} else {
power_set_state(POWER_OFF_REBOOT);
}
return;
}
pack_variant = variants_present[selected]; pack_variant = variants_present[selected];
gfx_clear_grey(0x1B); gfx_clear_grey(0x1B);
print_header(); print_header();
@@ -395,14 +438,19 @@ void ipl_main(void) {
gfx_printf("mit angeschlossenem Ladegeraet durchgefuehrt werden.\n\n"); gfx_printf("mit angeschlossenem Ladegeraet durchgefuehrt werden.\n\n");
set_color(COLOR_YELLOW); set_color(COLOR_YELLOW);
gfx_printf("Stecke das Ladegeraet an und warte...\n"); gfx_printf("Stecke das Ladegeraet an und warte...\n");
gfx_printf("Oder druecke + und - zum Abbrechen.\n"); gfx_printf("Oder Vol+ und Vol- (Konsole) zum Abbrechen.\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
jc_init_hw(); jc_init_hw();
while (btn_read() & BTN_POWER) { msleep(50); } while (btn_read() & BTN_POWER) { msleep(50); }
bool user_cancelled = false; bool user_cancelled = false;
while (batt_pct < BATT_LOW_THRESHOLD && !bq24193_charger_connected() && !user_cancelled) { while (batt_pct < BATT_LOW_THRESHOLD && !bq24193_charger_connected() && !user_cancelled) {
u8 pbtn = btn_read();
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
if (jc && jc->plus && jc->minus) { if (jc) {
if (jc->cap)
take_screenshot();
}
if (cancel_combo_pressed(pbtn)) {
user_cancelled = true; user_cancelled = true;
break; break;
} }
@@ -428,7 +476,90 @@ void ipl_main(void) {
gfx_con_setpos(0, 0); gfx_con_setpos(0, 0);
print_header(); print_header();
// UHS class check: recommend U2 / V30 / A2 for emuMMC (clean install only)
if (mode == INSTALL_MODE_CLEAN) {
#define UHS_U2_MIN 2
#define UHS_V30_MIN 30
#define UHS_A2_MIN 2
bool uhs_ok = (sd_storage.ssr.uhs_grade >= UHS_U2_MIN &&
sd_storage.ssr.video_class >= UHS_V30_MIN &&
sd_storage.ssr.app_class >= UHS_A2_MIN);
if (!uhs_ok) {
jc_init_hw();
gfx_clear_grey(0x1B);
gfx_con_setpos(0, 0);
set_color(COLOR_RED);
gfx_printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
gfx_printf("!! WARNUNG - SD-KARTEN-KLASSE !!\n");
gfx_printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n");
set_color(COLOR_WHITE);
gfx_printf("Diese SD-Karte erfuellt NICHT die empfohlenen\n");
gfx_printf("Mindestanforderungen (U2 | V30 | A2).\n\n");
set_color(COLOR_YELLOW);
gfx_printf("Aktuelle Werte: UHS%d | V%d | A%d\n\n",
(u32)sd_storage.ssr.uhs_grade,
(u32)sd_storage.ssr.video_class,
(u32)sd_storage.ssr.app_class);
set_color(COLOR_WHITE);
gfx_printf("Moegliche Folgen bei schwachen Karten:\n");
gfx_printf(" - Langsamere emuMMC / Spiel-Ladezeiten\n");
gfx_printf(" - Hoheres Risiko von Korruption oder Abstuerzen\n");
gfx_printf(" - Instabilitaet beim Schreiben groesserer Daten\n\n");
set_color(COLOR_CYAN);
gfx_printf("Empfohlen: U2 | V30 | A2 oder besser.\n");
gfx_printf("Empfohlene Karten: Samsung EVO Plus, EVO Select und PRO Plus.\n\n");
set_color(COLOR_GREEN);
gfx_printf("Druecke A (oder Power), um trotzdem fortzufahren.\n");
set_color(COLOR_CYAN);
gfx_printf("Vol+ und Vol- (Konsole) gleichzeitig: Abbrechen -> Hekate.\n");
set_color(COLOR_WHITE);
while (btn_read() & BTN_POWER) { msleep(50); }
bool acknowledged = false;
bool uhs_aborted = false;
while (!acknowledged && !uhs_aborted) {
u8 btn_state = btn_read();
if (btn_state & BTN_POWER) acknowledged = true;
jc_gamepad_rpt_t *jc = joycon_poll();
if (jc) {
if (jc->cap)
take_screenshot();
if (jc->a) acknowledged = true;
}
if (cancel_combo_pressed(btn_state)) {
uhs_aborted = true;
break;
}
msleep(50);
}
if (uhs_aborted) {
gfx_printf("\nAbgebrochen. Starte Hekate...\n");
msleep(500);
if (file_exists(PAYLOAD_PATH)) {
launch_payload(PAYLOAD_PATH);
} else {
power_set_state(POWER_OFF_REBOOT);
}
return;
}
// Wait for A and Power to be released so the same press doesn't start the install
while (btn_read() & BTN_POWER) { msleep(50); }
bool released = false;
while (!released) {
jc_gamepad_rpt_t *jc = joycon_poll();
released = (!jc || !jc->a) && !(btn_read() & BTN_POWER);
if (!released) msleep(50);
}
msleep(300); // Short delay so the next screen is visible before accepting input
gfx_clear_grey(0x1B);
gfx_con_setpos(0, 0);
print_header();
}
}
// Show information // Show information
set_color(COLOR_YELLOW);
gfx_printf("Hinweis: Verbindungen zu Nintendo-Servern sind\n");
gfx_printf("in OmniNX standardmaessig unterdrueckt.\n\n");
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("Installationsmodus: %s\n", mode == INSTALL_MODE_UPDATE ? "Update" : "Saubere Installation"); gfx_printf("Installationsmodus: %s\n", mode == INSTALL_MODE_UPDATE ? "Update" : "Saubere Installation");
gfx_printf("Paket-Variante: %s\n", get_variant_name(pack_variant)); gfx_printf("Paket-Variante: %s\n", get_variant_name(pack_variant));
@@ -437,6 +568,17 @@ void ipl_main(void) {
} else { } else {
gfx_printf("Aktuelle Installation: Keine\n"); gfx_printf("Aktuelle Installation: Keine\n");
} }
char new_version[32];
new_version[0] = '\0';
const char *pack_manifest = get_manifest_path(pack_variant);
if (pack_manifest)
read_manifest_version(pack_manifest, new_version, sizeof(new_version));
const char *cur_ver = !current.is_installed ? "Keine"
: (current.version_file[0] ? current.version_file : "Unbekannt");
const char *new_ver = new_version[0] ? new_version : "Unbekannt";
gfx_printf("Version: %s -> %s\n", cur_ver, new_ver);
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
gfx_printf("\n"); gfx_printf("\n");
@@ -453,7 +595,7 @@ void ipl_main(void) {
gfx_printf("um die Installation zu starten...\n"); gfx_printf("um die Installation zu starten...\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("Druecke + und - gleichzeitig zum Abbrechen (zurueck zu Hekate).\n"); gfx_printf("Vol+ und Vol- (Konsole) gleichzeitig: Abbrechen -> Hekate.\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
// Wait for A/Power to start, or +/- to cancel // Wait for A/Power to start, or +/- to cancel
@@ -473,18 +615,19 @@ void ipl_main(void) {
break; break;
} }
// Check joycon buttons // Check joycon buttons; Capture = screenshot
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
if (jc) { if (jc) {
if (jc->cap)
take_screenshot();
if (jc->a) { if (jc->a) {
button_pressed = true; button_pressed = true;
break; break;
} }
// + and - simultaneously = cancel, return to hekate }
if (jc->plus && jc->minus) { if (cancel_combo_pressed(btn_state)) {
cancelled = true; cancelled = true;
break; break;
}
} }
msleep(50); // Small delay to avoid busy-waiting msleep(50); // Small delay to avoid busy-waiting
@@ -520,32 +663,28 @@ void ipl_main(void) {
int result = perform_installation(pack_variant, mode); int result = perform_installation(pack_variant, mode);
// Wait 3 seconds before clearing screen to allow errors to be visible // Wait 3 seconds before clearing screen to allow errors to be visible
msleep(3000); msleep(1500);
// take_screenshot(); // Take a screenshot of the installation summary
// Clear screen for final summary to ensure it's visible // Clear screen for final summary to ensure it's visible
gfx_clear_grey(0x1B); gfx_clear_grey(0x1B);
gfx_con_setpos(0, 0); gfx_con_setpos(0, 0);
// Summary // Summary
set_color(COLOR_CYAN); const char *op_name = (mode == INSTALL_MODE_UPDATE) ? " Update" : " Installation";
gfx_printf("========================================\n");
gfx_printf(" OmniNX Installer Payload v%s\n", VERSION);
gfx_printf("========================================\n\n");
set_color(COLOR_WHITE);
if (result == FR_OK && total_errors == 0) { if (result == FR_OK && total_errors == 0) {
set_color(COLOR_GREEN); set_color(COLOR_GREEN);
gfx_printf("========================================\n"); gfx_printf("=======================================\n");
gfx_printf(" Installation abgeschlossen!\n"); gfx_printf(" %s abgeschlossen!\n", op_name);
gfx_printf("========================================\n"); gfx_printf("=======================================\n");
} else { } else {
set_color(COLOR_RED); set_color(COLOR_RED);
gfx_printf("========================================\n"); gfx_printf("=======================================\n");
gfx_printf(" Installation beendet\n"); gfx_printf(" %s beendet\n", op_name);
if (total_errors > 0) { if (total_errors > 0) {
gfx_printf(" %d Fehler\n", total_errors); gfx_printf(" %d Fehler\n", total_errors);
} }
gfx_printf("========================================\n"); gfx_printf("=======================================\n");
} }
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
@@ -573,11 +712,15 @@ void ipl_main(void) {
break; break;
} }
// Check joycon A button // Check joycon A button; Capture = screenshot
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
if (jc && jc->a) { if (jc) {
button_pressed = true; if (jc->cap)
break; take_screenshot();
if (jc->a) {
button_pressed = true;
break;
}
} }
msleep(50); // Small delay to avoid busy-waiting msleep(50); // Small delay to avoid busy-waiting
@@ -585,16 +728,16 @@ void ipl_main(void) {
gfx_printf("\n"); gfx_printf("\n");
set_color(COLOR_CYAN); set_color(COLOR_CYAN);
gfx_printf("Payload wird gestartet...\n"); gfx_printf("Hekate wird gestartet...\n");
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
msleep(500); // Brief delay before launch msleep(500); // Brief delay before launch
if (file_exists(PAYLOAD_PATH)) { if (file_exists(PAYLOAD_PATH)) {
launch_payload(PAYLOAD_PATH); launch_payload(PAYLOAD_PATH);
} else { } else {
// Payload not found, show error // Hekate (update.bin) not found, show error
set_color(COLOR_RED); set_color(COLOR_RED);
gfx_printf("\nFEHLER: Payload nicht gefunden!\n"); gfx_printf("\nFEHLER: Hekate nicht gefunden!\n");
gfx_printf("Pfad: %s\n", PAYLOAD_PATH); gfx_printf("Pfad: %s\n", PAYLOAD_PATH);
set_color(COLOR_WHITE); set_color(COLOR_WHITE);
gfx_printf("\nDruecke A oder Power zum Neustart...\n"); gfx_printf("\nDruecke A oder Power zum Neustart...\n");
@@ -615,9 +758,13 @@ void ipl_main(void) {
} }
jc_gamepad_rpt_t *jc = joycon_poll(); jc_gamepad_rpt_t *jc = joycon_poll();
if (jc && jc->a) { if (jc) {
button_pressed = true; if (jc->cap)
break; take_screenshot();
if (jc->a) {
button_pressed = true;
break;
}
} }
msleep(50); msleep(50);
+5
View File
@@ -184,6 +184,11 @@ static void _sd_deinit()
void sd_unmount() { _sd_deinit(); } void sd_unmount() { _sd_deinit(); }
void sd_end() { _sd_deinit(); } void sd_end() { _sd_deinit(); }
bool sd_get_card_mounted(void)
{
return sd_mounted;
}
void *sd_file_read(const char *path, u32 *fsize) void *sd_file_read(const char *path, u32 *fsize)
{ {
FIL fp; FIL fp;
+111
View File
@@ -0,0 +1,111 @@
/*
* Screenshot support for OmniNX Installer Payload
* Based on TegraExplorer (AllgemeinerProblemLoeser) TakeScreenshot implementation.
*/
#include "screenshot.h"
#include "gfx.h"
#include "nx_sd.h"
#include <libs/fatfs/ff.h>
#include <mem/heap.h>
#include <display/di.h>
#include <utils/util.h>
#include <utils/sprintf.h>
#include <rtc/max77620-rtc.h>
#include <string.h>
/* BMP file header (54 bytes), matching TegraExplorer tools.h */
typedef struct __attribute__((packed)) _bmp_hdr_t
{
u16 magic;
u32 size;
u32 rsvd;
u32 data_off;
u32 hdr_size;
u32 width;
u32 height;
u16 planes;
u16 pxl_bits;
u32 comp;
u32 img_size;
u32 res_h;
u32 res_v;
u64 rsvd2;
} bmp_hdr_t;
#define BMP_HEADER_SIZE 0x36
#define FB_SIZE 0x384000 /* 1280 * 720 * 4 */
#define SCREEN_W 1280
#define SCREEN_H 720
void take_screenshot(void)
{
static u32 last_timer = 0;
if (!sd_get_card_mounted())
return;
/* 3-second cooldown (same as TegraExplorer) */
u32 now = get_tmr_s();
if (last_timer != 0 && now < last_timer + 3)
return;
last_timer = now;
const char basepath[] = "sd:/switch/screenshot";
char name[48];
char path[80];
rtc_time_t rtc;
max77620_rtc_get_time(&rtc);
s_printf(name, "omninx_installer_%04d%02d%02d_%02d%02d%02d.bmp",
(u32)rtc.year, (u32)rtc.month, (u32)rtc.day,
(u32)rtc.hour, (u32)rtc.min, (u32)rtc.sec);
s_printf(path, "%s/%s", basepath, name);
f_mkdir("sd:/switch");
f_mkdir(basepath);
const u32 file_size = BMP_HEADER_SIZE + FB_SIZE;
u8 *bitmap = (u8 *)malloc(file_size);
u32 *fb_copy = (u32 *)malloc(FB_SIZE);
if (!bitmap || !fb_copy) {
if (bitmap) free(bitmap);
if (fb_copy) free(fb_copy);
return;
}
/* Copy framebuffer with same coordinate flip as TegraExplorer (BGR layout) */
u32 *fb_ptr = gfx_ctxt.fb;
for (int x = SCREEN_W - 1; x >= 0; x--) {
for (int y = SCREEN_H - 1; y >= 0; y--)
fb_copy[y * SCREEN_W + x] = *fb_ptr++;
}
memcpy(bitmap + BMP_HEADER_SIZE, fb_copy, FB_SIZE);
bmp_hdr_t *bmp = (bmp_hdr_t *)bitmap;
bmp->magic = 0x4D42;
bmp->size = file_size;
bmp->rsvd = 0;
bmp->data_off = BMP_HEADER_SIZE;
bmp->hdr_size = 40;
bmp->width = SCREEN_W;
bmp->height = SCREEN_H;
bmp->planes = 1;
bmp->pxl_bits = 32;
bmp->comp = 0;
bmp->img_size = FB_SIZE;
bmp->res_h = 2834;
bmp->res_v = 2834;
bmp->rsvd2 = 0;
sd_save_to_file(bitmap, file_size, path);
free(bitmap);
free(fb_copy);
/* Brief backlight flash (same as TegraExplorer) */
display_backlight_brightness(255, 1000);
msleep(100);
display_backlight_brightness(100, 1000);
}
+13
View File
@@ -0,0 +1,13 @@
/*
* Screenshot support for OmniNX Installer Payload
* Based on TegraExplorer (AllgemeinerProblemLoeser) TakeScreenshot implementation.
*/
#pragma once
#include <utils/types.h>
/* Take a screenshot of the current framebuffer and save as BMP to SD.
* Triggered by Capture button (jc->cap). Uses 3-second cooldown.
* Saves to sd:/switch/screenshot/omninx_installer_YYYYMMDD_HHMMSS.bmp */
void take_screenshot(void);
+5 -1
View File
@@ -78,8 +78,12 @@ static omninx_variant_t read_manifest_variant(const char *manifest_path) {
} }
// Read manifest.ini and extract version string // Read manifest.ini and extract version string
static void read_manifest_version(const char *manifest_path, char *version_buf, size_t buf_size) { void read_manifest_version(const char *manifest_path, char *version_buf, size_t buf_size) {
if (!version_buf || buf_size == 0)
return;
version_buf[0] = '\0'; version_buf[0] = '\0';
if (!manifest_path)
return;
link_t sections; link_t sections;
list_init(&sections); list_init(&sections);
+4
View File
@@ -4,6 +4,7 @@
#pragma once #pragma once
#include <utils/types.h> #include <utils/types.h>
#include <stddef.h>
// OmniNX variants // OmniNX variants
typedef enum { typedef enum {
@@ -37,3 +38,6 @@ const char* get_staging_path(omninx_variant_t variant);
// Get manifest.ini path for variant (in staging directory) // Get manifest.ini path for variant (in staging directory)
const char* get_manifest_path(omninx_variant_t variant); const char* get_manifest_path(omninx_variant_t variant);
// Read version string from a manifest.ini (empty string on failure)
void read_manifest_version(const char *manifest_path, char *version_buf, size_t buf_size);
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Generate C# deletion list arrays from payload header files."""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "source"
OUT = ROOT / "desktop" / "OmniNX.DesktopInstaller" / "Data" / "DeletionLists.Generated.cs"
HEADERS = {
"Clean": SOURCE / "deletion_lists_clean.h",
"Update": SOURCE / "deletion_lists_update.h",
}
def parse_arrays(text: str) -> dict[str, list[str]]:
arrays: dict[str, list[str]] = {}
pattern = re.compile(
r"static const char\*\s+(\w+)\[\]\s*=\s*\{([^}]*)\};",
re.MULTILINE | re.DOTALL,
)
for match in pattern.finditer(text):
name = match.group(1)
body = match.group(2)
paths = []
for line in body.splitlines():
line = line.strip()
if not line or line.startswith("//"):
continue
m = re.match(r'"([^"]+)"', line)
if m:
paths.append(m.group(1))
arrays[name] = paths
return arrays
def emit_cs(mode: str, arrays: dict[str, list[str]]) -> str:
lines = [f" public static class {mode}"]
lines.append(" {")
for name, paths in arrays.items():
lines.append(f" public static readonly string[] {name} =")
lines.append(" {")
for p in paths:
rel = p.replace("sd:/", "").replace("sd:\\", "").replace("/", "\\")
lines.append(f' @"{rel}",')
lines.append(" };")
lines.append("")
lines.append(" }")
return "\n".join(lines)
def main() -> None:
sections = []
for mode, header in HEADERS.items():
text = header.read_text(encoding="utf-8")
arrays = parse_arrays(text)
sections.append(emit_cs(mode, arrays))
OUT.parent.mkdir(parents=True, exist_ok=True)
content = """// <auto-generated />
// Generated by tools/generate_deletion_lists.py — do not edit manually.
namespace OmniNX.DesktopInstaller.Data;
public static partial class DeletionLists
{
"""
content += "\n\n".join(sections)
content += "\n}\n"
OUT.write_text(content, encoding="utf-8")
print(f"Wrote {OUT}")
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
/*
* Minimal RAM info test payload (fuse DRAM ID + SK table -> MiB).
* Build: make ram-test -> output/RAM-Test.bin
*/
#include <display/di.h>
#include <mem/heap.h>
#include <mem/minerva.h>
#include <memory_map.h>
#include <soc/bpmp.h>
#include <soc/fuse.h>
#include <soc/hw_init.h>
#include <soc/t210.h>
#include <utils/util.h>
#include "dram_fuse.h"
#include "gfx.h"
#undef COLOR_CYAN
#undef COLOR_WHITE
#define COLOR_CYAN 0xFF00FFFF
#define COLOR_WHITE 0xFFFFFFFF
/* Required by BDK */
boot_cfg_t __attribute__((section("._boot_cfg"))) b_cfg;
volatile nyx_storage_t *nyx_str = (nyx_storage_t *)NYX_STORAGE_ADDR;
extern void pivot_stack(u32 stack_top);
void ipl_main(void)
{
hw_init();
pivot_stack(IPL_STACK_TOP);
heap_init(IPL_HEAP_START);
minerva_init();
minerva_change_freq(FREQ_800);
display_init();
u32 *fb = display_init_framebuffer_pitch();
gfx_init_ctxt(fb, 720, 1280, 720);
gfx_con_init();
display_backlight_pwm_init();
display_backlight_brightness(100, 1000);
bpmp_clk_rate_set(BPMP_CLK_DEFAULT_BOOST);
gfx_clear_grey(0x1B);
gfx_con_setpos(0, 0);
gfx_con_setcol(COLOR_CYAN, gfx_con.fillbg, gfx_con.bgcol);
gfx_printf("RAM test payload\n\n");
gfx_con_setcol(COLOR_WHITE, gfx_con.fillbg, gfx_con.bgcol);
u32 raw = fuse_read_dramid(true);
u32 nid = fuse_read_dramid(false);
u32 chip = hw_get_chip_id();
int mib = dram_capacity_mib_from_fuse();
gfx_printf("SoC: %s\n", chip == GP_HIDREV_MAJOR_T210B01 ? "Mariko (T210B01)" : "Erista (T210)");
gfx_printf("DRAM fuse: raw %d norm %d\n", raw, nid);
if (mib > 0)
gfx_printf("Table MiB: %d (fuse SKU, not probe)\n", mib);
else
gfx_printf("Table MiB: (unknown id for this SoC)\n");
gfx_printf("\nHang — power off or inject payload.\n");
while (1)
msleep(500);
}