Skip to content

Instantly share code, notes, and snippets.

@CherryDT
Last active August 17, 2022 19:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CherryDT/760c6431bd47d63fb969c05000955f5c to your computer and use it in GitHub Desktop.
Save CherryDT/760c6431bd47d63fb969c05000955f5c to your computer and use it in GitHub Desktop.
Text-drawing hook for DynRPG

Text-drawing hook for DynRPG

by David "Cherry" Trapp

With the following code, we can add a hook that is called whenever text is drawn to the screen. We can then augment or replace the things that are drawn.

Important: This will only work for a single plugin in the game. If another plugin tries to use the same code below, only the last plugin that gets initialized will actually get its hook called.

Prerequisites

Add the following code to your onStartup hook:

// Set up hook
void *trampoline;
asm volatile("movl $_drawTextHookTrampoline, %%eax" : "=a" (trampoline));
*reinterpret_cast<unsigned char *>(0x4893A0) = 0xE9;
*reinterpret_cast<int *>(0x4893A1) = (int)trampoline - (0x4893A1 + 4);

Add the following code somewhere below your onStartup hook:

void clearCanvas(RPG::Canvas *canvas) {
    asm volatile("call *%%esi" : "=a" (RPG::_eax) : "S" (0x468180), "a" (canvas) : "edx", "ecx", "cc", "memory");
}

// Some more stuff for the hook
bool drawTextHook(RPG::Image* target, const char* text, int* textIndex, int* x, int* y, int color, RPG::DStringPtr* tempString, RPG::Canvas* tempCanvas);
extern "C" {
    unsigned int __stdcall drawTextHookTrampoline2(RPG::Image* target, const char* text, int* textIndex, int* x, int* y, int color, RPG::DStringPtr* tempString, RPG::Canvas* tempCanvas) {
        clearCanvas(tempCanvas);
        if (drawTextHook(target, text, textIndex, x, y, color, tempString, tempCanvas)) {
            return 0x4893A5;
        } else {
            return 0x48965F;
        }
    }
}
asm volatile("_drawTextHookTrampoline:;"
             "pushl %eax;" // tempCanvas
             "leal -0x1C(%ebp), %eax;" // tempString
             "pushl %eax;"
             "pushl 0x8(%ebp);" // color
             "leal 0x10(%ebp), %eax;" // y
             "pushl %eax;"
             "leal -0xC(%ebp), %eax;" // x
             "pushl %eax;"
             "leal -0x18(%ebp), %eax;" // textIndex
             "pushl %eax;"
             "pushl 0xC(%ebp);" // text
             "pushl -0x8(%ebp);" // target
             "movl $_drawTextHookTrampoline2@32, %eax;"
             "call *%eax;"
             "jmp *%eax");

The hook

You can then add a function as follows:

bool drawTextHook(RPG::Image* target, const char* text, int* textIndex, int* x, int* y, int color, RPG::DStringPtr* tempString, RPG::Canvas* tempCanvas) {
  // ...
}

Arguments:

Name Type Description
target RPG::Image* The target on image which the text is to be drawn. This image will have the same palette as RPG::system->systemGraphic->systemImage. You may draw to it.
text const char* The text a part of which is being drawn.
textIndex int* Pointer to the index of the current character in text. You can update this value, but see notes below.
x int* Pointer to the current X coordinate in target. You can update this value, but see notes below.
y int* Pointer to the current Y coordinate in target. You can update this value, but see notes below.
color int System color index (0-19) in which the text should be drawn. Can not be updated.
tempString RPG::DStringPtr* Pointer to the current character as RPG::DStringPtr. See notes for special usage of this value.
tempBoard RPG::Canvas* A temporary 12x12 canvas on which pixels can be drawn (in any nonzero color) which will later be applied to target with the correct system colors and shadow automatically.

Return value:

If this hook returns true, the character in tempString will be processed normally (including the next one if it was $). Otherwise, no further action occurs and the drawing loop repeats. In the latter case, you have to take care of updating *x and - crucially, to avoid an infinite loop - *textIndex yourself.

Operation flow

Whenever some text needs to be drawn, the game calls a text-drawing function with a target image (that has the same palette as the system graphic has), the string, X and Y coordinates and a system color index.

The text-drawing function will then loop though each character in the string, filling it into tempString. If it is a half-space (which is stored as ASCII character 0x01 in the string), then the X position is advanced by 3 pixels and that's it (note: in that case the hook is not invoked). Otherwise, the hook gets called.

If the hook returns false, the loop simply continues from the top. Otherwise, if it returns true, the following things happen:

The text in tempString will be drawn to tempCanvas (a 12x12 canvas) with the system font, or, in case tempString was $, an EXFONT icon is drawn instead, the next character is skipped and tempString is set to two spaces (unless the next character is also $).

Then, the contents of tempCanvas interpreted as 1-bit (zero = transparent, everything else = opaque) are used as mask for the color box in the system graphic as well as the shadow color and the results are drawn to the target image.

Important note: Texts in menus will be drawn all at once, so text will have the whole text that's drawn. But in message boxes, only 1 or 2 characters are drawn at a time, so text will never be the full message.

Ways to use the hook

Draw custom 1-bit sprite in system color and with shadow:

To draw some custom glyph instead of a regular character (can be 6 or 12 pixels wide), you can draw to tempBoard with any nonzero color. Then set *tempString to ' ' (single space) for a width of 6 pixels or ' ' (two spaces - I don't know why GitHub is swallowing them! Trust me, you need two.) for a width of 12 pixels and return true. If you want to draw something that is not exactly 6 or 12 pixels wide then you can leave some empty space to the left of the sprite in the canvas and subtract the difference to 6 or 12 pixels from *x. Then return true.

Draw custom multicolor sprite with any dimensions:

To draw some custom sprite with multiple colors (as long as they exist in the system graphic's palette) and any dimensions (as long as they fit into the target image, i.e. usually the window's/menu's content size), you can draw directly to target at *x and *y. You then need to advance *x by whatever width is needed and also advance *textIndex by at least 1 (you can add more if you want to skip further characters). Then return false.

Note that even with messages (where only 1-2 characters are drawn at a time) the character following a $ is always guaranteed to be in the same drawing operation so that EXFONT can work. This will not be the case for other combinations of characters. So, to trigger custom effects in messages, it's recommended to use something like $1, $!, etc.

Trigger something (e.g. sound, switch, etc.) when a certain text is drawn (or certain point in a message is reached):

You can simply observe when the text you are waiting for gets drawn and react on it. Then always return true.

Repurpose colors as fonts:

You could react on color being a certain value and then - if *tempString is not $ - draw the character in *tempString into tempCanvas yourself with the desired font and set *tempString to ' '. Then return true.

...of course there are many other creative uses...!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment