mythra
@mythra

I wanted my first post to be be talking about something I found interesting, and hopefully something that people could actually learn from. Since I probably want this to be a ~bit more structured with things rather than twitter? I may change that later on, but no harm in starting that way! So I figured... Let's talk about Wii Friend Codes!

I hope this is interesting to folks, and if not, well not a good start I guess.

note: I assume you know what a DS & Wii are for the purpose of this post, if you're not familiar with them -- they're gaming consoles released by Nintendo in 2004, and 2006 respectively.

DS Friend Codes

So the Nintendo DS, I realize that's not a Wii, and this is a post about Wii friend codes. However, just bare with me for a split second, I promise this will be relevant.

When developing online support for the Nintendo DS though Nintendo ran into a problem. See, Nintendo very much envisioned them as a shared console within families. They only wanted you to buy one DS for an entire family (why should a family have to buy two)? Which is actually kind-of nice of them, but it means you can't have a "single identity" throughout an entire console, you very much need multiple identities. After-all people would get pretty upset if their little brother traded their most valuable Pokemon, or tanked their ranking on a competitive game.

However, the original DS didn't actually have the space necessary to support profile features, like all future consoles would for Nintendo. In fact the original DS didn't even have enough room to have a console wide 'Wi-Fi connection' screen, instead games were going to have to implement it on their game-cart as part of the SDK, and all write to the same shared place so Wi-Fi settings would be shared. yes, depending on the printed version of your game-cart you will get different connection-test/connection screens. Even for the same game. So, what do you do?

Well Nintendo figured "We've got these game-carts which can only contain one save file. Why not just store... our online information to a game cart as if it were save file data?" After-all if parents wanted kids to share a console, they'd still have to buy multiple games one for each kid, if they wanted separate progress. So why over-complicate it by adding anything, and have yet another thing that needed to be aware of multiple identities?

So they ended up with an implementation where every game is going to boot up and get a new 'profile id' from an online service, which is just an unsigned number counting up forever. Great! However, if we just show kids a number like a profile-id... the chance of mistyping it especially on the tiny keyboard is relatively high. Plus with just a number incrementing forever, it can be very easy to misremember/mis-type, and still hit a valid profile somewhere. Also what happens if someone tries to type in the code for another game? I mean after-all just because Nintendo knows you get a unique profile per game, doesn't mean every kid knows the same.

