TamaSkrypt – Launcher

Modułowy launcher TamaSkrypt: pobiera manifest, weryfikuje hash, ładuje moduły i uruchamia grę.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         TamaSkrypt – Launcher
// @namespace    https://github.com/qxsxhsyoj7369/TamaSkrypt
// @version      3.2.2
// @description  Modułowy launcher TamaSkrypt: pobiera manifest, weryfikuje hash, ładuje moduły i uruchamia grę.
// @author       TamaSkrypt / Gelek
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.github.com
// @connect      raw.githubusercontent.com
// @connect      cdn.jsdelivr.net
// @connect      gelek-995f2-default-rtdb.europe-west1.firebasedatabase.app
// @run-at       document-end
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  const GITHUB_OWNER = 'qxsxhsyoj7369';
  const GITHUB_REPO = 'TamaSkrypt';
  const GITHUB_BRANCH = 'main';
  const MANIFEST_PATH = 'manifest.json';
  const MANIFEST_URL = `https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/${GITHUB_BRANCH}/${MANIFEST_PATH}`;
  const MANIFEST_CDN_URL = `https://cdn.jsdelivr.net/gh/${GITHUB_OWNER}/${GITHUB_REPO}@${GITHUB_BRANCH}/${MANIFEST_PATH}`;
  const GITHUB_REF_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/git/ref/heads/${GITHUB_BRANCH}`;
  const RAW_REPO_PREFIX = `https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/`;
  const FIREBASE_DB_URL = 'https://gelek-995f2-default-rtdb.europe-west1.firebasedatabase.app';
  const CHECK_INTERVAL_MS = 0;

  const KEY_CODE = '__ts_launcher_code__';
  const KEY_VERSION = '__ts_launcher_version__';
  const KEY_CHECKED = '__ts_launcher_checked__';
  const KEY_SCHEMA_VER = '__ts_schema_version__';
  const KEY_MODE = '__ts_launcher_mode__';
  const KEY_MODULE_MANIFEST = '__ts_launcher_modules_manifest__';
  const KEY_MODULE_META = '__ts_launcher_modules_meta__';
  const KEY_MODULE_CODE_PREFIX = '__ts_launcher_mod_code__::';
  const KEY_COMMIT_SHA = '__ts_launcher_commit_sha__';
  const HOST_SKIP_RE = /(^|\.)github\.com$|(^|\.)githubusercontent\.com$/i;

  function setLauncherDiagnostics(info) {
    const current = window.__TS_LAUNCHER_DIAGNOSTICS__ || {};
    window.__TS_LAUNCHER_DIAGNOSTICS__ = {
      mode: info && info.mode ? info.mode : current.mode || 'unknown',
      manifestVersion: info && info.manifestVersion ? String(info.manifestVersion) : current.manifestVersion || '',
      source: info && info.source ? info.source : current.source || 'unknown',
      commitSha: info && info.commitSha ? String(info.commitSha) : current.commitSha || '',
      updatedAt: Date.now(),
    };
  }

  function rewriteRawGithubUrl(url, ref) {
    if (!url || !ref || typeof url !== 'string') return url;
    if (!url.startsWith(RAW_REPO_PREFIX)) return url;
    const rest = url.slice(RAW_REPO_PREFIX.length);
    const slashIdx = rest.indexOf('/');
    if (slashIdx < 0) return url;
    const pathPart = rest.slice(slashIdx + 1);
    return RAW_REPO_PREFIX + ref + '/' + pathPart;
  }

  function applyCommitShaToManifest(manifest, commitSha) {
    if (!manifest || !commitSha) return manifest;
    const cloned = JSON.parse(JSON.stringify(manifest));
    cloned.__commitSha = commitSha;
    cloned.scriptUrl = rewriteRawGithubUrl(cloned.scriptUrl, commitSha);
    if (Array.isArray(cloned.modules)) {
      cloned.modules = cloned.modules.map((entry) => ({
        ...entry,
        url: rewriteRawGithubUrl(entry.url, commitSha),
      }));
    }
    return cloned;
  }

  function getJson(key, fallback) {
    try {
      const raw = GM_getValue(key, null);
      if (raw === null || raw === undefined || raw === '') return fallback;
      return JSON.parse(raw);
    } catch {
      return fallback;
    }
  }

  function setJson(key, value) {
    GM_setValue(key, JSON.stringify(value));
  }

  function stripHeader(code) {
    return code.replace(/\/\/\s*==UserScript==[\s\S]*?\/\/\s*==\/UserScript==/m, '');
  }

  async function runCode(code, label) {
    try {
      // eslint-disable-next-line no-eval
      eval(stripHeader(code));
      return true;
    } catch (error) {
      console.error('[TamaSkrypt Launcher] Błąd wykonania:', label, error);
      return false;
    }
  }

  function shouldSkipOnHost() {
    const host = (window.location && window.location.hostname) ? String(window.location.hostname) : '';
    return HOST_SKIP_RE.test(host);
  }

  function requestText(url, timeout = 15000) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: url + (url.includes('?') ? '&' : '?') + '_=' + Date.now(),
        timeout,
        onload: function (resp) {
          if (resp.status < 200 || resp.status >= 300) {
            reject(new Error(`HTTP ${resp.status} for ${url}`));
            return;
          }
          resolve(resp.responseText);
        },
        onerror: function () { reject(new Error(`Network error for ${url}`)); },
        ontimeout: function () { reject(new Error(`Timeout for ${url}`)); },
      });
    });
  }

  async function sha256Hex(text) {
    const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
    return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('');
  }

  function normalizeHashInput(text) {
    return String(text || '')
      .replace(/^\uFEFF/, '')
      .replace(/\r\n/g, '\n')
      .replace(/\r/g, '\n');
  }

  async function verifyHash(text, expectedHash) {
    if (!expectedHash) return true;
    const normalized = String(expectedHash).toLowerCase().replace(/^sha256:/, '');
    const got = await sha256Hex(normalizeHashInput(text));
    return got === normalized;
  }

  function showSchemaUpdateAlert(newVersion) {
    if (sessionStorage.getItem('__ts_schema_alert_shown__')) return;
    sessionStorage.setItem('__ts_schema_alert_shown__', 'true');
    console.warn(`[TamaSkrypt] Schemat bazy Firebase został zaktualizowany do v${newVersion}`);
  }

  async function checkFirebaseSchemaVersion() {
    try {
      const body = await requestText(FIREBASE_DB_URL + '/meta/schemaVersion.json', 5000);
      const schemaVer = JSON.parse(body);
      const localVer = parseFloat(GM_getValue(KEY_SCHEMA_VER, '1.0'));
      if (schemaVer > localVer) {
        showSchemaUpdateAlert(schemaVer);
        GM_setValue(KEY_SCHEMA_VER, String(schemaVer));
      }
      return schemaVer;
    } catch {
      return null;
    }
  }

  function createProgressUI(total) {
    const id = '__ts_loader_progress__';
    const old = document.getElementById(id);
    if (old) old.remove();

    const el = document.createElement('div');
    el.id = id;
    el.style.cssText = 'position:fixed;left:16px;right:16px;bottom:16px;z-index:2147483647;font-family:Segoe UI,Arial,sans-serif;background:#111a;border:1px solid #ffffff33;border-radius:10px;padding:8px 10px;color:#fff;backdrop-filter:blur(3px);';
    el.innerHTML = [
      '<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:6px;">',
      '<span id="__ts_loader_label__">Ładowanie modułów...</span>',
      `<span id="__ts_loader_count__">0/${total}</span>`,
      '</div>',
      '<div style="height:8px;background:#ffffff22;border-radius:999px;overflow:hidden;">',
      '<div id="__ts_loader_fill__" style="height:100%;width:0%;background:linear-gradient(90deg,#7c5cff,#36d1dc);transition:width .2s ease;"></div>',
      '</div>'
    ].join('');
    (document.body || document.documentElement).appendChild(el);
    return {
      update: function (done, label) {
        const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 100;
        const fill = document.getElementById('__ts_loader_fill__');
        const count = document.getElementById('__ts_loader_count__');
        const text = document.getElementById('__ts_loader_label__');
        if (fill) fill.style.width = pct + '%';
        if (count) count.textContent = `${done}/${total}`;
        if (text && label) text.textContent = label;
        window.dispatchEvent(new CustomEvent('__ts_loader_progress_event__', {
          detail: { done, total, label: label || '' }
        }));
      },
      done: function (label = 'Załadowano') {
        this.update(total, label);
        window.dispatchEvent(new CustomEvent('__ts_loader_done_event__'));
        setTimeout(() => {
          const node = document.getElementById(id);
          if (node) node.remove();
        }, 800);
      },
      fail: function (label = 'Błąd ładowania') {
        this.update(0, label);
        window.dispatchEvent(new CustomEvent('__ts_loader_fail_event__', {
          detail: { total, label }
        }));
        const fill = document.getElementById('__ts_loader_fill__');
        if (fill) fill.style.background = 'linear-gradient(90deg,#ff4d4f,#ff7a45)';
        setTimeout(() => {
          const node = document.getElementById(id);
          if (node) node.remove();
        }, 2500);
      }
    };
  }

  function normalizeModuleManifest(manifest) {
    const list = Array.isArray(manifest?.modules) ? manifest.modules : [];
    return list.map((entry, idx) => ({
      name: entry.name || `module_${idx}`,
      url: entry.url,
      version: entry.version || manifest.version || '0',
      hash: entry.hash || '',
      execute: entry.execute || 'raw',
      required: entry.required !== false,
    })).filter(m => typeof m.url === 'string' && m.url.length > 0);
  }

  async function fetchLatestCommitSha() {
    try {
      const body = await requestText(GITHUB_REF_API_URL, 10000);
      const data = JSON.parse(body);
      const sha = data && data.object && typeof data.object.sha === 'string' ? data.object.sha : '';
      return sha || '';
    } catch (error) {
      if (/HTTP 403/i.test(String(error && error.message))) {
        console.info('[TamaSkrypt Launcher] GitHub ref API rate-limited; using branch/CDN fallback.');
      } else {
        console.warn('[TamaSkrypt Launcher] GitHub ref API unavailable:', error.message);
      }
      return '';
    }
  }

  async function executeModules(manifest, options = {}) {
    const moduleList = normalizeModuleManifest(manifest);
    if (!moduleList.length) return false;
    const strictHashes = manifest && manifest.strictHashes === true;

    const allowNetwork = options.allowNetwork !== false;
    const progress = createProgressUI(moduleList.length);
    const moduleMeta = getJson(KEY_MODULE_META, {});
    const resolvedCode = {};
    let completed = 0;

    try {
      setLauncherDiagnostics({
        mode: 'modules',
        manifestVersion: manifest && manifest.version,
        source: allowNetwork ? 'network' : 'cache',
        commitSha: manifest && manifest.__commitSha,
      });

      for (const mod of moduleList) {
        const cacheKey = KEY_MODULE_CODE_PREFIX + mod.name;
        const cachedInfo = moduleMeta[mod.name] || {};
        const cachedCode = GM_getValue(cacheKey, '');
        const cacheMatches = cachedCode
          && cachedInfo.url === mod.url
          && String(cachedInfo.version) === String(mod.version)
          && String(cachedInfo.hash || '') === String(mod.hash || '');

        if (cacheMatches) {
          resolvedCode[mod.name] = cachedCode;
          completed += 1;
          progress.update(completed, `Cache: ${mod.name}`);
          continue;
        }

        if (!allowNetwork) throw new Error(`Brak cache dla modułu ${mod.name}`);

        let code = '';
        try {
          code = await requestText(mod.url, 15000);
        } catch (downloadError) {
          if (mod.required) throw downloadError;
          console.warn('[TamaSkrypt Launcher] Pomijam opcjonalny moduł (download):', mod.name, downloadError.message);
          completed += 1;
          progress.update(completed, `Skip: ${mod.name}`);
          continue;
        }

        const validHash = await verifyHash(code, mod.hash);
        if (!validHash) {
          if (strictHashes) {
            if (mod.required) throw new Error(`Hash mismatch: ${mod.name}`);
            console.warn('[TamaSkrypt Launcher] Pomijam opcjonalny moduł (hash mismatch):', mod.name);
            completed += 1;
            progress.update(completed, `Skip: ${mod.name}`);
            continue;
          }
          console.warn('[TamaSkrypt Launcher] Hash mismatch (advisory):', mod.name);
        }

        resolvedCode[mod.name] = code;
        moduleMeta[mod.name] = {
          url: mod.url,
          version: mod.version,
          hash: mod.hash || '',
          updatedAt: Date.now(),
        };
        GM_setValue(cacheKey, code);
        completed += 1;
        progress.update(completed, `Pobrano: ${mod.name}`);
      }

      window.GelekModules = window.GelekModules || {};
      for (const mod of moduleList) {
        const code = resolvedCode[mod.name];
        if (!(await runCode(code, mod.name)) && mod.required) {
          throw new Error(`Nie udało się uruchomić modułu ${mod.name}`);
        }
      }

      if (manifest.entryModule) {
        const entryFn = window.GelekModules && window.GelekModules[manifest.entryModule];
        if (typeof entryFn !== 'function') {
          throw new Error(`Brak entryModule: ${manifest.entryModule}`);
        }
        await Promise.resolve(entryFn({ manifest }));
      }

      setJson(KEY_MODULE_MANIFEST, {
        version: manifest.version,
        entryModule: manifest.entryModule || '',
        modules: moduleList,
        __commitSha: manifest.__commitSha || '',
      });
      setJson(KEY_MODULE_META, moduleMeta);
      GM_setValue(KEY_MODE, 'modules');
      GM_setValue(KEY_VERSION, String(manifest.version || ''));
      GM_setValue(KEY_COMMIT_SHA, String(manifest.__commitSha || ''));
      GM_setValue(KEY_CHECKED, Date.now());
      progress.done('Moduły gotowe');
      return true;
    } catch (error) {
      console.error('[TamaSkrypt Launcher] Błąd modułów:', error);
      progress.fail('Błąd ładowania modułów');
      return false;
    }
  }

  async function executeCachedModules() {
    const cachedManifest = getJson(KEY_MODULE_MANIFEST, null);
    if (!cachedManifest || !Array.isArray(cachedManifest.modules)) return false;
    return executeModules(cachedManifest, { allowNetwork: false });
  }

  async function executeLegacyScript(manifest, allowNetwork) {
    const cachedCode = GM_getValue(KEY_CODE, '');
    const cachedVersion = GM_getValue(KEY_VERSION, '');

    if (!allowNetwork || (manifest.version === cachedVersion && cachedCode)) {
      setLauncherDiagnostics({
        mode: 'legacy',
        manifestVersion: manifest && manifest.version ? manifest.version : cachedVersion,
        source: 'cache',
        commitSha: manifest && manifest.__commitSha ? manifest.__commitSha : GM_getValue(KEY_COMMIT_SHA, ''),
      });
      return await runCode(cachedCode, 'legacy-cache');
    }

    if (!manifest.scriptUrl) return false;
    try {
      setLauncherDiagnostics({
        mode: 'legacy',
        manifestVersion: manifest && manifest.version,
        source: 'network',
        commitSha: manifest && manifest.__commitSha,
      });
      const code = await requestText(manifest.scriptUrl, 15000);
      GM_setValue(KEY_CODE, code);
      GM_setValue(KEY_VERSION, String(manifest.version || ''));
      GM_setValue(KEY_COMMIT_SHA, String(manifest.__commitSha || ''));
      GM_setValue(KEY_CHECKED, Date.now());
      GM_setValue(KEY_MODE, 'legacy');
      return await runCode(code, 'legacy-download');
    } catch (error) {
      console.warn('[TamaSkrypt Launcher] Legacy download failed:', error.message);
      setLauncherDiagnostics({
        mode: 'legacy',
        manifestVersion: cachedVersion || (manifest && manifest.version) || '',
        source: 'cache-fallback',
        commitSha: GM_getValue(KEY_COMMIT_SHA, ''),
      });
      return cachedCode ? await runCode(cachedCode, 'legacy-cache-fallback') : false;
    }
  }

  async function runFromCacheByMode() {
    const mode = GM_getValue(KEY_MODE, 'legacy');
    if (mode === 'modules') {
      const ok = await executeCachedModules();
      if (ok) return true;
    }
    const code = GM_getValue(KEY_CODE, '');
    setLauncherDiagnostics({
      mode: 'legacy',
      manifestVersion: GM_getValue(KEY_VERSION, ''),
      source: 'cache',
      commitSha: GM_getValue(KEY_COMMIT_SHA, ''),
    });
    return code ? await runCode(code, 'legacy-cache') : false;
  }

  async function fetchManifest() {
    const latestCommitSha = await fetchLatestCommitSha();
    const preferredUrl = latestCommitSha
      ? rewriteRawGithubUrl(MANIFEST_URL, latestCommitSha)
      : MANIFEST_URL;
    const cdnPreferredUrl = latestCommitSha
      ? `https://cdn.jsdelivr.net/gh/${GITHUB_OWNER}/${GITHUB_REPO}@${latestCommitSha}/${MANIFEST_PATH}`
      : MANIFEST_CDN_URL;

    try {
      const body = await requestText(preferredUrl, 10000);
      const manifest = JSON.parse(body);
      return applyCommitShaToManifest(manifest, latestCommitSha);
    } catch (error) {
      console.warn('[TamaSkrypt Launcher] Primary manifest unavailable, trying fallback:', error.message);
      try {
        const body = await requestText(MANIFEST_URL, 10000);
        return JSON.parse(body);
      } catch (branchError) {
        console.warn('[TamaSkrypt Launcher] Branch manifest unavailable, trying CDN:', branchError.message);
        const body = await requestText(cdnPreferredUrl, 10000);
        const manifest = JSON.parse(body);
        return applyCommitShaToManifest(manifest, latestCommitSha);
      }
    }
  }

  async function run() {
    if (shouldSkipOnHost()) {
      return;
    }

    await checkFirebaseSchemaVersion();

    const mode = GM_getValue(KEY_MODE, 'legacy');
    const lastCheck = GM_getValue(KEY_CHECKED, 0);
    const needsCheck = mode !== 'modules' || (Date.now() - lastCheck) > CHECK_INTERVAL_MS;

    console.log('[TamaSkrypt Launcher] mode=', mode, 'needsCheck=', needsCheck);

    if (!needsCheck) {
      const ok = await runFromCacheByMode();
      if (ok) return;
    }

    try {
      const manifest = await fetchManifest();
      const hasModules = Array.isArray(manifest.modules) && manifest.modules.length > 0;

      let ok = false;
      if (hasModules) {
        ok = await executeModules(manifest, { allowNetwork: true });
        if (!ok) {
          ok = await executeCachedModules();
        }
        if (!ok) {
          ok = await executeLegacyScript(manifest, true);
        }
      } else {
        ok = await executeLegacyScript(manifest, true);
      }

      if (!ok) {
        await runFromCacheByMode();
      }
    } catch (error) {
      console.warn('[TamaSkrypt Launcher] Manifest unavailable, fallback to cache:', error.message);
      await runFromCacheByMode();
    }
  }

  run();

})();