Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion src/__tests__/emit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ describe('emit: direct LIR emission', () => {
expect(main).toContain('execute if score $a __emit matches 1..5 run function emitns:matches')
expect(main).toContain('execute unless score $a __emit matches 0 run function emitns:unless_matches')
expect(main).toContain('execute if score $a __emit = $b __emit run function emitns:if_score')
expect(main).toContain('execute unless score $a __emit = $b __emit run function emitns:unless_score')
// call_unless_score with ne: unless(a != b) == if(a == b), so sense flips
expect(main).toContain('execute if score $a __emit = $b __emit run function emitns:unless_score')
expect(main).toContain('execute as @a at @e[type=marker] at @s positioned ~1 ~2 ~3 rotated ~ ~10 in minecraft:the_nether anchored eyes if score $a __emit < $b __emit unless score $a __emit >= $b __emit if score $a __emit matches 1.. unless score $b __emit matches ..0 run function emitns:ctx')
expect(main).toContain('scoreboard players operation $ret __emit = $a __emit')
expect(main).toContain('$tp @s $(x) $(y) $(z)')
Expand Down Expand Up @@ -246,4 +247,130 @@ describe('emit: direct LIR emission', () => {
expect(neOut).not.toBe(eqOut)
})
})

test('call_if_score with ne emits unless score, not if score =', () => {
const module: LIRModule = {
namespace: 'ns',
objective: '__obj',
functions: [
{
name: 'Test',
isMacro: false,
macroParams: [],
instructions: [
{
kind: 'call_if_score',
fn: 'ns:ne_branch',
a: { player: '$x', obj: '__obj' },
op: 'ne',
b: { player: '$y', obj: '__obj' },
},
],
},
],
}

const files = emit(module, { namespace: 'ns', mcVersion: McVersion.v1_21 })
const content = getFile(files, 'data/ns/function/test.mcfunction')

expect(content).toContain('execute unless score $x __obj = $y __obj run function ns:ne_branch')
expect(content).not.toContain('if score $x __obj = $y __obj')
})

test('call_unless_score with ne emits if score (double negation cancels out)', () => {
const module: LIRModule = {
namespace: 'ns',
objective: '__obj',
functions: [
{
name: 'Test',
isMacro: false,
macroParams: [],
instructions: [
{
kind: 'call_unless_score',
fn: 'ns:ne_unless_branch',
a: { player: '$x', obj: '__obj' },
op: 'ne',
b: { player: '$y', obj: '__obj' },
},
],
},
],
}

const files = emit(module, { namespace: 'ns', mcVersion: McVersion.v1_21 })
const content = getFile(files, 'data/ns/function/test.mcfunction')

// unless (a != b) == if (a == b)
expect(content).toContain('execute if score $x __obj = $y __obj run function ns:ne_unless_branch')
expect(content).not.toContain('unless score $x __obj = $y __obj run function ns:ne_unless_branch')
})

test('call_if_score with non-ne operators emits if score without flipping', () => {
const ops = [
{ op: 'eq', mc: '=' },
{ op: 'lt', mc: '<' },
{ op: 'le', mc: '<=' },
{ op: 'gt', mc: '>' },
{ op: 'ge', mc: '>=' },
] as const

for (const { op, mc } of ops) {
const module: LIRModule = {
namespace: 'ns',
objective: '__obj',
functions: [
{
name: 'Test',
isMacro: false,
macroParams: [],
instructions: [
{
kind: 'call_if_score',
fn: `ns:fn_${op}`,
a: { player: '$x', obj: '__obj' },
op,
b: { player: '$y', obj: '__obj' },
},
],
},
],
}

const files = emit(module, { namespace: 'ns', mcVersion: McVersion.v1_21 })
const content = getFile(files, 'data/ns/function/test.mcfunction')

expect(content).toContain(`execute if score $x __obj ${mc} $y __obj run function ns:fn_${op}`)
}
})

