Source: public/frameshifter/helpers.js

/**
 * @namespace ClientHelpers
 */
window.frameShifterHelpers = {};

/**
 * @function hasData
 * @param  {object} obj object to test
 * @description test whether an object is empty or not
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.hasData = (obj) => {
  if (obj && Object.keys(obj).length > 0) {
    return true;
  }

  return false;
};

window.frameShifterHelpers.waitForDataInterval = null;

/**
 * @function waitForData
 * @param  {function} callback function to call when found
 * @description check for frameShifterState every 200ms until found, useful for standalone plugins on first load
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.waitForData = (callback) => {
  window.frameShifterHelpers.waitForDataInterval = setInterval(() => {
    if (window.frameShifterHelpers.hasData(window.frameShifterState)) {
      callback();
      clearInterval(window.frameShifterHelpers.waitForDataInterval);
    }
  }, 200);
};

/**
 * @function getPluginConfigBySlug
 * @param  {string} slug plugin slug in config.json
 * @description check for frameShifterState every 200ms until found, useful for standalone plugins on first load
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.getPluginConfigBySlug = (slug) => {
  if (!window.frameShifterConfig) {
    console.warn(`Plugin config for slug "${slug}" is not available.`);
    return false;
  }

  for (let i = 0; i < window.frameShifterConfig.plugins.length; i++) {
    const plugin = window.frameShifterConfig.plugins[i];

    if (plugin.slug && plugin.slug === slug) {
      return plugin;
    }
  }

  console.error(`Plugin config for slug "${slug}" is not found.`);
  return false;
};

/**
 * @function pipsToNumber
 * @param  {number} pip some type of number
 * @description converts status pip values into normal numbers because im dumb atm
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.pipsToNumber = (pip) => {
  // giving up and doing this bad
  // there is math number bases that i dont quite get

  switch (pip) {
    case 2:
      return 1;

    case 4:
      return 2;

    case 6:
      return 3;

    case 8:
      return 4;

    default:
      return 0;
  }
};

/**
 * @function testPlayerData
 * @param  {string} value value on window to test if present and correct type
 * @param  {string} obj data type to check
 * @description check that a value is present and of expected type
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.testPlayerData = (value, type) => {
  switch (type) {
    case "boolean":
    case "object":
    case "string":
    case "number":
      if (typeof value === type) return value;
      break;

    case "array":
      if (Array.isArray(value)) return value;
      break;

    default:
      // unsupported data type
      break;
  }

  return null;
};

/**
 * @function getBinaryFlags
 * @param  {encodedInteger} flagsInt encoded base10 integer from Flags/Flags2
 * @param  {int} len length of the expected binary number
 * @description turn encoded status integer into a 32 length series of binary flags
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.getBinaryFlags = (flagsInt, len) => {
  if (!flagsInt) return false;

  function padding(char, num) {
    let str = "";
    for (let i = 0; i < num; i++) {
      str += char;
    }
    return str;
  }

  let flagBinary = flagsInt.toString(2);

  if (flagBinary.length < len) {
    const offset = len - flagBinary.length;
    flagBinary = `${padding("0", offset)}${flagBinary}`;
  }

  return flagBinary;
};

/**
 * @function playerInfo
 * @param  {string} infoType data to request from various sources in FrameShifter
 * @description grab any data that FrameShifter knows about.
 * @memberof ClientHelpers
 */
