diff --git a/.changeset/tender-crews-perform.md b/.changeset/tender-crews-perform.md
new file mode 100644
index 00000000..4ee6f4f1
--- /dev/null
+++ b/.changeset/tender-crews-perform.md
@@ -0,0 +1,5 @@
+---
+"@nx.js/ncm": patch
+---
+
+Add `@nx.js/ncm` package
diff --git a/docs/app/toggle.tsx b/docs/app/toggle.tsx
index 40b79a30..e656e29e 100644
--- a/docs/app/toggle.tsx
+++ b/docs/app/toggle.tsx
@@ -1,5 +1,6 @@
import {
TbClockBolt,
+ TbDatabaseEdit,
TbGlobe,
TbBrowser,
TbBraces,
@@ -42,6 +43,12 @@ export function Toggle() {
url: '/inspect',
icon: ,
},
+ {
+ title: '@nx.js/ncm',
+ description: 'Content manager API',
+ url: '/ncm',
+ icon: ,
+ },
{
title: '@nx.js/repl',
description: 'Read-Eval-Print Loop for nx.js',
diff --git a/docs/content/ncm/index.mdx b/docs/content/ncm/index.mdx
new file mode 100644
index 00000000..2f9c277a
--- /dev/null
+++ b/docs/content/ncm/index.mdx
@@ -0,0 +1,43 @@
+---
+title: Usage
+description: "Content Manager (ncm) service IPC wrapper for nx.js applications"
+---
+
+The `@nx.js/ncm` package provides a wrapper for the `ncm` service IPC, which
+allows for management of the Nintendo Content storages and meta databases.
+
+In short, use this package for installing titles to your Switch.
+
+## Installation
+
+
+
+
+```bash
+npm install @nx.js/ncm
+```
+
+
+
+
+```bash
+pnpm add @nx.js/ncm
+```
+
+
+
+
+```bash
+yarn add @nx.js/ncm
+```
+
+
+
+
+```bash
+bun add @nx.js/ncm
+```
+
+
+
+
diff --git a/docs/content/ncm/meta.json b/docs/content/ncm/meta.json
new file mode 100644
index 00000000..0db30f5b
--- /dev/null
+++ b/docs/content/ncm/meta.json
@@ -0,0 +1,10 @@
+{
+ "root": true,
+ "pages": [
+ "--- Documentation ---",
+ "index",
+ "...",
+ "--- API Reference ---",
+ "...api"
+ ]
+}
diff --git a/docs/package.json b/docs/package.json
index 89f969f7..ed8fd67c 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -22,6 +22,7 @@
"@nx.js/constants": "workspace:*",
"@nx.js/clkrst": "workspace:*",
"@nx.js/http": "workspace:*",
+ "@nx.js/ncm": "workspace:*",
"@nx.js/repl": "workspace:*",
"@types/mdx": "^2.0.13",
"@types/node": "20",
diff --git a/packages/ncm/package.json b/packages/ncm/package.json
new file mode 100644
index 00000000..d90316dd
--- /dev/null
+++ b/packages/ncm/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@nx.js/ncm",
+ "version": "0.0.0",
+ "description": "Content Manager (ncm) service IPC wrapper for nx.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TooTallNate/nx.js.git",
+ "directory": "packages/ncm"
+ },
+ "homepage": "https://nxjs.n8.io/ncm",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc",
+ "test": "vitest",
+ "docs": "typedoc && ../../type-aliases-meta.sh"
+ },
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@nx.js/constants": "^0.3.0"
+ },
+ "devDependencies": {
+ "@nx.js/runtime": "workspace:*",
+ "vite": "^5.4.2",
+ "vitest": "^2.0.5"
+ },
+ "keywords": [
+ "nx.js",
+ "switch",
+ "ncm",
+ "content",
+ "manager"
+ ],
+ "author": "Nathan Rajlich ",
+ "license": "MIT"
+}
diff --git a/packages/ncm/src/content-meta-database.ts b/packages/ncm/src/content-meta-database.ts
new file mode 100644
index 00000000..ee8d6594
--- /dev/null
+++ b/packages/ncm/src/content-meta-database.ts
@@ -0,0 +1,141 @@
+import { SfBufferAttr } from '@nx.js/constants';
+import { ncm } from './service';
+import type {
+ NcmContentId,
+ NcmContentMetaKey,
+ NcmContentType,
+ NcmStorageId,
+} from './types';
+
+export class NcmContentMetaDatabase {
+ static open(storageId: NcmStorageId) {
+ //Result ncmOpenContentMetaDatabase(NcmContentMetaDatabase* out_content_meta_database, NcmStorageId storage_id) {
+ // return serviceDispatchIn(&g_ncmSrv, 5, storage_id,
+ // .out_num_objects = 1,
+ // .out_objects = out_content_meta_database,
+ // );
+ //}
+ const out = new Switch.Service();
+ const inArr = new Uint8Array([storageId]);
+ ncm.dispatchIn(5, inArr.buffer, {
+ outObjects: [out],
+ });
+ return new NcmContentMetaDatabase(out);
+ }
+
+ #srv: Switch.Service;
+
+ /**
+ * @private
+ */
+ constructor(srv: Switch.Service) {
+ this.#srv = srv;
+ }
+
+ set(key: NcmContentMetaKey, data: ArrayBuffer) {
+ //Result ncmContentMetaDatabaseSet(NcmContentMetaDatabase* db, const NcmContentMetaKey* key, const void* data, u64 data_size) {
+ // return serviceDispatchIn(&db->s, 0, *key,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
+ // .buffers = { { data, data_size } },
+ // );
+ //}
+ this.#srv.dispatchIn(0, key, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.In],
+ buffers: [data],
+ });
+ }
+
+ get(key: NcmContentMetaKey, data: ArrayBuffer): bigint {
+ //Result ncmContentMetaDatabaseGet(NcmContentMetaDatabase* db, const NcmContentMetaKey* key, u64* out_size, void* out_data, u64 out_data_size) {
+ // return serviceDispatchInOut(&db->s, 1, *key, *out_size,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
+ // .buffers = { { out_data, out_data_size } },
+ // );
+ //}
+ const out = new BigUint64Array(1);
+ this.#srv.dispatchInOut(1, key, out.buffer, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.Out],
+ buffers: [data],
+ });
+ return out[0];
+ }
+
+ delete(key: NcmContentMetaKey) {
+ //Result ncmContentMetaDatabaseRemove(NcmContentMetaDatabase* db, const NcmContentMetaKey *key) {
+ // return serviceDispatchIn(&db->s, 2, *key);
+ //}
+ this.#srv.dispatchIn(2, key);
+ }
+
+ getContentIdByType(
+ key: NcmContentMetaKey,
+ type: NcmContentType,
+ ): NcmContentId {
+ //Result ncmContentMetaDatabaseGetContentIdByType(NcmContentMetaDatabase* db, NcmContentId* out_content_id, const NcmContentMetaKey* key, NcmContentType type) {
+ // const struct {
+ // u8 type;
+ // u8 padding[7];
+ // NcmContentMetaKey key;
+ // } in = { type, {0}, *key };
+ // return serviceDispatchInOut(&db->s, 3, in, *out_content_id);
+ //}
+ const inData = new ArrayBuffer(0x18);
+ const inArr = new Uint8Array(inData);
+ inArr[0] = type;
+ inArr.set(new Uint8Array(key), 0x8);
+ const out = new ArrayBuffer(0x10);
+ this.#srv.dispatchInOut(3, inData, out);
+ return out as NcmContentId;
+ }
+
+ has(key: NcmContentMetaKey): boolean {
+ //Result ncmContentMetaDatabaseHas(NcmContentMetaDatabase* db, bool* out, const NcmContentMetaKey* key) {
+ // u8 tmp=0;
+ // Result rc = serviceDispatchInOut(&db->s, 8, *key, tmp);
+ // if (R_SUCCEEDED(rc) && out) *out = tmp & 1;
+ // return rc;
+ //}
+ const out = new Uint8Array(1);
+ this.#srv.dispatchInOut(8, key, out.buffer);
+ return Boolean(out[0] & 1);
+ }
+
+ hasAll(keys: NcmContentMetaKey[]): boolean {
+ //Result ncmContentMetaDatabaseHasAll(NcmContentMetaDatabase* db, bool* out, const NcmContentMetaKey* keys, s32 count) {
+ // u8 tmp=0;
+ // Result rc = serviceDispatchOut(&db->s, 9, *out,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
+ // .buffers = { { keys, count*sizeof(NcmContentMetaKey) } },
+ // );
+ // if (R_SUCCEEDED(rc) && out) *out = tmp & 1;
+ // return rc;
+ //}
+ const inData = new ArrayBuffer(keys.length * 0x10);
+ const inArr = new Uint8Array(inData);
+ for (let i = 0; i < keys.length; i++) {
+ inArr.set(new Uint8Array(keys[i]), i * 0x10);
+ }
+ const out = new Uint8Array(1);
+ this.#srv.dispatchOut(9, out.buffer, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.In],
+ buffers: [inData],
+ });
+ return Boolean(out[0] & 1);
+ }
+
+ getSize(key: NcmContentMetaKey): bigint {
+ //Result ncmContentMetaDatabaseGetSize(NcmContentMetaDatabase* db, u64* out_size, const NcmContentMetaKey* key) {
+ // return serviceDispatchInOut(&db->s, 10, *key, *out_size);
+ //}
+ const out = new BigUint64Array(1);
+ this.#srv.dispatchInOut(10, key, out.buffer);
+ return out[0];
+ }
+
+ commit() {
+ //Result ncmContentMetaDatabaseCommit(NcmContentMetaDatabase* db) {
+ // return _ncmCmdNoIO(&db->s, 15);
+ //}
+ this.#srv.dispatch(15);
+ }
+}
diff --git a/packages/ncm/src/content-storage.ts b/packages/ncm/src/content-storage.ts
new file mode 100644
index 00000000..1076c293
--- /dev/null
+++ b/packages/ncm/src/content-storage.ts
@@ -0,0 +1,326 @@
+import { SfBufferAttr } from '@nx.js/constants';
+import { ncm } from './service';
+import type { NcmContentId, NcmPlaceHolderId, NcmStorageId } from './types';
+
+export class NcmContentStorage {
+ static open(storageId: NcmStorageId) {
+ //Result ncmOpenContentStorage(NcmContentStorage* out_content_storage, NcmStorageId storage_id) {
+ // return serviceDispatchIn(&g_ncmSrv, 4, storage_id,
+ // .out_num_objects = 1,
+ // .out_objects = out_content_storage,
+ // );
+ //}
+ const out = new Switch.Service();
+ const inArr = new Uint8Array([storageId]);
+ ncm.dispatchIn(4, inArr.buffer, {
+ outObjects: [out],
+ });
+ return new NcmContentStorage(out);
+ }
+
+ #srv: Switch.Service;
+
+ /**
+ * @private
+ */
+ constructor(srv: Switch.Service) {
+ this.#srv = srv;
+ }
+
+ generatePlaceHolderId(): NcmPlaceHolderId {
+ //Result ncmContentStorageGeneratePlaceHolderId(NcmContentStorage* cs, NcmPlaceHolderId* out_id) {
+ // return serviceDispatchOut(&cs->s, 0, *out_id);
+ //}
+ const uuid = new ArrayBuffer(0x10);
+ this.#srv.dispatchOut(0, uuid);
+ return uuid as NcmPlaceHolderId;
+ }
+
+ createPlaceHolder(
+ contentId: NcmContentId,
+ placeholderId: NcmPlaceHolderId,
+ size: bigint,
+ ) {
+ //Result ncmContentStorageCreatePlaceHolder(NcmContentStorage* cs, const NcmContentId* content_id, const NcmPlaceHolderId* placeholder_id, s64 size) {
+ // if (hosversionBefore(16,0,0)) {
+ // const struct {
+ // NcmContentId content_id;
+ // NcmPlaceHolderId placeholder_id;
+ // s64 size;
+ // } in = { *content_id, *placeholder_id, size };
+ // return serviceDispatchIn(&cs->s, 1, in);
+ // } else {
+ // const struct {
+ // NcmPlaceHolderId placeholder_id;
+ // NcmContentId content_id;
+ // s64 size;
+ // } in = { *placeholder_id, *content_id, size };
+ // return serviceDispatchIn(&cs->s, 1, in);
+ // }
+ //}
+ const inData = new ArrayBuffer(0x28);
+ const inDataView = new Uint8Array(inData);
+ inDataView.set(new Uint8Array(contentId), 0);
+ inDataView.set(new Uint8Array(placeholderId), 0x10);
+ new BigInt64Array(inData, 0x20, 1)[0] = size;
+ this.#srv.dispatchIn(1, inData);
+ }
+
+ deletePlaceHolder(placeholderId: NcmPlaceHolderId) {
+ //Result ncmContentStorageDeletePlaceHolder(NcmContentStorage* cs, const NcmPlaceHolderId* placeholder_id) {
+ // return _ncmCmdInPlaceHolderId(&cs->s, placeholder_id, 2);
+ //}
+ this.#srv.dispatchIn(2, placeholderId);
+ }
+
+ hasPlaceHolder(placeholderId: NcmPlaceHolderId): boolean {
+ //Result ncmContentStorageHasPlaceHolder(NcmContentStorage* cs, bool* out, const NcmPlaceHolderId* placeholder_id) {
+ // u8 tmp=0;
+ // Result rc = serviceDispatchInOut(&cs->s, 3, *placeholder_id, tmp);
+ // if (R_SUCCEEDED(rc) && out) *out = tmp & 1;
+ // return rc;
+ //}
+ const out = new Uint8Array(1);
+ this.#srv.dispatchInOut(3, placeholderId, out.buffer);
+ return Boolean(out[0] & 1);
+ }
+
+ writePlaceHolder(
+ placeholderId: NcmPlaceHolderId,
+ offset: bigint,
+ data: ArrayBuffer,
+ ) {
+ //Result ncmContentStorageWritePlaceHolder(NcmContentStorage* cs, const NcmPlaceHolderId* placeholder_id, u64 offset, const void* data, size_t data_size) {
+ // const struct {
+ // NcmPlaceHolderId placeholder_id;
+ // u64 offset;
+ // } in = { *placeholder_id, offset };
+ // return serviceDispatchIn(&cs->s, 4, in,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
+ // .buffers = { { data, data_size } },
+ // );
+ //}
+ const inData = new ArrayBuffer(0x18);
+ const inDataView = new Uint8Array(inData);
+ inDataView.set(new Uint8Array(placeholderId), 0);
+ new BigUint64Array(inData, 0x10, 1)[0] = offset;
+ this.#srv.dispatchIn(4, inData, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.In],
+ buffers: [data],
+ });
+ }
+
+ register(contentId: NcmContentId, placeholderId: NcmPlaceHolderId) {
+ //Result ncmContentStorageRegister(NcmContentStorage* cs, const NcmContentId* content_id, const NcmPlaceHolderId* placeholder_id) {
+ // if (hosversionBefore(16,0,0)) {
+ // const struct {
+ // NcmContentId content_id;
+ // NcmPlaceHolderId placeholder_id;
+ // } in = { *content_id, *placeholder_id };
+ // return serviceDispatchIn(&cs->s, 5, in);
+ // } else {
+ // const struct {
+ // NcmPlaceHolderId placeholder_id;
+ // NcmContentId content_id;
+ // } in = { *placeholder_id, *content_id };
+ // return serviceDispatchIn(&cs->s, 5, in);
+ // }
+ //}
+ const inData = new ArrayBuffer(0x20);
+ const inDataView = new Uint8Array(inData);
+ inDataView.set(new Uint8Array(placeholderId), 0);
+ inDataView.set(new Uint8Array(contentId), 0x10);
+ this.#srv.dispatchIn(5, inData);
+ }
+
+ delete(contentId: NcmContentId) {
+ //Result ncmContentStorageDelete(NcmContentStorage* cs, const NcmContentId* content_id) {
+ // return _ncmCmdInContentId(&cs->s, content_id, 6);
+ //}
+ this.#srv.dispatchIn(6, contentId);
+ }
+
+ has(contentId: NcmContentId): boolean {
+ //Result ncmContentStorageHas(NcmContentStorage* cs, bool* out, const NcmContentId* content_id) {
+ // u8 tmp=0;
+ // Result rc = serviceDispatchInOut(&cs->s, 7, *content_id, tmp);
+ // if (R_SUCCEEDED(rc) && out) *out = tmp & 1;
+ // return rc;
+ //}
+ const out = new Uint8Array(1);
+ this.#srv.dispatchInOut(7, contentId, out.buffer);
+ return Boolean(out[0] & 1);
+ }
+
+ getPath(contentId: NcmContentId): string {
+ //Result ncmContentStorageGetPath(NcmContentStorage* cs, char* out_path, size_t out_size, const NcmContentId* content_id) {
+ // char tmpbuf[0x300]={0};
+ // Result rc = serviceDispatchIn(&cs->s, 8, *content_id,
+ // .buffer_attrs = { SfBufferAttr_FixedSize | SfBufferAttr_HipcPointer | SfBufferAttr_Out },
+ // .buffers = { { tmpbuf, sizeof(tmpbuf) } },
+ // );
+ // if (R_SUCCEEDED(rc) && out_path) {
+ // strncpy(out_path, tmpbuf, out_size-1);
+ // out_path[out_size-1] = 0;
+ // }
+ // return rc;
+ //}
+ const out = new ArrayBuffer(0x300);
+ this.#srv.dispatchIn(8, contentId, {
+ bufferAttrs: [
+ SfBufferAttr.FixedSize | SfBufferAttr.HipcPointer | SfBufferAttr.Out,
+ ],
+ buffers: [out],
+ });
+ const nul = new Uint8Array(out).indexOf(0);
+ return new TextDecoder().decode(out.slice(0, nul));
+ }
+
+ getPlaceHolderPath(placeholderId: NcmPlaceHolderId): string {
+ //Result ncmContentStorageGetPlaceHolderPath(NcmContentStorage* cs, char* out_path, size_t out_size, const NcmPlaceHolderId* placeholder_id) {
+ // char tmpbuf[0x300]={0};
+ // Result rc = serviceDispatchIn(&cs->s, 9, *placeholder_id,
+ // .buffer_attrs = { SfBufferAttr_FixedSize | SfBufferAttr_HipcPointer | SfBufferAttr_Out },
+ // .buffers = { { tmpbuf, sizeof(tmpbuf) } },
+ // );
+ // if (R_SUCCEEDED(rc) && out_path) {
+ // strncpy(out_path, tmpbuf, out_size-1);
+ // out_path[out_size-1] = 0;
+ // }
+ // return rc;
+ //}
+ const out = new ArrayBuffer(0x300);
+ this.#srv.dispatchIn(9, placeholderId, {
+ bufferAttrs: [
+ SfBufferAttr.FixedSize | SfBufferAttr.HipcPointer | SfBufferAttr.Out,
+ ],
+ buffers: [out],
+ });
+ const nul = new Uint8Array(out).indexOf(0);
+ return new TextDecoder().decode(out.slice(0, nul));
+ }
+
+ cleanupAllPlaceHolder() {
+ //Result ncmContentStorageCleanupAllPlaceHolder(NcmContentStorage * cs) {
+ // return _ncmCmdNoIO(& cs -> s, 10);
+ //}
+ this.#srv.dispatch(10);
+ }
+
+ listPlaceHolder(maxCount = 20): NcmPlaceHolderId[] {
+ //Result ncmContentStorageListPlaceHolder(NcmContentStorage* cs, NcmPlaceHolderId* out_ids, s32 count, s32* out_count) {
+ // return serviceDispatchOut(&cs->s, 11, *out_count,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
+ // .buffers = { { out_ids, count*sizeof(NcmPlaceHolderId) } },
+ // );
+ //}
+ const outIds = new ArrayBuffer(maxCount * 0x10);
+ const out = new Int32Array(1);
+ this.#srv.dispatchOut(11, out.buffer, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.Out],
+ buffers: [outIds],
+ });
+ const outCount = out[0];
+ const rtn: NcmPlaceHolderId[] = new Array(outCount);
+ for (let i = 0; i < outCount; i++) {
+ rtn[i] = outIds.slice(i * 0x10, i * 0x10 + 0x10) as NcmPlaceHolderId;
+ }
+ return rtn;
+ }
+
+ getContentCount(): number {
+ //Result ncmContentStorageGetContentCount(NcmContentStorage* cs, s32* out_count) {
+ // return serviceDispatchOut(&cs->s, 12, *out_count);
+ //}
+ const out = new Int32Array(1);
+ this.#srv.dispatchOut(12, out.buffer);
+ return out[0];
+ }
+
+ listContentId(
+ startOffset = 0,
+ maxCount = this.getContentCount(),
+ ): NcmContentId[] {
+ //Result ncmContentStorageListContentId(NcmContentStorage* cs, NcmContentId* out_ids, s32 count, s32* out_count, s32 start_offset) {
+ // return serviceDispatchInOut(&cs->s, 13, start_offset, *out_count,
+ // .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
+ // .buffers = { { out_ids, count*sizeof(NcmContentId) } },
+ // );
+ //}
+ const inData = new Int32Array([startOffset]);
+ const outIds = new ArrayBuffer(maxCount * 0x10);
+ const out = new Int32Array(1);
+ this.#srv.dispatchInOut(13, inData.buffer, out.buffer, {
+ bufferAttrs: [SfBufferAttr.HipcMapAlias | SfBufferAttr.Out],
+ buffers: [outIds],
+ });
+ const outCount = out[0];
+ const rtn: NcmContentId[] = new Array(outCount);
+ for (let i = 0; i < outCount; i++) {
+ rtn[i] = outIds.slice(i * 0x10, i * 0x10 + 0x10) as NcmContentId;
+ }
+ return rtn;
+ }
+
+ getSizeFromContentId(contentId: NcmContentId): bigint {
+ //Result ncmContentStorageGetSizeFromContentId(NcmContentStorage* cs, s64* out_size, const NcmContentId* content_id) {
+ // return _ncmCmdInContentIdOutU64(&cs->s, content_id, (u64*)out_size, 14);
+ //}
+ const out = new BigInt64Array(1);
+ this.#srv.dispatchInOut(14, contentId, out.buffer);
+ return out[0];
+ }
+
+ disableForcibly() {
+ //Result ncmContentStorageDisableForcibly(NcmContentStorage* cs) {
+ // return _ncmCmdNoIO(&cs->s, 15);
+ //}
+ this.#srv.dispatch(15);
+ }
+
+ get freeSpace(): bigint {
+ //Result ncmContentStorageGetFreeSpaceSize(NcmContentStorage* cs, s64* out_size) {
+ // if (hosversionBefore(2,0,0)) return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
+ // return _ncmCmdNoInOutU64(&cs->s, (u64*)out_size, 22);
+ //}
+ const out = new BigInt64Array(1);
+ this.#srv.dispatchOut(22, out.buffer);
+ return out[0];
+ }
+
+ get totalSpace(): bigint {
+ //Result ncmContentStorageGetTotalSpaceSize(NcmContentStorage* cs, s64* out_size) {
+ // if (hosversionBefore(2,0,0)) return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
+ // return _ncmCmdNoInOutU64(&cs->s, (u64*)out_size, 23);
+ //}
+ const out = new BigInt64Array(1);
+ this.#srv.dispatchOut(23, out.buffer);
+ return out[0];
+ }
+
+ flushPlaceHolder() {
+ //Result ncmContentStorageFlushPlaceHolder(NcmContentStorage* cs) {
+ // if (hosversionBefore(3,0,0)) return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
+ // return _ncmCmdNoIO(&cs->s, 24);
+ //}
+ this.#srv.dispatch(24);
+ }
+
+ getSizeFromPlaceHolderId(placeholderId: NcmPlaceHolderId): bigint {
+ //Result ncmContentStorageGetSizeFromPlaceHolderId(NcmContentStorage* cs, s64* out_size, const NcmPlaceHolderId* placeholder_id) {
+ // if (hosversionBefore(4,0,0)) return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
+ // return _ncmCmdInPlaceHolderIdOutU64(&cs->s, placeholder_id, (u64*)out_size, 25);
+ //}
+ const out = new BigInt64Array(1);
+ this.#srv.dispatchInOut(25, placeholderId, out.buffer);
+ return out[0];
+ }
+
+ repairInvalidFileAttribute() {
+ //Result ncmContentStorageRepairInvalidFileAttribute(NcmContentStorage* cs) {
+ // if (hosversionBefore(4,0,0)) return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
+ // return _ncmCmdNoIO(&cs->s, 26);
+ //}
+ this.#srv.dispatch(26);
+ }
+}
diff --git a/packages/ncm/src/index.ts b/packages/ncm/src/index.ts
new file mode 100644
index 00000000..b9d2ac1a
--- /dev/null
+++ b/packages/ncm/src/index.ts
@@ -0,0 +1,38 @@
+import { ncm } from './service';
+import type { NcmStorageId } from './types';
+
+export * from './types';
+export * from './content-storage';
+export * from './content-meta-database';
+
+export function createContentStorage(storageId: NcmStorageId) {
+ //Result ncmCreateContentStorage(NcmStorageId storage_id) {
+ // return _ncmCmdInU8(&g_ncmSrv, storage_id, 0);
+ //}
+ const inData = new Uint8Array([storageId]);
+ ncm.dispatchIn(0, inData.buffer);
+}
+
+export function createContentMetaDatabase(storageId: NcmStorageId) {
+ //Result ncmCreateContentMetaDatabase(NcmStorageId storage_id) {
+ // return _ncmCmdInU8(&g_ncmSrv, storage_id, 1);
+ //}
+ const inData = new Uint8Array([storageId]);
+ ncm.dispatchIn(1, inData.buffer);
+}
+
+export function verifyContentStorage(storageId: NcmStorageId) {
+ //Result ncmVerifyContentStorage(NcmStorageId storage_id) {
+ // return _ncmCmdInU8(&g_ncmSrv, storage_id, 2);
+ //}
+ const inData = new Uint8Array([storageId]);
+ ncm.dispatchIn(2, inData.buffer);
+}
+
+export function verifyContentMetaDatabase(storageId: NcmStorageId) {
+ //Result ncmVerifyContentMetaDatabase(NcmStorageId storage_id) {
+ // return _ncmCmdInU8(&g_ncmSrv, storage_id, 3);
+ //}
+ const inData = new Uint8Array([storageId]);
+ ncm.dispatchIn(3, inData.buffer);
+}
diff --git a/packages/ncm/src/service.ts b/packages/ncm/src/service.ts
new file mode 100644
index 00000000..3bffa726
--- /dev/null
+++ b/packages/ncm/src/service.ts
@@ -0,0 +1 @@
+export const ncm = new Switch.Service('ncm');
diff --git a/packages/ncm/src/types.ts b/packages/ncm/src/types.ts
new file mode 100644
index 00000000..48d67c85
--- /dev/null
+++ b/packages/ncm/src/types.ts
@@ -0,0 +1,85 @@
+/// StorageId
+export enum NcmStorageId {
+ None = 0, ///< None
+ Host = 1, ///< Host
+ GameCard = 2, ///< GameCard
+ BuiltInSystem = 3, ///< BuiltInSystem
+ BuiltInUser = 4, ///< BuiltInUser
+ SdCard = 5, ///< SdCard
+ Any = 6, ///< Any
+}
+
+/// ContentType
+export enum NcmContentType {
+ Meta = 0, ///< Meta
+ Program = 1, ///< Program
+ Data = 2, ///< Data
+ Control = 3, ///< Control
+ HtmlDocument = 4, ///< HtmlDocument
+ LegalInformation = 5, ///< LegalInformation
+ DeltaFragment = 6, ///< DeltaFragment
+}
+
+/// ContentMetaType
+export enum NcmContentMetaType {
+ Unknown = 0x0, ///< Unknown
+ SystemProgram = 0x1, ///< SystemProgram
+ SystemData = 0x2, ///< SystemData
+ SystemUpdate = 0x3, ///< SystemUpdate
+ BootImagePackage = 0x4, ///< BootImagePackage
+ BootImagePackageSafe = 0x5, ///< BootImagePackageSafe
+ Application = 0x80, ///< Application
+ Patch = 0x81, ///< Patch
+ AddOnContent = 0x82, ///< AddOnContent
+ Delta = 0x83, ///< Delta
+ DataPatch = 0x84, ///< DataPatch
+}
+NcmContentMetaType;
+
+/// ContentMetaAttribute
+export enum NcmContentMetaAttribute {
+ None = 0, ///< None
+ IncludesExFatDriver = 1 << 0, ///< IncludesExFatDriver
+ Rebootless = 1 << 1, ///< Rebootless
+ Compacted = 1 << 2, ///< Compacted
+}
+
+/// ContentInstallType
+export enum NcmContentInstallType {
+ Full = 0, ///< Full
+ FragmentOnly = 1, ///< FragmentOnly
+ Unknown = 7, ///< Unknown
+}
+
+/// ContentMetaPlatform
+export enum NcmContentMetaPlatform {
+ Nx = 0, ///< Nx
+}
+
+/// ContentId
+//typedef struct {
+// u8 c[0x10]; ///< Id
+//} NcmContentId;
+declare const contentIdSymbol: unique symbol;
+export type NcmContentId = ArrayBuffer & {
+ [contentIdSymbol]: void;
+};
+
+/// PlaceHolderId
+//typedef struct {
+// Uuid uuid; ///< UUID
+//} NcmPlaceHolderId;
+declare const placeHolderIdSymbol: unique symbol;
+export type NcmPlaceHolderId = ArrayBuffer & {
+ [placeHolderIdSymbol]: void;
+};
+
+/// ContentMetaKey
+//typedef struct {
+// u64 id; ///< Id.
+// u32 version; ///< Version.
+// u8 type; ///< \ref NcmContentMetaType
+// u8 install_type; ///< \ref NcmContentInstallType
+// u8 padding[2]; ///< Padding.
+//} NcmContentMetaKey;
+export type NcmContentMetaKey = ArrayBuffer;
diff --git a/packages/ncm/tsconfig.json b/packages/ncm/tsconfig.json
new file mode 100644
index 00000000..60215a01
--- /dev/null
+++ b/packages/ncm/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "target": "es2022",
+ "declaration": true,
+ "sourceMap": true,
+ "moduleResolution": "Bundler",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": [
+ "@nx.js/runtime"
+ ]
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/packages/ncm/typedoc.json b/packages/ncm/typedoc.json
new file mode 100644
index 00000000..291328c8
--- /dev/null
+++ b/packages/ncm/typedoc.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "extends": ["../../typedoc.json"],
+ "name": "@nx.js/ncm",
+ "entryPoints": ["./src/index.ts"],
+ "out": "docs"
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 576dd3a1..4df7da27 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -464,6 +464,9 @@ importers:
'@nx.js/http':
specifier: workspace:*
version: link:../packages/http
+ '@nx.js/ncm':
+ specifier: workspace:*
+ version: link:../packages/ncm
'@nx.js/repl':
specifier: workspace:*
version: link:../packages/repl
@@ -580,6 +583,22 @@ importers:
specifier: ^2.0.5
version: 2.0.5
+ packages/ncm:
+ dependencies:
+ '@nx.js/constants':
+ specifier: ^0.3.0
+ version: link:../constants
+ devDependencies:
+ '@nx.js/runtime':
+ specifier: workspace:*
+ version: link:../runtime
+ vite:
+ specifier: ^5.4.2
+ version: 5.4.2
+ vitest:
+ specifier: ^2.0.5
+ version: 2.0.5
+
packages/nro:
dependencies:
'@nx.js/patch-nacp':