Skip to content

Instantly share code, notes, and snippets.

@anadius
Last active November 15, 2024 20:20
Show Gist options
  • Save anadius/e42c8da5d97e93c8a8602dce7adb6afd to your computer and use it in GitHub Desktop.
Save anadius/e42c8da5d97e93c8a8602dce7adb6afd to your computer and use it in GitHub Desktop.
get download URL for your Origin games and generate Denuvo tokens
// ==UserScript==
// @name Origin Helper
// @author anadius
// @namespace anadius.hermietkreeft.site
// @homepageURL https://anadius.hermietkreeft.site/origin-helper
// @match *://www.ea.com/*
// @version 2.2.3
// @icon https://user-images.githubusercontent.com/8550471/187077252-2905e2dc-3241-4946-b1b3-f845a337d766.png
// @icon64 https://user-images.githubusercontent.com/8550471/187077254-476758f1-b784-45bf-a484-18d3ac704e44.png
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @connect api1.origin.com
// @connect api2.origin.com
// @connect api3.origin.com
// @connect proxy.novafusion.ea.com
// @connect achievements.gameservices.ea.com
// ==/UserScript==
///////////////////////////////////////////////////////
// custom setup for UserScript
///////////////////////////////////////////////////////
const customSetup = async () => {
const a = makeLink(null, "Origin Helper", createMainMenu);
a.style.padding = "11px";
const menu = makeElement(document.body, "div", null, {"class": HELPER_ID});
menu.append(a);
};
const HTML_FILE = false;
///////////////////////////////////////////////////////
// common code for HTML file and UserScript
///////////////////////////////////////////////////////
///////////////////////
// libraries
///////////////////////
//https://github.com/beatgammit/base64-js/blob/master/base64js.min.js
(function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"==typeof window?"undefined"==typeof global?"undefined"==typeof self?this:self:global:window,b.base64js=a()}})(function(){return function(){function b(d,e,g){function a(j,i){if(!e[j]){if(!d[j]){var f="function"==typeof require&&require;if(!i&&f)return f(j,!0);if(h)return h(j,!0);var c=new Error("Cannot find module '"+j+"'");throw c.code="MODULE_NOT_FOUND",c}var k=e[j]={exports:{}};d[j][0].call(k.exports,function(b){var c=d[j][1][b];return a(c||b)},k,k.exports,b,d,e,g)}return e[j].exports}for(var h="function"==typeof require&&require,c=0;c<g.length;c++)a(g[c]);return a}return b}()({"/":[function(a,b,c){'use strict';function d(a){var b=a.length;if(0<b%4)throw new Error("Invalid string. Length must be a multiple of 4");var c=a.indexOf("=");-1===c&&(c=b);var d=c===b?0:4-c%4;return[c,d]}function e(a,b,c){return 3*(b+c)/4-c}function f(a){var b,c,f=d(a),g=f[0],h=f[1],j=new m(e(a,g,h)),k=0,n=0<h?g-4:g;for(c=0;c<n;c+=4)b=l[a.charCodeAt(c)]<<18|l[a.charCodeAt(c+1)]<<12|l[a.charCodeAt(c+2)]<<6|l[a.charCodeAt(c+3)],j[k++]=255&b>>16,j[k++]=255&b>>8,j[k++]=255&b;return 2===h&&(b=l[a.charCodeAt(c)]<<2|l[a.charCodeAt(c+1)]>>4,j[k++]=255&b),1===h&&(b=l[a.charCodeAt(c)]<<10|l[a.charCodeAt(c+1)]<<4|l[a.charCodeAt(c+2)]>>2,j[k++]=255&b>>8,j[k++]=255&b),j}function g(a){return k[63&a>>18]+k[63&a>>12]+k[63&a>>6]+k[63&a]}function h(a,b,c){for(var d,e=[],f=b;f<c;f+=3)d=(16711680&a[f]<<16)+(65280&a[f+1]<<8)+(255&a[f+2]),e.push(g(d));return e.join("")}function j(a){for(var b,c=a.length,d=c%3,e=[],f=16383,g=0,j=c-d;g<j;g+=f)e.push(h(a,g,g+f>j?j:g+f));return 1===d?(b=a[c-1],e.push(k[b>>2]+k[63&b<<4]+"==")):2===d&&(b=(a[c-2]<<8)+a[c-1],e.push(k[b>>10]+k[63&b>>4]+k[63&b<<2]+"=")),e.join("")}c.byteLength=function(a){var b=d(a),c=b[0],e=b[1];return 3*(c+e)/4-e},c.toByteArray=f,c.fromByteArray=j;for(var k=[],l=[],m="undefined"==typeof Uint8Array?Array:Uint8Array,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=0,p=n.length;o<p;++o)k[o]=n[o],l[n.charCodeAt(o)]=o;l[45]=62,l[95]=63},{}]},{},[])("/")});
//https://github.com/bevacqua/dragula/blob/master/dist/dragula.min.js
!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).dragula=e()}(function(){return function o(r,i,u){function c(t,e){if(!i[t]){if(!r[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(a)return a(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=i[t]={exports:{}},r[t][0].call(n.exports,function(e){return c(r[t][1][e]||e)},n,n.exports,o,r,i,u)}return i[t].exports}for(var a="function"==typeof require&&require,e=0;e<u.length;e++)c(u[e]);return c}({1:[function(e,t,n){"use strict";var o={},r="(?:^|\\s)",i="(?:\\s|$)";function u(e){var t=o[e];return t?t.lastIndex=0:o[e]=t=new RegExp(r+e+i,"g"),t}t.exports={add:function(e,t){var n=e.className;n.length?u(t).test(n)||(e.className+=" "+t):e.className=t},rm:function(e,t){e.className=e.className.replace(u(t)," ").trim()}}},{}],2:[function(e,t,n){(function(r){"use strict";var M=e("contra/emitter"),k=e("crossvent"),j=e("./classes"),R=document,q=R.documentElement;function U(e,t,n,o){r.navigator.pointerEnabled?k[t](e,{mouseup:"pointerup",mousedown:"pointerdown",mousemove:"pointermove"}[n],o):r.navigator.msPointerEnabled?k[t](e,{mouseup:"MSPointerUp",mousedown:"MSPointerDown",mousemove:"MSPointerMove"}[n],o):(k[t](e,{mouseup:"touchend",mousedown:"touchstart",mousemove:"touchmove"}[n],o),k[t](e,n,o))}function K(e){if(void 0!==e.touches)return e.touches.length;if(void 0!==e.which&&0!==e.which)return e.which;if(void 0!==e.buttons)return e.buttons;e=e.button;return void 0!==e?1&e?1:2&e?3:4&e?2:0:void 0}function z(e,t){return void 0!==r[t]?r[t]:(q.clientHeight?q:R.body)[e]}function H(e,t,n){var o=(e=e||{}).className||"";return e.className+=" gu-hide",n=R.elementFromPoint(t,n),e.className=o,n}function V(){return!1}function $(){return!0}function G(e){return e.width||e.right-e.left}function J(e){return e.height||e.bottom-e.top}function Q(e){return e.parentNode===R?null:e.parentNode}function W(e){return"INPUT"===e.tagName||"TEXTAREA"===e.tagName||"SELECT"===e.tagName||function e(t){if(!t)return!1;if("false"===t.contentEditable)return!1;if("true"===t.contentEditable)return!0;return e(Q(t))}(e)}function Z(t){return t.nextElementSibling||function(){var e=t;for(;e=e.nextSibling,e&&1!==e.nodeType;);return e}()}function ee(e,t){var t=(n=t).targetTouches&&n.targetTouches.length?n.targetTouches[0]:n.changedTouches&&n.changedTouches.length?n.changedTouches[0]:n,n={pageX:"clientX",pageY:"clientY"};return e in n&&!(e in t)&&n[e]in t&&(e=n[e]),t[e]}t.exports=function(e,t){var l,f,s,d,m,o,r,v,p,h,n;1===arguments.length&&!1===Array.isArray(e)&&(t=e,e=[]);var i,g=null,y=t||{};void 0===y.moves&&(y.moves=$),void 0===y.accepts&&(y.accepts=$),void 0===y.invalid&&(y.invalid=function(){return!1}),void 0===y.containers&&(y.containers=e||[]),void 0===y.isContainer&&(y.isContainer=V),void 0===y.copy&&(y.copy=!1),void 0===y.copySortSource&&(y.copySortSource=!1),void 0===y.revertOnSpill&&(y.revertOnSpill=!1),void 0===y.removeOnSpill&&(y.removeOnSpill=!1),void 0===y.direction&&(y.direction="vertical"),void 0===y.ignoreInputTextSelection&&(y.ignoreInputTextSelection=!0),void 0===y.mirrorContainer&&(y.mirrorContainer=R.body);var w=M({containers:y.containers,start:function(e){e=S(e);e&&C(e)},end:O,cancel:L,remove:X,destroy:function(){c(!0),N({})},canMove:function(e){return!!S(e)},dragging:!1});return!0===y.removeOnSpill&&w.on("over",function(e){j.rm(e,"gu-hide")}).on("out",function(e){w.dragging&&j.add(e,"gu-hide")}),c(),w;function u(e){return-1!==w.containers.indexOf(e)||y.isContainer(e)}function c(e){e=e?"remove":"add";U(q,e,"mousedown",E),U(q,e,"mouseup",N)}function a(e){U(q,e?"remove":"add","mousemove",x)}function b(e){e=e?"remove":"add";k[e](q,"selectstart",T),k[e](q,"click",T)}function T(e){i&&e.preventDefault()}function E(e){var t,n;o=e.clientX,r=e.clientY,1!==K(e)||e.metaKey||e.ctrlKey||(n=S(t=e.target))&&(i=n,a(),"mousedown"===e.type&&(W(t)?t.focus():e.preventDefault()))}function x(e){if(i)if(0!==K(e)){if(!(void 0!==e.clientX&&Math.abs(e.clientX-o)<=(y.slideFactorX||0)&&void 0!==e.clientY&&Math.abs(e.clientY-r)<=(y.slideFactorY||0))){if(y.ignoreInputTextSelection){var t=ee("clientX",e)||0,n=ee("clientY",e)||0;if(W(R.elementFromPoint(t,n)))return}n=i;a(!0),b(),O(),C(n);n=function(e){e=e.getBoundingClientRect();return{left:e.left+z("scrollLeft","pageXOffset"),top:e.top+z("scrollTop","pageYOffset")}}(s);d=ee("pageX",e)-n.left,m=ee("pageY",e)-n.top,j.add(h||s,"gu-transit"),function(){if(l)return;var e=s.getBoundingClientRect();(l=s.cloneNode(!0)).style.width=G(e)+"px",l.style.height=J(e)+"px",j.rm(l,"gu-transit"),j.add(l,"gu-mirror"),y.mirrorContainer.appendChild(l),U(q,"add","mousemove",P),j.add(y.mirrorContainer,"gu-unselectable"),w.emit("cloned",l,s,"mirror")}(),P(e)}}else N({})}function S(e){if(!(w.dragging&&l||u(e))){for(var t=e;Q(e)&&!1===u(Q(e));){if(y.invalid(e,t))return;if(!(e=Q(e)))return}var n=Q(e);if(n)if(!y.invalid(e,t))if(y.moves(e,n,t,Z(e)))return{item:e,source:n}}}function C(e){var t,n;t=e.item,n=e.source,("boolean"==typeof y.copy?y.copy:y.copy(t,n))&&(h=e.item.cloneNode(!0),w.emit("cloned",h,e.item,"copy")),f=e.source,s=e.item,v=p=Z(e.item),w.dragging=!0,w.emit("drag",s,f)}function O(){var e;w.dragging&&_(e=h||s,Q(e))}function I(){a(!(i=!1)),b(!0)}function N(e){var t,n;I(),w.dragging&&(t=h||s,n=ee("clientX",e)||0,e=ee("clientY",e)||0,(e=B(H(l,n,e),n,e))&&(h&&y.copySortSource||!h||e!==f)?_(t,e):(y.removeOnSpill?X:L)())}function _(e,t){var n=Q(e);h&&y.copySortSource&&t===f&&n.removeChild(s),A(t)?w.emit("cancel",e,f,f):w.emit("drop",e,t,f,p),Y()}function X(){var e,t;w.dragging&&((t=Q(e=h||s))&&t.removeChild(e),w.emit(h?"cancel":"remove",e,t,f),Y())}function L(e){var t,n,o;w.dragging&&(t=0<arguments.length?e:y.revertOnSpill,!1===(e=A(o=Q(n=h||s)))&&t&&(h?o&&o.removeChild(h):f.insertBefore(n,v)),e||t?w.emit("cancel",n,f,f):w.emit("drop",n,o,f,p),Y())}function Y(){var e=h||s;I(),l&&(j.rm(y.mirrorContainer,"gu-unselectable"),U(q,"remove","mousemove",P),Q(l).removeChild(l),l=null),e&&j.rm(e,"gu-transit"),n&&clearTimeout(n),w.dragging=!1,g&&w.emit("out",e,g,f),w.emit("dragend",e),f=s=h=v=p=n=g=null}function A(e,t){t=void 0!==t?t:l?p:Z(h||s);return e===f&&t===v}function B(t,n,o){for(var r=t;r&&!function(){if(!1===u(r))return!1;var e=D(r,t),e=F(r,e,n,o);if(A(r,e))return!0;return y.accepts(s,r,f,e)}();)r=Q(r);return r}function P(e){if(l){e.preventDefault();var t=ee("clientX",e)||0,n=ee("clientY",e)||0,o=t-d,r=n-m;l.style.left=o+"px",l.style.top=r+"px";var i=h||s,e=H(l,t,n),o=B(e,t,n),u=null!==o&&o!==g;!u&&null!==o||(g&&a("out"),g=o,u&&a("over"));r=Q(i);if(o!==f||!h||y.copySortSource){var c,e=D(o,e);if(null!==e)c=F(o,e,t,n);else{if(!0!==y.revertOnSpill||h)return void(h&&r&&r.removeChild(i));c=v,o=f}(null===c&&u||c!==i&&c!==Z(i))&&(p=c,o.insertBefore(i,c),w.emit("shadow",i,o,f))}else r&&r.removeChild(i)}function a(e){w.emit(e,i,g,f)}}function D(e,t){for(var n=t;n!==e&&Q(n)!==e;)n=Q(n);return n===q?null:n}function F(r,t,i,u){var c="horizontal"===y.direction;return(t!==r?function(){var e=t.getBoundingClientRect();if(c)return n(i>e.left+G(e)/2);return n(u>e.top+J(e)/2)}:function(){var e,t,n,o=r.children.length;for(e=0;e<o;e++){if(t=r.children[e],n=t.getBoundingClientRect(),c&&n.left+n.width/2>i)return t;if(!c&&n.top+n.height/2>u)return t}return null})();function n(e){return e?Z(t):t}}}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./classes":1,"contra/emitter":5,crossvent:6}],3:[function(e,t,n){t.exports=function(e,t){return Array.prototype.slice.call(e,t)}},{}],4:[function(e,t,n){"use strict";var o=e("ticky");t.exports=function(e,t,n){e&&o(function(){e.apply(n||null,t||[])})}},{ticky:10}],5:[function(e,t,n){"use strict";var c=e("atoa"),a=e("./debounce");t.exports=function(r,e){var i=e||{},u={};return void 0===r&&(r={}),r.on=function(e,t){return u[e]?u[e].push(t):u[e]=[t],r},r.once=function(e,t){return t._once=!0,r.on(e,t),r},r.off=function(e,t){var n=arguments.length;if(1===n)delete u[e];else if(0===n)u={};else{e=u[e];if(!e)return r;e.splice(e.indexOf(t),1)}return r},r.emit=function(){var e=c(arguments);return r.emitterSnapshot(e.shift()).apply(this,e)},r.emitterSnapshot=function(o){var e=(u[o]||[]).slice(0);return function(){var t=c(arguments),n=this||r;if("error"===o&&!1!==i.throws&&!e.length)throw 1===t.length?t[0]:t;return e.forEach(function(e){i.async?a(e,t,n):e.apply(n,t),e._once&&r.off(o,e)}),r}},r}},{"./debounce":4,atoa:3}],6:[function(n,o,e){(function(r){"use strict";var i=n("custom-event"),u=n("./eventmap"),c=r.document,e=function(e,t,n,o){return e.addEventListener(t,n,o)},t=function(e,t,n,o){return e.removeEventListener(t,n,o)},a=[];function l(e,t,n){t=function(e,t,n){var o,r;for(o=0;o<a.length;o++)if((r=a[o]).element===e&&r.type===t&&r.fn===n)return o}(e,t,n);if(t){n=a[t].wrapper;return a.splice(t,1),n}}r.addEventListener||(e=function(e,t,n){return e.attachEvent("on"+t,function(e,t,n){var o=l(e,t,n)||function(n,o){return function(e){var t=e||r.event;t.target=t.target||t.srcElement,t.preventDefault=t.preventDefault||function(){t.returnValue=!1},t.stopPropagation=t.stopPropagation||function(){t.cancelBubble=!0},t.which=t.which||t.keyCode,o.call(n,t)}}(e,n);return a.push({wrapper:o,element:e,type:t,fn:n}),o}(e,t,n))},t=function(e,t,n){n=l(e,t,n);if(n)return e.detachEvent("on"+t,n)}),o.exports={add:e,remove:t,fabricate:function(e,t,n){var o=-1===u.indexOf(t)?new i(t,{detail:n}):function(){var e;c.createEvent?(e=c.createEvent("Event")).initEvent(t,!0,!0):c.createEventObject&&(e=c.createEventObject());return e}();e.dispatchEvent?e.dispatchEvent(o):e.fireEvent("on"+t,o)}}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./eventmap":7,"custom-event":8}],7:[function(e,r,t){(function(e){"use strict";var t=[],n="",o=/^on/;for(n in e)o.test(n)&&t.push(n.slice(2));r.exports=t}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],8:[function(e,n,t){(function(e){var t=e.CustomEvent;n.exports=function(){try{var e=new t("cat",{detail:{foo:"bar"}});return"cat"===e.type&&"bar"===e.detail.foo}catch(e){}}()?t:"undefined"!=typeof document&&"function"==typeof document.createEvent?function(e,t){var n=document.createEvent("CustomEvent");return t?n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail):n.initCustomEvent(e,!1,!1,void 0),n}:function(e,t){var n=document.createEventObject();return n.type=e,t?(n.bubbles=Boolean(t.bubbles),n.cancelable=Boolean(t.cancelable),n.detail=t.detail):(n.bubbles=!1,n.cancelable=!1,n.detail=void 0),n}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],9:[function(e,t,n){var o,r,t=t.exports={};function i(){throw new Error("setTimeout has not been defined")}function u(){throw new Error("clearTimeout has not been defined")}function c(t){if(o===setTimeout)return setTimeout(t,0);if((o===i||!o)&&setTimeout)return o=setTimeout,setTimeout(t,0);try{return o(t,0)}catch(e){try{return o.call(null,t,0)}catch(e){return o.call(this,t,0)}}}!function(){try{o="function"==typeof setTimeout?setTimeout:i}catch(e){o=i}try{r="function"==typeof clearTimeout?clearTimeout:u}catch(e){r=u}}();var a,l=[],f=!1,s=-1;function d(){f&&a&&(f=!1,a.length?l=a.concat(l):s=-1,l.length&&m())}function m(){if(!f){var e=c(d);f=!0;for(var t=l.length;t;){for(a=l,l=[];++s<t;)a&&a[s].run();s=-1,t=l.length}a=null,f=!1,function(t){if(r===clearTimeout)return clearTimeout(t);if((r===u||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(t);try{r(t)}catch(e){try{return r.call(null,t)}catch(e){return r.call(this,t)}}}(e)}}function v(e,t){this.fun=e,this.array=t}function p(){}t.nextTick=function(e){var t=new Array(arguments.length-1);if(1<arguments.length)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];l.push(new v(e,t)),1!==l.length||f||c(m)},v.prototype.run=function(){this.fun.apply(null,this.array)},t.title="browser",t.browser=!0,t.env={},t.argv=[],t.version="",t.versions={},t.on=p,t.addListener=p,t.once=p,t.off=p,t.removeListener=p,t.removeAllListeners=p,t.emit=p,t.prependListener=p,t.prependOnceListener=p,t.listeners=function(e){return[]},t.binding=function(e){throw new Error("process.binding is not supported")},t.cwd=function(){return"/"},t.chdir=function(e){throw new Error("process.chdir is not supported")},t.umask=function(){return 0}},{}],10:[function(e,n,t){(function(t){var e="function"==typeof t?function(e){t(e)}:function(e){setTimeout(e,0)};n.exports=e}).call(this,e("timers").setImmediate)},{timers:11}],11:[function(a,e,l){(function(e,t){var o=a("process/browser.js").nextTick,n=Function.prototype.apply,r=Array.prototype.slice,i={},u=0;function c(e,t){this._id=e,this._clearFn=t}l.setTimeout=function(){return new c(n.call(setTimeout,window,arguments),clearTimeout)},l.setInterval=function(){return new c(n.call(setInterval,window,arguments),clearInterval)},l.clearTimeout=l.clearInterval=function(e){e.close()},c.prototype.unref=c.prototype.ref=function(){},c.prototype.close=function(){this._clearFn.call(window,this._id)},l.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},l.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},l._unrefActive=l.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;0<=t&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},l.setImmediate="function"==typeof e?e:function(e){var t=u++,n=!(arguments.length<2)&&r.call(arguments,1);return i[t]=!0,o(function(){i[t]&&(n?e.apply(null,n):e.call(null),l.clearImmediate(t))}),t},l.clearImmediate="function"==typeof t?t:function(e){delete i[e]}}).call(this,a("timers").setImmediate,a("timers").clearImmediate)},{"process/browser.js":9,timers:11}]},{},[2])(2)});
// I had to mess with injection and custom events because it didn't work in UserScript...
const injection = () => {
/*!
* hash-wasm (https://www.npmjs.com/package/hash-wasm)
* (c) Dani Biro
* @license MIT
*/
!function(A,I){"object"==typeof exports&&"undefined"!=typeof module?I(exports):"function"==typeof define&&define.amd?define(["exports"],I):I((A="undefined"!=typeof globalThis?globalThis:A||self).hashwasm=A.hashwasm||{})}(this,(function(A){"use strict";function I(A,I,B,g){return new(B||(B=Promise))((function(i,e){function n(A){try{c(g.next(A))}catch(A){e(A)}}function Q(A){try{c(g.throw(A))}catch(A){e(A)}}function c(A){var I;A.done?i(A.value):(I=A.value,I instanceof B?I:new B((function(A){A(I)}))).then(n,Q)}c((g=g.apply(A,I||[])).next())}))}class B{constructor(){this.mutex=Promise.resolve()}lock(){let A=()=>{};return this.mutex=this.mutex.then((()=>new Promise(A))),new Promise((I=>{A=I}))}dispatch(A){return I(this,void 0,void 0,(function*(){const I=yield this.lock();try{return yield Promise.resolve(A())}finally{I()}}))}}var g;const i="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,e=null!==(g=i.Buffer)&&void 0!==g?g:null,n=i.TextEncoder?new i.TextEncoder:null;function Q(A,I){return(15&A)+(A>>6|A>>3&8)<<4|(15&I)+(I>>6|I>>3&8)}const c="a".charCodeAt(0)-10,t="0".charCodeAt(0);function o(A,I,B){let g=0;for(let i=0;i<B;i++){let B=I[i]>>>4;A[g++]=B>9?B+c:B+t,B=15&I[i],A[g++]=B>9?B+c:B+t}return String.fromCharCode.apply(null,A)}const a=null!==e?A=>{if("string"==typeof A){const I=e.from(A,"utf8");return new Uint8Array(I.buffer,I.byteOffset,I.length)}if(e.isBuffer(A))return new Uint8Array(A.buffer,A.byteOffset,A.length);if(ArrayBuffer.isView(A))return new Uint8Array(A.buffer,A.byteOffset,A.byteLength);throw new Error("Invalid data type!")}:A=>{if("string"==typeof A)return n.encode(A);if(ArrayBuffer.isView(A))return new Uint8Array(A.buffer,A.byteOffset,A.byteLength);throw new Error("Invalid data type!")},E="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",d=new Uint8Array(256);for(let A=0;A<E.length;A++)d[E.charCodeAt(A)]=A;function r(A){const I=function(A){let I=Math.floor(.75*A.length);const B=A.length;return"="===A[B-1]&&(I-=1,"="===A[B-2]&&(I-=1)),I}(A),B=A.length,g=new Uint8Array(I);let i=0;for(let I=0;I<B;I+=4){const B=d[A.charCodeAt(I)],e=d[A.charCodeAt(I+1)],n=d[A.charCodeAt(I+2)],Q=d[A.charCodeAt(I+3)];g[i]=B<<2|e>>4,i+=1,g[i]=(15&e)<<4|n>>2,i+=1,g[i]=(3&n)<<6|63&Q,i+=1}return g}const y=16384,h=new B,s=new Map;function C(A,B){return I(this,void 0,void 0,(function*(){let g=null,i=null,e=!1;if("undefined"==typeof WebAssembly)throw new Error("WebAssembly is not supported in this environment!");const n=()=>new DataView(g.exports.memory.buffer).getUint32(g.exports.STATE_SIZE,!0),c=h.dispatch((()=>I(this,void 0,void 0,(function*(){if(!s.has(A.name)){const I=r(A.data),B=WebAssembly.compile(I);s.set(A.name,B)}const I=yield s.get(A.name);g=yield WebAssembly.instantiate(I,{})})))),t=(A=null)=>{e=!0,g.exports.Hash_Init(A)},E=A=>{if(!e)throw new Error("update() called before init()");(A=>{let I=0;for(;I<A.length;){const B=A.subarray(I,I+y);I+=B.length,i.set(B),g.exports.Hash_Update(B.length)}})(a(A))},d=new Uint8Array(2*B),C=(A,I=null)=>{if(!e)throw new Error("digest() called before init()");return e=!1,g.exports.Hash_Final(I),"binary"===A?i.slice(0,B):o(d,i,B)},D=A=>"string"==typeof A?A.length<4096:A.byteLength<y;let F=D;switch(A.name){case"argon2":case"scrypt":F=()=>!0;break;case"blake2b":case"blake2s":F=(A,I)=>I<=512&&D(A);break;case"blake3":F=(A,I)=>0===I&&D(A);break;case"xxhash64":case"xxhash3":case"xxhash128":F=()=>!1}return yield(()=>I(this,void 0,void 0,(function*(){g||(yield c);const A=g.exports.Hash_GetBuffer(),I=g.exports.memory.buffer;i=new Uint8Array(I,A,y)})))(),{getMemory:()=>i,writeMemory:(A,I=0)=>{i.set(A,I)},getExports:()=>g.exports,setMemorySize:A=>{g.exports.Hash_SetMemorySize(A);const I=g.exports.Hash_GetBuffer(),B=g.exports.memory.buffer;i=new Uint8Array(B,I,A)},init:t,update:E,digest:C,save:()=>{if(!e)throw new Error("save() can only be called after init() and before digest()");const I=g.exports.Hash_GetState(),B=n(),i=g.exports.memory.buffer,c=new Uint8Array(i,I,B),t=new Uint8Array(4+B);return function(A,I){const B=I.length>>1;for(let g=0;g<B;g++){const B=g<<1;A[g]=Q(I.charCodeAt(B),I.charCodeAt(B+1))}}(t,A.hash),t.set(c,4),t},load:I=>{if(!(I instanceof Uint8Array))throw new Error("load() expects an Uint8Array generated by save()");const B=g.exports.Hash_GetState(),i=n(),c=4+i,t=g.exports.memory.buffer;if(I.length!==c)throw new Error(`Bad state length (expected ${c} bytes, got ${I.length})`);if(!function(A,I){if(A.length!==2*I.length)return!1;for(let B=0;B<I.length;B++){const g=B<<1;if(I[B]!==Q(A.charCodeAt(g),A.charCodeAt(g+1)))return!1}return!0}(A.hash,I.subarray(0,4)))throw new Error("This state was written by an incompatible hash implementation");const o=I.subarray(4);new Uint8Array(t,B,i).set(o),e=!0},calculate:(A,I=null,e=null)=>{if(!F(A,I))return t(I),E(A),C("hex",e);const n=a(A);return i.set(n),g.exports.Hash_Calculate(n.length,I,e),o(d,i,B)},hashLength:B}}))}var D={name:"sha1",data:"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",hash:"40d92e5d"};const F=new B;let f=null;A.createSHA1=function(){return C(D,20).then((A=>{A.init();const I={init:()=>(A.init(),I),update:B=>(A.update(B),I),digest:I=>A.digest(I),save:()=>A.save(),load:B=>(A.load(B),I),blockSize:64,digestSize:20};return I}))},A.sha1=function(A){if(null===f)return function(A,B,g){return I(this,void 0,void 0,(function*(){const I=yield A.lock(),i=yield C(B,g);return I(),i}))}(F,D,20).then((I=>(f=I,f.calculate(A))));try{const I=f.calculate(A);return Promise.resolve(I)}catch(A){return Promise.reject(A)}},Object.defineProperty(A,"__esModule",{value:!0})}));
const prog2percent = prog => Math.min(100, 100 * prog).toFixed() + "%";
const calculateSHA1 = async file => {
const oldTitle = document.title;
const sha1 = await hashwasm.createSHA1();
sha1.init();
for(let size=file.size, chunkSize = 8*1024*1024, offset=0; offset<size; offset+=chunkSize) {
document.title = prog2percent(offset / size);
const fileSlice = file.slice(offset, offset + chunkSize);
const chunk = await readAs(fileSlice, "arraybuffer");
sha1.update(new Uint8Array(chunk));
}
document.title = oldTitle;
return sha1.digest().toUpperCase();
};
window.addEventListener("hash_file", async event => {
window.dispatchEvent(new CustomEvent("hash_file_result", {detail: await calculateSHA1(event.detail)}));
}, false);
};
///////////////////////
// utilities
///////////////////////
const calculateSHA1 = file => new Promise(resolve => {
window.dispatchEvent(new CustomEvent("hash_file", {detail: file}));
const process = event => {
window.removeEventListener("hash_file_result", process, false);
resolve(event.detail);
};
window.addEventListener("hash_file_result", process, false);
});
const sleep = ms => new Promise(resolve => window.setTimeout(resolve, ms));
const b64toBlob = async (base64, type) => {
if(typeof type === "undefined")
type = "application/octet-stream";
const response = await fetch(`data:${type};base64,${base64}`);
return response.blob();
};
const makeElement = (parent, tagName, text, attributes) => {
if(typeof text === "undefined")
text = null;
if(typeof attributes === "undefined")
attributes = null;
const element = document.createElement(tagName);
if(text !== null)
element.innerHTML = text;
if(attributes !== null) {
for(const [key, value] of Object.entries(attributes)) {
if(typeof value !== "undefined") {
element.setAttribute(key, value);
}
}
}
if(parent !== null)
parent.append(element);
return element;
};
const makeLink = (parent, name, onClick) => {
const a = makeElement(parent, "a", name, {"href": "#"});
a.addEventListener("click", e => {
e.preventDefault();
onClick(e.target);
return false;
});
return a;
};
const makeRadioCheckbox = (type, parent, name, value) => {
const label = makeElement(parent, "label");
const input = makeElement(label, "input", null, {"type": type, "value": value});
label.append(" ");
label.append(name);
return input;
};
const makeRadioButtons = (parent, id, values) => {
let makeChecked = true;
for(const [name, value] of values) {
const radio = makeRadioCheckbox("radio", parent, name, value);
radio.setAttribute("name", id);
if(makeChecked) {
radio.setAttribute("checked", "checked");
makeChecked = false;
}
}
};
const makePopup = () => {
const container = makeElement(
document.body, "div", null, {"class": `${HELPER_ID} max`}
);
const main = makeElement(container, "div", null, {"class": "main"});
return [container, main];
};
const makeTextareas = (parent, info) => {
const textareas = [];
for(const [label, rows] of info) {
makeElement(parent, "p", label);
const t = makeElement(parent, "textarea", null, {"rows": rows});
textareas.push(t);
}
return textareas;
};
const makeTextareaPopup = (text, defaultValue) => new Promise(resolve => {
const [popup, main] = makePopup();
const resultElement = makeTextareas(main, [[text, 4]])[0];
if(typeof defaultValue !== "undefined")
resultElement.value = defaultValue;
const p = makeElement(main, "p");
const button = makeElement(p, "button", "Continue");
button.addEventListener("click", e => {
e.preventDefault();
popup.remove();
resolve(resultElement.value);
});
});
const makeFilePopup = (text, ext) => new Promise(resolve => {
const [popup, main] = makePopup();
makeElement(main, "p", text);
const fileInput = makeElement(main, "input", null, {"type": "file", "accept": ext});
fileInput.addEventListener("change", e => {
e.preventDefault();
popup.remove();
resolve(e.target.files[0]);
});
});
const decryptLicense_ = async ciphertext => {
const key = await crypto.subtle.importKey(
"raw",
new Uint8Array([65, 50, 114, 45, 208, 130, 239, 176, 220, 100, 87, 197, 118, 104, 202, 9]),
{name: "AES-CBC"}, false, ["decrypt"]
);
const decrypted = await crypto.subtle.decrypt(
{name: "AES-CBC", iv: new ArrayBuffer(16)}, key, ciphertext
);
const dec = new TextDecoder();
return dec.decode(decrypted);
};
const decryptLicense = async ciphertext => {
if(ciphertext instanceof Blob)
ciphertext = await ciphertext.arrayBuffer();
else if(!(ciphertext instanceof ArrayBuffer))
throw new Error("Unsupported ciphertext type!");
try {
return await decryptLicense_(ciphertext);
}
catch(error) {
return await decryptLicense_(ciphertext.slice(0x41));
}
};
// read file (blob) as text or array buffer asynchronously
const readAs = (file, type) => new Promise(resolve => {
const reader = new FileReader();
reader.addEventListener("load", () => {
resolve(reader.result);
}, false);
if(type == "text")
reader.readAsText(file);
else
reader.readAsArrayBuffer(file);
});
const getDom = (text, mimetype) => (new DOMParser()).parseFromString(text, mimetype || "application/xml");
const getEncryptionKey = dom => {
let key = dom.querySelector("CipherKey").innerHTML;
// trim to 22 characters - that's how much you need for a 16 byte key
key = key.substr(0, 22);
// trim the padding
key = key.replace(/=+$/, "");
// invalid base64, remove one character
if(key.length % 4 === 1) {
key = key.slice(0, -1);
}
// add padding if necessary
while(key.length % 4 !== 0) {
key += "=";
}
// 16 bytes or smaller
const keyBytes = base64js.toByteArray(key);
// with this we make sure it's 16 bytes long
const finalKeyBytes = new Uint8Array(new ArrayBuffer(16));
finalKeyBytes.fill(0);
finalKeyBytes.set(keyBytes);
return base64js.fromByteArray(finalKeyBytes);
};
const getHeaders = rawHeaders => {
const headers = Array.from(rawHeaders.matchAll(/^(.*?): (.*?)$/gm));
for(const x of headers) {
x.shift(); // remove the whole match
}
return new Headers(headers);
};
const [getAuthToken, getUsername, getUserID, getPersonaID] = (() => {
let authToken = null;
let username = null;
let userID = null;
let personaID = null;
let timestamp = 0;
const updateCache = async () => {
if(Date.now() - timestamp < 300000) // 5 minutes
return;
const message = (
'Run EA app, log in, and then run <a href="https://github.com/anadius/'
+ 'ea-get-token/releases/latest/download/get_token.exe">this program</a>. '
+ "Copy the 4 lines of output (access token, username, user ID, persona ID)"
+ ', paste them below and click "Continue".<br><br>'
+ "Source code for that program can be found "
+ '<a href="https://github.com/anadius/ea-get-token">here</a>.'
);
const defaultValue = (
authToken === null
? ""
: `${authToken}\n${username}\n${userID}\n${personaID}`
);
const result = (await makeTextareaPopup(message, defaultValue)).trim();
// username characters: https://answers.ea.com/t5/Origin-Client-Web-Technical/Changing-Origin-ID-Prohibited-word-or-character/m-p/3328603/highlight/true#M51146
const values = result.match(/^([a-z0-9\.=_\-\/\+]+)\n([a-z0-9\-_]+)\n(\d+)\n(\d+)$/i);
if(values === null)
throw new Error("Invalid user input!");
values.shift();
[authToken, username, userID, personaID] = values;
timestamp = Date.now();
};
const at = async () => {
await updateCache();
return authToken;
};
const un = async () => {
await updateCache();
return username;
};
const uid = async () => {
await updateCache();
return userID;
};
const pid = async () => {
await updateCache();
return personaID;
};
return [at, un, uid, pid];
})();
const getAllGames = async () => {
const url = (
"https://api3.origin.com/supercat/"
+ "US/en_US/supercat-PCWIN_MAC-US-en_US.json.gz"
);
const response = await xhr("GET", url, null, /*corsProxyAllowed=*/ true);
const offers = (await response.json())["offers"];
if(typeof offers === "undefined")
throw new Error("No offers found!");
return offers;
}
const getMyGames = async () => {
const url = (
"https://api2.origin.com/ecommerce2/consolidatedentitlements/"
+ `${await getUserID()}?machine_hash=1`
);
const headers = {
"Accept": "application/vnd.origin.v3+json; x-cache/force-write",
"AuthToken": await getAuthToken()
};
const response = await xhr("GET", url, headers, /*corsProxyAllowed=*/ false);
const entitlements = (await response.json())["entitlements"];
if(typeof entitlements === "undefined")
throw new Error("No entitlements found!");
return entitlements;
};
const getDownloadLinks = async (gameId, platform) => {
if(typeof platform === "undefined")
platform = "PCWIN";
const url = (
"https://api1.origin.com/ecommerce2/downloadURL?productId="
+ `${gameId}&userId=${await getUserID()}&https=true`
);
const headers = {
"X-Origin-Platform": platform,
"Accept": "application/vnd.origin.v3+json; x-cache/force-write",
"AuthToken": await getAuthToken()
}
const result = await xhr("GET", url, headers, /*corsProxyAllowed=*/ false);
return await result.json();
};
class LicenseError extends Error {
constructor(message) {
super(message);
this.name = "LicenseError";
}
}
const getLicense = async (contentId, machineHash, ticket, engine) => {
let url = (
"https://proxy.novafusion.ea.com/licenses"
+ `?contentId=${contentId}&machineHash=${machineHash}`
+ `&ea_eadmtoken=${await getAuthToken()}`
);
if(typeof ticket !== "undefined" && typeof engine !== "undefined")
url = `${url}&requestToken=${ticket}&requestType=${engine}`;
const headers = {
"User-Agent": "EACTransaction",
"X-Requester-Id": "Origin Online Activation"
}
const response = await xhr("GET", url, headers, /*corsProxyAllowed=*/ false);
const rawLicense = await response.blob();
const signature = response.headers.get("x-signature");
let license;
if(rawLicense.type.startsWith("application/octet-stream")) {
license = await decryptLicense(rawLicense);
}
else if(rawLicense.type.startsWith("application/xml")) {
const text = await rawLicense.text();
const dom = await getDom(text);
let errorCode = null;
try {
errorCode = dom.querySelector("error").getAttribute("code");
}
catch(ignore) {}
if(errorCode === "CG_LIMIT_EXCEEDED")
throw new LicenseError("Activation limit reached for today, try again later!");
else if(errorCode === "VALIDATION_FAILED") {
let cause = null;
try {
cause = dom.querySelector("failure").getAttribute("cause");
}
catch(ignore) {}
if(cause === "NOT_ENTITLED")
throw new LicenseError("You don't own this game!");
else if(cause === "AUTHENTICATION_FAILED")
throw new LicenseError("Bad auth token, refresh the page and try again!");
}
throw new Error(text);
}
else {
throw new Error(`Unknown type - ${rawLicense.type}`);
}
return [license, rawLicense, signature];
};
const sameName = (name1, name2) => (
name1.localeCompare(name2) === 0 ||
name1.replace(": ", " - ").localeCompare(name2) === 0 ||
name1.replace("™", "").localeCompare(name2) === 0 ||
name1.replace(": ", " - ").replace("™", "").localeCompare(name2) === 0
);
const getGameInfos = async (contentIds, name) => {
const allGames = await getAllGames();
const categories = ["BASEGAME", "DLC", "EXPANSION"];
const result = [];
for(const game of allGames) {
try {
if((contentIds.includes(game.masterTitleId) || contentIds.includes(game.contentId)) && categories.includes(game.gameTypeFacetKey)) {
let achievementsId = null;
try {
achievementsId = game.platforms[0].achievementSetOverride;
}
catch(ignore) {}
result.push({
"name": game.i18n.displayName,
"offerId": game.offerId,
"achievementsId": achievementsId,
});
}
}
catch(ignore) {}
}
if(result.length > 0)
return result;
const lowercaseName = name.toLowerCase();
for(const game of allGames) {
try {
let gameName = game.i18n.displayName.toLowerCase();
if(sameName(gameName, lowercaseName)) {
return [{
"name": game.i18n.displayName,
"offerId": game.offerId,
"achievementsId": game.platforms[0].achievementSetOverride,
}];
}
}
catch(ignore) {}
}
throw new Error("Game info not found!");
};
const getAchievements = async setId => {
const url = (
"https://achievements.gameservices.ea.com/achievements/personas/"
+ `${await getPersonaID()}/${setId}/all?lang=en_US&metadata=true&fullset=true`
);
const headers = {
"X-Api-Version": "2",
"X-Application-Key": "Origin",
"X-AuthToken": await getAuthToken(),
};
const response = await xhr("GET", url, headers, /*corsProxyAllowed=*/ false);
const dataText = await response.text();
try {
data = JSON.parse(dataText);
}
catch(error) {
console.error(error);
console.log(dataText);
throw Error("Can't load achievements. Are you sure this game has achievements?");
}
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: "base"});
const achievementIDs = Object.keys(data.achievements).sort(collator.compare);
const achievementTotals = new Map();
const achievementNames = new Map();
const achievementImages = {};
for(const index of achievementIDs) {
if(index.includes(ACHIEVEMENT_IDS_SEPARATOR)) {
throw new Error(
`Achievement index contains ${ACHIEVEMENT_IDS_SEPARATOR}. `
+ "Origin emu uses it as a separator for achievement IDs. Report that."
);
}
const achievement = data.achievements[index];
achievementNames.set(index, achievement.name);
if(achievement.t > 1)
achievementTotals.set(index, achievement.t);
if(achievement.hidden === false) {
achievementImages[index] = achievement.img;
}
}
return {
"ids": achievementIDs,
"totals": achievementTotals,
"names": achievementNames,
"images": achievementImages,
};
};
const getAchievementOverrides = (achievementsInfo, pairs, prefix) => {
const overrides = new Map();
for(const eaId of achievementsInfo.ids) {
const steamId = pairs[eaId];
if(typeof steamId === "undefined") {
continue;
}
if(prefix + eaId !== steamId) {
overrides.set(eaId, steamId);
}
}
return overrides;
};
const addNamesForHiddenAchievements = (achievementsInfo, pairs, achievementsDict) => {
for(const [eaId, eaName] of achievementsInfo.names.entries()) {
const steamId = pairs[eaId];
const steamName = achievementsDict[steamId];
if(typeof steamName !== "undefined" && eaName !== steamName && eaName.toLowerCase().includes("hidden")) {
achievementsInfo.names.set(eaId, steamName);
}
}
};
const getSteamPrefix = pairs => {
const prefixes = {};
for(const [eaId, steamId] of Object.entries(pairs)) {
if(!steamId.endsWith(eaId)) {
return "";
}
const prefix = steamId.slice(0, -(eaId.length));
if(typeof prefixes[prefix] === "undefined") {
prefixes[prefix] = 1;
}
else {
prefixes[prefix] += 1
}
}
let finalPrefix = "";
let maxCount = 1;
for(const [prefix, count] of Object.entries(prefixes)) {
if(count > maxCount || (count === maxCount && prefix.length < finalPrefix.length)) {
finalPrefix = prefix;
maxCount = count;
}
}
return finalPrefix;
};
const makeAchievement = (parent, id, name, url) => {
const container = makeElement(parent, "div", undefined, {"class": "achievement", "data-id": id, "data-name": name});
const inside = makeElement(container, "div");
makeElement(inside, "img", undefined, {"src": url});
makeElement(inside, "span", `${name}<br><code>ID: ${id}</code>`);
};
const getAchievementPairs = async (achievementsId, achievementsInfo, autoPairs, achievements) => new Promise(resolve => {
const [popup, main] = makePopup();
const message = (
"Make sure the achievements from Steam match those from EA. "
+ "You can drag the Steam achievements to change the order. "
+ "Achievements with the same ID or name will have a green background.<br><br>"
+ "Image links for hidden achievements from EA are guessed! "
+ "They may be duplicates of other images, may be shifted by 1, may not load at all!"
+ "<br><br>If you're not sure which achievement is which - "
+ "find someone with EA version that unlocked those achievements."
);
makeElement(main, "p", message);
const eaMain = makeElement(main, "div", undefined, {"class": "achievements ea"});
const steamMain = makeElement(main, "div", undefined, {"class": "achievements steam"});
makeElement(eaMain, "h1", "EA");
makeElement(steamMain, "h1", "Steam");
const ea = makeElement(eaMain, "div");
const steam = makeElement(steamMain, "div");
const knownImages = new Set(Object.values(achievementsInfo.images));
for(const [eaId, name] of achievementsInfo.names.entries()) {
let achievementId = achievementsInfo.images[eaId];
if(typeof achievementId === "undefined") {
// hidden achievements
let tmpId = eaId;
while(knownImages.has(tmpId)) {
const m = tmpId.match(/^([^\d]*)([\d]*)([^\d]*)$/);
tmpId = `${m[1]}${parseInt(m[2]) + 1}${m[3]}`;
}
knownImages.add(tmpId);
achievementId = tmpId;
}
const url = `${EA_ACH_PREFIX}/${achievementsId}-${achievementId}-${EA_ACH_SUFFIX}`;
makeAchievement(ea, eaId, name, url);
const steamAchievement = autoPairs[eaId] || achievements.shift();
if(typeof steamAchievement !== "undefined") {
makeAchievement(steam, ...steamAchievement);
}
}
const testSameName = () => {
const eaAchievements = ea.children;
const steamAchievements = steam.children;
for(const el of [...eaAchievements, ...steamAchievements]) {
el.classList.remove("same-name");
}
for(let i = 0; i < eaAchievements.length; ++i) {
const eaAch = eaAchievements[i];
const steamAch = steamAchievements[i];
if(
DEFAULT_PREFIX + eaAch.getAttribute("data-id") === steamAch.getAttribute("data-id")
|| eaAch.getAttribute("data-id") === steamAch.getAttribute("data-id")
|| eaAch.getAttribute("data-name") === steamAch.getAttribute("data-name")
) {
eaAch.classList.add("same-name");
steamAch.classList.add("same-name");
}
}
};
try {
testSameName();
}
catch(ignore) {} // in case there's more EA achievements than Steam ones
const drake = dragula([steam]);
drake.on("dragend", testSameName);
const p = makeElement(main, "p");
const button = makeElement(p, "button", "Continue");
button.addEventListener("click", e => {
e.preventDefault();
const pairs = {};
for(let i=0; i < ea.children.length && i < steam.children.length; ++i) {
const eaId = ea.children[i].getAttribute("data-id");
const steamId = steam.children[i].getAttribute("data-id");
pairs[eaId] = steamId;
}
popup.remove();
resolve(pairs);
});
});
const getEasyAchievementPairs = (achievementsInfo, achievements) => {
const autoPairs = {};
for(const [eaId, eaName] of achievementsInfo.names.entries()) {
for(let i=0; i < achievements.length; ++i) {
const [steamId, steamName, _] = achievements[i];
if(eaName === steamName) {
autoPairs[eaId] = achievements.splice(i, 1)[0];
break;
}
}
}
return autoPairs;
};
const parseSteamAchievementsPage = pageSource => {
const dom = getDom(pageSource, "text/html");
const appId = dom.querySelector(".scope-app").getAttribute("data-appid");
const achievementElements = dom.querySelectorAll("#js-achievements>table>tbody>tr");
const achievements = [];
const achievementsDict = {};
for(const achievementRow of achievementElements) {
const cells = achievementRow.querySelectorAll("td");
const id = cells[0].innerText.trim();
const name = cells[1].childNodes[0].textContent.trim();
const img = cells[2].querySelector("img").getAttribute("data-name");
achievements.push([id, name, `${STEAM_ACH_PREFIX}/${appId}/${img}`]);
achievementsDict[id] = name;
}
return [appId, achievements, achievementsDict];
};
const getSteamAchievements = async (achievementsId, achievementsInfo, searchUrl) => {
const message = (
"Origin Emulator can save achievements in a format compatible with "
+ '<a href="https://xan105.github.io/Achievement-Watcher/">Achievement Watcher</a>'
+ ", but it needs achievements info from Steam. Find the game on "
+ `<a href="${searchUrl}">SteamDB</a> page, click on the "Achievements" tab`
+ ", open the page source (Ctrl+U), select all text (Ctrl+A), copy it (Ctrl+C), "
+ "paste in the box below (Ctrl+V) and press Continue. "
+ "Leave empty if you want to skip this step."
);
const pageSource = await makeTextareaPopup(message);
if(pageSource.trim() === "") {
return null;
}
const [appId, achievements, achievementsDict] = parseSteamAchievementsPage(pageSource);
const autoPairs = getEasyAchievementPairs(achievementsInfo, achievements);
const pairs = await getAchievementPairs(achievementsId, achievementsInfo, autoPairs, achievements);
const prefix = getSteamPrefix(pairs);
addNamesForHiddenAchievements(achievementsInfo, pairs, achievementsDict);
const overrides = getAchievementOverrides(achievementsInfo, pairs, prefix);
return {
"appId": appId,
"prefix": prefix,
"overrides": overrides,
};
};
const xhr = async (method, url, headers, corsProxyAllowed) => new Promise(async (resolve, reject) => {
if(typeof headers === "undefined" || headers === null)
headers = {};
if(HTML_FILE) {
if(corsProxyAllowed) {
try {
const corsUrl = "https://corsproxy.io/?" + encodeURIComponent(url);
const response = await fetch(corsUrl, {"method": method, "headers": headers});
if(response.status === 200) {
resolve(response);
return;
}
}
catch(ignore) {}
}
if(method.toUpperCase() !== "GET")
reject(new Error("Not implemented!"));
const commandParts = [];
for(const [key, value] of Object.entries(headers)) {
commandParts.push("-H");
commandParts.push(`"${key}: ${value}"`);
}
commandParts.push(`"${url}"`);
const command = TEMPLATE_BAT.replace("{CURL_COMMAND}", commandParts.join(" "));
downloadBlob(new Blob([command], {"type": "application/x-bat"}), TMP_BAT_FILE);
let file;
try {
const ext = TMP_FILE.split(".").pop();
file = await makeFilePopup(
`${USE_USERSCRIPT}Download and run <code>${TMP_BAT_FILE}</code> file. `
+ `It will create a <code>${TMP_FILE}</code>. Select it below:`,
`.${ext}`
);
}
catch(error) {
reject(error);
return;
}
const rawText = await file.text();
const rawHeaders = (rawText).split("\r\n\r\n", 1)[0];
const match = rawHeaders.match(/^HTTP\/[\d\.]+ (\d+) (.*?)\r\n(.*?)$/s);
if(match === null) {
console.log(rawText);
reject(new Error("Invalid HTTP response"));
return;
}
resolve(new Response(
file.slice(rawHeaders.length + 4),
{
"status": parseInt(match[1]),
"statusText": match[2],
"headers": getHeaders(match[3]),
},
));
}
else {
GM.xmlHttpRequest({
"method": method,
"url": url,
"headers": headers,
"responseType": "blob",
"onload": res => {
const response = new Response(
res.response,
{
"status": res.status,
"statusText": res.statusText,
"headers": getHeaders(res.responseHeaders),
},
);
resolve(response);
},
"onerror": res => {
reject(new Error(`Failed to load ${url}`));
},
});
}
});
const downloadBlob = (blob, name) => {
const link = document.createElement("a");
const url = window.URL.createObjectURL(blob);
link.href = url;
link.download = name;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
link.remove();
};
const loadBlacklisted = () => {
try {
return JSON.parse(localStorage.getItem(BLACKLISTED_ID) || "[]");
}
catch(error) {
return [];
}
};
const blacklist = id => {
if(BLACKLISTED.indexOf(id) === -1) {
BLACKLISTED.push(id);
localStorage.setItem(BLACKLISTED_ID, JSON.stringify(BLACKLISTED));
const element = document.querySelector(`[data-offerid="${id}"]`);
if(element !== null)
element.remove();
}
};
class EmptyLine {};
class Comment {};
class CommentedOut {};
var mapToVDF = (object, align=8, tab=4, level=0) => {
if(typeof object !== "object") {
throw new TypeError("A key has value of type other than string or object");
}
let result = "";
const indent = " ".repeat(level * tab);
let maxKeyLength = -1;
for(let [key, value] of object.entries()) {
if(key instanceof EmptyLine) {}
else if(key instanceof Comment) {}
else if(typeof value === "object" && value !== null && !(key instanceof CommentedOut)) {}
else {
if(key.length > maxKeyLength) {
maxKeyLength = key.length;
}
}
}
let calculatedAlign = align * tab - indent.length;
if(maxKeyLength !== -1) {
while(calculatedAlign - (maxKeyLength + 2) < 1) {
calculatedAlign += tab;
}
}
for(let [key, value] of object.entries()) {
if(key instanceof EmptyLine) {
result += "\n";
}
else if(key instanceof Comment) {
result += `${indent}// ${value}\n`;
}
else if(typeof value === "object" && value !== null && !(key instanceof CommentedOut)) {
const section = mapToVDF(value, align, tab, level + 1);
if(section.length > 0)
result += `${indent}"${key}"\n${indent}{\n${section}${indent}}\n`;
}
else {
let commentStart = "";
if(key instanceof CommentedOut) {
[key, value] = value;
commentStart = "//";
}
let spacingRepeat = calculatedAlign;
spacingRepeat -= key.length + 2;
if(spacingRepeat < 1)
spacingRepeat = 1;
const spacing = " ".repeat(spacingRepeat);
result += `${indent}${commentStart}"${key}"${spacing}"${value.toString()}"\n`;
}
}
result = result.replace(/^\n+/, "");
result = result.replace(/\n{2,}$/, "\n");
result = result.replace(/\n{3,}/g, "\n\n");
return result;
};
const handleError = (error, functionOrElement) => {
console.error(error);
const err = `${error.name}: ${error.message}`;
if(typeof functionOrElement === "function")
functionOrElement(err, error);
else {
const el = functionOrElement || document.querySelector(`.${HELPER_ID} > .main`);
el.prepend(makeElement(
null, "p",
"An error occured, report that to anadius.",
{"class": "error"}
));
}
alert(err);
throw error;
};
///////////////////////
// main initialization
///////////////////////
const commonSetup = async () => {
const sc = document.createElement("script");
const lines = injection.toString().split("\n");
lines.pop();
lines.shift();
lines.push("const readAs = " + readAs.toString() + ";");
lines.unshift("(() => {");
lines.push("})();");
sc.innerHTML = lines.join("\n");
document.head.appendChild(sc);
document.head.removeChild(sc);
const style = makeElement(document.head, "style", COMMON_CSS);
await customSetup();
};
const createMainMenu = () => {
const [container, main] = makePopup();
const menu = makeElement(null, "div", null, {"class": "menu"});
main.before(menu);
if(!HTML_FILE) {
const a = makeLink(menu, "Close", () => {
container.remove();
document.body.classList.remove("overflow-hidden");
});
a.classList.add("close-button");
}
for(const [name, onClick] of MENU_ITEMS) {
const a = makeLink(menu, name, target => {
const active = menu.querySelector(".active");
if(active !== null) {
active.classList.remove("active");
}
target.classList.add("active");
main.replaceChildren();
onClick(main);
});
}
document.body.classList.add("overflow-hidden");
};
///////////////////////
// main functions
///////////////////////
// Generate a Denuvo token for people who want to play the game.
const generateDenuvoToken = parent => {
makeElement(parent, "h4", (
"Generate a Denuvo token for people who want to play the game. "
+ "You need to own the game to generate tokens for it!"
));
makeElement(parent, "p", (
"More information "
+ '<a href="https://anadius.su/origin-emulator">here</a> and '
+ '<a href="https://cs.rin.ru/forum/viewtopic.php?f=29&t=115335">here</a>.'
));
const textareas = makeTextareas(
parent,
[["Denuvo ticket:", 5], ["Denuvo token or error message:", 5]]
);
const p = makeElement(null, "p");
const button = makeElement(p, "button", "Get token");
textareas[0].after(p);
button.addEventListener("click", e => {
e.preventDefault();
generateDenuvoTokenAction(textareas);
});
};
const generateDenuvoTokenAction = async textareas => {
const [ticketElement, tokenElement] = textareas;
tokenElement.value = "Loading...";
const ticketLines = ticketElement.value.split("\n");
let ticket = null, engine, id;
for(const line of ticketLines) {
const parts = line.trim().split("|");
if(parts.length !== 3)
continue;
[ticket, engine, id] = parts;
break;
}
if(ticket === null) {
tokenElement.value = "Bad ticket!";
return;
}
try {
const [license, rawLicense, signature] = await getLicense(id, 1, ticket, engine);
const token = (await getDom(license)).querySelector("GameToken");
tokenElement.value = (
token === null
? `no Denuvo token in the license\n\n${license}`
: token.innerHTML
);
}
catch(error) {
if(error instanceof LicenseError) {
tokenElement.value = error.message;
alert(error.message);
}
else
handleError(error);
}
};
///////////////////////
// Get information from license file - license text, encryption key and Denuvo token.
const getInfoFromLicense = parent => {
makeElement(parent, "h4", (
"Get information from license file - license"
+ " text, encryption key and Denuvo token."
));
makeElement(parent, "p", (
"Select the license file from <code>%ProgramData%\\"
+ "Electronic Arts\\EA Services\\License</code>."
));
makeElement(parent, "p", (
"If you don't know which file to select - open "
+ "<code>__Installer\\installerdata.xml</code> from "
+ "your game and look for <code>contentIDs</code> section. The "
+ "file you're looking for will have one of these IDs in name."
));
const fileInput = makeElement(parent, "input", null, {"type": "file", "accept": ".dlf"});
const textareas = makeTextareas(
parent,
[["Encryption key:", 1], ["Denuvo token:", 5], ["License text:", 10]]
);
fileInput.addEventListener("change", e => {
getInfoFromLicenseAction(e.target.files[0], textareas);
});
};
const getInfoFromLicenseAction = async (file, textareas) => {
const [key, denuvo, license] = textareas;
key.value = "";
denuvo.value = "";
license.value = "";
let text;
try {
text = await decryptLicense(file);
}
catch(error) {
handleError(error);
}
license.value = text;
const dom = getDom(text);
const token = dom.querySelector("GameToken");
denuvo.value = (token === null ? "no Denuvo token in the license" : token.innerHTML);
key.value = getEncryptionKey(dom);
}
///////////////////////
// Generate a configuration file for my Origin Emulator.
const generateEmulatorConfig = parent => {
makeElement(parent, "h4", (
"Generate a configuration file for my Origin Emulator. You can toggle the "
+ "features, for some games it's pointless to leave them in the config."
));
makeElement(parent, "p", (
"Select the <code>__Installer\\installerdata.xml</code>"
+ " file from your game folder."
));
const data = {};
data.fileInput = makeElement(parent, "input", null, {"type": "file", "accept": ".xml"});
const p = makeElement(parent, "p");
data.hasDenuvo = makeRadioCheckbox("checkbox", p, "Denuvo game");
data.useDefaultPort = makeRadioCheckbox("checkbox", p, "Use default port");
data.noPretendOnline = makeRadioCheckbox("checkbox", p, "Don't pretend to be online");
data.noFakeAuth = makeRadioCheckbox("checkbox", p, "Don't send fake auth");
data.hasUsername = makeRadioCheckbox("checkbox", p, "Has username visible");
data.hasUserAvatar = makeRadioCheckbox("checkbox", p, "Has avatar visible");
data.addContentSection = makeRadioCheckbox("checkbox", p, "Add Content section");
data.addEntitlement = makeRadioCheckbox("checkbox", p, "Add placeholder Entitlement");
data.addAchievements = makeRadioCheckbox("checkbox", p, "Add achievements");
const button = makeElement(parent, "button", "Generate config");
button.addEventListener("click", e => {
e.preventDefault();
generateEmulatorConfigAction(data);
});
};
const generateEmulatorConfigAction = async d => {
const file = d.fileInput.files[0];
if(typeof file === "undefined") {
alert("No file selected!");
return;
}
let dom, name, version, contentIds, languages, regKey = null, gameInfos = null;
try {
dom = getDom(await file.text());
if(dom.querySelector("parsererror"))
dom = getDom(await readAs(file, "text"));
}
catch(error) {
handleError(error);
}
try {
contentIds = Array.prototype.map.call(
dom.querySelectorAll("contentID"),
(x) => x.innerHTML
);
}
catch(error) {
contentIds = ["0"];
}
try {
name = dom.querySelector('gameTitle[locale="en_US"]').innerHTML;
version = dom.querySelector("gameVersion").getAttribute("version");
const tmp = dom.querySelector("locales").innerHTML.split(",");
tmp.sort();
languages = tmp.join(",");
}
catch(error) {
try {
name = dom.querySelector('localeInfo[locale="en_US"] > title').innerHTML;
version = dom.querySelector("game").getAttribute("gameVersion");
const tmp = [];
for(const locale of dom.querySelectorAll("localeInfo")) {
tmp.push(locale.getAttribute("locale"));
}
tmp.sort();
languages = tmp.join(",");
}
catch(error) {
handleError(error);
}
}
try {
regKey = dom.children[0].outerHTML.match(/\[HKEY[^\\]+\\(.+?\\)Install Dir\]/)[1];
regKey += "Locale";
}
catch(error) {
console.log(error);
}
const config = new Map();
const mainSection = new Map();
const gameSection = new Map();
const emulatorSection = new Map();
const userSection = new Map();
const contentSection = new Map();
const entitlementsSection = new Map();
const achievementsSection = new Map();
config.set("Config2", mainSection);
mainSection.set(new EmptyLine());
mainSection.set("Game", gameSection);
mainSection.set(new EmptyLine());
mainSection.set("Emulator", emulatorSection);
mainSection.set(new EmptyLine());
mainSection.set("User", userSection);
mainSection.set(new EmptyLine());
mainSection.set("Content", contentSection);
mainSection.set(new EmptyLine());
mainSection.set("Entitlements", entitlementsSection);
mainSection.set(new EmptyLine());
mainSection.set("Achievements", achievementsSection);
gameSection.set("Name", name);
gameSection.set("Version", version);
gameSection.set(new EmptyLine());
gameSection.set("ContentId", contentIds[0]);
gameSection.set(new EmptyLine());
if(d.hasDenuvo.checked) {
gameSection.set("DenuvoToken", "PASTE_A_VALID_DENUVO_TOKEN_HERE");
const [popup, main] = makePopup();
makeElement(main, "p", "Hashing file:");
const name = makeElement(main, "p");
const exeFile = await makeFilePopup("Select the fixed exe/not a crack.", ".exe");
name.innerHTML = exeFile.name;
gameSection.set("DenuvoExeHash", await calculateSHA1(exeFile));
const dllFile = await makeFilePopup("Select <code>dbdata.dll</code> or some other DLL file <b>from the same folder as the game exe</b>. It's best if you select a file that changes in every game update.");
name.innerHTML = dllFile.name;
gameSection.set("DenuvoDllHash", await calculateSHA1(dllFile));
if(dllFile.name !== "dbdata.dll")
gameSection.set("DenuvoDllName", dllFile.name);
popup.remove();
}
gameSection.set(new EmptyLine());
gameSection.set("Languages", languages);
gameSection.set("Language", "en_US");
if(regKey !== null)
gameSection.set("LanguageRegistryKey", regKey);
if(d.useDefaultPort.checked)
emulatorSection.set("ServerPort", "default");
emulatorSection.set(new EmptyLine());
if(d.noPretendOnline.checked)
emulatorSection.set("PretendConnected", "false");
if(d.noFakeAuth.checked)
emulatorSection.set("FakeAuth", "false");
if(d.hasUsername.checked) {
userSection.set("Username", "anadius");
userSection.set("PersonaId", "1144668899");
userSection.set("UserId", "1000200030000");
}
userSection.set(new EmptyLine());
if(d.hasUserAvatar.checked) {
userSection.set(new Comment(), "uncomment to override the avatar");
userSection.set(new CommentedOut(), ["Avatar", "..\avatar.png"]);
}
if(d.addContentSection.checked) {
if(gameInfos === null)
{
try {
gameInfos = await getGameInfos(contentIds, name);
}
catch(error) {
handleError(error);
}
}
for(const info of gameInfos) {
const cnt = new Map();
contentSection.set(info.offerId, cnt);
cnt.set("Name", info.name);
cnt.set("Version", version);
cnt.set("State", "INSTALLED");
}
}
if(d.addEntitlement.checked) {
const ent = new Map();
entitlementsSection.set("PUT_ORIGIN_DLC_ID_HERE", ent);
ent.set("Group", "GAME_GROUP_NAME");
ent.set("Version", "0");
ent.set("Type", "DEFAULT");
ent.set("EntitlementTag", "THIS_IS_WHAT_MAKES_THE_DLCS_WORK");
}
if(d.addAchievements.checked) {
let achievementsId = null;
let achievementsInfo = null;
let steamAchievementsInfo = null;
try {
if(gameInfos === null)
gameInfos = await getGameInfos(contentIds, name);
for(const info of gameInfos) {
if(info.achievementsId !== null) {
achievementsId = info.achievementsId;
break;
}
}
if(achievementsId !== null)
achievementsInfo = await getAchievements(achievementsId);
}
catch(error) {
handleError(error);
}
if(achievementsInfo !== null) {
achievementsSection.set("AchievementsSet", achievementsId);
achievementsSection.set("AchievementNames", achievementsInfo.names);
achievementsSection.set("AchievementTotals", achievementsInfo.totals);
try {
const query = encodeURIComponent(name);
const searchUrl = `https://steamdb.info/search/?a=app&q=${query}&type=1&category=0`;
steamAchievementsInfo = await getSteamAchievements(achievementsId, achievementsInfo, searchUrl);
}
catch(error) {
handleError(error);
}
if(steamAchievementsInfo !== null) {
achievementsSection.set("SteamAppId", steamAchievementsInfo.appId);
if(steamAchievementsInfo.prefix !== "") {
achievementsSection.set("SteamIdPrefix", steamAchievementsInfo.prefix);
}
achievementsSection.set("SteamIdOverrides", steamAchievementsInfo.overrides);
}
}
}
downloadBlob(
new Blob(
[mapToVDF(config).replace(/\r\n|\n/g, "\r\n")],
{"type": "octet/stream"}
),
"anadius.cfg",
);
};
///////////////////////
// Generate a configuration file for my Origin Emulator.
const getUserValuesForEmu = async parent => {
makeElement(parent, "h4", (
"Get legit values that you can put in my Origin Emulator config file. "
+ "Useful when you want to play your legit save and the game uses your IDs "
+ "to encrypt it. Or, as in The Sims 4 case, when the game adds your IDs "
+ "to the items you create."
));
const textareas = makeTextareas(
parent,
[["Open <code>anadius.cfg</code> file and replace the values with:", 3]],
);
textareas[0].value = (
`\t\t"Username"\t\t\t\t"${await getUsername()}"\n`
+ `\t\t"PersonaId"\t\t\t\t"${await getPersonaID()}"\n`
+ `\t\t"UserId"\t\t\t\t"${await getUserID()}"`
);
};
///////////////////////
// Get download links for games and DLCs you own
const getGameDownloadLink = async parent => {
makeElement(parent, "h4", "Get download links for games and DLCs you own.");
let entitlements = null;
const loading = makeElement(parent, "p", "Loading...");
try {
entitlements = await getMyGames();
}
catch(error) {
handleError(error);
}
loading.remove();
for(let ent of entitlements) {
delete ent.cdKey;
delete ent.entitlementId;
delete ent.grantDate;
delete ent.updatedDate;
delete ent.terminationDate;
}
makeLink(
parent,
'download "entitlements.json" file',
() => {
downloadBlob(
new Blob(
[JSON.stringify(entitlements, null, '\t')],
{type: "octet/stream"}
),
"entitlements.json"
);
}
);
const radio = makeElement(parent, "p");
makeRadioButtons(radio, HELPER_ID, [["Windows", "PCWIN"], ["Mac", "MAC"]]);
const table = makeElement(
parent, "table", "<tr><th>ID</th><th>url</th><th></th></tr>"
);
for(const x of entitlements) {
if(x.status == "DISABLED" || BLACKLISTED.indexOf(x.offerId) > -1)
continue;
const tr = makeElement(table, "tr", null, {"data-offerid": x.offerId});
makeElement(tr, "td", x.offerId);
makeElement(tr, "td", x.offerPath || x.entitlementTag);
const download = makeElement(tr, "td");
makeLink(download, "get link", getGameDownloadLinkAction);
}
};
const getGameDownloadLinkAction = async target => {
const id = target.parentElement.parentElement.getAttribute("data-offerid");
const platform = document.querySelector('input[name="origin-helper-tools"]:checked').value;
let result;
try {
result = await getDownloadLinks(id, platform);
}
catch(error) {
handleError(error);
}
if(typeof result.error !== "undefined") {
console.log(result);
if(result.error.code === "11000") {
alert("This entitlement is not downloadable, added to ignored list.");
blacklist(id);
}
else {
alert("Some error occured, could be that this entitlement isn't downloadable.");
}
}
else {
console.log(JSON.stringify(result));
prompt("Download URL:", result.url);
}
};
///////////////////////
// Get a license file for a specific game and hardware hash.
const getLicenseFile = parent => {
makeElement(parent, "h4", (
"Get a license file for a specific game and hardware hash. "
+ "This is for people who want to help me with "
+ '<a href="https://anadius.su/origin-unwrapper">'
+ "Origin Unwrapper</a>."
));
makeElement(parent, "p", "gameName#contentID#machineHash:");
const textInput = makeElement(parent, "input", null, {"type": "text"});
const button = makeElement(parent, "button", "Get license");
const statusElement = makeElement(parent, "p");
button.addEventListener("click", e => {
e.preventDefault();
getLicenseFileAction(textInput.value.trim(), statusElement);
});
};
const getLicenseFileAction = async (text, statusElement) => {
statusElement.innerHTML = "Loading...";
const parts = text.split("#");
if(parts.length !== 3) {
statusElement.innerHTML = "Bad input!";
return;
}
const [gameName, contentId, machineHash] = parts;
try {
const [license, rawLicense, signature] = await getLicense(contentId, machineHash);
const signatureBlob = await b64toBlob(signature);
const licenseBlob = new Blob([
signatureBlob,
new Uint8Array(65 - signatureBlob.size),
rawLicense,
]);
downloadBlob(licenseBlob, `${contentId} ${gameName}.dlf`);
statusElement.innerHTML = "Downloaded!";
}
catch(error) {
handleError(error);
}
};
///////////////////////
// constants
///////////////////////
const HELPER_ID = "origin-helper-tools";
const BLACKLISTED_ID = "Origin_helper_blacklist";
const BLACKLISTED = loadBlacklisted();
const ACHIEVEMENT_IDS_SEPARATOR = ";";
const USE_USERSCRIPT = (
`<div class="info">If you don't want to see this popup - use my `
+ '<a href="https://anadius.su/origin-helper" target="_blank">'
+ "UserScript</a> instead.</div><br>"
);
const MENU_ITEMS = [
["Generate Denuvo token", generateDenuvoToken],
["Get info from license", getInfoFromLicense],
["Generate emulator config", generateEmulatorConfig],
["Get user values for emu", getUserValuesForEmu],
["Get game download link", getGameDownloadLink],
["Get license file", getLicenseFile],
];
const COMMON_CSS = `
:root {
--background-color: #212a2e;
--background-hover-color: #1a2225;
--border-color: #000000;
--text-color: #F7F8F8;
--link-color: #828fff;
--code-color: #d63384;
--menu-width: 250px;
}
body.overflow-hidden {
overflow: hidden;
}
.${HELPER_ID} {
display: flex;
position: fixed;
padding: 0;
margin: 0;
top: 0;
left: 0;
z-index: 99999999999;
background: var(--background-color);
color: var(--text-color);
font-family: Open Sans,sans-serif;
text-rendering: optimizeLegibility;
box-sizing: border-box;
}
.${HELPER_ID} > div {
box-sizing: border-box;
flex: 1;
overflow-y: auto;
}
.${HELPER_ID} > .menu {
flex: 0 var(--menu-width);
border-right: 1px solid var(--border-color);
}
.${HELPER_ID} > .menu a, .${HELPER_ID} > a {
display: block;
padding: 20px;
}
.${HELPER_ID} > .menu a.close-button {
border-bottom: 1px solid var(--border-color)
}
.${HELPER_ID} > .menu a:hover {
background: var(--background-color);
text-decoration: none;
filter: brightness(80%);
}
.${HELPER_ID} > .menu a.active {
background: var(--background-color);
filter: brightness(60%);
}
.${HELPER_ID} > .main {
padding: 20px;
}
.${HELPER_ID} > .main > :first-child {
margin-top: 0;
}
.${HELPER_ID} > .main > :last-child, .${HELPER_ID} > .main > :last-child > :last-child {
margin-bottom: 0;
}
.${HELPER_ID} textarea, .${HELPER_ID} input, .${HELPER_ID} button {
display: block;
width: 100%;
background: var(--background-color);
border: 1px solid var(--border-color);
filter: brightness(80%);
color: var(--text-color);
padding: 5px;
margin: 15px 0;
box-sizing: border-box;
}
.${HELPER_ID} input[type="radio"], .${HELPER_ID} input[type="checkbox"] {
width: auto;
display: inline-block;
margin: 0;
}
.${HELPER_ID} label {
display: block;
}
.${HELPER_ID} button {
cursor: pointer;
}
.${HELPER_ID} button:active {
filter: brightness(60%);
}
.${HELPER_ID} a {
color: var(--link-color);
text-decoration: none;
}
.${HELPER_ID} a:hover {
text-decoration: underline;
filter: brightness(80%);
}
.${HELPER_ID} code {
color: var(--code-color);
}
.${HELPER_ID} table {
border-collapse: collapse;
}
.${HELPER_ID} th {
text-align: left;
}
.${HELPER_ID} tr {
background: var(--background-color);
border: 1px solid var(--border-color);
}
.${HELPER_ID} tr:hover {
background: var(--background-hover-color);
}
.${HELPER_ID} tr :nth-child(1), .${HELPER_ID} tr :nth-child(3) {
white-space: nowrap;
}
.${HELPER_ID} tr :nth-child(2) {
width: 100%;
padding: 0 10px;
}
.${HELPER_ID}.max {
width: 100%;
height: 100%;
}
.${HELPER_ID} .error {
color: red;
}
.${HELPER_ID} .info {
color: #7aff4a;
}
.${HELPER_ID} .achievements {
width: 50%;
display: inline-grid;
}
.${HELPER_ID} .achievements > h1 {
text-align: center;
}
.${HELPER_ID} .achievement {
position: relative;
}
.${HELPER_ID} .achievement.same-name::before {
background: rgba(0, 128, 0, 0.40);
content: "";
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}
.${HELPER_ID} .achievement:nth-child(2n+1) {
background: rgba(0, 0, 0, 0.2);
}
.${HELPER_ID} .achievement > div {
display: table-row;
}
.${HELPER_ID} .achievement > div > * {
height: 64px;
display: table-cell;
vertical-align: middle;
}
.${HELPER_ID} .achievement > div > span {
padding: 0 20px;
}
`;
const TMP_BAT_FILE = "request.bat";
const TMP_FILE = "response.bin";
const TEMPLATE_BAT = `@echo off
del ${TMP_FILE} 2> nul
if %ERRORLEVEL% NEQ 0 (
echo Can't delete ${TMP_FILE}, do it yourself and run this script again.
pause
goto:eof
)
curl -i -o ${TMP_FILE} {CURL_COMMAND}
if %ERRORLEVEL% NEQ 0 (
echo cURL command failed.
pause
goto:eof
)
explorer /select,${TMP_FILE}
del ${TMP_BAT_FILE} 2> nul
`;
const STEAM_ACH_PREFIX = "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps";
const EA_ACH_PREFIX = "https://achievements.gameservices.ea.com/achievements/icons";
const EA_ACH_SUFFIX = "208.png";
const DEFAULT_PREFIX = "Achievement_GOSCC_";
(() => {
commonSetup();
})();
///////////////////////////////////////////////////////
// end of common code
///////////////////////////////////////////////////////
@RemmyDev1
Copy link

how does this code work and how do i make it generate the keys? what website do i have to go to?

@anadius
Copy link
Author

anadius commented Oct 17, 2024

Read it yourself. It's not obfuscated. And it clearly says at the top of the script which URLs it matches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment