Trials of Mana (SNES)/Scripting Language: Difference between revisions

From Data Crystal
Jump to navigation Jump to search
(Created page with "== Scripting Engine == Seiken Densetsu 3 includes a script engine, with scripts written in some scripting language. It's fairly full-featured. The interpreter loop and langua...")
 
 
(2 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{subpage}}
== Scripting Engine ==
== Scripting Engine ==



Latest revision as of 21:20, 28 January 2024

This is a sub-page of Trials of Mana (SNES).

Scripting Engine

Seiken Densetsu 3 includes a script engine, with scripts written in some scripting language. It's fairly full-featured. The interpreter loop and language operations are located in bank C4.

The engine makes use of at least 16 threads, with the context of each thread taking 0x100 bytes. The lower portion of the context is direct page memory, and the upper portion is stack memory. These are in LowRAM.

The scripts themselves have their own execution contexts. This memory is in bank 7F.

The following memory locations and register are used for the interpreter: (this is the LowRAM context)

  • Y : PC offset (indexed on $0A)
  • $00 : Memory location of script execution context
    • Holds both stacks, is #100 bytes in size
    • Memory operated on starts right afterward
  • $02 : Local Stack pointer
    • Push forward
    • Holds local variables/parameters to script operations
    • Values are removed as soon as they are used
  • $04 : Call Stack pointer
    • Push backward
    • Holds call stack locations and parameters to called routines
    • Values must be explicitly popped
  • $08 : Previous Call Stack frame pointer
  • $0a-0c : Script program counter
  • $12 : Current opcode (1 byte)
  • $14 : Control: End script on bit 1 set

So say $00 is ef60. Then from ef60-f05f is the script execution context memory. Starting at f060 is the game RAM it's operating on.

Scripts are made up of routines, similar to ASM.

At the beginning of a routine, the script will have opcode 04, and at the end, opcode 05 to reverse the operation. This pushes a new call stack frame. The routine will also request a certain amount of space as part of that frame push if it wants some local memory to use.

	Example:
		04 02		(new stack frame with 2 bytes of extra space)
			now $08 holds the location of the previous frame
			you can now use opcode 16 to get a value relative to this location:
		16 fe		(push $08 - 2 onto local stack)
			and use that as an address:
		14 01 00	(push 0001 onto local stack)
		e0			(write short value to short address)
			and then to read it:
		16 fe		(push $08 - 2 onto local stack)
		84			(read short from short address)
	And there, the routine's using local memory!

The advantage to using stack frames is that it doesn't matter if you balance pushes and pulls to the call stack. Once you call opcode 05, you're back to where you started.

Scripts can also call ASM routines via opcodes 40-47 or FD. These routines will often reference values on the call stack as parameters.


All possible opcodes with their ASM locations and functions:

Naming conventions:

  • A and B are operands on the local stack, with A being the first one pushed (earlier in memory)
  • J and K are the two nibbles that make up the script paramter byte (J is high, K is low)

These descriptions are summaries.

00:	25 20 Nothing
01-03:	26 20 Nothing
06:	25 20 Nothing
08-09:	25 20 Nothing
0b-0c:	25 20 Nothing
0f:	96 20 Nothing
d6-d7:	CC 1E Nothing
ec-ef:	CC 1E Nothing
fe-ff:	CC 1E Nothing

