diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 34337af..0799313 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Complete game catalog and bilingual galleries + run: python3 scripts/sync_game_catalog.py --check - name: Personal unsigned-build automation tests run: | python3 -m unittest discover -s scripts/tests diff --git a/AGENTS.md b/AGENTS.md index 7c85898..92919f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # Notes for agents -- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it (`dm/tools/patch_nso.py`), on a host that answers Horizon's SVCs and the SDK's services (`dm/host`) and implements NVN, Vulkan and NVIDIA's desktop OpenGL on Metal (`dm/gfx/nvn`, `dm/gfx/vk`, `dm/gfx/gl`). Seventy-six games are playable, each tested on the Mac or the iPhone (a Mac gameplay test is enough to count); the [games table](dm/games/README.md#games) lists each with its build, SDK, engine and graphics API, and links its note, `dm/games//README.md`: what its profile needs and why, what was found on it, how to drive it on the Mac, what is open. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and power. +- nxnative runs rewritten ARM64 game code on Apple hardware. The rewriter is `dm/tools/patch_nso.py`; `dm/host` implements Horizon services; `dm/gfx` maps NVN, Vulkan and OpenGL to Metal. The [games table](dm/games/README.md#games) records profiles, builds, engines and per-game notes. [README.md](README.md) covers players; [docs/development.md](docs/development.md) covers internals and builds. A Mac gameplay test is sufficient for gallery inclusion; retain tested platforms and limitations in the game note. +- For every upstream update, follow [07-upstream-maintenance.md](docs/agents/07-upstream-maintenance.md). Preserve our frontend, controls, saves and identities. Synchronize every accepted profile, artwork mapping and both README galleries; run the catalog gate before merging to main and publishing. - Where to start: | Task | Read first | diff --git a/README-es.md b/README-es.md index b661fb1..826f64d 100644 --- a/README-es.md +++ b/README-es.md @@ -48,7 +48,7 @@ Entregamos **código, una interfaz de preparación y herramientas de compilació ## Actualizaciones del motor y multijugador -Registramos las mejoras del original en el [informe y flujo de mantenimiento](docs/personal-pr-review.md): detección de .NET, empaquetado de pipelines para Mac, reutilización de cachés y detección de regresiones de shaders. Evaluamos por separado los cambios de gráficos y del motor para conservar los juegos que funcionan. +Nuestro main incorpora la revisión del original registrada en el [informe de integración](docs/personal-pr-review.md). Las siguientes actualizaciones del motor se integran y prueban en una copia separada, conservando nuestra interfaz y controles. Cada juego aceptado se sincroniza en la app, imágenes, fichas y ambas galerías del README; CI comprueba que no falte ninguno. **Actualizar ahora** instala nuestras versiones probadas, no cambios del original sin revisar. El motor actual funciona **sin conexión**. No admite los servicios en línea de Nintendo, LAN/netplay ni Windows. Una suscripción a Nintendo Switch Online no los activa aquí. Usar varios controles en un mismo Mac es una función distinta. [Cómo funcionan el código, los mods y la red](docs/development.md#game-binaries-mods-and-networking). @@ -96,123 +96,142 @@ El carrusel muestra los perfiles de esta copia; no proporciona ni descarga juego Esta base requiere iOS/iPadOS 26 o posterior, iPhone con A17 Pro o posterior o iPad con M1 o posterior. Los datos de los juegos y las llaves los aportas tú. + +78 perfiles. Consulta la ficha de cada juego para ver versiones y dispositivos probados. + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + +
Mario Kart 8 Deluxe
Mario Kart 8 Deluxe
~11.9 GB · no Mii heads
Mario vs. Donkey Kong
Mario vs. Donkey Kong
~1.6 GB
Paper Mario: The Thousand-Year Door
Paper Mario: The Thousand-Year Door
~5 GB · 60 fps with 1.0.1
13 Sentinels: Aegis Rim
13 Sentinels: Aegis Rim
~5 GB
13 Sentinels: Aegis Rim (Asia)
13 Sentinels: Aegis Rim (Asia)
~5 GB
Absolum
Absolum
~2 GB
Super Mario 3D World + Bowser's Fury
Super Mario 3D World + Bowser's Fury
~3 GB
Super Mario Bros. Wonder
Super Mario Bros. Wonder
~4 GB
Super Mario Galaxy
Super Mario Galaxy
~3 GB
Ace Attorney Investigations Collection
Ace Attorney Investigations Collection
~7 GB
Advance Wars 1+2: Re-Boot Camp
Advance Wars 1+2: Re-Boot Camp
~7.1 GB
AI: THE SOMNIUM FILES
AI: THE SOMNIUM FILES
~8.3 GB
Super Mario Galaxy 2
Super Mario Galaxy 2
~3 GB
Super Mario Odyssey
Super Mario Odyssey
~6 GB
The Legend of Zelda: Breath of the Wild
The Legend of Zelda: Breath of the Wild
~16 GB · 60 fps with 1.9.0
AI: THE SOMNIUM FILES - nirvanA Initiative
AI: THE SOMNIUM FILES - nirvanA Initiative
~11.6 GB
Animal Crossing: New Horizons
Animal Crossing: New Horizons
~10.5 GB
ANIMAL WELL
ANIMAL WELL
~54 MB
The Legend of Zelda: Link's Awakening
The Legend of Zelda: Link's Awakening
~6 GB
The Legend of Zelda: Tears of the Kingdom
The Legend of Zelda: Tears of the Kingdom
~17 GB · 60 fps with 1.4.3
13 Sentinels: Aegis Rim
13 Sentinels: Aegis Rim
~5 GB
ASTRAL CHAIN
ASTRAL CHAIN
~9.7 GB · 60 fps target with 1.0.1
Balatro
Balatro
~82 MB · played by touch
BALL x PIT
BALL x PIT
~1.1 GB
Absolum
Absolum
~2 GB
Ace Attorney Investigations Collection
Ace Attorney Investigations Collection
~7 GB
Advance Wars 1+2: Re-Boot Camp
Advance Wars 1+2: Re-Boot Camp
~7.1 GB
Bayonetta
Bayonetta
~8.8 GB
Bayonetta 2
Bayonetta 2
~12.5 GB
Bayonetta 3
Bayonetta 3
~14.2 GB
AI: THE SOMNIUM FILES
AI: THE SOMNIUM FILES
~8.3 GB
AI: THE SOMNIUM FILES - nirvanA Initiative
AI: THE SOMNIUM FILES - nirvanA Initiative
~11.6 GB
Animal Crossing: New Horizons
Animal Crossing: New Horizons
~10.5 GB
Big Brain Academy: Brain vs. Brain
Big Brain Academy: Brain vs. Brain
~0.8 GB
Brain Memory Training Academy
Brain Memory Training Academy
~0.1 GB
Captain Toad: Treasure Tracker
Captain Toad: Treasure Tracker
~2 GB
ANIMAL WELL
ANIMAL WELL
~54 MB
ASTRAL CHAIN
ASTRAL CHAIN
~9.7 GB · 60 fps target with 1.0.1
Balatro
Balatro
~82 MB · played by touch
Catherine: Full Body
Catherine: Full Body
~14 GB
Celeste
Celeste
~0.9 GB
Cuphead
Cuphead
~5 GB
BALL x PIT
BALL x PIT
~1.1 GB
Bayonetta
Bayonetta
~8.8 GB
Bayonetta 2
Bayonetta 2
~12.5 GB
DARK SOULS: REMASTERED
DARK SOULS: REMASTERED
~4 GB · 60 fps with 1.0.3
Digimon Story Cyber Sleuth: Complete Edition
Digimon Story Cyber Sleuth: Complete Edition
~5 GB
Donkey Kong Country Returns HD
Donkey Kong Country Returns HD
~11.6 GB
Bayonetta 3
Bayonetta 3
~14.2 GB
Big Brain Academy: Brain vs. Brain
Big Brain Academy: Brain vs. Brain
~0.8 GB
Brain Memory Training Academy
Brain Memory Training Academy
~0.1 GB
Donkey Kong Country: Tropical Freeze
Donkey Kong Country: Tropical Freeze
~7.3 GB
DRAGON QUEST VII Reimagined
DRAGON QUEST VII Reimagined
~5.9 GB
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
~15 GB · 60 fps with 1.0.4
Captain Toad: Treasure Tracker
Captain Toad: Treasure Tracker
~2 GB
Catherine: Full Body
Catherine: Full Body
~14 GB
Celeste
Celeste
~0.9 GB
FINAL FANTASY RESONANCE
FINAL FANTASY RESONANCE
~10 GB · 60 fps with 1.0.1
FINAL FANTASY TACTICS - The Ivalice Chronicles
FINAL FANTASY TACTICS - The Ivalice Chronicles
~11 GB
Fire Emblem Engage
Fire Emblem Engage
~15 GB · 60 fps with 2.0.0
Cuphead
Cuphead
~5 GB
DARK SOULS: REMASTERED
DARK SOULS: REMASTERED
~4 GB · 60 fps with 1.0.3
Digimon Story Cyber Sleuth: Complete Edition
Digimon Story Cyber Sleuth: Complete Edition
~5 GB
Fire Emblem: Three Houses
Fire Emblem: Three Houses
~13 GB · 60 fps with 1.2.0
Ghost Trick: Phantom Detective
Ghost Trick: Phantom Detective
~3 GB
Hades
Hades
~4 GB
DRAGON QUEST VII Reimagined
DRAGON QUEST VII Reimagined
~5.9 GB
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
~15 GB · 60 fps with 1.0.4
FINAL FANTASY RESONANCE
FINAL FANTASY RESONANCE
~10 GB · 60 fps with 1.0.1
Hades II
Hades II
~4 GB
Hollow Knight: Silksong
Hollow Knight: Silksong
~6.4 GB
Hyrule Warriors: Age of Calamity
Hyrule Warriors: Age of Calamity
~15 GB · 60 fps with 1.3.0
FINAL FANTASY TACTICS - The Ivalice Chronicles
FINAL FANTASY TACTICS - The Ivalice Chronicles
~11 GB
Fire Emblem Engage
Fire Emblem Engage
~15 GB · 60 fps with 2.0.0
Fire Emblem: Three Houses
Fire Emblem: Three Houses
~13 GB · 60 fps with 1.2.0
Kirby and the Forgotten Land
Kirby and the Forgotten Land
~6.4 GB
Luigi's Mansion 3
Luigi's Mansion 3
~7.5 GB · 60 fps with 1.4.0
Mario Kart 8 Deluxe
Mario Kart 8 Deluxe
~11.9 GB · no Mii heads
Ghost Trick: Phantom Detective
Ghost Trick: Phantom Detective
~3 GB
The Great Ace Attorney Chronicles
The Great Ace Attorney Chronicles
~4 GB
Hades
Hades
~4 GB
Mario + Rabbids® Sparks of Hope
Mario + Rabbids® Sparks of Hope
~9.8 GB
Mario vs. Donkey Kong
Mario vs. Donkey Kong
~1.6 GB
Metroid Dread
Metroid Dread
~5 GB
Hades II
Hades II
~4 GB
Hollow Knight: Silksong
Hollow Knight: Silksong
~6.4 GB
Hyrule Warriors: Age of Calamity
Hyrule Warriors: Age of Calamity
~15 GB · 60 fps with 1.3.0
Monster Hunter Rise
Monster Hunter Rise
~22 GB · 60 fps with 16.0.2
Monster Train 2
Monster Train 2
~1.6 GB · 60 fps with 1.1.1
NieR:Automata The End of YoRHa Edition
NieR:Automata The End of YoRHa Edition
~10.9 GB · 60 fps with 1.0.3
Luigi's Mansion 3
Luigi's Mansion 3
~7.5 GB · 60 fps with 1.4.0
Metroid Dread
Metroid Dread
~5 GB
Monster Hunter Rise
Monster Hunter Rise
~22 GB · 60 fps with 16.0.2
OCTOPATH TRAVELER
OCTOPATH TRAVELER
~3.6 GB
OCTOPATH TRAVELER 0
OCTOPATH TRAVELER 0
~4 GB · 60 fps with 1.0.5
OCTOPATH TRAVELER II
OCTOPATH TRAVELER II
~5.9 GB · 60 fps with 1.1.1
Monster Train 2
Monster Train 2
~1.6 GB · 60 fps with 1.1.1
Octopath Traveler 0
Octopath Traveler 0
~4 GB · 60 fps with 1.0.5
Octopath Traveler II
Octopath Traveler II
~5.9 GB · 60 fps with 1.1.1
Paper Mario: The Thousand-Year Door
Paper Mario: The Thousand-Year Door
~5 GB · 60 fps with 1.0.1
Patrick's Parabox
Patrick's Parabox
~0.2 GB
Persona 5 Royal
Persona 5 Royal
~14.9 GB
Patrick's Parabox
Patrick's Parabox
~0.2 GB
Persona 5 Royal
Persona 5 Royal
~14.9 GB
Pikmin 4
Pikmin 4
~10.8 GB
Pikmin 4
Pikmin 4
~10.8 GB
Pokémon Brilliant Diamond
Pokémon Brilliant Diamond
~4.6 GB · 60 fps with 1.3.0
Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB
Pokémon Brilliant Diamond
Pokémon Brilliant Diamond
~4.6 GB · 60 fps with 1.3.0
Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB
Pokémon Sword
Pokémon Sword
~12.4 GB
Pokémon Shield
Pokémon Shield
~12.4 GB
Pokémon Sword
Pokémon Sword
~12.4 GB
Portal
Portal
~2.6 GB
Pokémon Shield
Pokémon Shield
~12.4 GB
Portal
Portal
~2.6 GB
Raging Loop
Raging Loop
~0.9 GB
Raging Loop
Raging Loop
~0.9 GB
Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only
Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only
Stardew Valley
Stardew Valley
~0.6 GB
Sea of Stars
Sea of Stars
~5.1 GB
SHIN MEGAMI TENSEI V: Vengeance
SHIN MEGAMI TENSEI V: Vengeance
~14.6 GB · 60 fps with 1.0.3
Stardew Valley
Stardew Valley
~0.6 GB
STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB
Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline
Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch
STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB
Super Mario 3D World + Bowser's Fury
Super Mario 3D World + Bowser's Fury
~3 GB
Super Mario Bros. Wonder
Super Mario Bros. Wonder
~4 GB
Unicorn Overlord
Unicorn Overlord
~4 GB
NieR:Automata The End of YoRHa Edition
NieR:Automata The End of YoRHa Edition
~10.9 GB · 60 fps with 1.0.3
The Legend of Zelda: Echoes of Wisdom
The Legend of Zelda: Echoes of Wisdom
~7.8 GB
Super Mario Galaxy
Super Mario Galaxy
~3 GB
Super Mario Galaxy 2
Super Mario Galaxy 2
~3 GB
Super Mario Odyssey
Super Mario Odyssey
~6 GB
Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline
Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch
The Great Ace Attorney Chronicles
The Great Ace Attorney Chronicles
~4 GB
The Legend of Zelda: Breath of the Wild
The Legend of Zelda: Breath of the Wild
~16 GB · 60 fps with 1.9.0
The Legend of Zelda: Echoes of Wisdom
The Legend of Zelda: Echoes of Wisdom
~7.8 GB
The Legend of Zelda: Link's Awakening
The Legend of Zelda: Link's Awakening
~6 GB
The Legend of Zelda: Tears of the Kingdom
The Legend of Zelda: Tears of the Kingdom
~17 GB · 60 fps with 1.4.3
Unicorn Overlord
Unicorn Overlord
~4 GB
Xenoblade Chronicles 3
Xenoblade Chronicles 3
~19.2 GB
+ ## Controles Xbox, PS5 y PS4 diff --git a/README.md b/README.md index e0c6980..e1ba291 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ We provide **source code, a preparation interface and build tools**. The output ## Engine updates and multiplayer -Selected upstream build improvements are recorded in the [review and maintenance flow](docs/personal-pr-review.md): .NET discovery, Mac pipeline packaging, cache reuse and shader regression checks. Graphics/runtime upgrades are evaluated separately to preserve working games. +Our main incorporates the upstream revision recorded in the [integration review](docs/personal-pr-review.md). Future core updates are merged and tested in a separate checkout while preserving our interface and controls. Every accepted game is synchronized across the app, screenshots, notes and both README galleries; CI checks that none are missing. **Update now** installs our tested releases, rather than unreviewed upstream changes. The current runtime plays **offline**. Nintendo online services, LAN/netplay and Windows are not supported. A Nintendo Switch Online subscription does not enable them here. Local controllers on one Mac are a separate feature. [How the game code, mods and networking work](docs/development.md#game-binaries-mods-and-networking). @@ -92,123 +92,142 @@ The home carousel shows the game profiles in your checkout; it does not provide This base requires iOS/iPadOS 26 or later, an iPhone with A17 Pro or later, or an iPad with M1 or later. Supply your own game files and keys. + +78 profiles. See each game's notes for tested versions and devices. + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + +
Mario Kart 8 Deluxe
Mario Kart 8 Deluxe
~11.9 GB · no Mii heads
Mario vs. Donkey Kong
Mario vs. Donkey Kong
~1.6 GB
Paper Mario: The Thousand-Year Door
Paper Mario: The Thousand-Year Door
~5 GB · 60 fps with 1.0.1
13 Sentinels: Aegis Rim
13 Sentinels: Aegis Rim
~5 GB
13 Sentinels: Aegis Rim (Asia)
13 Sentinels: Aegis Rim (Asia)
~5 GB
Absolum
Absolum
~2 GB
Super Mario 3D World + Bowser's Fury
Super Mario 3D World + Bowser's Fury
~3 GB
Super Mario Bros. Wonder
Super Mario Bros. Wonder
~4 GB
Super Mario Galaxy
Super Mario Galaxy
~3 GB
Ace Attorney Investigations Collection
Ace Attorney Investigations Collection
~7 GB
Advance Wars 1+2: Re-Boot Camp
Advance Wars 1+2: Re-Boot Camp
~7.1 GB
AI: THE SOMNIUM FILES
AI: THE SOMNIUM FILES
~8.3 GB
Super Mario Galaxy 2
Super Mario Galaxy 2
~3 GB
Super Mario Odyssey
Super Mario Odyssey
~6 GB
The Legend of Zelda: Breath of the Wild
The Legend of Zelda: Breath of the Wild
~16 GB · 60 fps with 1.9.0
AI: THE SOMNIUM FILES - nirvanA Initiative
AI: THE SOMNIUM FILES - nirvanA Initiative
~11.6 GB
Animal Crossing: New Horizons
Animal Crossing: New Horizons
~10.5 GB
ANIMAL WELL
ANIMAL WELL
~54 MB
The Legend of Zelda: Link's Awakening
The Legend of Zelda: Link's Awakening
~6 GB
The Legend of Zelda: Tears of the Kingdom
The Legend of Zelda: Tears of the Kingdom
~17 GB · 60 fps with 1.4.3
13 Sentinels: Aegis Rim
13 Sentinels: Aegis Rim
~5 GB
ASTRAL CHAIN
ASTRAL CHAIN
~9.7 GB · 60 fps target with 1.0.1
Balatro
Balatro
~82 MB · played by touch
BALL x PIT
BALL x PIT
~1.1 GB
Absolum
Absolum
~2 GB
Ace Attorney Investigations Collection
Ace Attorney Investigations Collection
~7 GB
Advance Wars 1+2: Re-Boot Camp
Advance Wars 1+2: Re-Boot Camp
~7.1 GB
Bayonetta
Bayonetta
~8.8 GB
Bayonetta 2
Bayonetta 2
~12.5 GB
Bayonetta 3
Bayonetta 3
~14.2 GB
AI: THE SOMNIUM FILES
AI: THE SOMNIUM FILES
~8.3 GB
AI: THE SOMNIUM FILES - nirvanA Initiative
AI: THE SOMNIUM FILES - nirvanA Initiative
~11.6 GB
Animal Crossing: New Horizons
Animal Crossing: New Horizons
~10.5 GB
Big Brain Academy: Brain vs. Brain
Big Brain Academy: Brain vs. Brain
~0.8 GB
Brain Memory Training Academy
Brain Memory Training Academy
~0.1 GB
Captain Toad: Treasure Tracker
Captain Toad: Treasure Tracker
~2 GB
ANIMAL WELL
ANIMAL WELL
~54 MB
ASTRAL CHAIN
ASTRAL CHAIN
~9.7 GB · 60 fps target with 1.0.1
Balatro
Balatro
~82 MB · played by touch
Catherine: Full Body
Catherine: Full Body
~14 GB
Celeste
Celeste
~0.9 GB
Cuphead
Cuphead
~5 GB
BALL x PIT
BALL x PIT
~1.1 GB
Bayonetta
Bayonetta
~8.8 GB
Bayonetta 2
Bayonetta 2
~12.5 GB
DARK SOULS: REMASTERED
DARK SOULS: REMASTERED
~4 GB · 60 fps with 1.0.3
Digimon Story Cyber Sleuth: Complete Edition
Digimon Story Cyber Sleuth: Complete Edition
~5 GB
Donkey Kong Country Returns HD
Donkey Kong Country Returns HD
~11.6 GB
Bayonetta 3
Bayonetta 3
~14.2 GB
Big Brain Academy: Brain vs. Brain
Big Brain Academy: Brain vs. Brain
~0.8 GB
Brain Memory Training Academy
Brain Memory Training Academy
~0.1 GB
Donkey Kong Country: Tropical Freeze
Donkey Kong Country: Tropical Freeze
~7.3 GB
DRAGON QUEST VII Reimagined
DRAGON QUEST VII Reimagined
~5.9 GB
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
~15 GB · 60 fps with 1.0.4
Captain Toad: Treasure Tracker
Captain Toad: Treasure Tracker
~2 GB
Catherine: Full Body
Catherine: Full Body
~14 GB
Celeste
Celeste
~0.9 GB
FINAL FANTASY RESONANCE
FINAL FANTASY RESONANCE
~10 GB · 60 fps with 1.0.1
FINAL FANTASY TACTICS - The Ivalice Chronicles
FINAL FANTASY TACTICS - The Ivalice Chronicles
~11 GB
Fire Emblem Engage
Fire Emblem Engage
~15 GB · 60 fps with 2.0.0
Cuphead
Cuphead
~5 GB
DARK SOULS: REMASTERED
DARK SOULS: REMASTERED
~4 GB · 60 fps with 1.0.3
Digimon Story Cyber Sleuth: Complete Edition
Digimon Story Cyber Sleuth: Complete Edition
~5 GB
Fire Emblem: Three Houses
Fire Emblem: Three Houses
~13 GB · 60 fps with 1.2.0
Ghost Trick: Phantom Detective
Ghost Trick: Phantom Detective
~3 GB
Hades
Hades
~4 GB
DRAGON QUEST VII Reimagined
DRAGON QUEST VII Reimagined
~5.9 GB
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition
~15 GB · 60 fps with 1.0.4
FINAL FANTASY RESONANCE
FINAL FANTASY RESONANCE
~10 GB · 60 fps with 1.0.1
Hades II
Hades II
~4 GB
Hollow Knight: Silksong
Hollow Knight: Silksong
~6.4 GB
Hyrule Warriors: Age of Calamity
Hyrule Warriors: Age of Calamity
~15 GB · 60 fps with 1.3.0
FINAL FANTASY TACTICS - The Ivalice Chronicles
FINAL FANTASY TACTICS - The Ivalice Chronicles
~11 GB
Fire Emblem Engage
Fire Emblem Engage
~15 GB · 60 fps with 2.0.0
Fire Emblem: Three Houses
Fire Emblem: Three Houses
~13 GB · 60 fps with 1.2.0
Kirby and the Forgotten Land
Kirby and the Forgotten Land
~6.4 GB
Luigi's Mansion 3
Luigi's Mansion 3
~7.5 GB · 60 fps with 1.4.0
Mario Kart 8 Deluxe
Mario Kart 8 Deluxe
~11.9 GB · no Mii heads
Ghost Trick: Phantom Detective
Ghost Trick: Phantom Detective
~3 GB
The Great Ace Attorney Chronicles
The Great Ace Attorney Chronicles
~4 GB
Hades
Hades
~4 GB
Mario + Rabbids® Sparks of Hope
Mario + Rabbids® Sparks of Hope
~9.8 GB
Mario vs. Donkey Kong
Mario vs. Donkey Kong
~1.6 GB
Metroid Dread
Metroid Dread
~5 GB
Hades II
Hades II
~4 GB
Hollow Knight: Silksong
Hollow Knight: Silksong
~6.4 GB
Hyrule Warriors: Age of Calamity
Hyrule Warriors: Age of Calamity
~15 GB · 60 fps with 1.3.0
Monster Hunter Rise
Monster Hunter Rise
~22 GB · 60 fps with 16.0.2
Monster Train 2
Monster Train 2
~1.6 GB · 60 fps with 1.1.1
NieR:Automata The End of YoRHa Edition
NieR:Automata The End of YoRHa Edition
~10.9 GB · 60 fps with 1.0.3
Luigi's Mansion 3
Luigi's Mansion 3
~7.5 GB · 60 fps with 1.4.0
Metroid Dread
Metroid Dread
~5 GB
Monster Hunter Rise
Monster Hunter Rise
~22 GB · 60 fps with 16.0.2
OCTOPATH TRAVELER
OCTOPATH TRAVELER
~3.6 GB
OCTOPATH TRAVELER 0
OCTOPATH TRAVELER 0
~4 GB · 60 fps with 1.0.5
OCTOPATH TRAVELER II
OCTOPATH TRAVELER II
~5.9 GB · 60 fps with 1.1.1
Monster Train 2
Monster Train 2
~1.6 GB · 60 fps with 1.1.1
Octopath Traveler 0
Octopath Traveler 0
~4 GB · 60 fps with 1.0.5
Octopath Traveler II
Octopath Traveler II
~5.9 GB · 60 fps with 1.1.1
Paper Mario: The Thousand-Year Door
Paper Mario: The Thousand-Year Door
~5 GB · 60 fps with 1.0.1
Patrick's Parabox
Patrick's Parabox
~0.2 GB
Persona 5 Royal
Persona 5 Royal
~14.9 GB
Patrick's Parabox
Patrick's Parabox
~0.2 GB
Persona 5 Royal
Persona 5 Royal
~14.9 GB
Pikmin 4
Pikmin 4
~10.8 GB
Pikmin 4
Pikmin 4
~10.8 GB
Pokémon Brilliant Diamond
Pokémon Brilliant Diamond
~4.6 GB · 60 fps with 1.3.0
Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB
Pokémon Brilliant Diamond
Pokémon Brilliant Diamond
~4.6 GB · 60 fps with 1.3.0
Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB
Pokémon Sword
Pokémon Sword
~12.4 GB
Pokémon Shield
Pokémon Shield
~12.4 GB
Pokémon Sword
Pokémon Sword
~12.4 GB
Portal
Portal
~2.6 GB
Pokémon Shield
Pokémon Shield
~12.4 GB
Portal
Portal
~2.6 GB
Raging Loop
Raging Loop
~0.9 GB
Raging Loop
Raging Loop
~0.9 GB
Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only
Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only
Stardew Valley
Stardew Valley
~0.6 GB
Sea of Stars
Sea of Stars
~5.1 GB
SHIN MEGAMI TENSEI V: Vengeance
SHIN MEGAMI TENSEI V: Vengeance
~14.6 GB · 60 fps with 1.0.3
Stardew Valley
Stardew Valley
~0.6 GB
STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB
Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline
Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch
STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB
Super Mario 3D World + Bowser's Fury
Super Mario 3D World + Bowser's Fury
~3 GB
Super Mario Bros. Wonder
Super Mario Bros. Wonder
~4 GB
Unicorn Overlord
Unicorn Overlord
~4 GB
NieR:Automata The End of YoRHa Edition
NieR:Automata The End of YoRHa Edition
~10.9 GB · 60 fps with 1.0.3
The Legend of Zelda: Echoes of Wisdom
The Legend of Zelda: Echoes of Wisdom
~7.8 GB
Super Mario Galaxy
Super Mario Galaxy
~3 GB
Super Mario Galaxy 2
Super Mario Galaxy 2
~3 GB
Super Mario Odyssey
Super Mario Odyssey
~6 GB
Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline
Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch
The Great Ace Attorney Chronicles
The Great Ace Attorney Chronicles
~4 GB
The Legend of Zelda: Breath of the Wild
The Legend of Zelda: Breath of the Wild
~16 GB · 60 fps with 1.9.0
The Legend of Zelda: Echoes of Wisdom
The Legend of Zelda: Echoes of Wisdom
~7.8 GB
The Legend of Zelda: Link's Awakening
The Legend of Zelda: Link's Awakening
~6 GB
The Legend of Zelda: Tears of the Kingdom
The Legend of Zelda: Tears of the Kingdom
~17 GB · 60 fps with 1.4.3
Unicorn Overlord
Unicorn Overlord
~4 GB
Xenoblade Chronicles 3
Xenoblade Chronicles 3
~19.2 GB
+ ## Xbox, PS5 and PS4 controllers diff --git a/desktop/NxNativeEasy/game-artwork.json b/desktop/NxNativeEasy/game-artwork.json index b22880a..f82244b 100644 --- a/desktop/NxNativeEasy/game-artwork.json +++ b/desktop/NxNativeEasy/game-artwork.json @@ -13,27 +13,32 @@ "01002B00111A2000": "docs/images/hyrule-warriors-age-of-calamity.jpg", "01002B30028F6000": "docs/images/celeste.jpg", "0100300012F2A000": "docs/images/advance-wars-1-2-re-boot-camp.jpg", + "0100317013770000": "docs/images/mario-rabbids-sparks-of-hope.jpg", "0100346017304000": "docs/images/taiko-risshiden-v-dx.jpg", "010036E00FB20000": "docs/images/the-great-ace-attorney-chronicles.jpg", "010038B015560000": "docs/images/final-fantasy-tactics.jpg", "01003FC01670C000": "docs/images/13-sentinels.jpg", "01004A4010FEA000": "docs/images/bayonetta-3.jpg", "01004AB00A260000": "docs/images/dark-souls-remastered-creation.jpg", + "01004D300C5AE000": "docs/images/kirby-and-the-forgotten-land.jpg", "010051701FB46000": "docs/images/monster-train-2.jpg", "01005270232F2000": "docs/images/octopath-traveler-0.jpg", "0100535012974000": "docs/images/hades.jpg", "010055D009F78000": "docs/images/fire-emblem-three-houses.jpg", "010056B015FE8000": "docs/images/nier-automata.png", + "010057D006492000": "docs/images/octopath-traveler.jpg", "01005CA01580E000": "docs/images/persona-5-royal.jpg", "0100606026F12000": "docs/images/final-fantasy-resonance.jpg", "0100620012D6E000": "docs/images/big-brain-academy-brain-vs-brain.jpg", "010069401ADB8000": "docs/images/unicorn-overlord.jpg", + "010069C01AB82000": "docs/images/shin-megami-tensei-v-vengeance.jpg", "01006A800016E000": "docs/images/super-smash-bros-ultimate.jpg", "01006BB00C6F0000": "docs/images/zelda-links-awakening.jpg", "01006C300E9F0000": "docs/images/dragon-quest-xi-s.jpg", "01006F8002326000": "docs/images/animal-crossing-new-horizons.jpg", "0100713016CCC000": "docs/images/ai-the-somnium-files-nirvana-initiative.jpg", "01007300020FA000": "docs/images/astral-chain.png", + "010074F013262000": "docs/images/xenoblade-chronicles-3.jpg", "010076F0049A2000": "docs/images/bayonetta.jpg", "01007960049A0000": "docs/images/bayonetta-2.jpg", "01007BB017812000": "docs/images/portal.jpg", @@ -41,12 +46,14 @@ "010086A022444000": "docs/images/ball-x-pit.jpg", "010089B00D09C000": "docs/images/ai-the-somnium-files.jpg", "01008BC024FDE000": "docs/images/steins-gate-reboot.jpg", + "01008C0016544000": "docs/images/sea-of-stars.jpg", "01008CF01BAAC000": "docs/images/zelda-echoes-of-wisdom.jpg", "01008D7016438000": "docs/images/13-sentinels.jpg", "01008DB008C2C000": "docs/images/pokemon-shield.jpg", "010093801237C000": "docs/images/metroid-dread.jpg", "010099C022B96000": "docs/images/super-mario-galaxy.jpg", "01009BF0072D4000": "docs/images/captain-toad-treasure-tracker.jpg", + "01009D901BC56000": "docs/images/donkey-kong-country-returns-hd.png", "0100A00019DE0000": "docs/images/hades-ii.jpg", "0100A3501946E000": "docs/images/octopath-traveler-ii.jpg", "0100A5C00D162000": "docs/images/cuphead.jpg", @@ -58,6 +65,7 @@ "0100B7C00933A000": "docs/images/pikmin-4.jpg", "0100B99019412000": "docs/images/mario-vs-donkey-kong.jpg", "0100BF00112C0000": "docs/images/catherine-full-body.jpg", + "0100C1F0051B6000": "docs/images/donkey-kong-country-tropical-freeze.jpg", "0100C2A01A730000": "docs/images/romance-of-the-three-kingdoms-8-remake.jpg", "0100CD801CE5E000": "docs/images/balatro.jpg", "0100D9F01D474000": "docs/images/rhythm-heaven-groove.jpg", diff --git a/desktop/NxNativeEasy/game-catalog.json b/desktop/NxNativeEasy/game-catalog.json new file mode 100644 index 0000000..3ef54ed --- /dev/null +++ b/desktop/NxNativeEasy/game-catalog.json @@ -0,0 +1,787 @@ +{ + "schema": 1, + "games": [ + { + "id": "01003FC01670C000", + "name": "13 Sentinels: Aegis Rim", + "versions": [ + "1.10" + ], + "notes": "dm/games/01003FC01670C000/README.md", + "artwork": "docs/images/13-sentinels.jpg", + "caption": "~5 GB" + }, + { + "id": "01008D7016438000", + "name": "13 Sentinels: Aegis Rim (Asia)", + "versions": [ + "1.00" + ], + "notes": "dm/games/01008D7016438000/README.md", + "artwork": "docs/images/13-sentinels.jpg", + "caption": "~5 GB" + }, + { + "id": "0100A83022A22000", + "name": "Absolum", + "versions": [ + "1.1.1" + ], + "notes": "dm/games/0100A83022A22000/README.md", + "artwork": "docs/images/absolum.jpg", + "caption": "~2 GB" + }, + { + "id": "010005501E68C000", + "name": "Ace Attorney Investigations Collection", + "versions": [ + "1.0.0.2" + ], + "notes": "dm/games/010005501E68C000/README.md", + "artwork": "docs/images/ace-attorney-investigations.jpg", + "caption": "~7 GB" + }, + { + "id": "0100300012F2A000", + "name": "Advance Wars 1+2: Re-Boot Camp", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/0100300012F2A000/README.md", + "artwork": "docs/images/advance-wars-1-2-re-boot-camp.jpg", + "caption": "~7.1 GB" + }, + { + "id": "010089B00D09C000", + "name": "AI: THE SOMNIUM FILES", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/010089B00D09C000/README.md", + "artwork": "docs/images/ai-the-somnium-files.jpg", + "caption": "~8.3 GB" + }, + { + "id": "0100713016CCC000", + "name": "AI: THE SOMNIUM FILES - nirvanA Initiative", + "versions": [ + "1.0.2" + ], + "notes": "dm/games/0100713016CCC000/README.md", + "artwork": "docs/images/ai-the-somnium-files-nirvana-initiative.jpg", + "caption": "~11.6 GB" + }, + { + "id": "01006F8002326000", + "name": "Animal Crossing: New Horizons", + "versions": [ + "3.0.3" + ], + "notes": "dm/games/01006F8002326000/README.md", + "artwork": "docs/images/animal-crossing-new-horizons.jpg", + "caption": "~10.5 GB" + }, + { + "id": "010020D01AD24000", + "name": "ANIMAL WELL", + "versions": [ + "1.0.10" + ], + "notes": "dm/games/010020D01AD24000/README.md", + "artwork": "docs/images/animal-well.png", + "caption": "~54 MB" + }, + { + "id": "01007300020FA000", + "name": "ASTRAL CHAIN", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/01007300020FA000/README.md", + "artwork": "docs/images/astral-chain.png", + "caption": "~9.7 GB · 60 fps target with 1.0.1" + }, + { + "id": "0100CD801CE5E000", + "name": "Balatro", + "versions": [ + "1.1.3" + ], + "notes": "dm/games/0100CD801CE5E000/README.md", + "artwork": "docs/images/balatro.jpg", + "caption": "~82 MB · played by touch" + }, + { + "id": "010086A022444000", + "name": "BALL x PIT", + "versions": [ + "1.293" + ], + "notes": "dm/games/010086A022444000/README.md", + "artwork": "docs/images/ball-x-pit.jpg", + "caption": "~1.1 GB" + }, + { + "id": "010076F0049A2000", + "name": "Bayonetta", + "versions": [ + "1.1" + ], + "notes": "dm/games/010076F0049A2000/README.md", + "artwork": "docs/images/bayonetta.jpg", + "caption": "~8.8 GB" + }, + { + "id": "01007960049A0000", + "name": "Bayonetta 2", + "versions": [ + "1.2" + ], + "notes": "dm/games/01007960049A0000/README.md", + "artwork": "docs/images/bayonetta-2.jpg", + "caption": "~12.5 GB" + }, + { + "id": "01004A4010FEA000", + "name": "Bayonetta 3", + "versions": [ + "1.2.0" + ], + "notes": "dm/games/01004A4010FEA000/README.md", + "artwork": "docs/images/bayonetta-3.jpg", + "caption": "~14.2 GB" + }, + { + "id": "0100620012D6E000", + "name": "Big Brain Academy: Brain vs. Brain", + "versions": [ + "1.2.0" + ], + "notes": "dm/games/0100620012D6E000/README.md", + "artwork": "docs/images/big-brain-academy-brain-vs-brain.jpg", + "caption": "~0.8 GB" + }, + { + "id": "0100F5D01DA3A000", + "name": "Brain Memory Training Academy", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/0100F5D01DA3A000/README.md", + "artwork": "docs/images/brain-memory-training-academy.jpg", + "caption": "~0.1 GB" + }, + { + "id": "01009BF0072D4000", + "name": "Captain Toad: Treasure Tracker", + "versions": [ + "1.4.2" + ], + "notes": "dm/games/01009BF0072D4000/README.md", + "artwork": "docs/images/captain-toad-treasure-tracker.jpg", + "caption": "~2 GB" + }, + { + "id": "0100BF00112C0000", + "name": "Catherine: Full Body", + "versions": [ + "1.0.1a" + ], + "notes": "dm/games/0100BF00112C0000/README.md", + "artwork": "docs/images/catherine-full-body.jpg", + "caption": "~14 GB" + }, + { + "id": "01002B30028F6000", + "name": "Celeste", + "versions": [ + "1.4.1.1" + ], + "notes": "dm/games/01002B30028F6000/README.md", + "artwork": "docs/images/celeste.jpg", + "caption": "~0.9 GB" + }, + { + "id": "0100A5C00D162000", + "name": "Cuphead", + "versions": [ + "1.3.7" + ], + "notes": "dm/games/0100A5C00D162000/README.md", + "artwork": "docs/images/cuphead.jpg", + "caption": "~5 GB" + }, + { + "id": "01004AB00A260000", + "name": "DARK SOULS: REMASTERED", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/01004AB00A260000/README.md", + "artwork": "docs/images/dark-souls-remastered-creation.jpg", + "caption": "~4 GB · 60 fps with 1.0.3" + }, + { + "id": "010014E00DB56000", + "name": "Digimon Story Cyber Sleuth: Complete Edition", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/010014E00DB56000/README.md", + "artwork": "docs/images/digimon-story-cyber-sleuth.jpg", + "caption": "~5 GB" + }, + { + "id": "01009D901BC56000", + "name": "Donkey Kong Country Returns HD", + "versions": [ + "1.1.0" + ], + "notes": "dm/games/01009D901BC56000/README.md", + "artwork": "docs/images/donkey-kong-country-returns-hd.png", + "caption": "~11.6 GB" + }, + { + "id": "0100C1F0051B6000", + "name": "Donkey Kong Country: Tropical Freeze", + "versions": [ + "1.0.2" + ], + "notes": "dm/games/0100C1F0051B6000/README.md", + "artwork": "docs/images/donkey-kong-country-tropical-freeze.jpg", + "caption": "~7.3 GB" + }, + { + "id": "0100A9D01C446000", + "name": "DRAGON QUEST VII Reimagined", + "versions": [ + "1.2.0" + ], + "notes": "dm/games/0100A9D01C446000/README.md", + "artwork": "docs/images/dragon-quest-vii-reimagined.jpg", + "caption": "~5.9 GB" + }, + { + "id": "01006C300E9F0000", + "name": "DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition", + "versions": [ + "1.0.4" + ], + "notes": "dm/games/01006C300E9F0000/README.md", + "artwork": "docs/images/dragon-quest-xi-s.jpg", + "caption": "~15 GB · 60 fps with 1.0.4" + }, + { + "id": "0100606026F12000", + "name": "FINAL FANTASY RESONANCE", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/0100606026F12000/README.md", + "artwork": "docs/images/final-fantasy-resonance.jpg", + "caption": "~10 GB · 60 fps with 1.0.1" + }, + { + "id": "010038B015560000", + "name": "FINAL FANTASY TACTICS - The Ivalice Chronicles", + "versions": [ + "1.4.0" + ], + "notes": "dm/games/010038B015560000/README.md", + "artwork": "docs/images/final-fantasy-tactics.jpg", + "caption": "~11 GB" + }, + { + "id": "0100A6301214E000", + "name": "Fire Emblem Engage", + "versions": [ + "2.0.0" + ], + "notes": "dm/games/0100A6301214E000/README.md", + "artwork": "docs/images/fire-emblem-engage.jpg", + "caption": "~15 GB · 60 fps with 2.0.0" + }, + { + "id": "010055D009F78000", + "name": "Fire Emblem: Three Houses", + "versions": [ + "1.2.0" + ], + "notes": "dm/games/010055D009F78000/README.md", + "artwork": "docs/images/fire-emblem-three-houses.jpg", + "caption": "~13 GB · 60 fps with 1.2.0" + }, + { + "id": "010029B018432000", + "name": "Ghost Trick: Phantom Detective", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/010029B018432000/README.md", + "artwork": "docs/images/ghost-trick-phantom-detective.jpg", + "caption": "~3 GB" + }, + { + "id": "0100535012974000", + "name": "Hades", + "versions": [ + "1.0.38233" + ], + "notes": "dm/games/0100535012974000/README.md", + "artwork": "docs/images/hades.jpg", + "caption": "~4 GB" + }, + { + "id": "0100A00019DE0000", + "name": "Hades II", + "versions": [ + "0.0.126424" + ], + "notes": "dm/games/0100A00019DE0000/README.md", + "artwork": "docs/images/hades-ii.jpg", + "caption": "~4 GB" + }, + { + "id": "010013C00E930000", + "name": "Hollow Knight: Silksong", + "versions": [ + "1.0.30000" + ], + "notes": "dm/games/010013C00E930000/README.md", + "artwork": "docs/images/hollow-knight-silksong.jpg", + "caption": "~6.4 GB" + }, + { + "id": "01002B00111A2000", + "name": "Hyrule Warriors: Age of Calamity", + "versions": [ + "1.3.0" + ], + "notes": "dm/games/01002B00111A2000/README.md", + "artwork": "docs/images/hyrule-warriors-age-of-calamity.jpg", + "caption": "~15 GB · 60 fps with 1.3.0" + }, + { + "id": "01004D300C5AE000", + "name": "Kirby and the Forgotten Land", + "versions": [ + "1.1.0" + ], + "notes": "dm/games/01004D300C5AE000/README.md", + "artwork": "docs/images/kirby-and-the-forgotten-land.jpg", + "caption": "~6.4 GB" + }, + { + "id": "0100DCA0064A6000", + "name": "Luigi's Mansion 3", + "versions": [ + "1.4.0" + ], + "notes": "dm/games/0100DCA0064A6000/README.md", + "artwork": "docs/images/luigis-mansion-3.jpg", + "caption": "~7.5 GB · 60 fps with 1.4.0" + }, + { + "id": "0100152000022000", + "name": "Mario Kart 8 Deluxe", + "versions": [ + "4.0.0" + ], + "notes": "dm/games/0100152000022000/README.md", + "artwork": "docs/images/mario-kart-8-deluxe.jpg", + "caption": "~11.9 GB · no Mii heads" + }, + { + "id": "0100317013770000", + "name": "Mario + Rabbids® Sparks of Hope", + "versions": [ + "1.6.2225577" + ], + "notes": "dm/games/0100317013770000/README.md", + "artwork": "docs/images/mario-rabbids-sparks-of-hope.jpg", + "caption": "~9.8 GB" + }, + { + "id": "0100B99019412000", + "name": "Mario vs. Donkey Kong", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/0100B99019412000/README.md", + "artwork": "docs/images/mario-vs-donkey-kong.jpg", + "caption": "~1.6 GB" + }, + { + "id": "010093801237C000", + "name": "Metroid Dread", + "versions": [ + "2.1.0" + ], + "notes": "dm/games/010093801237C000/README.md", + "artwork": "docs/images/metroid-dread.jpg", + "caption": "~5 GB" + }, + { + "id": "0100B04011742000", + "name": "Monster Hunter Rise", + "versions": [ + "16.0.2" + ], + "notes": "dm/games/0100B04011742000/README.md", + "artwork": "docs/images/monster-hunter-rise.jpg", + "caption": "~22 GB · 60 fps with 16.0.2" + }, + { + "id": "010051701FB46000", + "name": "Monster Train 2", + "versions": [ + "1.1.1" + ], + "notes": "dm/games/010051701FB46000/README.md", + "artwork": "docs/images/monster-train-2.jpg", + "caption": "~1.6 GB · 60 fps with 1.1.1" + }, + { + "id": "010056B015FE8000", + "name": "NieR:Automata The End of YoRHa Edition", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/010056B015FE8000/README.md", + "artwork": "docs/images/nier-automata.png", + "caption": "~10.9 GB · 60 fps with 1.0.3" + }, + { + "id": "010057D006492000", + "name": "OCTOPATH TRAVELER", + "versions": [ + "1.0.5" + ], + "notes": "dm/games/010057D006492000/README.md", + "artwork": "docs/images/octopath-traveler.jpg", + "caption": "~3.6 GB" + }, + { + "id": "01005270232F2000", + "name": "OCTOPATH TRAVELER 0", + "versions": [ + "1.0.5" + ], + "notes": "dm/games/01005270232F2000/README.md", + "artwork": "docs/images/octopath-traveler-0.jpg", + "caption": "~4 GB · 60 fps with 1.0.5" + }, + { + "id": "0100A3501946E000", + "name": "OCTOPATH TRAVELER II", + "versions": [ + "1.1.1" + ], + "notes": "dm/games/0100A3501946E000/README.md", + "artwork": "docs/images/octopath-traveler-ii.jpg", + "caption": "~5.9 GB · 60 fps with 1.1.1" + }, + { + "id": "0100ECD018EBE000", + "name": "Paper Mario: The Thousand-Year Door", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/0100ECD018EBE000/README.md", + "artwork": "docs/images/paper-mario-the-thousand-year-door.jpg", + "caption": "~5 GB · 60 fps with 1.0.1" + }, + { + "id": "0100E44016C22000", + "name": "Patrick's Parabox", + "versions": [ + "1.0.1" + ], + "notes": "dm/games/0100E44016C22000/README.md", + "artwork": "docs/images/patricks-parabox.jpg", + "caption": "~0.2 GB" + }, + { + "id": "01005CA01580E000", + "name": "Persona 5 Royal", + "versions": [ + "1.0.2" + ], + "notes": "dm/games/01005CA01580E000/README.md", + "artwork": "docs/images/persona-5-royal.jpg", + "caption": "~14.9 GB" + }, + { + "id": "0100B7C00933A000", + "name": "Pikmin 4", + "versions": [ + "1.1.1" + ], + "notes": "dm/games/0100B7C00933A000/README.md", + "artwork": "docs/images/pikmin-4.jpg", + "caption": "~10.8 GB" + }, + { + "id": "0100000011D90000", + "name": "Pokémon Brilliant Diamond", + "versions": [ + "1.3.0" + ], + "notes": "dm/games/0100000011D90000/README.md", + "artwork": "docs/images/pokemon-brilliant-diamond.jpg", + "caption": "~4.6 GB · 60 fps with 1.3.0" + }, + { + "id": "0100F43008C44000", + "name": "Pokémon Legends: Z-A", + "versions": [ + "2.0.2" + ], + "notes": "dm/games/0100F43008C44000/README.md", + "artwork": "docs/images/pokemon-legends-z-a.jpg", + "caption": "~6 GB" + }, + { + "id": "01008DB008C2C000", + "name": "Pokémon Shield", + "versions": [ + "1.3.2" + ], + "notes": "dm/games/01008DB008C2C000/README.md", + "artwork": "docs/images/pokemon-shield.jpg", + "caption": "~12.4 GB" + }, + { + "id": "0100ABF008968000", + "name": "Pokémon Sword", + "versions": [ + "1.3.2" + ], + "notes": "dm/games/0100ABF008968000/README.md", + "artwork": "docs/images/pokemon-sword.jpg", + "caption": "~12.4 GB" + }, + { + "id": "01007BB017812000", + "name": "Portal", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/01007BB017812000/README.md", + "artwork": "docs/images/portal.jpg", + "caption": "~2.6 GB" + }, + { + "id": "010020400E1C2000", + "name": "Raging Loop", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/010020400E1C2000/README.md", + "artwork": "docs/images/raging-loop.jpg", + "caption": "~0.9 GB" + }, + { + "id": "0100D9F01D474000", + "name": "Rhythm Heaven Groove", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/0100D9F01D474000/README.md", + "artwork": "docs/images/rhythm-heaven-groove.jpg", + "caption": "~3 GB" + }, + { + "id": "0100C2A01A730000", + "name": "ROMANCE OF THE THREE KINGDOMS 8 REMAKE", + "versions": [ + "1.1.3" + ], + "notes": "dm/games/0100C2A01A730000/README.md", + "artwork": "docs/images/romance-of-the-three-kingdoms-8-remake.jpg", + "caption": "~16.8 GB · controller only" + }, + { + "id": "01008C0016544000", + "name": "Sea of Stars", + "versions": [ + "2.0.59001" + ], + "notes": "dm/games/01008C0016544000/README.md", + "artwork": "docs/images/sea-of-stars.jpg", + "caption": "~5.1 GB" + }, + { + "id": "010069C01AB82000", + "name": "SHIN MEGAMI TENSEI V: Vengeance", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/010069C01AB82000/README.md", + "artwork": "docs/images/shin-megami-tensei-v-vengeance.jpg", + "caption": "~14.6 GB · 60 fps with 1.0.3" + }, + { + "id": "0100E65002BB8000", + "name": "Stardew Valley", + "versions": [ + "1.6.15.13" + ], + "notes": "dm/games/0100E65002BB8000/README.md", + "artwork": "docs/images/stardew-valley.jpg", + "caption": "~0.6 GB" + }, + { + "id": "01008BC024FDE000", + "name": "STEINS;GATE RE:BOOT", + "versions": [ + "1.0.3" + ], + "notes": "dm/games/01008BC024FDE000/README.md", + "artwork": "docs/images/steins-gate-reboot.jpg", + "caption": "~6 GB" + }, + { + "id": "010028600EBDA000", + "name": "Super Mario 3D World + Bowser's Fury", + "versions": [ + "1.2.2" + ], + "notes": "dm/games/010028600EBDA000/README.md", + "artwork": "docs/images/super-mario-3d-world.jpg", + "caption": "~3 GB" + }, + { + "id": "010015100B514000", + "name": "Super Mario Bros. Wonder", + "versions": [ + "1.0.0", + "1.0.1", + "1.2.1" + ], + "notes": "dm/games/010015100B514000/README.md", + "artwork": "docs/images/super-mario-bros-wonder.jpg", + "caption": "~4 GB" + }, + { + "id": "010099C022B96000", + "name": "Super Mario Galaxy", + "versions": [ + "1.3.1" + ], + "notes": "dm/games/010099C022B96000/README.md", + "artwork": "docs/images/super-mario-galaxy.jpg", + "caption": "~3 GB" + }, + { + "id": "0100FD8022DAA000", + "name": "Super Mario Galaxy 2", + "versions": [ + "1.4.0" + ], + "notes": "dm/games/0100FD8022DAA000/README.md", + "artwork": "docs/images/super-mario-galaxy-2.jpg", + "caption": "~3 GB" + }, + { + "id": "0100000000010000", + "name": "Super Mario Odyssey", + "versions": [ + "1.4.1" + ], + "notes": "dm/games/0100000000010000/README.md", + "artwork": "docs/images/super-mario-odyssey.jpg", + "caption": "~6 GB" + }, + { + "id": "01006A800016E000", + "name": "Super Smash Bros. Ultimate", + "versions": [ + "13.0.5" + ], + "notes": "dm/games/01006A800016E000/README.md", + "artwork": "docs/images/super-smash-bros-ultimate.jpg", + "caption": "~17.5 GB · offline" + }, + { + "id": "0100346017304000", + "name": "Taiko Risshiden V DX", + "versions": [ + "1.1.1" + ], + "notes": "dm/games/0100346017304000/README.md", + "artwork": "docs/images/taiko-risshiden-v-dx.jpg", + "caption": "~3.3 GB · Japanese · played by touch" + }, + { + "id": "010036E00FB20000", + "name": "The Great Ace Attorney Chronicles", + "versions": [ + "1.0.0" + ], + "notes": "dm/games/010036E00FB20000/README.md", + "artwork": "docs/images/the-great-ace-attorney-chronicles.jpg", + "caption": "~4 GB" + }, + { + "id": "01007EF00011E000", + "name": "The Legend of Zelda: Breath of the Wild", + "versions": [ + "1.9.0" + ], + "notes": "dm/games/01007EF00011E000/README.md", + "artwork": "docs/images/zelda-breath-of-the-wild.jpg", + "caption": "~16 GB · 60 fps with 1.9.0" + }, + { + "id": "01008CF01BAAC000", + "name": "The Legend of Zelda: Echoes of Wisdom", + "versions": [ + "1.1.0" + ], + "notes": "dm/games/01008CF01BAAC000/README.md", + "artwork": "docs/images/zelda-echoes-of-wisdom.jpg", + "caption": "~7.8 GB" + }, + { + "id": "01006BB00C6F0000", + "name": "The Legend of Zelda: Link's Awakening", + "versions": [ + "1.1.0" + ], + "notes": "dm/games/01006BB00C6F0000/README.md", + "artwork": "docs/images/zelda-links-awakening.jpg", + "caption": "~6 GB" + }, + { + "id": "0100F2C0115B6000", + "name": "The Legend of Zelda: Tears of the Kingdom", + "versions": [ + "1.4.3" + ], + "notes": "dm/games/0100F2C0115B6000/README.md", + "artwork": "docs/images/zelda-tears-of-the-kingdom.jpg", + "caption": "~17 GB · 60 fps with 1.4.3" + }, + { + "id": "010069401ADB8000", + "name": "Unicorn Overlord", + "versions": [ + "1.05" + ], + "notes": "dm/games/010069401ADB8000/README.md", + "artwork": "docs/images/unicorn-overlord.jpg", + "caption": "~4 GB" + }, + { + "id": "010074F013262000", + "name": "Xenoblade Chronicles 3", + "versions": [ + "2.2.1" + ], + "notes": "dm/games/010074F013262000/README.md", + "artwork": "docs/images/xenoblade-chronicles-3.jpg", + "caption": "~19.2 GB" + } + ] +} diff --git a/desktop/NxNativeEasy/tests/catalog_check.swift b/desktop/NxNativeEasy/tests/catalog_check.swift index ff1d787..f703bdf 100644 --- a/desktop/NxNativeEasy/tests/catalog_check.swift +++ b/desktop/NxNativeEasy/tests/catalog_check.swift @@ -35,6 +35,16 @@ import Foundation assert(Set(actual.map {$0.id}).count==actual.count) assert(actual.allSatisfy {$0.artworkURL != nil},"Every current profile must have its existing repo screenshot") assert(actual.contains {$0.name=="Metroid Dread" && $0.versions.contains("2.1.0")}) + let manifestData=try Data(contentsOf:repo.appendingPathComponent("desktop/NxNativeEasy/game-catalog.json")) + let manifest=try JSONSerialization.jsonObject(with:manifestData) as! [String:Any] + let rows=manifest["games"] as! [[String:Any]] + assert(Set(rows.map {$0["id"] as! String})==Set(actual.map {$0.id})) + for game in actual { + let row=rows.first {$0["id"] as? String == game.id}! + assert(row["name"] as? String == game.name) + assert(row["versions"] as? [String] == game.versions) + assert(repo.appendingPathComponent(row["artwork"] as! String).path == game.artworkURL?.path) + } print("Catalog PASS: \(actual.count) repository profiles, live discovery, sorting, versions, accent-insensitive search and no-results state") } } diff --git a/desktop/release.json b/desktop/release.json index 316795b..ed9c0fc 100644 --- a/desktop/release.json +++ b/desktop/release.json @@ -1,5 +1,5 @@ { - "version": "1.4.3", - "build": 8, + "version": "1.5.0", + "build": 9, "minimumMacOSMajor": 26 } diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index 9b25b9a..853838c 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -24,7 +24,7 @@ t_transform_feedback: t_transform_feedback.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_transform_feedback.m $(MTL_T_SRC) $(FW) .PHONY: check_t_transform_feedback check_t_transform_feedback: t_transform_feedback - DM_TEST_TRANSFORM_FEEDBACK=$(CURDIR)/t_transform_feedback MTL_DEBUG_LAYER=1 python3 -m unittest test_transform_feedback.py + DM_TEST_TRANSFORM_FEEDBACK="$(CURDIR)/t_transform_feedback" MTL_DEBUG_LAYER=1 python3 -m unittest test_transform_feedback.py run: check_t_transform_feedback t_nvn: t_nvn.c ../nvn/hle_nvn.c ../../host/crash.c ../../host/hle_bigstack.c $(GFX) $(GFX_H) clang $(CFLAGS) -fobjc-arc -o $@ t_nvn.c ../../host/crash.c ../../host/hle_bigstack.c $(GFX) $(FW) diff --git a/dm/gfx/tests/t_pso_bundle.m b/dm/gfx/tests/t_pso_bundle.m index 45d47b3..f27071a 100644 --- a/dm/gfx/tests/t_pso_bundle.m +++ b/dm/gfx/tests/t_pso_bundle.m @@ -55,8 +55,9 @@ int main(void) { @autoreleasepool { NSString *gpu = g_dev.architecture.name; // the archive as build_ipa.sh makes it (the tool's own folder: this file is dm/gfx/tests/t_pso_bundle.m) NSString *tool = [[@(__FILE__) stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"../tools/pso_offline.py"]; - if (sh([NSString stringWithFormat:@"python3 %@ --platform macos --gpu %@ --msl %@ --pairs %@/shader_pairs.tsv --out %@ --work %@/work > %@/pso_offline.log 2>&1", - tool, gpu, msl, tables, bundle, root, root])) { + NSOperatingSystemVersion hostOS = NSProcessInfo.processInfo.operatingSystemVersion; + if (sh([NSString stringWithFormat:@"python3 %@ --platform macos --os-min %ld.0 --gpu %@ --msl %@ --pairs %@/shader_pairs.tsv --out %@ --work %@/work > %@/pso_offline.log 2>&1", + tool, (long)hostOS.majorVersion, gpu, msl, tables, bundle, root, root])) { NSString *log = [NSString stringWithContentsOfFile:[root stringByAppendingPathComponent:@"pso_offline.log"] encoding:NSUTF8StringEncoding error:nil]; if ([log containsString:@"metal-tt is not available"]) { printf("t_pso_bundle: no metal-tt (xcodebuild -downloadComponent MetalToolchain), GPU checks not run; %d file checks failed\n", failed); return failed != 0; } fprintf(stderr, "pso_offline.py failed:\n%s\n", log.UTF8String); return 1; @@ -165,7 +166,8 @@ int main(void) { @autoreleasepool { EXPECT("off: none opened", 0, list.count); g_arch_bundle = real; } - [NSFileManager.defaultManager removeItemAtPath:root error:nil]; + if (!failed) [NSFileManager.defaultManager removeItemAtPath:root error:nil]; + else fprintf(stderr, "failed fixture retained at %s\n", root.UTF8String); printf("t_pso_bundle: %d checks, %d failed\n", checks, failed); return failed != 0; } } diff --git a/dm/tools/tests/test_nsp_space.py b/dm/tools/tests/test_nsp_space.py index 65d2e0a..2024e36 100644 --- a/dm/tools/tests/test_nsp_space.py +++ b/dm/tools/tests/test_nsp_space.py @@ -7,6 +7,7 @@ import unittest REPO = Path(__file__).resolve().parents[3] spec = importlib.util.spec_from_file_location('build_ipa_from_nsp', REPO / 'scripts/build_ipa_from_nsp.py') +assert spec is not None and spec.loader is not None nsp = importlib.util.module_from_spec(spec); spec.loader.exec_module(nsp) diff --git a/docs/agents/07-upstream-maintenance.md b/docs/agents/07-upstream-maintenance.md new file mode 100644 index 0000000..4377c67 --- /dev/null +++ b/docs/agents/07-upstream-maintenance.md @@ -0,0 +1,72 @@ +# Maintaining this fork against upstream + +This is a standing repository-owner instruction for every future upstream update. +The owner requested that code updates reach our main and that accepted new games +appear everywhere without being forgotten. No recurring schedule is implied. + +## Preserve the working fork + +1. Inspect status, current revision and remotes first. Preserve user edits. Fetch + `seedds/nxnative` main and freeze its SHA. Record reviewed versus integrated + revisions separately in `docs/upstream-sync.json`. Use the previous integrated + head as the next comparison base; a reviewed head is not an integrated head. +2. Prepare a detached candidate using `scripts/sync_upstream.py --merge-main + --output `. Selected cherry-picks remain available through + `--stage`. Never reset our main to upstream or replace it with their README. + Conflicts stay in the candidate until deliberately resolved. +3. Retain English/Spanish UI and documentation, automatic app updates, Mac/iPhone + target selection, controller detection/test, shared touch editor, explicit + external key selection, unsigned/lightweight iOS output and complete Mac apps. + Keep personal commit attribution, original upstream authors and licensing. + Preserve saves, bundle identities and working game packages. Never sign with + an Apple account or ship games/keys. Do not carry unrelated open PRs into a + main merge without their own review and dependencies. + +## Every accepted game, everywhere + +4. Compare profile directories and versions, not only README names. For every + added/changed profile, retain its build IDs, SDK, shader tables, engine fixes, + game note and `dm/games/README.md` row together. A profile alone may depend on + renderer/service/translator changes. Removed profiles must disappear from + generated inventories too. Never announce a game merely because it is in an + unmerged PR or builds successfully. +5. Import existing upstream screenshots, then run: + + ```sh + python3 scripts/sync_game_catalog.py --write --upstream-ref PINNED_SHA + python3 scripts/sync_game_catalog.py --check + ``` + + This generates `game-artwork.json`, `game-catalog.json` and the marked galleries + in README.md/README-es.md from the installed profiles. The desktop carousel, + View all, search and notes already discover live profiles; ensure all mapped + images exist. Missing/ambiguous images stop the release; resolve the mapping + using the title ID, never substitute a different game's art or silently hide it. + Do not hand-edit generated galleries or hard-code a profile count in Swift. + Refresh app screenshots when visible labels or counts change. + Keep compatibility evidence attributed: upstream Mac testing does not prove + iPhone gameplay, and a build does not prove online support. State limitations + in game notes; all users still supply their own game files and keys. + +## Validate, integrate and release + +6. Run the catalog check, documentation tests, fork tests and affected upstream + tests. Compile the Mac host and unsigned iOS app when runtime/input changes. + Run host/GPU regressions for graphics/service changes. If the shader chain + changes, regenerate its header, check the locked translator and rebuild a + representative game's Mac/iOS shaders in separate output folders, then its + host. Keep shader/header/translator/profile inputs coherent; do not reuse old + libraries under a new chain. Preserve existing working game caches/packages. + Test game/device behavior where available; document untested platforms or + modes honestly. Failed checks stop integration/publication until resolved. +7. Integrate the reviewed candidate into our main, update `integrated_head` and + review evidence, and bump `desktop/release.json` for changed functionality or + accepted games so the in-app updater offers it. Verify checkout cleanliness, + build/ad-hoc signature, commit/push and remote synchronization when publication + is authorized. Explain what was integrated, what remains pending and whether + users must prepare their existing game again. A documentation-only commit + needs no release bump. Update this guide if the maintenance procedure changes. + +The CI catalog gate prevents incomplete game lists from silently shipping. +The tools stage and check updates; they do not auto-approve unreviewed engine +changes, erase conflicts, install on devices or overwrite a user's game files. diff --git a/docs/agents/README.md b/docs/agents/README.md index b1de25c..ee83453 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -12,6 +12,7 @@ Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the orig | [04-graphics.md](04-graphics.md) | NVN on Metal for a new title: finding every shader container, shader keys, binding layouts, bindless textures, depth mode, the diagnostic switches; what other titles needed is in the [rendering](../reference/rendering.md), [shaders](../reference/shaders.md) and [NVN objects](../reference/nvn-objects.md) references | | [05-debugging.md](05-debugging.md) | Recipes: crash reports, lldb scripts, Ghidra headless decompilation, objdump, memory growth, render-pass and texture dumps, on-device diagnostics, offline pipeline link checks | | [06-dev-loop.md](06-dev-loop.md) | The developer loop: which tool to build, run, debug, measure and check with, CI, and what a pull request needs by what it changes | +| [07-upstream-maintenance.md](07-upstream-maintenance.md) | Updating this fork from upstream main, preserving fork features, synchronizing new games and validating releases | | [symptoms.md](symptoms.md) | One line per symptom met so far (an assertion, a `[RED]` line, an abort or crash site, a visible result) → the reference entry that explains it | ## Ground rules diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index f3400e9..e6b54ac 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -8,6 +8,8 @@ One line per symptom in the [subsystem references](../reference/): what you see - packing succeeds, but `unzip -t` or Python's `ZipFile.testzip()` reports a bad CRC for a large deflated file. → [A streamed file changes size and the IPA fails its CRC check](../reference/packing.md#a-streamed-file-changes-size-and-the-ipa-fails-its-crc-check) - `decompress NSZ` stops immediately with `nsz: error: unrecognized arguments: --keys …`. → [NSZ decompression rejects `--keys`](../reference/packing.md#nsz-decompression-rejects---keys) - clicking Prepare game stops before any build tools start, with `PermissionError: [Errno 1] Operation not permitted` at `build_session.py` `os.setsid()`. → [Prepare game fails at `os.setsid()`](../reference/packing.md#prepare-game-fails-at-ossetsid) +- `check_t_transform_feedback` reports `Code/.../t_transform_feedback: No such file or directory` before running its checks. → [Graphics regression command fails in a workspace path containing spaces](../reference/packing.md#graphics-regression-command-fails-in-a-workspace-path-containing-spaces) +- `t_pso_bundle` builds an archive but reports ten failures after Metal rejects its deployment target. → [Bundled-pipeline regression fixture targets a newer macOS than the host](../reference/packing.md#bundled-pipeline-regression-fixture-targets-a-newer-macos-than-the-host) ## Rewriter diff --git a/docs/personal-ios.md b/docs/personal-ios.md index 8a9c914..f807281 100644 --- a/docs/personal-ios.md +++ b/docs/personal-ios.md @@ -25,9 +25,11 @@ Cancellation signals the entire build process group. This branch retains nxnative's MIT license and attribution. It starts from upstream `ecbeae164c0b419d1ba1031248fe1402050a3eb0`, includes the shared iOS touch -editor and the tested #277 extraction-space fix, and keeps the working Dread shader -chain. Selective upstream packaging/.NET/cache/regression-check imports are tracked -in [the PR review and repeatable sync flow](personal-pr-review.md). +editor and the tested #277 extraction-space fix. Release 1.5 integrates upstream +main through the SHA in [upstream-sync.json](upstream-sync.json); the previous +working game apps/caches remain untouched. The new chain has a separate validated +Dread shader build. See [the integration review](personal-pr-review.md) and the +[standing agent procedure](agents/07-upstream-maintenance.md). ## One command, including lightweight export diff --git a/docs/personal-pr-review.md b/docs/personal-pr-review.md index ae009fc..6fb40d7 100644 --- a/docs/personal-pr-review.md +++ b/docs/personal-pr-review.md @@ -1,3 +1,50 @@ +# Full upstream main integration and permanent catalog maintenance + +Current release: **1.5.0 / build 9**. Our main now incorporates the complete upstream +history through `40e6baede556acd454169ac75b33d1686063f528`, replacing the selective-only state documented below. +All earlier reviews remain historical evidence, not the current integration state. +The next update starts from `integrated_head` in [upstream-sync.json](upstream-sync.json). + +Eight upstream profiles are now included with their existing screenshots, notes, +versions and required core changes: Mario + Rabbids® Sparks of Hope, Kirby and the Forgotten Land, OCTOPATH TRAVELER, SHIN MEGAMI TENSEI V: Vengeance, Xenoblade Chronicles 3, Sea of Stars, Donkey Kong Country Returns HD, Donkey Kong Country: Tropical Freeze. +There are 78 installed profiles. Both README galleries, the generated inventory +and artwork map are synchronized; the app's live carousel, View all and search +read these profiles. No independent iPhone-playability claim was added for the +new games; upstream platform-specific evidence stays in each game note. + +The standing agent procedure is [07-upstream-maintenance.md](agents/07-upstream-maintenance.md), +linked directly from root AGENTS.md. `sync_upstream.py --merge-main` creates a +separate candidate, reports added/updated/removed profiles, synchronizes catalog +outputs and runs synthetic checks. Conflicts remain isolated for agent review. +`sync_game_catalog.py --check` is also a required CI gate: missing notes, images, +version updates, manifest entries or bilingual gallery rows fail the check. + +The merge retains our Spanish/English interface, updater, controller test, touch +editor, lightweight unsigned iOS output, complete Mac apps and explicit own-key +selection. Working game packages and saves were not changed. The new translation +chain is `7fbca6dd477d`: all 1,010 Dread shaders and their variants/pulling libraries +were rebuilt for macOS and iOS in separate scratch directories, then the new host +was built. The production build's own-compilers wrapper was used because this +Mac's spirv-cross binary fingerprint differs from the maintainer's lock; the +locked translator was retained, and no mismatch was concealed by editing the lock. +An unsigned production iOS project with synthetic Dread-profile modules compiled +and linked, including our touch controls. Host tests passed. Full native GPU regression checks passed. The initial full Python run passed +61 test files (441 cases, 19 skipped optional fixtures); final fork checks +were rerun after maintenance/lint fixes. The pinned lint ratchet reports zero +new findings, without expanding its existing upstream baseline. Compilation and +synthetic checks are not a new physical iPhone gameplay test. + +Two native test fixture issues were corrected during integration: an unquoted +workspace path in the transform-feedback Makefile, and an offline-pipeline test +that targeted the SDK's macOS 27 default while running on macOS 26.6. The fixture +now requests the running OS major and retains failed artifacts for diagnostics; +production's archive fallback is unchanged. The newly imported Python lint gate +also exposed small pre-existing fork issues; these were fixed without expanding +the upstream baseline. This included a missing interpreter argument in the sync +command's validation call, now covered by a full command-path regression test. + +--- + # Upstream maintenance: latest selective integration Snapshot captured on 2026-10-26 (America/Mexico_City), pinned to upstream diff --git a/docs/reference/packing.md b/docs/reference/packing.md index ce084f9..7529ceb 100644 --- a/docs/reference/packing.md +++ b/docs/reference/packing.md @@ -35,3 +35,17 @@ - **Cause:** Foundation `Process` already launches the worker as its own process-group leader on macOS. POSIX rejects `setsid()` for a group leader. A normal Python subprocess launch did not expose this difference, so the original regression test missed the GUI path. - **Fix:** reuse the existing group when `os.getpgrp() == os.getpid()`; call `setsid()` only for a worker that still shares its parent's group. Cancellation still signals the worker's separate group. Do not suppress unrelated permission failures. Added regression coverage for Foundation `Process`, an existing group leader, an existing session leader and a normal subprocess launch, including cancellation. - **Found on:** a user's clean desktop installation, before game detection; reproduced locally with a Swift Foundation launcher. Applies to every Mac/iPhone game preparation, independent of the game, keys or saves. Released in 1.4.2/build 7. + +### Graphics regression command fails in a workspace path containing spaces + +- **Symptom:** `check_t_transform_feedback` reports `Code/.../t_transform_feedback: No such file or directory` before running its checks. +- **Cause:** the Makefile passed `$(CURDIR)` into a shell environment assignment without quoting; the space split the executable path into a command. +- **Fix:** quote the complete `DM_TEST_TRANSFORM_FEEDBACK` value. +- **Found on:** the full upstream-main integration in this fork's `Claude Code/ios_games` workspace; independent of game data. + +### Bundled-pipeline regression fixture targets a newer macOS than the host + +- **Symptom:** `t_pso_bundle` builds an archive but reports ten failures after Metal rejects its deployment target. +- **Cause:** the fixture used `pso_offline.py`'s SDK-major default (27) on a Mac running macOS 26.6. Production intentionally falls back to compiling bases on-device when it cannot load such an archive. +- **Fix:** the native fixture explicitly passes the current macOS major as `--os-min`, so it tests archive reuse on the system actually running it; keep its scratch fixture on failure for diagnostics. Production fallback remains intact. +- **Found on:** full-main GPU validation with Xcode 27.1 on macOS 26.6.1, Apple M2 Max. diff --git a/docs/upstream-sync.json b/docs/upstream-sync.json index 485fb46..218f6e3 100644 --- a/docs/upstream-sync.json +++ b/docs/upstream-sync.json @@ -2,7 +2,7 @@ "schema": 1, "repository": "seedds/nxnative", "baseline": "ecbeae164c0b419d1ba1031248fe1402050a3eb0", - "reviewed_head": "15805ef52578afe54196504e3f98e15a22221664", + "reviewed_head": "40e6baede556acd454169ac75b33d1686063f528", "integrated": [ { "upstream_sha": "4639c5114cbb8a8f250645941fa3a25c1ba08cc6", @@ -21,5 +21,45 @@ "title": "Make repeated IPA and Mac app builds incremental (#279)" } ], - "note": "Selective tooling imports, not a merge of all upstream history. Runtime and translation chain remain at the baseline." + "note": "Full upstream main integrated through integrated_head. Earlier selective imports are retained for provenance. Future comparisons start at integrated_head; all fork features and game catalog gates remain required.", + "integrated_head": "40e6baede556acd454169ac75b33d1686063f528", + "latest_full_integration": { + "from_fork": "f23448d216e2806650c0e6728830fe1328333b0e", + "upstream_head": "40e6baede556acd454169ac75b33d1686063f528", + "added_games": [ + { + "id": "0100317013770000", + "name": "Mario + Rabbids® Sparks of Hope" + }, + { + "id": "01004D300C5AE000", + "name": "Kirby and the Forgotten Land" + }, + { + "id": "010057D006492000", + "name": "OCTOPATH TRAVELER" + }, + { + "id": "010069C01AB82000", + "name": "SHIN MEGAMI TENSEI V: Vengeance" + }, + { + "id": "010074F013262000", + "name": "Xenoblade Chronicles 3" + }, + { + "id": "01008C0016544000", + "name": "Sea of Stars" + }, + { + "id": "01009D901BC56000", + "name": "Donkey Kong Country Returns HD" + }, + { + "id": "0100C1F0051B6000", + "name": "Donkey Kong Country: Tropical Freeze" + } + ], + "translation_chain": "7fbca6dd477d" + } } diff --git a/scripts/build_desktop_app.py b/scripts/build_desktop_app.py index 57f2dae..a33fd6f 100755 --- a/scripts/build_desktop_app.py +++ b/scripts/build_desktop_app.py @@ -4,7 +4,6 @@ import argparse from pathlib import Path import plistlib import subprocess -import sys import tempfile import shutil import json diff --git a/scripts/build_ios.py b/scripts/build_ios.py index a49cfd5..e468ecc 100755 --- a/scripts/build_ios.py +++ b/scripts/build_ios.py @@ -26,8 +26,8 @@ def build_lock(): with open(Path(tempfile.gettempdir()) / ('nxnative-ios-'+digest+'.lock'),'a') as lock: try: fcntl.flock(lock,fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - raise ValueError('Another build is using this checkout; wait for it to finish.') + except BlockingIOError as error: + raise ValueError('Another build is using this checkout; wait for it to finish.') from error try: yield finally: @@ -36,6 +36,7 @@ def build_lock(): def load_builder(): spec = importlib.util.spec_from_file_location('nxnative_builder', REPO / 'scripts/build_ipa_from_nsp.py') + assert spec is not None and spec.loader is not None builder = importlib.util.module_from_spec(spec) sys.modules[spec.name] = builder spec.loader.exec_module(builder) @@ -205,7 +206,7 @@ if __name__ == '__main__': try: raise SystemExit(main()) except KeyboardInterrupt: - raise SystemExit(130) + raise SystemExit(130) from None except (OSError,ValueError,subprocess.SubprocessError,zipfile.BadZipFile) as error: print('Error:',error,file=sys.stderr) - raise SystemExit(1) + raise SystemExit(1) from error diff --git a/scripts/sync_game_catalog.py b/scripts/sync_game_catalog.py new file mode 100644 index 0000000..c0c27cd --- /dev/null +++ b/scripts/sync_game_catalog.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Keep game profiles, artwork, the desktop inventory and both README galleries aligned.""" +import argparse +import html +import json +from pathlib import Path +import re +import subprocess +import sys +import unicodedata + +ROOT=Path(__file__).resolve().parents[1] +ART='desktop/NxNativeEasy/game-artwork.json' +CATALOG='desktop/NxNativeEasy/game-catalog.json' +START='' +END='' + + +def normalized(name): + return ''.join(c for c in unicodedata.normalize('NFKD',html.unescape(name)).casefold() if c.isalnum() and not unicodedata.combining(c)) + + +def gallery_rows(source): + rows=[] + for cell in re.findall(r']*>(.*?)',source,re.S|re.I): + image=re.search(r']*\bsrc="(docs/images/[^"<>]+)"[^>]*\balt="([^"]*)"',cell,re.I) + if not image:continue + caption=re.search(r'(.*?)',cell,re.S|re.I) + rows.append(dict(image=html.unescape(image[1]),name=html.unescape(image[2]), + caption=html.unescape(re.sub('<[^>]+>','',caption[1])) if caption else '')) + return rows + + +def read_json(root,name,default): + path=root/name + return json.loads(path.read_text()) if path.exists() else default + + +def inventory(root): + table=(root/'dm/games/README.md').read_text() + table_names={} + for line in table.splitlines(): + match=re.match(r'\| \[([^\]]+)\]\(([0-9A-Fa-f]{16})/README\.md\) \| `([0-9A-Fa-f]{16})`',line) + if match: + if match[2].upper()!=match[3].upper():raise ValueError('Mismatched title ID in game table.') + table_names[match[2].upper()]=match[1] + games=[] + for folder in sorted((root/'dm/games').iterdir()): + if not re.fullmatch('[0-9A-Fa-f]{16}',folder.name) or not (folder/'game.tsv').is_file():continue + title=folder.name.upper() + if title not in table_names or not (folder/'README.md').is_file():raise ValueError('Missing game table row or note: '+title) + name=None;versions=[] + for line in (folder/'game.tsv').read_text().splitlines(): + if line.lstrip().startswith('#'):continue + fields=[f.strip() for f in line.split('\t')] + if len(fields)>1 and fields[0]=='name':name=fields[1] + if len(fields)>2 and fields[0]=='build' and fields[2] and fields[2] not in versions:versions.append(fields[2]) + if not name:raise ValueError('Missing profile name: '+title) + games.append(dict(id=title,name=name,table_name=table_names[title],versions=versions,notes=f'dm/games/{title}/README.md')) + ids={g['id'] for g in games} + if len(ids)!=len(games):raise ValueError('Duplicate profile title ID.') + if ids!=set(table_names):raise ValueError('The game table and profile directories disagree.') + if not games:raise ValueError('No game profiles found.') + return sorted(games,key=lambda g:(normalized(g['name']),g['id'])) + + +def safe_image(root,image): + path=Path(image) + if path.is_absolute() or '..' in path.parts or not image.startswith('docs/images/'): + raise ValueError('Artwork must be an existing repository image: '+image) + resolved=(root/path).resolve() + if not resolved.is_relative_to(root.resolve()) or not resolved.is_relative_to((root/'docs/images').resolve()) or not resolved.is_file(): + raise ValueError('Missing or external artwork: '+image) + + +def render(games,spanish): + count=f"{len(games)} perfiles. Consulta la ficha de cada juego para ver versiones y dispositivos probados." if spanish else f"{len(games)} profiles. See each game's notes for tested versions and devices." + lines=[START,count,'',''] + for i,game in enumerate(games): + if i%3==0:lines.append('') + name=html.escape(game['name'],quote=True);image=html.escape(game['artwork'],quote=True);notes=game['notes'] + caption=html.escape(game['caption'] or ('Ver ficha del juego' if spanish else 'See game notes')) + lines.append(f'') + if i%3==2 or i==len(games)-1:lines.append('') + return '\n'.join(lines+['
{name}
{name}
{caption}
',END]) + + +def replace_gallery(source,block): + if START in source or END in source: + if source.count(START)!=1 or source.count(END)!=1:raise ValueError('Expected exactly one generated gallery block.') + first=source.index(START);last=source.index(END)+len(END) + if last');last=source.index('',first)+len('') + return source[:first]+block+source[last:] + + +def outputs(root,upstream_html=''): + games=inventory(root) + images=read_json(root,ART,{}) + previous={g['id']:g for g in read_json(root,CATALOG,{'games':[]})['games']} + sources=[] + for name in ['README.md','README-es.md']:sources+=gallery_rows((root/name).read_text()) + sources+=gallery_rows(upstream_html) # pinned upstream captions override older gallery captions + by_name={normalized(row['name']):row for row in sources};by_image={row['image']:row for row in sources} + for game in games: + match=by_name.get(normalized(game['name'])) or by_name.get(normalized(game.pop('table_name'))) + game.pop('table_name',None) + image=images.get(game['id']) or (match['image'] if match else None) + if not image:raise ValueError(f"No screenshot mapped for {game['id']} ({game['name']}); import the upstream screenshot and map its title ID before release.") + safe_image(root,image) + game['artwork']=image + row=match if match and match['image']==image else by_image.get(image) + game['caption']=row['caption'] if row else previous.get(game['id'],{}).get('caption','') + result={ART:json.dumps({g['id']:g['artwork'] for g in sorted(games,key=lambda g:g['id'])},indent=2,ensure_ascii=False)+'\n', + CATALOG:json.dumps({'schema':1,'games':games},indent=2,ensure_ascii=False)+'\n'} + for name in ['README.md','README-es.md']: + result[name]=replace_gallery((root/name).read_text(),render(games,name=='README-es.md')) + return result + + +def sync(root,write=False,upstream_html=''): + expected=outputs(root,upstream_html) + changed=[name for name,text in expected.items() if not (root/name).exists() or (root/name).read_text()!=text] + if write: + for name in changed:(root/name).write_text(expected[name]) + elif changed:raise ValueError('Game catalog is stale: '+', '.join(changed)+'. Run scripts/sync_game_catalog.py --write with the pinned upstream ref.') + return changed + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + mode=parser.add_mutually_exclusive_group(required=True) + mode.add_argument('--write',action='store_true');mode.add_argument('--check',action='store_true') + parser.add_argument('--upstream-ref',help='pinned, already fetched commit used only to discover upstream screenshot/caption mappings') + args=parser.parse_args() + source='' + if args.upstream_ref: + if not re.fullmatch('[0-9a-fA-F]{7,40}',args.upstream_ref):parser.error('Use a pinned upstream commit SHA.') + source=subprocess.check_output(['git','-C',str(ROOT),'show',args.upstream_ref+':README.md'],text=True) + changed=sync(ROOT,args.write,source) + print(f'Game catalog: {len(inventory(ROOT))} profiles, '+('updated '+', '.join(changed) if changed else 'all mappings and galleries current')) + + +if __name__=='__main__': + try:main() + except (ValueError,OSError,subprocess.SubprocessError) as error: + print('Error:',error,file=sys.stderr);sys.exit(1) diff --git a/scripts/sync_upstream.py b/scripts/sync_upstream.py index a8621dd..767ea4a 100644 --- a/scripts/sync_upstream.py +++ b/scripts/sync_upstream.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone import json from pathlib import Path import subprocess +import re import sys ROOT=Path(__file__).resolve().parents[1] @@ -50,6 +51,33 @@ def stage(root,destination,head,selected,integrated): subprocess.run(['git','-C',str(destination),'cherry-pick','-x',*resolved],check=True) +def merge_main(root,destination,head): + if git(root,'status','--porcelain'):raise ValueError('Commit or stash local edits before staging; nothing was changed.') + git(root,'worktree','add','--detach',str(destination),'HEAD') + subprocess.run(['git','-C',str(destination),'merge','--no-edit',head],check=True) + + +def profile_changes(root,head): + def profiles(revision): + found={} + for row in git(root,'ls-tree','-r',revision,'--','dm/games').splitlines(): + metadata,path=row.split('\t',1) + match=re.fullmatch(r'dm/games/([0-9A-Fa-f]{16})/game.tsv',path) + if match:found[match[1].upper()]=(metadata.split()[2],path) + return found + def summary(revision,title,path): + name=title;versions=[] + for line in git(root,'show',revision+':'+path).splitlines(): + fields=line.split('\t') + if fields[0]=='name' and len(fields)>1:name=fields[1] + if fields[0]=='build' and len(fields)>2 and fields[2] not in versions:versions.append(fields[2]) + return dict(id=title,name=name,versions=versions) + old,new=profiles('HEAD'),profiles(head) + return dict(added=[summary(head,i,new[i][1]) for i in sorted(new.keys()-old.keys())], + updated=[summary(head,i,new[i][1]) for i in sorted(old.keys()&new.keys()) if old[i][0]!=new[i][0]], + removed=[summary('HEAD',i,old[i][1]) for i in sorted(old.keys()-new.keys())]) + + def build_python(): candidates=[Path(sys.executable),ROOT.parent/'venv/bin/python', Path.home()/'Library/Application Support/nxnative Easy/Tools/nxnative-tools/venv/bin/python'] @@ -63,6 +91,7 @@ def build_python(): def validate(candidate,output,python): env=__import__('os').environ.copy();env.setdefault('DM_GAME','010015100B514000') logs=output/'checks';logs.mkdir() + subprocess.run([python,str(candidate/'scripts/sync_game_catalog.py'),'--check'],check=True) suites=['scripts/tests','dm/tools/tests','dm/ios/tests','dm/gfx/tests'] for suite in suites: for test in sorted((candidate/suite).glob('test_*.py')): @@ -76,7 +105,9 @@ def validate(candidate,output,python): def main(argv=None): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--output',required=True,type=Path,help='new directory outside this checkout') - parser.add_argument('--stage',nargs='+',metavar='SHA',help='explicitly reviewed commits, in dependency order; stage and run synthetic checks') + mode=parser.add_mutually_exclusive_group() + mode.add_argument('--stage',nargs='+',metavar='SHA',help='explicitly reviewed commits, in dependency order; stage and run synthetic checks') + mode.add_argument('--merge-main',action='store_true',help='merge pinned upstream main into a detached candidate, synchronize game lists and run checks') args=parser.parse_args(argv) output=args.output.expanduser().resolve() if output==ROOT or ROOT in output.parents or output.exists():parser.error('Choose a new folder outside the source checkout.') @@ -87,9 +118,11 @@ def main(argv=None): git(ROOT,'fetch','upstream','main') head=git(ROOT,'rev-parse','FETCH_HEAD') integrated={item['upstream_sha'] for item in policy['integrated']} - rows=commits(ROOT,policy['baseline'],head,integrated) + base=policy.get('integrated_head',policy['baseline']) + rows=commits(ROOT,base,head,integrated) + games=profile_changes(ROOT,head) output.mkdir(parents=True) - report=dict(reviewed_at=datetime.now(timezone.utc).isoformat(),upstream_head=head,baseline=policy['baseline'], + report=dict(reviewed_at=datetime.now(timezone.utc).isoformat(),upstream_head=head,baseline=base,game_changes=games, local_head=git(ROOT,'rev-parse','HEAD'),commits=rows,candidate_status='not staged') report_path=output/'report.json' def save():report_path.write_text(json.dumps(report,indent=2)+'\n') @@ -99,22 +132,27 @@ def main(argv=None): for row in rows: title=row['title'].replace('|','/').replace('\n',' ') lines.append(f"| `{row['sha'][:12]}` | {row['status']} | {', '.join(row['areas'])} | {title} |") + lines+=['','## Game profile changes',''] + for status,items in games.items(): + lines += [f"- {status}: {g['name']} (`{g['id']}`), versions {', '.join(g['versions'])}" for g in items] (output/'report.md').write_text('\n'.join(lines)+'\n') print('Review:',output/'report.md',flush=True) - if args.stage: - report['requested_commits']=args.stage;report['candidate_status']='staging';save() + if args.stage or args.merge_main: + report['requested_commits']=args.stage;report['mode']='merge-main' if args.merge_main else 'selected commits';report['candidate_status']='staging';save() try: python=build_python() - stage(ROOT,output/'candidate',head,args.stage,integrated) + if args.merge_main:merge_main(ROOT,output/'candidate',head) + else:stage(ROOT,output/'candidate',head,args.stage,integrated) + subprocess.run([python,str(output/'candidate/scripts/sync_game_catalog.py'),'--write','--upstream-ref',head],check=True) report['candidate_status']='checking';save() - validate(output/'candidate',output) + validate(output/'candidate',output,python) report['candidate_status']='synthetic checks passed; gameplay and release review still required';save() except Exception: report['candidate_status']='failed; inspect candidate and checks, active checkout preserved';save();raise print('Candidate:',output/'candidate') print('Inspect changes, compile affected targets and test games before integration.') else: - print('To evaluate selected commits, run again with a new --output folder and --stage SHA [SHA ...].') + print('To evaluate selected commits, run again with a new --output folder and --merge-main, or --stage SHA [SHA ...].') return 0 diff --git a/scripts/tests/test_desktop_install.py b/scripts/tests/test_desktop_install.py index 5c83556..756742c 100644 --- a/scripts/tests/test_desktop_install.py +++ b/scripts/tests/test_desktop_install.py @@ -10,6 +10,7 @@ from unittest.mock import patch ROOT=Path(__file__).resolve().parents[2] spec=importlib.util.spec_from_file_location('desktop_builder',ROOT/'scripts/build_desktop_app.py') +assert spec is not None and spec.loader is not None builder=importlib.util.module_from_spec(spec);spec.loader.exec_module(builder) class DesktopInstall(unittest.TestCase): @@ -33,7 +34,7 @@ class DesktopInstall(unittest.TestCase): root=Path(temporary);app=self.old_app(root);staged=root/'New.app';staged.mkdir() (staged/'marker').write_text('new') replace=builder.os.replace - def swap(source,destination): + def swap(source,destination,fail=fail,staged=staged,replace=replace): if fail and source==staged:raise OSError('simulated swap failure') return replace(source,destination) with patch.object(builder.os,'replace',side_effect=swap): diff --git a/scripts/tests/test_desktop_updates.py b/scripts/tests/test_desktop_updates.py index 6e97f74..245b44c 100644 --- a/scripts/tests/test_desktop_updates.py +++ b/scripts/tests/test_desktop_updates.py @@ -10,6 +10,7 @@ import zipfile ROOT=Path(__file__).resolve().parents[2] spec=importlib.util.spec_from_file_location('desktop_update',ROOT/'desktop/NxNativeEasy/update_app.py') +assert spec is not None and spec.loader is not None update=importlib.util.module_from_spec(spec);spec.loader.exec_module(update) REVISION='a'*40 PREFIX='nxnative-easy-'+REVISION+'/' diff --git a/scripts/tests/test_game_catalog_sync.py b/scripts/tests/test_game_catalog_sync.py new file mode 100644 index 0000000..61a2666 --- /dev/null +++ b/scripts/tests/test_game_catalog_sync.py @@ -0,0 +1,77 @@ +"""New profiles must reach artwork, live catalog inventory and both README galleries.""" +import importlib.util +import json +import shutil +from pathlib import Path +import tempfile +import unittest + +ROOT=Path(__file__).resolve().parents[2] +spec=importlib.util.spec_from_file_location('catalog_sync',ROOT/'scripts/sync_game_catalog.py') +assert spec is not None and spec.loader is not None +sync=importlib.util.module_from_spec(spec);spec.loader.exec_module(sync) +ID='0100000000000001' + +class GameCatalogSync(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) + self.root=Path(self.temp.name) + (self.root/'dm/games').mkdir(parents=True) + (self.root/'docs/images').mkdir(parents=True) + (self.root/'desktop/NxNativeEasy').mkdir(parents=True) + for name in ['README.md','README-es.md']:(self.root/name).write_text('Personal intro\n\n
\nPersonal footer\n') + self.add_game(ID,'Example',image=True) + + def add_game(self,title,name,image=True): + folder=self.root/'dm/games'/title;folder.mkdir() + (folder/'README.md').write_text('# '+name) + (folder/'game.tsv').write_text(f'name\t{name}\nbuild\tv0\t1.0\n') + table=self.root/'dm/games/README.md' + with table.open('a') as out:out.write(f'| [{name}]({title}/README.md) | `{title}` |\n') + if image:(self.root/f'docs/images/{title}.png').write_bytes(b'synthetic image') + + def source(self,title=ID,name='Example'): + return f'
{name}
{name}~1 GB
' + + def test_addition_and_version_change_propagate_everywhere_and_are_idempotent(self): + sync.sync(self.root,write=True,upstream_html=self.source()) + other='0100000000000002';self.add_game(other,'Another Game') + with self.assertRaises(ValueError):sync.sync(self.root) + sync.sync(self.root,write=True,upstream_html=self.source(other,'Another Game')) + with (self.root/f'dm/games/{ID}/game.tsv').open('a') as out:out.write('build\tv1\t2.0\n') + with self.assertRaises(ValueError):sync.sync(self.root) + sync.sync(self.root,write=True) + games=json.loads((self.root/sync.CATALOG).read_text())['games'] + self.assertEqual(len(games),2) + self.assertEqual(next(g for g in games if g['id']==ID)['versions'],['1.0','2.0']) + self.assertEqual(set(json.loads((self.root/sync.ART).read_text())),{ID,other}) + for name in ['README.md','README-es.md']: + text=(self.root/name).read_text() + self.assertIn('Personal intro',text);self.assertIn('Personal footer',text) + self.assertIn(other,text);self.assertIn(ID,text) + self.assertEqual(sync.sync(self.root),[]) + + def test_missing_image_or_game_note_stops_before_writing(self): + original=(self.root/'README.md').read_text() + with self.assertRaises(ValueError):sync.sync(self.root,write=True) + self.assertEqual((self.root/'README.md').read_text(),original) + (self.root/f'dm/games/{ID}/README.md').unlink() + with self.assertRaises(ValueError):sync.outputs(self.root,self.source()) + + def test_external_image_is_rejected(self): + outside=self.root/'outside.png';outside.write_bytes(b'synthetic') + image=self.root/f'docs/images/{ID}.png';image.unlink();image.symlink_to(outside) + with self.assertRaises(ValueError):sync.outputs(self.root,self.source()) + + def test_repository_catalog_is_current(self): + self.assertEqual(sync.sync(ROOT),[]) + + def test_removed_profile_is_removed_from_every_generated_surface(self): + other='0100000000000002';self.add_game(other,'Another Game') + sync.sync(self.root,write=True,upstream_html=self.source()+self.source(other,'Another Game')) + shutil.rmtree(self.root/'dm/games'/ID) + table=self.root/'dm/games/README.md';table.write_text('\n'.join(line for line in table.read_text().splitlines() if ID not in line)+'\n') + sync.sync(self.root,write=True) + self.assertNotIn(ID,json.loads((self.root/sync.ART).read_text())) + self.assertEqual([g['id'] for g in json.loads((self.root/sync.CATALOG).read_text())['games']],[other]) + for name in ['README.md','README-es.md']:self.assertNotIn(ID,(self.root/name).read_text()) diff --git a/scripts/tests/test_personal_pipeline.py b/scripts/tests/test_personal_pipeline.py index 1b66f89..929eeb7 100644 --- a/scripts/tests/test_personal_pipeline.py +++ b/scripts/tests/test_personal_pipeline.py @@ -1,5 +1,4 @@ """Exercise the wrapper and archive splitter with synthetic packages, never game data.""" -import importlib.util import json from pathlib import Path import plistlib diff --git a/scripts/tests/test_upstream_sync.py b/scripts/tests/test_upstream_sync.py index 0140abd..be3e264 100644 --- a/scripts/tests/test_upstream_sync.py +++ b/scripts/tests/test_upstream_sync.py @@ -1,5 +1,8 @@ """Selected upstream changes stay isolated from the active checkout, even on conflicts.""" import importlib.util +import json +import sys +from unittest.mock import patch from pathlib import Path import subprocess import tempfile @@ -7,6 +10,7 @@ import unittest ROOT=Path(__file__).resolve().parents[2] spec=importlib.util.spec_from_file_location('sync_upstream',ROOT/'scripts/sync_upstream.py') +assert spec is not None and spec.loader is not None sync=importlib.util.module_from_spec(spec);spec.loader.exec_module(sync) class UpstreamSync(unittest.TestCase): @@ -58,3 +62,42 @@ class UpstreamSync(unittest.TestCase): self.assertEqual(sync.commits(self.root,self.base,self.upstream,{self.upstream})[0]['status'],'integrated') self.assertIn('graphics',sync.areas(['dm/gfx/nvn/file.c'])) self.assertIn('runtime/saves/input',sync.areas(['dm/host/hle_fs.c'])) + + def test_full_main_merge_is_isolated(self): + sync.merge_main(self.root,self.candidate,self.upstream) + self.assertEqual((self.candidate/'file').read_text(),'upstream\n') + self.assertEqual((self.root/'file').read_text(),'base\n') + self.assertEqual(sync.git(self.root,'rev-parse','HEAD'),self.base) + + def test_report_detects_new_profiles_with_versions(self): + sync.git(self.root,'switch','upstream') + folder=self.root/'dm/games/0100000000000001';folder.mkdir(parents=True) + (folder/'game.tsv').write_text('name\tExample\nbuild\tv0\t1.0\n') + self.commit('new game');head=sync.git(self.root,'rev-parse','HEAD') + sync.git(self.root,'switch','main') + change=sync.profile_changes(self.root,head) + self.assertEqual(change['added'],[dict(id='0100000000000001',name='Example',versions=['1.0'])]) + self.assertEqual(change['updated'],[]) + self.assertEqual(change['removed'],[]) + + def test_full_merge_command_passes_python_to_validation(self): + policy=Path(self.temp.name)/'policy.json' + policy.write_text(json.dumps(dict(baseline=self.base,integrated=[]))) + output=(Path(self.temp.name)/'report').resolve() + original_git=sync.git;original_run=subprocess.run + def git(root,*args): + if args==('remote','get-url','upstream'):return 'https://github.com/seedds/nxnative.git' + if args==('fetch','upstream','main'):return '' + if args==('rev-parse','FETCH_HEAD'):return self.upstream + return original_git(root,*args) + def run(command,**kwargs): + if len(command)>1 and str(command[1]).endswith('scripts/sync_game_catalog.py'): + return subprocess.CompletedProcess(command,0) + return original_run(command,**kwargs) + with patch.object(sync,'ROOT',self.root),patch.object(sync,'POLICY',policy),patch.object(sync,'git',side_effect=git), \ + patch.object(sync,'build_python',return_value=sys.executable),patch.object(sync,'validate') as check, \ + patch.object(sync.subprocess,'run',side_effect=run): + self.assertEqual(sync.main(['--output',str(output),'--merge-main']),0) + check.assert_called_once_with(output/'candidate',output,sys.executable) + self.assertEqual((self.root/'file').read_text(),'base\n') + self.assertIn('synthetic checks passed',json.loads((output/'report.json').read_text())['candidate_status'])