Aternos Auto Start & Extend

Auto-start when offline; auto-extend when online with 0 players and <60s left; dismiss notifications

// ==UserScript==
// @name         Aternos Auto Start & Extend
// @namespace    https://aternos.org/
// @version      1.1.0
// @description  Auto-start when offline; auto-extend when online with 0 players and <60s left; dismiss notifications
// @match        https://aternos.org/server
// @match        https://aternos.org/server/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const SELECTORS = {
    offlineContainer: 'div.status.offline',
    onlineContainer: 'div.status.online',
    startButton: '#start',
    players: '.live-status-box-value.js-players',
    countdown: '.server-end-countdown',
    extendButton: 'button.btn.btn-tiny.btn-success.server-extend-end',
    notificationHeader: 'header span.alert-title',
    notificationClose: 'i.fa-times.fa-solid',
  };

  let lastStartClickMs = 0;
  let lastExtendClickMs = 0;
  let lastNotificationClickMs = 0;
  let isRunning = true;

  // Cleanup function to prevent memory leaks
  function cleanup() {
    isRunning = false;
    if (window.aternosObserver) {
      window.aternosObserver.disconnect();
      delete window.aternosObserver;
    }
    if (window.aternosIntervals) {
      window.aternosIntervals.forEach(clearInterval);
      window.aternosIntervals = [];
    }
  }

  // Cleanup on page unload
  window.addEventListener('beforeunload', cleanup);

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
  }

  function queryText(selector) {
    const el = document.querySelector(selector);
    return el ? el.textContent.trim() : '';
  }

  function isOffline() {
    return !!document.querySelector(SELECTORS.offlineContainer);
  }

  function isOnline() {
    return !!document.querySelector(SELECTORS.onlineContainer);
  }

  function playersAreZero() {
    const txt = queryText(SELECTORS.players); // e.g., "0/20"
    return /^0\s*\/\s*\d+$/i.test(txt);
  }

  function getCountdownSeconds() {
    const txt = queryText(SELECTORS.countdown); // e.g., "5:22" or "59s"
    if (!txt) return null;

    const trimmed = txt.replace(/\s+/g, '');
    if (/^\d{1,2}:\d{2}$/.test(trimmed)) {
      const [m, s] = trimmed.split(':').map(Number);
      if (Number.isFinite(m) && Number.isFinite(s)) return m * 60 + s;
    } else if (/^\d{1,3}s$/i.test(trimmed)) {
      const s = Number(trimmed.slice(0, -1));
      if (Number.isFinite(s)) return s;
    }
    return null;
  }

  function hasNotification() {
    return !!document.querySelector(SELECTORS.notificationHeader);
  }

  function clickIfAvailable(selector, lastClickRef, minIntervalMs) {
    if (!isRunning) return false;
    
    const now = Date.now();
    if (now - lastClickRef.value < minIntervalMs) return false;
    
    const btn = document.querySelector(selector);
    if (!btn || !isVisible(btn) || btn.disabled) return false;
    
    btn.click();
    lastClickRef.value = now;
    return true;
  }

  function tryStartIfOffline() {
    if (!isRunning) return;
    if (!isOffline()) return;
    clickIfAvailable(SELECTORS.startButton, { get value() { return lastStartClickMs; }, set value(v) { lastStartClickMs = v; } }, 10000);
  }

  function tryExtendIfEndingSoon() {
    if (!isRunning) return;
    if (!isOnline()) return;
    if (!playersAreZero()) return;

    const seconds = getCountdownSeconds();
    if (seconds === null) return;

    // If <= 59 seconds remaining, extend
    if (seconds <= 59) {
      clickIfAvailable(SELECTORS.extendButton, { get value() { return lastExtendClickMs; }, set value(v) { lastExtendClickMs = v; } }, 5000);
    }
  }

  function tryDismissNotification() {
    if (!isRunning) return;
    if (!hasNotification()) return;
    
    clickIfAvailable(SELECTORS.notificationClose, { get value() { return lastNotificationClickMs; }, set value(v) { lastNotificationClickMs = v; } }, 2000);
  }

  // Initialize intervals array for cleanup
  window.aternosIntervals = [];

  // Initial delay to allow SPA content to render
  setTimeout(() => {
    if (!isRunning) return;

    // Poll for offline -> start (every 5 seconds)
    window.aternosIntervals.push(setInterval(tryStartIfOffline, 5000));
    
    // Poll for extend condition (every second)
    window.aternosIntervals.push(setInterval(tryExtendIfEndingSoon, 1000));
    
    // Poll for notifications (every 2 seconds)
    window.aternosIntervals.push(setInterval(tryDismissNotification, 2000));

    // Optimized mutation observer with throttling
    let mutationTimeout;
    const throttledMutationHandler = () => {
      if (mutationTimeout) return;
      mutationTimeout = setTimeout(() => {
        if (isRunning) {
          tryStartIfOffline();
          tryExtendIfEndingSoon();
          tryDismissNotification();
        }
        mutationTimeout = null;
      }, 1000); // Throttle to max once per second
    };

    // More targeted mutation observer to reduce memory usage
    window.aternosObserver = new MutationObserver((mutations) => {
      // Only react to significant changes
      const hasRelevantChanges = mutations.some(mutation => {
        if (mutation.type === 'childList') {
          // Check if status containers, buttons, or notifications were added/removed
          return Array.from(mutation.addedNodes).some(node => 
            node.nodeType === 1 && (
              node.matches && (
                node.matches('.status') || 
                node.matches('#start') || 
                node.matches('.server-extend-end') ||
                node.matches('header span.alert-title')
              )
            )
          );
        }
        return false;
      });

      if (hasRelevantChanges) {
        throttledMutationHandler();
      }
    });

    // Observe only the main content area instead of entire document
    const mainContent = document.querySelector('#main') || document.body;
    if (mainContent) {
      window.aternosObserver.observe(mainContent, { 
        childList: true, 
        subtree: true,
        attributes: false, // Don't watch attribute changes to reduce memory usage
        characterData: false // Don't watch text changes to reduce memory usage
      });
    }

    // Initial check
    tryStartIfOffline();
    tryExtendIfEndingSoon();
    tryDismissNotification();

  }, 1500);

  // Additional cleanup on visibility change (when tab becomes inactive)
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      // Pause some operations when tab is not visible
      isRunning = false;
    } else {
      // Resume when tab becomes visible again
      isRunning = true;
    }
  });

})();