So taking these problems into account, Nintendo invented well.... Friend Codes. Although the friend code algorithm changed depending on the version of the SDK you were using, they all roughly boil down to something like the following:

  1. Allocate a 64 bit number as all 0's. We'll call this buff.
  2. Set the first 32 bits of buff to the profile_id you registered.
  3. Set the last 32 bits of buff to "GAME_ID4" of the game you are playing (the "ID4" is a unique identifier for the DS game, if you look at a DS cartridge it's the 4 numbers in the middle, and unique for every single Game, e.g. Pokemon HeartGold US release is 'IPGE').
  4. Calculate a CRC8 of buff, and take only the lower 7 bits (this helps put a cap on the 'highest number' you can generate so friend codes are always less than or equal to 12 chars).
  5. Your friend code is now also a 64 bit number, where the highest 32 bits are 'checksum', and the lower 32 bits are the profile_id.
  6. If displaying to the user, make sure the number is at least 12 characters long when displayed in decimal, if it's not prepend 0's until it is.
  7. Add dashes in-between every 4 characters.
A Rust PoC for those interested
#[must_use]
pub fn friend_code(game_id4: [char; 4], profile_id: u32) -> String {
  let mut buff = [0_u8; 8];
  let pid_le = profile_id.to_le_bytes();
  buff[0] = pid_le[0]; buff[1] = pid_le[1]; buff[2] = pid_le[2]; buff[3] = pid_le[3];
  buff[4] = game_id4[0] as u8; buff[5] = game_id4[1] as u8; buff[6] = game_id4[2] as u8; buff[7] = game_id4[3] as u8;
  let checksum = calculate_7bit_crc(&buff);
  let friend_code = (u64::from(checksum) << 32_u64) | u64::from(profile_id);
  displayable_friend_code(friend_code)
}

#[must_use]
pub fn displayable_friend_code(encoded: u64) -> String {
  let raw_fc = format!("{}", encoded);
  let mut result = String::new();
  let mut added_dashes = 0;

  for _ in 0..(12 - raw_fc.len()) {
    result.push('0');
    if (result.len() - added_dashes) % 4 == 0 {
      added_dashes += 1;
      result.push('-');
    }
  }

  let raw_len = raw_fc.len();
  for (idx, character) in raw_fc.chars().enumerate() {
    result.push(character);
    if (result.len() - added_dashes) % 4 == 0 && idx + 1 != raw_len {
      added_dashes += 1;
      result.push('-');
    }
  }

  result
}

#[must_use]
pub const fn calculate_7bit_crc(data: &[u8]) -> u8 {
	let mut crc = 0;
	let mut idx = 0;
	while idx < data.len() {
		crc = (&CRC8_TABLE)[(data[idx] ^ crc) as usize];
		idx += 1;
	}
	crc & 0x7F
}

#[allow(
	// This is an okay truncation.
	clippy::cast_possible_truncation,
)]
const fn generate_crc_table() -> [u8; 256] {
	let mut table = [0_u8; 256];

	let mut table_idx = 0_u16;
	while table_idx < 256_u16 {
		let mut base = table_idx as u8;

		let mut second_idx = 0_u8;
		while second_idx < 8_u8 {
			if base & 0x80 == 0 {
				base <<= 1;
			} else {
				base <<= 1;
				base ^= 7;
			}
			second_idx += 1;
		}

		table[table_idx as usize] = base;
		table_idx += 1;
	}

	table
}
const CRC8_TABLE: [u8; 256] = generate_crc_table();

Congrats! You now have a friend code, with a built-in checksum to catch typo's, and confirmation that you're adding a friend code from the game you want to be adding from! Yay! This meant they couldn't do anything globally across the whole console, but that's fine. The DS wasn't powerful enough for any thing like that anyway, so we can just move on.

The Wii

However, when Nintendo got to the Wii they ran into a bit of a problem. Not only did they have a console with much more room, and power because it wasn't a handheld, they actually wanted to take advantage of it. They wanted you to be able to send mail to an entire Wii console! Uh-Oh! We don't have a way of getting a global identity though!

I guess we could just add the equivalent of a 'GAME_ID4' to represent the "Wii System", but that means everyone is bottle-necked on the exact same path as they setup the Wii! If you have a large launch like you're hoping for, or ever go offline no one can setup their Wii. For games this is fine, because if the system goes offline, no one is going to be able to play anyway. We don't want it to block System Setup though because then you can't play any games, even those that are offline only.

Well let's take a look at friend-codes as they exist. They have a lot of nice properties: they're relatively typo-proof, they're 'hard to guess' so spamming is hard (at least in this point of time because the algorithm's aren't well known), and folks don't have to to fight over usernames. Unfortunately though, they're really hard to remember. I mean after-all lots of the player-base has let them know the disdain for it from the beginning.

