The current URL is datacrystal.tcrf.net.
Trials of Mana (SNES)/Scripting Language
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)