Search code examples
javascriptnode.jsencodingbytedecoding

How can I encode a parsed CS:GO crosshair code object back into a string representation?


I have a function decoding CS:GO crosshair codes into key value object.

(Previously I asked question about how to decode share codes from CS:GO Here)

How can it be reversed from decoding these values, to encoding them into "share code" which consist of alphanumeric characters?

Function decoding share codes:

const BigNumber = require("bignumber.js");

// Intentionally no 0 and 1 number in DICTIONARY
const DICTIONARY = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789";
const DICTIONARY_LENGTH = DICTIONARY.length;
const SHARECODE_PATTERN = /CSGO(-?[\w]{5}){5}$/;

const bigNumberToByteArray = big => {
  const str = big.toString(16).padStart(36, "0");
  const bytes = [];

  for (let i = 0; i < str.length; i += 2) {
    bytes.push(parseInt(str.slice(i, i + 2), 16));
  }

  return bytes;
}

const parseBytes = bytes => {
  return {
    cl_crosshairgap: Int8Array.of(bytes[2])[0] / 10.0,

    cl_crosshair_outlinethickness: (bytes[3] & 7) / 2.0,

    cl_crosshaircolor_r: bytes[4],
    cl_crosshaircolor_g: bytes[5],
    cl_crosshaircolor_b: bytes[6],
    cl_crosshairalpha: bytes[7],
    cl_crosshair_dynamic_splitdist: bytes[8],

    cl_fixedcrosshairgap: Int8Array.of(bytes[9])[0] / 10.0,

    cl_crosshaircolor: bytes[10] & 7,
    cl_crosshair_drawoutline: bytes[10] & 8 ? 1 : 0,
    cl_crosshair_dynamic_splitalpha_innermod: ((bytes[10] & 0xF0) >> 4) / 10.0,

    cl_crosshair_dynamic_splitalpha_outermod: (bytes[11] & 0xF) / 10.0,
    cl_crosshair_dynamic_maxdist_splitratio: ((bytes[11] & 0xF0) >> 4) / 10.0,

    cl_crosshairthickness: (bytes[12] & 0x3F) / 10.0,

    cl_crosshairstyle: (bytes[13] & 0xE) >> 1,
    cl_crosshairdot: bytes[13] & 0x10 ? 1 : 0,
    cl_crosshairgap_useweaponvalue: bytes[13] & 0x20 ? 1 : 0,
    cl_crosshairusealpha: bytes[13] & 0x40 ? 1 : 0,
    cl_crosshair_t: bytes[13] & 0x80 ? 1 : 0,

    cl_crosshairsize: (((bytes[15] & 0x1f) << 8) + bytes[14]) / 10.0
  };
}

const decode = shareCode => {
  if (!shareCode.match(SHARECODE_PATTERN)) {
    throw new Error('Invalid share code');
  }

  shareCode = shareCode.replace(/CSGO|-/g, '');
  const chars = Array.from(shareCode).reverse();
  let big = new BigNumber(0);

  for (let i = 0; i < chars.length; i++) {
    big = big.multipliedBy(DICTIONARY_LENGTH).plus(DICTIONARY.indexOf(chars[i]));
  }
  
  return parseBytes(bigNumberToByteArray(big));
}

console.log(decode('CSGO-O4Jsi-V36wY-rTMGK-9w7qF-jQ8WB'))
// OUTPUT:
// {
//   cl_crosshairgap: 1,
//   cl_crosshair_outlinethickness: 1.5,
//   cl_crosshaircolor_r: 50,
//   cl_crosshaircolor_g: 250,
//   cl_crosshaircolor_b: 84,
//   cl_crosshairalpha: 200,
//   cl_crosshair_dynamic_splitdist: 127,
//   cl_fixedcrosshairgap: -10,
//   cl_crosshaircolor: 5,
//   cl_crosshair_drawoutline: 0,
//   cl_crosshair_dynamic_splitalpha_innermod: 0.6,
//   cl_crosshair_dynamic_splitalpha_outermod: 0.8,
//   cl_crosshair_dynamic_maxdist_splitratio: 0.3,
//   cl_crosshairthickness: 4.1,
//   cl_crosshairstyle: 2,
//   cl_crosshairdot: 1,
//   cl_crosshairgap_useweaponvalue: 0,
//   cl_crosshairusealpha: 0,
//   cl_crosshair_t: 1,
//   cl_crosshairsize: 33
// }

