Skip to content

Commit

Permalink
feat(core): add define.str and stringToNumberLE functions
Browse files Browse the repository at this point in the history
  • Loading branch information
suXinjke committed Sep 4, 2024
1 parent 3a10747 commit 1468954
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 2 deletions.
27 changes: 26 additions & 1 deletion packages/core/define.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
```
52 changes: 51 additions & 1 deletion packages/core/src/define.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Condition } from './condition.js'
import { DeepPartial } from './util.js'
import { DeepPartial, stringToNumberLE } from './util.js'

type ConditionBuilderInput = Array<boolean | Condition.Input | ConditionBuilder>

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
51 changes: 51 additions & 0 deletions packages/core/src/test/define.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"`,
)
})
})
23 changes: 23 additions & 0 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 1468954

Please sign in to comment.