As a result, Nintendo decided "what if we have the same type of system generating a number, but rather than using Game Data, we just use system data". Congrats! You've just invented a Wii Friend Code! Specifically Nintendo used the following pieces of data to generate friend codes:

  • "Hollywood Chip ID" (the Hollywood chip is the name of the CPU for the Wii.)
  • "Reset Counter" (up to 32, This way if you need to 'resell' a wii, you can reset it, and not leak someone's friend code. At least up to 32 times, after 32 it just repeats.)
  • "Hardware Model" (if this is a Development Wii, Production Wii, etc.)
  • "Console Region" (gotta love region-locking!)

Now we just throw these all in a pot, and add a CRC. Boom! We can now generate a friend-code entirely offline, Wii's can be resold without sharing friend-code's safely, and we get some level of sharding between development consoles & production consoles.

I just find this incredibly cool globally unique console IDs generated fully offline, and it's possible to figure out all the information about a Wii purely from a friend-code. Want to know how many times a Wii you're buying has been resold before? Just grab the friend code, and decode it! Are a support rep and want to lookup all the parts that went into a Wii? Grab the friend code, and lookup the hollywood chip id!

Unfortunately the "Wii Friend Code" algorithm is a lot more complex, and to be honest I don't have quite the knowledge of all the bit-twiddling magic to fully understand it. However, I do know roughly it is the same idea, throwing all the data into a 64 bit number, generate a checksum, and turning that into a number that is your friend code. Small implementations exist: HERE.

I also have a PoC Rust implementation here
use std::num::ParseIntError;
use thiserror::Error;

/// All of the information about a Wii Chip.
#[derive(Debug, PartialEq, Eq)]
pub struct WiiChip {
	hollywood_id: u32,
	reset_counter: u16,
	hardware_model: HardwareModel,
	area_code: ConsoleRegion,
	checksum: u16,
}

impl WiiChip {
	#[must_use]
	pub const fn new(
		hollywood_id: u32,
		reset_count: u16,
		hardware_model: HardwareModel,
		region: ConsoleRegion,
	) -> Self {
		// The easiest way to do this is create a count, and then use that to
		// decompose into an actual code.
		let value = Self::make_id(hollywood_id, reset_count, hardware_model, region);

		let unscramble_id = WiiChip::unscramble_id(value);
		let hardware_model = ((unscramble_id >> 47) & 7) as u8;
		let area_code = ((unscramble_id >> 50) & 7) as u8;
		let hollywood_id = ((unscramble_id >> 15) & 0xFFFF_FFFF) as u32;
		let reset_counter = ((unscramble_id >> 10) & 0x1F) as u16;
		let crc = (value & 0x3FF) as u16;

		WiiChip {
			hollywood_id,
			reset_counter,
			hardware_model: HardwareModel::from_const_u8(hardware_model),
			area_code: ConsoleRegion::from_const_u8(area_code),
			checksum: crc,
		}
	}

	pub const fn from_u64(value: u64) -> Result<Self, WiinoError> {
		let unscramble_id = WiiChip::unscramble_id(value);

		let hardware_model = ((unscramble_id >> 47) & 7) as u8;
		let area_code = ((unscramble_id >> 50) & 7) as u8;
		let hollywood_id = ((unscramble_id >> 15) & 0xFFFF_FFFF) as u32;
		let reset_counter = ((unscramble_id >> 10) & 0x1F) as u16;
		let crc = (value & 0x3FF) as u16;

		if !WiiChip::crc_is_valid(unscramble_id) {
			return Err(WiinoError::InvalidCRC);
		}

		Ok(WiiChip {
			hollywood_id,
			reset_counter,
			hardware_model: HardwareModel::from_const_u8(hardware_model),
			area_code: ConsoleRegion::from_const_u8(area_code),
			checksum: crc,
		})
	}

	#[must_use]
	pub fn to_friend_code(self) -> String {
		format!(
			"{}",
			Self::make_id(
				self.hollywood_id,
				self.reset_counter,
				self.hardware_model,
				self.area_code,
			)
		)
	}

	const fn make_id(
		hollywood_id: u32,
		reset_count: u16,
		hardware_model: HardwareModel,
		region: ConsoleRegion,
	) -> u64 {
		let mut mix_id = ((region.to_const_u8() as u64) << 50_u64)
			| ((hardware_model.to_const_u8() as u64) << 47_u64)
			| ((hollywood_id as u64) << 15_u64)
			| ((reset_count as u64) << 10_u64);
		let mix_id_cp = mix_id;

		mix_id = Self::calc_crc(mix_id);
		mix_id = (mix_id_cp | (mix_id & 0xFFFF_FFFF)) ^ 0x0000_B3B3_B3B3_B3B3;
		mix_id = (mix_id >> 10) | ((mix_id & 0x3FF) << (11 + 32));

		let mut ctr = 0_u8;
		while ctr < 6 {
			let ret = u64_get_byte(mix_id, ctr);
			let foobar =
				((TABLE1[((ret >> 4) & 0xF) as usize]) << 4) | (TABLE1[(ret & 0xF) as usize]);
			mix_id = u64_insert_byte(mix_id, ctr, foobar);
			ctr += 1;
		}

		let mix_id_second_cp = mix_id;

		ctr = 0;
		while ctr < 6 {
			let ret = u64_get_byte(mix_id_second_cp, ctr);
			mix_id = u64_insert_byte(mix_id, SHIFT_TABLE[ctr as usize], ret);
			ctr += 1;
		}

		mix_id &= 0x001F_FFFF_FFFF_FFFF;
		mix_id = (mix_id << 1) | ((mix_id >> 52) & 1);
		mix_id ^= 0x0000_5E5E_5E5E_5E5E;
		mix_id &= 0x001F_FFFF_FFFF_FFFF;

		mix_id
	}

	#[must_use]
	pub const fn hollywood_id(&self) -> u32 {
		self.hollywood_id
	}
	#[must_use]
	pub const fn reset_counter(&self) -> u16 {
		self.reset_counter
	}
	#[must_use]
	pub const fn hardware_model(&self) -> &HardwareModel {
		&self.hardware_model
	}
	#[must_use]
	pub const fn region(&self) -> &ConsoleRegion {
		&self.area_code
	}
	#[must_use]
	pub const fn checksum(&self) -> u16 {
		self.checksum
	}

	const fn calc_crc(mut mix_id: u64) -> u64 {
		let mut ctr = 0;
		while ctr < 43 {
			let mut value = mix_id >> (52_u64 - ctr);
			if value & 1 != 0 {
				value = 0x0000_0000_0000_0635 << (42_u64 - ctr);
				mix_id ^= value;
			}
			ctr += 1;
		}
		mix_id
	}

	const fn crc_is_valid(mix_id: u64) -> bool {
		//(Self::calc_crc(mix_id) & 0xFFFF_FFFF) == 0
		Self::calc_crc(mix_id).trailing_zeros() >= 32
	}

	const fn unscramble_id(nwc24_id: u64) -> u64 {
		let mut mix_id = nwc24_id;

		mix_id &= 0x001F_FFFF_FFFF_FFFF; // 2^53-1
		mix_id ^= 0x0000_5E5E_5E5E_5E5E;
		mix_id &= 0x001F_FFFF_FFFF_FFFF; // 2^53-1

		{
			let mix_id_copy = (mix_id << 5) & 0x20;
			mix_id |= mix_id_copy << 48;
			mix_id >>= 1;
		}

		{
			let mix_id_copy = mix_id;

			let mut ctr = 0_u8;
			while ctr < 6 {
				let ret = u64_get_byte(mix_id_copy, SHIFT_TABLE[ctr as usize]);
				mix_id = u64_insert_byte(mix_id, ctr, ret);
				ctr += 1;
			}
		}

		let mut ctr = 0_u8;
		while ctr < 6 {
			let ret = u64_get_byte(mix_id, ctr);
			let foobar = ((TABLE1_INV[((ret >> 4) & 0xF) as usize]) << 4)
				| (TABLE1_INV[(ret & 0xF) as usize]);
			mix_id = u64_insert_byte(mix_id, ctr, foobar);
			ctr += 1;
		}

		let mix_id_copy3 = mix_id >> 0x20;
		let mix_id_copy4 = mix_id >> 0x16 | (mix_id_copy3 & 0x7FF) << 10;
		let mix_id_copy5 = (mix_id * 0x400) | (mix_id_copy3 >> 0xb & 0x3FF);
		let mix_id_copy6 = (mix_id_copy4 << 32) | mix_id_copy5;
		mix_id_copy6 ^ 0x0000_B3B3_B3B3_B3B3
	}
}

impl std::fmt::Display for WiiChip {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(
			f,
			"WiiChip(hollywood_id={},reset_count={},model={},region={},checksum={})",
			self.hollywood_id,
			self.reset_counter,
			self.hardware_model,
			self.area_code,
			self.checksum,
		)
	}
}