So values below:

{
  cl_crosshairgap: 1,
  cl_crosshair_outlinethickness: 1.5,
  cl_crosshaircolor_r: 50,
  cl_crosshaircolor_g: 250,
  cl_crosshaircolor_b: 84,
  cl_crosshairalpha: 200,
  cl_crosshair_dynamic_splitdist: 127,
  cl_fixedcrosshairgap: -10,
  cl_crosshaircolor: 5,
  cl_crosshair_drawoutline: 0,
  cl_crosshair_dynamic_splitalpha_innermod: 0.6,
  cl_crosshair_dynamic_splitalpha_outermod: 0.8,
  cl_crosshair_dynamic_maxdist_splitratio: 0.3,
  cl_crosshairthickness: 4.1,
  cl_crosshairstyle: 2,
  cl_crosshairdot: 1,
  cl_crosshairgap_useweaponvalue: 0,
  cl_crosshairusealpha: 0,
  cl_crosshair_t: 1,
  cl_crosshairsize: 33
}

Should be encoded into: CSGO-O4Jsi-V36wY-rTMGK-9w7qF-jQ8WB

Function encoding match share codes, which probably could be a base to encoding crosshair codes:

const BigNumber = require("bignumber.js");

// Intentionally no 0 and 1 number in DICTIONARY
const DICTIONARY = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789";
const DICTIONARY_LENGTH = DICTIONARY.length;
const SHARECODE_PATTERN = /CSGO(-?[\w]{5}){5}$/;

function bytesToHex(bytes) {
  return Array.from(bytes, (byte) => {
    return ('0' + (byte & 0xff).toString(16)).slice(-2);
  }).join('');
}

function bigNumberToByteArray(big) {
  const str = big.toString(16).padStart(36, '0');
  const bytes = [];
  for (let i = 0; i < str.length; i += 2) {
    bytes.push(parseInt(str.slice(i, i + 2), 16));
  }

  return bytes;
}

function longToBytesBE(high, low) {
  return [
    (high >>> 24) & 0xff,
    (high >>> 16) & 0xff,
    (high >>> 8) & 0xff,
    high & 0xff,
    (low >>> 24) & 0xff,
    (low >>> 16) & 0xff,
    (low >>> 8) & 0xff,
    low & 0xff,
  ];
}

function int16ToBytes(number) {
  return [(number & 0x0000ff00) >> 8, number & 0x000000ff];
}

function bytesToInt32(bytes) {
  let number = 0;
  for (let i = 0; i < bytes.length; i++) {
    number += bytes[i];
    if (i < bytes.length - 1) {
      number = number << 8;
    }
  }

  return number;
}

function bigNumberToByteArray(big) {
  const str = big.toString(16).padStart(36, "0");
  const bytes = [];
  for (let i = 0; i < str.length; i += 2) {
    bytes.push(parseInt(str.slice(i, i + 2), 16));
  }

  return bytes;
}

const encode = (matchId, reservationId, tvPort) => {
  const matchBytes = longToBytesBE(matchId.high, matchId.low).reverse();
  const reservationBytes = longToBytesBE(reservationId.high, reservationId.low).reverse();
  const tvBytes = int16ToBytes(tvPort).reverse();
  const bytes = Array.prototype.concat(matchBytes, reservationBytes, tvBytes);
  const bytesHex = bytesToHex(bytes);
  let total = new BigNumber(bytesHex, 16);

  // This part would probably be identical
  let c = '';
  let rem = new BigNumber(0);
  for (let i = 0; i < 25; i++) {
    rem = total.mod(DICTIONARY_LENGTH);
    c += DICTIONARY[rem.integerValue(BigNumber.ROUND_FLOOR).toNumber()];
    total = total.div(DICTIONARY_LENGTH);
  }

  return `CSGO-${c.substr(0, 5)}-${c.substr(5, 5)}-${c.substr(10, 5)}-${c.substr(15, 5)}-${c.substr(20, 5)}`;
};

