thcrap
thcrap copied to clipboard
[AoCF] Move and resize balloons
Find a way to move and resize balloons, center the text correctly in them, create bigger ones etc.
It's way harder than it seems to be, it will take a long time to be fixed.
Someone found a Squirrel decompiler and put a decompiled version of every nut file on Github (https://github.com/MathyFurret/th155-decomp), so we can start working on this issue! I'll start by trying to look back at what we know today, what we did in the past, what we might want to keep, what we might want to throw.
First, the problem: the game puts the text balloons at a nice place, and centers the text in them automatically. It works for vertical texts, but we want horizontal texts. It have never been tested with horizontal texts, and the various values used were chosen for vertical texts.
The most important file is this one: https://github.com/MathyFurret/th155-decomp/blob/v1.10d/src/data/script/talk/talk_balloon.nut . The code in it is in charge of choosing the correct balloon, placing it, and placing the text inside it.
First, we have local data = ::actor.LoadAnimationData("data/event/balloon/balloon.pat");. balloon.pat is a json file that describes all the balloons. These balloons have names like a05x2 or c15x3. a, b, c or d is related to the tone of voice: talking normally, thinking etc. Here, we won't change it. Then the 2 digits (05 or 15 here) are the balloon height. x is a separator, and the last digit (2 or 3 here) is the balloon width, in number of lines (remember, vertical text).
There may be 2 names for the same balloon image. For example, both a05x1 and a05x2 use the a05x2.png image.
balloon.pat is a JSON file with a list of balloons. One example of balloon is:
{
"frame":[
{
"point":[
{
"x":91,
"y":34
}
]
}
],
"id":0,
"is_child":false,
"layer":[
{
"blend":0,
"element":[
{
"argb":4294967295,
"cx":5,
"cy":77,
"height":0,
"left":0,
"name":"a05x2.png",
"rx":0,
"ry":0,
"rz":0,
"sx":100,
"sy":100,
"top":0,
"width":0
}
],
"filter":1,
"flags":0,
"shader":0,
"type":0
}
],
"name":"a05x1"
},
A lot of these values are unused, or are always the same. If I write only the useful values, we have:
{
"frame":[
{
"point":[
{
"x":91,
"y":34
}
]
}
],
"layer":[
{
"element":[
{
"cx":5,
"cy":77,
"name":"a05x2.png"
}
]
}
],
"name":"a05x1"
},
This list is loaded in the foreach block at the top of talk_balloon.nut. Here is a copy, annotated with changes we might want to make in thcrap:
foreach( key, v in data )
{
local t = {};
t.point <- v.point[0];
local texture = ::manbow.Texture();
texture.Load(v.texture_name);
// For example, key=a15x3
// a, b, c, or d ('a' here, from a15x3)
t.type <- key.slice(0, 1);
// 15 here - height for vertical balloons.
// TODO swap with width? (/Literals/15 or /Instructions/62)
t.height <- key.slice(1, 3).tointeger();
// 3 here - width for vertical balloons.
// TODO swap with height? (/Literals/17 or /Instructions/71)
t.width <- key.slice(4, 5).tointeger();
// These 2 should probably be swapped as well (it might be easier to swap the "width" and "height" variable names)
t.width = t.width * ::talk.font.size + (t.width - 1) * 16;
t.height = t.height * (::talk.font.size + 1);
t.texture <- texture;
// We could swap these 2, but I think we would better edit them all manually in balloon.pat
t.cx <- v.offset_x;
t.cy <- v.offset_y;
_balloon_src[v.name] <- t;
_balloon_src_auto[t.type].append(t);
}
Next, we have a compare function, used to sort the balloons fr automatic balloon selection later. They are sorted by width, then by height (which makes sense - remember, vertical text). If we switch width/height in the initialization above, we probably need to switch them here as well. And... I'm quite sure the function is broken. Annotated copy below.
local compare = function ( a, b )
{
// TODO: switch width (/Functions/0/Literals/0) and height (/Functions/0/Literals/1)
if (a.width < b.width)
{
return -1;
}
if (a.width > b.width)
{
return 1;
}
if (a.height < b.height)
{
// TODO: return -1
// /Functions/0/Instructions/16 = loadint(3, -1)
return 1;
}
if (a.height > b.height)
{
// TODO: return 1
// /Functions/0/Instructions/22 = loadint(3, 1)
return -1;
}
return 0;
};
Next, we declare the Balloon class and its function. Nothing interesting in the members declaration and default values.
We have one really important function: Create. It initializes the internal variables, which include choosing a balloon, placing it, and placing the text inside it. Basically everything we care about. Before digging into it, we'll take a quick look at the other functions.
We have a Perse function (typo for "parse", I guess). It takes the balloon's text and do some initialization with it. One of the things in there is really important to us:
this.text.SetVertical(true);
We must change this value to false. This is what controls whether the text will be displayed vertically or horizontally. Today, we do that with "/Functions/2/Instructions/36/2": "0", and we will keep that.
None of the other functions matter. We can go back to Create.
There are 2 important blocks in there. The 1st one is lines 80 to 98, which chooses which balloon will be used, depending on the text width and height. Here is the version used today (basically, we switched text.width and text.height - these 2 changed because the text is now horizontal, but we didn't change v.width and v.height):
foreach( v in src_array )
{
if (v.width < this.text.height)
continue;
if (v.width > this.text.height && src)
break;
if (v.height < this.text.width)
continue;
src = v;
}
We might need to switch every occurrence of "width" and "height" here, because we want our balloons list to be sorted by height, then by width (while the original is sorted by width, then by height). On the other hand, I can't really grasp the logic around this, it seems broken, so some part of me also want to rewrite completely. Like, in the original code, when we execute the src = v, we know that this.text.width >= v.width && this.text.height >= v.height, which means our text fits into the balloon, so... why not just exit there?
A small note on line 111: direction is either -1 or 1, depending on who is talking.
And then, the other important bit of code: lines 119 to 122.
this.sprite.x = -src.cx;
this.sprite.y = -src.cy;
this.text.x = src.point.x * this.direction + this.text.width / 2.00000000;
this.text.y = src.point.y - this.text.height / 2.00000000;
I need to do some more testing on them, but sprite.x / sprite.y are definitely related to the position of the balloon, and text.x / text.y is the position of the text inside the balloon. Everything we do wrt text centering will be in the 3rd line. In the current thcrap implementation, I switched cx and cy, which rather makes sense. On the other hand, I don't know why I did that for text.x and text.y, and I'm quite amazed that it works as well as it does today:
local direction = this.direction;
if (direction == 1)
direction = -0.3;
this.text.x = this.text.width / 1.5 * direction
this.text.y = src.point.x - this.text.height / 2.00000000;
In this ticket, I don't think I want to keep the cx/cy switch, I'd rather switch all the values in balloon.pat. And I definitely don't want to keep this for text.x and text.y.
First test with:
- this.owner.balloon_x = 640
- this.owner.balloon_y = 0
- this.sprite.x = 0
- this.sprite.y = 0
- this.text.x = 0
- this.text.y = 0 I first tried with this.owner.balloon_x = 0, but 640 (the middle of the screen) is way better to understand how these coordinates work. Relevant code:
// text.x = 0; text.y = 0;
"/Functions/1/Instructions/114": "loadint(7,0)",
"/Functions/1/Instructions/125": "loadint(7,0)",
// sprite.x = 0; sprite.y = 0;
"/Functions/1/Instructions/95": "loadint(7,0)",
"/Functions/1/Instructions/101": "loadint(7,0)",
// this.SetPosition(640, 0);
"/Functions/4/Instructions/0": "loadint(1,640)",
"/Functions/4/Instructions/3": "loadint(2,0)",
This is a dialogue for the 1st player:
And for the 2nd player:

First, let's look at the 1st player. We can already learn a few interesting things.
- We use screen coordinates. The origin (0;0) is the top left pixel of the screen, and (1279;719) is the bottom right pixel of the screen.
- sprite.x and sprite.y seems relative to owner.balloon_x and owner.balloon_y. When we change these, the balloon image moves with it.
- text.x and text.y are relative to either owner.balloon_x/owner.balloon_y or sprite.x/sprite.y. When we change them, the text coordinates changes with them.
Now, let's look at the 2nd player. We can see how it works, and have a glimpse at which challenges we will face.
- It also uses raw screen coordinates. It isn't the most obvious in this screenshot, but when using this.owner.balloon_x = 0, the text is in the top left corner of the screen.
- The balloon is drawn from its origin, towards the left For both players, the balloon starts at 640. For Player 1, it ends at 640+balloon_size (in this example, 640+511=1151), but for Player 2, it ends at 640-balloon_size (in this example, 640-511=129).
- The text starts at the balloon's origin, but is drawn normally, left to right.
Next, cx and cy (which are stored in the Balloon object as sprite.x and sprite.y). Testing with cx=100 and cy=100, for Player 1:
And for Player 2:
Nothing too fancy here. This moves the balloon sprite by the specified offset in pixels. For player 2, the offset is reversed. It doesn't move the text.
In the game code, you can see that sprite.x takes the negative value of src.cx (and same for sprite.y and src.cy). With this, we can guess how it is intended to work:
- owner.balloon_x (and y) control the origin for drawing text.
- cx and cy are used to move the balloon out of the way, so that the text is drawn inside of the balloon (instead ob being drawn at the start of the sprite).
- text.x (and y) are used to center the text inside of its area.
For text.x and text.y, we already know that they are relative to owner.balloon_x (and y). The only important question left is whether x is reversed for Player 2. And after testing, it isn't. Which means x needs to be negative if we want the text to be inside the balloon.
The last question is about text.width and text.height. Is it just a number of characters? If is computed correctly according to its font? In order to test that, I'll set cx and cy back to 0, and I'll set text.x and text.y to text.width and text.height. By looking at the position of the text from the balloon's origin, we'll know these values.
I'll also replace the balloon sprite with a white sprite in order to make its origin easier to see.
text.width x text.height is around 114x30, while the actual text width x height is around 119x21. Let's check if it at least uses the font to calculate the width and height.
It does. Great. I used 25 l in the 1st image, and 25 _ in the 2nd one (because a l uses less width than a _), and we can see that they are indeed displayed at a different position.
I think I got a decent horizontal auto-centering.
This one completely ignores cx. Which, actually, is fine, because the balloons tend to have the same amount of non-usable space on the left and the right. In the final version, I think I'll have to make sure to make cx unused (or apply it to sprite.x instead).
Relevant code, in Squirrel:
text_x = src.texture.width / 2 - this.text.width / 2;
if (direction < 0)
text_x -= src.texture.width;
this.text.x = text_x;
And in thcrap jdiff:
"/Functions/1": {
"insert_instructions": {
"104": [
"getk(s5, \"text\", s0)",
"load(s6, \"x\")",
// s4 == src
// s7 = src.texture.width / 2
"getk(s7, \"texture\", s4)",
"getk(s7, \"width\", s7)",
"move(s10, s7)", // We might need it later
"loadfloat(s9, 2.0)",
"div(s7, s9, s7)",
// s8 = this.text.width / 2
"getk(s8, \"text\", s0)",
"getk(s8, \"width\", s8)",
"div(s8, s9, s8)",
// text_x = s7 - s8
"sub(s7, s8, s7)",
// if (this.direction < 0)
"getk(s8, \"direction\", s0)",
"loadint(s9, 0)",
"jcmp(s8, 1, s9, 0)", // 0 == CMP_G
// text_x -= src.texture.width;
"sub(s7, s10, s7)",
// this.text.x = text_x;
"set(s255, s5, s6, s7)",
]
},
"remove_instructions": [
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
]
},
~~Text centering is done (the last commits on base_tsa still have to be merged to master, this will be done when we release the next thcrap version)~~ Nvm, endings are broken.
Next step is balloon position: making the balloons point to the character's head. The balloon position is set at https://github.com/MathyFurret/th155-decomp/blob/7d5021d9a54ec135a2eff968ed9866cf4421d24f/src/data/script/talk/talk_character.nut#L71 (this.balloon_x and this.balloon_y are used for the balloon's SetPosition call). this.take is the return value of ::actor.LoadAnimationData("data/event/pic/reimu/face.pat") (for reimu for example, the filename comes from the LoadImageDef call in the pl file), and this.current is a value in the this.take object.
this.balloon_x = this.x + this.current.point[0].x * this.direction;
this.balloon_y = this.y + 720 + this.current.point[0].y;
The best way to place balloons correctly would be to edit all the values in these pat files manually, I guess. It will be easier if we have repatching support.
I decided to ignore the endings bug, because it was already present before all this, and nobody ever noticed (you have to use the break command with an @ to trigger it, but every language patch uses . which IIRC was the th145 syntax).
Repatching support is done, so I'll work on balloon position next.