test('if_score subcmd with ne emits unless score in execute chain', () => {
const module: LIRModule = {
namespace: 'ns',
objective: '__obj',
functions: [
{
name: 'Test',
isMacro: false,
macroParams: [],
instructions: [
{
kind: 'call_context',
fn: 'ns:ctx_ne',
subcommands: [
{ kind: 'if_score', a: '$x __obj', op: 'ne', b: '$y __obj' },
],
},
],
},
],
}

const files = emit(module, { namespace: 'ns', mcVersion: McVersion.v1_21 })
const content = getFile(files, 'data/ns/function/test.mcfunction')

expect(content).toContain('execute unless score $x __obj = $y __obj run function ns:ctx_ne')
expect(content).not.toContain('if score $x __obj = $y __obj run function ns:ctx_ne')
})
})
33 changes: 24 additions & 9 deletions src/emit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,14 +841,10 @@ function emitInstr(instr: LIRInstr, ns: string, obj: string, mcVersion: McVersio
return `execute unless score ${slot(instr.slot)} matches ${instr.range} run function ${instr.fn}`

case 'call_if_score':
// 'ne' has no direct MC operator; invert to `unless ... =` for correct semantics.
if (instr.op === 'ne') {
return `execute unless score ${slot(instr.a)} = ${slot(instr.b)} run function ${instr.fn}`
}
return `execute if score ${slot(instr.a)} ${cmpToMC(instr.op)} ${slot(instr.b)} run function ${instr.fn}`
return `execute ${scoreCondition('if', instr.op, slot(instr.a), slot(instr.b))} run function ${instr.fn}`

case 'call_unless_score':
return `execute unless score ${slot(instr.a)} ${cmpToMC(instr.op)} ${slot(instr.b)} run function ${instr.fn}`
return `execute ${scoreCondition('unless', instr.op, slot(instr.a), slot(instr.b))} run function ${instr.fn}`

case 'call_context': {
const subcmds = instr.subcommands.map(emitSubcmd).join(' ')
Expand Down Expand Up @@ -900,14 +896,33 @@ function slot(s: Slot): string {
function cmpToMC(op: CmpOp): string {
switch (op) {
case 'eq': return '='
case 'ne': return '=' // 'ne' maps to '=' — callers must wrap in `unless` (MC has no != operator)
case 'ne': return '=' // ne is expressed via "unless score ... =" rather than a distinct operator
case 'lt': return '<'
case 'le': return '<='
case 'gt': return '>'
case 'ge': return '>='
}
}

/**
* Emit a score condition fragment, e.g. "if score $a obj = $b obj".
*
* MC has no "!=" operator; not-equal is expressed by flipping the if/unless
* keyword and using "=" as the operator:
* a != b → unless score a = b (not: if score a = b)
* a == b → if score a = b
*
* The `sense` parameter is the caller's intended polarity ('if' or 'unless').
* For 'ne', both the sense and operator are adjusted automatically.
*/
function scoreCondition(sense: 'if' | 'unless', op: CmpOp, a: string, b: string): string {
if (op === 'ne') {
const flipped = sense === 'if' ? 'unless' : 'if'
return `${flipped} score ${a} = ${b}`
}
return `${sense} score ${a} ${cmpToMC(op)} ${b}`
}

/**
* Pre-1.20.2 compat: emit a macro template as a plain command.
* $(param) placeholders are replaced with `storage rs:macro_args <param>` data-get
Expand Down Expand Up @@ -995,9 +1010,9 @@ function emitSubcmd(sub: ExecuteSubcmd): string {
case 'anchored':
return `anchored ${sub.anchor}`
case 'if_score':
return `if score ${sub.a} ${cmpToMC(sub.op)} ${sub.b}`
return scoreCondition('if', sub.op, sub.a, sub.b)
case 'unless_score':
return `unless score ${sub.a} ${cmpToMC(sub.op)} ${sub.b}`
return scoreCondition('unless', sub.op, sub.a, sub.b)
case 'if_matches':
return `if score ${sub.score} matches ${sub.range}`
case 'unless_matches':
Expand Down
Loading