04: 27 20 ; Push new stack frame ; call Stack pointer -= param byte ; Old call stack pointer is preserved in $08, old $08 is preserved on old call stack
05: 43 20 ; Pop call stack (complement to 04) ; Restores call stack pointer and $08
07: 50 20 ; Jump absolute short/long from call stack ; If short != 0, jump short ; If short == 0, update long following (5 bytes read from stack)
0a: 72 20 ; return long
0d: 84 20 ; Remove (param byte) bytes from call stack
0e: 90 20 ; END script execution
10: 97 20 ; Push param to local stack: signed byte -> signed short
11: AC 20 ; Push param to local stack: signed byte -> signed long
12: C7 20 ; Push param to local stack: byte -> unsigned short
13: D4 20 ; Push param to local stack: byte -> unsigned long
14: E5 20 ; Push param to local stack: short -> short
15: F0 20 ; Push param to local stack: long -> long
16: 02 21 ; Push $08 + signed param byte to local stack as short (bit #50 is considered negative instead of bit #80)
17: 1A 21 ; Switch/case
18: 44 21 ; Push param as offset into memory (param byte + #100 + $00 (short))

19-1f: 57 21 ; Low 3 bits of opcode are combined with param byte for an 11-bit number ; Push param (11-bit) + #100 + $00 (short) to local stack
20-2f: 74 21 ; Low 4 bits of opcode are param signed nibble (0 becomes +8) ; Add to short on top of local stack
30-3f: 94 21 ; Low 4 bits of opcode are param signed nibble (0 becomes +8) ; Add to long on top of local stack
40-47: CA 21 ; Redirects to a routine determined by opcode and param byte (range C0-C7) ; Param byte mutated: * 4 - 1 ; Resolves Y to zero
48-4f: 03 22 ; Pushes previous script pointer (+Y) onto call stack ; Script pointer becomes dereferenced: [bank(CA + opcode nibble) (param byte * 2)] ; So there are tables at the beginning of banks D2-D9
50-5f: 4D 22 ; Jump relative (adds param to script pointer, resolves Y to zero) ; Low nibble of opcode and 1 param byte become 12-bit signed number
60-6f: 83 22 ; Conditional jump relative: If short on stack is truthy, do the same as 50-5F
70-7f: C1 22 ; Conditional jump relative: If short on stack is false, do the same as 50-5F

Take specified bits
	Param byte: JK (K: skipBits, J: takeBits)
	Skip K bits on the right, and take J on the right after that.
	Operand and result on local stack.
80: 74 06 ; Signed short
81: D1 06 ; Signed long
82: 54 07 ; Short
83: A0 07 ; Long

84: 05 08 ; Dereference short to short
85: 16 08 ; Dereference long to short
86: 2D 08 ; Dereference short to long
87: 49 08 ; Dereference long to long
88: 6E 08 ; Dereference short to signed byte (writes 2)
89: 8A 08 ; Dereference long to signed byte (writes 2)
8a: AC 08 ; Dereference short to unsigned byte (writes 2)
8b: C0 08 ; Dereference long to unsigned byte (writes 2)
8c: DA 08 ; Dereference short to signed byte (writes 3)
8d: FE 08 ; Dereference long to signed byte (writes 3)
8e: 28 09 ; Dereference short to unsigned byte (writes 3)
8f: 40 09 ; Dereference long to unsigned byte (writes 3)

Compares: result written to local stack as 0000 or 0001
90: 5E 09 ; Signed shorts: A < B
91: 8F 09 ; Signed longs: A < B
92: D2 09 ; Unsigned shorts: A < B
93: F2 09 ; Unsigned longs: A < B
94: 24 0A ; Signed shorts: A > B
95: 55 0A ; Signed longs: A > B
96: 98 0A ; Unsigned shorts: A > B
97: B4 0A ; Unsigned longs: A > B
98: E6 0A ; Signed shorts: A <= B
99: 1E 0B ; Signed longs: A <= B
9a: 6F 0B ; Unsigned shorts: A <= B
9b: 91 0B ; Unsigned longs: A <= B
9c: D1 0B ; Signed shorts: A >= B
9d: 09 0C ; Signed longs: A >= B
9e: 5A 0C ; Unsigned shorts: A >= B
9f: 78 0C ; Unsigned longs: A >= B

Math:
a0: B8 0C ; Signed shorts: A * B (SMR)
a1: 01 0D ; Signed longs: A * B (SMR)
a2: 71 0D ; Unsigned shorts: A * B
a3: A0 0D ; Unsigned longs: A * B
a4: F6 0D ; Signed shorts: A / B (2's C)
a5: 6F 0E ; Signed longs: A / B (2's C)
a6: 59 0F ; Unsigned shorts: A / B
a7: 98 0F ; Unsigned longs: A / B
a8: 15 10 ; Signed shorts: A << B
a9: 4B 10 ; Signed longs: A << B
aa: 97 10 ; Unsigned shorts: A << B
ab: B1 10 ; Unsigned longs: A << B
ac: DC 10 ; Signed shorts: A >> B
ad: 0B 11 ; Signed longs: A >> B
ae: 4F 11 ; Unsigned shorts: A >> B
af: 69 11 ; Unsigned longs: A >> B
b0: 94 11 ; Signed shorts: A / B (2's C) Remainder
b1: 1C 12 ; Signed longs: A / B (2's C) Remainder
b2: 1A 13 ; Unsigned shorts: A / B Remainder
b3: 5E 13 ; Unsigned longs: A / B Remainder

b4: E5 13 ; Expand signed byte to signed short
b5,b7: FF 13 ; Go back 1 (local stack)
b6: 02 14 ; Push 00 (byte) to local stack

b8: 0F 14 ; A: short address, B: signed short value (SMR) ; Store low byte (signed) of B at A, put B back on stack
b9: 35 14 ; A: short address, B: signed long value (SMR) ; Store low byte (signed) of B at A, put B back on stack
ba: 66 14 ; A: short address, B: unsigned short value ; Store low byte of B at A, put B back on stack
bb: 84 14 ; Identical to BE (long address, unsigned short) (bug)
bc: A8 14 ; A: long address, B: signed short value (SMR) ; Store low byte (signed) of B at A, put B back on stack
bd: D4 14 ; A: long address, B: signed long value (SMR) ; Store low byte (signed) of B at A, put B back on stack
be: 0B 15 ; A: long address, B: unsigned short value ; Store low byte of B at A, put B back on stack
bf: 2F 15 ; A: long address, B: unsigned long value ; Store low byte of B at A, put B back on stack

c0: 5F 15 ; Shorts: A == B
c1: 7B 15 ; Longs: A == B
c2: AB 15 ; Shorts: A != B
c3: C7 15 ; Longs: A != B
c4: F7 15 ; Shorts: A != 0 || B == 0
c5: 15 16 ; Longs: (A != 0 || ) B == 0 (bugged)
c6: 41 16 ; Shorts: A != 0 || B != 0
c7: 5D 16 ; Longs: A != 0 || B != 0
c8: 89 16 ; Shorts: A + B
c9: 9B 16 ; Longs: A + B
ca: C0 16 ; Shorts: A - B
cb: D6 16 ; Longs: A - B
cc: FF 16 ; Move Short local stack -> 2
cd: 0C 17 ; Move Long local stack -> 2
ce: 21 17 ; Move Short call stack -> 1
cf: 2E 17 ; Move Long call stack -> 1

d0: 43 17 ; Shorts: A & B
d1: 54 17 ; Longs: A & B
d2: 78 17 ; Shorts: A | B
d3: 89 17 ; Longs: A | B
d4: AD 17 ; Shorts: A ^ B
d5: BE 17 ; Longs: A ^ B
d8: E6 17 ; Short: -A (1's C)
d9: F4 17 ; Long: -A (1's C)
da: 0F 18 ; Short: -A (2's C)
db: 1E 18 ; Long: -A (2's C)
dc: 49 18 ; Short: A != 0
dd: 5F 18 ; Long: A != 0
de: 7B 18 ; Write #7F to local stack
df: 86 18 ; Long: A != 0 (different implementation)

e0: A2 18 ; A (short address), B (short value) ; Writes B to A
e1: B9 18 ; A (long address), B (short value) ; Writes B to A
e2: D2 18 ; A (short address), B (long value) ; Writes B to A
e3: F3 18 ; A (long address), B (long value) ; Writes B to A
e4: 1A 19 ; A (short address), B (short value) ; Writes low byte of B to A
e5: 31 19 ; A (long address), B (short value) ; Writes low byte of B to A
e6: 4E 19 ; A (short address), B (long value) ; Writes low byte of B to A
e7: 67 19 ; A (long address), B (long value) ; Writes low byte of B to A
e8: 86 19 ; A (short address), B (short value) ; Write B to A, put B back on stack
e9: 9F 19 ; A (long address), B (short value) ; Write B to A, put B back on stack
ea: BE 19 ; A (short address), B (long value) ; Write B to A, put B back on stack
eb: EA 19 ; A (long address), B (long value) ; Write B to A, put B back on stack

; Partial-byte writing. A is address, B is value, param byte: JK (J 0 becomes 16)
; Takes the low J bits of B, then the low K bits of value at A ; Writes the result to A (auto byte/short depending on size
; All the long-value functions seem to have a bug where the low byte of B is never read.
f0: 1C 1A ; short address, short value
f1: 84 1A ; short address, long value
f2: F7 1A ; long address, short value
f3: 70 1B ; long address, long value
f4: F2 1B ; short address, short value ; Pushes B back on stack
f5: 62 1C ; short address, long value ; Pushes B back on stack
f6: E3 1C ; long address, short value ; Pushes B back on stack
f7: 64 1D ; long address, long value ; Pushes B back on stack

f8: F4 1D ; Pop 2 (local stack)
f9: F9 1D ; Pop 3 (local stack)
fa: 00 1E ; Repeat last 2 bytes (local stack)
fb: 0F 1E ; Repeat last 3 bytes (local stack)
fc: 27 1E ; JSR absolute (short address on local stack) (pushes current short location onto call stack)
fd: 3D 1E ; JSL absolute (long address on local stack) (pushes current long location onto call stack)
	; If bank of address is 00, instead redirects to a routine at CX:YYYY where X is low nibble of middle byte of address, and YYYY is the low byte of address * 4
	; Range: 0003-03FB in banks C0-CF
	; (This is how the top half of op 40-47 is done)