impl TryFrom<&str> for WiiChip {
	type Error = WiinoError;

	fn try_from(mut value: &str) -> Result<Self, Self::Error> {
		// Certain wii-chip id's start with `w`.
		if value.starts_with('w') {
			value = &value[1..];
		}
		if value.len() != 16 {
			return Err(WiinoError::IncorrectLength(value.len()));
		}

		match value.parse::<u64>() {
			Ok(as_num) => WiiChip::try_from(as_num),
			Err(num_err) => Err(WiinoError::NotANumber(num_err)),
		}
	}
}

impl TryFrom<u64> for WiiChip {
	type Error = WiinoError;

	fn try_from(value: u64) -> Result<Self, Self::Error> {
		Self::from_u64(value)
	}
}

#[derive(Debug, Error)]
pub enum WiinoError {
	#[error("Wii's CRC was not valid!")]
	InvalidCRC,
	#[error("Wii Numbers are actual numbers, and should be parsable as such: {0}.")]
	NotANumber(ParseIntError),
	#[error("Wii Numbers were exactly 16 characters long, was {0}.")]
	IncorrectLength(usize),
}

/// The models of Wii Console's.
#[derive(Debug, PartialEq, Eq)]
pub enum HardwareModel {
	/// RVT is a "development" model.
	Development,
	/// RVL is the "production" wii model.
	Production,
	/// "RVD" is mentioned in internal firmware, but it is not clear exactly
	/// what this model is.
	RVD,
	/// An unknown hardware model with a particular ID.
	Unknown(u8),
}

