1 #include "AppHdr.h"
2 
3 #ifdef USE_TILE_WEB
4 
5 #include "tileweb.h"
6 
7 #include <cerrno>
8 #include <cstdarg>
9 
10 #include <sys/socket.h>
11 #include <sys/time.h>
12 #include <sys/types.h>
13 #include <sys/un.h>
14 #if defined(UNIX) || defined(TARGET_COMPILER_MINGW)
15 #include <unistd.h>
16 #endif
17 
18 #include "artefact.h"
19 #include "branch.h"
20 #include "command.h"
21 #include "coord.h"
22 #include "database.h"
23 #include "directn.h"
24 #include "english.h"
25 #include "env.h"
26 #include "files.h"
27 #include "item-name.h"
28 #include "item-prop.h" // is_weapon()
29 #include "json.h"
30 #include "json-wrapper.h"
31 #include "lang-fake.h"
32 #include "libutil.h"
33 #include "macro.h"
34 #include "map-knowledge.h"
35 #include "menu.h"
36 #include "outer-menu.h"
37 #include "message.h"
38 #include "mon-util.h"
39 #include "notes.h"
40 #include "options.h"
41 #include "player.h"
42 #include "player-equip.h"
43 #include "religion.h"
44 #include "scroller.h"
45 #include "skills.h"
46 #include "state.h"
47 #include "stringutil.h"
48 #include "throw.h"
49 #include "tile-flags.h"
50 #include "tile-player-flag-cut.h"
51 #include "rltiles/tiledef-dngn.h"
52 #include "rltiles/tiledef-gui.h"
53 #include "rltiles/tiledef-icons.h"
54 #include "rltiles/tiledef-main.h"
55 #include "rltiles/tiledef-player.h"
56 #include "tilepick.h"
57 #include "tilepick-p.h"
58 #include "tileview.h"
59 #include "transform.h"
60 #include "travel.h"
61 #include "ui.h"
62 #include "unicode.h"
63 #include "unwind.h"
64 #include "version.h"
65 #include "viewgeom.h"
66 #include "view.h"
67 
68 //#define DEBUG_WEBSOCKETS
69 
get_milliseconds()70 static unsigned int get_milliseconds()
71 {
72     // This is Unix-only, but so is Webtiles at the moment.
73     timeval tv;
74     gettimeofday(&tv, nullptr);
75 
76     return ((unsigned int) tv.tv_sec) * 1000 + tv.tv_usec / 1000;
77 }
78 
79 TilesFramework tiles;
80 
TilesFramework()81 TilesFramework::TilesFramework() :
82       m_controlled_from_web(false),
83       _send_lock(false),
84       m_last_ui_state(UI_INIT),
85       m_view_loaded(false),
86       m_current_view(coord_def(GXM, GYM)),
87       m_next_view(coord_def(GXM, GYM)),
88       m_next_view_tl(0, 0),
89       m_next_view_br(-1, -1),
90       m_current_flash_colour(BLACK),
91       m_next_flash_colour(BLACK),
92       m_need_full_map(true),
93       m_text_menu("menu_txt"),
94       m_print_fg(15)
95 {
96     screen_cell_t default_cell;
97     default_cell.tile.bg = TILE_FLAG_UNSEEN;
98     m_current_view.fill(default_cell);
99     m_next_view.fill(default_cell);
100 }
101 
~TilesFramework()102 TilesFramework::~TilesFramework()
103 {
104 }
105 
shutdown()106 void TilesFramework::shutdown()
107 {
108     if (m_sock_name.empty())
109         return;
110 
111     close(m_sock);
112     remove(m_sock_name.c_str());
113 }
114 
draw_doll_edit()115 void TilesFramework::draw_doll_edit()
116 {
117 }
118 
initialise()119 bool TilesFramework::initialise()
120 {
121     m_cursor[CURSOR_MOUSE] = NO_CURSOR;
122     m_cursor[CURSOR_TUTORIAL] = NO_CURSOR;
123     m_cursor[CURSOR_MAP] = NO_CURSOR;
124 
125     // Initially, switch to CRT.
126     cgotoxy(1, 1, GOTO_CRT);
127 
128     if (m_sock_name.empty())
129         return true;
130 
131     // Init socket
132     m_sock = socket(PF_UNIX, SOCK_DGRAM, 0);
133     if (m_sock < 0)
134         die("Can't open the webtiles socket!");
135     sockaddr_un addr;
136     addr.sun_family = AF_UNIX;
137     strcpy(addr.sun_path, m_sock_name.c_str());
138     if (::bind(m_sock, (sockaddr*) &addr, sizeof(sockaddr_un)))
139         die("Can't bind the webtiles socket!");
140 
141     int bufsize = 64 * 1024;
142     if (setsockopt(m_sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)))
143         die("Can't set buffer size!");
144     // Need small maximum message size to avoid crashes in OS X
145     m_max_msg_size = 2048;
146 
147     struct timeval tv;
148     tv.tv_sec = 1;
149     tv.tv_usec = 0;
150     if (setsockopt(m_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0)
151         die("Can't set send timeout!");
152 
153     if (m_await_connection)
154         _await_connection();
155 
156     _send_version();
157     send_exit_reason("unknown");
158     _send_options();
159     _send_layout();
160 
161     return true;
162 }
163 
get_message()164 string TilesFramework::get_message()
165 {
166     return m_msg_buf;
167 }
168 
write_message(const char * format,...)169 void TilesFramework::write_message(const char *format, ...)
170 {
171     char buf[2048];
172     int len;
173 
174     va_list argp;
175     va_start(argp, format);
176     if ((len = vsnprintf(buf, sizeof(buf), format, argp)) < 0)
177         die("Webtiles message format error! (%s)", format);
178     else if (len >= (int)sizeof(buf))
179         die("Webtiles message too long! (%d)", len);
180     va_end(argp);
181 
182     m_msg_buf.append(buf);
183 }
184 
finish_message()185 void TilesFramework::finish_message()
186 {
187     if (m_msg_buf.size() == 0)
188         return;
189 #ifdef DEBUG_WEBSOCKETS
190     const int initial_buf_size = m_msg_buf.size();
191     fprintf(stderr, "websocket: About to send %d bytes.\n", initial_buf_size);
192 #endif
193 
194     if (m_sock_name.empty())
195     {
196         m_msg_buf.clear();
197         return;
198     }
199 
200     m_msg_buf.append("\n");
201     const char* fragment_start = m_msg_buf.data();
202     const char* data_end = m_msg_buf.data() + m_msg_buf.size();
203     int fragments = 0;
204     while (fragment_start < data_end)
205     {
206         int fragment_size = data_end - fragment_start;
207         if (fragment_size > m_max_msg_size)
208             fragment_size = m_max_msg_size;
209         fragments++;
210 
211         for (unsigned int i = 0; i < m_dest_addrs.size(); ++i)
212         {
213             int retries = 30;
214             ssize_t sent = 0;
215             while (sent < fragment_size)
216             {
217                 ssize_t retval = sendto(m_sock, fragment_start + sent,
218                     fragment_size - sent, 0, (sockaddr*) &m_dest_addrs[i],
219                     sizeof(sockaddr_un));
220 #ifdef DEBUG_WEBSOCKETS
221                 fprintf(stderr,
222                             "    trying to send fragment to client %d...", i);
223 #endif
224                 if (retval <= 0)
225                 {
226                     const char *errmsg = retval == 0 ? "No bytes sent"
227                                                      : strerror(errno);
228                     if (--retries <= 0)
229                         die("Socket write error: %s", errmsg);
230 
231                     if (retval == 0 || errno == ENOBUFS || errno == EWOULDBLOCK
232                         || errno == EINTR || errno == EAGAIN)
233                     {
234                         // Wait for half a second at first (up to five), then
235                         // try again.
236                         const int sleep_time = retries > 25 ? 2 * 1000
237                                              : retries > 10 ? 500 * 1000
238                                              : 5000 * 1000;
239 #ifdef DEBUG_WEBSOCKETS
240                         fprintf(stderr, "failed (%s), sleeping for %dms.\n",
241                                                     errmsg, sleep_time / 1000);
242 #endif
243                         usleep(sleep_time);
244                     }
245                     else if (errno == ECONNREFUSED || errno == ENOENT)
246                     {
247                         // the other side is dead
248 #ifdef DEBUG_WEBSOCKETS
249                         fprintf(stderr,
250                             "failed (%s), breaking.\n", errmsg);
251 #endif
252                         m_dest_addrs.erase(m_dest_addrs.begin() + i);
253                         i--;
254                         break;
255                     }
256                     else
257                         die("Socket write error: %s", errmsg);
258                 }
259                 else
260                 {
261 #ifdef DEBUG_WEBSOCKETS
262                     fprintf(stderr, "fragment size %d sent.\n", fragment_size);
263 #endif
264                     sent += retval;
265                 }
266             }
267         }
268 
269         fragment_start += fragment_size;
270     }
271     m_msg_buf.clear();
272     m_need_flush = true;
273 #ifdef DEBUG_WEBSOCKETS
274     // should the game actually crash in this case?
275     if (m_controlled_from_web && m_dest_addrs.size() == 0)
276         fprintf(stderr, "No open websockets after finish_message!!\n");
277 
278     fprintf(stderr, "websocket: Sent %d bytes in %d fragments.\n",
279                                                 initial_buf_size, fragments);
280 #endif
281 }
282 
send_message(const char * format,...)283 void TilesFramework::send_message(const char *format, ...)
284 {
285     char buf[2048];
286     int len;
287 
288     va_list argp;
289     va_start(argp, format);
290     if ((len = vsnprintf(buf, sizeof(buf), format, argp)) >= (int)sizeof(buf)
291         || len == -1)
292     {
293         if (len == -1)
294             die("Webtiles message format error! (%s)", format);
295         else
296             die("Webtiles message too long! (%d)", len);
297     }
298     va_end(argp);
299 
300     m_msg_buf.append(buf);
301 
302     finish_message();
303 }
304 
flush_messages()305 void TilesFramework::flush_messages()
306 {
307     if (_send_lock)
308         return;
309     unwind_bool no_rentry(_send_lock, true);
310 
311     if (m_need_flush)
312     {
313         send_message("*{\"msg\":\"flush_messages\"}");
314         m_need_flush = false;
315     }
316 }
317 
_await_connection()318 void TilesFramework::_await_connection()
319 {
320     if (m_sock_name.empty())
321         return;
322 
323     while (m_dest_addrs.size() == 0)
324         _receive_control_message();
325 }
326 
_receive_control_message()327 wint_t TilesFramework::_receive_control_message()
328 {
329     if (m_sock_name.empty())
330         return 0;
331 
332     char buf[4096]; // Should be enough for client->server messages
333     sockaddr_un srcaddr;
334     socklen_t srcaddr_len;
335     memset(&srcaddr, 0, sizeof(struct sockaddr_un));
336 
337     srcaddr_len = sizeof(srcaddr);
338 
339     int len = recvfrom(m_sock, buf, sizeof(buf),
340                        0,
341                        (sockaddr *) &srcaddr, &srcaddr_len);
342 
343     if (len == -1)
344         die("Socket read error: %s", strerror(errno));
345 
346     string data(buf, len);
347     try
348     {
349         return _handle_control_message(srcaddr, data);
350     }
351     catch (JsonWrapper::MalformedException&)
352     {
353         dprf("Malformed control message!");
354         return 0;
355     }
356 }
357 
_handle_control_message(sockaddr_un addr,string data)358 wint_t TilesFramework::_handle_control_message(sockaddr_un addr, string data)
359 {
360     JsonWrapper obj = json_decode(data.c_str());
361     obj.check(JSON_OBJECT);
362 
363     JsonWrapper msg = json_find_member(obj.node, "msg");
364     msg.check(JSON_STRING);
365     string msgtype(msg->string_);
366 #ifdef DEBUG_WEBSOCKETS
367     fprintf(stderr, "websocket: Received control message '%s' in %d byte.\n", msgtype.c_str(), (int) data.size());
368 #endif
369 
370     int c = 0;
371 
372     if (msgtype == "attach")
373     {
374         JsonWrapper primary = json_find_member(obj.node, "primary");
375         primary.check(JSON_BOOL);
376 
377         m_dest_addrs.push_back(addr);
378         m_controlled_from_web = primary->bool_;
379     }
380     else if (msgtype == "key")
381     {
382         JsonWrapper keycode = json_find_member(obj.node, "keycode");
383         keycode.check(JSON_NUMBER);
384 
385         // TODO: remove this fixup call
386         c = function_keycode_fixup((int) keycode->number_);
387     }
388     else if (msgtype == "spectator_joined")
389     {
390         flush_messages();
391         _send_everything();
392         flush_messages();
393     }
394     else if (msgtype == "menu_hover")
395     {
396         JsonWrapper hover = json_find_member(obj.node, "hover");
397         hover.check(JSON_NUMBER);
398 
399         if (!m_menu_stack.empty() && m_menu_stack.back().type == UIStackFrame::MENU)
400             m_menu_stack.back().menu->set_hovered((int) hover->number_);
401 
402     }
403     else if (msgtype == "menu_scroll")
404     {
405         JsonWrapper first = json_find_member(obj.node, "first");
406         first.check(JSON_NUMBER);
407         JsonWrapper hover = json_find_member(obj.node, "hover");
408         hover.check(JSON_NUMBER);
409         // last visible item is sent too, but currently unused
410 
411         if (!m_menu_stack.empty() && m_menu_stack.back().type == UIStackFrame::MENU)
412             m_menu_stack.back().menu->webtiles_scroll((int) first->number_, (int) hover->number_);
413     }
414     else if (msgtype == "*request_menu_range")
415     {
416         JsonWrapper start = json_find_member(obj.node, "start");
417         start.check(JSON_NUMBER);
418         JsonWrapper end = json_find_member(obj.node, "end");
419         end.check(JSON_NUMBER);
420 
421         if (!m_menu_stack.empty() && m_menu_stack.back().type == UIStackFrame::MENU)
422         {
423             m_menu_stack.back().menu->webtiles_handle_item_request((int) start->number_,
424                                                                    (int) end->number_);
425         }
426     }
427     else if (msgtype == "note")
428     {
429         JsonWrapper content = json_find_member(obj.node, "content");
430         content.check(JSON_STRING);
431 
432         if (Options.note_chat_messages)
433             take_note(Note(NOTE_MESSAGE, MSGCH_PLAIN, 0, content->string_));
434     }
435     else if (msgtype == "server_announcement")
436     {
437         JsonWrapper content = json_find_member(obj.node, "content");
438         content.check(JSON_STRING);
439         string m = "<red>Serverwide announcement:</red> ";
440         m += content->string_;
441 
442         mprf(MSGCH_DGL_MESSAGE, "%s", m.c_str());
443         // The following two lines are a magic incantation to get this mprf
444         // to actually render without waiting on player inout
445         flush_prev_message();
446         c = CK_REDRAW;
447     }
448     else if (msgtype == "click_travel" &&
449              mouse_control::current_mode() == MOUSE_MODE_COMMAND)
450     {
451         JsonWrapper x = json_find_member(obj.node, "x");
452         JsonWrapper y = json_find_member(obj.node, "y");
453         x.check(JSON_NUMBER);
454         y.check(JSON_NUMBER);
455         JsonWrapper force = json_find_member(obj.node, "force");
456 
457         coord_def gc = coord_def((int) x->number_, (int) y->number_) + m_origin;
458         c = click_travel(gc, force.node && force->tag == JSON_BOOL && force->bool_);
459         if (c != CK_MOUSE_CMD)
460         {
461             clear_messages();
462             process_command((command_type) c);
463         }
464         c = CK_MOUSE_CMD;
465     }
466     else if (msgtype == "formatted_scroller_scroll")
467     {
468         JsonWrapper scroll = json_find_member(obj.node, "scroll");
469         scroll.check(JSON_NUMBER);
470         recv_formatted_scroller_scroll((int)scroll->number_);
471     }
472     else if (msgtype == "outer_menu_focus")
473     {
474         JsonWrapper menu_id = json_find_member(obj.node, "menu_id");
475         JsonWrapper hotkey = json_find_member(obj.node, "hotkey");
476         menu_id.check(JSON_STRING);
477         hotkey.check(JSON_NUMBER);
478         OuterMenu::recv_outer_menu_focus(menu_id->string_, (int)hotkey->number_);
479     }
480     else if (msgtype == "ui_state_sync")
481         ui::recv_ui_state_change(obj.node);
482 
483     return c;
484 }
485 
await_input(wint_t & c,bool block)486 bool TilesFramework::await_input(wint_t& c, bool block)
487 {
488     int result;
489     fd_set fds;
490     int maxfd = m_sock_name.empty() ? STDIN_FILENO : m_sock;
491 
492     while (true)
493     {
494         do
495         {
496             FD_ZERO(&fds);
497             FD_SET(STDIN_FILENO, &fds);
498             if (!m_sock_name.empty())
499                 FD_SET(m_sock, &fds);
500 
501             if (block)
502             {
503                 tiles.flush_messages();
504                 result = select(maxfd + 1, &fds, nullptr, nullptr, nullptr);
505             }
506             else
507             {
508                 timeval timeout;
509                 timeout.tv_sec = 0;
510                 timeout.tv_usec = 0;
511 
512                 result = select(maxfd + 1, &fds, nullptr, nullptr, &timeout);
513             }
514         }
515         while (result == -1 && errno == EINTR);
516 
517         if (result == 0)
518             return false;
519         else if (result > 0)
520         {
521             if (!m_sock_name.empty() && FD_ISSET(m_sock, &fds))
522             {
523                 c = _receive_control_message();
524 
525                 if (c != 0)
526                     return true;
527             }
528 
529             if (FD_ISSET(STDIN_FILENO, &fds))
530             {
531                 c = 0;
532                 return true;
533             }
534         }
535         else if (errno == EBADF)
536         {
537             // This probably means that stdin got closed because of a
538             // SIGHUP. We'll just return.
539             c = 0;
540             return false;
541         }
542         else
543             die("select error: %s", strerror(errno));
544     }
545 }
546 
dump()547 void TilesFramework::dump()
548 {
549     fprintf(stderr, "Webtiles message buffer: %s\n", m_msg_buf.c_str());
550     fprintf(stderr, "Webtiles JSON stack:\n");
551     for (const JsonFrame &frame : m_json_stack)
552     {
553         fprintf(stderr, "start: %d end: %d type: %c\n",
554                 frame.start, frame.prefix_end, frame.type);
555     }
556 }
557 
send_exit_reason(const string & type,const string & message)558 void TilesFramework::send_exit_reason(const string& type, const string& message)
559 {
560     write_message("*");
561     write_message("{\"msg\":\"exit_reason\",\"type\":\"");
562     write_message_escaped(type);
563     if (!message.empty())
564     {
565         write_message("\",\"message\":\"");
566         write_message_escaped(message);
567     }
568     write_message("\"}");
569     finish_message();
570 }
571 
send_dump_info(const string & type,const string & filename)572 void TilesFramework::send_dump_info(const string& type, const string& filename)
573 {
574     write_message("*");
575     write_message("{\"msg\":\"dump\",\"type\":\"");
576     write_message_escaped(type);
577     write_message("\",\"filename\":\"");
578     write_message_escaped(strip_filename_unsafe_chars(filename));
579     write_message("\"}");
580     finish_message();
581 }
582 
_send_version()583 void TilesFramework::_send_version()
584 {
585 #ifdef WEB_DIR_PATH
586     // The star signals a message to the server
587     send_message("*{\"msg\":\"client_path\",\"path\":\"%s\",\"version\":\"%s\"}", WEB_DIR_PATH, Version::Long);
588 #endif
589 
590     string title = CRAWL " " + string(Version::Long);
591     send_message("{\"msg\":\"version\",\"text\":\"%s\"}", title.c_str());
592 }
593 
_send_options()594 void TilesFramework::_send_options()
595 {
596     json_open_object();
597     json_write_string("msg", "options");
598     Options.write_webtiles_options("options");
599     json_close_object();
600     finish_message();
601 }
602 
603 #define ZOOM_INC 10
604 
_set_option_int(string name,int value)605 static void _set_option_int(string name, int value)
606 {
607     tiles.json_open_object();
608     tiles.json_write_string("msg", "set_option");
609     tiles.json_write_string("name", name);
610     tiles.json_write_int("value", value);
611     tiles.json_close_object();
612     tiles.finish_message();
613 }
614 
zoom_dungeon(bool in)615 void TilesFramework::zoom_dungeon(bool in)
616 {
617     if (m_ui_state == UI_VIEW_MAP)
618     {
619         Options.tile_map_scale = min(300, max(20,
620                     Options.tile_map_scale + (in ? ZOOM_INC : -ZOOM_INC)));
621         _set_option_int("tile_map_scale", Options.tile_map_scale);
622         dprf("Zooming map to %d", Options.tile_map_scale);
623     }
624     else
625     {
626         Options.tile_viewport_scale = min(300, max(20,
627                     Options.tile_viewport_scale + (in ? ZOOM_INC : -ZOOM_INC)));
628         _set_option_int("tile_viewport_scale", Options.tile_viewport_scale);
629         dprf("Zooming to %d", Options.tile_viewport_scale);
630     }
631     // calling redraw explicitly is not needed here: it triggers from a
632     // listener on the webtiles side.
633     // TODO: how to implement dynamic max zoom that reacts to the webtiles side?
634 }
635 
_send_layout()636 void TilesFramework::_send_layout()
637 {
638     tiles.json_open_object();
639     tiles.json_write_string("msg", "layout");
640     tiles.json_open_object("message_pane");
641     tiles.json_write_int("height",
642                         max(Options.msg_webtiles_height, crawl_view.msgsz.y));
643     tiles.json_write_bool("small_more", Options.small_more);
644     tiles.json_close_object();
645     tiles.json_close_object();
646     tiles.finish_message();
647 }
648 
push_menu(Menu * m)649 void TilesFramework::push_menu(Menu* m)
650 {
651     UIStackFrame frame;
652     frame.type = UIStackFrame::MENU;
653     frame.menu = m;
654     frame.centred = !crawl_state.need_save;
655     m_menu_stack.push_back(frame);
656     m->webtiles_write_menu();
657     tiles.finish_message();
658 }
659 
push_crt_menu(string tag)660 void TilesFramework::push_crt_menu(string tag)
661 {
662     UIStackFrame frame;
663     frame.type = UIStackFrame::CRT;
664     frame.crt_tag = tag;
665     frame.centred = !crawl_state.need_save;
666     m_menu_stack.push_back(frame);
667 
668     json_open_object();
669     json_write_string("msg", "menu");
670     json_write_string("type", "crt");
671     json_write_string("tag", tag);
672     json_write_bool("ui-centred", frame.centred);
673     json_close_object();
674     finish_message();
675 }
676 
is_in_crt_menu()677 bool TilesFramework::is_in_crt_menu()
678 {
679     return !m_menu_stack.empty() && m_menu_stack.back().type == UIStackFrame::CRT;
680 }
681 
is_in_menu(Menu * m)682 bool TilesFramework::is_in_menu(Menu* m)
683 {
684     return !m_menu_stack.empty() && m_menu_stack.back().type == UIStackFrame::MENU
685         && m_menu_stack.back().menu == m;
686 }
687 
pop_menu()688 void TilesFramework::pop_menu()
689 {
690     if (m_menu_stack.empty()) return;
691     m_menu_stack.pop_back();
692     send_message("{\"msg\":\"close_menu\"}");
693 }
694 
pop_all_ui_layouts()695 void TilesFramework::pop_all_ui_layouts()
696 {
697     for (auto it = m_menu_stack.crbegin(); it != m_menu_stack.crend(); it++)
698     {
699         if (it->type == UIStackFrame::UI)
700             send_message("{\"msg\":\"ui-pop\"}");
701         else
702             send_message("{\"msg\":\"close_menu\"}");
703     }
704     m_menu_stack.clear();
705 
706     // This is a bit of a hack, in case the client-side menu stack ever gets
707     // out of sync with m_menu_stack. (This can maybe happen for reasons that I
708     // don't fully understand, on spectator join.)
709     send_message("{\"msg\":\"close_all_menus\"}");
710 }
711 
push_ui_layout(const string & type,unsigned num_state_slots)712 void TilesFramework::push_ui_layout(const string& type, unsigned num_state_slots)
713 {
714     ASSERT(m_json_stack.size() == 1);
715     ASSERT(m_json_stack.back().type == '}'); // enums, schmenums
716     tiles.json_write_string("msg", "ui-push");
717     tiles.json_write_string("type", type);
718     tiles.json_write_bool("ui-centred", !crawl_state.need_save);
719     tiles.json_write_int("generation_id", ui::layout_generation_id());
720     tiles.json_close_object();
721     UIStackFrame frame;
722     frame.type = UIStackFrame::UI;
723     frame.ui_json.resize(num_state_slots+1);
724     frame.ui_json[0] = m_msg_buf;
725     frame.centred = !crawl_state.need_save;
726     m_menu_stack.push_back(frame);
727     tiles.finish_message();
728 }
729 
pop_ui_layout()730 void TilesFramework::pop_ui_layout()
731 {
732     if (m_menu_stack.empty()) return;
733     m_menu_stack.pop_back();
734     send_message("{\"msg\":\"ui-pop\"}");
735 }
736 
ui_state_change(const string & type,unsigned state_slot)737 void TilesFramework::ui_state_change(const string& type, unsigned state_slot)
738 {
739     ASSERT(!m_menu_stack.empty());
740     UIStackFrame &top = m_menu_stack.back();
741     ASSERT(top.type == UIStackFrame::UI);
742     ASSERT(m_json_stack.size() == 1);
743     ASSERT(m_json_stack.back().type == '}');
744     tiles.json_write_string("msg", "ui-state");
745     tiles.json_write_string("type", type);
746     tiles.json_close_object();
747     ASSERT(state_slot + 1 < top.ui_json.size());
748     top.ui_json[state_slot+1] = m_msg_buf;
749     tiles.finish_message();
750 }
751 
push_ui_cutoff()752 void TilesFramework::push_ui_cutoff()
753 {
754     int cutoff = static_cast<int>(m_menu_stack.size());
755     m_ui_cutoff_stack.push_back(cutoff);
756     send_message("{\"msg\":\"ui_cutoff\",\"cutoff\":%d}", cutoff);
757 }
758 
pop_ui_cutoff()759 void TilesFramework::pop_ui_cutoff()
760 {
761     m_ui_cutoff_stack.pop_back();
762     int cutoff = m_ui_cutoff_stack.empty() ? -1 : m_ui_cutoff_stack.back();
763     send_message("{\"msg\":\"ui_cutoff\",\"cutoff\":%d}", cutoff);
764 }
765 
_send_text_cursor(bool enabled)766 static void _send_text_cursor(bool enabled)
767 {
768     tiles.send_message("{\"msg\":\"text_cursor\",\"enabled\":%s}",
769                        enabled ? "true" : "false");
770 }
771 
set_text_cursor(bool enabled)772 void TilesFramework::set_text_cursor(bool enabled)
773 {
774     if (m_text_cursor == enabled) return;
775 
776     m_text_cursor = enabled;
777 }
778 
_send_ui_state(WebtilesUIState state)779 static void _send_ui_state(WebtilesUIState state)
780 {
781     tiles.json_open_object();
782     tiles.json_write_string("msg", "ui_state");
783     tiles.json_write_int("state", state);
784     tiles.json_close_object();
785     tiles.finish_message();
786 }
787 
set_ui_state(WebtilesUIState state)788 void TilesFramework::set_ui_state(WebtilesUIState state)
789 {
790     if (m_ui_state == state) return;
791 
792     m_ui_state = state;
793 }
794 
update_input_mode(mouse_mode mode,bool force)795 void TilesFramework::update_input_mode(mouse_mode mode, bool force)
796 {
797     auto prev_mode = mouse_control::current_mode();
798     if (prev_mode == mode && !force)
799         return;
800 
801     // we skip redrawing in this case because it happens on every key input,
802     // and is very heavy on held down keys
803     if (force
804         || !(prev_mode == MOUSE_MODE_COMMAND && mode == MOUSE_MODE_NORMAL
805              || prev_mode == MOUSE_MODE_NORMAL && mode == MOUSE_MODE_COMMAND))
806     {
807         redraw();
808     }
809 
810     json_open_object();
811     json_write_string("msg", "input_mode");
812     json_write_int("mode", mode);
813     json_close_object();
814     finish_message();
815 }
816 
_update_string(bool force,string & current,const string & next,const string & name,bool update=true)817 static bool _update_string(bool force, string& current,
818                            const string& next,
819                            const string& name,
820                            bool update = true)
821 {
822     if (force || current != next)
823     {
824         tiles.json_write_string(name, next);
825         if (update)
826             current = next;
827         return true;
828     }
829     else
830         return false;
831 }
832 
_update_int(bool force,T & current,T next,const string & name,bool update=true)833 template<class T> static bool _update_int(bool force, T& current, T next,
834                                           const string& name,
835                                           bool update = true)
836 {
837     if (force || current != next)
838     {
839         tiles.json_write_int(name, next);
840         if (update)
841             current = next;
842         return true;
843     }
844     else
845         return false;
846 }
847 
_update_statuses(player_info & c)848 static bool _update_statuses(player_info& c)
849 {
850     bool changed = false;
851     unsigned int counter = 0;
852     status_info inf;
853     for (unsigned int status = 0; status <= STATUS_LAST_STATUS; ++status)
854     {
855         if (status == DUR_DIVINE_SHIELD)
856         {
857             inf = status_info();
858             if (!you.duration[status])
859                 continue;
860             inf.short_text = "divinely shielded";
861         }
862         else if (status == DUR_ICEMAIL_DEPLETED)
863         {
864             inf = status_info();
865             if (you.duration[status] <= ICEMAIL_TIME / ICEMAIL_MAX)
866                 continue;
867             inf.short_text = "icemail depleted";
868         }
869         else if (status == DUR_ACROBAT)
870         {
871             inf = status_info();
872             if (!acrobat_boost_active())
873                 continue;
874             inf.short_text = "acrobatic";
875         }
876         else if (!fill_status_info(status, inf)) // this will reset inf itself
877             continue;
878 
879         if (!inf.light_text.empty() || !inf.short_text.empty())
880         {
881             if (!changed)
882             {
883                 // up until now, c.status has not changed. Does this dur differ
884                 // from the counter-th element in c.status?
885                 if (counter >= c.status.size()
886                     || inf.light_text != c.status[counter].light_text
887                     || inf.light_colour != c.status[counter].light_colour
888                     || inf.short_text != c.status[counter].short_text)
889                 {
890                     changed = true;
891                 }
892             }
893 
894             if (changed)
895             {
896                 // c.status has changed at some point before counter, so all
897                 // bets are off for any future statuses.
898                 c.status.resize(counter + 1);
899                 c.status[counter] = inf;
900             }
901 
902             counter++;
903         }
904     }
905     if (c.status.size() != counter)
906     {
907         // the only thing that has happened is that some durations are removed
908         ASSERT(!changed);
909         changed = true;
910         c.status.resize(counter);
911     }
912 
913     return changed;
914 }
915 
player_info()916 player_info::player_info()
917 {
918     _state_ever_synced = false;
919     for (auto &eq : equip)
920         eq = -1;
921     position = coord_def(-1, -1);
922 }
923 
924 /**
925  * Send the player properties to the webserver. Any player properties that
926  * must be available to the WebTiles client must be sent here through an
927  * _update_* function call of the correct data type.
928  * @param force_full  If true, all properties will be updated in the json
929  *                    regardless whether their values are the same as the
930  *                    current info in m_current_player_info.
931  */
_send_player(bool force_full)932 void TilesFramework::_send_player(bool force_full)
933 {
934     player_info& c = m_current_player_info;
935     if (!c._state_ever_synced)
936     {
937         // force the initial sync to be full: otherwise the _update_blah
938         // functions will incorrectly detect initial values to be ones that
939         // have previously been sent to the client, when they will not have
940         // been. (This is made ever worse by the fact that player_info does
941         // not initialize most of its values...)
942         c._state_ever_synced = true;
943         force_full = true;
944     }
945 
946     json_open_object();
947     json_write_string("msg", "player");
948     json_treat_as_empty();
949 
950     _update_string(force_full, c.name, you.your_name, "name");
951     _update_string(force_full, c.job_title, filtered_lang(player_title()),
952                    "title");
953     _update_int(force_full, c.wizard, you.wizard, "wizard");
954     _update_string(force_full, c.species, species::name(you.species),
955                    "species");
956     string god = "";
957     if (you_worship(GOD_JIYVA))
958         god = god_name_jiyva(true);
959     else if (!you_worship(GOD_NO_GOD))
960         god = god_name(you.religion);
961     _update_string(force_full, c.god, god, "god");
962     _update_int(force_full, c.under_penance, (bool) player_under_penance(), "penance");
963     uint8_t prank = 0;
964     if (!you_worship(GOD_NO_GOD))
965         prank = max(0, piety_rank());
966     else if (you.char_class == JOB_MONK && !you.has_mutation(MUT_FORLORN)
967              && !had_gods())
968     {
969         prank = 2;
970     }
971     _update_int(force_full, c.piety_rank, prank, "piety_rank");
972 
973     _update_int(force_full, c.form, (uint8_t) you.form, "form");
974 
975     _update_int(force_full, c.hp, you.hp, "hp");
976     _update_int(force_full, c.hp_max, you.hp_max, "hp_max");
977     int max_max_hp = get_real_hp(true, false);
978 
979     _update_int(force_full, c.real_hp_max, max_max_hp, "real_hp_max");
980     _update_int(force_full, c.mp, you.magic_points, "mp");
981     _update_int(force_full, c.mp_max, you.max_magic_points, "mp_max");
982     _update_int(force_full, c.dd_real_mp_max,
983                 you.species == SP_DEEP_DWARF ? get_real_mp(false) : 0,
984                 "dd_real_mp_max");
985 
986     _update_int(force_full, c.poison_survival, max(0, poison_survival()),
987                 "poison_survival");
988 
989     _update_int(force_full, c.armour_class, you.armour_class(), "ac");
990     _update_int(force_full, c.evasion, you.evasion(), "ev");
991     _update_int(force_full, c.shield_class, player_displayed_shield_class(),
992                 "sh");
993 
994     _update_int(force_full, c.strength, (int8_t) you.strength(false), "str");
995     _update_int(force_full, c.strength_max, (int8_t) you.max_strength(), "str_max");
996     _update_int(force_full, c.intel, (int8_t) you.intel(false), "int");
997     _update_int(force_full, c.intel_max, (int8_t) you.max_intel(), "int_max");
998     _update_int(force_full, c.dex, (int8_t) you.dex(false), "dex");
999     _update_int(force_full, c.dex_max, (int8_t) you.max_dex(), "dex_max");
1000 
1001     if (you.has_mutation(MUT_MULTILIVED))
1002     {
1003         _update_int(force_full, c.lives, you.lives, "lives");
1004         _update_int(force_full, c.deaths, you.deaths, "deaths");
1005     }
1006 
1007     _update_int(force_full, c.experience_level, you.experience_level, "xl");
1008     _update_int(force_full, c.exp_progress, (int8_t) get_exp_progress(), "progress");
1009     _update_int(force_full, c.gold, you.gold, "gold");
1010     _update_int(force_full, c.noise,
1011                 (you.wizard ? you.get_noise_perception(false) : -1), "noise");
1012     _update_int(force_full, c.adjusted_noise, you.get_noise_perception(true), "adjusted_noise");
1013 
1014     if (you.running == 0) // Don't update during running/resting
1015     {
1016         _update_int(force_full, c.elapsed_time, you.elapsed_time, "time");
1017         _update_int(force_full, c.num_turns, you.num_turns, "turn");
1018     }
1019 
1020     const PlaceInfo& place = you.get_place_info();
1021     string short_name = branches[place.branch].shortname;
1022 
1023     if (brdepth[place.branch] == 1)
1024     {
1025         // Definite articles
1026         if (place.branch == BRANCH_ABYSS)
1027             short_name.insert(0, "The ");
1028         // Indefinite articles
1029         else if (place.branch != BRANCH_PANDEMONIUM &&
1030                  !is_connected_branch(place.branch))
1031         {
1032             short_name = article_a(short_name);
1033         }
1034     }
1035     _update_string(force_full, c.place, short_name, "place");
1036     _update_int(force_full, c.depth, brdepth[place.branch] > 1 ? you.depth : 0, "depth");
1037 
1038     if (m_origin.equals(-1, -1))
1039         m_origin = you.position;
1040     coord_def pos = you.position - m_origin;
1041     if (force_full || c.position != pos)
1042     {
1043         json_open_object("pos");
1044         json_write_int("x", pos.x);
1045         json_write_int("y", pos.y);
1046         json_close_object();
1047         c.position = pos;
1048     }
1049 
1050     if (force_full || _update_statuses(c))
1051     {
1052         json_open_array("status");
1053         for (const status_info &status : c.status)
1054         {
1055             json_open_object();
1056             if (!status.light_text.empty())
1057             {
1058                 json_write_string("light", status.light_text);
1059                 // split off any extra info, e.g. counts for things like Zot
1060                 // and Flay. (Status db descriptions never have spaces.)
1061                 string dbname = split_string(" ", status.light_text, true, true, 1)[0];
1062                 // Don't claim Zot is impending when it's not near.
1063                 if (dbname == "Zot" && status.light_colour == WHITE)
1064                     dbname = "Zot count";
1065                 const string dbdesc = getLongDescription(dbname + " status");
1066                 json_write_string("desc", dbdesc.size() ? dbdesc : "No description found");
1067             }
1068             if (!status.short_text.empty())
1069                 json_write_string("text", status.short_text);
1070             if (status.light_colour)
1071                 json_write_int("col", macro_colour(status.light_colour));
1072             json_close_object(true);
1073         }
1074         json_close_array();
1075     }
1076 
1077     json_open_object("inv");
1078     for (unsigned int i = 0; i < ENDOFPACK; ++i)
1079     {
1080         json_open_object(to_string(i));
1081         item_def item = get_item_known_info(you.inv[i]);
1082         if ((char)i == you.equip[EQ_WEAPON] && is_weapon(item) && you.duration[DUR_CORROSION])
1083             item.plus -= 4 * you.props["corrosion_amount"].get_int();
1084         _send_item(c.inv[i], item, force_full);
1085         json_close_object(true);
1086     }
1087     json_close_object(true);
1088 
1089     json_open_object("equip");
1090     for (unsigned int i = EQ_FIRST_EQUIP; i < NUM_EQUIP; ++i)
1091     {
1092         const int8_t equip = !you.melded[i] ? you.equip[i] : -1;
1093         _update_int(force_full, c.equip[i], equip, to_string(i));
1094     }
1095     json_close_object(true);
1096 
1097     _update_int(force_full, c.launcher_item,
1098                 you.launcher_action.is_empty()
1099                 ? (int8_t) -1
1100                 : (int8_t) you.launcher_action.get()->get_item(), "launcher_item");
1101     _update_int(force_full, c.quiver_item,
1102                 (int8_t) you.quiver_action.get()->get_item(), "quiver_item");
1103 
1104     _update_string(force_full, c.quiver_desc,
1105                 you.quiver_action.get()->quiver_description().to_colour_string(),
1106                 "quiver_desc");
1107 
1108     _update_string(force_full, c.unarmed_attack,
1109                    you.unarmed_attack_name(), "unarmed_attack");
1110     _update_int(force_full, c.unarmed_attack_colour,
1111                 (uint8_t) get_form()->uc_colour, "unarmed_attack_colour");
1112     _update_int(force_full, c.quiver_available,
1113                     you.quiver_action.get()->is_valid()
1114                                 && you.quiver_action.get()->is_enabled(),
1115                 "quiver_available");
1116 
1117     json_close_object(true);
1118 
1119     finish_message();
1120 }
1121 
_send_item(item_def & current,const item_def & next,bool force_full)1122 void TilesFramework::_send_item(item_def& current, const item_def& next,
1123                                 bool force_full)
1124 {
1125     bool changed = false;
1126     bool defined = next.defined();
1127 
1128     if (force_full || current.base_type != next.base_type)
1129     {
1130         changed = true;
1131         json_write_int("base_type", next.base_type);
1132     }
1133 
1134     changed |= _update_int(force_full, current.quantity, next.quantity,
1135                            "quantity", false);
1136 
1137     if (!defined)
1138     {
1139         current = next;
1140         return; // For undefined items, only send base_type and quantity
1141     }
1142     else if (!current.defined())
1143         force_full = true; // if the item was undefined before, send everything
1144 
1145     changed |= _update_int(force_full, current.sub_type, next.sub_type,
1146                            "sub_type", false);
1147     changed |= _update_int(force_full, current.plus, next.plus,
1148                            "plus", false);
1149     changed |= _update_int(force_full, current.plus2, next.plus2,
1150                            "plus2", false);
1151     changed |= _update_int(force_full, current.flags, next.flags,
1152                            "flags", false);
1153     changed |= _update_string(force_full, current.inscription,
1154                               next.inscription, "inscription", false);
1155 
1156     // TODO: props?
1157 
1158     changed |= (current.special != next.special);
1159 
1160     // Derived stuff
1161     if (changed && defined)
1162     {
1163         string name = next.name(DESC_A, true, false, true);
1164         if (force_full || current.name(DESC_A, true, false, true) != name)
1165             json_write_string("name", name);
1166 
1167         const string prefix = item_prefix(next);
1168         const int prefcol = menu_colour(next.name(DESC_INVENTORY), prefix);
1169         if (force_full)
1170             json_write_int("col", macro_colour(prefcol));
1171         else
1172         {
1173             const string current_prefix = item_prefix(current);
1174             const int current_prefcol = menu_colour(current.name(DESC_INVENTORY), current_prefix);
1175 
1176             if (current_prefcol != prefcol)
1177                 json_write_int("col", macro_colour(prefcol));
1178         }
1179 
1180         tileidx_t tile = tileidx_item(next);
1181         if (force_full || tileidx_item(current) != tile)
1182         {
1183             json_open_array("tile");
1184             tileidx_t base_tile = tileidx_known_base_item(tile);
1185             if (base_tile)
1186                 json_write_int(base_tile);
1187             json_write_int(tile);
1188             json_close_array();
1189         }
1190 
1191         current = next;
1192     }
1193 }
1194 
send_doll(const dolls_data & doll,bool submerged,bool ghost)1195 void TilesFramework::send_doll(const dolls_data &doll, bool submerged, bool ghost)
1196 {
1197     // Ordered from back to front.
1198     // FIXME: Implement this logic in one place in e.g. pack_doll_buf().
1199     int p_order[TILEP_PART_MAX] =
1200     {
1201         // background
1202         TILEP_PART_SHADOW,
1203         TILEP_PART_HALO,
1204         TILEP_PART_ENCH,
1205         TILEP_PART_DRCWING,
1206         TILEP_PART_CLOAK,
1207         // player
1208         TILEP_PART_BASE,
1209         TILEP_PART_BOOTS,
1210         TILEP_PART_LEG,
1211         TILEP_PART_BODY,
1212         TILEP_PART_ARM,
1213         TILEP_PART_HAIR,
1214         TILEP_PART_BEARD,
1215         TILEP_PART_DRCHEAD,
1216         TILEP_PART_HELM,
1217         TILEP_PART_HAND1,
1218         TILEP_PART_HAND2,
1219     };
1220 
1221     int flags[TILEP_PART_MAX];
1222     tilep_calc_flags(doll, flags);
1223 
1224     // For skirts, boots go under the leg armour. For pants, they go over.
1225     if (doll.parts[TILEP_PART_LEG] < TILEP_LEG_SKIRT_OFS)
1226     {
1227         p_order[7] = TILEP_PART_BOOTS;
1228         p_order[6] = TILEP_PART_LEG;
1229     }
1230 
1231     // Draw scarves above other clothing.
1232     if (doll.parts[TILEP_PART_CLOAK] >= TILEP_CLOAK_SCARF_FIRST_NORM)
1233     {
1234         p_order[4] = p_order[5];
1235         p_order[5] = p_order[6];
1236         p_order[6] = p_order[7];
1237         p_order[7] = p_order[8];
1238         p_order[8] = p_order[9];
1239         p_order[9] = TILEP_PART_CLOAK;
1240     }
1241 
1242     // Special case bardings from being cut off.
1243     const bool is_naga = is_player_tile(doll.parts[TILEP_PART_BASE],
1244                                         TILEP_BASE_NAGA);
1245 
1246     if (doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_NAGA_BARDING
1247         && doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_NAGA_BARDING_RED
1248         || doll.parts[TILEP_PART_BOOTS] == TILEP_BOOTS_LIGHTNING_SCALES)
1249     {
1250         flags[TILEP_PART_BOOTS] = is_naga ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
1251     }
1252 
1253     const bool is_ptng = is_player_tile(doll.parts[TILEP_PART_BASE],
1254                                         TILEP_BASE_PALENTONGA);
1255 
1256     if (doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_CENTAUR_BARDING
1257         && doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_CENTAUR_BARDING_RED
1258         || doll.parts[TILEP_PART_BOOTS] == TILEP_BOOTS_BLACK_KNIGHT)
1259     {
1260         flags[TILEP_PART_BOOTS] = is_ptng ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
1261     }
1262 
1263     tiles.json_open_array("doll");
1264 
1265     for (int i = 0; i < TILEP_PART_MAX; ++i)
1266     {
1267         int p = p_order[i];
1268 
1269         if (!doll.parts[p] || flags[p] == TILEP_FLAG_HIDE)
1270             continue;
1271 
1272         if (p == TILEP_PART_SHADOW && (submerged || ghost))
1273             continue;
1274 
1275         int ymax = TILE_Y;
1276 
1277         if (flags[p] == TILEP_FLAG_CUT_CENTAUR
1278             || flags[p] == TILEP_FLAG_CUT_NAGA)
1279         {
1280             ymax = 18;
1281         }
1282 
1283         tiles.json_write_comma();
1284         tiles.write_message("[%u,%d]", (unsigned int) doll.parts[p], ymax);
1285     }
1286     tiles.json_close_array();
1287 }
1288 
send_mcache(mcache_entry * entry,bool submerged,bool send)1289 void TilesFramework::send_mcache(mcache_entry *entry, bool submerged, bool send)
1290 {
1291     bool trans = entry->transparent();
1292     if (trans && send)
1293         tiles.json_write_int("trans", 1);
1294 
1295     const dolls_data *doll = entry->doll();
1296     if (send)
1297     {
1298         if (doll)
1299             send_doll(*doll, submerged, trans);
1300         else
1301         {
1302             tiles.json_write_comma();
1303             tiles.write_message("\"doll\":[]");
1304         }
1305     }
1306 
1307     tiles.json_open_array("mcache");
1308 
1309     tile_draw_info dinfo[mcache_entry::MAX_INFO_COUNT];
1310     int draw_info_count = entry->info(&dinfo[0]);
1311     for (int i = 0; i < draw_info_count; i++)
1312     {
1313         tiles.json_write_comma();
1314         tiles.write_message("[%u,%d,%d]", (unsigned int) dinfo[i].idx,
1315                             dinfo[i].ofs_x, dinfo[i].ofs_y);
1316     }
1317 
1318     tiles.json_close_array();
1319 }
1320 
_in_water(const packed_cell & cell)1321 static bool _in_water(const packed_cell &cell)
1322 {
1323     return (cell.bg & TILE_FLAG_WATER) && !(cell.fg & TILE_FLAG_FLYING);
1324 }
1325 
_needs_flavour(const packed_cell & cell)1326 static bool _needs_flavour(const packed_cell &cell)
1327 {
1328     tileidx_t bg_idx = cell.bg & TILE_FLAG_MASK;
1329     if (bg_idx >= TILE_DNGN_FIRST_TRANSPARENT)
1330         return true; // Needs flv.floor
1331     if (cell.is_liquefied || cell.is_bloody)
1332         return true; // Needs flv.special
1333     return false;
1334 
1335 }
1336 
_get_brand(int col)1337 static inline unsigned _get_brand(int col)
1338 {
1339     return (col & COLFLAG_FRIENDLY_MONSTER) ? Options.friend_brand :
1340            (col & COLFLAG_NEUTRAL_MONSTER)  ? Options.neutral_brand :
1341            (col & COLFLAG_ITEM_HEAP)        ? Options.heap_brand :
1342            (col & COLFLAG_WILLSTAB)         ? Options.stab_brand :
1343            (col & COLFLAG_MAYSTAB)          ? Options.may_stab_brand :
1344            (col & COLFLAG_FEATURE_ITEM)     ? Options.feature_item_brand :
1345            (col & COLFLAG_TRAP_ITEM)        ? Options.trap_item_brand :
1346            (col & COLFLAG_REVERSE)          ? unsigned{CHATTR_REVERSE}
1347                                             : unsigned{CHATTR_NORMAL};
1348 }
1349 
write_tileidx(tileidx_t t)1350 void TilesFramework::write_tileidx(tileidx_t t)
1351 {
1352     // JS can only handle signed ints
1353     const int lo = t & 0xFFFFFFFF;
1354     const int hi = t >> 32;
1355     if (hi == 0)
1356         tiles.write_message("%d", lo);
1357     else
1358         tiles.write_message("[%d,%d]", lo, hi);
1359 }
1360 
_send_cell(const coord_def & gc,const screen_cell_t & current_sc,const screen_cell_t & next_sc,const map_cell & current_mc,const map_cell & next_mc,map<uint32_t,coord_def> & new_monster_locs,bool force_full)1361 void TilesFramework::_send_cell(const coord_def &gc,
1362                                 const screen_cell_t &current_sc, const screen_cell_t &next_sc,
1363                                 const map_cell &current_mc, const map_cell &next_mc,
1364                                 map<uint32_t, coord_def>& new_monster_locs,
1365                                 bool force_full)
1366 {
1367     if (current_mc.feat() != next_mc.feat())
1368         json_write_int("f", next_mc.feat());
1369 
1370     if (next_mc.monsterinfo())
1371         _send_monster(gc, next_mc.monsterinfo(), new_monster_locs, force_full);
1372     else if (current_mc.monsterinfo())
1373         json_write_null("mon");
1374 
1375     map_feature mf = get_cell_map_feature(gc);
1376     if (get_cell_map_feature(current_mc) != mf)
1377         json_write_int("mf", mf);
1378 
1379     // Glyph and colour
1380     char32_t glyph = next_sc.glyph;
1381     if (current_sc.glyph != glyph)
1382     {
1383         char buf[5];
1384         buf[wctoutf8(buf, glyph)] = 0;
1385         json_write_string("g", buf);
1386     }
1387     if ((current_sc.colour != next_sc.colour
1388          || current_sc.glyph == ' ') && glyph != ' ')
1389     {
1390         int col = next_sc.colour;
1391         col = (_get_brand(col) << 4) | macro_colour(col & 0xF);
1392         json_write_int("col", col);
1393     }
1394 
1395     json_open_object("t");
1396     {
1397         // Tile data
1398         const packed_cell &next_pc = next_sc.tile;
1399         const packed_cell &current_pc = current_sc.tile;
1400 
1401         const tileidx_t fg_idx = next_pc.fg & TILE_FLAG_MASK;
1402 
1403         const bool in_water = _in_water(next_pc);
1404         bool fg_changed = false;
1405 
1406         if (next_pc.fg != current_pc.fg)
1407         {
1408             fg_changed = true;
1409 
1410             json_write_name("fg");
1411             write_tileidx(next_pc.fg);
1412             if (get_tile_texture(fg_idx) == TEX_DEFAULT)
1413                 json_write_int("base", (int) tileidx_known_base_item(fg_idx));
1414         }
1415 
1416         if (next_pc.bg != current_pc.bg)
1417         {
1418             json_write_name("bg");
1419             write_tileidx(next_pc.bg);
1420         }
1421 
1422         if (next_pc.cloud != current_pc.cloud)
1423         {
1424             json_write_name("cloud");
1425             write_tileidx(next_pc.cloud);
1426         }
1427 
1428         if (next_pc.is_bloody != current_pc.is_bloody)
1429             json_write_bool("bloody", next_pc.is_bloody);
1430 
1431         if (next_pc.old_blood != current_pc.old_blood)
1432             json_write_bool("old_blood", next_pc.old_blood);
1433 
1434         if (next_pc.is_silenced != current_pc.is_silenced)
1435             json_write_bool("silenced", next_pc.is_silenced);
1436 
1437         if (next_pc.halo != current_pc.halo)
1438             json_write_int("halo", next_pc.halo);
1439 
1440         if (next_pc.is_highlighted_summoner
1441             != current_pc.is_highlighted_summoner)
1442         {
1443             json_write_bool("highlighted_summoner",
1444                             next_pc.is_highlighted_summoner);
1445         }
1446 
1447         if (next_pc.is_sanctuary != current_pc.is_sanctuary)
1448             json_write_bool("sanctuary", next_pc.is_sanctuary);
1449 
1450         if (next_pc.is_liquefied != current_pc.is_liquefied)
1451             json_write_bool("liquefied", next_pc.is_liquefied);
1452 
1453         if (next_pc.orb_glow != current_pc.orb_glow)
1454             json_write_int("orb_glow", next_pc.orb_glow);
1455 
1456         if (next_pc.quad_glow != current_pc.quad_glow)
1457             json_write_bool("quad_glow", next_pc.quad_glow);
1458 
1459         if (next_pc.disjunct != current_pc.disjunct)
1460             json_write_bool("disjunct", next_pc.disjunct);
1461 
1462         if (next_pc.mangrove_water != current_pc.mangrove_water)
1463             json_write_bool("mangrove_water", next_pc.mangrove_water);
1464 
1465         if (next_pc.awakened_forest != current_pc.awakened_forest)
1466             json_write_bool("awakened_forest", next_pc.awakened_forest);
1467 
1468         if (next_pc.blood_rotation != current_pc.blood_rotation)
1469             json_write_int("blood_rotation", next_pc.blood_rotation);
1470 
1471         if (next_pc.travel_trail != current_pc.travel_trail)
1472             json_write_int("travel_trail", next_pc.travel_trail);
1473 
1474         if (_needs_flavour(next_pc) &&
1475             (next_pc.flv.floor != current_pc.flv.floor
1476              || next_pc.flv.special != current_pc.flv.special
1477              || !_needs_flavour(current_pc)
1478              || force_full))
1479         {
1480             json_open_object("flv");
1481             json_write_int("f", next_pc.flv.floor);
1482             if (next_pc.flv.special)
1483                 json_write_int("s", next_pc.flv.special);
1484             json_close_object();
1485         }
1486 
1487         if (fg_idx >= TILEP_MCACHE_START)
1488         {
1489             if (fg_changed)
1490             {
1491                 mcache_entry *entry = mcache.get(fg_idx);
1492                 if (entry)
1493                     send_mcache(entry, in_water);
1494                 else
1495                 {
1496                     json_write_comma();
1497                     write_message("\"doll\":[[%d,%d]]", TILEP_MONS_UNKNOWN, TILE_Y);
1498                     json_write_null("mcache");
1499                 }
1500             }
1501         }
1502         else if (fg_idx == TILEP_PLAYER)
1503         {
1504             bool player_doll_changed = false;
1505             dolls_data result = player_doll;
1506             fill_doll_equipment(result);
1507             if (result != last_player_doll)
1508             {
1509                 player_doll_changed = true;
1510                 last_player_doll = result;
1511             }
1512             if (fg_changed || player_doll_changed)
1513             {
1514                 send_doll(last_player_doll, in_water, false);
1515                 if (Options.tile_use_monster != MONS_0)
1516                 {
1517                     monster_info minfo(MONS_PLAYER, MONS_PLAYER);
1518                     minfo.props["monster_tile"] =
1519                         short(last_player_doll.parts[TILEP_PART_BASE]);
1520                     item_def *item;
1521                     if (you.slot_item(EQ_WEAPON))
1522                     {
1523                         item = new item_def(
1524                             get_item_known_info(*you.slot_item(EQ_WEAPON)));
1525                         minfo.inv[MSLOT_WEAPON].reset(item);
1526                     }
1527                     if (you.slot_item(EQ_SHIELD))
1528                     {
1529                         item = new item_def(
1530                             get_item_known_info(*you.slot_item(EQ_SHIELD)));
1531                         minfo.inv[MSLOT_SHIELD].reset(item);
1532                     }
1533                     tileidx_t mcache_idx = mcache.register_monster(minfo);
1534                     mcache_entry *entry = mcache.get(mcache_idx);
1535                     if (entry)
1536                         send_mcache(entry, in_water, false);
1537                     else
1538                         json_write_null("mcache");
1539                 }
1540                 else
1541                     json_write_null("mcache");
1542             }
1543         }
1544         else if (get_tile_texture(fg_idx) == TEX_PLAYER)
1545         {
1546             if (fg_changed)
1547             {
1548                 json_write_comma();
1549                 write_message("\"doll\":[[%u,%d]]", (unsigned int) fg_idx, TILE_Y);
1550                 json_write_null("mcache");
1551             }
1552         }
1553         else
1554         {
1555             if (fg_changed)
1556             {
1557                 json_write_comma();
1558                 json_write_null("doll");
1559                 json_write_null("mcache");
1560             }
1561         }
1562 
1563         bool overlays_changed = false;
1564 
1565         if (next_pc.num_dngn_overlay != current_pc.num_dngn_overlay)
1566             overlays_changed = true;
1567         else
1568         {
1569             for (int i = 0; i < next_pc.num_dngn_overlay; i++)
1570             {
1571                 if (next_pc.dngn_overlay[i] != current_pc.dngn_overlay[i])
1572                 {
1573                     overlays_changed = true;
1574                     break;
1575                 }
1576             }
1577         }
1578 
1579         if (overlays_changed)
1580         {
1581             json_open_array("ov");
1582             for (int i = 0; i < next_pc.num_dngn_overlay; ++i)
1583                 json_write_int(next_pc.dngn_overlay[i]);
1584             json_close_array();
1585         }
1586     }
1587     json_close_object(true);
1588 }
1589 
_send_cursor(cursor_type type)1590 void TilesFramework::_send_cursor(cursor_type type)
1591 {
1592     if (m_cursor[type] == NO_CURSOR)
1593         send_message("{\"msg\":\"cursor\",\"id\":%d}", type);
1594     else
1595     {
1596         if (m_origin.equals(-1, -1))
1597             m_origin = m_cursor[type];
1598         send_message("{\"msg\":\"cursor\",\"id\":%d,\"loc\":{\"x\":%d,\"y\":%d}}",
1599                      type, m_cursor[type].x - m_origin.x,
1600                      m_cursor[type].y - m_origin.y);
1601     }
1602 }
1603 
_mcache_ref(bool inc)1604 void TilesFramework::_mcache_ref(bool inc)
1605 {
1606     for (int y = 0; y < GYM; y++)
1607         for (int x = 0; x < GXM; x++)
1608         {
1609             coord_def gc(x, y);
1610 
1611             int fg_idx = m_current_view(gc).tile.fg & TILE_FLAG_MASK;
1612             if (fg_idx >= TILEP_MCACHE_START)
1613             {
1614                 mcache_entry *entry = mcache.get(fg_idx);
1615                 if (entry)
1616                 {
1617                     if (inc)
1618                         entry->inc_ref();
1619                     else
1620                         entry->dec_ref();
1621                 }
1622             }
1623         }
1624 }
1625 
_send_map(bool force_full)1626 void TilesFramework::_send_map(bool force_full)
1627 {
1628     // TODO: prevent in some other / better way?
1629     if (_send_lock)
1630         return;
1631 
1632     unwind_bool no_rentry(_send_lock, true);
1633 
1634     map<uint32_t, coord_def> new_monster_locs;
1635 
1636     force_full = force_full || m_need_full_map;
1637     m_need_full_map = false;
1638 
1639     json_open_object();
1640     json_write_string("msg", "map");
1641     json_treat_as_empty();
1642 
1643     if (force_full)
1644         json_write_bool("clear", true);
1645 
1646     if (force_full || you.on_current_level != m_player_on_level)
1647     {
1648         json_write_bool("player_on_level", you.on_current_level);
1649         m_player_on_level = you.on_current_level;
1650     }
1651 
1652     if (force_full || m_current_gc != m_next_gc)
1653     {
1654         if (m_origin.equals(-1, -1))
1655             m_origin = m_next_gc;
1656         json_open_object("vgrdc");
1657         json_write_int("x", m_next_gc.x - m_origin.x);
1658         json_write_int("y", m_next_gc.y - m_origin.y);
1659         json_close_object();
1660         m_current_gc = m_next_gc;
1661     }
1662 
1663     screen_cell_t default_cell;
1664     default_cell.tile.bg = TILE_FLAG_UNSEEN;
1665     default_cell.glyph = ' ';
1666     default_cell.colour = 7;
1667     map_cell default_map_cell;
1668 
1669     coord_def last_gc(0, 0);
1670     bool send_gc = true;
1671 
1672     json_open_array("cells");
1673     for (int y = 0; y < GYM; y++)
1674         for (int x = 0; x < GXM; x++)
1675         {
1676             coord_def gc(x, y);
1677 
1678             if (!is_dirty(gc) && !force_full)
1679                 continue;
1680 
1681             if (cell_needs_redraw(gc))
1682             {
1683                 screen_cell_t *cell = &m_next_view(gc);
1684 
1685                 draw_cell(cell, gc, false, m_current_flash_colour);
1686                 pack_cell_overlays(gc, m_next_view);
1687             }
1688 
1689             mark_clean(gc);
1690 
1691             if (m_origin.equals(-1, -1))
1692                 m_origin = gc;
1693 
1694             json_open_object();
1695             if (send_gc
1696                 || last_gc.x + 1 != gc.x
1697                 || last_gc.y != gc.y)
1698             {
1699                 json_write_int("x", x - m_origin.x);
1700                 json_write_int("y", y - m_origin.y);
1701                 json_treat_as_empty();
1702             }
1703 
1704             const screen_cell_t& sc = force_full ? default_cell
1705                 : m_current_view(gc);
1706             const map_cell& mc = force_full ? default_map_cell
1707                 : m_current_map_knowledge(gc);
1708             _send_cell(gc,
1709                        sc,
1710                        m_next_view(gc),
1711                        mc, env.map_knowledge(gc),
1712                        new_monster_locs, force_full);
1713 
1714             if (!json_is_empty())
1715             {
1716                 send_gc = false;
1717                 last_gc = gc;
1718             }
1719             json_close_object(true);
1720         }
1721     json_close_array(true);
1722 
1723     json_close_object(true);
1724 
1725     finish_message();
1726 
1727     if (force_full)
1728         _send_cursor(CURSOR_MAP);
1729 
1730     if (m_mcache_ref_done)
1731         _mcache_ref(false);
1732 
1733     m_current_map_knowledge = env.map_knowledge;
1734     m_current_view = m_next_view;
1735 
1736     _mcache_ref(true);
1737     m_mcache_ref_done = true;
1738 
1739     m_monster_locs = new_monster_locs;
1740 }
1741 
_send_monster(const coord_def & gc,const monster_info * m,map<uint32_t,coord_def> & new_monster_locs,bool force_full)1742 void TilesFramework::_send_monster(const coord_def &gc, const monster_info* m,
1743                                    map<uint32_t, coord_def>& new_monster_locs,
1744                                    bool force_full)
1745 {
1746     json_open_object("mon");
1747     if (m->client_id)
1748     {
1749         json_write_int("id", m->client_id);
1750         json_treat_as_empty();
1751         new_monster_locs[m->client_id] = gc;
1752     }
1753 
1754     const monster_info* last = nullptr;
1755     auto it = m_monster_locs.find(m->client_id);
1756     if (m->client_id == 0 || it == m_monster_locs.end())
1757     {
1758         last = m_current_map_knowledge(gc).monsterinfo();
1759 
1760         if (last && last->client_id != m->client_id)
1761             json_treat_as_nonempty(); // Force sending at least the id
1762     }
1763     else
1764     {
1765         last = m_current_map_knowledge(it->second).monsterinfo();
1766 
1767         if (it->second != gc)
1768             json_treat_as_nonempty(); // As above
1769     }
1770 
1771     if (last == nullptr)
1772         force_full = true;
1773 
1774     if (force_full || (last->full_name() != m->full_name()))
1775         json_write_string("name", m->full_name());
1776 
1777     if (force_full || (last->pluralised_name() != m->pluralised_name()))
1778         json_write_string("plural", m->pluralised_name());
1779 
1780     if (force_full || last->type != m->type)
1781     {
1782         json_write_int("type", m->type);
1783 
1784         // TODO: get this information to the client in another way
1785         json_open_object("typedata");
1786         json_write_int("avghp", mons_avg_hp(m->type));
1787         if (!mons_class_gives_xp(m->type))
1788             json_write_bool("no_exp", true);
1789         json_close_object();
1790     }
1791 
1792     if (force_full || last->attitude != m->attitude)
1793         json_write_int("att", m->attitude);
1794 
1795     if (force_full || last->base_type != m->base_type)
1796         json_write_int("btype", m->base_type);
1797 
1798     if (force_full || last->threat != m->threat)
1799         json_write_int("threat", m->threat);
1800 
1801     // tiebreakers for two monsters with the same custom name
1802     if (m->is_named())
1803         json_write_int("clientid", m->client_id);
1804 
1805     json_close_object(true);
1806 }
1807 
load_dungeon(const crawl_view_buffer & vbuf,const coord_def & gc)1808 void TilesFramework::load_dungeon(const crawl_view_buffer &vbuf,
1809                                   const coord_def &gc)
1810 {
1811     if (vbuf.size().equals(0, 0))
1812         return;
1813 
1814     m_view_loaded = true;
1815 
1816     if (m_ui_state == UI_CRT)
1817         set_ui_state(UI_NORMAL);
1818 
1819     m_next_flash_colour = you.flash_colour;
1820     if (m_next_flash_colour == BLACK)
1821         m_next_flash_colour = viewmap_flash_colour();
1822 
1823     // First re-render the area that was covered by vbuf the last time
1824     for (int y = m_next_view_tl.y; y <= m_next_view_br.y; y++)
1825         for (int x = m_next_view_tl.x; x <= m_next_view_br.x; x++)
1826         {
1827             if (x < 0 || x >= GXM || y < 0 || y >= GYM)
1828                 continue;
1829 
1830             if (!crawl_view.in_viewport_g(coord_def(x, y)))
1831                 mark_for_redraw(coord_def(x, y));
1832         }
1833 
1834     // re-cache the map knowledge for the whole map, not just the updated portion
1835     // fixes render bugs for out-of-LOS when transitioning levels in shoals/slime
1836     for (int y = 0; y < GYM; y++)
1837         for (int x = 0; x < GXM; x++)
1838         {
1839             const coord_def cache_gc(x, y);
1840             screen_cell_t *cell = &m_next_view(cache_gc);
1841             cell->tile.map_knowledge = map_bounds(cache_gc) ? env.map_knowledge(cache_gc) : map_cell();
1842         }
1843 
1844     m_next_view_tl = view2grid(coord_def(1, 1));
1845     m_next_view_br = view2grid(crawl_view.viewsz);
1846 
1847     // Copy vbuf into m_next_view
1848     for (int y = 0; y < vbuf.size().y; y++)
1849         for (int x = 0; x < vbuf.size().x; x++)
1850         {
1851             coord_def pos(x+1, y+1);
1852             coord_def grid = view2grid(pos);
1853 
1854             if (grid.x < 0 || grid.x >= GXM || grid.y < 0 || grid.y >= GYM)
1855                 continue;
1856 
1857             screen_cell_t *cell = &m_next_view(grid);
1858 
1859             *cell = ((const screen_cell_t *) vbuf)[x + vbuf.size().x * y];
1860             pack_cell_overlays(grid, m_next_view);
1861 
1862             mark_clean(grid); // Remove redraw flag
1863             mark_dirty(grid);
1864         }
1865 
1866     m_next_gc = gc;
1867 }
1868 
load_dungeon(const coord_def & cen)1869 void TilesFramework::load_dungeon(const coord_def &cen)
1870 {
1871     unwind_var<coord_def> viewp(crawl_view.viewp, cen - crawl_view.viewhalfsz);
1872     unwind_var<coord_def> vgrdc(crawl_view.vgrdc, cen);
1873     unwind_var<coord_def> vlos1(crawl_view.vlos1);
1874     unwind_var<coord_def> vlos2(crawl_view.vlos2);
1875 
1876     m_next_gc = cen;
1877 
1878     crawl_view.calc_vlos();
1879     viewwindow(false, true);
1880     place_cursor(CURSOR_MAP, cen);
1881 }
1882 
resize()1883 void TilesFramework::resize()
1884 {
1885     m_text_menu.resize(crawl_view.termsz.x, crawl_view.termsz.y);
1886 }
1887 
_send_messages()1888 void TilesFramework::_send_messages()
1889 {
1890     if (_send_lock)
1891         return;
1892     unwind_bool no_rentry(_send_lock, true);
1893 
1894     webtiles_send_messages();
1895 }
1896 
1897 /*
1898   Send everything a newly joined spectator needs
1899  */
_send_everything()1900 void TilesFramework::_send_everything()
1901 {
1902     _send_version();
1903     _send_options();
1904     _send_layout();
1905 
1906     _send_text_cursor(m_text_cursor);
1907 
1908     // UI State
1909     _send_ui_state(m_ui_state);
1910     m_last_ui_state = m_ui_state;
1911 
1912     send_message("{\"msg\":\"flash\",\"col\":%d}", m_current_flash_colour);
1913 
1914     _send_cursor(CURSOR_MOUSE);
1915     _send_cursor(CURSOR_TUTORIAL);
1916 
1917      // Player
1918     _send_player(true);
1919 
1920     // Map is sent after player, otherwise HP/MP bar can be left behind in the
1921     // old location if the player has moved
1922     _send_map(true);
1923 
1924     // Menus
1925     json_open_object();
1926     json_write_string("msg", "ui-stack");
1927     json_open_array("items");
1928     for (UIStackFrame &frame : m_menu_stack)
1929     {
1930         json_write_comma(); // noop immediately following open
1931         if (frame.type == UIStackFrame::MENU)
1932             frame.menu->webtiles_write_menu();
1933         else if (frame.type == UIStackFrame::CRT)
1934         {
1935             json_open_object();
1936             json_write_string("msg", "menu");
1937             json_write_string("type", "crt");
1938             json_write_string("tag", frame.crt_tag);
1939             json_write_bool("ui-centred", frame.centred);
1940             json_close_object();
1941         }
1942         else
1943         {
1944             for (const auto& json : frame.ui_json)
1945                 if (!json.empty())
1946                 {
1947                     m_msg_buf.append(json);
1948                     json_write_comma();
1949                 }
1950             continue;
1951         }
1952     }
1953     json_close_array();
1954     json_close_object();
1955     finish_message();
1956 
1957     _send_messages();
1958 
1959     update_input_mode(mouse_control::current_mode(), true);
1960 
1961     m_text_menu.send(true);
1962 
1963     ui::sync_ui_state();
1964 }
1965 
clrscr()1966 void TilesFramework::clrscr()
1967 {
1968     m_text_menu.clear();
1969 
1970     cgotoxy(1, 1);
1971 
1972     set_need_redraw();
1973 }
1974 
layout_reset()1975 void TilesFramework::layout_reset()
1976 {
1977     m_layout_reset = true;
1978 }
1979 
cgotoxy(int x,int y,GotoRegion region)1980 void TilesFramework::cgotoxy(int x, int y, GotoRegion region)
1981 {
1982     m_print_x = x - 1;
1983     m_print_y = y - 1;
1984 
1985     bool crt_popup = region == GOTO_CRT && !m_menu_stack.empty() &&
1986             m_menu_stack.back().type == UIStackFrame::CRT;
1987     m_print_area = crt_popup ? &m_text_menu : nullptr;
1988     m_cursor_region = region;
1989 }
1990 
redraw()1991 void TilesFramework::redraw()
1992 {
1993     if (!has_receivers())
1994     {
1995         if (m_mcache_ref_done)
1996         {
1997             _mcache_ref(false);
1998             m_mcache_ref_done = false;
1999         }
2000         return;
2001     }
2002 
2003     if (m_layout_reset)
2004     {
2005         _send_layout();
2006         m_layout_reset = false;
2007     }
2008 
2009     if (m_last_text_cursor != m_text_cursor)
2010     {
2011         _send_text_cursor(m_text_cursor);
2012         m_last_text_cursor = m_text_cursor;
2013     }
2014 
2015     if (m_last_ui_state != m_ui_state)
2016     {
2017         _send_ui_state(m_ui_state);
2018         m_last_ui_state = m_ui_state;
2019     }
2020 
2021     m_text_menu.send();
2022 
2023     _send_player();
2024     _send_messages();
2025 
2026     if (m_need_redraw && m_view_loaded)
2027     {
2028         if (m_current_flash_colour != m_next_flash_colour)
2029         {
2030             send_message("{\"msg\":\"flash\",\"col\":%d}",
2031                          m_next_flash_colour);
2032             m_current_flash_colour = m_next_flash_colour;
2033         }
2034         _send_map(false);
2035     }
2036 
2037     m_need_redraw = false;
2038     m_last_tick_redraw = get_milliseconds();
2039 }
2040 
update_minimap(const coord_def & gc)2041 void TilesFramework::update_minimap(const coord_def& gc)
2042 {
2043     if (gc.x < 0 || gc.x >= GXM || gc.y < 0 || gc.y >= GYM)
2044         return;
2045 
2046     mark_for_redraw(gc);
2047 }
2048 
clear_minimap()2049 void TilesFramework::clear_minimap()
2050 {
2051     m_origin = coord_def(-1, -1);
2052     // Changing the origin invalidates coordinates on the client side
2053     m_current_gc = coord_def(-1, -1);
2054     m_need_full_map = true;
2055 }
2056 
update_minimap_bounds()2057 void TilesFramework::update_minimap_bounds()
2058 {
2059 }
2060 
update_tabs()2061 void TilesFramework::update_tabs()
2062 {
2063 }
2064 
place_cursor(cursor_type type,const coord_def & gc)2065 void TilesFramework::place_cursor(cursor_type type, const coord_def &gc)
2066 {
2067     // This is mainly copied from DungeonRegion::place_cursor.
2068     coord_def result = gc;
2069 
2070     // If we're only looking for a direction, put the mouse
2071     // cursor next to the player to let them know that their
2072     // spell/wand will only go one square.
2073     if (mouse_control::current_mode() == MOUSE_MODE_TARGET_DIR
2074         && type == CURSOR_MOUSE && gc != INVALID_COORD)
2075     {
2076         coord_def delta = gc - you.pos();
2077 
2078         int ax = abs(delta.x);
2079         int ay = abs(delta.y);
2080 
2081         result = you.pos();
2082         if (1000 * ay < 414 * ax)
2083             result += (delta.x > 0) ? coord_def(1, 0) : coord_def(-1, 0);
2084         else if (1000 * ax < 414 * ay)
2085             result += (delta.y > 0) ? coord_def(0, 1) : coord_def(0, -1);
2086         else if (delta.x > 0)
2087             result += (delta.y > 0) ? coord_def(1, 1) : coord_def(1, -1);
2088         else if (delta.x < 0)
2089             result += (delta.y > 0) ? coord_def(-1, 1) : coord_def(-1, -1);
2090     }
2091 
2092     if (m_cursor[type] != result)
2093     {
2094         m_cursor[type] = result;
2095         if (type == CURSOR_MOUSE)
2096             m_last_clicked_grid = coord_def();
2097 
2098         // if map is going to be updated, send the cursor after that
2099         if (type == CURSOR_MAP && m_need_full_map)
2100             return;
2101 
2102         _send_cursor(type);
2103     }
2104 }
2105 
clear_text_tags(text_tag_type)2106 void TilesFramework::clear_text_tags(text_tag_type /*type*/)
2107 {
2108 }
2109 
add_text_tag(text_tag_type,const string &,const coord_def &)2110 void TilesFramework::add_text_tag(text_tag_type /*type*/, const string &/*tag*/,
2111                                   const coord_def &/*gc*/)
2112 {
2113 }
2114 
add_text_tag(text_tag_type,const monster_info &)2115 void TilesFramework::add_text_tag(text_tag_type /*type*/, const monster_info& /*mon*/)
2116 {
2117 }
2118 
get_cursor() const2119 const coord_def &TilesFramework::get_cursor() const
2120 {
2121     return m_cursor[CURSOR_MOUSE];
2122 }
2123 
set_need_redraw(unsigned int min_tick_delay)2124 void TilesFramework::set_need_redraw(unsigned int min_tick_delay)
2125 {
2126     unsigned int ticks = (get_milliseconds() - m_last_tick_redraw);
2127     if (min_tick_delay && ticks <= min_tick_delay)
2128         return;
2129 
2130     m_need_redraw = true;
2131 }
2132 
need_redraw() const2133 bool TilesFramework::need_redraw() const
2134 {
2135     return m_need_redraw;
2136 }
2137 
textcolour(int col)2138 void TilesFramework::textcolour(int col)
2139 {
2140     m_print_fg = col & 0xF;
2141     m_print_bg = (col >> 4) & 0xF;
2142 }
2143 
textbackground(int col)2144 void TilesFramework::textbackground(int col)
2145 {
2146     m_print_bg = col;
2147 }
2148 
put_ucs_string(char32_t * str)2149 void TilesFramework::put_ucs_string(char32_t *str)
2150 {
2151     if (m_print_area == nullptr)
2152         return;
2153 
2154     while (*str)
2155     {
2156         if (*str == '\r')
2157             continue;
2158 
2159         if (*str == '\n')
2160         {
2161             m_print_x = 0;
2162             m_print_y++;
2163             // TODO: Clear end of line?
2164         }
2165         else
2166         {
2167             if (m_print_x >= m_print_area->mx)
2168             {
2169                 m_print_x = 0;
2170                 m_print_y++;
2171             }
2172 
2173             if (m_print_y < m_print_area->my)
2174             {
2175                 m_print_area->put_character(*str, m_print_fg, m_print_bg,
2176                                             m_print_x, m_print_y);
2177             }
2178 
2179             m_print_x++;
2180         }
2181 
2182         str++;
2183     }
2184 }
2185 
clear_to_end_of_line()2186 void TilesFramework::clear_to_end_of_line()
2187 {
2188     if (m_print_area == nullptr || m_print_y >= m_print_area->my)
2189         return;
2190 
2191     for (int x = m_print_x; x < m_print_area->mx; ++x)
2192         m_print_area->put_character(' ', m_print_fg, m_print_bg, x, m_print_y);
2193 }
2194 
mark_for_redraw(const coord_def & gc)2195 void TilesFramework::mark_for_redraw(const coord_def& gc)
2196 {
2197     mark_dirty(gc);
2198     m_cells_needing_redraw[gc.y * GXM + gc.x] = true;
2199 }
2200 
mark_dirty(const coord_def & gc)2201 void TilesFramework::mark_dirty(const coord_def& gc)
2202 {
2203     m_dirty_cells[gc.y * GXM + gc.x] = true;
2204 }
2205 
mark_clean(const coord_def & gc)2206 void TilesFramework::mark_clean(const coord_def& gc)
2207 {
2208     m_cells_needing_redraw[gc.y * GXM + gc.x] = false;
2209     m_dirty_cells[gc.y * GXM + gc.x] = false;
2210 }
2211 
is_dirty(const coord_def & gc)2212 bool TilesFramework::is_dirty(const coord_def& gc)
2213 {
2214     return m_dirty_cells[gc.y * GXM + gc.x];
2215 }
2216 
cell_needs_redraw(const coord_def & gc)2217 bool TilesFramework::cell_needs_redraw(const coord_def& gc)
2218 {
2219     return m_cells_needing_redraw[gc.y * GXM + gc.x];
2220 }
2221 
write_message_escaped(const string & s)2222 void TilesFramework::write_message_escaped(const string& s)
2223 {
2224     for (unsigned char c : s)
2225     {
2226         if (c == '"')
2227             m_msg_buf.append("\\\"");
2228         else if (c == '\\')
2229             m_msg_buf.append("\\\\");
2230         else if (c < 0x20)
2231         {
2232             char buf[7];
2233             snprintf(buf, sizeof(buf), "\\u%04x", c);
2234             m_msg_buf.append(buf);
2235         }
2236         else
2237             m_msg_buf.push_back(c);
2238     }
2239 }
2240 
json_open(const string & name,char opener,char type)2241 void TilesFramework::json_open(const string& name, char opener, char type)
2242 {
2243     m_json_stack.resize(m_json_stack.size() + 1);
2244     JsonFrame& fr = m_json_stack.back();
2245     fr.start = m_msg_buf.size();
2246 
2247     json_write_comma();
2248     if (!name.empty())
2249         json_write_name(name);
2250 
2251     m_msg_buf.append(1, opener);
2252 
2253     fr.prefix_end = m_msg_buf.size();
2254     fr.type = type;
2255 }
2256 
json_treat_as_empty()2257 void TilesFramework::json_treat_as_empty()
2258 {
2259     if (m_json_stack.empty())
2260         die("json error: empty stack");
2261     m_json_stack.back().prefix_end = m_msg_buf.size();
2262 }
2263 
json_treat_as_nonempty()2264 void TilesFramework::json_treat_as_nonempty()
2265 {
2266     if (m_json_stack.empty())
2267         die("json error: empty stack");
2268     m_json_stack.back().prefix_end = -1;
2269 }
2270 
json_is_empty()2271 bool TilesFramework::json_is_empty()
2272 {
2273     if (m_json_stack.empty())
2274         die("json error: empty stack");
2275     return m_json_stack.back().prefix_end == (int) m_msg_buf.size();
2276 }
2277 
json_close(bool erase_if_empty,char type)2278 void TilesFramework::json_close(bool erase_if_empty, char type)
2279 {
2280     if (m_json_stack.empty())
2281         die("json error: attempting to close object/array on empty stack");
2282     if (m_json_stack.back().type != type)
2283         die("json error: attempting to close wrong type");
2284 
2285     if (erase_if_empty && json_is_empty())
2286         m_msg_buf.resize(m_json_stack.back().start);
2287     else
2288         m_msg_buf.append(1, type);
2289 
2290     m_json_stack.pop_back();
2291 }
2292 
json_open_object(const string & name)2293 void TilesFramework::json_open_object(const string& name)
2294 {
2295     json_open(name, '{', '}');
2296 }
2297 
json_close_object(bool erase_if_empty)2298 void TilesFramework::json_close_object(bool erase_if_empty)
2299 {
2300     json_close(erase_if_empty, '}');
2301 }
2302 
json_open_array(const string & name)2303 void TilesFramework::json_open_array(const string& name)
2304 {
2305     json_open(name, '[', ']');
2306 }
2307 
json_close_array(bool erase_if_empty)2308 void TilesFramework::json_close_array(bool erase_if_empty)
2309 {
2310     json_close(erase_if_empty, ']');
2311 }
2312 
json_write_comma()2313 void TilesFramework::json_write_comma()
2314 {
2315     if (m_msg_buf.empty()) return;
2316     char last = m_msg_buf[m_msg_buf.size() - 1];
2317     if (last == '{' || last == '[' || last == ',' || last == ':') return;
2318     write_message(",");
2319 }
2320 
json_write_name(const string & name)2321 void TilesFramework::json_write_name(const string& name)
2322 {
2323     json_write_comma();
2324 
2325     write_message("\"");
2326     write_message_escaped(name);
2327     write_message("\":");
2328 }
2329 
json_write_int(int value)2330 void TilesFramework::json_write_int(int value)
2331 {
2332     json_write_comma();
2333 
2334     write_message("%d", value);
2335 }
2336 
json_write_int(const string & name,int value)2337 void TilesFramework::json_write_int(const string& name, int value)
2338 {
2339     if (!name.empty())
2340         json_write_name(name);
2341 
2342     json_write_int(value);
2343 }
2344 
json_write_bool(bool value)2345 void TilesFramework::json_write_bool(bool value)
2346 {
2347     json_write_comma();
2348 
2349     if (value)
2350         write_message("true");
2351     else
2352         write_message("false");
2353 }
2354 
json_write_bool(const string & name,bool value)2355 void TilesFramework::json_write_bool(const string& name, bool value)
2356 {
2357     if (!name.empty())
2358         json_write_name(name);
2359 
2360     json_write_bool(value);
2361 }
2362 
json_write_null()2363 void TilesFramework::json_write_null()
2364 {
2365     json_write_comma();
2366 
2367     write_message("null");
2368 }
2369 
json_write_null(const string & name)2370 void TilesFramework::json_write_null(const string& name)
2371 {
2372     if (!name.empty())
2373         json_write_name(name);
2374 
2375     json_write_null();
2376 }
2377 
json_write_string(const string & value)2378 void TilesFramework::json_write_string(const string& value)
2379 {
2380     json_write_comma();
2381 
2382     write_message("\"");
2383     write_message_escaped(value);
2384     write_message("\"");
2385 }
2386 
json_write_string(const string & name,const string & value)2387 void TilesFramework::json_write_string(const string& name, const string& value)
2388 {
2389     if (!name.empty())
2390         json_write_name(name);
2391 
2392     json_write_string(value);
2393 }
2394 
is_tiles()2395 bool is_tiles()
2396 {
2397     return tiles.is_controlled_from_web();
2398 }
2399 #endif
2400