1 /*
2 * This file is part of EasyRPG Player.
3 *
4 * EasyRPG Player is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * EasyRPG Player is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 // Headers
19 #include <cctype>
20 #include <sstream>
21 #include <iterator>
22
23 #include "compiler.h"
24 #include "window_message.h"
25 #include "game_actors.h"
26 #include "game_map.h"
27 #include "game_message.h"
28 #include "game_party.h"
29 #include "game_system.h"
30 #include "game_variables.h"
31 #include "input.h"
32 #include "output.h"
33 #include "player.h"
34 #include "util_macro.h"
35 #include "game_battle.h"
36 #include "bitmap.h"
37 #include "font.h"
38 #include "cache.h"
39 #include "text.h"
40
41 // FIXME: Off by 1 bug in window base class
42 constexpr int message_animation_frames = 7;
43
44 namespace {
45 #if defined(EP_DEBUG_MESSAGE) || defined(EP_DEBUG_MESSAGE_TEXT)
46 static int frame_offset = 0;
47
DebugLogResetFrameCounter()48 void DebugLogResetFrameCounter() {
49 frame_offset = Main_Data::game_system->GetFrameCounter();
50 }
51 #else
52 void DebugLogResetFrameCounter() { }
53 #endif
54
55 #ifdef EP_DEBUG_MESSAGE
56 template <typename... Args>
DebugLog(const char * fmt,Args &&...args)57 void DebugLog(const char* fmt, Args&&... args) {
58 int frames = Main_Data::game_system->GetFrameCounter() - frame_offset;
59 Output::Debug(fmt, frames, std::forward<Args>(args)...);
60 }
61 #else
62
63 template <typename... Args>
DebugLog(const char *,Args &&...)64 void DebugLog(const char*, Args&&...) { }
65 #endif
66
67 #ifdef EP_DEBUG_MESSAGE_TEXT
68 template <typename... Args>
DebugLogText(const char * fmt,Args &&...args)69 void DebugLogText(const char* fmt, Args&&... args) {
70 int frames = Main_Data::game_system->GetFrameCounter() - frame_offset;
71 Output::Debug(fmt, frames, std::forward<Args>(args)...);
72 }
73 #else
74
75 template <typename... Args>
DebugLogText(const char *,Args &&...)76 void DebugLogText(const char*, Args&&...) { }
77 #endif
78 } //namespace
79
80 // C4428 is nonsense
81 #ifdef _MSC_VER
82 #pragma warning (disable : 4428)
83 #endif
84
Window_Message(int ix,int iy,int iwidth,int iheight)85 Window_Message::Window_Message(int ix, int iy, int iwidth, int iheight) :
86 Window_Selectable(ix, iy, iwidth, iheight),
87 number_input_window(new Window_NumberInput(0, 0)),
88 gold_window(new Window_Gold(232, 0, 88, 32))
89 {
90 SetContents(Bitmap::Create(width - 16, height - 16));
91
92 // 2k3 transparent message boxes
93 bool msg_transparent = Player::IsRPG2k3()
94 // if the flag is set ..
95 && (lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_transparent)
96 // if we're not in battle, or if we are in battle but not using mode A
97 && (!Game_Battle::IsBattleRunning() || lcf::Data::battlecommands.battle_type != lcf::rpg::BattleCommands::BattleType_traditional)
98 // RPG_RT < 1.11 bug, map messages were not transparent if the battle type was mode A.
99 && (Player::IsRPG2k3E() || lcf::Data::battlecommands.battle_type != lcf::rpg::BattleCommands::BattleType_traditional);
100 if (msg_transparent) {
101 SetBackOpacity(160);
102 }
103 gold_window->SetBackOpacity(GetBackOpacity());
104
105 SetVisible(false);
106 // Above other windows
107 SetZ(Priority_Window + 100);
108
109 active = true;
110 SetIndex(-1);
111 text_color = Font::ColorDefault;
112
113 number_input_window->SetVisible(false);
114
115 gold_window->SetVisible(false);
116
117 Main_Data::game_system->ClearMessageFace();
118 Game_Message::SetWindow(this);
119 }
120
~Window_Message()121 Window_Message::~Window_Message() {
122 if (Game_Message::GetWindow() == this) {
123 Game_Message::SetWindow(nullptr);
124 }
125 }
126
StartMessageProcessing(PendingMessage pm)127 void Window_Message::StartMessageProcessing(PendingMessage pm) {
128 text.clear();
129 pending_message = std::move(pm);
130
131 if (!IsVisible()) {
132 DebugLogResetFrameCounter();
133 }
134 DebugLog("{}: MSG START");
135
136 if (!pending_message.IsActive()) {
137 return;
138 }
139
140 const auto& lines = pending_message.GetLines();
141
142 int num_lines = 0;
143 auto append = [&](const std::string& line) {
144 bool force_page_break = (!line.empty() && line.back() == '\f');
145
146 text.append(line, 0, line.size() - force_page_break);
147 if (line.empty() || text.back() != '\n') {
148 text.push_back('\n');
149 }
150 ++num_lines;
151
152 if (num_lines == GetMaxLinesPerPage() || force_page_break) {
153 text.push_back('\f');
154 num_lines = 0;
155 }
156 };
157
158 if (pending_message.IsWordWrapped()) {
159 for (const std::string& line : lines) {
160 /* TODO: don't take commands like \> \< into account when word-wrapping */
161 Game_Message::WordWrap(
162 line,
163 width - 24,
164 [&](StringView wrapped_line) {
165 append(std::string(wrapped_line));
166 }
167 );
168 }
169 } else {
170 for (const std::string& line : lines) {
171 append(line);
172 }
173 }
174
175 if (text.empty() || text.back() != '\f') {
176 text.push_back('\f');
177 }
178
179 item_max = min(4, pending_message.GetNumChoices());
180
181 text_index = text.data();
182
183 DebugLog("{}: MSG TEXT \n{}", text);
184
185 auto open_frames = (!IsVisible() && !Game_Battle::IsBattleRunning()) ? message_animation_frames : 0;
186 SetOpenAnimation(open_frames);
187 DebugLog("{}: MSG START OPEN {}", open_frames);
188
189 InsertNewPage();
190 }
191
OnFinishPage()192 void Window_Message::OnFinishPage() {
193 DebugLog("{}: FINISH PAGE");
194
195 if (pending_message.GetNumChoices() > 0) {
196 StartChoiceProcessing();
197 } else if (pending_message.HasNumberInput()) {
198 StartNumberInputProcessing();
199 } else if (!kill_page) {
200 DebugLog("{}: SET PAUSE");
201 SetPause(true);
202 }
203
204 line_count = 0;
205 kill_page = false;
206 line_char_counter = 0;
207 }
208
StartChoiceProcessing()209 void Window_Message::StartChoiceProcessing() {
210 SetIndex(0);
211 }
212
StartNumberInputProcessing()213 void Window_Message::StartNumberInputProcessing() {
214 number_input_window->SetMaxDigits(pending_message.GetNumberInputDigits());
215 if (IsFaceEnabled() && !Main_Data::game_system->IsMessageFaceRightPosition()) {
216 number_input_window->SetX(LeftMargin + FaceSize + RightFaceMargin);
217 } else {
218 number_input_window->SetX(x);
219 }
220 number_input_window->SetY(y + contents_y - 2);
221 number_input_window->SetActive(true);
222 if (IsVisible() && !IsOpeningOrClosing()) {
223 number_input_window->SetVisible(true);
224 }
225 number_input_window->Update();
226 }
227
ShowGoldWindow()228 void Window_Message::ShowGoldWindow() {
229 if (Game_Battle::IsBattleRunning()) {
230 return;
231 }
232 if (!gold_window->IsVisible()) {
233 gold_window->SetY(y == 0 ? SCREEN_TARGET_HEIGHT - 32 : 0);
234 gold_window->SetOpenAnimation(message_animation_frames);
235 } else if (gold_window->IsClosing()) {
236 gold_window->SetOpenAnimation(0);
237 }
238 gold_window->Refresh();
239 }
240
InsertNewPage()241 void Window_Message::InsertNewPage() {
242 DebugLog("{}: MSG NEW PAGE");
243 // Cancel pending face requests for async
244 // Otherwise they render on the wrong page
245 face_request_ids.clear();
246
247 contents->Clear();
248 SetIndex(-1);
249 SetPause(false);
250 number_input_window->SetActive(false);
251 number_input_window->SetVisible(false);
252 kill_page = false;
253 line_count = 0;
254 text_color = Font::ColorDefault;
255 speed = 1;
256 kill_page = false;
257 instant_speed = false;
258 prev_char_printable = false;
259 prev_char_waited = true;
260
261 y = Game_Message::GetRealPosition() * 80;
262
263 if (Main_Data::game_system->IsMessageTransparent()) {
264 SetOpacity(0);
265 gold_window->SetBackOpacity(0);
266 } else {
267 SetOpacity(255);
268 gold_window->SetBackOpacity(GetBackOpacity());
269 }
270
271 if (IsFaceEnabled()) {
272 if (!Main_Data::game_system->IsMessageFaceRightPosition()) {
273 contents_x = LeftMargin + FaceSize + RightFaceMargin;
274 DrawFace(Main_Data::game_system->GetMessageFaceName(), Main_Data::game_system->GetMessageFaceIndex(), LeftMargin, TopMargin, Main_Data::game_system->IsMessageFaceFlipped());
275 } else {
276 contents_x = 0;
277 DrawFace(Main_Data::game_system->GetMessageFaceName(), Main_Data::game_system->GetMessageFaceIndex(), 248, TopMargin, Main_Data::game_system->IsMessageFaceFlipped());
278 }
279 } else {
280 contents_x = 0;
281 }
282
283 if (pending_message.GetChoiceStartLine() == 0 && pending_message.HasChoices()) {
284 contents_x += 12;
285 }
286
287 contents_y = 2;
288
289 if (pending_message.GetNumberInputStartLine() == 0 && pending_message.HasNumberInput()) {
290 // If there is an input window on the first line
291 StartNumberInputProcessing();
292 }
293 line_char_counter = 0;
294
295 if (pending_message.ShowGoldWindow()) {
296 ShowGoldWindow();
297 } else {
298 // If first character is gold, the gold window appears immediately and animates open with the main window.
299 auto tret = Utils::TextNext(text_index, (text.data() + text.size()), Player::escape_char);
300 if (tret && tret.is_escape && tret.ch == '$') {
301 ShowGoldWindow();
302 }
303 }
304
305 }
306
InsertNewLine()307 void Window_Message::InsertNewLine() {
308 DebugLog("{}: MSG NEW LINE");
309 if (IsFaceEnabled() && !Main_Data::game_system->IsMessageFaceRightPosition()) {
310 contents_x = LeftMargin + FaceSize + RightFaceMargin;
311 } else {
312 contents_x = 0;
313 }
314
315 contents_y += 16;
316 ++line_count;
317
318 if (pending_message.HasChoices() && line_count >= pending_message.GetChoiceStartLine()) {
319 unsigned choice_index = line_count - pending_message.GetChoiceStartLine();
320 if (pending_message.GetChoiceResetColor()) {
321 // Check for disabled choices
322 if (!pending_message.IsChoiceEnabled(choice_index)) {
323 text_color = Font::ColorDisabled;
324 } else {
325 text_color = Font::ColorDefault;
326 }
327 }
328
329 contents_x += 12;
330 }
331 line_char_counter = 0;
332 prev_char_printable = false;
333 prev_char_waited = true;
334 }
335
FinishMessageProcessing()336 void Window_Message::FinishMessageProcessing() {
337 DebugLog("{}: FINISH MSG");
338 text.clear();
339 text_index = text.data();
340
341 SetPause(false);
342 kill_page = false;
343 line_char_counter = 0;
344 SetIndex(-1);
345
346 pending_message = {};
347
348 auto close_frames = Game_Battle::IsBattleRunning() ? 0 : message_animation_frames;
349
350 if (number_input_window->IsVisible()) {
351 number_input_window->SetActive(false);
352 number_input_window->SetVisible(false);
353 }
354
355 SetCloseAnimation(close_frames);
356 close_started_this_frame = true;
357 DebugLog("{}: MSG START CLOSE {}", close_frames);
358
359 // RPG_RT updates window open/close at the end of the main loop.
360 // To simulate this timing, we run base class Update() again once
361 // to animate the closing by 1 frame.
362 Window::Update();
363
364 if (gold_window->IsVisible()) {
365 gold_window->SetCloseAnimation(close_frames);
366 gold_window->Update();
367 }
368 }
369
ResetWindow()370 void Window_Message::ResetWindow() {
371
372 }
373
Update()374 void Window_Message::Update() {
375 aop = {};
376 if (IsOpening()) { DebugLog("{}: MSG OPENING"); }
377 if (IsClosing()) { DebugLog("{}: MSG CLOSING"); }
378
379 close_started_this_frame = false;
380 close_finished_this_frame = false;
381
382 const bool was_closing = IsClosing();
383
384 Window_Selectable::Update();
385 number_input_window->Update();
386 gold_window->Update();
387
388 if (was_closing && !IsClosing()) {
389 close_finished_this_frame = true;
390 }
391
392 if (!IsVisible()) {
393 return;
394 }
395
396 if (IsOpeningOrClosing()) {
397 return;
398 }
399
400 if (wait_count > 0) {
401 DebugLog("{}: MSG WAIT {}", wait_count);
402 --wait_count;
403 return;
404 }
405
406 if (GetPause()) {
407 DebugLog("{}: MSG PAUSE");
408 WaitForInput();
409
410 if (GetPause()) {
411 return;
412 }
413 }
414
415 if (GetIndex() >= 0) {
416 DebugLog("{}: MSG CHOICE");
417 InputChoice();
418 if (GetIndex() >= 0) {
419 return;
420 }
421 }
422
423 if (number_input_window->GetActive()) {
424 DebugLog("{}: MSG NUMBER");
425 InputNumber();
426 if (number_input_window->GetActive()) {
427 return;
428 }
429 }
430
431 DebugLog("{}: MSG UPD");
432 UpdateMessage();
433 }
434
UpdateMessage()435 void Window_Message::UpdateMessage() {
436 // Message Box Show Message rendering loop
437 bool instant_speed_forced = false;
438
439 if (Player::debug_flag && Input::IsPressed(Input::SHIFT)) {
440 instant_speed = true;
441 instant_speed_forced = true;
442 }
443
444 auto system = Cache::SystemOrBlack();
445 auto font = Font::Default();
446
447 while (true) {
448 const auto* end = text.data() + text.size();
449
450 if (wait_count > 0) {
451 DebugLog("{}: MSG WAIT LOOP {}", wait_count);
452 --wait_count;
453 break;
454 }
455
456 if (GetPause() || GetIndex() >= 0 || number_input_window->GetActive()) {
457 break;
458 }
459
460 if (text_index == end) {
461 FinishMessageProcessing();
462 break;
463 }
464
465 auto tret = Utils::TextNext(text_index, end, Player::escape_char);
466 auto text_prev = text_index;
467 text_index = tret.next;
468
469 if (EP_UNLIKELY(!tret)) {
470 continue;
471 }
472
473 const auto ch = tret.ch;
474 if (tret.is_exfont) {
475 if (!DrawGlyph(*font, *system, ch, true)) {
476 text_index = text_prev;
477 }
478 continue;
479 }
480
481 if (ch == '\f') {
482 if (text_index != end) {
483 InsertNewPage();
484 SetWait(1);
485 }
486 continue;
487 }
488
489 if (ch == '\n') {
490 int wait_frames = 0;
491 bool end_page = (*text_index == '\f');
492
493 if (!instant_speed) {
494 if (!prev_char_printable) {
495 wait_frames += 1 + end_page;
496 }
497 } else if (end_page) {
498 // When the page ends and speed is instant, RPG_RT always waits 2 frames.
499 wait_frames += 2;
500 }
501
502 InsertNewLine();
503
504 if (end_page) {
505 OnFinishPage();
506 }
507 SetWait(wait_frames);
508
509 if (instant_speed && !instant_speed_forced) {
510 // instant_speed stops at the end of the line
511 // unless it was triggered by the shift key.
512 instant_speed = false;
513 }
514 continue;
515 }
516
517 if (Utils::IsControlCharacter(ch)) {
518 // control characters not handled
519 continue;
520 }
521
522 if (tret.is_escape && ch != Player::escape_char) {
523 // Special message codes
524 switch (ch) {
525 case 'c':
526 case 'C':
527 {
528 // Color
529 auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true);
530 auto value = pres.value;
531 text_index = pres.next;
532 DebugLogText("{}: MSG Color \\c[{}]", value);
533 SetWaitForNonPrintable(0);
534 text_color = value > 19 ? 0 : value;
535 }
536 break;
537 case 's':
538 case 'S':
539 {
540 // Speed modifier
541 auto pres = Game_Message::ParseSpeed(text_index, end, Player::escape_char, true);
542 text_index = pres.next;
543 DebugLogText("{}: MSG Speed \\s[{}]", pres.value);
544 SetWaitForNonPrintable(0);
545 speed = Utils::Clamp(pres.value, 1, 20);
546 }
547 break;
548 case '_':
549 // Insert half size space
550 contents_x += Font::Default()->GetSize(" ").width / 2;
551 DebugLogText("{}: MSG HalfWait \\_");
552 SetWaitForCharacter(1);
553 break;
554 case '$':
555 // Show Gold Window
556 ShowGoldWindow();
557 DebugLogText("{}: MSG Gold \\$");
558 SetWaitForNonPrintable(speed);
559 break;
560 case '!':
561 // Text pause
562 DebugLogText("{}: MSG Pause \\!");
563 SetWaitForNonPrintable(0);
564 SetPause(true);
565 break;
566 case '^':
567 // Force message close
568 // The close happens at the end of the message, not where
569 // the ^ is encountered
570 DebugLogText("{}: MSG Kill Page \\^");
571 kill_page = true;
572 SetWaitForNonPrintable(speed);
573 break;
574 case '>':
575 // Instant speed start
576 DebugLogText("{}: MSG Instant Speed Start \\>");
577 SetWaitForNonPrintable(0);
578 instant_speed = true;
579 break;
580 case '<':
581 // Instant speed stop - also cancels shift key and forces a delay.
582 instant_speed = false;
583 instant_speed_forced = false;
584 DebugLogText("{}: MSG Instant Speed Stop \\<");
585 SetWaitForNonPrintable(speed);
586 break;
587 case '.':
588 // 1/4 second sleep
589 // Despite documentation saying 1/4 second, RPG_RT waits for 16 frames.
590 // RPG_RT also has a bug(??) where speeds >= 17 slow this down by 1 more frame per speed.
591 SetWaitForNonPrintable(16 + Utils::Clamp(speed - 16, 0, 4));
592 DebugLogText("{}: MSG Quick Sleep \\.");
593 break;
594 case '|':
595 // Second sleep
596 // Despite documentation saying 1 second, RPG_RT waits for 61 frames.
597 SetWaitForNonPrintable(61);
598 DebugLogText("{}: MSG Sleep \\|");
599 break;
600 default:
601 // Unknown characters will not display anything but do wait.
602 SetWaitForNonPrintable(speed);
603 break;
604 }
605 continue;
606 }
607
608 if (!DrawGlyph(*font, *system, ch, false)) {
609 text_index = text_prev;
610 continue;
611 }
612 }
613 }
614
DrawGlyph(Font & font,const Bitmap & system,char32_t glyph,bool is_exfont)615 bool Window_Message::DrawGlyph(Font& font, const Bitmap& system, char32_t glyph, bool is_exfont) {
616 if (is_exfont) {
617 DebugLogText("{}: MSG DrawGlyph Exfont {}", static_cast<uint32_t>(glyph));
618 } else {
619 if (glyph < 128) {
620 DebugLogText("{}: MSG DrawGlyph ASCII {}", static_cast<char>(glyph));
621 } else {
622 DebugLogText("{}: MSG DrawGlyph UTF32 {#:X}", static_cast<uint32_t>(glyph));
623 }
624 }
625
626 // RPG_RT compatible for half-width (6) and full-width (12)
627 // generalizes the algo for even bigger glyphs
628 auto get_width = [](int w) {
629 return (w > 0) ? (w - 1) / 6 + 1 : 0;
630 };
631
632 // Wide characters cause an extra wait if the last printed character did not wait.
633 if (prev_char_printable && !prev_char_waited) {
634 auto& wide_font = is_exfont ? *Font::exfont : font;
635 auto rect = wide_font.GetSize(glyph);
636 auto width = get_width(rect.width);
637 if (width >= 2) {
638 prev_char_waited = true;
639 ++line_char_counter;
640 SetWait(1);
641 return false;
642 }
643 }
644
645 auto rect = Text::Draw(*contents, contents_x, contents_y, font, system, text_color, glyph, is_exfont);
646
647 int glyph_width = rect.width;
648 contents_x += glyph_width;
649 int width = get_width(glyph_width);
650 SetWaitForCharacter(width);
651
652 return true;
653 }
654
IncrementLineCharCounter(int width)655 void Window_Message::IncrementLineCharCounter(int width) {
656 // For speed 1, RPG_RT prints 2 half width chars every frame. This
657 // resets anytime we print a full width character or another
658 // character with a different speed.
659 // To emulate this, we increment by 2 and clear the low bit anytime
660 // we're not a speed 1 half width char.
661 if (width == 1 && speed <= 1) {
662 line_char_counter++;
663 } else {
664 line_char_counter = (line_char_counter & ~1) + 2;
665 }
666 }
667
UpdateCursorRect()668 void Window_Message::UpdateCursorRect() {
669 if (index >= 0) {
670 int x_pos = 2;
671 int y_pos = (pending_message.GetChoiceStartLine() + index) * 16;
672 int width = contents->GetWidth() - 4;
673
674 if (IsFaceEnabled()) {
675 if (!Main_Data::game_system->IsMessageFaceRightPosition()) {
676 x_pos += LeftMargin + FaceSize + RightFaceMargin;
677 }
678 width = width - LeftMargin - FaceSize - RightFaceMargin;
679 }
680
681 SetCursorRect(Rect(x_pos, y_pos, width, 16));
682 } else {
683 SetCursorRect(Rect());
684 }
685 }
686
WaitForInput()687 void Window_Message::WaitForInput() {
688 if (Input::IsTriggered(Input::DECISION) ||
689 Input::IsTriggered(Input::CANCEL)) {
690 SetPause(false);
691 }
692 }
693
InputChoice()694 void Window_Message::InputChoice() {
695 int choice_result = -1;
696
697 if (Input::IsTriggered(Input::CANCEL)) {
698 if (pending_message.GetChoiceCancelType() > 0) {
699 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel));
700 choice_result = pending_message.GetChoiceCancelType() - 1; // Cancel
701 }
702 } else if (Input::IsTriggered(Input::DECISION)) {
703 if (!pending_message.IsChoiceEnabled(index)) {
704 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer));
705 return;
706 }
707
708 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
709 choice_result = index;
710 }
711
712 if (choice_result >= 0) {
713 auto& continuation = pending_message.GetChoiceContinuation();
714 if (continuation) {
715 aop = continuation(choice_result);
716 }
717 // This disables choices
718 index = -1;
719 }
720 }
721
InputNumber()722 void Window_Message::InputNumber() {
723 number_input_window->SetVisible(true);
724 if (Input::IsTriggered(Input::DECISION)) {
725 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
726 Main_Data::game_variables->Set(pending_message.GetNumberInputVariable(), number_input_window->GetNumber());
727 Game_Map::SetNeedRefresh(true);
728 number_input_window->SetNumber(0);
729 number_input_window->SetActive(false);
730 }
731 }
732
SetWaitForNonPrintable(int frames)733 void Window_Message::SetWaitForNonPrintable(int frames) {
734 if (!instant_speed) {
735 if (speed <= 1) {
736 frames += (line_char_counter & 1);
737 }
738 SetWait(frames);
739 }
740 prev_char_waited = (instant_speed || frames > 0);
741 prev_char_printable = false;
742 // Non printables only contribute to character count after the first printable..
743 if (line_char_counter > 0) {
744 IncrementLineCharCounter(1);
745 }
746 }
747
SetWaitForCharacter(int width)748 void Window_Message::SetWaitForCharacter(int width) {
749 int frames = 0;
750 if (!instant_speed && width > 0) {
751 bool is_last_for_page = (text.data() + text.size() - text_index) < 2 || (*text_index == '\n' && *(text_index + 1) == '\f');
752
753 if (is_last_for_page) {
754 // RPG_RT always waits 2 frames for last character on the page.
755 // FIXME: Exfonts / wide last on page?
756 frames = 2;
757 } else {
758 if (speed > 1) {
759 frames = speed * width / 2 + 1;
760 } else {
761 frames = width / 2;
762 if (width & 1) {
763 bool is_last_for_line = (*text_index == '\n');
764
765 // RPG_RT waits for every even character. Also always waits
766 // for the last character.
767 frames += (line_char_counter & 1) || is_last_for_line;
768 }
769 }
770 }
771 }
772 prev_char_waited = (instant_speed || frames > 0);
773 prev_char_printable = true;
774 SetWait(frames);
775 IncrementLineCharCounter(width);
776 }
777
SetWait(int frames)778 void Window_Message::SetWait(int frames) {
779 assert(speed >= 1 && speed <= 20);
780 DebugLogText("{}: MSG SetWait {}", frames);
781 wait_count = frames;
782 }
783
IsFaceEnabled() const784 bool Window_Message::IsFaceEnabled() const {
785 return pending_message.IsFaceEnabled() && !Main_Data::game_system->GetMessageFaceName().empty();
786 }
787
788