THUMBLIB2

Happy fangame release day, everybody.

Have you ever wanted to incorporate assembly into your EA buildfile? lyn mode not your thing? I’ve got the thing for you!

THUMBLIB2!

With it, you can write THUMB assembly seamlessly within an event file. No file inclusions, external tool calls, or mess of SHORTs.

Here’s a quick breakdown of the pros and cons:

Pros:

  1. Doesn’t require any external tools
  2. No intermediate steps
  3. Now fully cooked
  4. Fewer files to clutter your folders

Cons:

  1. Requires learning new syntax and aliases
  2. Not fully tested
  3. Fewer diagnostics compared to an actual assembler*
  4. Some assembly required

Included within the GitHub repository is an example file (a THUMBLIB2'd version of @StanH’s nice.event). Using THUMBLIB2 is as easy as #includeing it. Each opcode has a comment above it detailing its original assembler syntax. You’ll notice that some opcodes have different names to avoid conflicts.

*

THUMBLIB2 has a lot of error checking compared to most macro libraries, but some opcodes weren’t playing nice with EA. Most notably, branch- and label-based instructions lack any kind of bounds checking. I don’t actually recommend prototyping your ASM with this (or even using it at all lol).

I’m not liable for any bugs/screeches/build time slowdown that may occur when using THUMBLIB2. Ask your doctor if THUMBLIB2 is right for you.

16 Likes

Hopefully fixed a bug where ldr wasn’t referring to the right place when the opcode wasn’t 4-aligned. Redownload if you need to.

2 Likes

cam can we make mashing the like button give zane more likes because I need to give zane more likes

1 Like

I figured that it’d probably be good to show off some examples in this thread so that people don’t think this is purely an April Fools joke.

Here’s an updated version of the ubiquitous anti-huffman hack that comes with FEditor and the essential fixes EA installer. I’ll split it up a bit to comment on parts and post the whole text at the end, alright?

#include "THUMBLIB2.event"

// Temporary helpers

#define t_push_lr "t_push(False, False, False, False, False, False, False, False, True)"
#define t_pop_pc  "t_pop(False, False, False, False, False, False, False, False, True)"

One of the big flaws with THUMBLIB2 is that there isn’t a good way to implement register lists, so opcodes like push and pop are big chunks of true/false. I don’t want to have to copy/paste this every time, so I just use these two macros.

#ifndef GetStringFromIndex
  #define GetStringFromIndex (0x0800A280 | 1)
#endif

#ifndef bx_r2
  #define bx_r2 (0x080D18C8 | 1)
#endif

#ifndef TextBuffer
  #define TextBuffer 0x0202A6AC
#endif

#ifndef ARM_HuffmanTextDecomp
  #define ARM_HuffmanTextDecomp 0x03003C0C
#endif

Next, some vanilla routine and data offsets that’ll be referenced. There are probably better ways to do this, but I’m out of practice. Notice the THUMB bit on the first two and the lack of it on the ARM routine.

PUSH

  ORG 0x00002BA4

  AntihuffmanPointerTester:
  {
    /* push {lr}        */ t_push_lr
    /* lsr  r2, r0, #31 */ t_lsr(r2, r0, 31)
    /* beq  _AHCompr... */ t_beq(_AHCompressed)

    /* bl   Antihuff... */ t_bl(AntihuffmanUncompressed)
    /* b    _AHReturn   */ t_b(_AHReturn)

    _AHCompressed:
    /* bl   Antihuff... */ t_bl(AntihuffmanCompressed)

    _AHReturn:
    /* pop  {pc}        */ t_pop_pc
  }

This is 1-to-1 from the normal AH code. I like to use the braces for scope shenanigans. I’ve included the normal assembly formatting as comments.

I would have liked both of these bls to instead be straight up branches and not subroutine calls, because there’s nothing to be done after calling them, but this routine is exactly small enough to fit here and using bxs and a literal pool is too big.

If you’ve never seen how the anti-huffman patch works: pointers in the text table to text that is not compressed are flagged by having their uppermost bit set. This check shifts the pointer right until just the upper bit remains and then uses that to pick a routine. We’ll get to the actual routines in a moment.

  ORG 0x0000A240

  GetStringFromIndexExt:
  {
    /* ldr  r1, offs_TextBuffer */ t_ldr(r1, offs_TextBuffer)
    /* b    GetStringFromIndex  */ t_b(GetStringFromIndex)

    ALIGN 4

    offs_TextBuffer: WORD TextBuffer
  }

The original AH hack actually starts editing this routine midway through, attempting to preserve some vanilla functionality that would save time if the requested string was already in the buffer. The problem is that the AH hack also destroys the original function’s literal pool, causing it to load garbage and making the attempt useless. I just gutted the vanilla stuff and the AH’s attempt at preserving it, which gives me plenty of extra space to fit the rest inline.

Also notice that the call to GetStringFromIndex is a b and not a bl. GetStringFromIndex pushes lr and returns using it, so I don’t have to. It’s also really close by, so I can use b instead of bx. This little tail call optimization ends up saving me another instruction, which is nice.

  AntihuffmanCompressed:
  {
    /* push {lr}        */ t_push_lr
    /* ldr  r2, offs... */ t_ldr(r2, offs_ARM_HuffmanTextDecomp)
    /* bl   bx_r2       */ t_bl(bx_r2)
    /* pop  {pc}        */ t_pop_pc

    ALIGN 4

    offs_ARM_HuffmanTextDecomp: WORD ARM_HuffmanTextDecomp
  }

Small optimization here. The original AH grabs a pointer to the ARM routine indirectly, using another ldr to fetch it from RAM. As far as I know, this pointer in RAM isn’t ever changed after booting the game, so I just call the function directly.

  AntihuffmanUncompressed:
  {
    /* mov  r3, #0x80   */ t_mvi(r3, 0x80)
    /* lsl  r3, r3, #24 */ t_lsl(r3, r3, 24)
    /* sub  r0, r0, r3  */ t_sbr(r0, r0, r3)

    _AHLoop:
    /* ldrb r2, [r0]    */ t_ldbi(r2, r0, 0)
    /* strb r2, [r1]    */ t_stbi(r2, r1, 0)
    /* add  r1, #1      */ t_adi(r1, 1)
    /* add  r0, #1      */ t_adi(r0, 1)
    /* cmp  r2, #0      */ t_cpi(r2, 0)
    /* bne  _AHLoop     */ t_bne(_AHLoop)

    /* bx   lr          */ t_bx(lr)
  }

The original AH hack couldn’t fit this inline and had to put it in freespace, jumping to it with a bx. I saved enough space to fit it, so the bx part is missing.

    ASSERT (0x0000A280 - CURRENTOFFSET)

POP

#undef t_push_lr
#undef t_pop_pc

I use ASSERT here just to make sure all of this fits inline. It’ll throw an error if the current offset is larger than 0xA280. I also undefine the macros I made just for a bit of cleanliness, although it really isn’t necessary.

The whole thing

#include "THUMBLIB2.event"

// Temporary helpers

#define t_push_lr "t_push(False, False, False, False, False, False, False, False, True)"
#define t_pop_pc  "t_pop(False, False, False, False, False, False, False, False, True)"

#ifndef GetStringFromIndex
  #define GetStringFromIndex (0x0800A280 | 1)
#endif

#ifndef bx_r2
  #define bx_r2 (0x080D18C8 | 1)
#endif

#ifndef TextBuffer
  #define TextBuffer 0x0202A6AC
#endif

#ifndef ARM_HuffmanTextDecomp
  #define ARM_HuffmanTextDecomp 0x03003C0C
#endif

PUSH

  ORG 0x00002BA4

  AntihuffmanPointerTester:
  {
    t_push_lr
    t_lsr(r2, r0, 31)
    t_beq(_AHCompressed)

      t_bl(AntihuffmanUncompressed)
      t_b(_AHReturn)

    _AHCompressed:
      t_bl(AntihuffmanCompressed)

    _AHReturn:
      t_pop_pc
  }

  ORG 0x0000A240

  GetStringFromIndexExt:
  {
    t_ldr(r1, offs_TextBuffer)
    t_b(GetStringFromIndex)

    ALIGN 4

    offs_TextBuffer: WORD TextBuffer
  }

  AntihuffmanCompressed:
  {
    t_push_lr
    t_ldr(r2, offs_ARM_HuffmanTextDecomp)
    t_bl(bx_r2)
    t_pop_pc

    ALIGN 4

    offs_ARM_HuffmanTextDecomp: WORD ARM_HuffmanTextDecomp
  }

  AntihuffmanUncompressed:
  {
    t_mvi(r3, 0x80)
    t_lsl(r3, r3, 24)
    t_sbr(r0, r0, r3)

    _AHLoop:
      t_ldbi(r2, r0, 0)
      t_stbi(r2, r1, 0)
      t_adi(r1, 1)
      t_adi(r0, 1)
      t_cpi(r2, 0)
      t_bne(_AHLoop)

    t_bx(lr)
  }

    ASSERT (0x0000A280 - CURRENTOFFSET)

POP

#undef t_push_lr
#undef t_pop_pc

3 Likes