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.
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");
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.
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.
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...!