4 * @author 2014 Deskull rearranged comment for Doxygen.
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"
27 #include "system/h-basic.h"
28 #ifdef HAVE_SYS_TIME_H
31 #define WAIT 100 * 1000 /* ブラウズ側のウエイト(us単位) */
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;
41 static long epoch_time; /* バッファ開始時刻 */
42 static int browse_delay; /* 表示するまでの時間(100ms単位)(この間にラグを吸収する) */
44 static int movie_mode;
46 /* 描画する時刻を覚えておくキュー構造体 */
48 int time[FRESH_QUEUE_SIZE];
55 std::vector<char> buf{};
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);
70 static void disable_chuukei_server(void)
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;
80 /* ANSI Cによればstatic変数は0で初期化されるが一応初期化する */
81 static void init_buffer(void)
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);
89 /* 現在の時間を100ms単位で取得する */
90 static long get_current_time(void)
93 return timeGetTime() / 100;
96 gettimeofday(&tv, nullptr);
98 return tv.tv_sec * 10 + tv.tv_usec / 100000;
103 * @brief リングバッファにヘッダとペイロードを追加する
105 * @param payload ペイロード (オプション)
108 static errr insert_ringbuf(std::string_view header, std::string_view payload = "")
111 fd_write(movie_fd, header.data(), header.length());
112 if (!payload.empty()) {
113 fd_write(movie_fd, payload.data(), payload.length());
115 fd_write(movie_fd, "", 1);
120 auto all_length = header.length() + payload.length();
121 if (ring.inlen + all_length + 1 >= RINGBUF_SIZE) {
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());
131 ring.buf[ring.wptr + all_length] = '\0';
132 ring.wptr += all_length + 1;
134 /* バッファの終端までに収まらない(ピッタリ収まる場合も含む) */
136 int head = RINGBUF_SIZE - ring.wptr; /* 前半 */
137 int tail = all_length - head; /* 後半 */
139 if ((int)header.length() <= head) {
140 std::copy_n(header.begin(), header.length(), ring.buf.begin() + ring.wptr);
141 head -= header.length();
143 std::copy_n(payload.begin(), head, ring.buf.begin() + ring.wptr + header.length());
145 std::copy_n(payload.data() + head, tail, ring.buf.begin());
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());
151 std::copy_n(payload.begin(), tail - part, ring.buf.begin() + part);
154 ring.buf[tail] = '\0';
155 ring.wptr = tail + 1;
158 ring.inlen += all_length + 1;
164 /* strが同じ文字の繰り返しかどうか調べる */
165 static bool string_is_repeat(concptr str, int len)
179 for (i = 1; i < len; i++) {
181 if (c != str[i] || iskanji(str[i])) {
195 /* マルチバイト文字を分割せずに str を分割する長さを len 以下で返す */
196 static int find_split(concptr str, int len)
199 * SJIS が定義されている場合でも、str は常に EUC-JP としてエンコードされます。
200 * 他の場所で行われているような 3 バイトのエンコーディングは想定していません。
202 int last_split = 0, i = 0;
204 if (iseuckanji(str[i])) {
218 static errr send_text_to_chuukei_server(TERM_LEN x, TERM_LEN y, int len, TERM_COLOR col, concptr str)
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);
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));
232 std::string formatted_text;
234 formatted_text = format("n%c%c%c%c%c", x + 1, y + 1, len, col, *str);
236 formatted_text = format("s%c%c%c%c", x + 1, y + 1, col, *str);
239 insert_ringbuf(formatted_text);
240 return (*old_text_hook)(x, y, len, col, str);
243 #if defined(SJIS) && defined(JP)
244 std::string buffer = str; // strは書き換わって欲しくないのでコピーする.
245 auto *payload = buffer.data();
248 const auto *payload = str;
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));
255 payload += split_len;
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);
262 static errr send_wipe_to_chuukei_server(int x, int y, int len)
264 while (len > SPLIT_MAX) {
265 insert_ringbuf(format("w%c%c%c", x + 1, y + 1, SPLIT_MAX));
269 insert_ringbuf(format("w%c%c%c", x + 1, y + 1, len));
271 return (*old_wipe_hook)(x, y, len);
274 static errr send_xtra_to_chuukei_server(int n, int v)
276 if (n == TERM_XTRA_CLEAR || n == TERM_XTRA_FRESH || n == TERM_XTRA_SHAPE) {
277 insert_ringbuf(format("x%c", n + 1));
279 if (n == TERM_XTRA_FRESH) {
280 insert_ringbuf("d", std::to_string(get_current_time() - epoch_time));
284 /* Verify the hook */
285 if (!old_xtra_hook) {
289 return (*old_xtra_hook)(n, v);
292 static errr send_curs_to_chuukei_server(int x, int y)
294 insert_ringbuf(format("c%c%c", x + 1, y + 1));
296 return (*old_curs_hook)(x, y);
299 static errr send_bigcurs_to_chuukei_server(int x, int y)
301 insert_ringbuf(format("C%c%c", x + 1, y + 1));
303 return (*old_bigcurs_hook)(x, y);
307 * Prepare z-term hooks to call send_*_to_chuukei_server()'s
309 void prepare_chuukei_hooks(void)
311 term_type *t0 = angband_terms[0];
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;
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;
329 * Prepare z-term hooks to call send_*_to_chuukei_server()'s
331 void prepare_movie_hooks(PlayerType *player_ptr)
333 TermCenteredOffsetSetter tcos(std::nullopt, std::nullopt);
337 disable_chuukei_server();
339 msg_print(_("録画を終了しました。", "Stopped recording."));
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) {
352 const auto &path = path_build(ANGBAND_DIR_USER, *movie_filename);
353 auto fd = fd_open(path, O_RDONLY);
355 const auto &filename = path.string();
357 std::string query = _("現存するファイルに上書きしますか? (", "Replace existing file ");
358 query.append(filename);
359 query.append(_(")", "? "));
360 if (!input_check(query)) {
364 movie_fd = fd_open(path, O_WRONLY | O_TRUNC);
366 movie_fd = fd_make(path);
370 msg_print(_("ファイルを開けません!", "Can not open file."));
375 prepare_chuukei_hooks();
376 do_cmd_redraw(player_ptr);
379 static int handle_movie_timestamp_data(int timestamp)
381 static int initialized = false;
385 /* バッファリングし始めの時間を保存しておく */
386 epoch_time = get_current_time();
387 epoch_time += browse_delay;
388 epoch_time -= timestamp;
389 // time_diff = current_time - timestamp;
393 /* 描画キューに保存し、保存位置を進める */
394 fresh_queue.time[fresh_queue.tail] = timestamp;
397 /* キューの最後尾に到達したら先頭に戻す */
398 fresh_queue.tail %= FRESH_QUEUE_SIZE;
404 static int read_movie_file(void)
406 static char recv_buf[RECVBUF_SIZE];
407 static int remain_bytes = 0;
411 recv_bytes = read(movie_fd, recv_buf + remain_bytes, RECVBUF_SIZE - remain_bytes);
413 if (recv_bytes <= 0) {
417 /* 前回残ったデータ量に今回読んだデータ量を追加 */
418 remain_bytes += recv_bytes;
420 for (i = 0, start = 0; i < remain_bytes; i++) {
421 /* データのくぎり('\0')を探す */
422 if (recv_buf[i] == '\0') {
423 /* 'd'で始まるデータ(タイムスタンプ)の場合は
425 if ((recv_buf[start] == 'd') && (handle_movie_timestamp_data(atoi(recv_buf + start + 1)) < 0)) {
430 if (insert_ringbuf(std::string_view(recv_buf + start, i - start)) < 0) {
438 if (remain_bytes >= start) {
439 memmove(recv_buf, recv_buf + start, remain_bytes - start);
440 remain_bytes -= start;
450 /* Win版の床の中点と壁の豆腐をピリオドとシャープにする。*/
451 static void win2unix(int col, char *buf)
469 } else if (*buf == 31) {
477 static bool get_nextbuf(char *buf)
482 *ptr = ring.buf[ring.rptr++];
484 if (ring.rptr == RINGBUF_SIZE) {
487 if (*ptr++ == '\0') {
499 /* プレイホストのマップが大きいときクライアントのマップもリサイズする */
500 static void update_term_size(int x, int y, int len)
502 const auto &[ox, oy] = term_get_size();
515 if (nx != ox || ny != oy) {
520 static bool flush_ringbuf_client()
523 if (fresh_queue.next == fresh_queue.tail) {
528 if (fresh_queue.time[fresh_queue.next] > get_current_time() - epoch_time) {
532 /* 時間情報(区切り)が得られるまで書く */
534 while (get_nextbuf(buf)) {
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];
553 #if defined(SJIS) && defined(JP)
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;
565 for (auto i = 1; i < len + 1; i++) {
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;
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;
589 update_term_size(x, y, len);
590 (void)((*angband_terms[0]->wipe_hook)(x, y, len));
593 if (x == TERM_XTRA_CLEAR) {
597 (void)((*angband_terms[0]->xtra_hook)(x, 0));
600 update_term_size(x, y, 1);
601 (void)((*angband_terms[0]->curs_hook)(x, y));
604 update_term_size(x, y, 1);
605 (void)((*angband_terms[0]->bigcurs_hook)(x, y));
611 if (fresh_queue.next == FRESH_QUEUE_SIZE) {
612 fresh_queue.next = 0;
617 void prepare_browse_movie_without_path_build(const std::filesystem::path &path)
619 movie_fd = fd_open(path, O_RDONLY);
623 void browse_movie(void)
627 term_xtra(TERM_XTRA_REACT, 0);
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);
634 /* ソケットにデータが来ているかどうか調べる */
646 void prepare_browse_movie_with_path_build(std::string_view filename)
648 const auto &path = path_build(ANGBAND_DIR_USER, filename);
649 movie_fd = fd_open(path, O_RDONLY);