window.frameShifterHelpers.playerInfo = (infoType) => {
  const checkFlag = (flags, num, max) => {
    return flags[max - num] === "1";
  };

  // boolean information available in binary status flags
  const flagNames = [
    "docked",
    "landed",
    "landing-gear",
    "shields-up",
    "supercruise",
    "fa-off",
    "hardpoints",
    "in-wing",
    "external-lights",
    "cargo-scoop",
    "silent-running",
    "scooping-fuel",
    "srv-handbrake",
    "srv-turret",
    "srv-turret-retracted",
    "srv-drive-assist",
    "mass-locked",
    "fsd-charging",
    "fsd-cooldown",
    "low-fuel",
    "overheat",
    "has-lat-lng",
    "danger",
    "interdicted",
    "in-ship",
    "in-fighter",
    "in-srv",
    "analysis-mode",
    "night-vision",
    "has-altitude",
    "fsd-jump",
    "srv-highbeam",
  ];

  if (flagNames.includes(infoType)) {
    const flags = window.frameShifterHelpers.getBinaryFlags(
      window?.frameShifterState?.status?.Flags,
      32
    );

    if (flags) return checkFlag(flags, flagNames.indexOf(infoType), 31);
    return null;
  }

  // data available from various places in Status.json
  const statusNames = [
    "cargo-weight",
    "firegroup",
    "legal-state",
    "fuel-main",
    "fuel-res",
    "pips-sys",
    "pips-eng",
    "pips-wep",
  ];

  if (statusNames.includes(infoType)) {
    switch (infoType) {
      case "cargo-weight":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Cargo,
          "number"
        );

      case "firegroup":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.FireGroup,
          "number"
        );

      case "legal-state":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.LegalState,
          "string"
        );

      case "fuel-main":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Fuel?.FuelMain,
          "number"
        );

      case "fuel-res":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Fuel?.FuelReservoir,
          "number"
        );

      case "pips-sys":
        const tPipSys = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Pips,
          "array"
        );
        return tPipSys
          ? window.frameShifterHelpers.pipsToNumber(tPipSys[0])
          : null;

      case "pips-eng":
        const tPipEng = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Pips,
          "array"
        );
        return tPipEng
          ? window.frameShifterHelpers.pipsToNumber(tPipEng[1])
          : null;

      case "pips-wep":
        const tPipWep = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Pips,
          "array"
        );
        return tPipWep
          ? window.frameShifterHelpers.pipsToNumber(tPipWep[2])
          : null;

      default:
        return null;
    }
  }

  // data available in odyssey flags2
  const flag2Names = [
    "on-foot",
    "in-taxi",
    "multicrew",
    "on-foot-in-station",
    "on-foot-on-planet",
    "aim-down-sight",
    "low-o2",
    "low-health",
    "cold",
    "hot",
    "very-cold",
    "very-hot",
    "glide",
    "on-foot-in-hangar",
    "on-foot-in-social",
    "on-foot-exterior",
    "breathable-atom",
  ];

  if (flag2Names.includes(infoType)) {
    const flags2 = window.frameShifterHelpers.getBinaryFlags(
      window?.frameShifterState?.status?.Flags2,
      16
    );

    // this is bugged but i'm not sure how
    // i don't understand why the correct max in 31 above but 16 here
    if (flags2) return checkFlag(flags2, flag2Names.indexOf(infoType), 15);
    return null;
  }

  // data available from loadout and loadgame journal events
  const journalNames = [
    "ship",
    "ship-ident",
    "ship-name",
    "hull-perc",
    "hull-value",
    "unladen-mass",
    "fuel-main-cap",
    "fuel-res-cap",
    "cargo-cap",
    "max-jump",
    "rebuy",
    "cmdr-name",
    "horizons",
    "odyssey",
    "game-mode",
    "credits-at-load",
  ];

  if (journalNames.includes(infoType)) {
    switch (infoType) {
      case "ship":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.Ship,
          "string"
        );

      case "ship-ident":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.ShipIdent,
          "string"
        );

      case "ship-name":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.ShipName,
          "string"
        );

      case "hull-perc":
        const tHullPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.HullHealth,
          "number"
        );
        return tHullPerc !== null ? `${tHullPerc * 100}%` : null;

      case "hull-value":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.HullValue,
          "number"
        );

      case "hull-value":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.HullValue,
          "number"
        );

      case "unladen-mass":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.UnladenMass,
          "number"
        );

      case "fuel-main-cap":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.FuelCapacity.Main,
          "number"
        );

      case "fuel-res-cap":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.FuelCapacity.Reserve,
          "number"
        );

      case "cargo-cap":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.CargoCapacity,
          "number"
        );

      case "max-jump":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.MaxJumpRange,
          "number"
        );

      case "rebuy":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.Rebuy,
          "number"
        );

      case "cmdr-name":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadgame?.Commander,
          "string"
        );

      case "horizons":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadgame?.Horizons,
          "boolean"
        );

      case "odyssey":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadgame?.Odyssey,
          "booelan"
        );

      case "game-mode":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadgame?.GameMode,
          "string"
        );

      case "credits-at-load":
        return window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadgame?.Credits,
          "number"
        );

      default:
        return null;
    }
  }

  // combinations of multiple data sources
  const comboNames = ["fuel-main-perc", "fuel-res-perc", "cargo-perc"];

  if (comboNames.includes(infoType)) {
    switch (infoType) {
      case "fuel-main-perc":
        const tFuelMainPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Cargo,
          "number"
        );

        const tFuelMainCapPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.CargoCapacity,
          "number"
        );

        if (tFuelMainPerc !== null && tFuelMainCapPerc !== null)
          return `${100 - tFuelMainPerc / tFuelMainCapPerc}%`;
        return null;

      case "fuel-res-perc":
        const tFuelResPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.FuelCapacity.Reserve,
          "number"
        );

        const tFuelResCapPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.FuelCapacity.Reserve,
          "number"
        );

        if (tFuelResPerc !== null && tFuelResCapPerc !== null)
          return `${100 - tFuelResPerc / tFuelResCapPerc}%`;
        return null;

      case "cargo-perc":
        const tCargoPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.status?.Cargo,
          "number"
        );

        const tCargoCapPerc = window.frameShifterHelpers.testPlayerData(
          window?.frameShifterState?.loadout?.CargoCapacity,
          "number"
        );

        if (tCargoPerc !== null && tCargoCapPerc !== null)
          return `${(tCargoPerc / tCargoCapPerc) * 100}%`;
        return null;

      default:
        return null;
    }
  }

  // suggest more types and data combinations on github

  // unknown info type, avoid this :)

  console.warn(`"${infoType}" is not a valid player info type.`);
  return null;
};

/**
 * @function sendRelay
 * @param  {string} eventName event name to relay, will be recieved at RELAY_EVENTNAME
 * @param  {object} data data to send along with the relay event
 * @description send data to all connected clients
 * @memberof ClientHelpers
 */

window.frameShifterHelpers.sendRelay = (eventName, data) => {
  const browserEvent = new CustomEvent("RELAY", {
    detail: {
      eventName,
      data,
    },
  });
  window.dispatchEvent(browserEvent);
};