From 14689543c9fdf925549cbbebc9cb651be4b2ccfa Mon Sep 17 00:00:00 2001 From: suXin Date: Wed, 4 Sep 2024 12:34:29 +0700 Subject: [PATCH] feat(core): add define.str and stringToNumberLE functions --- packages/core/define.md | 27 +++++++++++++- packages/core/src/define.ts | 52 ++++++++++++++++++++++++++- packages/core/src/index.ts | 1 + packages/core/src/test/define.test.ts | 51 ++++++++++++++++++++++++++ packages/core/src/util.ts | 23 ++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/packages/core/define.md b/packages/core/define.md index c775b76..85e84c7 100644 --- a/packages/core/define.md +++ b/packages/core/define.md @@ -16,7 +16,7 @@ To keep things concise, all examples focus on usage of the function and how it a - [Mapping](#mapping) - [withLast](#withlast) - [toString / toJSON](#tostring--tojson) - +- [String comparisons](#string-comparisons) ## Basic usage @@ -346,4 +346,29 @@ JSON.stringify({ conditions1, conditions2 }, null, 2 ) "conditions2": "0=3_0=4" } */ +``` + +## String comparisons + +Sometimes you need to compare strings, which can be tedious if you have long strings and AddAddress chains. + +`define.str` function helps defining such comparisons: + +```js +import { define as $ } from '@cruncheevos/core' + +$.str( + 'abcde', + ( + size, // '32bit' | '24bit' | '16bit' | '8bit' + value // ['Value', '', someNumber] + ) => $( + ['AddAddress', 'Mem', '32bit', 0xcafe], + ['AddAddress', 'Mem', '32bit', 0xfeed], + ['', 'Mem', size, 0xabcd, '=', ...value], + ) +) +// "I:0xXcafe_I:0xXfeed_N:0xXabcd=1684234849_I:0xXcafe_I:0xXfeed_0xHabcd=101" +// abcd = 0x64636261 = 1684234849 +// e = 0x65 = 101 ``` \ No newline at end of file diff --git a/packages/core/src/define.ts b/packages/core/src/define.ts index 8957ee0..7ef7f95 100644 --- a/packages/core/src/define.ts +++ b/packages/core/src/define.ts @@ -1,5 +1,5 @@ import { Condition } from './condition.js' -import { DeepPartial } from './util.js' +import { DeepPartial, stringToNumberLE } from './util.js' type ConditionBuilderInput = Array @@ -16,6 +16,37 @@ type DefineFunction = ((...args: ConditionBuilderInput) => ConditionBuilder) & { * const notNTSC = isNTSC.with({ cmp: '!=' }) */ one: (arg: Condition.Input) => Condition + + /** + * Allows to generate conditions for comparing strings + * + * The string is split into numeric chunks, little endian, up to 32bit, + * which are provided to the supplied callback. The final result is also + * wrapped with `andNext` + * + * Internally, TextEncoder is used and the input is treated as UTF-8 + * + * If you need to treat input as UTF-16, currently you need to convert it to UTF-16 yourself + * + * @example + * import { define as $ } from '@cruncheevos/core' + * + * $.str( + * 'abcde', + * (size, value) => $( + * ['AddAddress', 'Mem', '32bit', 0xcafe], + * ['AddAddress', 'Mem', '32bit', 0xfeed], + * ['', 'Mem', size, 0xabcd, '=', ...value], + * ) + * ) + * // "I:0xXcafe_I:0xXfeed_N:0xXabcd=1684234849_I:0xXcafe_I:0xXfeed_0xHabcd=101" + * // abcd = 0x64636261 = 1684234849 + * // e = 0x65 = 101 + */ + str: ( + input: string, + callback: (s: Condition.Size, v: ['Value', '', number]) => ConditionBuilder, + ) => ConditionBuilder } function makeBuilder(flag: Condition.Flag) { @@ -52,6 +83,25 @@ define.one = function (arg) { return new Condition(arg) } +define.str = function ( + input: string, + cb: (s: Condition.Size, v: ['Value', '', number]) => ConditionBuilder, +) { + return andNext( + ...stringToNumberLE(input).map(value => + cb( + // prettier-ignore + value > 0xFFFFFF ? '32bit' : + value > 0xFFFF ? '24bit' : + value > 0xFF ? '16bit' : + '8bit', + + ['Value', '', value], + ), + ), + ) +} + /** * Same as {@link define}, but starts the condition chain * by wrapping the passed conditions with Trigger flag diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4321836..9dc853a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,3 +6,4 @@ import { AchievementSet } from './set.js' export { Condition, Achievement, Leaderboard, AchievementSet } export * from './define.js' export { RichPresence } from './rich.js' +export { stringToNumberLE } from './util.js' diff --git a/packages/core/src/test/define.test.ts b/packages/core/src/test/define.test.ts index e53cc04..2026178 100644 --- a/packages/core/src/test/define.test.ts +++ b/packages/core/src/test/define.test.ts @@ -206,3 +206,54 @@ describe('define', () => { ).toThrowErrorMatchingInlineSnapshot(`[Error: expected only one condition argument, but got 3]`) }) }) + +describe('ASCII to Conditions', () => { + const $ = define + + test('regular tests', () => { + expect($.str('a', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"0xHcafe=97"`, + ) + expect($.str('ab', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"0x cafe=25185"`, + ) + expect($.str('abc', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"0xWcafe=6513249"`, + ) + expect($.str('abcd', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"0xXcafe=1684234849"`, + ) + expect($.str('abcde', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"N:0xXcafe=1684234849_0xHcafe=101"`, + ) + expect($.str('abcdef', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"N:0xXcafe=1684234849_0x cafe=26213"`, + ) + }) + + test('unicode hack', () => { + expect($.str('\u0000a', (s, v) => $(['', 'Mem', s, 0xcafe, '=', ...v]))).toMatchInlineSnapshot( + `"0x cafe=24832"`, + ) + }) + + test('with a pointer', () => { + const builder = (s: Condition.Size, v: ['Value', '', number]) => + $( + ['AddAddress', 'Mem', '32bit', 0xcafe], + ['AddAddress', 'Mem', '32bit', 0xfeed], + ['', 'Mem', s, 0xabcd, '=', ...v], + ) + + expect($.str('a', builder)).toMatchInlineSnapshot(`"I:0xXcafe_I:0xXfeed_0xHabcd=97"`) + expect($.str('ab', builder)).toMatchInlineSnapshot(`"I:0xXcafe_I:0xXfeed_0x abcd=25185"`) + expect($.str('abc', builder)).toMatchInlineSnapshot(`"I:0xXcafe_I:0xXfeed_0xWabcd=6513249"`) + expect($.str('abcd', builder)).toMatchInlineSnapshot(`"I:0xXcafe_I:0xXfeed_0xXabcd=1684234849"`) + expect($.str('abcde', builder)).toMatchInlineSnapshot( + `"I:0xXcafe_I:0xXfeed_N:0xXabcd=1684234849_I:0xXcafe_I:0xXfeed_0xHabcd=101"`, + ) + expect($.str('abcdef', builder)).toMatchInlineSnapshot( + `"I:0xXcafe_I:0xXfeed_N:0xXabcd=1684234849_I:0xXcafe_I:0xXfeed_0x abcd=26213"`, + ) + }) +}) diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 3176c05..a73e5ae 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -222,3 +222,26 @@ export const validate = { export function indexToConditionGroupName(index: number) { return index === 0 ? 'Core' : `Alt ${index}` } + +/** + * Splits string into numeric chunks, little endian + * + * @example + * stringToNumberLE('abcde') // [ 1684234849, 101 ] + * // abcd = 0x64636261 = 1684234849 + * // e = 0x65 = 101 + */ +export function stringToNumberLE(input: string) { + const bytes = new TextEncoder().encode(input) + const values: number[] = [] + + for (let i = 0; i < bytes.length; i += 4) { + const value = [...bytes.slice(i, i + 4)] + .reverse() + .map(x => x.toString(16).padStart(2, '0')) + .join('') + values.push(parseInt(value, 16)) + } + + return values +}