Encoding numerical values into alphanumeric code (reversing decoding function)

1

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
javascript
node.js
encoding
byte
decoding
asked on Stack Overflow Jul 25, 2020 by user3075373 • edited Jul 26, 2020 by user3075373

1 Answer

1

With the information provided, it would be difficult to impossible to create an endcoder, because there are a lot more encoded bits than you have specified a source for. The code is big enough for 18 bytes. You decoding only uses 14 of them, and not even all of those. Most interesting (to me) is that the least significant bit of byte 13 is unaccounted for.

Even if you come up with reasonable default values for most of these, you have to figure out how to compute byte 0, which appears to be some kind of checksum.

answered on Stack Overflow Jul 26, 2020 by Old Pro

User contributions licensed under CC BY-SA 3.0