1 // -*- Mode: C++; tab-width:2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 // vi:tw=80:et:ts=2:sts=2
3 //
4 // -----------------------------------------------------------------------
5 //
6 // This file is part of RLVM, a RealLive virtual machine clone.
7 //
8 // -----------------------------------------------------------------------
9 //
10 // Copyright (C) 2006, 2007 Elliot Glaysher
11 //
12 // This program is free software; you can redistribute it and/or modify
13 // it under the terms of the GNU General Public License as published by
14 // the Free Software Foundation; either version 3 of the License, or
15 // (at your option) any later version.
16 //
17 // This program is distributed in the hope that it will be useful,
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 // GNU General Public License for more details.
21 //
22 // You should have received a copy of the GNU General Public License
23 // along with this program; if not, write to the Free Software
24 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
25 //
26 // -----------------------------------------------------------------------
27
28 #include <boost/archive/text_iarchive.hpp>
29 #include <boost/archive/text_oarchive.hpp>
30
31 #include "systems/base/text_system.h"
32
33 #include <algorithm>
34 #include <map>
35 #include <sstream>
36 #include <string>
37 #include <vector>
38
39 #include "base/notification_details.h"
40 #include "base/notification_service.h"
41 #include "base/notification_source.h"
42 #include "libreallive/gameexe.h"
43 #include "machine/memory.h"
44 #include "machine/rlmachine.h"
45 #include "machine/serialization.h"
46 #include "systems/base/graphics_system.h"
47 #include "systems/base/surface.h"
48 #include "systems/base/system.h"
49 #include "systems/base/text_key_cursor.h"
50 #include "systems/base/text_page.h"
51 #include "systems/base/text_window.h"
52 #include "utf8cpp/utf8.h"
53 #include "utilities/exception.h"
54 #include "utilities/string_utilities.h"
55
56 using std::back_inserter;
57 using std::endl;
58 using std::ostringstream;
59 using std::string;
60 using std::vector;
61
62 const unsigned int MAX_PAGE_HISTORY = 100;
63
64 const int FULLWIDTH_NUMBER_SIGN = 0xFF03;
65 const int FULLWIDTH_A = 0xFF21;
66 const int FULLWIDTH_B = 0xFF22;
67 const int FULLWIDTH_ZERO = 0xFF10;
68 const int FULLWIDTH_NINE = 0xFF19;
69
70 // -----------------------------------------------------------------------
71 // TextSystemGlobals
72 // -----------------------------------------------------------------------
TextSystemGlobals()73 TextSystemGlobals::TextSystemGlobals()
74 : auto_mode_base_time(100),
75 auto_mode_char_time(100),
76 message_speed(30),
77 font_weight(0),
78 font_shadow(1) {}
79
80 // -----------------------------------------------------------------------
81
TextSystemGlobals(Gameexe & gexe)82 TextSystemGlobals::TextSystemGlobals(Gameexe& gexe)
83 : auto_mode_base_time(gexe("MESSAGE_KEY_WAIT_TIME").ToInt(1500)),
84 auto_mode_char_time(gexe("INIT_MESSAGE_SPEED").ToInt(30)),
85 message_speed(gexe("INIT_MESSAGE_SPEED").ToInt(30)),
86 font_weight(0),
87 font_shadow(1) {
88 GameexeInterpretObject in_window_attr(gexe("WINDOW_ATTR"));
89 if (in_window_attr.Exists())
90 window_attr = in_window_attr;
91 }
92
93 // -----------------------------------------------------------------------
94 // TextSystem
95 // -----------------------------------------------------------------------
TextSystem(System & system,Gameexe & gexe)96 TextSystem::TextSystem(System& system, Gameexe& gexe)
97 : auto_mode_(false),
98 ctrl_key_skip_(true),
99 fast_text_mode_(false),
100 message_no_wait_(false),
101 script_message_no_wait_(false),
102 active_window_(0),
103 is_reading_backlog_(false),
104 current_pageset_(),
105 in_pause_state_(false),
106 // #WINDOW_*_USE
107 move_use_(false),
108 clear_use_(false),
109 read_jump_use_(false),
110 automode_use_(false),
111 msgbk_use_(false),
112 msgbkleft_use_(false),
113 msgbkright_use_(false),
114 exbtn_use_(false),
115 globals_(gexe),
116 system_visible_(true),
117 skip_mode_(false),
118 kidoku_read_(false),
119 in_selection_mode_(false),
120 system_(system) {
121 GameexeInterpretObject ctrl_use(gexe("CTRL_USE"));
122 if (ctrl_use.Exists())
123 ctrl_key_skip_ = ctrl_use;
124
125 CheckAndSetBool(gexe, "WINDOW_MOVE_USE", move_use_);
126 CheckAndSetBool(gexe, "WINDOW_CLEAR_USE", clear_use_);
127 CheckAndSetBool(gexe, "WINDOW_READJUMP_USE", read_jump_use_);
128 CheckAndSetBool(gexe, "WINDOW_AUTOMODE_USE", automode_use_);
129 CheckAndSetBool(gexe, "WINDOW_MSGBK_USE", msgbk_use_);
130 CheckAndSetBool(gexe, "WINDOW_MSGBKLEFT_USE", msgbkleft_use_);
131 CheckAndSetBool(gexe, "WINDOW_MSGBKRIGHT_USE", msgbkright_use_);
132 CheckAndSetBool(gexe, "WINDOW_EXBTN_USE", exbtn_use_);
133
134 // Iterate over all the NAMAE keys, which is a feature that Clannad English
135 // Edition uses to translate the Japanese names into English.
136 for (GameexeFilteringIterator it = gexe.filtering_begin("NAMAE");
137 it != gexe.filtering_end();
138 ++it) {
139 try {
140 // Data in the Gameexe.ini file is implicitly in Shift-JIS and needs to
141 // be converted to UTF-8.
142 std::string key = cp932toUTF8(it->GetStringAt(0), 0);
143 std::string value = cp932toUTF8(it->GetStringAt(1), 0);
144 namae_mapping_[key] = value;
145 }
146 catch (...) {
147 // Gameexe.ini file is malformed.
148 }
149 }
150 previous_page_it_ = previous_page_sets_.end();
151 }
152
~TextSystem()153 TextSystem::~TextSystem() {}
154
ExecuteTextSystem()155 void TextSystem::ExecuteTextSystem() {
156 // Check to see if the cursor is displayed
157 if (ShowWindow(active_window_)) {
158 WindowMap::iterator it = text_window_.find(active_window_);
159 if (it != text_window_.end() && it->second->is_visible() &&
160 in_pause_state_ && !IsReadingBacklog()) {
161 if (!text_key_cursor_)
162 SetKeyCursor(0);
163
164 text_key_cursor_->Execute();
165 }
166 }
167
168 // Let each window update any TextWindowButton s.
169 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
170 ++it) {
171 if (ShowWindow(it->first)) {
172 it->second->Execute();
173 }
174 }
175 }
176
Render(std::ostream * tree)177 void TextSystem::Render(std::ostream* tree) {
178 if (system_visible()) {
179 if (tree) {
180 *tree << "Text System:" << endl;
181 }
182
183 for (WindowMap::iterator it = text_window_.begin();
184 it != text_window_.end();
185 ++it) {
186 if (ShowWindow(it->first)) {
187 it->second->Render(tree);
188 }
189 }
190
191 if (ShowWindow(active_window_)) {
192 WindowMap::iterator it = text_window_.find(active_window_);
193
194 if (it != text_window_.end() && it->second->is_visible() &&
195 in_pause_state_ && !IsReadingBacklog()) {
196 if (!text_key_cursor_)
197 SetKeyCursor(0);
198
199 text_key_cursor_->Render(*it->second, tree);
200 }
201 }
202 }
203 }
204
HideTextWindow(int win_number)205 void TextSystem::HideTextWindow(int win_number) {
206 WindowMap::iterator it = text_window_.find(win_number);
207 if (it != text_window_.end()) {
208 it->second->set_is_visible(0);
209 }
210 }
211
HideAllTextWindows()212 void TextSystem::HideAllTextWindows() {
213 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
214 ++it) {
215 it->second->set_is_visible(0);
216 }
217 }
218
HideAllTextWindowsExcept(int i)219 void TextSystem::HideAllTextWindowsExcept(int i) {
220 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
221 ++it) {
222 if (it->first != i) {
223 it->second->set_is_visible(0);
224 }
225 }
226 }
227
ShowTextWindow(int win_number)228 void TextSystem::ShowTextWindow(int win_number) {
229 WindowMap::iterator it = text_window_.find(win_number);
230 if (it != text_window_.end()) {
231 it->second->set_is_visible(1);
232 }
233 }
234
ShowAllTextWindows()235 void TextSystem::ShowAllTextWindows() {
236 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
237 ++it) {
238 it->second->set_is_visible(1);
239 }
240 }
241
ClearAllTextWindows()242 void TextSystem::ClearAllTextWindows() {
243 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
244 ++it) {
245 it->second->ClearWin();
246 }
247 }
248
SetVisualOverride(int win_number,bool show_window)249 void TextSystem::SetVisualOverride(int win_number, bool show_window) {
250 window_visual_override_[win_number] = show_window;
251 }
252
SetVisualOverrideAll(bool show_window)253 void TextSystem::SetVisualOverrideAll(bool show_window) {
254 for (int i = 0; i < 64; ++i) {
255 window_visual_override_[i] = show_window;
256 }
257 }
258
ClearVisualOverrides()259 void TextSystem::ClearVisualOverrides() { window_visual_override_.clear(); }
260
GetCurrentWindow()261 std::shared_ptr<TextWindow> TextSystem::GetCurrentWindow() {
262 return GetTextWindow(active_window_);
263 }
264
CheckAndSetBool(Gameexe & gexe,const std::string & key,bool & out)265 void TextSystem::CheckAndSetBool(Gameexe& gexe,
266 const std::string& key,
267 bool& out) {
268 GameexeInterpretObject key_obj(gexe(key));
269 if (key_obj.Exists())
270 out = key_obj.ToInt();
271 }
272
ExpireOldPages()273 void TextSystem::ExpireOldPages() {
274 while (previous_page_sets_.size() > MAX_PAGE_HISTORY)
275 previous_page_sets_.pop_front();
276 }
277
MouseButtonStateChanged(MouseButton mouse_button,bool pressed)278 bool TextSystem::MouseButtonStateChanged(MouseButton mouse_button,
279 bool pressed) {
280 if (CurrentlySkipping() && !in_selection_mode_) {
281 SetSkipMode(false);
282 return true;
283 }
284
285 return false;
286 }
287
KeyStateChanged(KeyCode key_code,bool pressed)288 bool TextSystem::KeyStateChanged(KeyCode key_code, bool pressed) {
289 if (CurrentlySkipping() && !in_selection_mode_) {
290 SetSkipMode(false);
291 return true;
292 }
293
294 return false;
295 }
296
GetActiveWindows()297 vector<int> TextSystem::GetActiveWindows() {
298 vector<int> tmp;
299 for (PageSet::iterator it = current_pageset_.begin();
300 it != current_pageset_.end();
301 ++it) {
302 tmp.push_back(it->first);
303 }
304 return tmp;
305 }
306
Snapshot()307 void TextSystem::Snapshot() {
308 bool all_empty = std::all_of(
309 current_pageset_.begin(),
310 current_pageset_.end(),
311 [&](std::pair<const int, TextPage>& rhs) { return rhs.second.empty(); });
312
313 if (!all_empty) {
314 previous_page_sets_.push_back(current_pageset_);
315 ExpireOldPages();
316 }
317 }
318
NewPageOnWindow(int window)319 void TextSystem::NewPageOnWindow(int window) {
320 // Erase the current instance of this window if it exists
321 PageSet::iterator it = current_pageset_.find(window);
322 if (it != current_pageset_.end()) {
323 current_pageset_.erase(it);
324 }
325
326 previous_page_it_ = previous_page_sets_.end();
327 current_pageset_.emplace(window, TextPage(system(), window));
328 ExpireOldPages();
329 }
330
GetCurrentPage()331 TextPage& TextSystem::GetCurrentPage() {
332 // Check to see if the active window has a current page.
333 PageSet::iterator it = current_pageset_.find(active_window_);
334 if (it == current_pageset_.end())
335 it = current_pageset_.emplace(active_window_,
336 TextPage(system(), active_window_)).first;
337
338 return it->second;
339 }
340
BackPage()341 void TextSystem::BackPage() {
342 is_reading_backlog_ = true;
343
344 if (previous_page_it_ != previous_page_sets_.begin()) {
345 previous_page_it_ = std::prev(previous_page_it_);
346
347 // Clear all windows
348 ClearAllTextWindows();
349 HideAllTextWindows();
350
351 ReplayPageSet(*previous_page_it_, false);
352 }
353 }
354
ForwardPage()355 void TextSystem::ForwardPage() {
356 is_reading_backlog_ = true;
357
358 if (previous_page_it_ != previous_page_sets_.end()) {
359 previous_page_it_ = std::next(previous_page_it_);
360
361 // Clear all windows
362 ClearAllTextWindows();
363 HideAllTextWindows();
364
365 if (previous_page_it_ != previous_page_sets_.end())
366 ReplayPageSet(*previous_page_it_, false);
367 else
368 ReplayPageSet(current_pageset_, false);
369 }
370 }
371
ReplayPageSet(PageSet & set,bool is_current_page)372 void TextSystem::ReplayPageSet(PageSet& set, bool is_current_page) {
373 for (PageSet::iterator it = set.begin(); it != set.end(); ++it) {
374 try {
375 it->second.Replay(is_current_page);
376 }
377 catch (rlvm::Exception& e) {
378 // Currently, the text system can throw on a few unimplemented situations,
379 // such as ruby across lines.
380
381 // Ignore what would normally be an ignored command when encountered from
382 // the main loop.
383 }
384 }
385 }
386
IsReadingBacklog() const387 bool TextSystem::IsReadingBacklog() const { return is_reading_backlog_; }
388
StopReadingBacklog()389 void TextSystem::StopReadingBacklog() {
390 is_reading_backlog_ = false;
391
392 // Clear all windows
393 ClearAllTextWindows();
394 HideAllTextWindows();
395 ReplayPageSet(current_pageset_, true);
396 }
397
InterpretName(const std::string & utf8name)398 std::string TextSystem::InterpretName(const std::string& utf8name) {
399 auto it = namae_mapping_.find(utf8name);
400 if (it == namae_mapping_.end())
401 return utf8name;
402 return it->second;
403 }
404
SetAutoMode(int i)405 void TextSystem::SetAutoMode(int i) {
406 auto_mode_ = (bool)i;
407
408 NotificationService::current()->Notify(
409 NotificationType::AUTO_MODE_STATE_CHANGED,
410 Source<TextSystem>(this),
411 Details<const int>(&i));
412 }
413
GetAutoTime(int num_chars)414 int TextSystem::GetAutoTime(int num_chars) {
415 return globals_.auto_mode_base_time +
416 globals_.auto_mode_char_time * num_chars;
417 }
418
SetKeyCursor(int new_cursor)419 void TextSystem::SetKeyCursor(int new_cursor) {
420 if (new_cursor == -1) {
421 text_key_cursor_.reset();
422 } else if (!text_key_cursor_ ||
423 text_key_cursor_->cursor_number() != new_cursor) {
424 text_key_cursor_.reset(new TextKeyCursor(system(), new_cursor));
425 }
426 }
427
GetCursorNumber() const428 int TextSystem::GetCursorNumber() const {
429 if (text_key_cursor_)
430 return text_key_cursor_->cursor_number();
431 else
432 return -1;
433 }
434
UpdateWindowsForChangeToWindowAttr()435 void TextSystem::UpdateWindowsForChangeToWindowAttr() {
436 // Check each text window to see if it needs updating
437 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
438 ++it) {
439 if (!it->second->windowAttrMod())
440 it->second->SetRGBAF(window_attr());
441 }
442 }
443
ShowWindow(int win_num) const444 bool TextSystem::ShowWindow(int win_num) const {
445 std::map<int, bool>::const_iterator it =
446 window_visual_override_.find(win_num);
447 if (it != window_visual_override_.end())
448 return it->second;
449 else
450 return true;
451 }
452
SetDefaultWindowAttr(const std::vector<int> & attr)453 void TextSystem::SetDefaultWindowAttr(const std::vector<int>& attr) {
454 globals_.window_attr = attr;
455 UpdateWindowsForChangeToWindowAttr();
456 }
457
SetWindowAttrR(int i)458 void TextSystem::SetWindowAttrR(int i) {
459 globals_.window_attr.at(0) = i;
460 UpdateWindowsForChangeToWindowAttr();
461 }
462
SetWindowAttrG(int i)463 void TextSystem::SetWindowAttrG(int i) {
464 globals_.window_attr.at(1) = i;
465 UpdateWindowsForChangeToWindowAttr();
466 }
467
SetWindowAttrB(int i)468 void TextSystem::SetWindowAttrB(int i) {
469 globals_.window_attr.at(2) = i;
470 UpdateWindowsForChangeToWindowAttr();
471 }
472
SetWindowAttrA(int i)473 void TextSystem::SetWindowAttrA(int i) {
474 globals_.window_attr.at(3) = i;
475 UpdateWindowsForChangeToWindowAttr();
476 }
477
SetWindowAttrF(int i)478 void TextSystem::SetWindowAttrF(int i) {
479 globals_.window_attr.at(4) = i;
480 UpdateWindowsForChangeToWindowAttr();
481 }
482
SetMousePosition(const Point & pos)483 void TextSystem::SetMousePosition(const Point& pos) {
484 for (WindowMap::iterator it = text_window_.begin(); it != text_window_.end();
485 ++it) {
486 if (ShowWindow(it->first)) {
487 it->second->SetMousePosition(pos);
488 }
489 }
490 }
491
HandleMouseClick(RLMachine & machine,const Point & pos,bool pressed)492 bool TextSystem::HandleMouseClick(RLMachine& machine,
493 const Point& pos,
494 bool pressed) {
495 if (system_visible()) {
496 for (WindowMap::iterator it = text_window_.begin();
497 it != text_window_.end();
498 ++it) {
499 if (ShowWindow(it->first)) {
500 if (it->second->HandleMouseClick(machine, pos, pressed))
501 return true;
502 }
503 }
504 }
505
506 return false;
507 }
508
TakeSavepointSnapshot()509 void TextSystem::TakeSavepointSnapshot() {
510 savepoint_active_window_ = active_window();
511 savepoint_cursor_number_ = GetCursorNumber();
512 }
513
parseInteger(std::string::const_iterator & begin,std::string::const_iterator end,int & out_value)514 bool parseInteger(std::string::const_iterator& begin,
515 std::string::const_iterator end,
516 int& out_value) {
517 std::string::const_iterator str_begin = begin;
518
519 if (begin != end && *begin == '-')
520 begin++;
521 while (begin != end && *begin >= '0' && *begin <= '9')
522 begin++;
523
524 if (str_begin == begin)
525 return false;
526
527 out_value = std::stoi(std::string(str_begin, begin));
528 return true;
529 }
530
RenderText(const std::string & utf8str,int size,int xspace,int yspace,const RGBColour & colour,RGBColour * shadow_colour,int max_chars_in_line)531 std::shared_ptr<Surface> TextSystem::RenderText(const std::string& utf8str,
532 int size,
533 int xspace,
534 int yspace,
535 const RGBColour& colour,
536 RGBColour* shadow_colour,
537 int max_chars_in_line) {
538 const int line_max_width =
539 (max_chars_in_line > 0) ? (size + xspace) * max_chars_in_line : INT_MAX;
540
541 // On the first pass, we figure out how large of a surface we need for
542 // rendering the text.
543 int current_size = size;
544 int total_height = 0;
545 int max_width = 0;
546 int current_line_width = 0;
547 int current_line_height = 0;
548 bool should_break = false;
549 std::string::const_iterator it = utf8str.begin();
550 std::string::const_iterator strend = utf8str.end();
551 while (it != strend) {
552 int codepoint = utf8::next(it, strend);
553 bool add_char = true;
554 bool is_emoji = false;
555
556 if (codepoint == '#') {
557 add_char = false;
558 codepoint = utf8::next(it, strend);
559 switch (codepoint) {
560 case 'D':
561 case 'd': {
562 should_break = true;
563 break;
564 }
565 case 'S':
566 case 's': {
567 int val;
568 if (parseInteger(it, strend, val)) {
569 current_size = val;
570 } else {
571 current_size = size;
572 }
573 break;
574 }
575 case 'C':
576 case 'c': {
577 // Consume an integer. Or don't.
578 int val;
579 parseInteger(it, strend, val);
580 }
581 case 'X':
582 case 'x':
583 case 'Y':
584 case 'y':
585 case '#':
586 break;
587 default: {
588 // Ooops. This isn't a control code.
589 add_char = true;
590 }
591 }
592 } else if (codepoint == FULLWIDTH_NUMBER_SIGN) {
593 // The codepoint is a fullwidth '#'. If the codepoint after this is a
594 // fullwidth 'A' or 'B', followed by two fullwidth digits, we have an
595 // emoji code, and should treat it like a fullwidth space during the
596 // allocation phase.
597 std::string::const_iterator n = it;
598 int next_codepoint = utf8::next(n, strend);
599 if (next_codepoint == FULLWIDTH_A || next_codepoint == FULLWIDTH_B) {
600 int num_one = utf8::next(n, strend);
601 int num_two = utf8::next(n, strend);
602 if (num_one >= FULLWIDTH_ZERO && num_one <= FULLWIDTH_NINE &&
603 num_two >= FULLWIDTH_ZERO && num_two <= FULLWIDTH_NINE) {
604 // This is an emoji mark. We should consume all input so far.
605 it = n;
606 is_emoji = true;
607 }
608 }
609 }
610
611 int added_width = 0;
612 if (add_char)
613 added_width = GetCharWidth(current_size, codepoint) + xspace;
614 else if (is_emoji)
615 added_width = size + xspace;
616
617 if (added_width) {
618 if (should_break || current_line_width + added_width > line_max_width) {
619 total_height += current_line_height + yspace;
620 current_line_height = current_size;
621 current_line_width = added_width;
622 should_break = false;
623 } else {
624 current_line_width += added_width;
625 current_line_height = std::max(current_line_height, current_size);
626 }
627
628 max_width = std::max(max_width, current_line_width);
629 }
630 }
631 total_height += current_line_height;
632 should_break = false;
633
634 // If this text has a shadow, our surface needs to have a final two pixels
635 // added to the bottom and right to accommodate it.
636 if (shadow_colour) {
637 total_height += 2;
638 max_width += 2;
639 }
640
641 // If we have an empty width or height, make sure we create a non-empty
642 // surface to render onto.
643 if (max_width == 0)
644 max_width = 1;
645 if (total_height == 0)
646 total_height = 1;
647
648 // TODO(erg): Surely there's a way to allocate with something other than
649 // black, right?
650 std::shared_ptr<Surface> surface(
651 system().graphics().BuildSurface(Size(max_width, total_height)));
652 surface->Fill(RGBAColour::Clear());
653
654 RGBColour current_colour = colour;
655 int currentX = 0;
656 int currentY = 0;
657 current_size = size;
658 current_line_height = 0;
659 it = utf8str.begin();
660 std::string::const_iterator cur_end = it;
661 while (it != strend) {
662 int codepoint = utf8::next(cur_end, strend);
663 std::string character(it, cur_end);
664 bool add_char = true;
665 bool is_emoji = false;
666 int emoji_id = 0;
667 std::shared_ptr<const Surface> emoji_surface;
668
669 if (codepoint == '#') {
670 add_char = false;
671 codepoint = utf8::next(cur_end, strend);
672 switch (codepoint) {
673 case 'D':
674 case 'd': {
675 should_break = true;
676 break;
677 }
678 case 'S':
679 case 's': {
680 int val;
681 if (parseInteger(cur_end, strend, val)) {
682 current_size = val;
683 } else {
684 current_size = size;
685 }
686 break;
687 }
688 case 'C':
689 case 'c': {
690 // Consume an integer. Or don't.
691 int val;
692 if (parseInteger(cur_end, strend, val)) {
693 Gameexe& gexe = system().gameexe();
694 current_colour = RGBColour(gexe("COLOR_TABLE", val));
695 } else {
696 current_colour = colour;
697 }
698 }
699 case 'X':
700 case 'x':
701 case 'Y':
702 case 'y':
703 case '#':
704 break;
705 default: {
706 // Ooops. This isn't a control code.
707 add_char = true;
708 }
709 }
710 } else if (codepoint == FULLWIDTH_NUMBER_SIGN) {
711 // The codepoint is a fullwidth '#'. If the codepoint after this is a
712 // fullwidth 'A' or 'B', followed by two fullwidth digits, we have an
713 // emoji code, and should treat it like a fullwidth space during the
714 // allocation phase.
715 std::string::const_iterator n = cur_end;
716 int next_codepoint = utf8::next(n, strend);
717 if (next_codepoint == FULLWIDTH_A || next_codepoint == FULLWIDTH_B) {
718 int num_one = utf8::next(n, strend);
719 int num_two = utf8::next(n, strend);
720 if (num_one >= FULLWIDTH_ZERO && num_one <= FULLWIDTH_NINE &&
721 num_two >= FULLWIDTH_ZERO && num_two <= FULLWIDTH_NINE) {
722 // This is an emoji mark. We should consume all input so far.
723 cur_end = n;
724
725 // Get the emoji number from the text.
726 num_one -= FULLWIDTH_ZERO;
727 num_two -= FULLWIDTH_ZERO;
728 emoji_id = num_one * 10 + num_two;
729
730 // Look up what g00 surface we should use for emoji.
731 emoji_surface = system().graphics().GetEmojiSurface();
732 is_emoji = true;
733 add_char = false;
734 }
735 }
736 }
737
738 int item_width = 0;
739 if (add_char) {
740 // If we add this character, will we horizontally overflow?
741 item_width = GetCharWidth(current_size, codepoint);
742 } else if (is_emoji) {
743 // Whatever the real size is, we only allocate the incoming size. This
744 // means that if the emoji is larger than |size|, we'll draw text over
745 // it. This is to be bug for bug compatible with RealLive.
746 item_width = size;
747 }
748
749 if (item_width) {
750 if (should_break || currentX + item_width > max_width) {
751 currentX = 0;
752 currentY += current_line_height + yspace;
753 current_line_height = 0;
754 should_break = false;
755 }
756 }
757
758 if (add_char) {
759 Size s = RenderGlyphOnto(character,
760 current_size,
761 false,
762 current_colour,
763 shadow_colour,
764 currentX,
765 currentY,
766 surface);
767 currentX += s.width() + xspace;
768 current_line_height = std::max(current_line_height, current_size);
769 } else if (is_emoji) {
770 if (emoji_surface) {
771 // Emoji surfaces don't have internal pattnos. Instead, assume that
772 // icons are square and laid out to the right, sort of like mouse
773 // cursors.
774 int height = emoji_surface->GetSize().height();
775 Rect src = Rect(height * emoji_id, 0, Size(height, height));
776 Rect dst = Rect(currentX, currentY, Size(height, height));
777 emoji_surface->BlitToSurface(*surface, src, dst, 255, false);
778 }
779
780 currentX += size + xspace;
781 current_line_height = std::max(current_line_height, current_size);
782 }
783
784 it = cur_end;
785 }
786
787 return surface;
788 }
789
Reset()790 void TextSystem::Reset() {
791 is_reading_backlog_ = false;
792 script_message_no_wait_ = false;
793
794 current_pageset_.clear();
795 previous_page_sets_.clear();
796 previous_page_it_ = previous_page_sets_.end();
797
798 window_visual_override_.clear();
799 text_window_.clear();
800
801 system_visible_ = true;
802 in_pause_state_ = false;
803 in_selection_mode_ = false;
804 kidoku_read_ = false;
805 skip_mode_ = false;
806 }
807
SetKidokuRead(const int in)808 void TextSystem::SetKidokuRead(const int in) {
809 bool value_changed = kidoku_read_ != in;
810
811 kidoku_read_ = in;
812 NotificationService::current()->Notify(
813 NotificationType::SKIP_MODE_ENABLED_CHANGED,
814 Source<TextSystem>(this),
815 Details<const int>(&in));
816
817 if (value_changed && !kidoku_read_ && skip_mode_) {
818 // Auto leave skip mode when we stop reading previously read text.
819 SetSkipMode(false);
820 }
821 }
822
SetSkipMode(int in)823 void TextSystem::SetSkipMode(int in) {
824 bool value_changed = skip_mode_ != in;
825 skip_mode_ = in;
826
827 if (value_changed) {
828 NotificationService::current()->Notify(
829 NotificationType::SKIP_MODE_STATE_CHANGED,
830 Source<TextSystem>(this),
831 Details<int>(&in));
832 }
833 }
834
835 template <class Archive>
load(Archive & ar,unsigned int version)836 void TextSystem::load(Archive& ar, unsigned int version) {
837 int win, cursor_num;
838 ar& win& cursor_num;
839
840 set_active_window(win);
841 SetKeyCursor(cursor_num);
842 }
843
844 template <class Archive>
save(Archive & ar,unsigned int version) const845 void TextSystem::save(Archive& ar, unsigned int version) const {
846 ar& savepoint_active_window_& savepoint_cursor_number_;
847 }
848
849 // -----------------------------------------------------------------------
850
851 // Explicit instantiations for text archives (since we hide the
852 // implementation)
853
854 template void TextSystem::save<boost::archive::text_oarchive>(
855 boost::archive::text_oarchive& ar,
856 unsigned int version) const;
857
858 template void TextSystem::load<boost::archive::text_iarchive>(
859 boost::archive::text_iarchive& ar,
860 unsigned int version);
861
862 // -----------------------------------------------------------------------
863
parseNames(const Memory & memory,const std::string & input,std::string & output)864 void parseNames(const Memory& memory,
865 const std::string& input,
866 std::string& output) {
867 const char* cur = input.c_str();
868
869 const char LOWER_BYTE_FULLWIDTH_ASTERISK = 0x96;
870 const char LOWER_BYTE_FULLWIDTH_PERCENT = 0x93;
871
872 while (*cur) {
873 if (cur[0] == 0x81 && (cur[1] == LOWER_BYTE_FULLWIDTH_ASTERISK ||
874 cur[1] == LOWER_BYTE_FULLWIDTH_PERCENT)) {
875 char type = cur[1];
876 cur += 2;
877
878 string strindex;
879 if (ReadFullwidthLatinLetter(cur, strindex)) {
880 // Try to read a second character. We don't care if it fails.
881 ReadFullwidthLatinLetter(cur, strindex);
882 } else {
883 // It was "as-is" character, not a name reference.
884 output += (char)0x81;
885 output += type;
886 continue;
887 }
888
889 int index = Memory::ConvertLetterIndexToInt(strindex);
890 if (type == LOWER_BYTE_FULLWIDTH_ASTERISK)
891 output += memory.GetName(index);
892 else
893 output += memory.GetLocalName(index);
894 } else {
895 CopyOneShiftJisCharacter(cur, output);
896 }
897 }
898 }
899
CurrentlySkipping() const900 bool TextSystem::CurrentlySkipping() const {
901 return kidoku_read_ && skip_mode();
902 }
903
operator ()(RLMachine & machine)904 bool RestoreTextSystemVisibility::operator()(RLMachine& machine) {
905 machine.system().text().set_system_visible(true);
906 return true;
907 }
908