Dreadcast Development Kit

13/11/2023 02:55:01

目前為 2024-09-07 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Dreadcast Development Kit
// @namespace   Violentmonkey Scripts
// @match       https://www.dreadcast.net/Main
// @version     1.0.10
// @author      Pelagia/Isilin
// @description 13/11/2023 02:55:01
// @license     http://creativecommons.org/licenses/by-nc-nd/4.0/
// @connect     docs.google.com
// @connect     googleusercontent.com
// @connect     sheets.googleapis.com
// @connect     raw.githubusercontent.com
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @downloadURL
// @updateURL
// ==/UserScript==

// TODO add guards in each function to check Game/EDC/Forum
// TODO add function to add deck command
console.log('DDK - Loading ...');

// ===== JQuery utilities =====

$.fn.insertAt = function (index, element) {
  var lastIndex = this.children().size();
  if (index < 0) {
    index = Math.max(0, lastIndex + 1 + index);
  }
  this.append(element);
  if (index < lastIndex) {
    this.children().eq(index).before(this.children().last());
  }
  return this;
};

// ===== Lib =====

const Util = {
  guard: (condition, message) => {
    if (!condition) throw new Error(message);
    return;
  },

  deprecate: (name, replacement) => {
    console.warn(
      name +
        ': this function has been deprecated and should not be used anymore.' +
        (replacement && replacement !== ''
          ? 'Prefer: ' + replacement + '.'
          : ''),
    );
  },

  isArray: (o, optional = false) =>
    $.type(o) === 'array' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isString: (o, optional = false) =>
    $.type(o) === 'string' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isBoolean: (o, optional = false) =>
    $.type(o) === 'boolean' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isNumber: (o, optional = false) =>
    $.type(o) === 'number' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isFunction: (o, optional = false) =>
    $.type(o) === 'function' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isDate: (o, optional = false) =>
    $.type(o) === 'date' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isError: (o, optional = false) =>
    $.type(o) === 'error' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isRegex: (o, optional = false) =>
    $.type(o) === 'regexp' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isObject: (o, optional = false) =>
    $.type(o) === 'object' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isColor: (o, optional = false) => {
    if (optional && ($.type(o) === 'undefined' || $.type(o) === 'null'))
      return true;
    else {
      const colors = ['rouge', 'bleu', 'vert', 'jaune'];
      return (
        $.type(o) === 'string' &&
        (colors.includes(o) ||
          o.match(/^[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3}$/gi))
      );
    }
  },

  isGame: () => window.location.href.includes('https://www.dreadcast.net/Main'),

  isForum: () =>
    window.location.href.includes('https://www.dreadcast.net/Forum'),

  isEDC: () => window.location.href.includes('https://www.dreadcast.net/EDC'),

  isWiki: () => window.location.href.includes('http://wiki.dreadcast.eu/wiki'),

  getContext: () => {
    return Util.isGame()
      ? 'game'
      : Util.isForum()
      ? 'forum'
      : Util.isEDC()
      ? 'edc'
      : 'wiki';
  },
};

// ===== Overwrite DC functions =====

if (Util.isGame() && MenuChat.prototype.originalSend === undefined) {
  MenuChat.prototype.originalSend = MenuChat.prototype.send;
  MenuChat.prototype.sendCallbacks = [];
  MenuChat.prototype.afterSendCallbacks = [];
  MenuChat.prototype.send = function () {
    const $nextFn = () => true;
    const $abortFn = () => false;
    const $message = $('#chatForm .text_chat').val();
    const $res = this.sendCallbacks.every((callback) =>
      callback($message, $nextFn, $abortFn),
    );
    if (!$res) {
      throw new Error('MenuChat.prototype.send: Error on sending message.');
    }

    this.originalSend();

    this.afterSendCallbacks.every((callback) => callback($message));
  };
  MenuChat.prototype.onSend = (callback) => {
    MenuChat.prototype.sendCallbacks.push(callback);
  };
  MenuChat.prototype.onAfterSend = (callback) => {
    MenuChat.prototype.afterSendCallbacks.push(callback);
  };
}

// ============================

const DC = {};

DC.LocalMemory = {
  init: (label, defaultValue) => {
    const $currentVal = GM_getValue(label);
    if ($currentVal === undefined) {
      GM_setValue(label, defaultValue);
      return defaultValue;
    } else {
      return $currentVal;
    }
  },

  set: (label, value) => GM_setValue(label, value),

  get: (label) => GM_getValue(label),

  delete: (label) => GM_deleteValue(label),

  list: () => GM_listValues(),
};

