`;
//Insert html for custom preset export dialogue box.
let modSelectorModalContainer = document.createElement('div');
modSelectorModalContainer.innerHTML = modSelectorModal;
modSelectorModalContainer.id = 'mod-selector-dialogue-container';
modSelectorModalContainer.style = 'display:none; position:fixed; width:100%; height:100%; z-index: 999999; left:0; top:0';
document.body.appendChild(modSelectorModalContainer);
showUpdateLinkIfNeeded();
let hideEndScreenImg = document.createElement('img');
hideEndScreenImg.style = "position: absolute;left: 10px;top: 10px;cursor: pointer; height:20px; width:auto;";
hideEndScreenImg.src = "https://raw.githubusercontent.com/DarkSnakeGang/GoogleSnakeIcons/main/ToggleDeathscreen/EyeIcon.png";
hideEndScreenImg.title = "Click to hide. Click anywhere to bring back";
hideEndScreenImg.id = "death-screen-toggle";
let firstMenuScreen = document.getElementsByClassName('T7SB3d')[0];
if(firstMenuScreen) {
firstMenuScreen.appendChild(hideEndScreenImg);
}
let showEndScreenCover = document.createElement('div');
showEndScreenCover.style = "display:none; background-color:transparent; position: fixed; top:0;left:0; width:100vw; height:100vh;box-shadow: cyan 0px 0px 10px inset; box-sizing: border-box;z-index: 1000000;"
document.body.appendChild(showEndScreenCover);
hideEndScreenImg.addEventListener('click', function() {
let endScreenContainer = document.getElementsByClassName('wjOYOd')[0];
if(endScreenContainer) {
endScreenContainer.style.visibility = 'hidden';
}
showEndScreenCover.style.display = 'block';
});
showEndScreenCover.addEventListener('click', function() {
let endScreenContainer = document.getElementsByClassName('wjOYOd')[0];
if(endScreenContainer) {
endScreenContainer.style.visibility = 'visible';
}
showEndScreenCover.style.display = 'none';
});
//Tick the currently selected mod choice according to localStorage. Also, set the mod name in the indicator
const currentlySelectedMod = localStorage.getItem('snakeChosenMod');
let newlySelectedMod = currentlySelectedMod;
if(modsConfig.hasOwnProperty(currentlySelectedMod) && currentlySelectedMod !== null && currentlySelectedMod !== 'none') {
document.querySelector(`input[name="mod-selector"][value="${currentlySelectedMod}"]`).checked = true;
document.getElementById('mod-name-span').textContent = modsConfig[currentlySelectedMod].displayName;
let descriptionEl = document.querySelector(`div.mod-description[data-linked-option="${currentlySelectedMod}"]`);
if(descriptionEl) {descriptionEl.style.display = 'block';}
} else {
document.querySelector('input[name="mod-selector"][value="none"]').checked = true;
document.getElementById('mod-name-span').textContent = 'None';
}
attemptRenderGameVersionOptions(currentlySelectedMod, true);
//Update the checked/ticked mod when clicking on any of the radio buttons. Also show the description for it.
[...document.querySelectorAll('input[type="radio"][name="mod-selector"]')].forEach(radioEl=>{
radioEl.addEventListener('click', function(){
//Mark mod as selected for when we hit apply
newlySelectedMod = this.value;
attemptRenderGameVersionOptions(this.value, false);
showSettingChanged();
[...document.getElementsByClassName('mod-description')].forEach(el=>{
el.style.display = 'none';
let descriptionEl = document.querySelector(`div.mod-description[data-linked-option="${this.value}"]`);
if(descriptionEl) {descriptionEl.style.display = 'block';}
});
});
});
if(WEB_VERSION) {
document.getElementById('mod-game-version').addEventListener('change',showSettingChanged);
}
if(!WEB_VERSION && !localStorage.getItem('snakeGsmPromoDismissed')) {
document.getElementById('gsm-dismiss').addEventListener('click',()=>{
if(confirm('Never show message again? Press OK to confirm or cancel to keep the message.')) {
localStorage.setItem('snakeGsmPromoDismissed', 'true');
document.getElementById('gsm-message').remove();
alert('Successfully dismissed. If you want to visit the standalone mods website in the future, the link can be found in the settings.');
}
});
}
//Load advanced settings
let advancedSettings = JSON.parse(localStorage.getItem('snakeAdvancedSettings')) ?? {};
delete advancedSettings.fbxCentered; //Remove the fbx centered property as it's no longer needed
let advancedSettingsOriginal = {...advancedSettings}; //Shallow copy, but it's ok as nothing is nested.
//Make sure the inputs are set the right values when starting up the mod
updateAdvancedSettingInputs();
//Event listeners for advanced settings
document.getElementById('advanced-options-toggle').addEventListener('click', (event)=>{
let modSelectorEl = document.getElementById('mod-selector-dialogue');
modSelectorEl.classList.toggle('show-settings-page');
event.preventDefault();
});
document.getElementById('use-custom-theme').addEventListener('change', function() {
document.getElementById('custom-theme-pickers').style.display = (this.checked ? 'block' : 'none');
updateAdvancedSetting('useCustomTheme', this.checked);
});
document.getElementById('fullscreen-at-start').addEventListener('change', function() {
updateAdvancedSetting('fullscreenStartsOn', this.checked);
});
document.getElementById('timer-starts-on').addEventListener('change', function() {
updateAdvancedSetting('timerStartsOn', this.checked);
});
/*document.getElementById('hidden-mod-toggle').addEventListener('change', function() {
updateAdvancedSetting('showHiddenMods', this.checked);
});*/
document.getElementById('muted-starts-on').addEventListener('change', function() {
updateAdvancedSetting('mutedStartsOn', this.checked);
});
document.getElementById('background-color-picker').addEventListener('input', function() {
updateAdvancedSetting('backgroundColor', this.value);
});
document.getElementById('custom-theme-col1').addEventListener('input', function() {
updateAdvancedSetting('themeCol1', this.value);
});
document.getElementById('custom-theme-col2').addEventListener('input', function() {
updateAdvancedSetting('themeCol2', this.value);
});
document.getElementById('custom-theme-col3').addEventListener('input', function() {
updateAdvancedSetting('themeCol3', this.value);
});
document.getElementById('custom-theme-col4').addEventListener('input', function() {
updateAdvancedSetting('themeCol4', this.value);
});
document.getElementById('custom-theme-col5').addEventListener('input', function() {
updateAdvancedSetting('themeCol5', this.value);
});
document.getElementById('custom-theme-col6').addEventListener('input', function() {
updateAdvancedSetting('themeCol6', this.value);
});
document.getElementById('custom-theme-col7').addEventListener('input', function() {
updateAdvancedSetting('themeCol7', this.value);
});
document.getElementById('hide-indicator').addEventListener('change', function() {
updateAdvancedSetting('hideIndicator', this.checked);
});
document.getElementById('dark-mod-theme').addEventListener('change', function() {
updateAdvancedSetting('darkModTheme', this.checked);
});
if(WEB_VERSION) {
document.getElementById('use-mobile-website').addEventListener('change', function() {
updateAdvancedSetting('useMobileWebsite', this.value);
})
}
//background image url text box also has some code to prevent wasd being eaten
document.getElementById('background-image-url').addEventListener('input', function() {
updateAdvancedSetting('backgroundImageUrl', this.value);
});
document.getElementById('background-image-url').addEventListener('keypress', function(e) {
e.stopPropagation();
});
document.getElementById('background-image-url').addEventListener('keydown', function(e) {
e.stopPropagation();
});
if(IS_DEVELOPER_MODE) {
document.getElementById('custom-mod-name').addEventListener('input', function() {
updateAdvancedSetting('customModName', this.value);
});
document.getElementById('custom-url').addEventListener('input', function() {
updateAdvancedSetting('customUrl', this.value);
});
document.getElementById('custom-mod-name').addEventListener('keypress', function(e) {
e.stopPropagation();
});
document.getElementById('custom-url').addEventListener('keypress', function(e) {
e.stopPropagation();
});
document.getElementById('custom-mod-name').addEventListener('keydown', function(e) {
e.stopPropagation();
});
document.getElementById('custom-url').addEventListener('keydown', function(e) {
e.stopPropagation();
});
}
//Event listener for toggling the early access/hidden mods
/*document.getElementById('hidden-mod-toggle').addEventListener('change', function() {
if(this.checked) {
[...document.getElementsByClassName('start-hidden')].forEach(el=>el.classList.add('show-hidden'));
} else {
[...document.getElementsByClassName('start-hidden')].forEach(el=>el.classList.remove('show-hidden'));
}
});*/
//Hide mod selector dialogue when clicking close button
document.getElementById('close-mod-selector').addEventListener('click', function() {
document.getElementById('mod-selector-dialogue-container').style.display = 'none';
});
//Apply button should save settings and refresh page
document.getElementById('apply-mod').addEventListener('click', function(event) {
//Figure out if advanced settings have been changed.
let shallowEquality = true;
for(let setting in advancedSettings) {
if(advancedSettings[setting] !== advancedSettingsOriginal[setting]) {
shallowEquality = false;
break;
}
}
let gameVersionChanged = false;
if(WEB_VERSION) {
const originalGameVersion = getGameVersionFromUrl();
const newGameVersion = parseInt(document.getElementById('mod-game-version').value);
if(originalGameVersion !== newGameVersion) {
gameVersionChanged = true;
}
}
//Skip if settings/mod chosen are the same as before.
if(shallowEquality && newlySelectedMod === currentlySelectedMod && !gameVersionChanged) {
alert('Settings are the same as before!')
return;
}
//Save new settings
localStorage.setItem('snakeChosenMod', newlySelectedMod);
localStorage.setItem('snakeAdvancedSettings',JSON.stringify(advancedSettings));
//Refresh if the mod has changed or the developer settings (custom mod name/url) have been changed
//otherwise apply the settings to the "live" game
if(WEB_VERSION && gameVersionChanged) {
const newGameVersion = parseInt(document.getElementById('mod-game-version').value);
redirectToSpecificGameVersion(newGameVersion);
} else if(
newlySelectedMod !== currentlySelectedMod ||
(advancedSettings.hasOwnProperty('customModName') && advancedSettings.customModName !== advancedSettingsOriginal.customModName) ||
(advancedSettings.hasOwnProperty('customUrl') && advancedSettings.customUrl !== advancedSettingsOriginal.customUrl) ||
(WEB_VERSION && advancedSettings.hasOwnProperty('useMobileWebsite') && advancedSettings.useMobileWebsite !== advancedSettingsOriginal.useMobileWebsite)
) {
location.reload();
} else {
//Apply dark mod theme setting if toggled.
if(advancedSettings.darkModTheme) {
document.getElementById('mod-indicator').classList.add('dark-mod-theme');
document.getElementById('mod-selector-dialogue-container').classList.add('dark-mod-theme');
} else {
document.getElementById('mod-indicator').classList.remove('dark-mod-theme');
document.getElementById('mod-selector-dialogue-container').classList.remove('dark-mod-theme');
}
//Apply background colour on fbx
//web snake
if (IS_FBX_OR_WEB && typeof advancedSettings.backgroundColor === 'string') {
document.body.style.backgroundColor = advancedSettings.backgroundColor;
}
//Apply url to background
if(IS_FBX_OR_WEB && typeof advancedSettings.backgroundImageUrl === 'string' &&
advancedSettingsOriginal.backgroundImageUrl !== advancedSettings.backgroundImageUrl) {
updateBackgroundImage(advancedSettings.backgroundImageUrl, true);
}
//Apply custom theme setting
if(advancedSettings.useCustomTheme) {
//Only update this if the colours have been changed or the theme was turned off.
let hasThemeChanged = advancedSettings.themeCol1 !== advancedSettingsOriginal.themeCol1 ||
advancedSettings.themeCol2 !== advancedSettingsOriginal.themeCol2 ||
advancedSettings.themeCol3 !== advancedSettingsOriginal.themeCol3 ||
advancedSettings.themeCol4 !== advancedSettingsOriginal.themeCol4 ||
advancedSettings.themeCol5 !== advancedSettingsOriginal.themeCol5 ||
advancedSettings.themeCol6 !== advancedSettingsOriginal.themeCol6 ||
advancedSettings.themeCol7 !== advancedSettingsOriginal.themeCol7 ||
(!advancedSettingsOriginal.useCustomTheme && advancedSettings.useCustomTheme);
if(hasThemeChanged) {
window.snake && window.snake.setCustomTheme(
advancedSettings.themeCol1 ?? '#1D1D1D',
advancedSettings.themeCol2 ?? '#161616',
advancedSettings.themeCol3 ?? '#111111',
advancedSettings.themeCol4 ?? '#000000',
advancedSettings.themeCol5 ?? '#1D1D1D',
advancedSettings.themeCol6 ?? '#111111',
advancedSettings.themeCol7 ?? '#000000'
);
}
}
//Clear the theme if useCustomTheme was just unchecked
if(advancedSettingsOriginal.useCustomTheme && !advancedSettings.useCustomTheme) {
window.snake && window.snake.clearCustomTheme();
}
//Make note of the new settings in-case the user decides to change them again.
advancedSettingsOriginal = {...advancedSettings};//Shallow copy, but it's ok as nothing is nested.
document.getElementById('apply-mod').textContent = 'Applied!'
}
});
let attemptsApplyingAdvancedSettings = 0;
setTimeout(applyAdvancedSnakeSettingsToGame, 300);//Small delay to give the game more time to load.
setTimeout(applyAdvancedNonSnakeSettingsToGame,300);
document.body.addEventListener('keydown',function(event) {
if(event.key === 'h') {
let modIndicatorEl = document.getElementById('mod-indicator');
modIndicatorEl.style.display = (modIndicatorEl.style.display === 'block' ? 'none' : 'block');
}
});
function updateAdvancedSettingInputs() {
if(advancedSettings.hasOwnProperty('fullscreenStartsOn')) {
document.getElementById('fullscreen-at-start').checked = advancedSettings.fullscreenStartsOn;
}
if(advancedSettings.hasOwnProperty('timerStartsOn')) {
document.getElementById('timer-starts-on').checked = advancedSettings.timerStartsOn;
}
/*if(advancedSettings.hasOwnProperty('showHiddenMods')) {
document.getElementById('hidden-mod-toggle').checked = advancedSettings.showHiddenMods;
}*/
if(advancedSettings.hasOwnProperty('mutedStartsOn')) {
document.getElementById('muted-starts-on').checked = advancedSettings.mutedStartsOn;
}
if(advancedSettings.hasOwnProperty('darkModTheme')) {
document.getElementById('dark-mod-theme').checked = advancedSettings.darkModTheme;
}
if(WEB_VERSION && advancedSettings.hasOwnProperty('useMobileWebsite')) {
document.getElementById('use-mobile-website').value = advancedSettings.useMobileWebsite;
}
if(advancedSettings.hasOwnProperty('hideIndicator')) {
document.getElementById('hide-indicator').checked = advancedSettings.hideIndicator;
}
if(advancedSettings.hasOwnProperty('useCustomTheme')) {
document.getElementById('use-custom-theme').checked = advancedSettings.useCustomTheme;
document.getElementById('custom-theme-pickers').style.display = (advancedSettings.useCustomTheme ? 'block' : 'none');
} else {
document.getElementById('custom-theme-pickers').style.display = 'none';
}
if(advancedSettings.hasOwnProperty('backgroundColor')) {
document.getElementById('background-color-picker').value = advancedSettings.backgroundColor;
}
if(advancedSettings.hasOwnProperty('backgroundImageUrl')) {
document.getElementById('background-image-url').value = advancedSettings.backgroundImageUrl;
}
if(advancedSettings.hasOwnProperty('themeCol1')) {
document.getElementById('custom-theme-col1').value = advancedSettings.themeCol1;
}
if(advancedSettings.hasOwnProperty('themeCol2')) {
document.getElementById('custom-theme-col2').value = advancedSettings.themeCol2;
}
if(advancedSettings.hasOwnProperty('themeCol3')) {
document.getElementById('custom-theme-col3').value = advancedSettings.themeCol3;
}
if(advancedSettings.hasOwnProperty('themeCol4')) {
document.getElementById('custom-theme-col4').value = advancedSettings.themeCol4;
}
if(advancedSettings.hasOwnProperty('themeCol5')) {
document.getElementById('custom-theme-col5').value = advancedSettings.themeCol5;
}
if(advancedSettings.hasOwnProperty('themeCol6')) {
document.getElementById('custom-theme-col6').value = advancedSettings.themeCol6;
}
if(advancedSettings.hasOwnProperty('themeCol7')) {
document.getElementById('custom-theme-col7').value = advancedSettings.themeCol7;
}
if(IS_DEVELOPER_MODE) {
if(advancedSettings.hasOwnProperty('customModName')) {
document.getElementById('custom-mod-name').value = advancedSettings.customModName;
}
if(advancedSettings.hasOwnProperty('customUrl')) {
document.getElementById('custom-url').value = advancedSettings.customUrl;
}
}
}
function updateAdvancedSetting(settingName, settingValue) {
advancedSettings[settingName] = settingValue;
showSettingChanged();
}
//Advanced settings that need to wait for window.snake
function applyAdvancedSnakeSettingsToGame() {
if(attemptsApplyingAdvancedSettings > 10) {
//Stop trying to apply advanced setting if we've tried this much and the game still isn't ready
console.log('window.snake is still not available after retrying many times. Skipping applying advanced snake settings');
} else if(!window.snake) {
//Game not ready. Wait a bit and then try again.
console.log('window.snake not ready for when we apply advanced settings. Will retry again after waiting.');
attemptsApplyingAdvancedSettings++;
setTimeout(applyAdvancedSnakeSettingsToGame, 300);
} else {
if(advancedSettings.timerStartsOn) {
if(window.isSnakeMobileVersion) {
console.log('Skipping turning timer on due to being on mobile version.');
} else {
window.snake.speedrun();
}
}
if(advancedSettings.useCustomTheme) {
window.snake.setCustomTheme(
advancedSettings.themeCol1 ?? '#1D1D1D',
advancedSettings.themeCol2 ?? '#161616',
advancedSettings.themeCol3 ?? '#111111',
advancedSettings.themeCol4 ?? '#000000',
advancedSettings.themeCol5 ?? '#1D1D1D',
advancedSettings.themeCol6 ?? '#111111',
advancedSettings.themeCol7 ?? '#000000'
);
}
}
}
function applyAdvancedNonSnakeSettingsToGame() {
if(advancedSettings.fullscreenStartsOn) {
applyFullscreenToGame();
}
if(advancedSettings.mutedStartsOn) {
applyMuteToGame();
}
if(advancedSettings.showHiddenMods) {
[...document.getElementsByClassName('start-hidden')].forEach(el=>el.classList.add('show-hidden'));
}
if(advancedSettings.darkModTheme) {
document.getElementById('mod-indicator').classList.add('dark-mod-theme');
document.getElementById('mod-selector-dialogue-container').classList.add('dark-mod-theme');
}
if(advancedSettings.hideIndicator) {
setTimeout(function() {
if(window.showSnakeErrMessage) {return;}
let modIndicatorEl = document.getElementById('mod-indicator');
if(modIndicatorEl.style.display === 'block') {
modIndicatorEl.style.display = 'none';
}
}, 5000);
}
if(IS_FBX_OR_WEB && typeof advancedSettings.backgroundColor === 'string') {
document.body.style.backgroundColor = advancedSettings.backgroundColor;
}
//Apply url to background
if(IS_FBX_OR_WEB && typeof advancedSettings.backgroundImageUrl === 'string') {
updateBackgroundImage(advancedSettings.backgroundImageUrl, false);
}
}
function applyMuteToGame() {
//On fbx we can mute right way. On search, we need to wait until the game is visible.
if(IS_FBX_OR_WEB) {
//Match mute button, but only if it's on
//Old method (mute button is an image)
//Check image url includes the word up instead of the word off, to detect whether or not it is on
let muteButtonOld = document.querySelector('img.EFcTud[jsaction="DGXxE"]:not([src*="off"])');
if(muteButtonOld) {muteButtonOld.click();}
//New method (mute button is a div containing an svg)
//get the 2nd image, and check if it is hidden. If so, then click on it to toggle mute
let muteButtonNew = document.querySelectorAll('div.EFcTud[jsaction="DGXxE"]')?.[1];
if(muteButtonNew && muteButtonNew.classList.contains('LaTyvd')) {
muteButtonNew.click();
}
return;
}
//Only true if we can find the invis el and it has style "None"
let someRandomGameContainer = document.getElementsByClassName('ynlwjd')[0];
let isGameInvis = someRandomGameContainer && someRandomGameContainer.style.display === 'none';
//Handle search snake here.
if(isGameInvis) {
console.log('Game not visible yet. Waiting to apply mute.');
setTimeout(applyMuteToGame, 400);
} else {
//Game is visible so safe to mute. (See comment several lines above for why we have two ways to do this)
let muteButtonOld = document.querySelector('img.EFcTud[jsaction="DGXxE"]:not([src*="off"])');
if(muteButtonOld) {muteButtonOld.click();}
let muteButtonNew = document.querySelectorAll('div.EFcTud[jsaction="DGXxE"]')?.[1];
if(muteButtonNew && muteButtonNew.classList.contains('LaTyvd')) {
muteButtonNew.click();
}
}
}
function applyFullscreenToGame() {
//On fbx we can fullscreen right way. On search, we need to wait until the game is visible.
if(IS_FBX_OR_WEB) {
//Match fullscreen button, but only if it's on
//Old method (button is an image)
//Check if the button is off (i.e. the image url doesn't have the word exit)
let fullscreenButtonOld = document.querySelector('img.EFcTud[jsaction="zeJAAd"]:not([src*="exit"])');
if(fullscreenButtonOld) {fullscreenButtonOld.click();}
//New method (button is a div containing an svg)
//get the 2nd image, and check if it is hidden. If so, then click on it to toggle fullscreen
let fullscreenButtonNew = document.querySelectorAll('div.EFcTud[jsaction="zeJAAd"]')?.[1];
if(fullscreenButtonNew && fullscreenButtonNew.classList.contains('LaTyvd')) {
fullscreenButtonNew.click();
}
return;
}
//Only true if we can find the invis el and it has style "None"
let someRandomGameContainer = document.getElementsByClassName('ynlwjd')[0];
let isGameInvis = someRandomGameContainer && someRandomGameContainer.style.display === 'none';
//Handle search snake here.
if(isGameInvis) {
console.log('Game not visible yet. Waiting to apply fullscreen.');
setTimeout(applyFullscreenToGame, 400);
} else {
//Game is visible so safe to fullscreen. (See comment several lines above for why we have two ways to do this)
let fullscreenButtonOld = document.querySelector('img.EFcTud[jsaction="zeJAAd"]:not([src*="exit"])');
if(fullscreenButtonOld) {fullscreenButtonOld.click();}
let fullscreenButtonNew = document.querySelectorAll('div.EFcTud[jsaction="zeJAAd"]')?.[1];
if(fullscreenButtonNew && fullscreenButtonNew.classList.contains('LaTyvd')) {
fullscreenButtonNew.click();
}
}
}
//Change the background image on body.
function updateBackgroundImage(url, alertValidationMessage) {
url = url.trim();
if(url === '' || typeof url !== 'string') {
document.body.style.backgroundImage = 'none';
return;
}
if((!url.includes('.') || !url.includes('/')) && !url.startsWith('data:')) {
document.body.style.backgroundImage = 'none';
alertValidationMessage && alert(`Background image is not a proper url. Received: ${url}`);
return;
}
//Add https if missing
if(!url.startsWith('http') && !url.startsWith('data:')) {
url = `https://${url}`;
}
let image = new Image();
image.addEventListener('load', function() {
document.body.style.backgroundImage = `url(${url})`;
document.body.style.backgroundRepeat = 'no-repeat';
document.body.style.backgroundAttachment = 'fixed';
document.body.style.backgroundPosition = 'center center';
document.body.style.backgroundSize = 'cover';
});
image.addEventListener('error', function() {
alertValidationMessage && alert(`Background image failed to load. Tried to load the following: ${url}`);
});
image.src = url;
}
//Fetches json file with latest version and shows the update link if it is different to our version
function showUpdateLinkIfNeeded() {
if(externalConfig.hasError) {
return;
}
try {
let modInfo = externalConfig.modInfo;
let latestVersion = modInfo.version;
let updateNeeded = latestVersion !== VERSION;
//Web version
if(WEB_VERSION) {
updateNeeded = false;
}
if(updateNeeded && !IS_DEVELOPER_MODE) {
document.getElementById('update-link').style.display = 'inline';
document.getElementById('update-link-text').textContent = `(update to ${latestVersion})`;
}
//Have an option to not show the popup on either fbx or search snake (e.g. if only one of them is broken)
if(IS_FBX_OR_WEB ? modInfo.startMessage.excludeFbx : modInfo.startMessage.excludeSearch) {
return;
}
if(modInfo.startMessage.showToEveryone || (modInfo.startMessage.showIfUpdateNeeded && updateNeeded)) {
showStartMessagePopup(modInfo);
}
} catch(err) {
console.error(err);
}
}
function showStartMessagePopup(modInfo) {
//web snake
if(WEB_VERSION) {
modInfo.startMessage.showUpdatePrompt = false;
}
const messageHeading = `
`;
//Insert html for start-message dialogue box.
let startMessageModalContainer = document.createElement('div');
startMessageModalContainer.innerHTML = startMessagePopup;
startMessageModalContainer.id = 'start-message-dialogue-container';
startMessageModalContainer.style = 'position:fixed; width:100%; height:100%; z-index: 10000001; left:0; top:0';
document.body.appendChild(startMessageModalContainer);
document.getElementById('start-message-explanatory-text').textContent = modInfo.startMessage.explanatoryText;
if(advancedSettings && advancedSettings.darkModTheme) {
document.getElementById('start-message-dialogue-container').classList.add('dark-mod-theme');
}
document.getElementById('close-start-message').addEventListener('click', function() {
document.getElementById('start-message-dialogue-container').remove();
});
}
//Currently, we just ensure that the apply button doesn't have the "Applied!" text
//In the future, we may make is more obvious that clicking apply is needed.
function showSettingChanged() {
document.getElementById('apply-mod').textContent = 'Apply';
}
//It's possible that the snake code has changed URL or changed in some other way and we weren't able to alter it. If so then show an indicator.
function checkFoundSnakeCode() {
const currentlySelectedMod = localStorage.getItem('snakeChosenMod');
if(currentlySelectedMod === null || currentlySelectedMod === 'none') {
//Don't worry about whether or not we've found the snake code since we're not running a mod.
return;
}
if(window.hasFoundSnakeCodeYet) {
//We're good, we've found the snake code so it hasn't changed location
return;
}
//Otherwise show a message to the user.
let codeNotFoundMessage = document.getElementById('code-not-found-message');
if(codeNotFoundMessage) {
codeNotFoundMessage.style.display = 'block';
}
//Make sure that the indicator is actually visible
let modIndicatorEl = document.getElementById('mod-indicator');
if(modIndicatorEl) {
modIndicatorEl.style.display = 'block';
}
window.showSnakeErrMessage = true;//Used to prevent the indicator being auto-hidden
}
}
function showErrorTryGsmPopup() {
//Don't show anything if already on web snake
if(WEB_VERSION) {
return;
}
let googlesnakemodscomHref = 'https://googlesnakemods.com/v/current/';
let storedMod = localStorage.getItem('snakeChosenMod');
if(typeof storedMod === 'string' && /^[a-z0-9 ._]*$/i.test(storedMod)) {
googlesnakemodscomHref += 'index.html?mod=' + storedMod;
}
const tryGsmPopup = `
Error Occurred
We have a site where mods will always work. We recommend playing there instead.
`;
//Insert html for try-gsm dialogue box.
let tryGsmModalContainer = document.createElement('div');
tryGsmModalContainer.innerHTML = tryGsmPopup;
tryGsmModalContainer.id = 'try-gsm-dialogue-container';
tryGsmModalContainer.style = 'position:fixed; width:100%; height:100%; z-index: 10000000; left:0; top:0';
document.body.appendChild(tryGsmModalContainer);
//Dark theme - lazy code to grab advanced settings again
let advancedSettings = JSON.parse(localStorage.getItem('snakeAdvancedSettings')) ?? {};
if(advancedSettings && advancedSettings.darkModTheme) {
document.getElementById('try-gsm-dialogue-container').classList.add('dark-mod-theme');
}
document.getElementById('close-try-gsm').addEventListener('click', function() {
document.getElementById('try-gsm-dialogue-container').remove();
});
document.getElementById('confirm-try-gsm').addEventListener('click', function() {
location.href = googlesnakemodscomHref;
});
}
//Generate the game versions option dropdown for the mod-game-version select element
function attemptRenderGameVersionOptions(selectedMod, initial = false) {
if(!WEB_VERSION) {
//Only available for web version
return;
}
let modsConfig = externalConfig.modInfo.modsConfig;
let versionsArray = []; //Contains the versions we want to show in dropdown
//"none" mod and similar will show all versions in dropdown
let mightHaveVersionsSpecified = modsConfig.hasOwnProperty(selectedMod) && ['customUrl', 'testMod'].includes(selectedMod) === false;
if(mightHaveVersionsSpecified) {
//Try to get versions that this mod supports
let webVersions;
webVersions = modsConfig[selectedMod].web;
if(Array.isArray(webVersions) && webVersions.length !== 0) {
versionsArray = webVersions.map(v=>v.version);
//Check for invalid version numbers
if(versionsArray.find(v=>typeof v !== 'number' || !Number.isInteger(v) || v <= 0)) {
alert('Invalid game version numbers for selected mod');
throw new Error('Illegal version numbers for selected mod, these should be integers');
}
//Strip out version numbers that are too high
versionsArray = versionsArray.filter(v=>v<=webLatestVersion);
versionsArray = versionsArray.sort();
}
}
//Fallback behaviour is to just list versions as 1, 2, 3, ..., webLatestVersion
if(versionsArray.length === 0) {
versionsArray = new Array(webLatestVersion).fill(null).map((_, i)=>i + 1);
}
const versionsDropDown = document.getElementById('mod-game-version');
versionsDropDown.innerHTML = '';
//Add options to the versions from high to low
for(let i = versionsArray.length - 1; i >= 0; i--) {
let option = document.createElement('option');
option.value = versionsArray[i];
option.textContent = versionsArray[i];
versionsDropDown.appendChild(option);
}
//Pre-select the correct option
if(initial) {
let currentGameVersion = getGameVersionFromUrl();
//Default to the first version number greater than the one in the url
let version = versionsArray.find(v=>v >= currentGameVersion) ?? versionsArray[versionsArray.length - 1];
versionsDropDown.value = version;
} else {
versionsDropDown.value = versionsArray[versionsArray.length - 1];
}
}
//Figures out which version the current page is on
function getGameVersionFromUrl() {
if(!WEB_VERSION) {
throw new Error('This function should only be used on the web version');
}
let thisUrl = window.location.href;
if(thisUrl.includes('v/current')) {
return webLatestVersion
} else {
return parseInt(thisUrl.match(/v\/(\d+)/)[1]);
}
}
function redirectToSpecificGameVersion(gameVersion) {
if(gameVersion === webLatestVersion) {
window.location.href = '../../v/current/';
} else {
window.location.href = `../../v/${gameVersion}`;
}
console.log('Redirecting to ' + gameVersion);
}
window.testMod = {};
window.testMod.runCodeBefore = function() {
}
window.testMod.alterSnakeCode = function(code) {
code = code.replaceAll('.66','.36')
return code;
}
window.testMod.runCodeAfter = function() {
}
function loadAndRunCodeSynchronous(url) {
let req = new XMLHttpRequest();
req.open('GET', url, false);
req.onload = function() {
if(this.status === 200) {
(1,eval)(this.responseText);
} else {
console.log(`Loading selected mod returned non-200 status. Received: ${this.status}`);
}
};
req.onerror = function(event) {
console.error(`Error when attempting to retrieve mod code from ${url}`);
console.log(event);
};
req.send();
}
window.addEventListener('load', addModSelectorPopup);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Utility functions below
////////////////////////////////////////////////////////////////////////////////////////////////////////////
window.swapInMainClassPrototype = function(mainClass, functionText) {
if(/^[$a-zA-Z0-9_]{0,8}=function/.test(functionText)) {
throw new Error("Error, function is of form abc=function(), but this only works for stuff like s_.abc=function()");
}
functionText = assertReplace(functionText, /^[$a-zA-Z0-9_]{0,8}/,`${mainClass}.prototype`);
return functionText;
}
/*
This function will search for a function/method in some code and return this function as a string
code will usually be the snake source code
functionSignature will be regex matching the beginning of the function/method (must end in $),
for example if we are trying to find a function like s_xD = function(a, b, c, d, e) {......}
then put functionSignature = /[$a-zA-Z0-9_]{0,8}=function\(a,b,c,d,e\)$/
somethingInsideFunction will be regex matching something in the function
for example if we are trying to find a function like s_xD = function(a, b, c, d, e) {...a.Xa&&10!==a.Qb...}
then put somethingInsideFunction = /a\.[$a-zA-Z0-9_]{0,8}&&10!==a\.[$a-zA-Z0-9_]{0,8}/
*/
window.findFunctionInCode = function(code, functionSignature, somethingInsideFunction, logging = false) {
let functionSignatureSource = functionSignature.source;
let functionSignatureFlags = functionSignature.flags;//Probably empty string
if(!functionSignatureFlags.includes('g')) {
functionSignatureFlags += 'g';
}
/*Remove any trailling $ signs from the end of the function signature (this is a legacy issue, I know it's bad)*/
functionSignatureSource = functionSignatureSource.replaceAll(/\$(?=\|)|\$$/g, '');
/*Allow line breaks after commas or =. This is bit sketchy, but should be ok as findFunctionInCode is used in a quite limited way*/
functionSignatureSource.replaceAll(/,|=/g,'$&\\n?');
functionSignature = new RegExp(functionSignatureSource, functionSignatureFlags);
/*get the position of somethingInsideFunction*/
let indexWithinFunction = code.search(somethingInsideFunction);
if (indexWithinFunction == -1) {
console.log("%cCouldn't find a match for somethingInsideFunction", "color:red;");
diagnoseRegexError(code, somethingInsideFunction);
}
/*Find the occurence of function signature closest to where we found somethingInsideFunction, then count brackets
to find the end of the function*/
let codeBeforeMatch = code.substring(0, indexWithinFunction);
let signatureMatches = [...codeBeforeMatch.matchAll(functionSignature)];
if(signatureMatches.length === 0) {
throw new Error("Couldn't find function signature");
}
let startIndex = signatureMatches[signatureMatches.length - 1].index;
let bracketCount = 0;
let foundFirstBracket = false;
let endIndex = 0;
/*Use bracket counting to find the whole function*/
let codeLength = code.length;
for (let i = startIndex; i <= codeLength; i++) {
if (!foundFirstBracket && code[i] == "{") {
foundFirstBracket = true;
}
if (code[i] == "{") {
bracketCount++;
}
if (code[i] == "}") {
bracketCount--;
}
if (foundFirstBracket && bracketCount == 0) {
endIndex = i;
break;
}
if (i == codeLength) {
throw new Error("Couldn't pair up brackets");
}
}
let fullFunction = code.substring(startIndex, endIndex + 1);
/*throw error if fullFunction doesn't contain something inside function - i.e. function signature was wrong*/
if (fullFunction.search(somethingInsideFunction) === -1) {
throw new Error("Function signature does not belong to the same function as somethingInsideFunction");
}
if (logging) {
console.log(fullFunction);
}
return fullFunction;
}
/*
Same as replace, but throws an error if nothing is changed
*/
window.assertReplace = function(baseText, regex, replacement) {
if (typeof baseText !== 'string') {
throw new Error('String argument expected for assertReplace');
}
let outputText = baseText.replace(regex, replacement);
//Throw warning if nothing is replaced
if (baseText === outputText) {
diagnoseRegexError(baseText, regex);
}
return outputText;
}
/*
Same as replaceAll, but throws an error if nothing is changed
*/
window.assertReplaceAll = function(baseText, regex, replacement) {
if (typeof baseText !== 'string') {
throw new Error('String argument expected for assertReplace');
}
let outputText = baseText.replaceAll(regex, replacement);
//Throw warning if nothing is replaced
if (baseText === outputText) {
diagnoseRegexError(baseText, regex);
}
return outputText;
}
//Alternate way to use assertReplace. Example: code = code.assertReplace('Thing to change', 'New thing');
String.prototype.assertReplace = function(regex, replacement) {
return assertReplace(this.toString(), regex, replacement);
};
//Same as above for assertReplaceAll.
String.prototype.assertReplaceAll = function(regex, replacement) {
return assertReplaceAll(this.toString(), regex, replacement);
};
//Also do this for assertMatch (We just have the prototype version for this).
//Same as match, but throws an error if it's null.
String.prototype.assertMatch = function(regex) {
let output = this.match(regex);
//Throw error if nothing was found
if(output === null) {
diagnoseRegexError(this, regex)
}
return output;
}
window.diagnoseRegexError = function(baseText, regex) {
if(!(regex instanceof RegExp)) {
throw new Error('Failed to find match using string argument. No more details available');
}
//see if removing line breaks works - in that case we can give a more useful error message
let oneLineText = baseText.replaceAll(/\n/g,'');
let res = regex.test(oneLineText);
//If line breaks don't solve the issue then throw a general error
if (!res) {
throw new Error('Failed to find match for regex.');
}
//Try to suggest correct regex to use for searching
let regexSource = regex.source;
let regexFlags = regex.flags;
//Look at all the spots where line breaks might occur and try adding \n? there to see if it makes a difference
//It might be easier to just crudely brute force putting \n? at each possible index?
for(let breakableChar of ["%","&","\\*","\\+",",","-","\\/",":",";","<","=",">","\\?","{","\\|","}"]) {
for(let pos = regexSource.indexOf(breakableChar); pos !== -1; pos = regexSource.indexOf(breakableChar, pos + 1)) {
//Remake the regex with a new line at the candidate position
let candidateRegexSource = `${regexSource.slice(0,pos + breakableChar.length)}\\n?${regexSource.slice(pos + breakableChar.length)}`;
let candidateRegex;
try{
candidateRegex = new RegExp(candidateRegexSource, regexFlags);
} catch(err) {
continue;
}
//See if the new regex works
let testReplaceResult = candidateRegex.test(baseText);
if(testReplaceResult) {
//Success we found the working regex! Give descriptive error message to user and log suggested regex with new line in correct place
console.log(`Suggested regex improvement:
${candidateRegex}`);
throw new Error('Suggested improvement found! Error with line break, failed to find match for regex. See logged output for regex to use instead that should hopefully fix this.');
}
}
}
throw new Error('Line break error! Failed to failed to find match for regex - most likely caused by a new line break. No suggestions provided');
}
window.appendCodeWithinSnakeModule = function(snakeCode, codeToAdd, addSemicolonAfter) {
if(addSemicolonAfter) {
codeToAdd += ';';
}
//We could remove the first bit before the pipe symbol in the future. This is just to handle before + after a snake update.
var newSnakeCode = snakeCode.replace(/}\)\(this\._s\);\n\/\/ Google Inc\.|}\);\n\/\/ Google Inc\./, codeToAdd + '$&');
return newSnakeCode;
}
//Turns _.abc into _s.abc
window.swapInSnakeGlobal = function(text) {
return assertReplace(text, /^_\./, '_s.');
}