Automatic Line Breaks, somewhat even lines (#5689)

Co-authored-by: Hedara <hedara90@gmail.com>
Co-authored-by: Eduardo Quezada <eduardo602002@gmail.com>
This commit is contained in:
hedara90 2024-11-29 18:46:45 +01:00 committed by GitHub
parent 5da1f322d2
commit e4ef3a440f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 331 additions and 50 deletions

View file

@ -10,6 +10,7 @@
max(POKEMON_NAME_LENGTH + 1, \
ABILITY_NAME_LENGTH + 1)))
#define BATTLE_MSG_MAX_WIDTH 208
#define BATTLE_MSG_MAX_LINES 2
// for 0xFD
#define B_TXT_BUFF1 0x0

33
include/line_break.h Normal file
View file

@ -0,0 +1,33 @@
#ifndef GUARD_LINE_BREAK_H
#define GUARD_LINE_BREAK_H
#define BADNESS_UNFILLED 1 // Badness added per pixel diff from max width
#define BADNESS_JAGGED 1 // Badness added per pixel diff from longest, squared per line
#define BADNESS_RUNT 100 // Badness added if there's a runt
#define BADNESS_OVERFLOW 100 // Badness added per pixel overflow, squared per line (not used)
#define BADNESS_WIDE_SPACE 1 // Badness added per extra pixel width (not used)
#define MAX_SPACE_WIDTH 5
struct StringWord {
u32 startIndex:16;
u32 length:8;
u32 width:8;
};
struct StringLine {
struct StringWord *words;
u16 numWords;
u8 spaceWidth;
u8 extraSpaceWidth;
};
void StripLineBreaks(u8 *src);
void BreakStringAutomatic(u8 *src, u32 maxWidth, u32 screenLines, u8 fontId);
void BreakSubStringAutomatic(u8 *src, u32 maxWidth, u32 screenLines, u8 fontId);
bool32 IsWordSplittingChar(const u8 *src, u32 index);
u32 GetStringBadness(struct StringLine *stringLines, u32 numLines, u32 maxWidth);
void BuildNewString(struct StringLine *stringLines, u32 numLines, u32 maxLines, u8 *str);
bool32 StringHasManualBreaks(u8 *src);
#endif // GUARD_LINE_BREAK_H

View file

@ -31,6 +31,7 @@
#include "text.h"
#include "util.h"
#include "window.h"
#include "line_break.h"
#include "constants/battle_anim.h"
#include "constants/battle_move_effects.h"
#include "constants/battle_partner.h"
@ -2044,6 +2045,7 @@ static void PlayerHandleChooseAction(u32 battler)
ActionSelectionCreateCursorAt(gActionSelectionCursor[battler], 0);
PREPARE_MON_NICK_BUFFER(gBattleTextBuff1, battler, gBattlerPartyIndexes[battler]);
BattleStringExpandPlaceholdersToDisplayedString(gText_WhatWillPkmnDo);
BreakStringAutomatic(gDisplayedStringBattle, WindowWidthPx(B_WIN_ACTION_PROMPT), 2, FONT_NORMAL);
if (B_SHOW_PARTNER_TARGET && gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && IsBattlerAlive(B_POSITION_PLAYER_RIGHT))
{

View file

@ -20,6 +20,7 @@
#include "text.h"
#include "util.h"
#include "window.h"
#include "line_break.h"
#include "constants/battle_anim.h"
#include "constants/songs.h"
#include "constants/trainers.h"
@ -298,6 +299,7 @@ static void SafariHandleChooseAction(u32 battler)
ActionSelectionCreateCursorAt(gActionSelectionCursor[battler], 0);
BattleStringExpandPlaceholdersToDisplayedString(gText_WhatWillPkmnDo2);
BreakStringAutomatic(gDisplayedStringBattle, WindowWidthPx(B_WIN_ACTION_PROMPT), 2, FONT_NORMAL);
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_ACTION_PROMPT);
}

View file

@ -22,6 +22,7 @@
#include "text.h"
#include "trainer_hill.h"
#include "window.h"
#include "line_break.h"
#include "constants/abilities.h"
#include "constants/battle_dome.h"
#include "constants/battle_string_ids.h"
@ -165,6 +166,11 @@ const u8 gText_drastically[] = _("drastically ");
const u8 gText_severely[] = _("severely ");
static const u8 sText_TerrainReturnedToNormal[] = _("The terrain returned to normal!"); // Unused
// Remove these when done testing
static const u8 sTest_TempTestText1[] = _("This is a text for testing stuff.");
static const u8 sTest_TempTestText2[] = _("This is a text for testing stuff that should be two lines.");
static const u8 sTest_TempTestText3[] = _("This is a text for testing stuff that should be three lines so it has to have some extra text.");
const u8 *const gBattleStringsTable[BATTLESTRINGS_COUNT] =
{
[STRINGID_TRAINER1LOSETEXT] = COMPOUND_STRING("{B_TRAINER1_LOSE_TEXT}"),
@ -1402,8 +1408,8 @@ const u8 gText_PkmnIsEvolving[] = _("What?\n{STR_VAR_1} is evolving!");
const u8 gText_CongratsPkmnEvolved[] = _("Congratulations! Your {STR_VAR_1}\nevolved into {STR_VAR_2}!{WAIT_SE}\p");
const u8 gText_PkmnStoppedEvolving[] = _("Huh? {STR_VAR_1}\nstopped evolving!\p");
const u8 gText_EllipsisQuestionMark[] = _("……?\p");
const u8 gText_WhatWillPkmnDo[] = _("What will\n{B_BUFF1} do?");
const u8 gText_WhatWillPkmnDo2[] = _("What will\n{B_PLAYER_NAME} do?");
const u8 gText_WhatWillPkmnDo[] = _("What will {B_BUFF1} do?");
const u8 gText_WhatWillPkmnDo2[] = _("What will {B_PLAYER_NAME} do?");
const u8 gText_WhatWillWallyDo[] = _("What will\nWALLY do?");
const u8 gText_LinkStandby[] = _("{PAUSE 16}Link standby…");
const u8 gText_BattleMenu[] = _("Battle{CLEAR_TO 56}Bag\nPokémon{CLEAR_TO 56}Run");
@ -2421,8 +2427,7 @@ static void GetBattlerNick(u32 battler, u8 *dst)
} \
} \
GetBattlerNick(battler, text); \
toCpy = text; \
dstWidth = GetStringLineWidth(fontId, dst, letterSpacing, lineNum, dstSize);
toCpy = text;
#define HANDLE_NICKNAME_STRING_LOWERCASE(battler) \
if (GetBattlerSide(battler) != B_SIDE_PLAYER) \
@ -2439,8 +2444,7 @@ static void GetBattlerNick(u32 battler, u8 *dst)
} \
} \
GetBattlerNick(battler, text); \
toCpy = text; \
dstWidth = GetStringLineWidth(fontId, dst, letterSpacing, lineNum, dstSize);
toCpy = text;
static const u8 *BattleStringGetOpponentNameByTrainerId(u16 trainerId, u8 *text, u8 multiplayerId, u8 battler)
{
@ -2589,17 +2593,10 @@ u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize)
{
u32 dstID = 0; // if they used dstID, why not use srcID as well?
const u8 *toCpy = NULL;
u32 lastValidSkip = 0;
u32 toCpyWidth = 0;
u32 dstWidth = 0;
// This buffer may hold either the name of a trainer, Pokémon, or item.
u8 text[max(max(max(32, TRAINER_NAME_LENGTH + 1), POKEMON_NAME_LENGTH + 1), ITEM_NAME_LENGTH)];
u8 *textStart = &text[0];
u8 multiplayerId;
u8 fontId = FONT_NORMAL;
s16 letterSpacing = 0;
u32 lineNum = 1;
u32 displayedLineNums = 1;
if (gBattleTypeFlags & BATTLE_TYPE_RECORDED_LINK)
multiplayerId = gRecordedBattleMultiplayerId;
@ -2617,7 +2614,6 @@ u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize)
while (*src != EOS)
{
toCpy = NULL;
dstWidth = GetStringLineWidth(fontId, dst, letterSpacing, lineNum, dstSize);
if (*src == PLACEHOLDER_BEGIN)
{
@ -3122,18 +3118,6 @@ u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize)
if (toCpy != NULL)
{
toCpyWidth = GetStringLineWidth(fontId, toCpy, letterSpacing, 1, dstSize);
if (dstWidth + toCpyWidth > BATTLE_MSG_MAX_WIDTH)
{
dst[lastValidSkip] = displayedLineNums == 1 ? CHAR_NEWLINE : CHAR_PROMPT_SCROLL;
dstWidth = GetStringLineWidth(fontId, dst, letterSpacing, lineNum, dstSize);
if (displayedLineNums == 1)
displayedLineNums++;
else
displayedLineNums = 1;
lineNum++;
}
while (*toCpy != EOS)
{
dst[dstID] = *toCpy;
@ -3153,31 +3137,7 @@ u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize)
}
else
{
toCpyWidth = GetGlyphWidth(*src, FALSE, fontId);
dst[dstID] = *src;
if (dstWidth + toCpyWidth > BATTLE_MSG_MAX_WIDTH)
{
dst[lastValidSkip] = displayedLineNums == 1 ? CHAR_NEWLINE : CHAR_PROMPT_SCROLL;
if (displayedLineNums == 1)
displayedLineNums++;
else
displayedLineNums = 1;
lineNum++;
dstWidth = 0;
}
switch (*src)
{
case CHAR_PROMPT_CLEAR:
case CHAR_PROMPT_SCROLL:
displayedLineNums = 1;
case CHAR_NEWLINE:
lineNum++;
dstWidth = 0;
//fallthrough
case CHAR_SPACE:
lastValidSkip = dstID;
break;
}
dstID++;
}
src++;
@ -3186,6 +3146,8 @@ u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize)
dst[dstID] = *src;
dstID++;
BreakStringAutomatic(dst, BATTLE_MSG_MAX_WIDTH, BATTLE_MSG_MAX_WIDTH, fontId);
return dstID;
}

281
src/line_break.c Normal file
View file

@ -0,0 +1,281 @@
#include "global.h"
#include "line_break.h"
#include "text.h"
#include "malloc.h"
void StripLineBreaks(u8 *src)
{
u32 currIndex = 0;
while (src[currIndex] != EOS)
{
if (src[currIndex] == CHAR_PROMPT_SCROLL || src[currIndex] == CHAR_NEWLINE)
src[currIndex] = CHAR_SPACE;
currIndex++;
}
}
void BreakStringAutomatic(u8 *src, u32 maxWidth, u32 screenLines, u8 fontId)
{
u32 currIndex = 0;
u8 *currSrc = src;
while (src[currIndex] != EOS)
{
if (src[currIndex] == CHAR_PROMPT_CLEAR)
{
u8 replacedChar = src[currIndex + 1];
src[currIndex + 1] = EOS;
BreakSubStringAutomatic(currSrc, maxWidth, screenLines, fontId);
src[currIndex + 1] = replacedChar;
currSrc = &src[currIndex + 1];
}
currIndex++;
}
BreakSubStringAutomatic(currSrc, maxWidth, screenLines, fontId);
}
void BreakSubStringAutomatic(u8 *src, u32 maxWidth, u32 screenLines, u8 fontId)
{
// If the string already has line breaks, don't interfere with them
if (StringHasManualBreaks(src))
return;
// Sanity check
if (src[0] == EOS)
return;
u32 numChars = 1;
u32 numWords = 1;
u32 currWordIndex = 0;
u32 currWordLength = 1;
bool32 isPrevCharSplitting = FALSE;
bool32 isCurrCharSplitting;
// Get numbers of chars in string and count words
while (src[numChars] != EOS)
{
isCurrCharSplitting = IsWordSplittingChar(src, numChars);
if (isCurrCharSplitting && !isPrevCharSplitting)
numWords++;
isPrevCharSplitting = isCurrCharSplitting;
numChars++;
}
// Allocate enough space for word data
struct StringWord *allWords = Alloc(numWords*sizeof(struct StringWord));
allWords[currWordIndex].startIndex = 0;
allWords[currWordIndex].width = 0;
isPrevCharSplitting = FALSE;
// Fill in word begin index and lengths
for (u32 i = 1; i < numChars; i++)
{
isCurrCharSplitting = IsWordSplittingChar(src, i);
if (isCurrCharSplitting && !isPrevCharSplitting)
{
allWords[currWordIndex].length = currWordLength;
currWordIndex++;
currWordLength = 0;
}
else if (!isCurrCharSplitting && isPrevCharSplitting)
{
allWords[currWordIndex].startIndex = i;
allWords[currWordIndex].width = 0;
currWordLength++;
}
else
{
currWordLength++;
}
isPrevCharSplitting = isCurrCharSplitting;
}
allWords[currWordIndex].length = currWordLength;
// Fill in individual word widths
for (u32 i = 0; i < numWords; i++)
{
for (u32 j = 0; j < allWords[i].length; j++)
allWords[i].width += GetGlyphWidth(src[allWords[i].startIndex + j], FALSE, fontId);
}
// Step 1: Does it all fit one one line? Then no break
// Step 2: Try to split across minimum number of lines
u32 spaceWidth = GetGlyphWidth(0, FALSE, fontId);
u32 totalWidth = allWords[0].width;
// Calculate total widths without any line breaks
for (u32 i = 1; i < numWords; i++)
totalWidth += allWords[i].width + spaceWidth;
// If it doesn't fit on 1 line, do fancy line break calculation
// NOTE: Currently the line break calculation isn't fancy
if (totalWidth > maxWidth)
{
// Figure out how many lines are needed with naive method
u32 currLineWidth = 0;
u32 totalLines = 1;
bool32 shouldTryAgain;
for (currWordIndex = 0; currWordIndex < numWords; currWordIndex++)
{
if (currLineWidth + allWords[currWordIndex].length > maxWidth)
{
totalLines++;
currLineWidth = allWords[currWordIndex].width;
}
else
{
currLineWidth += allWords[currWordIndex].width + spaceWidth;
}
}
// LINE LAYOUT STARTS HERE
struct StringLine *stringLines;
do
{
shouldTryAgain = FALSE;
u16 targetLineWidth = totalWidth/totalLines;
stringLines = Alloc(totalLines*sizeof(struct StringLine));
for (u32 lineIndex = 0; lineIndex < totalLines; lineIndex++)
{
stringLines[lineIndex].numWords = 0;
stringLines[lineIndex].spaceWidth = spaceWidth;
stringLines[lineIndex].extraSpaceWidth = 0;
}
currWordIndex = 0;
u16 currLineIndex = 0;
stringLines[currLineIndex].words = &allWords[currWordIndex];
stringLines[currLineIndex].numWords = 1;
currLineWidth = allWords[currWordIndex].width;
currWordIndex++;
while (currWordIndex < numWords)
{
if (currLineWidth + spaceWidth + allWords[currWordIndex].width > maxWidth)
{
// go to next line
currLineIndex++;
if (currLineIndex == totalLines)
{
totalLines++;
Free(stringLines);
shouldTryAgain = TRUE;
break;
}
stringLines[currLineIndex].words = &allWords[currWordIndex];
stringLines[currLineIndex].numWords = 1;
currLineWidth = allWords[currWordIndex].width;
currWordIndex++;
}
else if (currLineWidth > targetLineWidth)
{
// go to next line
currLineIndex++;
if (currLineIndex == totalLines)
{
totalLines++;
Free(stringLines);
shouldTryAgain = TRUE;
break;
}
stringLines[currLineIndex].words = &allWords[currWordIndex];
stringLines[currLineIndex].numWords = 1;
currLineWidth = allWords[currWordIndex].width;
currWordIndex++;
}
else
{
// continue on current line
// add word and space width
currLineWidth += spaceWidth + allWords[currWordIndex].width;
stringLines[currLineIndex].numWords++;
currWordIndex++;
}
}
} while (shouldTryAgain);
//u32 currBadness = GetStringBadness(stringLines, totalLines, maxWidth);
BuildNewString(stringLines, totalLines, screenLines, src);
Free(stringLines);
}
Free(allWords);
}
// Only allow word splitting on allowed chars
bool32 IsWordSplittingChar(const u8 *src, u32 index)
{
switch (src[index])
{
case CHAR_SPACE:
return TRUE;
default:
return FALSE;
}
}
// Badness calculation
// unfilled lines scale linerarly
// jagged lines scales by the square
// runts scale linearly
// numbers not final
// ISN'T ACTUALLY USED RIGHT NOW
u32 GetStringBadness(struct StringLine *stringLines, u32 numLines, u32 maxWidth)
{
u32 badness = 0;
u32 *lineWidths = Alloc(numLines*4);
u32 widestWidth = 0;
for (u32 i = 0; i < numLines; i++)
{
lineWidths[i] = 0;
for (u32 j = 0; j < stringLines[i].numWords; j++)
lineWidths[i] += stringLines[i].words[j].width;
lineWidths[i] += (stringLines[i].numWords-1)*stringLines[i].spaceWidth;
if (lineWidths[i] > widestWidth)
widestWidth = lineWidths[i];
if (stringLines[i].numWords == 1)
badness += BADNESS_RUNT;
}
for (u32 i = 0; i < numLines; i++)
{
u32 extraSpaceWidth = 0;
if (lineWidths[i] != widestWidth)
{
// Not the best way to do this, ideally a line should be allowed to get longer than current widest
// line. But then the widest line has to be recalculated.
while (lineWidths[i] + (extraSpaceWidth + 1) * (stringLines[i].numWords - 1) < widestWidth && extraSpaceWidth < MAX_SPACE_WIDTH)
extraSpaceWidth++;
lineWidths[i] += extraSpaceWidth*(stringLines[i].numWords-1);
}
badness += (maxWidth - lineWidths[i]) * BADNESS_UNFILLED;
u32 baseBadness = (widestWidth - lineWidths[i]) * BADNESS_JAGGED;
badness += baseBadness*baseBadness;
stringLines[i].extraSpaceWidth = extraSpaceWidth;
}
Free(lineWidths);
return badness;
}
// Build the new string from the data stored in the StringLine structs
void BuildNewString(struct StringLine *stringLines, u32 numLines, u32 maxLines, u8 *str)
{
u32 srcCharIndex = 0;
for (u32 lineIndex = 0; lineIndex < numLines; lineIndex++)
{
srcCharIndex += stringLines[lineIndex].words[0].length;
for (u32 wordIndex = 1; wordIndex < stringLines[lineIndex].numWords; wordIndex++)
// Add length of word and a space
srcCharIndex += stringLines[lineIndex].words[wordIndex].length + 1;
if (lineIndex + 1 < numLines)
{
// Add the appropriate line break depending on line number
if (lineIndex >= maxLines - 1 && numLines > maxLines)
str[srcCharIndex] = CHAR_PROMPT_SCROLL;
else
str[srcCharIndex] = CHAR_NEWLINE;
srcCharIndex++;
}
}
}
bool32 StringHasManualBreaks(u8 *src)
{
u32 charIndex = 0;
while (src[charIndex] != EOS)
{
if (src[charIndex] == CHAR_PROMPT_SCROLL || src[charIndex] == CHAR_NEWLINE)
return TRUE;
charIndex++;
}
return FALSE;
}