DC.Style = {
  apply: (css) => {
    Util.guard(
      Util.isString(css, true),
      "DC.Style.apply: 'css' parameter should be a string.",
    );

    if (typeof GM_addStyle !== 'undefined') {
      GM_addStyle(css);
    } else {
      let $styleNode = document.createElement('style');
      $styleNode.appendChild(document.createTextNode(css));
      (document.querySelector('head') || document.documentElement).appendChild(
        $styleNode,
      );
    }
  },
};

DC.TopMenu = {
  get: () => {
    return $('.menus');
  },

  add: (element, index = 0) => {
    Util.guard(
      Util.isNumber(index),
      "DC.TopMenu.add: 'index' parameter should be a number.",
    );

    const $dom = DC.TopMenu.get();
    if (index === 0) {
      $dom.prepend(element);
    } else {
      $dom.insertAt(index, element);
    }
  },
};

DC.UI = {
  Separator: () => $('<li class="separator" />'),

  Menu: (label, fn) => {
    Util.guard(
      Util.isString(label),
      "DC.UI.Menu: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(fn),
      "DC.UI.Menu: 'fn' parameter should be a function.",
    );

    return $(`<li id="${label}" class="couleur5">${label}</li>`).bind(
      'click',
      fn,
    );
  },

  SubMenu: (label, fn, separatorBefore = false) => {
    Util.guard(
      Util.isString(label),
      "DC.UI.SubMenu: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(fn),
      "DC.UI.SubMenu: 'fn' parameter should be a function.",
    );
    Util.guard(
      Util.isBoolean(separatorBefore),
      "DC.UI.SubMenu: 'separatorBefore' parameter should be a boolean.",
    );

    return $(
      `<li class="link couleur2 ${
        separatorBefore ? 'separator' : ''
      }">${label}</li>`,
    ).bind('click', fn);
  },

  DropMenu: (label, submenu) => {
    Util.guard(
      Util.isString(label),
      "DC.UI.DropMenu: 'label' parameter should be a string.",
    );

    const $label = label + '▾';

    const $list = $('<ul></ul>');
    if (!Array.isArray(submenu)) {
      throw new Error("'submenu' should be an array in DC.UI.DropMenu !");
    }
    submenu.forEach(($submenu) => {
      $($list).append($submenu);
    });

    return $(
      `<li id="${label}" class="parametres couleur5 right hover" onclick="$(this).find('ul').slideDown();">${$label}</li>`,
    ).append($list);
  },

  addSubMenuTo: (name, element, index = 0) => {
    Util.guard(
      Util.isString(name),
      "DC.UI.addSubMenuTo: 'name' parameter should be a string.",
    );
    Util.guard(
      Util.isNumber(index),
      "DC.UI.addSubMenuTo: 'index' parameter should be a string.",
    );

    const $menu = $(`.menus li:contains("${name}") ul`);

    if (index === 0) {
      $menu.prepend(element);
    } else {
      $menu.insertAt(index, element);
    }
  },

  TextButton: (id, label, fn) => {
    Util.guard(
      Util.isString(id),
      "DC.UI.TextButton: 'id' parameter should be a string.",
    );
    Util.guard(
      Util.isString(label),
      "DC.UI.TextButton: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(fn),
      "DC.UI.TextButton: 'fn' parameter should be a function.",
    );

    return $(`<div id="${id}" class="btnTxt">${label}</div>`).bind('click', fn);
  },

  Button: (id, label, fn) => {
    Util.guard(
      Util.isString(id),
      "DC.UI.Button: 'id' parameter should be a string.",
    );
    Util.guard(
      Util.isString(label),
      "DC.UI.Button: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(fn),
      "DC.UI.Button: 'fn' parameter should be a function.",
    );

    return $(
      `<div id="${id}" class="btn add link infoAide"><div class="gridCenter">${label}</div></div>`,
    ).bind('click', fn);
  },

  Tooltip: (text, content) => {
    DC.Style.apply(`
        .tooltip {
          position: relative;
          display: inline-block;
        }
        .tooltip .tooltiptext {
          visibility: hidden;
          background-color: rgba(24,24,24,0.95);
          color: #fff;
          text-align: center;
          padding: 5px;
          border-radius: 6px;
          position: absolute;
          z-index: 1;
          font-size: 1rem;
        }
        .tooltip:hover .tooltiptext {
          visibility: visible;
        }
      `);

    return $(`<div class="tooltip">
        <span class="tooltiptext">${text}</span>
        </div>`).prepend(content);
  },

  Checkbox: (id, defaultEnable = true, onAfterClick) => {
    Util.guard(
      Util.isString(id),
      "DC.UI.Checkbox: 'id' parameter should be a string.",
    );
    Util.guard(
      Util.isBoolean(defaultEnable),
      "DC.UI.Checkbox: 'defaultEnable' parameter should be a boolean.",
    );
    Util.guard(
      Util.isFunction(onAfterClick, true),
      "DC.UI.Checkbox: 'onAfterClick' optional parameter should be a function.",
    );

    DC.Style.apply(`
        .dc_ui_checkbox {
          cursor: pointer;
          width: 30px;
          height: 18px;
          background: url(../../../images/fr/design/boutons/b_0.png) 0 0 no-repeat;
        }

        .dc_ui_checkbox_on {
          background: url(../../../images/fr/design/boutons/b_1.png) 0 0 no-repeat;
        }
      `);

    return $(
      `<div id="${id}" class="dc_ui_checkbox ${
        defaultEnable ? 'dc_ui_checkbox_on' : ''
      }" />`,
    ).bind('click', () => {
      $(`#${id}`).toggleClass('dc_ui_checkbox_on');
      onAfterClick?.($(`#${id}`).hasClass('dc_ui_checkbox_on'));
    });
  },

  PopUp: (id, title, content) => {
    Util.guard(
      Util.isString(id),
      "DC.UI.PopUp: 'id' parameter should be a string.",
    );
    Util.guard(
      Util.isString(title),
      "DC.UI.PopUp: 'title' parameter should be a string.",
    );

    $('#loader').fadeIn('fast');

    const html = `
        <div id="${id}" class="dataBox"  onClick="engine.switchDataBox(this)" style="display: block; z-index: 5; left: 764px; top: 16px;">
          <relative>
            <div class="head" ondblclick="$('#${id}').toggleClass('reduced');">
            <div title="Fermer la fenêtre (Q)" class="info1 link close transition3s" onClick="engine.closeDataBox($(this).parent().parent().parent().attr('id'));" alt="$('${id}').removeClass('active')">
              <i class="fas fa-times"></i>
            </div>
            <div title="Reduire/Agrandir la fenêtre" class="info1 link reduce transition3s" onClick="$('#${id}').toggleClass('reduced');">
              <span>-</span>
            </div>
            <div class="title">${title}</div>
          </div>
          <div class="dbloader"></div>
          <div class="content" style="max-width: 800px; max-height: 600px; overflow-y: auto; overflow-x: hidden;">
          </div>
        </relative>
      </div>`;

    engine.displayDataBox(html);
    $(`#${id} .content`).append(content);

    $('#loader').hide();
  },

  SideMenu: (id, label, content) => {
    Util.guard(
      Util.isString(id),
      "DC.UI.SideMenu: 'id' parameter should be a string.",
    );
    Util.guard(
      Util.isString(label),
      "DC.UI.SideMenu: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isString(content),
      "DC.UI.SideMenu: 'content' parameter should be a string.",
    );

    const idContainer = id + '_container';
    const idButton = id + '_button';
    const idContent = id + '_content';

    if ($('div#zone_sidemenu').length === 0) {
      $('body').append('<div id="zone_sidemenu"></div>');
    }
    $('#zone_sidemenu').append(
      `<div id="${idContainer}" class="sidemenu_container"></div>`,
    );

    $(`#${idContainer}`).append(
      DC.UI.TextButton(
        idButton,
        '<i class="fas fa-chevron-left"></i>' + label,
        () => {
          const isOpen = $(`#${idButton}`).html().includes('fa-chevron-right');
          if (isOpen) {
            $(`#${idButton}`)
              .empty()
              .append('<i class="fas fa-chevron-left"></i>' + label);
            $(`#${idContainer}`).css('right', '-220px');
          } else {
            $(`#${idButton}`)
              .empty()
              .append('<i class="fas fa-chevron-right"></i>' + label);
            $(`#${idContainer}`).css('right', '0px');
          }
        },
      ),
    );

    $(`#${idContainer}`).append(
      `<div id="${idContent}" class="sidemenu_content">${content}</div>`,
    );

    DC.Style.apply(`
        #zone_sidemenu {
          display: flex;
          flex-direction: column;
          position: absolute;
          right: 0px;
          top: 80px;
          z-index: 999999;
        }

        .sidemenu_container {
          display: flex;
          right: -220px;
        }

        #zone_sidemenu .btnTxt {
          margin: 0 auto;
          min-width: 100px;
          max-width: 100px;
          font-size: 1rem;
          padding: 1%;
          display: grid;
          height: 100%;
          box-sizing: border-box;
          grid-template-columns: 10% 1fr;
          align-items: center;
          text-transform: uppercase;
          font-family: Arial !important;
          line-height: normal !important;
        }

        #zone_sidemenu .btnTxt:hover {
          background: #0b9bcb;
          color: #fff;
        }

        .sidemenu_content {
          background-color: #000;
          color: #fff !important;
          box-shadow: 0 0 15px -5px inset #a2e4fc !important;
          padding: 10px;
          width: 200px;
        }
      `);
  },
};

