Created Thursday, February 10, 2022
Mario Maker 2 is a game I’ve played for years, since launch, and I’ve always been interested in putting my skills in programming to the test here. I’ve finally done just that. Using the incredible NintendoClients made by Kinnay, I, and individuals from the Ninji Speedrunning discord, 0% discord, and Kinnay himself, we now have an entirely free and public way of accessing data previously only available to the switch itself.
Nintendo Switch Online games tend to use a specific library with a specific protocol to communicate with Nintendo’s servers. Since the WiiU, this protocol has been NEX. NEX, partially due to the incredible success of many of the games using it like Mario Kart 8 and Splatoon 2, has been extensively documented, to the point where it is fully possible to spoof both the client and server. The Pretendo project has been working on a WiiU NEX server for years, and alongside it NintendoClients, a NEX client, has been becoming more stable and safe to run on your personal account.
In October 2021 I finally took the plunge and started using NintendoClients for a dream I had years prior: to make all Mario Maker 2 data easily accessible to computers. After watching the project for years, debating whether to purchase a digital copy of the game and put my Switch certificate on the line, Tatiaus, a talented kaizo player, contacted me about using the API for the 0% team as I had mentioned it in passing earlier. Once I realized there was interest past my own goals I bought the game and began working. On the 13th, I released the first build, publicly available with no login required.
First release on discord
First, the Ninji server used it to round out their leaderboards, making them 100% accurate for the first time ever. The features I considered most important, the level_info
and level_data
endpoints, went unused. Not many people outside the Ninji Speedrunning discord knew this existed. Slowly, the API got around, but it stayed a novelty. One of the funniest misconceptions I saw was the guess that my switch was running 24/7 and downloading levels for me. The truth is that this was running on a linux server, using a complete recreation of the NEX client. No switch was involved, except for the various credentials the NEX protocol needs to authenticate with the server.
In November a kaizo player well known for performing incredibly difficult "trump jumps", jumps in the game which were pixel perfect, came to me about using the API for a level viewer he had designed. In time Ji Xiaomai released his viewer, and that was the spark that ignited the entire community. Suddenly now anyone could view any level without the game and even without a switch. Tricky levels that were only possible due to information only the creator could know were exposed. Suddenly now no level could hide secrets.
After the release of this viewer, I ported it to C++ and made it portable enough to run in the browser, so now it wasn’t even neccesary to download the app to your PC. With time came new incredible viewers, including one that had animated textures. The count of level viewers was now 5, including the continuation of an old level viewer by streamer and programmer Shoujo and one made from the ground up by Ingram. The community had collectively come together to tear the game apart, utterly destroying the roughly 1 level viewer made for the first game. And in another impressive effort, almost every field of every endpoint was determined, so now stats previously thought impossible to obtain, like the boos on a level or the k/d ratio of a player, were accessible. All this hype has generated an impressive amount of traffic as well:
Web traffic after release
Through all this, the API has stayed public and free. Information should always be as accessible as possible and designed to be as easy to parse as possible by people and computers alike. While I am putting my personal Nintendo Switch Online account on the line, this tool is invaluable to research about the game. So I can proudly say, while Youtube made dislikes private, we made Mario Maker 2 dislikes public.
The logical conclusion to this venture is to pull as much data as we can. So I did the only reasonable thing, went out and bought a 14 terabyte hard drive in order to download Mario Maker 2’s entire database. Now it might seem impossible to simply download everything, we can request up to 300 random levels at a time through endpoints like search_endless_mode
, but statistically we will recieve less and less unique items over time, almost guaranteed to take years to download 100% of available levels. Nintendo, however, made this easy for us:
1def course_id_to_dataid(id): 2 course_id = id[::-1] 3 charset = "0123456789BCDFGHJKLMNPQRSTVWXY" 4 number = 0 5 for char in course_id: 6 number = number * 30 + charset.index(char) 7 left_side = number 8 left_side = left_side << 34 9 left_side_replace_mask = 0b1111111111110000000000000000000000000000000000 10 number = number ^ ((number ^ left_side) & left_side_replace_mask) 11 number = number >> 14 12 number = number ^ 0b00010110100000001110000001111100 13 return number
Course ids are constructed from a regular algorithm from a consistent incrementing index called a data_id
. Internally these ids are used to request many known levels, like a list of your own uploaded levels. Because these data ids increment regularly and we know both the first data id, 3 million, and the last through a call to search_new
, we can download every uploaded level by sweeping through all 31 million data ids currently allocated. Because users follow similar rules, downloading every user account is also possible.
Running a python program for 4 straight weeks to generate a 2 terabyte database may seem foolish to most, but I’m not putting this perfectly good PC to waste by turning it off. Instead, I did it, ran my computer for all of those 4 weeks, and the resulting data will be a goldmine into how players interact with levels and each other.
1CREATE TABLE level ( 2 data_id INTEGER, -- Data IDs are regularly incrementing values, gaps in the table are due to levels deleted by users or Nintendo 3 name TEXT, -- Course name 4 description TEXT, -- Course description 5 uploaded INTEGER, -- UTC timestamp for when the level was uploaded 6 course_id TEXT, -- Course ID as seen in game 7 gamestyle INTEGER, -- Gamestyle, enum below 8 theme INTEGER, -- Theme, enum below 9 difficulty INTEGER, -- Difficulty, enum below 10 tag1 INTEGER, -- The first tag, if it exists, enum below 11 tag2 INTEGER, -- The second tag, if it exists, enum below 12 world_record INTEGER, -- The world record in milliseconds 13 upload_time INTEGER, -- The upload time in milliseconds 14 num_comments INTEGER, -- Number of comments, may not reflect the archived comments if there were more than 1000 comments 15 clear_condition INTEGER, -- Clear condition, enum below 16 clear_condition_magnitude INTEGER, -- If applicable, the magnitude of the clear condition 17 clears INTEGER, -- Course clears 18 attempts INTEGER, -- Course attempts 19 clear_rate REAL, -- Course clear rate as a float between 0 and 1 20 plays INTEGER, -- Course plays, or "footprints" 21 versus_matches INTEGER, -- Course versus matches 22 coop_matches INTEGER, -- Course coop matches 23 likes INTEGER, -- Course likes 24 boos INTEGER, -- Course boos 25 unique_players_and_versus INTEGER, -- All unique players that have ever played this level, including the number of versus matches 26 weekly_likes INTEGER, -- The weekly likes on this course 27 weekly_plays INTEGER, -- The weekly plays on this course 28 one_screen_thumbnail BLOB, -- The one screen course thumbnail, as a JPEG binary 29 one_screen_thumbnail_url TEXT, -- The old URL of this thumbnail 30 one_screen_thumbnail_size INTEGER, -- The filesize of this thumbnail 31 one_screen_thumbnail_filename TEXT, -- The filename of this thumbnail 32 entire_thumbnail BLOB, -- The entire course thumbnail, as a JPEG binary 33 entire_thumbnail_url TEXT, -- The old URL of this thumbnail 34 entire_thumbnail_size INTEGER, -- The filesize of this thumbnail 35 entire_thumbnail_filename TEXT, -- The filename of this thumbnail 36 uploader_pid TEXT, -- The player ID of the uploader 37 first_completer_pid TEXT, -- The player ID of the user who first cleared this course 38 record_holder_pid TEXT, -- The player ID of the user who held the world record at time of archival 39 level_data BLOB, -- The GZIP compressed decrypted level data, kaitai struct file is provided to read the header 40 unk2 INTEGER, -- Unknown 41 unk3 BLOB, -- Unknown 42 unk9 INTEGER, -- Unknown 43 unk10 INTEGER, -- Unknown 44 unk11 INTEGER, -- Unknown 45 unk12 INTEGER -- Unknown 46); 47CREATE TABLE user ( 48 pid TEXT, -- The player ID of this user, an unsigned 64 bit integer as a string 49 data_id INTEGER, -- The data ID of this user, while not used internally user codes are generated using this 50 code TEXT, -- User code 51 region INTEGER, -- User region, enum below 52 name TEXT, -- User name 53 country TEXT, -- User country as a 2 letter code 54 last_active INTEGER, -- UTC timestamp of when this user was last active, not known what constitutes active 55 mii_data BLOB, -- The CHARINFO blob of this user's mii 56 mii_image TEXT, -- An image of this mii as reported by Nintendo's debug endpoint 57 mii_studio_code TEXT, -- The mii studio code of this user 58 pose INTEGER, -- Pose, enum below 59 hat INTEGER, -- Hat, enum below 60 shirt INTEGER, -- Shirt, enum below 61 pants INTEGER, -- Pants, enum below 62 wearing_outfit INTEGER, -- Whether this user is wearing pants 63 courses_played INTEGER, -- How many courses this user has played 64 courses_cleared INTEGER, -- How many courses this user has cleared 65 courses_attempted INTEGER, -- How many courses this user has attempted 66 courses_deaths INTEGER, -- How many times this user has died 67 likes INTEGER, -- How many likes this user has recieved 68 maker_points INTEGER, -- Maker points 69 easy_highscore INTEGER, -- Easy highscore 70 normal_highscore INTEGER, -- Normal highscore 71 expert_highscore INTEGER, -- Expert highscore 72 super_expert_highscore INTEGER, -- Super expert high score 73 versus_rating INTEGER, -- Versus rating 74 versus_rank INTEGER, -- Versus rank, enum below 75 versus_won INTEGER, -- How many courses this user has won in versus 76 versus_lost INTEGER, -- How many courses this user has lost in versus 77 versus_win_streak INTEGER, -- Versus win streak 78 versus_lose_streak INTEGER, -- Versus lose streak 79 versus_plays INTEGER, -- Versus plays 80 versus_disconnected INTEGER, -- Times user has disconnected in versus 81 coop_clears INTEGER, -- Coop clears 82 coop_plays INTEGER, -- Coop plays 83 recent_performance INTEGER, -- Unknown variable relating to versus performance 84 versus_kills INTEGER, -- Kills in versus, unknown what activities constitute a kill 85 versus_killed_by_others INTEGER, -- Deaths in versus from other users, unknown what activities constitute a death 86 multiplayer_unk13 INTEGER, -- Unknown, relating to multiplayer 87 multiplayer_unk14 INTEGER, -- Unknown, relating to multiplayer 88 first_clears INTEGER, -- First clears 89 world_records INTEGER, -- World records 90 unique_super_world_clears INTEGER, -- Super world clears 91 uploaded_levels INTEGER, -- Number of uploaded levels 92 maximum_uploaded_levels INTEGER, -- Maximum number of levels this user may upload 93 weekly_maker_points INTEGER, -- Weekly maker points 94 last_uploaded_level INTEGER, -- UTC timestamp of when this user last uploaded a level 95 is_nintendo_employee INTEGER, -- Whether this user is an official Nintendo account 96 comments_enabled INTEGER, -- Whether this user has comments enabled on their levels 97 tags_enabled INTEGER, -- Whether this user has tags enabled on their levels 98 super_world_id TEXT, -- The ID of this user's super world, blank if they do not have one 99 unk3 INTEGER, -- Unknown 100 unk12 INTEGER, -- Unknown 101 unk16 INTEGER -- Unknown 102); 103CREATE TABLE user_badges ( 104 pid TEXT, -- Player ID 105 type INTEGER, -- The kind of badge, enum below 106 rank INTEGER -- The rank of badge, enum below 107); 108CREATE TABLE level_comments ( 109 data_id INTEGER, -- The data ID of the level this comment corresponds to 110 comment_id TEXT, -- Comment ID 111 type INTEGER, -- Type of comment, enum below 112 pid TEXT, -- Player ID of the comment creator 113 posted INTEGER, -- UTC timestamp of when this comment was created 114 clear_required INTEGER, -- Whether this comment requires a clear to view 115 text TEXT, -- If the comment type is text, the text of the comment 116 reaction_image_id INTEGER, -- If this comment is a reaction image, the id of the reaction image, enum below 117 custom_image BLOB, -- If this comment is a custom drawing, the custom drawing as a PNG binary 118 custom_image_url TEXT, -- If this comment is a custom drawing, the old url of the drawing 119 custom_image_size INTEGER, -- If this comment is a custom drawing, the filesize of the drawing in its old format 120 custom_image_filename TEXT, -- If this comment is a custom drawing, the filename of the drawing 121 has_beaten INTEGER, -- Whether the user had beaten the level when they created the comment 122 x INTEGER, -- The X position of the comment in game 123 y INTEGER, -- The Y position of the comment in game 124 reaction_face INTEGER, -- The reaction the mii of the user has on this comment 125 unk8 INTEGER, -- Unknown 126 unk10 INTEGER, -- Unknown 127 unk12 INTEGER, -- Unknown 128 unk14 BLOB, -- Unknown 129 unk17 INTEGER -- Unknown 130); 131CREATE TABLE level_played ( 132 data_id INTEGER, -- The data ID of the level this play corresponds to 133 pid TEXT, -- The player ID of the user who made this play 134 cleared INTEGER, -- Whether the user cleared the level 135 liked INTEGER -- Whether the user liked the level 136); 137CREATE TABLE level_deaths ( 138 data_id INTEGER, -- The data ID of the level this death corresponds to 139 x INTEGER, -- The X position of where this death occured 140 y INTEGER, -- The Y position of where this death occured 141 is_subworld INTEGER -- Whether this death occured in the subworld 142); 143CREATE TABLE world ( 144 pid TEXT, -- The player ID of the user who created this super world 145 world_id TEXT, -- World ID 146 worlds INTEGER, -- Number of worlds 147 levels INTEGER, -- Number of levels 148 planet_type INTEGER, -- Planet type, enum below 149 created INTEGER, -- UTC timestamp of when this super world was created 150 unk1 BLOB, -- Unknown 151 unk5 INTEGER, -- Unknown 152 unk6 INTEGER, -- Unknown 153 unk7 INTEGER, -- Unknown 154 thumbnail BLOB, -- The thumbnail, as a JPEG binary 155 thumbnail_url TEXT, -- The old URL of this thumbnail 156 thumbnail_size INTEGER, -- The filesize of this thumbnail 157 thumbnail_filename TEXT -- The filename of this thumbnail 158); 159CREATE TABLE world_levels ( 160 pid TEXT, -- The player ID of the user who created the super world with this level 161 data_id INTEGER, -- The data ID of the level 162 ninjis INTEGER -- Number of ninjis shown on this level 163); 164CREATE TABLE user_posted ( 165 pid TEXT, -- The player ID of the user who posted this 166 data_id INTEGER -- The data ID of the level 167); 168CREATE TABLE user_liked ( 169 pid TEXT, -- The player ID of the user who liked this 170 data_id INTEGER -- The data ID of the level 171); 172CREATE TABLE user_played ( 173 pid TEXT, -- The player ID of the user who played this 174 data_id INTEGER -- The data ID of the level 175); 176CREATE TABLE user_first_cleared ( 177 pid TEXT, -- The player ID of the user who first cleared this 178 data_id INTEGER -- The data ID of the level 179); 180CREATE TABLE user_world_record ( 181 pid TEXT, -- The player ID of the user who got world record on this 182 data_id INTEGER -- The data ID of the level 183); 184CREATE TABLE ninji ( 185 data_id INTEGER, -- The data ID of the level this ninji played on 186 pid TEXT, -- The player ID of the user who made this ninji 187 time INTEGER, -- The time of this ninji in milliseconds 188 replay BLOB, -- The replay binary of this ninji 189 replay_url TEXT, -- The old url of this ninji 190 replay_size INTEGER, -- The filesize of this ninji 191 replay_filename TEXT -- The filename of this ninji 192); 193CREATE TABLE ninji_level ( 194 data_id INTEGER, -- The data ID of this ninji level 195 name TEXT, -- Name 196 description TEXT, -- Description 197 uploaded INTEGER, -- UTC timestamp of when this was uploaded 198 ended INTEGER, -- UTC timestamp of when this event ended 199 gamestyle INTEGER, -- Gamestyle, enum below 200 theme INTEGER, -- Theme, enum below 201 medal_time INTEGER, -- Time to get a medal in milliseconds 202 clear_condition INTEGER, -- Clear condition, enum below 203 clear_condition_magnitude INTEGER, -- If applicable, the magnitude of the clear condition 204 unk3_0 INTEGER, -- Unknown 205 unk3_1 INTEGER, -- Unknown 206 unk3_2 INTEGER, -- Unknown 207 unk5 INTEGER, -- Unknown 208 unk6 INTEGER, -- Unknown 209 unk9 INTEGER, -- Unknown 210 level_data BLOB, -- The GZIP compressed decrypted level data, a kaitai struct file is provided to read this 211 one_screen_thumbnail BLOB, -- The one screen course thumbnail, as a JPEG binary 212 one_screen_thumbnail_url TEXT, -- The old URL of this thumbnail 213 one_screen_thumbnail_size INTEGER, -- The filesize of this thumbnail 214 one_screen_thumbnail_filename TEXT, -- The filename of this thumbnail 215 entire_thumbnail BLOB, -- The entire course thumbnail, as a JPEG binary 216 entire_thumbnail_url TEXT, -- The old URL of this thumbnail 217 entire_thumbnail_size INTEGER, -- The filesize of this thumbnail 218 entire_thumbnail_filename TEXT -- The filename of this thumbnail 219); 220CREATE INDEX idx_level_data_id ON level (data_id); 221CREATE INDEX idx_level_comments_data_id ON level_comments (data_id); 222CREATE INDEX idx_level_deaths_data_id ON level_deaths (data_id); 223CREATE INDEX idx_level_played_data_id ON level_played (data_id); 224CREATE INDEX idx_ninji_data_id ON ninji (data_id); 225CREATE INDEX idx_user_pid ON user (pid); 226CREATE INDEX idx_user_badges_pid ON user_badges (pid); 227CREATE INDEX idx_user_first_cleared_pid ON user_first_cleared (pid); 228CREATE INDEX idx_user_liked_pid ON user_liked (pid); 229CREATE INDEX idx_user_played_pid ON user_played (pid); 230CREATE INDEX idx_user_posted_pid ON user_posted (pid); 231CREATE INDEX idx_user_world_record_pid ON user_world_record (pid); 232CREATE INDEX idx_world_pid ON world (pid); 233CREATE INDEX idx_world_levels_pid ON world_levels (pid);
In an update post I’ll cover the data analysis I do on this massive archive, and release it publicly. Make sure to look out for that!