├── .gitattributes ├── README.md ├── minified.js └── google-doodle-mod-loader.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-doodle-mod-loader 2 | __*NEVER* PASTE ANYTHING IN THE DEVELOPER CONSOLE IF YOU DON'T KNOW WHAT IT DOES!__ This mod loader only reads the selected files and modifies in-game elements, but it never hurts to double check what you're running before running it. The unminified JavaScript is located [here](https://github.com/cabalex/google-doodle-mod-loader/blob/main/google-doodle-mod-loader.js). 3 | 4 | A mod loader for Google's 2021 Doodle Champion Island Games. Currently only supports replacing the Artistic Swimming minigame with custom Friday Night Funkin' beatmaps/audio. 5 | 6 | ## Artistic Swimming song replacements 7 | Make sure you have a valid vanilla FNF song and its MP3/OGG on hand. Note that songs with separated instrumental and vocal tracks aren't supported, so you'll need to condense the two tracks into one before modding. 8 | 9 | Go to [here](https://raw.githubusercontent.com/cabalex/google-doodle-mod-loader/main/minified.js), select everything (CTRL + A) and copy. Then, go to google.com, or [the Google Doodle webpage if the event is over](https://www.google.com/doodles/doodle-champion-island-games-begin). *Alternatively, you can play in full screen [here](https://www.google.com/logos/2020/kitsune/rc2/kitsune20.html?hl=en).* 10 | 11 | Then, go into the developer console by hitting **CTRL + SHIFT + J**, and paste the code you copied into the terminal. Then, hit enter, select your beatmap and song, and you should be all good to go! 12 | 13 | Once the program finishes running, your song should be in the first Artistic Swimming minigame (the one you can teleport to). Enter the minigame and play your song! 14 | 15 | ![image](https://user-images.githubusercontent.com/31020729/126930529-3fa9b98d-722a-40a5-94e2-1d9e15a30a0a.png) 16 | 17 | 18 | ## Notes 19 | * If you want to go back to playing the game vanilla, just refresh your webpage. 20 | * Currently, only replacing the first Artistic Swimming minigame is supported. More features [maybe] coming soon! 21 | * All notes for the second player are ignored in the conversion, which may result in boring segments where not much is happening. 22 | * The mod loader disables score uploading to Google's servers, for obvious reasons. Any new scores won't be uploaded unless you refresh your page. This does not affect your local high scores. 23 | 24 | The 2021 Doodle Champion Island Games is owned by Google. [See who developed it here](https://www.google.com/doodles/doodle-champion-island-games-begin). 25 | -------------------------------------------------------------------------------- /minified.js: -------------------------------------------------------------------------------- 1 | new Promise(async(t,e)=>{Eu||zu||Hu||e("I couldn't find the game. Maybe try refreshing?"),console.log("GOOGLE DOODLE MOD LOADER\ncreated by cabalex for the 2021 Doodle Champion Island Games\nv0.2.0\n\nUpload your FNF song beatmap/mp3."),window.google.doodle.doodle_args.submitScoreUrl="";var[r]=await window.showOpenFilePicker({multiple:!1,types:[{description:"FNF Beatmaps",accept:{"application/json":[".json"]}}]});r=await r.getFile();var[s]=await window.showOpenFilePicker({multiple:!1,types:[{description:"FNF Songs",accept:{"audio/mpeg":[".mp3",".ogg"]}}]});s=await s.getFile(),console.log("Loading files...");var o=[],a=120,i=document.createElement("audio");i.setAttribute("id","tempModLoaderAudio");var n=new FileReader;n.onloadend=async function(r){if(r.target.readyState==FileReader.DONE){if("string"==typeof r.target.result){var c=JSON.parse(r.target.result);a=c.song.bpm;for(var l=[3,2,0,1],g=0;g3&&o.push({type:l[d[1]%4],time:d[0]})}}o.sort((t,e)=>t.time-e.time),n.readAsArrayBuffer(s)}else{var $=new Blob([r.target.result],{type:"audio/mpeg"});i.src=URL.createObjectURL($),i.addEventListener("durationchange",async function(){o.push({type:10,time:1e3*i.duration});var r=document.getElementsByTagName("html")[0].innerHTML;let s=parseInt(r.split("/kitsune/rc")[1].split("/")[0]);switch(console.log("Running RC"+s+" - https://www.google.com/logos/2020/kitsune/rc"+s+"/kitsune20.html?hl=en"),s){case 1:A.C1.Ud.Sb=i.src,A.C1.Qc=1e3*i.duration;var n=zu.toString().replace(/.SPEED=[0-9.]*\}this/mg,".SPEED="+a+"}this");zu.prototype.constructor=Function("b",n.substr(12,n.length-13));break;case 2:A.E1.Rd.Tb=i.src,A.E1.Oc=1e3*i.duration;var n=Eu.toString().replace(/.SPEED=[0-9.]*\}this/mg,".SPEED="+a+"}this");Eu.prototype.constructor=Function("b",n.substr(12,n.length-13)),Du=o;break;case 3:A.I1.Rd.Tb=i.src,A.I1.Oc=1e3*i.duration;var n=Hu.toString().replace(/.SPEED=[0-9.]*\}this/mg,".SPEED="+a+"}this");Hu.prototype.constructor=Function("b",n.substr(12,n.length-13)),Du=o;break;case 4:case 5:case 6:A.K1.le.oc=i.src,A.K1.Sc=1e3*i.duration;var n=Hu.toString().replace(/.SPEED=[0-9.]*\}this/mg,".SPEED="+a+"}this");Hu.prototype.constructor=Function("b",n.substr(12,n.length-13)),Gu=o;break;case 7:A.K6.fe.Jc=i.src,A.K6.Bd=1e3*i.duration;var n=Mu.toString().replace(/.SPEED=[0-9.]*\}this/mg,".SPEED="+a+"}this");Mu.prototype.constructor=Function("b",n.substr(12,n.length-13)),Lu=o;break;default:console.error("Error: Unknown rc type? RC"+s),e()}console.log("Finished loading successfully! Visit the main Artistic Swimming minigame to play."),t()},!1)}}},n.readAsText(r)}); 2 | -------------------------------------------------------------------------------- /google-doodle-mod-loader.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | return new Promise(async (resolve, reject) => { 3 | if (!Eu && !zu && !Hu) { 4 | reject("I couldn't find the game. Maybe try refreshing?") 5 | } 6 | console.log("GOOGLE DOODLE MOD LOADER\ncreated by cabalex for the 2021 Doodle Champion Island Games\nv0.2.0\n\nUpload your FNF song beatmap/mp3.") 7 | window.google.doodle.doodle_args.submitScoreUrl = ""; 8 | // Get files 9 | var [f] = await window.showOpenFilePicker({"multiple": false, "types": [{"description": "FNF Beatmaps", "accept": {'application/json': ['.json']}}]}) 10 | f = await f.getFile() 11 | var [f2] = await window.showOpenFilePicker({"multiple": false, "types": [{"description": "FNF Songs", "accept": {'audio/mpeg': ['.mp3', '.ogg']}}]}) 12 | f2 = await f2.getFile() 13 | // 14 | // File reading begin 15 | // 16 | console.log("Loading files...") 17 | var beatmap = [] 18 | var bpm = 120; 19 | var audio = document.createElement('audio'); 20 | audio.setAttribute('id', 'tempModLoaderAudio') 21 | var reader = new FileReader(); 22 | reader.onloadend = async function(e) { 23 | if (e.target.readyState == FileReader.DONE) { 24 | if (typeof(e.target.result) == "string") { 25 | var txt = JSON.parse(e.target.result); 26 | bpm = txt['song']['bpm']; 27 | var assignment = [3, 2, 0, 1]; 28 | for (var j = 0; j < txt['song']['notes'].length; j++) { 29 | var section = txt['song']['notes'][j] 30 | /* Google Doodle mappings: 31 | # 0 - Right (FNF Left) 32 | # 1 - Up (FNF down) 33 | # 2 - Down (FNF up) 34 | # 3 - Left (FNF right)*/ 35 | if (section['mustHitSection']) { 36 | // Ignore 2P notes (4-7) 37 | for (var i = 0; i < section['sectionNotes'].length; i++) { 38 | var x = section['sectionNotes'][i]; 39 | if (x[1] < 4) { 40 | beatmap.push({"type": assignment[x[1] % 4], "time": x[0]}); 41 | } 42 | } 43 | } else { 44 | // Ignore 2P notes (1-3) 45 | for (var i = 0; i < section['sectionNotes'].length; i++) { 46 | var x = section['sectionNotes'][i]; 47 | if (x[1] > 3) { 48 | beatmap.push({"type": assignment[x[1] % 4], "time": x[0]}); 49 | } 50 | } 51 | } 52 | } 53 | beatmap.sort((a, b) => a['time'] - b['time']); 54 | // Load this afterward 55 | reader.readAsArrayBuffer(f2); 56 | } else { 57 | var blob = new Blob([e.target.result], { type: 'audio/mpeg' }) 58 | audio.src = URL.createObjectURL(blob) 59 | audio.addEventListener('durationchange', async function() { 60 | // Songs end with a type 10 note 61 | beatmap.push({"type": 10, "time": audio.duration*1000}) 62 | // Find the key of disco 63 | var source = document.getElementsByTagName('html')[0].innerHTML; 64 | /* 65 | There is a different version of the Google Doodle hosted on rc1, rc2, rc3, rc4-rc6, and rc7. 66 | Each variable name is randomized, so you can't just use one on any other. 67 | This script checks for that and changes the variables accordingly. 68 | */ 69 | const version = parseInt(source.split("/kitsune/rc")[1].split("/")[0]) 70 | console.log("Running RC" + version + " - https://www.google.com/logos/2020/kitsune/rc" + version + "/kitsune20.html?hl=en" ) 71 | switch(version) { 72 | case 1: 73 | A.C1.Ud.Sb = audio.src 74 | A.C1.Qc = audio.duration*1000 75 | var funcstr = zu.toString().replace(/.SPEED=[0-9.]*\}this/mg, ".SPEED=" + bpm + "}this") 76 | zu.prototype.constructor = new Function("b", funcstr.substr(12, funcstr.length-13)); 77 | break; 78 | case 2: 79 | A.E1.Rd.Tb = audio.src 80 | A.E1.Oc = audio.duration*1000 81 | var funcstr = Eu.toString().replace(/.SPEED=[0-9.]*\}this/mg, ".SPEED=" + bpm + "}this") 82 | Eu.prototype.constructor = new Function("b", funcstr.substr(12, funcstr.length-13)); 83 | // Cu - rock (third one, the three sisters gate) 84 | // Bu - ballad (second one, underwater) 85 | // Du - disco (first one, normal gate) 86 | Du = beatmap; 87 | break; 88 | case 3: 89 | A.I1.Rd.Tb = audio.src 90 | A.I1.Oc = audio.duration*1000 91 | var funcstr = Hu.toString().replace(/.SPEED=[0-9.]*\}this/mg, ".SPEED=" + bpm + "}this") 92 | Hu.prototype.constructor = new Function("b", funcstr.substr(12, funcstr.length-13)); 93 | Du = beatmap; 94 | break; 95 | case 4: 96 | case 5: 97 | case 6: 98 | A.K1.le.oc = audio.src 99 | A.K1.Sc = audio.duration*1000 100 | var funcstr = Hu.toString().replace(/.SPEED=[0-9.]*\}this/mg, ".SPEED=" + bpm + "}this") 101 | Hu.prototype.constructor = new Function("b", funcstr.substr(12, funcstr.length-13)); 102 | Gu = beatmap; 103 | break; 104 | case 7: 105 | A.K6.fe.Jc = audio.src 106 | A.K6.Bd = audio.duration*1000 107 | var funcstr = Mu.toString().replace(/.SPEED=[0-9.]*\}this/mg, ".SPEED=" + bpm + "}this") 108 | Mu.prototype.constructor = new Function("b", funcstr.substr(12, funcstr.length-13)); 109 | Lu = beatmap; 110 | break; 111 | default: 112 | console.error("Error: Unknown rc type? RC" + version) 113 | reject(); 114 | } 115 | // 116 | console.log("Finished loading successfully! Visit the main Artistic Swimming minigame to play.") 117 | resolve(); 118 | }, false); 119 | } 120 | } 121 | } 122 | reader.readAsText(f) 123 | }) 124 | })(); 125 | --------------------------------------------------------------------------------