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 ¤t_sc, const screen_cell_t &next_sc,
1363 const map_cell ¤t_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 ¤t_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