UNPKG

@cruncheevos/core

Version:

Parse and generate achievements and leaderboards for RetroAchievements.org

1,625 lines 57.8 kB
function H(e) { return Object.entries(e).reduce((t, r) => (t[r[1]] = r[0], t), {}); } function ae(e) { return e[0].toUpperCase() + e.slice(1); } function j(e, t = !1) { let r = Math.abs(e).toString(16); return t && (r = r.toUpperCase()), `${e < 0 ? "-" : ""}0x` + r; } function s(e, ...t) { return e.map((r, o) => { let i = t[o]; return typeof i == "string" ? i = `"${i}"` : typeof i == "symbol" ? i = String(i) : o >= t.length && (i = ""), r + i; }).join(""); } function x(e) { return Object.prototype.toString.call(e) === "[object Object]"; } function U(e, t = {}) { return !(e === null || typeof e == "symbol" || typeof e == "boolean" || typeof e == "string" && e.trim().length === 0 || (e = Number(e), Number.isNaN(e) || Number.isFinite(e) === !1) || t.isInteger && Number.isInteger(e) === !1 || t.isPositive && e < 0); } function P(e) { for (const t in e) { const r = e[t]; if (x(r)) P(r); else if (Array.isArray(r)) { for (const o of r) (x(o) || Array.isArray(o)) && P(o); Object.freeze(r); } } return Object.freeze(e); } function y(e, t) { const r = new Error(t); return r.cause = e, r; } function le(e) { const t = []; let r = !1, o = 0; for (let i = 0; i < e.length; i++) { const n = e[i]; if (t[o] = t[o] || "", r && n == "\\" && e[i + 1] == '"') { t[o] += '"', i++; continue; } if (n == '"') { r = !r; continue; } if (n == ":" && !r) { o++; continue; } t[o] += n; } return t; } function M(e) { return e.match(/[:"]/g) ? `"${e.replace(/"/g, '\\"')}"` : e; } const F = { andNormalizeId(e, t = "id") { const r = e; if (typeof e == "string") { if (e.trim().length === 0) throw new Error(`expected ${t} as unsigned integer, but got ""`); e = Number(e); } if (Number.isInteger(e) === !1) throw new Error( `expected ${t} as unsigned integer, but got ` + s`${r}` ); if (e < 0 || e >= Number.MAX_SAFE_INTEGER) throw new Error( `expected ${t} to be within the range of 0x0 .. 0xFFFFFFFF, but got ` + s`${r}` ); return e; }, title(e, t = "title") { if (typeof e != "string" || e.trim().length === 0) throw new Error( `expected ${t} as non-empty string, but got ` + s`${e}` ); }, andNormalizeDescription(e) { if (e == null) return ""; if (typeof e != "string") throw new Error(s`expected description as string, but got ${e}`); return e; } }; function W(e) { return e === 0 ? "Core" : `Alt ${e}`; } function Ee(e) { const t = new TextEncoder().encode(e), r = []; for (let o = 0; o < t.length; o += 4) { const i = [...t.slice(o, o + 4)].reverse().map((n) => n.toString(16).padStart(2, "0")).join(""); r.push(parseInt(i, 16)); } return r; } function $(e) { return function(...t) { const r = new p(); return m.call(r, e, ...t), r; }; } const ce = $(""); ce.one = function(e) { if (arguments.length > 1) throw new Error("expected only one condition argument, but got " + arguments.length); return new v(e); }; ce.str = function(e, t) { return Fe( ...Ee(e).map((r, o) => { let i = t( // prettier-ignore r > 16777215 ? "32bit" : r > 65535 ? "24bit" : r > 255 ? "16bit" : "8bit", ["Value", "", r] ); return o > 0 ? i.withLast({ lvalue: { value: i.conditions[i.conditions.length - 1].lvalue.value + o * 4 } }) : i; }) ); }; const Pe = $("Trigger"), Te = $("ResetIf"), ke = $("PauseIf"), Ve = $("AddHits"), Be = $("SubHits"), De = $("Measured"), Ue = $("Measured%"), He = $("MeasuredIf"), We = $("ResetNextIf"), Fe = $("AndNext"), Ge = $("OrNext"), Xe = (...e) => new p().also("once", ...e), _ = /* @__PURE__ */ new WeakMap(); class p { constructor() { this.conditions = [], _.set(this, ""); } /** * Adds conditions wrapped with Trigger flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').trigger('0=2', '0=3').toString() // 0=1_T:0=2_T:0=3 */ trigger(...t) { return m.call(this, "Trigger", ...t), this; } /** * Adds conditions wrapped with ResetIf flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').resetIf('0=2', '0=3').toString() // 0=1_R:0=2_R:0=3 */ resetIf(...t) { return m.call(this, "ResetIf", ...t), this; } /** * Adds conditions wrapped with PauseIf flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').pauseIf('0=2', '0=3').toString() // 0=1_P:0=2_P:0=3 */ pauseIf(...t) { return m.call(this, "PauseIf", ...t), this; } /** * Adds conditions wrapped with AddHits flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').addHits('0=2', '0=3').toString() // 0=1_C:0=2_C:0=3 */ addHits(...t) { return m.call(this, "AddHits", ...t), this; } /** * Adds conditions wrapped with SubHits flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').subHits('0=2', '0=3').toString() // 0=1_D:0=2_D:0=3 */ subHits(...t) { return m.call(this, "SubHits", ...t), this; } /** * Adds conditions wrapped with Measured flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').measured('0=2', '0=3').toString() // 0=1_M:0=2_M:0=3 */ measured(...t) { return m.call(this, "Measured", ...t), this; } /** * Adds conditions wrapped with Measured% flag to the chain * * RAIntegration converts Measured flags to Measured% if *Track as %* checkbox is ticked * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').measuredPercent('0=2', '0=3').toString() // 0=1_G:0=2_G:0=3 */ measuredPercent(...t) { return m.call(this, "Measured%", ...t), this; } /** * Adds conditions wrapped with Measured flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').measuredIf('0=2', '0=3').toString() // 0=1_Q:0=2_Q:0=3 */ measuredIf(...t) { return m.call(this, "MeasuredIf", ...t), this; } /** * Adds conditions wrapped with ResetNextIf flag to the chain * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').resetNextIf('0=2', '0=3').toString() // 0=1_Z:0=2_Z:0=3 */ resetNextIf(...t) { return m.call(this, "ResetNextIf", ...t), this; } /** * Adds conditions wrapped with AndNext flag to the chain * * The final condition in the chain will not have AndNext flag * applied, because the condition will not work correctly that way * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').andNext('0=2', '0=3').toString() // 0=1_N:0=2_0=3 * $('0=1') * .andNext('0=2', '0=3') * .resetIf('0=4').toString() // 0=1_N:0=2_N:0=3_R:0=4 */ andNext(...t) { return m.call(this, "AndNext", ...t), this; } /** * Adds conditions wrapped with OrNext flag to the chain * * The final condition in the chain will not have OrNext flag * applied, because the condition will not work correctly that way * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1').orNext('0=2', '0=3').toString() // 0=1_O:0=2_0=3 * $('0=1') * .orNext('0=2', '0=3') * .resetIf('0=4').toString() // 0=1_O:0=2_O:0=3_R:0=4 */ orNext(...t) { return m.call(this, "OrNext", ...t), this; } /** * Adds conditions to the chain as is * * @example * import { define as $, resetIf } from '@cruncheevos/core' * resetIf('0=1', '0=2') * .also('0=3') * .toString() // R:0=1_R:0=2_0=3 */ also(...t) { return m.call(this, "", ...t), this; } /** * Adds conditions as is with final condition set to have 1 hit * * @example * import { define as $ } from '@cruncheevos/core' * $('0=1') * .once( * andNext('0=2', '0=3') * ).toString() // 0=1_N:0=2_0=3.1. */ once(...t) { return m.call(this, "", "once", ...t), this; } *[Symbol.iterator]() { for (const t of this.conditions) yield t; } /** * Returns new instance of ConditionBuilder with mapped conditions * * Accepts a callback function that acts similar to Array.prototype.map * * If any conditional condition was ignored, it will not appear in the callback * * @example * $('0=1', false && '0=2', '0=3') * .map((c, i) => c.with({ hits: i + 1 })) * .toString() // 0=1.1._0=3.2. */ map(t) { const r = this.conditions.map(t); return new p().also(...r); } /** * Returns new instance of ConditionBuilder with different * values merged into last condition * * `lvalue` and `rvalue` can be specified as partial array, which can be less verbose * * Useful when combined with pointer chains * * @param {Condition.PartialMergedData} data Condition.PartialMergedData * * @example * $( * ['AddAddress', 'Mem', '32bit', 0xcafe], * ['AddAddress', 'Mem', '32bit', 0xbeef], * ['', 'Mem', '32bit', 0, '=', 'Value', '', 120], * ).withLast({ cmp: '!=', rvalue: { value: 9 } }) * .toString() // I:0xXcafe_I:0xXbeef_0xX0!=9 * * $( * ['AddAddress', 'Mem', '32bit', 0xcafe], * ['AddAddress', 'Mem', '32bit', 0xbeef], * ['', 'Mem', '32bit', 0, '=', 'Value', '', 120], * ).withLast({ cmp: '!=', rvalue: rvalue: ['Delta', '32bit', 0] }) * .toString() // I:0xXcafe_I:0xXbeef_0xX0!=d0xX0 */ withLast(t) { return this.map((r, o, i) => o !== i.length - 1 ? r : r.with(t)); } /** * Returns a string with raw condition code * * @example * $( * ['AndNext', 'Mem', '32bit', 0xCAFE, '=', 'Value', '', 5], * ['', 'Delta', '32bit', 0xCAFE, '=', 'Value', '', 4] * ).toString() // N:0xXcafe=5_d0xXcafe=4 */ toString() { return this.conditions.join("_"); } /** * Same as {@link ConditionBuilder.prototype.toString toString()} * * @example * JSON.stringify({ conditions: $('0=1', '0=2') }) * // {"conditions":"0=1_0=2"} */ toJSON() { return this.toString(); } } const xe = /\s+/; function m(e, ...t) { let r = 0; const o = t.filter((n, a) => { if (typeof n == "string" && (n === "once" || n.startsWith("hits"))) { if (a > 0) throw new Error("strings 'once' and 'hits %number%' must be placed before any conditions"); return n === "once" && (r = 1), n.startsWith("hits") && (r = parseInt(n.split(xe)[1])), !1; } return n instanceof p && n.conditions.length === 0 ? !1 : !!n; }); if (o.length === 0) return; const i = _.get(this); if (i === "AndNext" || i === "OrNext") { const n = this.conditions[this.conditions.length - 1]; n.flag === "" && (this.conditions[this.conditions.length - 1] = n.with({ flag: i })); } for (let n = 0; n < o.length; n++) { const a = o[n]; if (a instanceof p) { o.splice(n, 1, ...a), n--; continue; } let l = new v(a); const u = n === o.length - 1, c = u && (e === "AndNext" || e === "OrNext"); u && r > 0 && (l = l.with({ hits: r })), e && l.flag === "" && c === !1 && (l = l.with({ flag: e })), this.conditions.push(l); } _.set(this, e); } function T(e) { const t = e < 0 ? e + 1 + 4294967295 : e; if (e < -2147483648) throw new Error( `${e} (${j(e)}) underflows into positive ${t} (${j( t )}), it's very unlikely you intended for that to happen` ); return t; } const I = (() => { const e = { "": "", PauseIf: "P", ResetIf: "R", ResetNextIf: "Z", AddHits: "C", SubHits: "D", AndNext: "N", OrNext: "O", Measured: "M", "Measured%": "G", MeasuredIf: "Q", Trigger: "T" }, t = { AddSource: "A", SubSource: "B", AddAddress: "I", Remember: "K" }, r = { ...e, ...t }, o = H(r); return delete o[""], { forReading: { toRaw: e }, forCalc: { toRaw: t }, toRaw: r, fromRaw: o }; })(), A = (() => { const e = { Mem: "", Delta: "d", Prior: "p", BCD: "b", Invert: "~" }; return { withSize: { toRaw: e, array: Object.keys(e), fromRaw: H(e) }, withoutSize: { array: ["Value", "Float", "Recall"] } }; })(), G = (() => { const e = { "": "", Bit0: "M", Bit1: "N", Bit2: "O", Bit3: "P", Bit4: "Q", Bit5: "R", Bit6: "S", Bit7: "T", Lower4: "L", Upper4: "U", "8bit": "H", "16bit": " ", "24bit": "W", "32bit": "X", "16bitBE": "I", "24bitBE": "J", "32bitBE": "G", BitCount: "K" }; return { toRaw: e, fromRaw: H(e) }; })(), k = (() => { const e = { Float: "F", FloatBE: "B", Double32: "H", Double32BE: "I", MBF32: "M", MBF32LE: "L" }; return { toRaw: e, fromRaw: H(e) }; })(), w = { forReading: ["=", "!=", "<", "<=", ">", ">="], forCalc: ["+", "-", "*", "/", "%", "&", "^"], isLegalForReading(e) { return typeof e == "string" && this.forReading.includes(e); }, isLegalForCalc(e) { return typeof e == "string" && this.forCalc.includes(e); }, isLegal(e) { return this.isLegalForReading(e) || this.isLegalForCalc(e); } }; function ue(e) { return I.forReading.toRaw.hasOwnProperty(e.flag); } function he(e) { return I.forCalc.toRaw.hasOwnProperty(e.flag); } function Se(e) { return e.lvalue.type === "Recall" && S(e) === !1; } function fe(e) { return e.flag === "Measured" && S(e) === !1; } function ge(e) { return e.flag === "Measured" && S(e) && w.isLegalForCalc(e.cmp); } function S(e) { const t = !!e.rvalue.type, r = !!e.rvalue.size; return !r && !t ? !1 : r ? t : A.withSize.toRaw.hasOwnProperty(e.rvalue.type) ? r : !0; } const O = { value(e, t) { if (A.withSize.array.some((o) => o === e.type)) { if (Number.isInteger(e.value) === !1) throw new Error( `expected ${t} memory address as unsigned integer, but got ` + s`${e.value}` ); if (e.value < 0 || e.value > 4294967295) throw new Error( `expected ${t} memory address to be within the range of 0x0 .. 0xFFFFFFFF, but got ` + s`${e.value}` ); } else if (e.type === "Recall") { if (e.value !== 0 && (t === "lvalue" || e.size !== void 0)) throw new Error( `expected Recall ${t} value to be 0, but got ` + s`${e.value}` ); } else if (e.type === "Value") { if (Number.isInteger(e.value) === !1) throw new Error(`expected ${t} as integer, but got ` + s`${e.value}`); try { var r = T(e.value); } catch (o) { throw y(o, `${t}: ${o.message}`); } if (r > 4294967295) throw new Error( `expected ${t} to be within the range of 0x0 .. 0xFFFFFFFF, but got ` + s`${e.value}` ); } else if (e.type === "Float") { if (Number.isNaN(e.value) || Number.isFinite(e.value) === !1) throw new Error(`expected ${t} as float, but got ` + s`${e.value}`); const o = -294967040, i = 4294967040; if (e.value < o || e.value > i) throw new Error( `expected ${t} to be within the range of ${o} .. ${i}, but got ` + s`${e.value}` ); } else if (e.type !== "") throw new Error(`expected valid ${t} type, but got ` + s`${e.type}`); if (A.withoutSize.array.some((o) => o === e.type) && e.size) throw new Error( `${t} value cannot have size specified, but got ` + s`${e.size}` ); return e.type === "Value" ? { ...e, value: T(e.value) } : e.type === "Recall" && t === "rvalue" ? { ...e, size: e.size === void 0 ? "" : e.size, value: e.value === void 0 ? 0 : e.value } : e; }, calculations(e) { if (he(e) !== !1) { if (e.cmp) { if (w.isLegalForCalc(e.cmp) === !1) throw new Error( `expected an accumulation operator (${w.forCalc.join(" ")}), but got ` + s`${e.cmp}` ); if (S(e) === !1) throw new Error("rvalue must be fully provided if operator is specified"); e.rvalue = O.value(e.rvalue, "rvalue"); } else if (e.cmp === "" && S(e)) throw new Error(`expected an accumulation operator (${w.forCalc.join(" ")}), but got ""`); } }, memoryComparisons(e) { if (!(ue(e) === !1 || Se(e) || fe(e) || ge(e)) && (e.rvalue = O.value(e.rvalue, "rvalue"), w.isLegalForReading(e.cmp) === !1 || !e.cmp)) throw new Error( `expected comparison operator (${w.forReading.join(" ")}), but got ` + s`${e.cmp}` ); } }, Z = { enums(e) { if (I.toRaw.hasOwnProperty(e.flag) === !1) throw new Error(s`expected valid condition flag, but got ${e.flag}`); for (const [t, r] of [ [e.lvalue, "lvalue"], [e.rvalue, "rvalue"] ]) { if (t.type === "Recall") { if (t.size !== "" && (r === "lvalue" || t.size !== void 0)) throw new Error( `expected Recall ${r} size to be empty string, but got ` + s`${t.size}` ); continue; } if (G.toRaw.hasOwnProperty(t.size) === !1 && k.toRaw.hasOwnProperty(t.size) === !1) throw new Error(`expected valid ${r} size, but got ` + s`${t.size}`); } if (e.cmp && w.isLegal(e.cmp) === !1) throw new Error(s`expected an operator or lack of it, but got ${e.cmp}`); }, hits(e) { if (Number.isInteger(e.hits) === !1) throw new Error(s`expected hits as unsigned integer, but got ${e.hits}`); if (e.hits < 0 || e.hits > 4294967295) throw new Error( `expected hits to be within the range of 0x0 .. 0xFFFFFFFF, but got ${e.hits}` ); if (he(e) && e.hits > 0) throw new Error(`hits value cannot be specified with ${e.flag} condition flag`); } }, C = { flag(e) { const t = e.match(d.flag); if (!t) return ["", e]; const r = t[1]; if (I.fromRaw.hasOwnProperty(r.toUpperCase()) === !1) throw new Error(s`expected a legal condition flag, but got ${t[0]}`); return [I.fromRaw[r.toUpperCase()], e.slice(t[0].length)]; }, value(e) { const t = { type: "Mem", size: "", value: 0 }; let r = null, o = !0; if ((r = e.match(d.type)) && (e = e.slice(r[0].length), t.type = A.withSize.fromRaw[r[1].toLowerCase()], o = !1), e.startsWith("{recall}")) e = e.slice(8), t.type = "Recall"; else if (r = e.match(d.valueFloat)) e = e.slice(r[0].length), t.type = "Float", t.value = Number(r[1]); else if (r = e.match(d.memAddress)) { if (e = e.slice(r[0].length), r[1].toLowerCase() === "0x") if (r = e.match(d.sizesRegular)) e = e.slice(r[0].length), t.size = G.fromRaw[r[1].toUpperCase()]; else if (e.match(d.hexValue)) t.size = "16bit"; else throw new Error(s`expected valid size specifier, but got ${e.slice(0, 6)}`); else if (r = e.match(d.sizesExt)) e = e.slice(r[0].length), t.size = k.fromRaw[r[1].toUpperCase()]; else throw new Error(s`expected valid size specifier, but got ${e.slice(0, 6)}`); if (r = e.match(d.hexValue)) { e = e.slice(r[0].length); const i = r[1]; t.value = +("0x" + i); } else throw new Error( s`expected memory address as hex number, but got ${e.slice(0, 6)}` ); } else if (o && (r = e.match(d.valueHex))) e = e.slice(r[0].length), t.type = "Value", t.value = T(parseInt(r[1].replace(d.hexPrefix, "0x"))); else if (o && (r = e.match(d.valueInteger))) e = e.slice(r[0].length), t.type = "Value", t.value = T(Number(r[1])); else throw new Error(s`expected proper definition, but got ${e.slice(0, 6)}`); return [t, e]; }, cmp(e) { const t = e.match(d.cmp); if (!t) throw new Error(s`expected an operator, but got ${e.slice(0, 6)}`); return [t[1], e.slice(t[0].length)]; }, hits(e) { const t = e.match(d.hits); if (!t) throw new Error(s`expected hits definition, but got ${e}`); const r = t[1]; if (U(r, { isInteger: !0, isPositive: !0 })) { const o = Number(r); if (o > 4294967295) throw new Error( `expected hits to be within the range of 0x0 .. 0xFFFFFFFF, but got ${r}` ); return [o, e.slice(t[0].length)]; } else throw new Error(s`expected hits as unsigned integer, but got ${r}`); } }; function Re(e) { e = e.trim(); const t = { flag: "", lvalue: { type: "", size: "", value: 0 }, cmp: "", rvalue: { type: "", size: "", value: 0 }, hits: 0 }; [t.flag, e] = C.flag(e); try { [t.lvalue, e] = C.value(e); } catch (r) { throw y(r, `lvalue: ${r.message}`); } if (e) { [t.cmp, e] = C.cmp(e); const r = ue(t), o = w.isLegalForReading(t.cmp); if ((o || w.isLegalForCalc(t.cmp)) === !1) throw r ? new Error( `expected comparison operator (${w.forReading.join(" ")}), but got ` + s`${t.cmp}` ) : new Error( `expected calculation operator (${w.forCalc.join(" ")}), but got ` + s`${t.cmp}` ); try { [t.rvalue, e] = C.value(e); } catch (n) { throw y(n, `rvalue: ${n.message}`); } e && ([t.hits, e] = C.hits(e)), r === !1 && o && (t.cmp = "", t.rvalue = { type: "", size: "", value: 0 }); } return t; } function Y(e) { if (e.type === "Value") { const t = e.value - 4294967295 - 1; return t >= -4096 && t < 0 ? t.toString() : e.value >= 1e5 ? j(e.value) : e.value.toString(); } else if (e.type) return e.type !== "Float" || e.value >= 1e5 ? j(e.value) : e.value.toString(); } function Ne(e) { const t = e[4] === void 0 && e[5] === void 0 && e[6] === void 0 && e[7] === void 0; return { flag: e[0], lvalue: { type: e[1], size: e[2], value: e[3] }, cmp: t ? "" : e[4], rvalue: { type: t ? "" : e[5], size: t ? "" : e[6], value: t ? 0 : e[7] }, hits: e[8] === void 0 ? 0 : e[8] }; } function ee(e) { let t = ""; return e.type === "Value" ? t += e.value : e.type === "Float" ? (t += "f", t += e.value, Number.isInteger(e.value) && (t += ".0")) : e.type === "Recall" ? t += "{recall}" : (t += A.withSize.toRaw[e.type], k.toRaw.hasOwnProperty(e.size) ? (t += "f", t += k.toRaw[e.size]) : (t += "0x", t += G.toRaw[e.size]), t += e.value.toString(16)), t; } function de(e) { e.forEach((t, r) => { const o = W(r); t.forEach((i, n) => { if (fe(i)) throw new Error( `${o}, condition ${n + 1}: cannot have Measured condition without rvalue specified` ); if (ge(i)) throw new Error( `${o}, condition ${n + 1}: expected comparison operator (${w.forReading.join(" ")}), but got ` + s`${i.cmp}` ); }); }); } function te(e) { return Array.isArray(e) ? Object.assign( {}, typeof e[0] == "string" && { type: e[0] }, typeof e[1] == "string" && { size: e[1] }, typeof e[2] == "number" && { value: e[2] } ) : e; } class v { constructor(t) { if (t instanceof v) return t; if (typeof t == "string") Object.assign(this, Re(t)); else if (Array.isArray(t)) Object.assign(this, Ne(t)); else if (x(t)) this.flag = t.flag, this.cmp = t.cmp, this.rvalue = { ...t.rvalue }, this.lvalue = { ...t.lvalue }, this.hits = t.hits; else throw new Error( s`condition data must be an array, object or string with condition code, but got ${t}` ); Z.enums(this), this.lvalue = O.value(this.lvalue, "lvalue"), O.memoryComparisons(this), O.calculations(this), Z.hits(this), P(this); } /** * Returns new Condition instance with different values merged. * * `lvalue` and `rvalue` can be specified as partial array, which can be less verbose * * @param {DeepPartial<Condition.Data>} data DeepPartial<Condition.Data> * * @example * new Condition('0=1') * .with({ cmp: '!=', rvalue: { value: 47 } }) * .toString() // 0!=47 * * new Condition('0xXcafe=0xXfeed') * .with({ rvalue: ['Delta', '16bit', 0xabcd] }) * .toString() // 0xXcafe=d0x abcd * * new Condition('0xXcafe=0xXfeed') * .with({ rvalue: ['Delta'] }) * .toString() // 0xXcafe=d0xXfeed */ with(t) { return new v({ ...this, ...t, lvalue: { ...this.lvalue, ...te(t.lvalue) }, rvalue: { ...this.rvalue, ...te(t.rvalue) } }); } /** * Returns string representation of Condition * suitable for RetroAchievements and local files. * @example * new Condition(['ResetIf', 'Mem', 'Bit0', 71, '>', 'Delta', 'Bit1', 71, 3]).toString() // 'R:0xM47>d0xN47.3.' */ toString() { let t = ""; return this.flag !== "" && (t += I.toRaw[this.flag] + ":"), t += ee(this.lvalue), S(this) && (t += this.cmp, t += ee(this.rvalue), this.hits && (t += "." + this.hits + ".")), t; } /** * Returns direct Array representation of Condition, * values are exactly same as properties of Condition. * * @example * new Condition(['Measured', 'Mem', '8bit', 4]).toArray() * // [ "Measured", "Mem", "8bit", 4, "", "", "", 0, 0 ] */ toArray() { return [ this.flag, this.lvalue.type, this.lvalue.size, this.lvalue.value, this.cmp, this.rvalue.type, this.rvalue.size, this.rvalue.value, this.hits ]; } /** * Returns prettier Array representation of Condition, which is more suitable for display: * * * Everything is a string * * Values are formatted as hexadecimal if they are greater or equal to 100000 * * Negative values are formatted as decimal if they are greater or equal to -4096, otherwise formatted as hexadecimal with underflow correction * * Hits are empty string if equal to zero * * @example * new cruncheevos.Condition(['ResetIf', 'Mem', '32bit', 0xfeedcafe, '>', 'Value', '', 71]).toArrayPretty() * // [ "ResetIf", "Mem", "32bit", "0xfeedcafe", ">", "Value", "", "71", "" ] * * new cruncheevos.Condition(['', 'Value', '', -4097, '>', 'Value', '', -1]).toArrayPretty() * // [ "", "Value", "", "0xffffefff", ">", "Value", "", "-1", "" ] */ toArrayPretty() { const t = this.rvalue.type === "Recall", r = S(this); return [ this.flag, this.lvalue.type, this.lvalue.size, Y(this.lvalue), this.cmp, r ? this.rvalue.type : "", // prettier-ignore t ? "" : r ? this.rvalue.size : "", // prettier-ignore t ? "" : r ? Y(this.rvalue) : "", this.hits > 0 ? this.hits.toString() : "" ]; } } function L(e, t = {}) { const { considerLegacyValueFormat: r = !1 } = t, o = e.split(r ? "$" : new RegExp("(?<!0x)S")).map((n) => n.trim().length > 0 ? n.split("_") : []), i = r && o.every( (n) => n.every((a) => a.match(d.flag) === null) ); return o.map( (n, a) => n.map((l, u) => { if (i) { l.match(d.legacyTrailingFloat) && (l = l.replace( d.legacyTrailingFloat, (b) => "f" + b )); const c = l.match(d.legacyValue); if (c) { const b = c[1] || "+"; let f = Number(c[2]); f < 0 && (f = T(f)), f > 2147483647 && (f = 2147483647), b === "-" && f > 0 && (f = -f), l = f.toString(); } l = (u === n.length - 1 ? "M" : "A") + ":" + l; } try { return new v(l); } catch (c) { const h = W(a); throw y(c, `${h}, condition ${u + 1}: ${c.message}`); } }) ); } function Q(e, t = {}) { const r = []; if (typeof e == "string") return L(e, t); if (Array.isArray(e)) { const o = []; for (let i = 0; i < e.length; i++) { const n = e[i]; try { n instanceof p ? o.push(...n) : o.push(new v(n)); } catch (a) { throw y(a, `conditions[${i}]: ${a.message}`); } } r.push(o); } else if (e instanceof p) r.push([...e]); else if (x(e)) { let o = !1; const i = []; for (const a in e) { const l = a.match(/^(?:core|alt([1-9]\d*))$/); if (l) l[0] === "core" ? o = !0 : l[1] && i.push(Number(l[1])); else throw new Error(`conditions.${a}: group name must be "core" or "alt1", "alt2"...`); } if (!o) throw new Error('conditions: expected "core" group'); i.sort((a, l) => a - l).forEach((a, l) => { if (a !== l + 1) throw new Error( `conditions: expected "alt${l + 1}" group, but got "alt${a}", make sure there are no gaps` ); }); const n = ["core", ...i.map((a) => `alt${a}`)]; for (const a of n) { const l = e[a]; if (typeof l == "string") try { r.push(...L(l, t)); } catch (u) { throw y(u, `conditions.${a}: ${u.message}`); } else if (l instanceof p) r.push([...l]); else if (Array.isArray(l)) { const u = []; for (let c = 0; c < l.length; c++) try { const h = l[c]; h instanceof p ? u.push(...h) : u.push(new v(h)); } catch (h) { throw y(h, `conditions.${a}[${c}]: ${h.message}`); } r.push(u); } else throw new Error( `conditions.${a}: expected an array of conditions or string, but got ` + s`${l}` ); } } else throw new Error( s`expected conditions as object, array of arrays or string, but got ${e}` ); return r; } const d = (() => { const e = [...w.forCalc, ...w.forReading].sort((t, r) => r.length - t.length).map( (t) => t.split("").map((r) => `\\${r}`).join("") ); return { cmp: new RegExp(`^(${e.join("|")})`), flag: /^(.*?):/i, hits: /^\.(.*)\./, hexPrefix: /h/i, hexValue: /^([\dabcdef]+)/i, sizesRegular: new RegExp( "^(" + Object.values(G.toRaw).filter(Boolean).join("|") + ")", "i" ), sizesExt: new RegExp("^(" + Object.values(k.toRaw).filter(Boolean).join("|") + ")", "i"), memAddress: /^(0x|f)/i, type: new RegExp( "^(" + Object.values(A.withSize.toRaw).filter(Boolean).join("|") + ")", "i" ), valueHex: /^(-?h[\dabcdef]+)/i, valueInteger: /^(-?\d+)/, valueFloat: /^f(-?\d+\.\d+)/i, legacyTrailingFloat: /(-?\d+\.\d+)$/, legacyValue: /^v([-+])?([-+]?\d+)$/i }; })(), pe = ["missable", "progression", "win_condition"], Ie = /* @__PURE__ */ new Set(["", ...pe]), N = { points(e) { if (U(e, { isInteger: !0, isPositive: !0 }) === !1) throw new Error( "expected points value to be a positive integer, but got " + s`${e}` ); }, measuredConditionsMixing(e) { const t = [], r = []; if (e.forEach((o, i) => { const n = W(i); o.forEach((a, l) => { a.flag === "Measured" && t.push([n, l]), a.flag === "Measured%" && r.push([n, l]); }); }), r.length > 0 && t.length > 0) { const o = t[0], i = r[0]; throw new Error( `${o[0]}, condition ${o[1] + 1}: Measured conflicts with ${i[0]}, condition ${i[1] + 1} Measured%, make sure you exclusively use Measured or Measured%` ); } }, andNormalizeAuthor(e) { if (e == null && (e = ""), typeof e != "string") throw new Error(s`expected author as string, but got ${e}`); return e || "cruncheevos"; }, andNormalizeAchievementType(e) { if (e = e === void 0 ? "" : e, Ie.has(e) === !1) throw new Error( `expected type to be one of: [${[...pe].join(", ")}], or empty string, or undefined, but got ` + s`${e}` ); return e; }, andNormalizeBadge(e) { const t = s`expected badge as unsigned integer or filepath starting with local\\\\ and going strictly down, but got ${e}`; if (e == null) return "00000"; if (U(e, { isInteger: !0 })) { const r = Number(e); if (r < 0 || r > 4294967295) throw new Error( `expected badge id to be within the range of 0x0 .. 0xFFFFFFFF, but got ${e}` ); return e.toString().padStart(5, "0"); } else if (typeof e == "string") { const r = e.split("\\\\"); if (r.length < 2 || r[0] !== "local") throw new Error(t); for (const i of r) if (/^\.+$/.test(i)) throw new Error(`encountered ${i} within ${e}, path can only go down`); const o = r[r.length - 1]; if (/^.+\.(png|jpe?g|gif)$/.test(o) === !1) throw new Error(`expected badge filename to be *.(png|jpg|jpeg|gif) but got "${o}"`); return e; } else throw new Error(t); } }, Ae = /^\s+$/; function Le(e) { const t = le(e); if (t.length !== 13 && t.length !== 14) throw new Error( "got an unexpected amount of data when parsing raw achievement string, either there's not enough data or it's not escaped/quoted correctly" ); let r = t[6]; return r.match(Ae) && (r = ""), { id: F.andNormalizeId(t[0]), title: t[2], description: t[3], type: r, author: t[7], points: Number(t[8]), badge: N.andNormalizeBadge(t[13] || ""), conditions: L(t[1]) }; } const re = Symbol(); class V { constructor(t) { const r = t instanceof V; if (typeof t == "string") Object.assign(this, Le(t)); else if (x(t) && r === !1) { let o = t.conditions; t[re] || (o = Q(t.conditions)), Object.assign(this, { id: F.andNormalizeId(t.id), title: t.title, description: t.description, author: t.author, points: t.points, type: t.type, badge: N.andNormalizeBadge(t.badge), conditions: o }); } else throw new Error( "achievement data must be an object or string with achievement code, but got " + (r ? "another Achievement instance" : s`${t}`) ); F.title(this.title), this.description = F.andNormalizeDescription(this.description), this.author = N.andNormalizeAuthor(this.author), N.points(this.points), this.type = N.andNormalizeAchievementType(this.type), de(this.conditions), N.measuredConditionsMixing(this.conditions), P(this); } /** * Returns new Achievement instance with different values merged. * * @param {DeepPartial<Achievement.InputObject>} data DeepPartial<Achievement.InputObject> * * @example * someAchievement * .with({ title: someAchievement.title + 'suffix' }) */ with(t) { return new V({ ...this, ...t, [re]: t.hasOwnProperty("conditions") === !1 }); } /** * Returns string representation of Achievement suitable * for `RACache/Data/GameId-User.txt` file. * * @param desiredData optional parameter, set this to `'achievement'` or `'conditions'` to have corresponding string returned. Default option is `'achievement'`. * * @example * * someAchievement.toString() * someAchievement.toString('achievement') * // '58:"0=1":My Achievement:Do something funny::::cruncheevos:5:::::00000' * * someAchievement.toString('conditions') // '0=1' */ toString(t = "achievement") { const r = this.conditions.map((o) => o.map((i) => i.toString()).join("_")).join("S"); if (t === "conditions") return r; if (t === "achievement") { let o = ""; return o += this.id + ":", o += `"${r}":`, o += M(this.title) + ":", o += M(this.description), o += ":::", o += this.type + ":", o += M(this.author) + ":", o += this.points, o += ":::::", o += this.badge.startsWith("local\\\\") ? `"${this.badge}"` : this.badge, o; } else throw new Error(s`unexpected achievement data toString request: ${t}`); } } const oe = /* @__PURE__ */ new Set(["start", "cancel", "submit", "value"]), ie = /* @__PURE__ */ new Set([ "SCORE", "TIME", "FRAMES", "MILLISECS", "SECS", "TIMESECS", "MINUTES", "SECS_AS_MINS", "VALUE", "UNSIGNED", "TENS", "HUNDREDS", "THOUSANDS", "FIXED1", "FIXED2", "FIXED3" ]), E = { andNormalizeLeaderboardId(e) { if (typeof e == "string") if (e.startsWith("L")) e = e.slice(1); else throw new Error(`expected id to start with L, but got "${e}"`); return F.andNormalizeId(e); }, andNormalizeConditions(e) { let t; if (typeof e == "string") t = ze(e); else if (x(e)) t = Object.keys(e).reduce((r, o) => { if (oe.has(o) === !1) throw new Error( `expected leaderboard condition group name to be one of: [${[ ...oe ].join(", ")}], but got ` + s`${o}` ); return r[o] = Q(e[o], { considerLegacyValueFormat: o === "value" }), r; }, {}); else throw new Error(s`expected conditions to be an object, but got ${e}`); for (const r of t.value) if (r.some((i) => i.flag === "Measured") === !1) for (let i = 0; i < r.length; i++) { const n = r[i]; if (n.flag === "") { r[i] = n.with({ flag: "Measured" }); break; } } return t; }, leaderboardType(e) { if (ie.has(e) === !1) throw new Error( `expected type to be one of: [${[...ie].join(", ")}], but got ` + s`${e}` ); }, measuredConditions(e) { for (const t of ["start", "cancel", "submit", "value"]) try { t !== "value" && de(e[t]), E.lackOfMeasuredPercent(e[t]); } catch (r) { throw y(r, `${ae(t)}, ` + r.message); } }, lackOfMeasuredPercent(e) { e.forEach((t, r) => { t.forEach((o, i) => { if (o.flag === "Measured%") { const n = W(r); throw new Error( `${n}, condition ${i + 1}: Measured% conditions are not allowed in leaderboards` ); } }); }); }, andNormalizeLowerIsBetter(e) { if (typeof e == "string") return e.length > 0 && e !== "0"; if (typeof e == "boolean") return e; throw new Error( s`expected lowerIsBetter as boolean or string, but got ${e}` ); } }; function D(e, t = "S") { return e.map((r) => r.map((o) => o.toString()).join("_")).join(t); } function Ce(e) { const t = le(e); if (t.length !== 9) throw new Error( "got an unexpected amount of data when parsing raw leaderboard string, either there's not enough data or it's not escaped/quoted correctly" ); return { id: E.andNormalizeLeaderboardId(t[0]), conditions: E.andNormalizeConditions({ start: t[1], cancel: t[2], submit: t[3], value: t[4] }), type: t[5], title: t[6], description: t[7], lowerIsBetter: E.andNormalizeLowerIsBetter(t[8]) }; } const ne = Symbol(); class B { constructor(t) { const r = t instanceof B; if (typeof t == "string") Object.assign(this, Ce(t)); else if (x(t) && r === !1) { let o = t.conditions; t[ne] || (o = E.andNormalizeConditions(t.conditions)), Object.assign(this, { ...t, id: E.andNormalizeLeaderboardId(t.id), title: t.title, description: t.description, type: t.type, lowerIsBetter: E.andNormalizeLowerIsBetter(t.lowerIsBetter), conditions: o }); } else throw new Error( "leaderboard data must be an object or string with leaderboard code, but got " + (r ? "another Leaderboard instance" : s`${t}`) ); F.title(this.title), this.description = F.andNormalizeDescription(this.description), E.leaderboardType(this.type), E.measuredConditions(this.conditions), P(this); } /** * Returns new Leaderboard instance with different values merged. * * @param {DeepPartial<Leaderboard.InputObject>} data DeepPartial<Leaderboard.InputObject> * * @example * someLeaderboard * .with({ title: someLeaderboard.title + 'suffix' }) */ with(t) { return new B({ ...this, ...t, [ne]: t.hasOwnProperty("conditions") === !1 }); } /** * Returns string representation of Leaderboard suitable * for `RACache/Data/GameId-User.txt` file. * * @param desiredData optional parameter, set this to `'leaderboard'` or `'conditions'` to have corresponding string returned. Default option is `'leaderboard'`. * * @example * * someLeaderboard.toString() * someLeaderboard.toString('leaderboard') * // 'L58:"0xHfff0=1S":"0=1":"1=1":"M:0xX34440*2":SCORE:My Leaderboard:Best score while doing something funny:0' * * someLeaderboard.toString('conditions') // '"0xHfff0=1S":"0=1":"1=1":"M:0xX34440*2"' */ toString(t = "leaderboard") { const r = [ D(this.conditions.start), D(this.conditions.cancel), D(this.conditions.submit), D(this.conditions.value, "$") ].map((o) => `"${o}"`); if (t === "conditions") return r.join(":"); if (t === "leaderboard") { let o = ""; return o += "L" + this.id + ":", o += r.join(":") + ":", o += this.type + ":", o += M(this.title) + ":", o += M(this.description) + ":", o += Number(this.lowerIsBetter), o; } else throw new Error(s`unexpected leaderboard data toString request: ${t}`); } } function ze(e) { const t = { start: null, cancel: null, submit: null, value: null }; let r = null; for (const [o, i] of [ ["STA", "start"], ["CAN", "cancel"], ["SUB", "submit"], ["VAL", "value"] ]) { const n = o === "VAL"; if (r = e.match(new RegExp(n ? /VAL:(.+)/ : `${o}:(.+?)::`))) { e = e.slice(r[0].length); try { t[i] = Q(r[1], { considerLegacyValueFormat: n }); } catch (a) { throw y(a, `${ae(i)}, ${a.message}`); } } else throw new Error(`expected ${o}:<conditions>::, but got ${e.slice(0, 6)}`); } return t; } const q = /* @__PURE__ */ new WeakMap(); function* se() { for (const e in this) yield this[e]; } class qe { /** * Creates AchievementSet. * * @example * new AchievementSet({ gameId: 1234, title: 'Funny Game' }) */ constructor(t) { this.achievements = { [Symbol.iterator]: se }, this.leaderboards = { [Symbol.iterator]: se }; const { gameId: r, title: o } = t; this.gameId = F.andNormalizeId(r, "gameId"), F.title(o, "achievement set title"), this.title = o, q.set(this, { achievementIdCounter: 111000001, leaderboardIdCounter: 111000001 }); } /** * Adds Achievement to the set, accepts same data as {@link Achievement} class constructor, * but you're allowed to omit id when passing an object (id will be assigned automatically, similar to how RAIntegration does it). * * Also returns current AchievementSet instance, allowing you to chain calls. * * @example * import { AchievementSet, define as $ } from '@cruncheevos/core' * * const set = new AchievementSet({ gameId: 1234, title: 'Funny Game' }) * * set.addAchievement({ * id: 58, // optional, or numeric string * title: 'My Achievement', * description: 'Do something funny', * points: 5, * badge: `local\\\\my_achievement.png`, // optional, or ID of badge on server * author: 'peepy', // optional and is not uploaded to server * conditions: { * core: [ * ['', 'Mem', '8bit', 0x00fff0, '=', 'Value', '', 0], * ['', 'Mem', '8bit', 0x00fffb, '=', 'Value', '', 0], * ], * alt1: $( * ['', 'Mem', '8bit', 0x00fe10, '>', 'Delta', '8bit', 0x00fe10], * ['', 'Mem', '8bit', 0x00fe11, '=', 'Value', '', 0], * ), * alt2: '0=1' * } * }).addAchievement(...) */ addAchievement(t) { const r = q.get(this), o = t instanceof V ? t : new V( typeof t == "string" ? t : { ...t, id: t.id || r.achievementIdCounter } ), { id: i } = o; if (this.achievements[i]) throw new Error(`achievement with id ${i}: "${this.achievements[i].title}", already exists`); return this.achievements[i] = o, o.id >= r.achievementIdCounter && (r.achievementIdCounter = Math.max(r.achievementIdCounter + 1, o.id + 1)), this; } /** * Adds Leaderboard to the set, accepts same data as {@link Leaderboard} class constructor, * but you're allowed to omit id when passing an object (id will be assigned automatically, similar to how RAIntegration does it). * * Also returns current AchievementSet instance, allowing you to chain calls. * * @example * import { AchievementSet, define as $ } from '@cruncheevos/core' * * const set = new AchievementSet({ gameId: 1234, title: 'Funny Game' }) * * set.addLeaderboard({ * id: 58, // optional, or numeric string * title: 'My Leaderboard', * description: 'Best score while doing something funny', * type: 'SCORE', * lowerIsBetter: false, * conditions: { * start: { * core: [ * ['', 'Mem', '8bit', 0x00fff0, '=', 'Value', '', 0], * ['', 'Mem', '8bit', 0x00fffb, '=', 'Value', '', 0], * ], * alt1: $( * ['', 'Mem', '8bit', 0x00fe10, '>', 'Delta', '8bit', 0x00fe10], * ['', 'Mem', '8bit', 0x00fe11, '=', 'Value', '', 0], * ), * alt2: '0=1', * }, * cancel: [ * ['', 'Mem', '16bit', 0x34684, '=', 'Value', '', 0x140] * ], // same as providing an object: { core: [ ... ] } * submit: '0xH59d76=2', * value: [['Measured', 'Mem', '32bit', 0x34440, '*', 'Value', '', 2]], * }, * }).addLeaderboard(...) */ addLeaderboard(t) { const r = q.get(this), o = t instanceof B ? t : new B( typeof t == "string" ? t : { ...t, id: t.id || r.leaderboardIdCounter } ), { id: i } = o; if (this.leaderboards[i]) throw new Error(`leaderboard with id ${i}: "${this.leaderboards[i].title}", already exists`); return this.leaderboards[i] = o, o.id >= r.leaderboardIdCounter && (r.leaderboardIdCounter = Math.max(r.leaderboardIdCounter + 1, o.id + 1)), this; } /** * Allows to iterate the whole set for both achievements and leaderboards. * * @example * for (const asset of achSet) { * if (asset instanceof Achievement) { * // ... * } * if (asset instanceof Leaderboard) { * // ... * } * } */ *[Symbol.iterator]() { for (const t of this.achievements) yield t; for (const t of this.leaderboards) yield t; } /** * Returns string representation of AchievementSet suitable for * `RACache/Data/GameId-User.txt` file. * * First line is version, always set to 1.0, second line is game's title. * Then come string representations of achievements and leaderboards, * each sorted by id. * * @example * new AchievementSet({ gameId: 1234, title: 'Funny Game' }) * .addAchievement(...) * .addAchievement(...) * .addLeaderboard(...) * .addLeaderboard(...) * .toString() * // may result in: * ` * 1.0 * Funny Game * 57:"0x cafe=102":Ach2:Desc2::::cruncheevos:2:::::00000 * 111000001:"0x cafe=101":Ach1:Desc1::::cruncheevos:1:::::00000 * L58:"0x cafe=102":"0=1":"1=1":"M:0x feed":FRAMES:Lb2:Desc2:1 * L111000001:"0x cafe=101":"0=1":"1=1":"M:0x feed":SCORE:Lb1:Desc1:0 * ` */ toString() { let t = ""; return t += `1.0 `, t += this.title + ` `, Object.keys(this.achievements).sort((r, o) => Number(r) - Number(o)).forEach((r) => { t += this.achievements[r].toString() + ` `; }), Object.keys(this.leaderboards).sort((r, o) => Number(r) - Number(o)).forEach((r) => { t += this.leaderboards[r].toString() + ` `; }), t; } } const me = Symbol("isRichLookupOrFormat"), Me = /* @__PURE__ */ new Set([ "VALUE", "SCORE", "POINTS", "TIME", "FRAMES", "MILLISECS", "SECS", "MINUTES", "SECS_AS_MINS", "FLOAT1", "FLOAT2", "FLOAT3", "FLOAT4", "FLOAT5", "FLOAT6" ]); function Oe(e) { const { numbers: t, notNumbers: r } = e.reduce( (a, l) => { const u = Number(l); return Number.isNaN(u) ? a.notNumbers.push(l) : a.numbers.push(u), a; }, { numbers: [], notNumbers: [] } ); if (t.length === 0) return { formattedRanges: "", notNumbers: r }; const o = []; let i = t[0], n = t[0]; for (let a = 1; a < t.length; a++) t[a] - n === 1 || (o.push([i, n]), i = t[a]), n = t[a]; return o.push([i, n]), { notNumbers: r, formattedRanges: o.map(([a, l]) => a === l ? `${a}` : `${a}-${l}`).join(",") }; } function je(e) { return e && typeof e != "string" && e[me]; } function we(e, ...t) { return e.map((r, o) => { let i = o === e.length - 1 ? "" : t[o]; return je(i) && (i = i.at()), `${r}${i}`; }).join(""); } function z(e) { const t = e instanceof p ? e.conditions : Array.isArray(e) ? e : [e]; return t[t.length - 1].flag !== "Measured"; } function K(e) { return (e instanceof p ? e.conditions : [e]).length === 1 ? e.toString().replace("M:", "").replace(" ", "") : e.toString(); } function be(e, t) { return `?${e}?${t}`; } function g(e) { const { name: t, type: r } = e; return { /** Name of this Rich Presence Format */ name: t, /** Type of this Rich Presence Format, such as SCORE, FRAMES, etc. */ type: r, /** * Returns string representation of Rich Presence Format definition * * @example * import { RichPresence } from '@cruncheevos/core' * const format = RichPresence.format({ name: 'Score', type: 'VALUE' }) * format.toString() // 'Format:Score\nFormatType=VALUE' */ toString() { return `Format:${t} FormatType=${r}`; }, /** * Returns string representation of Rich PresenceFormat macro call * * If there's only one condition - output may be shortened. * * When passing Condition or ConditionBuilder - you must have * at least one condition mark