console.log(encode(
  {
    low: -2147483492, high: 752192506
  },
  {
    low: 143, high: 752193760
  },
  55788
));

// OUTPUT:
// CSGO-GADqf-jjyJ8-cSP2r-smZRo-TO2xK

Also I found Python code doing the same (with "match codes", containing less values to encode) - I'm aware it's JS question, and including this only to recognize similarities

import re
 
dictionary = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789"

def _swap_endianness(number):
    result = 0
 
    for n in range(0, 144, 8):
        result = (result << 8) + ((number >> n) & 0xFF)
 
    return result

def encode(matchid, outcomeid, token):
    a = _swap_endianness((token << 128) | (outcomeid << 64) | matchid)
 
    code = ''
    for _ in range(25):
        a, r = divmod(a, len(dictionary))
        code += dictionary[r]
 
    return "CSGO-%s-%s-%s-%s-%s" % (code[:5], code[5:10], code[10:15], code[15:20], code[20:])
    
print(encode(250, 34, 10))
# CSGO-t4kTW-mcVyA-TcReG-hviRe-pXNtQ

Solution

  • The following appears to work. I tried it on objects decoded in the previous question and all of them round-trip losslessly.

    const DICTIONARY = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789";
    const DICTIONARY_LENGTH = BigInt(DICTIONARY.length);
    
    const serializeToBytes = info => {
        const bytes = [
            0,
            1,
            (info.cl_crosshairgap * 10) & 0xff,
            (info.cl_crosshair_outlinethickness * 2) & 7,
            info.cl_crosshaircolor_r,
            info.cl_crosshaircolor_g,
            info.cl_crosshaircolor_b,
            info.cl_crosshairalpha,
            info.cl_crosshair_dynamic_splitdist,
            (info.cl_fixedcrosshairgap * 10) & 0xff,
            (info.cl_crosshaircolor & 7) |
                (info.cl_crosshair_drawoutline ? 8 : 0) |
                (info.cl_crosshair_dynamic_splitalpha_innermod * 10) << 4,
            ((info.cl_crosshair_dynamic_splitalpha_outermod * 10) & 0xf) |
                ((info.cl_crosshair_dynamic_maxdist_splitratio * 10) << 4),
            (info.cl_crosshairthickness * 10) & 0x3f,
            ((info.cl_crosshairstyle << 1) & 0xe) |
                (info.cl_crosshairdot ? 0x10 : 0) |
                (info.cl_crosshairgap_useweaponvalue ? 0x20 : 0) |
                (info.cl_crosshairusealpha ? 0x40 : 0) |
                (info.cl_crosshair_t ? 0x80 : 0),
            (info.cl_crosshairsize * 10) & 0xff,
            ((info.cl_crosshairsize * 10) >> 8) & 0x1f,
            0,
            0
        ];
    
        let sum = 0;
        for (let i = 1; i < bytes.length; ++i) {
            sum += bytes[i];
        }
        bytes[0] = sum & 0xff;
    
        return bytes;
    };
    
    const encode = info => {
        const bytes = serializeToBytes(info);
        
        let acc = 0n;
        let pos = 1n;
        for (let i = bytes.length; i --> 0;) {
            acc += BigInt(bytes[i]) * pos;
            pos *= 256n;
        }
        
        let result = '';
        for (let i = 0; i < 25; ++i) {
            const digit = acc % DICTIONARY_LENGTH;
            acc = acc / DICTIONARY_LENGTH;
            result += DICTIONARY.charAt(Number(digit));
        }
        
        return `CSGO-${result.slice(0, 5)}-${result.slice(5, 10)}-${result.slice(10, 15)}-${result.slice(15, 20)}-${result.slice(20, 25)}`;
    };