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