impl HardwareModel {
	#[must_use]
	pub const fn from_const_u8(number: u8) -> HardwareModel {
		match number {
			0 => HardwareModel::Development,
			1 => HardwareModel::Production,
			2 => HardwareModel::RVD,
			_ => HardwareModel::Unknown(number),
		}
	}

	#[must_use]
	pub const fn to_const_u8(self) -> u8 {
		match self {
			HardwareModel::Development => 0,
			HardwareModel::Production => 1,
			HardwareModel::RVD => 2,
			HardwareModel::Unknown(number) => number,
		}
	}

	#[must_use]
	pub const fn to_const_u8_ref(&self) -> u8 {
		match *self {
			HardwareModel::Development => 0,
			HardwareModel::Production => 1,
			HardwareModel::RVD => 2,
			HardwareModel::Unknown(number) => number,
		}
	}
}

impl std::fmt::Display for HardwareModel {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match *self {
			HardwareModel::Development => write!(f, "RVT (dev)"),
			HardwareModel::Production => write!(f, "RVL (production)"),
			HardwareModel::RVD => write!(f, "RVD (???)"),
			HardwareModel::Unknown(id) => write!(f, "UNK ({})", id),
		}
	}
}

impl From<u8> for HardwareModel {
	fn from(number: u8) -> HardwareModel {
		match number {
			0 => HardwareModel::Development,
			1 => HardwareModel::Production,
			2 => HardwareModel::RVD,
			_ => HardwareModel::Unknown(number),
		}
	}
}

impl From<HardwareModel> for u8 {
	fn from(model: HardwareModel) -> u8 {
		u8::from(&model)
	}
}

impl From<&HardwareModel> for u8 {
	fn from(model: &HardwareModel) -> u8 {
		match *model {
			HardwareModel::Development => 0,
			HardwareModel::Production => 1,
			HardwareModel::RVD => 2,
			HardwareModel::Unknown(number) => number,
		}
	}
}

/// The list of regions a console could be from.
#[derive(Debug, PartialEq, Eq)]
pub enum ConsoleRegion {
	Japan,
	UnitedStates,
	Europe,
	Taiwan,
	Korea,
	HongKong,
	China,
	Unknown(u8),
}

impl ConsoleRegion {
	#[must_use]
	pub const fn from_const_u8(number: u8) -> ConsoleRegion {
		match number {
			0 => ConsoleRegion::Japan,
			1 => ConsoleRegion::UnitedStates,
			2 => ConsoleRegion::Europe,
			3 => ConsoleRegion::Taiwan,
			4 => ConsoleRegion::Korea,
			5 => ConsoleRegion::HongKong,
			6 => ConsoleRegion::China,
			_ => ConsoleRegion::Unknown(number),
		}
	}

