OSDN Git Service

Merge pull request #3845 from dis-/feature/refactor-save-floors
[hengbandforosx/hengbandosx.git] / src / io / record-play-movie.cpp
1 /*!
2  * @brief 録画・再生機能
3  * @date 2014/01/02
4  * @author 2014 Deskull rearranged comment for Doxygen.
5  */
6
7 #include "io/record-play-movie.h"
8 #include "cmd-io/cmd-dump.h"
9 #include "cmd-visual/cmd-draw.h"
10 #include "core/asking-player.h"
11 #include "io/files-util.h"
12 #include "io/signal-handlers.h"
13 #include "locale/japanese.h"
14 #include "system/player-type-definition.h"
15 #include "term/gameterm.h"
16 #include "term/z-form.h"
17 #include "util/angband-files.h"
18 #include "view/display-messages.h"
19 #include <algorithm>
20 #include <sstream>
21 #include <vector>
22
23 #ifdef WINDOWS
24 #include <windows.h>
25 #define WAIT 100
26 #else
27 #include "system/h-basic.h"
28 #ifdef HAVE_SYS_TIME_H
29 #include <sys/time.h>
30 #endif
31 #define WAIT 100 * 1000 /* ブラウズ側のウエイト(us単位) */
32 #endif
33
34 #define RINGBUF_SIZE 1024 * 1024
35 #define FRESH_QUEUE_SIZE 4096
36 #define DEFAULT_DELAY 50
37 #define RECVBUF_SIZE 1024
38 /* 「n」、「t」、および「w」コマンドでは、長さが「signed char」に配置されるときに負の値を回避するために、これよりも長い長さを使用しないでください。 */
39 static constexpr auto SPLIT_MAX = 127;
40
41 static long epoch_time; /* バッファ開始時刻 */
42 static int browse_delay; /* 表示するまでの時間(100ms単位)(この間にラグを吸収する) */
43 static int movie_fd;
44 static int movie_mode;
45
46 /* 描画する時刻を覚えておくキュー構造体 */
47 static struct {
48     int time[FRESH_QUEUE_SIZE];
49     int next;
50     int tail;
51 } fresh_queue;
52
53 /* リングバッファ構造体 */
54 static struct {
55     std::vector<char> buf{};
56     int wptr = 0;
57     int rptr = 0;
58     int inlen = 0;
59 } ring;
60
61 /*
62  * Original hooks
63  */
64 static errr (*old_xtra_hook)(int n, int v);
65 static errr (*old_curs_hook)(int x, int y);
66 static errr (*old_bigcurs_hook)(int x, int y);
67 static errr (*old_wipe_hook)(int x, int y, int n);
68 static errr (*old_text_hook)(int x, int y, int n, TERM_COLOR a, concptr s);
69
70 static void disable_chuukei_server(void)
71 {
72     term_type *t = angband_terms[0];
73     t->xtra_hook = old_xtra_hook;
74     t->curs_hook = old_curs_hook;
75     t->bigcurs_hook = old_bigcurs_hook;
76     t->wipe_hook = old_wipe_hook;
77     t->text_hook = old_text_hook;
78 }
79
80 /* ANSI Cによればstatic変数は0で初期化されるが一応初期化する */
81 static void init_buffer(void)
82 {
83     fresh_queue.next = fresh_queue.tail = 0;
84     ring.wptr = ring.rptr = ring.inlen = 0;
85     fresh_queue.time[0] = 0;
86     ring.buf.resize(RINGBUF_SIZE);
87 }
88
89 /* 現在の時間を100ms単位で取得する */
90 static long get_current_time(void)
91 {
92 #ifdef WINDOWS
93     return timeGetTime() / 100;
94 #else
95     struct timeval tv;
96     gettimeofday(&tv, nullptr);
97
98     return tv.tv_sec * 10 + tv.tv_usec / 100000;
99 #endif
100 }
101
102 /*!
103  * @brief リングバッファにヘッダとペイロードを追加する
104  * @param header ヘッダ
105  * @param payload ペイロード (オプション)
106  * @return エラーコード
107  */
108 static errr insert_ringbuf(std::string_view header, std::string_view payload = "")
109 {
110     if (movie_mode) {
111         fd_write(movie_fd, header.data(), header.length());
112         if (!payload.empty()) {
113             fd_write(movie_fd, payload.data(), payload.length());
114         }
115         fd_write(movie_fd, "", 1);
116         return 0;
117     }
118
119     /* バッファをオーバー */
120     auto all_length = header.length() + payload.length();
121     if (ring.inlen + all_length + 1 >= RINGBUF_SIZE) {
122         return -1;
123     }
124
125     /* バッファの終端までに収まる */
126     if (ring.wptr + all_length + 1 < RINGBUF_SIZE) {
127         std::copy_n(header.begin(), header.length(), ring.buf.begin() + ring.wptr);
128         if (!payload.empty()) {
129             std::copy_n(payload.begin(), payload.length(), ring.buf.begin() + ring.wptr + header.length());
130         }
131         ring.buf[ring.wptr + all_length] = '\0';
132         ring.wptr += all_length + 1;
133     }
134     /* バッファの終端までに収まらない(ピッタリ収まる場合も含む) */
135     else {
136         int head = RINGBUF_SIZE - ring.wptr; /* 前半 */
137         int tail = all_length - head; /* 後半 */
138
139         if ((int)header.length() <= head) {
140             std::copy_n(header.begin(), header.length(), ring.buf.begin() + ring.wptr);
141             head -= header.length();
142             if (head > 0) {
143                 std::copy_n(payload.begin(), head, ring.buf.begin() + ring.wptr + header.length());
144             }
145             std::copy_n(payload.data() + head, tail, ring.buf.begin());
146         } else {
147             std::copy_n(header.begin(), head, ring.buf.begin() + ring.wptr);
148             int part = header.length() - head;
149             std::copy_n(header.data() + head, part, ring.buf.begin());
150             if (tail > part) {
151                 std::copy_n(payload.begin(), tail - part, ring.buf.begin() + part);
152             }
153         }
154         ring.buf[tail] = '\0';
155         ring.wptr = tail + 1;
156     }
157
158     ring.inlen += all_length + 1;
159
160     /* Success */
161     return 0;
162 }
163
164 /* strが同じ文字の繰り返しかどうか調べる */
165 static bool string_is_repeat(concptr str, int len)
166 {
167     char c = str[0];
168     int i;
169
170     if (len < 2) {
171         return false;
172     }
173 #ifdef JP
174     if (iskanji(c)) {
175         return false;
176     }
177 #endif
178
179     for (i = 1; i < len; i++) {
180 #ifdef JP
181         if (c != str[i] || iskanji(str[i])) {
182             return false;
183         }
184 #else
185         if (c != str[i]) {
186             return false;
187         }
188 #endif
189     }
190
191     return true;
192 }
193
194 #ifdef JP
195 /* マルチバイト文字を分割せずに str を分割する長さを len 以下で返す */
196 static int find_split(concptr str, int len)
197 {
198     /*
199      * SJIS が定義されている場合でも、str は常に EUC-JP としてエンコードされます。
200      * 他の場所で行われているような 3 バイトのエンコーディングは想定していません。
201      */
202     int last_split = 0, i = 0;
203     while (i < len) {
204         if (iseuckanji(str[i])) {
205             if (i < len - 1) {
206                 last_split = i + 2;
207             }
208             i += 2;
209         } else {
210             last_split = i + 1;
211             ++i;
212         }
213     }
214     return last_split;
215 }
216 #endif
217
218 static errr send_text_to_chuukei_server(TERM_LEN x, TERM_LEN y, int len, TERM_COLOR col, concptr str)
219 {
220     if (len == 1) {
221         insert_ringbuf(format("s%c%c%c%c", x + 1, y + 1, col, *str));
222         return (*old_text_hook)(x, y, len, col, str);
223     }
224
225     if (string_is_repeat(str, len)) {
226         while (len > SPLIT_MAX) {
227             insert_ringbuf(format("n%c%c%c%c%c", x + 1, y + 1, SPLIT_MAX, col, *str));
228             x += SPLIT_MAX;
229             len -= SPLIT_MAX;
230         }
231
232         std::string formatted_text;
233         if (len > 1) {
234             formatted_text = format("n%c%c%c%c%c", x + 1, y + 1, len, col, *str);
235         } else {
236             formatted_text = format("s%c%c%c%c", x + 1, y + 1, col, *str);
237         }
238
239         insert_ringbuf(formatted_text);
240         return (*old_text_hook)(x, y, len, col, str);
241     }
242
243 #if defined(SJIS) && defined(JP)
244     std::string buffer = str; // strは書き換わって欲しくないのでコピーする.
245     auto *payload = buffer.data();
246     sjis2euc(payload);
247 #else
248     const auto *payload = str;
249 #endif
250     while (len > SPLIT_MAX) {
251         auto split_len = _(find_split(payload, SPLIT_MAX), SPLIT_MAX);
252         insert_ringbuf(format("t%c%c%c%c", x + 1, y + 1, split_len, col), std::string_view(payload, split_len));
253         x += split_len;
254         len -= split_len;
255         payload += split_len;
256     }
257
258     insert_ringbuf(format("t%c%c%c%c", x + 1, y + 1, len, col), std::string_view(payload, len));
259     return (*old_text_hook)(x, y, len, col, str);
260 }
261
262 static errr send_wipe_to_chuukei_server(int x, int y, int len)
263 {
264     while (len > SPLIT_MAX) {
265         insert_ringbuf(format("w%c%c%c", x + 1, y + 1, SPLIT_MAX));
266         x += SPLIT_MAX;
267         len -= SPLIT_MAX;
268     }
269     insert_ringbuf(format("w%c%c%c", x + 1, y + 1, len));
270
271     return (*old_wipe_hook)(x, y, len);
272 }
273
274 static errr send_xtra_to_chuukei_server(int n, int v)
275 {
276     if (n == TERM_XTRA_CLEAR || n == TERM_XTRA_FRESH || n == TERM_XTRA_SHAPE) {
277         insert_ringbuf(format("x%c", n + 1));
278
279         if (n == TERM_XTRA_FRESH) {
280             insert_ringbuf("d", std::to_string(get_current_time() - epoch_time));
281         }
282     }
283
284     /* Verify the hook */
285     if (!old_xtra_hook) {
286         return -1;
287     }
288
289     return (*old_xtra_hook)(n, v);
290 }
291
292 static errr send_curs_to_chuukei_server(int x, int y)
293 {
294     insert_ringbuf(format("c%c%c", x + 1, y + 1));
295
296     return (*old_curs_hook)(x, y);
297 }
298
299 static errr send_bigcurs_to_chuukei_server(int x, int y)
300 {
301     insert_ringbuf(format("C%c%c", x + 1, y + 1));
302
303     return (*old_bigcurs_hook)(x, y);
304 }
305
306 /*
307  * Prepare z-term hooks to call send_*_to_chuukei_server()'s
308  */
309 void prepare_chuukei_hooks(void)
310 {
311     term_type *t0 = angband_terms[0];
312
313     /* Save original z-term hooks */
314     old_xtra_hook = t0->xtra_hook;
315     old_curs_hook = t0->curs_hook;
316     old_bigcurs_hook = t0->bigcurs_hook;
317     old_wipe_hook = t0->wipe_hook;
318     old_text_hook = t0->text_hook;
319
320     /* Prepare z-term hooks */
321     t0->xtra_hook = send_xtra_to_chuukei_server;
322     t0->curs_hook = send_curs_to_chuukei_server;
323     t0->bigcurs_hook = send_bigcurs_to_chuukei_server;
324     t0->wipe_hook = send_wipe_to_chuukei_server;
325     t0->text_hook = send_text_to_chuukei_server;
326 }
327
328 /*
329  * Prepare z-term hooks to call send_*_to_chuukei_server()'s
330  */
331 void prepare_movie_hooks(PlayerType *player_ptr)
332 {
333     TermCenteredOffsetSetter tcos(std::nullopt, std::nullopt);
334
335     if (movie_mode) {
336         movie_mode = 0;
337         disable_chuukei_server();
338         fd_close(movie_fd);
339         msg_print(_("録画を終了しました。", "Stopped recording."));
340         return;
341     }
342
343     std::stringstream ss;
344     ss << player_ptr->base_name << ".amv";
345     auto initial_movie_filename = ss.str();
346     constexpr auto prompt = _("ムービー記録ファイル: ", "Movie file name: ");
347     const auto movie_filename = input_string(prompt, 80, initial_movie_filename.data());
348     if (!movie_filename) {
349         return;
350     }
351
352     const auto &path = path_build(ANGBAND_DIR_USER, *movie_filename);
353     auto fd = fd_open(path, O_RDONLY);
354     if (fd >= 0) {
355         const auto &filename = path.string();
356         (void)fd_close(fd);
357         std::string query = _("現存するファイルに上書きしますか? (", "Replace existing file ");
358         query.append(filename);
359         query.append(_(")", "? "));
360         if (!input_check(query)) {
361             return;
362         }
363
364         movie_fd = fd_open(path, O_WRONLY | O_TRUNC);
365     } else {
366         movie_fd = fd_make(path);
367     }
368
369     if (!movie_fd) {
370         msg_print(_("ファイルを開けません!", "Can not open file."));
371         return;
372     }
373
374     movie_mode = 1;
375     prepare_chuukei_hooks();
376     do_cmd_redraw(player_ptr);
377 }
378
379 static int handle_movie_timestamp_data(int timestamp)
380 {
381     static int initialized = false;
382
383     /* 描画キューは空かどうか? */
384     if (!initialized) {
385         /* バッファリングし始めの時間を保存しておく */
386         epoch_time = get_current_time();
387         epoch_time += browse_delay;
388         epoch_time -= timestamp;
389         // time_diff = current_time - timestamp;
390         initialized = true;
391     }
392
393     /* 描画キューに保存し、保存位置を進める */
394     fresh_queue.time[fresh_queue.tail] = timestamp;
395     fresh_queue.tail++;
396
397     /* キューの最後尾に到達したら先頭に戻す */
398     fresh_queue.tail %= FRESH_QUEUE_SIZE;
399
400     /* Success */
401     return 0;
402 }
403
404 static int read_movie_file(void)
405 {
406     static char recv_buf[RECVBUF_SIZE];
407     static int remain_bytes = 0;
408     int recv_bytes;
409     int i, start;
410
411     recv_bytes = read(movie_fd, recv_buf + remain_bytes, RECVBUF_SIZE - remain_bytes);
412
413     if (recv_bytes <= 0) {
414         return -1;
415     }
416
417     /* 前回残ったデータ量に今回読んだデータ量を追加 */
418     remain_bytes += recv_bytes;
419
420     for (i = 0, start = 0; i < remain_bytes; i++) {
421         /* データのくぎり('\0')を探す */
422         if (recv_buf[i] == '\0') {
423             /* 'd'で始まるデータ(タイムスタンプ)の場合は
424                描画キューに保存する処理を呼ぶ */
425             if ((recv_buf[start] == 'd') && (handle_movie_timestamp_data(atoi(recv_buf + start + 1)) < 0)) {
426                 return -1;
427             }
428
429             /* 受信データを保存 */
430             if (insert_ringbuf(std::string_view(recv_buf + start, i - start)) < 0) {
431                 return -1;
432             }
433
434             start = i + 1;
435         }
436     }
437     if (start > 0) {
438         if (remain_bytes >= start) {
439             memmove(recv_buf, recv_buf + start, remain_bytes - start);
440             remain_bytes -= start;
441         } else {
442             remain_bytes = 0;
443         }
444     }
445
446     return 0;
447 }
448
449 #ifndef WINDOWS
450 /* Win版の床の中点と壁の豆腐をピリオドとシャープにする。*/
451 static void win2unix(int col, char *buf)
452 {
453     char wall;
454     if (col == 9) {
455         wall = '%';
456     } else {
457         wall = '#';
458     }
459
460     while (*buf) {
461 #ifdef JP
462         if (iskanji(*buf)) {
463             buf += 2;
464             continue;
465         }
466 #endif
467         if (*buf == 127) {
468             *buf = wall;
469         } else if (*buf == 31) {
470             *buf = '.';
471         }
472         buf++;
473     }
474 }
475 #endif
476
477 static bool get_nextbuf(char *buf)
478 {
479     char *ptr = buf;
480
481     while (true) {
482         *ptr = ring.buf[ring.rptr++];
483         ring.inlen--;
484         if (ring.rptr == RINGBUF_SIZE) {
485             ring.rptr = 0;
486         }
487         if (*ptr++ == '\0') {
488             break;
489         }
490     }
491
492     if (buf[0] == 'd') {
493         return false;
494     }
495
496     return true;
497 }
498
499 /* プレイホストのマップが大きいときクライアントのマップもリサイズする */
500 static void update_term_size(int x, int y, int len)
501 {
502     const auto &[ox, oy] = term_get_size();
503     auto nx = ox;
504     auto ny = oy;
505
506     /* 横方向のチェック */
507     if (x + len > ox) {
508         nx = x + len;
509     }
510     /* 縦方向のチェック */
511     if (y + 1 > oy) {
512         ny = y + 1;
513     }
514
515     if (nx != ox || ny != oy) {
516         term_resize(nx, ny);
517     }
518 }
519
520 static bool flush_ringbuf_client()
521 {
522     /* 書くデータなし */
523     if (fresh_queue.next == fresh_queue.tail) {
524         return false;
525     }
526
527     /* まだ書くべき時でない */
528     if (fresh_queue.time[fresh_queue.next] > get_current_time() - epoch_time) {
529         return false;
530     }
531
532     /* 時間情報(区切り)が得られるまで書く */
533     char buf[1024]{};
534     while (get_nextbuf(buf)) {
535         auto id = buf[0];
536         auto x = static_cast<uint8_t>(buf[1]) - 1;
537         auto y = static_cast<uint8_t>(buf[2]) - 1;
538         int len = static_cast<uint8_t>(buf[3]);
539         uint8_t col = buf[4];
540         char *mesg;
541         if (id == 's') {
542             col = buf[3];
543             mesg = &buf[4];
544         } else {
545             mesg = &buf[5];
546         }
547 #ifndef WINDOWS
548         win2unix(col, mesg);
549 #endif
550
551         switch (id) {
552         case 't': /* 通常 */
553 #if defined(SJIS) && defined(JP)
554             euc2sjis(mesg);
555 #endif
556             update_term_size(x, y, len);
557             (void)((*angband_terms[0]->text_hook)(x, y, len, (byte)col, mesg));
558             std::copy_n(mesg, len, &game_term->scr->c[y][x]);
559             for (auto i = x; i < x + len; i++) {
560                 game_term->scr->a[y][i] = col;
561             }
562
563             break;
564         case 'n': /* 繰り返し */
565             for (auto i = 1; i < len + 1; i++) {
566                 if (i == len) {
567                     mesg[i] = '\0';
568                     break;
569                 }
570
571                 mesg[i] = mesg[0];
572             }
573
574             update_term_size(x, y, len);
575             (void)((*angband_terms[0]->text_hook)(x, y, len, (byte)col, mesg));
576             std::copy_n(mesg, len, &game_term->scr->c[y][x]);
577             for (auto i = x; i < x + len; i++) {
578                 game_term->scr->a[y][i] = col;
579             }
580
581             break;
582         case 's': /* 一文字 */
583             update_term_size(x, y, 1);
584             (void)((*angband_terms[0]->text_hook)(x, y, 1, (byte)col, mesg));
585             std::copy_n(&game_term->scr->c[y][x], 1, mesg);
586             game_term->scr->a[y][x] = col;
587             break;
588         case 'w':
589             update_term_size(x, y, len);
590             (void)((*angband_terms[0]->wipe_hook)(x, y, len));
591             break;
592         case 'x':
593             if (x == TERM_XTRA_CLEAR) {
594                 term_clear();
595             }
596
597             (void)((*angband_terms[0]->xtra_hook)(x, 0));
598             break;
599         case 'c':
600             update_term_size(x, y, 1);
601             (void)((*angband_terms[0]->curs_hook)(x, y));
602             break;
603         case 'C':
604             update_term_size(x, y, 1);
605             (void)((*angband_terms[0]->bigcurs_hook)(x, y));
606             break;
607         }
608     }
609
610     fresh_queue.next++;
611     if (fresh_queue.next == FRESH_QUEUE_SIZE) {
612         fresh_queue.next = 0;
613     }
614     return true;
615 }
616
617 void prepare_browse_movie_without_path_build(const std::filesystem::path &path)
618 {
619     movie_fd = fd_open(path, O_RDONLY);
620     init_buffer();
621 }
622
623 void browse_movie(void)
624 {
625     term_clear();
626     term_fresh();
627     term_xtra(TERM_XTRA_REACT, 0);
628
629     while (read_movie_file() == 0) {
630         while (fresh_queue.next != fresh_queue.tail) {
631             if (!flush_ringbuf_client()) {
632                 term_xtra(TERM_XTRA_FLUSH, 0);
633
634                 /* ソケットにデータが来ているかどうか調べる */
635 #ifdef WINDOWS
636                 Sleep(WAIT);
637 #else
638                 usleep(WAIT);
639 #endif
640             }
641         }
642     }
643 }
644
645 #ifndef WINDOWS
646 void prepare_browse_movie_with_path_build(std::string_view filename)
647 {
648     const auto &path = path_build(ANGBAND_DIR_USER, filename);
649     movie_fd = fd_open(path, O_RDONLY);
650     init_buffer();
651 }
652 #endif