DC.Network = {
  fetch: (args) => {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest(
        Object.assign({}, args, {
          onload: (e) => resolve(e.response),
          onerror: reject,
          ontimeout: reject,
        }),
      );
    });
  },

  loadSpreadsheet: async (sheetId, tabName, range, apiKey, onLoad) => {
    Util.guard(
      Util.isString(sheetId),
      "DC.Network.loadSpreadsheet: 'sheetId' parameter should be a string.",
    );
    Util.guard(
      Util.isString(tabName),
      "DC.Network.loadSpreadsheet: 'tabName' parameter should be a string.",
    );
    Util.guard(
      Util.isString(range),
      "DC.Network.loadSpreadsheet: 'range' parameter should be a string.",
    );
    Util.guard(
      Util.isString(apiKey),
      "DC.Network.loadSpreadsheet: 'apiKey' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(onLoad),
      "DC.Network.loadSpreadsheet: 'onLoad' parameter should be a function.",
    );

    const urlGoogleSheetDatabase = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${tabName}!${range}?key=${apiKey}`;
    const result = await DC.Network.fetch({
      method: 'GET',
      url: urlGoogleSheetDatabase,
      headers: {
        'Content-Type': 'application/json',
      },
      responseType: 'json',
    });
    onLoad(result.values);
  },

  loadScript: async (url, onAfterLoad) => {
    Util.guard(
      Util.isString(url),
      "DC.Network.loadScript: 'url' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(onAfterLoad, true),
      "DC.Network.loadScript: 'onAfterLoad' optional parameter should be a function.",
    );

    // TODO we should check that url is from a valid and secure source.
    const result = await DC.Network.fetch({
      method: 'GET',
      url,
      headers: {
        'Content-Type': 'text/javascript',
      },
    });
    // TODO we have to secure more this call
    eval(result);

    onAfterLoad?.();
  },

  loadJson: async (url) => {
    Util.guard(
      Util.isString(url),
      "DC.Network.loadJson: 'url' parameter should be a string.",
    );

    const result = await DC.Network.fetch({
      method: 'GET',
      url,
      headers: {
        'Content-Type': 'application/json',
      },
      responseType: 'json',
    });
    return result;
  },
};

DC.Chat = {
  sendMessage: (message) => {
    Util.guard(
      Util.isString(message),
      "DC.Chat.sendMessage: 'message' parameter should be a string.",
    );

    $('#chatForm .text_chat').val(message);
    $('#chatForm .text_valider').click();
  },

  t: (message, decoration) => {
    Util.guard(
      Util.isString(message, true),
      "DC.Chat.t: 'message' parameter should be a string.",
    );
    Util.guard(
      Util.isBoolean(decoration.bold, true),
      "DC.Chat.t: 'bold' optional parameter should be a boolean.",
    );
    Util.guard(
      Util.isBoolean(decoration.italic, true),
      "DC.Chat.t: 'italic' optional parameter should be a boolean.",
    );
    Util.guard(
      Util.isColor(decoration.color, true),
      "DC.Chat.t: 'color' optional parameter should be a color string.",
    );

    var prefix = '';
    var suffix = '';

    if (decoration.bold) {
      prefix += '[b]';
      suffix += '[b]';
    }

    if (decoration.italic) {
      prefix += '[i]';
      suffix = '[/i]' + suffix;
    }

    if (decoration.color && decoration.color !== '') {
      prefix += '[c=' + decoration.color + ']';
      suffix = '[/c]' + suffix;
    }

    return prefix + message + suffix;
  },

  addCommand: (label, fn) => {
    Util.guard(
      Util.isString(label),
      "DC.Chat.addCommand: 'label' parameter should be a string.",
    );
    Util.guard(
      Util.isFunction(fn),
      "DC.Chat.addCommand: 'fn' parameter should be a function.",
    );

    nav.getChat().onSend((message, next, abort) => {
      const forbiden = ['me', 'y', 'ye', 'yme', 'w', 'we', 'wme', 'roll', ''];

      const labelUsed = message.split(' ')[0].substr(1);
      if (
        message[0] !== '/' ||
        labelUsed !== label ||
        forbiden.includes(labelUsed)
      ) {
        return next();
      }

      const content = message.substr(labelUsed.length + 1);

      if (fn(labelUsed, content)) {
        return next();
      } else {
        return abort();
      }
    });
  },
};