diff --git a/docs/docs/setup/connecting/cemu.mdx b/docs/docs/setup/connecting/cemu.mdx index 6aaba3a..ec8f629 100644 --- a/docs/docs/setup/connecting/cemu.mdx +++ b/docs/docs/setup/connecting/cemu.mdx @@ -26,21 +26,20 @@ This guide will show you how to access your server from the Cemu emulator. - Prefer using the Wii U method if you have access to a Wii U console. This method is more complicated and - requires using an account server patch that disables the console verification, which normally blocks invalid OTP - and SEEPROM dumps. **The fake OTP and SEEPROM dumps will not work on the official Pretendo server.** + Prefer using the Wii U method if you have access to a Wii U console. This method is more complicated and requires + using an account server patch that disables console verification, which normally blocks invalid OTP and SEEPROM + dumps. **The fake OTP and SEEPROM dumps will not work on the official Pretendo server.** - 1. Follow the [browser connecting guide](./browser.mdx) first. - 2. Visit [the account settings page](https://pretendo.network/account) in your proxied browser and click the - `Download account files` button. - - This button will only show up on your server. This feature was purposefully disabled on the official Pretendo - servers to prevent abuse and ban evasion. - - 3. Copy `otp.bin`, `seeprom.bin`, and `mlc01` from the downloaded ZIP file to your Cemu directory. - 4. Continue with the [official Pretendo Network Cemu installation guide](https://pretendo.network/docs/install/cemu) - to enable online mode with Pretendo. + 1. Run `./scripts/create-cemu-online-files.sh --fake-dumps ` to generate the necessary online files, including + the `account.dat` and the fake dumps. Replace `` with your PNID username, and enter the password when + prompted. + - If you already have another account in Cemu, you can use the `--persistent-id` option to change the persistent + ID (like `80000001`) of the generated `account.dat` to one that is not already in use. + 2. Copy `otp.bin`, `seeprom.bin`, and `mlc01` from the created `online-files` directory to your Cemu directory. + Merge the `mlc01` directory with your existing Cemu MLC directory if you have one. + 3. Continue with the [official Pretendo Network Cemu installation guide](https://pretendo.network/docs/install/cemu) + to enable online mode with Pretendo in the account settings. diff --git a/scripts/create-cemu-online-files.sh b/scripts/create-cemu-online-files.sh new file mode 100755 index 0000000..19f6a7a --- /dev/null +++ b/scripts/create-cemu-online-files.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This creates the necessary files for Cemu online play with Pretendo as an alternative to using Dumpling.\ +It can also create fake OTP and SEEPROM dumps, which will only work on servers with console verification disabled." +add_option "-f --force" "force" "Always overwrite the existing online files in the output directory without asking" +add_option "-d --fake-dumps" "fake_dumps" "Create fake OTP and SEEPROM dumps (with all null bytes) in addition to the account.dat file" +add_option_with_value "-i --persistent-id" "persistent_id" "id" "The persistent ID to use for the account.dat file" false "80000001" +add_option_with_value "-o --output" "output_dir" "directory" "The output directory for the online files" false "./online-files" +add_option_with_value "-p --password" "password" "password" "The password to use for the account.dat file (if not provided, you will be prompted to enter a password)" false +add_positional_argument "pnid" "pnid" "The PNID to create an account.dat file" true +parse_arguments "$@" + +if [[ -z "$password" ]]; then + printf "Enter the password for PNID $pnid: " + read -rs password + echo +fi + +account_dat_path="$output_dir/mlc01/usr/save/system/act/$persistent_id/account.dat" +otp_path="$output_dir/otp.bin" +seeprom_path="$output_dir/seeprom.bin" +if [[ -n "$fake_dumps" ]]; then + paths=("$account_dat_path" "$otp_path" "$seeprom_path") +else + paths=("$account_dat_path") +fi + +needs_confirmation=false +for path in "${paths[@]}"; do + mkdir -p "$(dirname "$path")" + if [[ -f "$path" ]]; then + print_warning "Output file $path already exists. Continuing will overwrite it!" + needs_confirmation=true + fi +done +if [[ "$needs_confirmation" == true && -z "$force" ]]; then + printf "Continue? [y/N] " + read -r continue + if [[ "$continue" != "Y" && "$continue" != "y" ]]; then + echo "Aborting." + exit 1 + fi +fi + +print_info "Generating online files for PNID $pnid..." + +compose_no_progress up -d account + +create_account_dat_script=$(cat "$git_base_dir/scripts/run-in-container/create-account-dat.js") + +docker compose exec -u root account sh -c "touch /tmp/account.dat && chmod 777 /tmp/account.dat" +run_verbose docker compose exec account node -e "$create_account_dat_script" "$pnid" "$password" "$persistent_id" + +compose_no_progress cp account:/tmp/account.dat "$account_dat_path" +docker compose exec -u root account rm -f /tmp/account.dat + +if [[ -n "$fake_dumps" ]]; then + print_info "Creating fake OTP and SEEPROM dumps..." + head -c 1024 /dev/zero >"$otp_path" + head -c 512 /dev/zero >"$seeprom_path" +fi + +print_success "Successfully generated online files for PNID $pnid." diff --git a/scripts/run-in-container/create-account-dat.js b/scripts/run-in-container/create-account-dat.js new file mode 100644 index 0000000..56d4716 --- /dev/null +++ b/scripts/run-in-container/create-account-dat.js @@ -0,0 +1,70 @@ +// This should be evaled in the account container +const fs = require("fs").promises; +const { v4: uuidv4 } = require("uuid"); +const { compare } = require("bcrypt"); +const { config } = require("./dist/config-manager"); +const { connect } = require("./dist/database"); +const { PNID } = require("./dist/models/pnid"); +const { nintendoPasswordHash } = require("./dist/util"); + +// See https://github.com/GabIsAwesome/accountfile-generator and +// https://github.com/PretendoNetwork/website/blob/99ee7ebe0aa1c2b632526f1de42f9f8b0d15940d/src/routes/account.js#L245-L284 + +async function runAsync() { + if (process.argv.length < 4) { + console.log("Usage: "); + process.exit(1); + } + + await connect(); + + const pnid = await PNID.findOne({ + usernameLower: process.argv[1].toLowerCase(), + }); + if (pnid) { + const accountDat = await generateAccountDat(pnid, process.argv[2], process.argv[3]); + await fs.writeFile(`/tmp/account.dat`, accountDat); + } else { + throw new Error(`No PNID found for username ${process.argv[1]}.`); + } +} + +runAsync().then(() => { + process.exit(0); +}); + +async function generateAccountDat(pnid, password, persistentId) { + const hashedPassword = nintendoPasswordHash(password, pnid.pid); + if (!(await compare(hashedPassword, pnid.password))) { + throw new Error("Incorrect password specified."); + } + + const [year, month, day] = pnid.birthdate.split("-"); + const birthYear = parseInt(year, 10); + const birthMonth = parseInt(month, 10); + const birthDay = parseInt(day, 10); + + let accountDat = "AccountInstance_00000000\n"; + accountDat += `PersistentId=${persistentId}\n`; + accountDat += "TransferableIdBase=0\n"; + accountDat += `Uuid=${uuidv4().replace(/-/g, "")}\n`; + accountDat += `MiiData=${Buffer.from(pnid.mii.data, "base64").toString("hex")}\n`; + accountDat += `MiiName=${Buffer.from(pnid.mii.name, "utf16le").swap16().toString("hex")}\n`; + accountDat += `AccountId=${pnid.username}\n`; + accountDat += `BirthYear=${birthYear.toString(16)}\n`; + accountDat += `BirthMonth=${birthMonth.toString(16)}\n`; + accountDat += `BirthDay=${birthDay.toString(16)}\n`; + accountDat += `Gender=${pnid.gender === "M" ? 1 : 0}\n`; + accountDat += "IsMailAddressValidated=1\n"; + accountDat += `EmailAddress=${pnid.email.address}\n`; + // No convenient way to turn the country code into the necessary numerical ID format + accountDat += "Country=0\n"; + accountDat += "SimpleAddressId=0\n"; + accountDat += `PrincipalId=${pnid.pid.toString(16)}\n`; + accountDat += "NeedsToDownloadMiiImage=1\n"; + accountDat += `MiiImageUrl=${config.cdn.base_url}/mii/${pnid.pid}/standard.tga\n`; + accountDat += "IsPasswordCacheEnabled=1\n"; + accountDat += `AccountPasswordCache=${hashedPassword}`; + + return accountDat; +}