	#[must_use]
	pub const fn to_const_u8(self) -> u8 {
		Self::to_const_u8_ref(&self)
	}

	#[must_use]
	pub const fn to_const_u8_ref(&self) -> u8 {
		match *self {
			ConsoleRegion::Japan => 0,
			ConsoleRegion::UnitedStates => 1,
			ConsoleRegion::Europe => 2,
			ConsoleRegion::Taiwan => 3,
			ConsoleRegion::Korea => 4,
			ConsoleRegion::HongKong => 5,
			ConsoleRegion::China => 6,
			ConsoleRegion::Unknown(number) => number,
		}
	}
}

impl std::fmt::Display for ConsoleRegion {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match *self {
			ConsoleRegion::Japan => write!(f, "JPN (Japan)"),
			ConsoleRegion::UnitedStates => write!(f, "USA (United States of America)"),
			ConsoleRegion::Europe => write!(f, "EUR (Europe)"),
			ConsoleRegion::Taiwan => write!(f, "TWN (Taiwan)"),
			ConsoleRegion::Korea => write!(f, "KOR (Korea)"),
			ConsoleRegion::HongKong => write!(f, "HKG (Hong Kong)"),
			ConsoleRegion::China => write!(f, "CHN (China)"),
			ConsoleRegion::Unknown(id) => write!(f, "UNK ({})", id),
		}
	}
}

impl From<u8> for ConsoleRegion {
	fn from(number: u8) -> ConsoleRegion {
		match number {
			0 => ConsoleRegion::Japan,
			1 => ConsoleRegion::UnitedStates,
			2 => ConsoleRegion::Europe,
			3 => ConsoleRegion::Taiwan,
			4 => ConsoleRegion::Korea,
			5 => ConsoleRegion::HongKong,
			6 => ConsoleRegion::China,
			_ => ConsoleRegion::Unknown(number),
		}
	}
}

impl From<ConsoleRegion> for u8 {
	fn from(region: ConsoleRegion) -> u8 {
		u8::from(&region)
	}
}

impl From<&ConsoleRegion> for u8 {
	fn from(region: &ConsoleRegion) -> u8 {
		match *region {
			ConsoleRegion::Japan => 0,
			ConsoleRegion::UnitedStates => 1,
			ConsoleRegion::Europe => 2,
			ConsoleRegion::Taiwan => 3,
			ConsoleRegion::Korea => 4,
			ConsoleRegion::HongKong => 5,
			ConsoleRegion::China => 6,
			ConsoleRegion::Unknown(number) => number,
		}
	}
}

const SHIFT_TABLE: [u8; 8] = [0x1, 0x5, 0x0, 0x4, 0x2, 0x3, 0x6, 0x7];
const TABLE1: [u8; 16] = [
	0x4, 0xB, 0x7, 0x9, 0xF, 0x1, 0xD, 0x3, 0xC, 0x2, 0x6, 0xE, 0x8, 0x0, 0xA, 0x5,
];
const TABLE1_INV: [u8; 16] = [
	0xD, 0x5, 0x9, 0x7, 0x0, 0xF, 0xA, 0x2, 0xC, 0x3, 0xE, 0x1, 0x8, 0x6, 0xB, 0x4,
];

#[allow(clippy::cast_possible_truncation)]
const fn u64_get_byte(value: u64, shift: u8) -> u8 {
	(value >> ((shift as u64) * 8_u64)) as u8
}
const fn u64_insert_byte(value: u64, shift: u8, byte: u8) -> u64 {
	let masked: u64 = 0x0000_0000_0000_00FF_u64 << (shift * 8);
	let inst = (byte as u64) << (shift * 8);
	(value & !masked) | inst
}

Conclusion

Anyway, there wasn't really a point to all this, I just found how Wii Friend Codes work (especially when you can generate them completely offline) super interesting, and wanted to try Cohost. Hopefully this deep dive was interesting? I guess lemme know!