FlightGear next
replay-internal.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: replay.cxx
3 * SPDX-FileComment: a system to record and replay FlightGear flights
4 * SPDX-FileCopyrightText: Copyright (C) 2003 Curtis L. Olson - http://www.flightgear.org/~curt
5 * SPDX-FileContributor: Updated by Thorsten Brehm, September 2011 and November 2012
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9#include <float.h>
10#include <locale>
11#include <string.h>
12
13#include <simgear/misc/sg_dir.hxx>
14#include <simgear/props/props_io.hxx>
15#include <simgear/structure/commands.hxx>
16#include <simgear/structure/exception.hxx>
17#include <simgear/structure/subsystem_mgr.hxx>
18
19#include <Main/fg_props.hxx>
21#include <Time/TimeManager.hxx>
22#include <Viewer/viewmgr.hxx>
23
24#include "continuous.hxx"
25#include "flightrecorder.hxx"
26#include "replay.hxx"
27
28
29const char* const FlightRecorderFileMagic = "FlightGear Flight Recorder Tape";
30
32{
33 size_t bytes_raw_data_old = m_bytes_raw_data;
34 size_t bytes_multiplayer_messages_old = m_bytes_multiplayer_messages;
35 size_t num_multiplayer_messages_old = m_num_multiplayer_messages;
36
39 for (auto m : multiplayer_messages) {
41 }
43
44 /* Update global stats by adding how much we have changed since last time
45 UpdateStats() was called. */
46 s_bytes_raw_data += m_bytes_raw_data - bytes_raw_data_old;
47 s_bytes_multiplayer_messages += m_bytes_multiplayer_messages - bytes_multiplayer_messages_old;
48 s_num_multiplayer_messages += m_num_multiplayer_messages - num_multiplayer_messages_old;
49
50 if (!s_prop_num) {
51 s_prop_num = fgGetNode("/sim/replay/datastats_num", true);
52 }
54 s_prop_bytes_raw_data = fgGetNode("/sim/replay/datastats_bytes_raw_data", true);
55 }
57 s_prop_bytes_multiplayer_messages = fgGetNode("/sim/replay/datastats_bytes_multiplayer_messages", true);
58 }
60 s_prop_num_multiplayer_messages = fgGetNode("/sim/replay/datastats_num_multiplayer_messages", true);
61 }
62
63 s_prop_num->setLongValue(s_num);
67}
68
73
81
89
90size_t FGReplayData::s_num = 0;
94
95SGPropertyNode_ptr FGReplayData::s_prop_num;
99
100
102enum Type {
104 Header = 0,
111};
112}
113
114/* Constructor */
116 : m_sim_time(0),
117 m_last_mt_time(0.0),
118 m_last_lt_time(0.0),
121 m_replay_error(fgGetNode("sim/replay/replay-error", true)),
122 m_record_normal_begin(fgGetNode("sim/replay/record-normal-begin", true)),
123 m_record_normal_end(fgGetNode("sim/replay/record-normal-end", true)),
124 m_sim_startup_xpos(fgGetNode("sim/startup/xpos", true)),
125 m_sim_startup_ypos(fgGetNode("sim/startup/ypos", true)),
126 m_sim_startup_xsize(fgGetNode("sim/startup/xsize", true)),
127 m_sim_startup_ysize(fgGetNode("sim/startup/ysize", true)),
128 m_simple_time_enabled(fgGetNode("/sim/time/simple-time/enabled", true)),
129 m_replay_time_prev(-1.0),
130 m_high_res_time(60.0),
131 m_medium_res_time(600.0),
132 m_low_res_time(3600.0),
133 m_medium_sample_rate(0.5), // medium term sample rate (sec)
134 m_long_sample_rate(5.0), // long term sample rate (sec)
135 m_flight_recorder(new FGFlightRecorder("replay-config")),
137 m_MultiplayMgr(globals->get_subsystem<FGMultiplayMgr>())
138{
139}
140
141/* Reads binary data from a stream into an instance of a type. */
142template <typename T>
143static void readRaw(std::istream& in, T& data)
144{
145 in.read(reinterpret_cast<char*>(&data), sizeof(data));
146}
147
148/* Writes instance of a type as binary data to a stream. */
149template <typename T>
150static void writeRaw(std::ostream& out, const T& data)
151{
152 out.write(reinterpret_cast<const char*>(&data), sizeof(data));
153}
154
155/* Read properties from stream into *node. */
156static int PropertiesRead(std::istream& in, SGPropertyNode* node)
157{
158 uint32_t buffer_len;
159 readRaw(in, buffer_len);
160 std::vector<char> buffer(buffer_len);
161 in.read(&buffer.front(), buffer.size());
162 readProperties(&buffer.front(), buffer.size() - 1, node);
163 return 0;
164}
165
166static void popupTip(const char* message, int delay)
167{
168 SGPropertyNode_ptr args(new SGPropertyNode);
169 args->setStringValue("label", message);
170 args->setIntValue("delay", delay);
171 globals->get_commands()->execute("show-message", args);
172}
173
174
175SGPath makeSavePath(FGTapeType type, SGPath* path_timeless)
176{
177 std::string tape_directory = fgGetString("/sim/replay/tape-directory", "");
178 std::string aircraftType = fgGetString("/sim/aircraft", "unknown");
179 if (path_timeless) *path_timeless = "";
180 SGPath path = SGPath(tape_directory);
181 path.append(aircraftType);
182 if (type == FGTapeType_RECOVERY) {
183 path.concat("-recovery");
184 } else {
185 if (path_timeless) *path_timeless = path;
186 time_t calendar_time = time(NULL);
187 struct tm* local_tm;
188 local_tm = localtime(&calendar_time);
189 char time_str[256];
190 strftime(time_str, 256, "-%Y%m%d-%H%M%S", local_tm);
191 path.concat(time_str);
192 }
193 if (type == FGTapeType_CONTINUOUS) {
194 path.concat("-continuous");
195 if (path_timeless) path_timeless->concat("-continuous");
196 }
197 path.concat(".fgtape");
198 if (path_timeless) path_timeless->concat(".fgtape");
199 return path;
200}
201
202/* Clear all internal buffers. */
203static void clear(FGReplayInternal& self)
204{
205 while (!self.m_short_term.empty()) {
206 delete self.m_short_term.front();
207 self.m_short_term.pop_front();
208 }
209 while (!self.m_medium_term.empty()) {
210 delete self.m_medium_term.front();
211 self.m_medium_term.pop_front();
212 }
213 while (!self.m_long_term.empty()) {
214 delete self.m_long_term.front();
215 self.m_long_term.pop_front();
216 }
217 while (!self.m_recycler.empty()) {
218 delete self.m_recycler.front();
219 self.m_recycler.pop_front();
220 }
221
222 // clear messages belonging to old replay session
223 fgGetNode("/sim/replay/messages", 0, true)->removeChildren("msg");
224}
225
226/* Destructor */
228{
229 if (m_continuous->m_out.is_open()) {
230 m_continuous->m_out.close();
231 }
232 clear(*this);
233}
234
235/* Initialize the data structures */
237{
238 m_disable_replay = fgGetNode("/sim/replay/disable", true);
239 m_replay_master = fgGetNode("/sim/replay/replay-state", true);
240 m_replay_master_eof = fgGetNode("/sim/replay/replay-state-eof", true);
241 m_replay_time = fgGetNode("/sim/replay/time", true);
242 m_replay_time_str = fgGetNode("/sim/replay/time-str", true);
243 m_replay_looped = fgGetNode("/sim/replay/looped", true);
244 m_replay_duration_act = fgGetNode("/sim/replay/duration-act", true);
245 m_speed_up = fgGetNode("/sim/speed-up", true);
246 m_replay_multiplayer = fgGetNode("/sim/replay/record-multiplayer", true);
247 m_recovery_period = fgGetNode("/sim/replay/recovery-period", true);
248 m_log_frame_times = fgGetNode("/sim/replay/log-frame-times", true);
249
250 // alias to keep backward compatibility
251 fgGetNode("/sim/freeze/replay-state", true)->alias(m_replay_master, false);
252
253 reinit();
254}
255
256/* Loads messages from /sim/replay/messages. */
258{
259 // load messages
260 self.m_replay_messages.clear();
261 simgear::PropertyList msgs = fgGetNode("/sim/replay/messages", true)->getChildren("msg");
262
263 for (auto it = msgs.begin(); it != msgs.end(); ++it) {
264 std::string msgText = (*it)->getStringValue("text", "");
265 const double msgTime = (*it)->getDoubleValue("time", -1.0);
266 std::string msgSpeaker = (*it)->getStringValue("speaker", "pilot");
267 if (!msgText.empty() && msgTime >= 0) {
268 FGReplayMessages data;
269 data.sim_time = msgTime;
270 data.message = msgText;
271 data.speaker = msgSpeaker;
272 self.m_replay_messages.push_back(data);
273 }
274 }
275 self.m_current_msg = self.m_replay_messages.begin();
276}
277
279{
280 // Create an estimated nr of required ReplayData objects
281 // 120 is an estimated maximum frame rate.
282 int estNrObjects = (int)(self.m_high_res_time * 120 + self.m_medium_res_time * self.m_medium_sample_rate + self.m_low_res_time * self.m_long_sample_rate);
283 for (int i = 0; i < estNrObjects; i++) {
284 FGReplayData* r = new FGReplayData;
285 if (r)
286 self.m_recycler.push_back(r);
287 else {
288 SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Out of memory!");
289 }
290 }
291}
292
293/* Reset replay queues. */
295{
296 m_sim_time = 0.0;
297 m_last_mt_time = 0.0;
298 m_last_lt_time = 0.0;
299 m_last_msg_time = 0.0;
300
301 // Flush queues
302 clear(*this);
303 m_flight_recorder->reinit();
304
305 m_high_res_time = fgGetDouble("/sim/replay/buffer/high-res-time", 60.0);
306 m_medium_res_time = fgGetDouble("/sim/replay/buffer/medium-res-time", 600.0); // 10 mins
307 m_low_res_time = fgGetDouble("/sim/replay/buffer/low-res-time", 3600.0); // 1 h
308 // short term sample rate is as every frame
309 m_medium_sample_rate = fgGetDouble("/sim/replay/buffer/medium-res-sample-dt", 0.5); // medium term sample rate (sec)
310 m_long_sample_rate = fgGetDouble("/sim/replay/buffer/low-res-sample-dt", 5.0); // long term sample rate (sec)
311
312 fillRecycler(*this);
313 loadMessages(*this);
314
315 m_replay_master->setIntValue(0);
316 m_disable_replay->setBoolValue(0);
317 m_replay_time->setDoubleValue(0);
318 m_replay_time_str->setStringValue("");
319}
320
321/* Bind to the property tree */
323{
324}
325
326/* Unbind from the property tree */
328{
329 // nothing to unbind
330}
331
332/* Returns string such as HH:MM:SS. */
333static void
334printTimeStr(char* pStrBuffer, int bufferSize, double _Time, bool ShowDecimal = true)
335{
336 if (_Time < 0)
337 _Time = 0;
338 unsigned int Time = _Time * 10;
339 unsigned int h = Time / 36000;
340 unsigned int m = (Time % 36000) / 600;
341 unsigned int s = (Time % 600) / 10;
342 unsigned int d = Time % 10;
343
344 int len;
345 if (h > 0)
346 len = snprintf(pStrBuffer, bufferSize, "%u:%02u:%02u", h, m, s);
347 else
348 len = snprintf(pStrBuffer, bufferSize, "%u:%02u", m, s);
349
350 if (len < 0) {
351 *pStrBuffer = 0;
352 return;
353 }
354
355 if (ShowDecimal)
356 snprintf(&pStrBuffer[len], bufferSize - len, ".%u", d);
357}
358
359/* Sets specified property to representation of the specified time. */
360static void setTimeStr(const char* property_path, double t, bool show_decimal = false)
361{
362 char buffer[30];
363 printTimeStr(buffer, 30, t, show_decimal);
364 fgSetString(property_path, buffer);
365}
366
367/* Sets /sim/messages/copilot to specified message. */
368static void guiMessage(const char* message)
369{
370 fgSetString("/sim/messages/copilot", message);
371}
372
374{
375 double ret;
376 std::lock_guard<std::mutex> lock(self.m_continuous->m_in_time_to_frameinfo_lock);
377 if (!self.m_continuous->m_in_time_to_frameinfo.empty()) {
378 ret = self.m_continuous->m_in_time_to_frameinfo.begin()->first;
379 SG_LOG(SG_SYSTEMS, SG_DEBUG,
380 "ret=" << ret
381 << " m_in_time_to_frameinfo is "
382 << self.m_continuous->m_in_time_to_frameinfo.begin()->first
383 << ".."
384 << self.m_continuous->m_in_time_to_frameinfo.rbegin()->first);
385 // We don't set /sim/replay/end-time here - it is updated when indexing
386 // in the background.
387 return ret;
388 }
389
390 if (!self.m_long_term.empty())
391 ret = self.m_long_term.front()->sim_time;
392 else if (!self.m_medium_term.empty())
393 ret = self.m_medium_term.front()->sim_time;
394 else if (!self.m_short_term.empty())
395 ret = self.m_short_term.front()->sim_time;
396 else
397 ret = 0.0;
398 fgSetDouble("/sim/replay/start-time", ret);
399 setTimeStr("/sim/replay/start-time-str", ret);
400 return ret;
401}
402
404{
405 std::lock_guard<std::mutex> lock(self.m_continuous->m_in_time_to_frameinfo_lock);
406 if (!self.m_continuous->m_in_time_to_frameinfo.empty()) {
407 double ret = self.m_continuous->m_in_time_to_frameinfo.rbegin()->first;
408 SG_LOG(SG_SYSTEMS, SG_DEBUG,
409 "ret=" << ret
410 << " m_in_time_to_frameinfo is "
411 << self.m_continuous->m_in_time_to_frameinfo.begin()->first
412 << ".."
413 << self.m_continuous->m_in_time_to_frameinfo.rbegin()->first);
414 // We don't set /sim/replay/end-time here - it is updated when indexing
415 // in the background.
416 return ret;
417 }
418 double ret = self.m_short_term.empty() ? 0 : self.m_short_term.back()->sim_time;
419 fgSetDouble("/sim/replay/end-time", ret);
420 setTimeStr("/sim/replay/end-time-str", ret);
421 return ret;
422}
423
426bool FGReplayInternal::start(bool new_tape)
427{
428 // freeze the fdm, resume from sim pause
429 if (m_log_frame_times->getBoolValue()) {
430 // Clear existing log.
431 m_log_frame_times->removeAllChildren();
432 }
433 double StartTime = get_start_time(*this);
434 double EndTime = get_end_time(*this);
436 char StrBuffer[30];
437 printTimeStr(StrBuffer, 30, StartTime, false);
438 fgSetString("/sim/replay/start-time-str", StrBuffer);
439 printTimeStr(StrBuffer, 30, EndTime, false);
440 fgSetString("/sim/replay/end-time-str", StrBuffer);
441
442 unsigned long buffer_elements = m_short_term.size() + m_medium_term.size() + m_long_term.size();
443 fgSetDouble("/sim/replay/buffer-size-mbyte",
444 buffer_elements * m_flight_recorder->getRecordSize() / (1024 * 1024.0));
445 if (fgGetBool("/sim/freeze/master") || !m_replay_master->getIntValue()) {
446 guiMessage("Replay active. 'Esc' to stop.");
447 }
448 fgSetBool("/sim/freeze/master", 0);
449 fgSetBool("/sim/freeze/clock", 0);
450 if (!m_replay_master->getIntValue()) {
451 m_replay_master->setIntValue(1);
452 m_replay_master_eof->setIntValue(0);
453 if (new_tape) {
454 // start replay at initial time, when loading a new tape
455 m_replay_time->setDoubleValue(StartTime);
456 } else {
457 // start replay at "loop interval" when starting instant replay
458 m_replay_time->setDoubleValue(-1);
459 }
460 m_replay_time_str->setStringValue("");
461 }
462 loadMessages(*this);
463
464 m_MultiplayMgr->ClearMotion();
465
466 return true;
467}
468
469static const char* MultiplayerMessageCallsign(const std::vector<char>& raw_message)
470{
471 const T_MsgHdr* header = reinterpret_cast<const T_MsgHdr*>(&raw_message.front());
472 return header->Callsign;
473}
474
475static bool CallsignsEqual(const std::vector<char>& a_message, const std::vector<char>& b_message)
476{
477 const char* a_callsign = MultiplayerMessageCallsign(a_message);
478 const char* b_callsign = MultiplayerMessageCallsign(b_message);
479 return 0 == strncmp(a_callsign, b_callsign, MAX_CALLSIGN_LEN);
480}
481
482/* Moves all multiplayer packets from first item in <list> to second item in
483<list>, but only for callsigns that are not already mentioned in the second
484item's list.
485
486To be called before first item is discarded, so that we don't end up with large
487gaps in multiplayer information when replaying - this can cause multiplayer
488aircraft to spuriously disappear and reappear. */
489static void MoveFrontMultiplayerPackets(std::deque<FGReplayData*>& list)
490{
491 auto it = list.begin();
492 FGReplayData* a = *it;
493 ++it;
494 if (it == list.end()) {
495 return;
496 }
497 FGReplayData* b = *it;
498
499 // Copy all multiplayer packets in <a> that are for multiplayer aircraft
500 // that are not in <b>, into <b>'s multiplayer_messages.
501 //
502 for (auto a_message : a->multiplayer_messages) {
503 bool found = false;
504 for (auto b_message : b->multiplayer_messages) {
505 if (CallsignsEqual(*a_message, *b_message)) {
506 found = true;
507 break;
508 }
509 }
510 if (!found) {
511 b->multiplayer_messages.push_back(a_message);
512 }
513 }
514}
515
516
519 simgear::gzContainerWriter& output,
520 const std::deque<FGReplayData*>& replay_data,
521 size_t record_size,
522 SGPropertyNode* meta)
523{
524 // get number of records in this stream
525 size_t count = replay_data.size();
526
527 // write container header for raw data
528 if (!output.writeContainerHeader(ReplayContainer::RawData, count * record_size)) {
529 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to save replay data."
530 << " Cannot write data container. Disk full?");
531 return false;
532 }
533
534 // read the raw data (all records in the given list)
535 auto it = replay_data.begin();
536 size_t check_count = 0;
537 while (it != replay_data.end() && !output.fail()) {
538 const FGReplayData* frame = *it++;
539 assert(record_size == frame->raw_data.size());
540 writeRaw(output, frame->sim_time);
541 output.write(&frame->raw_data.front(), frame->raw_data.size());
542
543 for (auto data : meta->getNode("meta")->getChildren("data")) {
544 SG_LOG(SG_SYSTEMS, SG_DEBUG, "data->getStringValue()=" << data->getStringValue());
545 if (data->getStringValue() == "multiplayer") {
546 uint32_t length = 0;
547 for (auto message : frame->multiplayer_messages) {
548 length += sizeof(uint16_t) + message->size();
549 }
550 writeRaw(output, length);
551 for (auto message : frame->multiplayer_messages) {
552 uint16_t message_size = message->size();
553 writeRaw(output, message_size);
554 output.write(&message->front(), message_size);
555 }
556 break;
557 }
558 }
559 check_count++;
560 }
561
562 // Did we really write as much as we intended?
563 if (check_count != count) {
564 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to save replay data."
565 " Expected to write "
566 << count << " records"
567 << ", but wrote " << check_count);
568 return false;
569 }
570
571 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Saved " << check_count << " records"
572 << " of size " << record_size);
573 return !output.fail();
574}
575
576
577SGPropertyNode_ptr saveSetup(
578 const SGPropertyNode* extra,
579 const SGPath& path,
580 double duration,
581 FGTapeType tape_type,
582 int continuous_compression)
583{
584 SGPropertyNode_ptr config;
585 if (path.exists()) {
586 // same timestamp!?
587 SG_LOG(SG_SYSTEMS, SG_ALERT, "Error, flight recorder tape file with same name already exists: " << path);
588 return config;
589 }
590
591 config = new SGPropertyNode;
592 SGPropertyNode* meta = config->getNode("meta", 0 /*index*/, true /*create*/);
593
594 // add some data to the file - so we know for which aircraft/version it was recorded
595 meta->setStringValue("aircraft-type", fgGetString("/sim/aircraft", "unknown"));
596 meta->setStringValue("aircraft-description", fgGetString("/sim/description", ""));
597 meta->setStringValue("aircraft-fdm", fgGetString("/sim/flight-model", ""));
598 meta->setStringValue("closest-airport-id", fgGetString("/sim/airport/closest-airport-id", ""));
599 meta->setStringValue("aircraft-version", fgGetString("/sim/aircraft-version", "(undefined)"));
600 if (tape_type == FGTapeType_CONTINUOUS) {
601 meta->setIntValue("continuous-compression", continuous_compression);
602 }
603 // add information on the tape's recording duration
604 meta->setDoubleValue("tape-duration", duration);
605 char buffer[30];
606 printTimeStr(buffer, 30, duration, false);
607 meta->setStringValue("tape-duration-str", buffer);
608
609 // add simulator version
610 copyProperties(fgGetNode("/sim/version", 0, true), meta->getNode("version", 0, true));
611 if (extra && extra->getNode("user-data")) {
612 copyProperties(extra->getNode("user-data"), meta->getNode("user-data", 0, true));
613 }
614
615 if (tape_type != FGTapeType_CONTINUOUS || fgGetBool("/sim/replay/record-signals", true)) {
616 config->addChild("data")->setStringValue("signals");
617 }
618
619 if (tape_type == FGTapeType_CONTINUOUS) {
620 if (fgGetBool("/sim/replay/record-multiplayer", false)) {
621 config->addChild("data")->setStringValue("multiplayer");
622 }
623 if (0 || fgGetBool("/sim/replay/record-extra-properties", false) || fgGetBool("/sim/replay/record-main-window", false) || fgGetBool("/sim/replay/record-main-view", false)) {
624 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Adding data[]=extra-properties."
625 << " record-extra-properties=" << fgGetBool("/sim/replay/record-extra-properties", false) << " record-main-window=" << fgGetBool("/sim/replay/record-main-window", false) << " record-main-view=" << fgGetBool("/sim/replay/record-main-view", false));
626 config->addChild("data")->setStringValue("extra-properties");
627 }
628 }
629
630 // store replay messages
631 copyProperties(fgGetNode("/sim/replay/messages", 0, true), meta->getNode("messages", 0, true));
632
633 return config;
634}
635
636std::ostream& operator<<(std::ostream& out, const FGFrameInfo& frame_info)
637{
638 return out << "{"
639 << " offset=" << frame_info.offset
640 << " has_multiplayer=" << frame_info.has_multiplayer
641 << " has_extra_properties=" << frame_info.has_extra_properties
642 << "}";
643}
644
645static void replayMessage(FGReplayInternal& self, double time)
646{
647 if (time < self.m_last_msg_time) {
648 self.m_current_msg = self.m_replay_messages.begin();
649 }
650
651 // check if messages have to be triggered
652 while (self.m_current_msg != self.m_replay_messages.end() && time >= self.m_current_msg->sim_time) {
653 // don't trigger messages when too long ago (fast-forward/skipped replay)
654 if (time - self.m_current_msg->sim_time < 3.0) {
655 fgGetNode("/sim/messages", true)
656 ->getNode(self.m_current_msg->speaker, true)
657 ->setStringValue(self.m_current_msg->message);
658 }
659 ++self.m_current_msg;
660 }
661 self.m_last_msg_time = time;
662}
663
667static void replayNormal2(
668 FGReplayInternal& self,
669 double time,
670 const FGReplayData* current_frame,
671 const FGReplayData* old_frame = nullptr,
672 int* xpos = nullptr,
673 int* ypos = nullptr,
674 int* xsize = nullptr,
675 int* ysize = nullptr)
676{
677 self.m_flight_recorder->replay(time, current_frame, old_frame, xpos, ypos, xsize, ysize);
678}
679
683static void interpolate(FGReplayInternal& self, double time, const std::deque<FGReplayData*>& list)
684{
685 // sanity checking
686 if (list.empty()) {
687 return;
688 } else if (list.size() == 1) {
689 replayNormal2(self, time, list[0]);
690 return;
691 }
692
693 unsigned int last = list.size() - 1;
694 unsigned int first = 0;
695 unsigned int mid = (last + first) / 2;
696
697 bool done = false;
698 while (!done) {
699 // cout << " " << first << " <=> " << last << endl;
700 if (last == first) {
701 done = true;
702 } else if (list[mid]->sim_time < time && list[mid + 1]->sim_time < time) {
703 // too low
704 first = mid;
705 mid = (last + first) / 2;
706 } else if (list[mid]->sim_time > time && list[mid + 1]->sim_time > time) {
707 // too high
708 last = mid;
709 mid = (last + first) / 2;
710 } else {
711 done = true;
712 }
713 }
714
715 replayNormal2(self, time, list[mid + 1], list[mid]);
716}
717
718bool replayNormal(FGReplayInternal& self, double time)
719{
720 if (!self.m_short_term.empty()) {
721 // We use /sim/replay/end-time instead of m_short_term.back()->sim_time
722 // because if we are recording multiplayer aircraft, new items will
723 // be appended to m_short_term while we replay, and we don't want to
724 // continue replaying into them. /sim/replay/end-time will remain
725 // pointing to the last frame at the time we started replaying.
726 //
727 double t1 = fgGetDouble("/sim/replay/end-time");
728 double t2 = self.m_short_term.front()->sim_time;
729 if (time > t1) {
730 // replay the most recent frame
731 replayNormal2(self, time, self.m_short_term.back());
732 // replay is finished now
733 return true;
734 } else if (time <= t1 && time >= t2) {
735 interpolate(self, time, self.m_short_term);
736 } else if (!self.m_medium_term.empty()) {
737 t1 = self.m_short_term.front()->sim_time;
738 t2 = self.m_medium_term.back()->sim_time;
739 if (time <= t1 && time >= t2) {
740 replayNormal2(self, time, self.m_medium_term.back(), self.m_short_term.front());
741 } else {
742 t1 = self.m_medium_term.back()->sim_time;
743 t2 = self.m_medium_term.front()->sim_time;
744 if (time <= t1 && time >= t2) {
745 interpolate(self, time, self.m_medium_term);
746 } else if (!self.m_long_term.empty()) {
747 t1 = self.m_medium_term.front()->sim_time;
748 t2 = self.m_long_term.back()->sim_time;
749 if (time <= t1 && time >= t2) {
750 replayNormal2(self, time, self.m_long_term.back(), self.m_medium_term.front());
751 } else {
752 t1 = self.m_long_term.back()->sim_time;
753 t2 = self.m_long_term.front()->sim_time;
754 if (time <= t1 && time >= t2) {
755 interpolate(self, time, self.m_long_term);
756 } else {
757 // replay the oldest long term frame
758 replayNormal2(self, time, self.m_long_term.front());
759 }
760 }
761 } else {
762 // replay the oldest medium term frame
763 replayNormal2(self, time, self.m_medium_term.front());
764 }
765 }
766 } else {
767 // replay the oldest short term frame
768 replayNormal2(self, time, self.m_short_term.front());
769 }
770 } else {
771 // nothing to replay
772 return true;
773 }
774 return false;
775}
776
782bool replay(FGReplayInternal& self, double time)
783{
784 // cout << "replay: " << time << " ";
785 // find the two frames to interpolate between
786 replayMessage(self, time);
787
788 std::lock_guard<std::mutex> lock(self.m_continuous->m_in_time_to_frameinfo_lock);
789
790 if (!self.m_continuous->m_in_time_to_frameinfo.empty()) {
791 // We are replaying a continuous recording.
792 //
793 return replayContinuous(self, time);
794 } else {
795 return replayNormal(self, time);
796 }
797}
798
799
800static FGReplayData* record(FGReplayInternal& self, double sim_time)
801{
802 FGReplayData* r = NULL;
803
804 if (!self.m_recycler.empty()) {
805 r = self.m_recycler.front();
806 self.m_recycler.pop_front();
807 }
808
809 return self.m_flight_recorder->capture(sim_time, r);
810}
811
813{
814 int current_replay_state = m_last_replay_state;
815
816 if (m_disable_replay->getBoolValue()) {
817 if (fgGetBool("/sim/freeze/master") || fgGetBool("/sim/freeze/clock")) {
818 // unpause - disable the replay system in next loop
819 fgSetBool("/sim/freeze/master", false);
820 fgSetBool("/sim/freeze/clock", false);
822 } else if (m_replay_master->getIntValue() != 3 || m_last_replay_state == 3) {
823 // disable the replay system
824 SG_LOG(SG_SYSTEMS, SG_DEBUG, "End replay");
825 current_replay_state = m_replay_master->getIntValue();
826 m_replay_master->setIntValue(0);
827 m_replay_time->setDoubleValue(0);
828 m_replay_time_str->setStringValue("");
829 m_disable_replay->setBoolValue(0);
830 m_speed_up->setDoubleValue(1.0);
831 m_speed_up->setDoubleValue(1.0);
832 if (fgGetBool("/sim/replay/mute")) {
833 fgSetBool("/sim/sound/enabled", true);
834 fgSetBool("/sim/replay/mute", false);
835 }
836
837 // Close any continuous replay file that we have open.
838 //
839 // This allows the user to use the in-memory record/replay system,
840 // instead of replay always showing the continuous recording.
841 //
842 if (m_continuous->m_in.is_open()) {
843 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Unloading continuous recording");
844 m_continuous->m_in.close();
845 m_continuous->m_in_time_to_frameinfo.clear();
846 }
847 assert(m_continuous->m_in_time_to_frameinfo.empty());
848
850 guiMessage("Replay stopped. Your controls!");
851 }
852 }
853
854 int replay_state = m_replay_master->getIntValue();
855 if (replay_state == 0 && m_last_replay_state > 0) {
856 if (current_replay_state == 3) {
857 // take control at current replay position ("My controls!").
858 // May need to uncrash the aircraft here :)
859 fgSetBool("/sim/crashed", false);
860 } else {
861 // normal replay exit, restore most recent frame
862 replay(*this, DBL_MAX);
863 }
864
865 // replay is finished
866 m_last_replay_state = replay_state;
867 return;
868 }
869
870 // remember recent state
871 m_last_replay_state = replay_state;
872
873 if (replay_state == 1) // normal replay
874 {
875 if (m_log_frame_times->getBoolValue()) {
876 m_log_frame_times->addChild("dt")->setDoubleValue(dt);
877 }
878 }
879
880 switch (replay_state) {
881 case 0:
882 // replay inactive, keep recording
883 break;
884 case 1: // normal replay
885 case 3: // prepare to resume normal flight at current replay position
886 {
887 // replay active
888 double current_time = m_replay_time->getDoubleValue();
889 bool reset_time = (current_time <= 0.0);
890 if (reset_time) {
891 // initialize start time
892 double startTime = get_start_time(*this);
893 double endTime = get_end_time(*this);
894 double duration = 0;
895 if (m_replay_duration_act->getBoolValue())
896 duration = fgGetDouble("/sim/replay/duration");
897 if (duration && duration < (endTime - startTime)) {
898 current_time = endTime - duration;
899 } else {
900 current_time = startTime;
901 }
902 }
903 if (current_time != m_replay_time_prev) {
904 // User has skipped backwards or forward. Reset list of multiplayer aircraft.
905 //
906 m_MultiplayMgr->ClearMotion();
907 }
908 current_time += dt;
909 SG_LOG(SG_GENERAL, SG_BULK, "current_time=" << std::fixed << std::setprecision(6) << current_time);
910
911 bool is_finished = replay(*this, current_time);
912 if (is_finished) {
914 m_replay_master_eof->setIntValue(1);
915 guiMessage("End of tape. 'Esc' to return.");
917 }
918 current_time = (m_replay_looped->getBoolValue()) ? -1 : get_end_time(*this) + 0.01;
919 } else {
921 }
922 m_replay_time->setDoubleValue(current_time);
923 m_replay_time_prev = current_time;
924 char StrBuffer[30];
925 printTimeStr(StrBuffer, 30, current_time);
926 m_replay_time_str->setStringValue((const char*)StrBuffer);
927
928 // when time skipped (looped replay), trigger listeners to reset views etc
929 if (reset_time)
930 m_replay_master->setIntValue(replay_state);
931
932 if (m_replay_multiplayer->getIntValue() && !m_continuous->m_in_time_to_frameinfo.empty()) {
933 // Carry on recording while replaying a Continuous
934 // recording. We don't want to do this with a Normal recording
935 // because it will prune the short, medium and long-term
936 // buffers, which will degrade the replay experience.
937 break;
938 } else {
939 // Don't record while replaying.
940 return;
941 }
942 }
943 case 2: // normal replay operation
944 if (m_replay_multiplayer->getIntValue()) {
945 // Carry on recording while replaying.
946 break;
947 } else {
948 // Don't record while replaying.
949 return;
950 }
951 default:
952 throw sg_range_exception("unknown FGReplayInternal state");
953 }
954
955 // flight recording
956
957 // sanity check, don't collect data if FDM data isn't good
958 if (!fgGetBool("/sim/fdm-initialized", false) || dt == 0.0)
959 return;
960
961 if (m_simple_time_enabled->getBoolValue()) {
962 m_sim_time = globals->get_subsystem<TimeManager>()->getMPProtocolClockSec();
963 } else {
964 double new_sim_time = m_sim_time + dt;
965 // don't record multiple records with the same timestamp (or go backwards in time)
966 if (new_sim_time <= m_sim_time) {
967 SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Time warp detected!");
968 return;
969 }
970 m_sim_time = new_sim_time;
971 }
972
973 // We still record even if we are replaying, if /sim/replay/multiplayer is
974 // true.
975 //
976 FGReplayData* r = record(*this, m_sim_time);
977 if (!r) {
978 SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Out of memory!");
979 return;
980 }
981
982 // update the short term list
983 assert(r->raw_data.size() != 0);
984 m_short_term.push_back(r);
985 FGReplayData* st_front = m_short_term.front();
986
987 m_record_normal_end->setDoubleValue(r->sim_time);
988 if (m_record_normal_begin->getDoubleValue() == 0) {
989 m_record_normal_begin->setDoubleValue(r->sim_time);
990 }
991
992 if (!st_front) {
993 SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Inconsistent data!");
994 }
995
996 if (m_continuous->m_out.is_open()) {
998 }
999
1000 if (replay_state == 0) {
1001 // Update recovery tape.
1002 //
1003 double recovery_period_s = m_recovery_period->getDoubleValue();
1004 if (recovery_period_s > 0) {
1005 static time_t s_last_recovery = 0;
1006 time_t t = time(NULL);
1007 if (s_last_recovery == 0) {
1008 /* Don't save immediately. */
1009 s_last_recovery = t;
1010 }
1011
1012 // Write recovery recording periodically.
1013 //
1014 if (t - s_last_recovery >= recovery_period_s) {
1015 s_last_recovery = t;
1016
1017 // We use static variables here to avoid calculating the same
1018 // data each time we are called.
1019 //
1020 static SGPath path = makeSavePath(FGTapeType_RECOVERY);
1021 static SGPath path_temp = SGPath(path.str() + "-");
1022 static SGPropertyNode_ptr Config;
1023
1024 SG_LOG(SG_SYSTEMS, SG_BULK, "Creating recovery file: " << path);
1025 // We write to <path_temp> then rename to <path>, which should
1026 // guarantee that there is always a valid recovery tape even if
1027 // flightgear crashes or is killed while we are writing.
1028 //
1029 path.create_dir();
1030 (void)remove(path_temp.c_str());
1031 std::ofstream out;
1032 bool ok = true;
1033 SGPropertyNode_ptr config = continuousWriteHeader(
1034 *m_continuous,
1035 m_flight_recorder.get(),
1036 out,
1037 path_temp,
1039 if (!config) ok = false;
1040 if (ok) ok = continuousWriteFrame(*m_continuous, r, out, config, FGTapeType_RECOVERY);
1041 out.close();
1042 if (ok) {
1043 rename(path_temp.c_str(), path.c_str());
1044 } else {
1045 std::string message = "Failed to update recovery snapshot file '" + path.str() + "';" + " See File / Flight Recorder Control / 'Maintain recovery snapshot'.";
1046 popupTip(message.c_str(), 10 /*delay*/);
1047 }
1048 }
1049 }
1050 }
1051
1052 if (m_sim_time - st_front->sim_time > m_high_res_time) {
1053 while (!m_short_term.empty() && m_sim_time - st_front->sim_time > m_high_res_time) {
1054 st_front = m_short_term.front();
1056 m_recycler.push_back(st_front);
1057 m_short_term.pop_front();
1058 }
1059
1060 // update the medium term list
1063 if (!m_short_term.empty()) {
1064 st_front = m_short_term.front();
1065 m_medium_term.push_back(st_front);
1066 m_short_term.pop_front();
1067 }
1068
1069 if (!m_medium_term.empty()) {
1070 FGReplayData* mt_front = m_medium_term.front();
1071 if (m_sim_time - mt_front->sim_time > m_medium_res_time) {
1072 while (!m_medium_term.empty() && m_sim_time - mt_front->sim_time > m_medium_res_time) {
1073 mt_front = m_medium_term.front();
1075 m_recycler.push_back(mt_front);
1076 m_medium_term.pop_front();
1077 }
1078 // update the long term list
1081 if (!m_medium_term.empty()) {
1082 mt_front = m_medium_term.front();
1083 m_long_term.push_back(mt_front);
1084 m_medium_term.pop_front();
1085 }
1086
1087 if (!m_long_term.empty()) {
1088 FGReplayData* lt_front = m_long_term.front();
1089 if (m_sim_time - lt_front->sim_time > m_low_res_time) {
1090 while (!m_long_term.empty() && m_sim_time - lt_front->sim_time > m_low_res_time) {
1091 lt_front = m_long_term.front();
1093 m_recycler.push_back(lt_front);
1094 m_long_term.pop_front();
1095 }
1096 }
1097 }
1098 }
1099 }
1100 }
1101 }
1102 }
1103
1104#if 0
1105 cout << "short term size = " << m_short_term.size()
1106 << " time = " << m_sim_time - m_short_term.front().sim_time
1107 << endl;
1108 cout << "medium term size = " << m_medium_term.size()
1109 << " time = " << m_sim_time - m_medium_term.front().sim_time
1110 << endl;
1111 cout << "long term size = " << m_long_term.size()
1112 << " time = " << m_sim_time - m_long_term.front().sim_time
1113 << endl;
1114#endif
1115 //stamp("point_finished");
1116}
1117
1119static bool
1121 simgear::gzContainerReader& input,
1122 //FGFlightRecorder* pRecorder,
1123 std::deque<FGReplayData*>& replay_data,
1124 size_t record_size,
1125 bool multiplayer,
1126 bool multiplayer_legacy)
1127{
1128 size_t size = 0;
1129 simgear::ContainerType Type = ReplayContainer::Invalid;
1130
1131 // write container header for raw data
1132 if (!input.readContainerHeader(&Type, &size)) {
1133 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Missing data container.");
1134 return false;
1135 } else if (Type != ReplayContainer::RawData) {
1136 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Expected data container, got " << Type);
1137 return false;
1138 }
1139
1140 SG_LOG(SG_SYSTEMS, SG_DEBUG, "multiplayer=" << multiplayer << " record_size=" << record_size << " Type=" << Type << " size=" << size);
1141
1142 // read the raw data
1143 size_t count = size / record_size;
1144 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Loading replay data. Container size is " << size << ", record size " << record_size << ", expected record count " << count << ".");
1145
1146 size_t check_count = 0;
1147 for (check_count = 0; (check_count < count) && (!input.eof()); ++check_count) {
1148 FGReplayData* buffer = new FGReplayData;
1149 readRaw(input, buffer->sim_time);
1150 buffer->raw_data.resize(record_size);
1151 input.read(&buffer->raw_data.front(), record_size);
1152 replay_data.push_back(buffer);
1153
1154 if (multiplayer) {
1155 if (multiplayer_legacy) {
1156 SG_LOG(SG_SYSTEMS, SG_DEBUG, "reading Normal multiplayer recording with legacy format");
1157 size_t num_messages = 0;
1158 input.read(reinterpret_cast<char*>(&num_messages), sizeof(num_messages));
1159 for (size_t i = 0; i < num_messages; ++i) {
1160 size_t message_size;
1161 input.read(reinterpret_cast<char*>(&message_size), sizeof(message_size));
1162 std::shared_ptr<std::vector<char>> message(new std::vector<char>(message_size));
1163 input.read(&message->front(), message_size);
1164 buffer->multiplayer_messages.push_back(message);
1165 }
1166 } else {
1167 SG_LOG(SG_SYSTEMS, SG_DEBUG, "reading Normal multiplayer recording");
1168 uint32_t length;
1169 readRaw(input, length);
1170 uint32_t pos = 0;
1171 for (;;) {
1172 if (pos == length) break;
1173 assert(pos < length);
1174 uint16_t message_size;
1175 readRaw(input, message_size);
1176 pos += sizeof(message_size) + message_size;
1177 if (pos > length) {
1178 SG_LOG(SG_SYSTEMS, SG_ALERT, "Tape appears to have corrupted multiplayer data"
1179 << " length=" << length << " message_size=" << message_size << " pos=" << pos);
1180 return false;
1181 }
1182 auto message = std::make_shared<std::vector<char>>(message_size);
1183 input.read(&message->front(), message_size);
1184 buffer->multiplayer_messages.push_back(message);
1185 }
1186 }
1187 }
1188 }
1189
1190 // did we get all we have hoped for?
1191 if (check_count != count) {
1192 if (input.eof()) {
1193 SG_LOG(SG_SYSTEMS, SG_ALERT, "Unexpected end of file.");
1194 }
1195 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Expected " << count << " records, but got " << check_count);
1196 return false;
1197 }
1198
1199 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Loaded " << check_count << " records of size " << record_size);
1200 return true;
1201}
1202
1204static bool saveTape2(FGReplayInternal& self, const SGPath& filename, SGPropertyNode_ptr metadata)
1205{
1206 bool ok = true;
1207
1208 /* open output stream *******************************************/
1209 simgear::gzContainerWriter output(filename, FlightRecorderFileMagic);
1210
1211 if (!output.good()) {
1212 SG_LOG(SG_SYSTEMS, SG_ALERT, "Cannot open file" << filename);
1213 return false;
1214 }
1215
1216 SG_LOG(SG_SYSTEMS, SG_DEBUG, "writing MetaData:");
1217 /* write meta data **********************************************/
1218 ok &= output.writeContainer(ReplayContainer::MetaData, metadata.get());
1219
1220 /* write flight recorder configuration **************************/
1221 SGPropertyNode_ptr config;
1222 if (ok) {
1223 config = new SGPropertyNode();
1224 self.m_flight_recorder->getConfig(config.get());
1225 ok &= output.writeContainer(ReplayContainer::Properties, config.get());
1226 }
1227
1228 /* write raw data ***********************************************/
1229 if (config) {
1230 size_t record_size = config->getIntValue("recorder/record-size", 0);
1231 SG_LOG(SG_SYSTEMS, SG_DEBUG, ""
1232 << "Config:recorder/signal-count=" << config->getIntValue("recorder/signal-count", 0) << " RecordSize: " << record_size);
1233 if (ok)
1234 ok &= saveRawReplayData(output, self.m_short_term, record_size, metadata);
1235 if (ok)
1236 ok &= saveRawReplayData(output, self.m_medium_term, record_size, metadata);
1237 if (ok)
1238 ok &= saveRawReplayData(output, self.m_long_term, record_size, metadata);
1239 config = 0;
1240 }
1241
1242 /* done *********************************************************/
1243 output.close();
1244
1245 if (!ok) SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to write to file " << filename);
1246 return ok;
1247}
1248
1250bool FGReplayInternal::saveTape(const SGPropertyNode* extra)
1251{
1252 SGPath path_timeless;
1253 SGPath path = makeSavePath(FGTapeType_NORMAL, &path_timeless);
1254 SGPropertyNode_ptr metadata = saveSetup(
1255 extra,
1256 path,
1257 get_end_time(*this) - get_start_time(*this),
1259 bool ok = false;
1260 if (metadata) {
1261 ok = saveTape2(*this, path, metadata);
1262 if (ok) {
1263 // Make a convenience link to the recording. E.g.
1264 // harrier-gr3.fgtape ->
1265 // harrier-gr3-20201224-005034.fgtape.
1266 //
1267 // Link destination is in same directory as link so we use leafname
1268 // path.file().
1269 //
1270 path_timeless.remove();
1271 bool ok_link = path_timeless.makeLink(path.file());
1272 if (!ok_link) {
1273 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to create link " << path_timeless.c_str() << " => " << path.file());
1274 }
1275 }
1276 }
1277 if (ok)
1278 guiMessage("Flight recorder tape saved successfully!");
1279 else
1280 guiMessage("Failed to save tape! See log output.");
1281
1282 return ok;
1283}
1284
1285
1286int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties)
1287{
1288 std::ifstream in0;
1289 if (!in) {
1290 in0.open(path);
1291 in = &in0;
1292 }
1293 if (!*in) {
1294 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to open path=" << path);
1295 return +1;
1296 }
1297 std::vector<char> buffer(strlen(FlightRecorderFileMagic) + 1);
1298 in->read(&buffer.front(), buffer.size());
1299 SG_LOG(SG_SYSTEMS, SG_DEBUG, "in->gcount()=" << in->gcount() << " buffer.size()=" << buffer.size());
1300 if ((size_t)in->gcount() != buffer.size()) {
1301 // Further download is needed.
1302 return +1;
1303 }
1304 if (strcmp(&buffer.front(), FlightRecorderFileMagic)) {
1305 SG_LOG(SG_SYSTEMS, SG_DEBUG, "fgtape prefix doesn't match FlightRecorderFileMagic in path: " << path);
1306 return -1;
1307 }
1308 bool ok = false;
1309 try {
1310 PropertiesRead(*in, properties);
1311 ok = true;
1312 } catch (std::exception& e) {
1313 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read Config properties in: " << path << ": " << e.what());
1314 }
1315 if (!ok) {
1316 // Failed to read properties, so indicate that further download is needed.
1317 return +1;
1318 }
1319 SG_LOG(SG_SYSTEMS, SG_BULK, "properties is:\n"
1320 << writePropertiesInline(properties, true /*write_all*/) << "\n");
1321 return 0;
1322}
1323
1324// Build up in-memory cache of simulator time to file offset, so we can handle
1325// random access.
1326//
1327// We also cache any frames that modify extra-properties.
1328//
1329// Can be called multiple times, e.g. if recording is being downloaded.
1330//
1331static void indexContinuousRecording(FGReplayInternal& self, const void* data, size_t numbytes)
1332{
1333 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexing Continuous recording "
1334 << " data=" << data << " numbytes=" << numbytes << " m_indexing_pos=" << self.m_continuous->m_indexing_pos << " m_in_compression=" << self.m_continuous->m_in_compression << " m_in_time_to_frameinfo.size()=" << self.m_continuous->m_in_time_to_frameinfo.size());
1335 time_t t0 = time(NULL);
1336 std::streampos original_pos = self.m_continuous->m_indexing_pos;
1337 size_t original_num_frames = self.m_continuous->m_in_time_to_frameinfo.size();
1338
1339 // Reset any EOF because there might be new data.
1340 self.m_continuous->m_indexing_in.clear();
1341
1342 struct data_stats_t {
1343 size_t num_frames = 0;
1344 size_t bytes = 0;
1345 };
1346 std::map<std::string, data_stats_t> stats;
1347
1348 for (;;) {
1349 SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame."
1350 << " m_in.tellg()=" << self.m_continuous->m_in.tellg());
1351 self.m_continuous->m_indexing_in.seekg(self.m_continuous->m_indexing_pos);
1352 double sim_time;
1353 readRaw(self.m_continuous->m_indexing_in, sim_time);
1354
1355 SG_LOG(SG_SYSTEMS, SG_BULK, ""
1356 << " m_indexing_pos=" << self.m_continuous->m_indexing_pos << " m_indexing_in.tellg()=" << self.m_continuous->m_indexing_in.tellg() << " sim_time=" << sim_time);
1357
1358 FGFrameInfo frameinfo;
1359 frameinfo.offset = self.m_continuous->m_indexing_pos;
1360 if (self.m_continuous->m_in_compression) {
1361 // Skip compressed frame data without decompressing it.
1362 uint8_t flags;
1363 self.m_continuous->m_indexing_in.read((char*)&flags, sizeof(flags));
1364 frameinfo.has_signals = flags & 1;
1365 frameinfo.has_multiplayer = flags & 2;
1366 frameinfo.has_extra_properties = flags & 4;
1367
1368 if (frameinfo.has_signals) {
1369 stats["signals"].num_frames += 1;
1370 }
1371 if (frameinfo.has_multiplayer) {
1372 stats["multiplayer"].num_frames += 1;
1373 ++self.m_continuous->m_num_frames_multiplayer;
1374 self.m_continuous->m_in_multiplayer = true;
1375 }
1376 if (frameinfo.has_extra_properties) {
1377 stats["extra-properties"].num_frames += 1;
1378 ++self.m_continuous->m_num_frames_extra_properties;
1379 self.m_continuous->m_in_extra_properties = true;
1380 }
1381
1382 uint32_t compressed_size;
1383 readRaw(self.m_continuous->m_indexing_in, compressed_size);
1384 SG_LOG(SG_SYSTEMS, SG_BULK, "compressed_size=" << compressed_size);
1385
1386 self.m_continuous->m_indexing_in.seekg(compressed_size, std::ios_base::cur);
1387 } else {
1388 // Skip frame data.
1389 auto datas = self.m_continuous->m_in_config->getChildren("data");
1390 SG_LOG(SG_SYSTEMS, SG_BULK, "datas.size()=" << datas.size());
1391 for (auto l_data : datas) {
1392 uint32_t length;
1393 readRaw(self.m_continuous->m_indexing_in, length);
1394 SG_LOG(SG_SYSTEMS, SG_BULK,
1395 "m_in.tellg()=" << self.m_continuous->m_indexing_in.tellg()
1396 << " Skipping data_type=" << l_data->getStringValue()
1397 << " length=" << length);
1398 // Move forward <length> bytes.
1399 self.m_continuous->m_indexing_in.seekg(length, std::ios_base::cur);
1400 if (!self.m_continuous->m_indexing_in) {
1401 // Dont add bogus info to <stats>.
1402 break;
1403 }
1404 if (length) {
1405 std::string data_type = l_data->getStringValue();
1406 stats[data_type].num_frames += 1;
1407 stats[data_type].bytes += length;
1408 if (data_type == "signals") {
1409 frameinfo.has_signals = true;
1410 } else if (data_type == "multiplayer") {
1411 frameinfo.has_multiplayer = true;
1412 ++self.m_continuous->m_num_frames_multiplayer;
1413 self.m_continuous->m_in_multiplayer = true;
1414 } else if (data_type == "extra-properties") {
1415 frameinfo.has_extra_properties = true;
1416 ++self.m_continuous->m_num_frames_extra_properties;
1417 self.m_continuous->m_in_extra_properties = true;
1418 }
1419 }
1420 }
1421 }
1422
1423 SG_LOG(SG_SYSTEMS, SG_BULK, ""
1424 << " pos=" << self.m_continuous->m_indexing_pos << " sim_time=" << sim_time << " m_num_frames_multiplayer=" << self.m_continuous->m_num_frames_multiplayer << " m_num_frames_extra_properties=" << self.m_continuous->m_num_frames_extra_properties);
1425
1426 if (!self.m_continuous->m_indexing_in) {
1427 // Failed to read a frame, e.g. because of EOF. Leave
1428 // m_indexing_pos unchanged so we can try again at same
1429 // starting position if recording is updated by background download.
1430 //
1431 SG_LOG(SG_SYSTEMS, SG_BULK, "m_indexing_in failed, giving up");
1432 break;
1433 }
1434
1435 // We have successfully read a frame, so add it to
1436 // m_in_time_to_frameinfo[].
1437 //
1438 self.m_continuous->m_indexing_pos = self.m_continuous->m_indexing_in.tellg();
1439 std::lock_guard<std::mutex> lock(self.m_continuous->m_in_time_to_frameinfo_lock);
1440 self.m_continuous->m_in_time_to_frameinfo[sim_time] = frameinfo;
1441 }
1442 time_t t = time(NULL) - t0;
1443 auto new_bytes = self.m_continuous->m_indexing_pos - original_pos;
1444 auto num_frames = self.m_continuous->m_in_time_to_frameinfo.size();
1445 auto num_new_frames = num_frames - original_num_frames;
1446 if (num_new_frames) {
1447 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Continuous recording: index updated:"
1448 << " num_frames=" << num_frames << " num_new_frames=" << num_new_frames << " new_bytes=" << new_bytes);
1449 }
1450 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Continuous recording indexing complete."
1451 << " time taken=" << t << "s."
1452 << " num_new_frames=" << num_new_frames << " m_indexing_pos=" << self.m_continuous->m_indexing_pos << " m_in_time_to_frameinfo.size()=" << self.m_continuous->m_in_time_to_frameinfo.size() << " m_num_frames_multiplayer=" << self.m_continuous->m_num_frames_multiplayer << " m_num_frames_extra_properties=" << self.m_continuous->m_num_frames_extra_properties);
1453 for (auto stat : stats) {
1454 SG_LOG(SG_SYSTEMS, SG_DEBUG, "data type " << stat.first << ":"
1455 << " num_frames=" << stat.second.num_frames << " bytes=" << stat.second.bytes);
1456 }
1457
1458 std::lock_guard<std::mutex> lock(self.m_continuous->m_in_time_to_frameinfo_lock);
1459 fgSetInt("/sim/replay/continuous-stats-num-frames", self.m_continuous->m_in_time_to_frameinfo.size());
1460 fgSetInt("/sim/replay/continuous-stats-num-frames-extra-properties", self.m_continuous->m_num_frames_extra_properties);
1461 fgSetInt("/sim/replay/continuous-stats-num-frames-multiplayer", self.m_continuous->m_num_frames_multiplayer);
1462 if (!self.m_continuous->m_in_time_to_frameinfo.empty()) {
1463 double t_begin = self.m_continuous->m_in_time_to_frameinfo.begin()->first;
1464 double t_end = self.m_continuous->m_in_time_to_frameinfo.rbegin()->first;
1465 fgSetDouble("/sim/replay/start-time", t_begin);
1466 fgSetDouble("/sim/replay/end-time", t_end);
1467 setTimeStr("/sim/replay/start-time-str", t_begin);
1468 setTimeStr("/sim/replay/end-time-str", t_end);
1469 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Have set /sim/replay/end-time to " << fgGetDouble("/sim/replay/end-time"));
1470 }
1471 if (!numbytes) {
1472 SG_LOG(SG_SYSTEMS, SG_ALERT, "Continuous recording: indexing finished"
1473 << " m_in_time_to_frameinfo.size()=" << self.m_continuous->m_in_time_to_frameinfo.size());
1474 self.m_continuous->m_indexing_in.close();
1475 }
1476}
1477
1478
1480 FGReplayInternal& replay_internal,
1481 std::ifstream& in,
1482 const SGPath& filename,
1483 bool preview,
1484 bool create_video,
1485 double fixed_dt,
1486 SGPropertyNode& meta_meta,
1487 simgear::HTTP::FileRequestRef file_request)
1488{
1489 auto continuous = replay_internal.m_continuous.get();
1490 SG_LOG(SG_SYSTEMS, SG_BULK, "m_in_config is:\n"
1491 << writePropertiesInline(continuous->m_in_config, true /*write_all*/) << "\n");
1492 copyProperties(continuous->m_in_config->getNode("meta", 0, true), &meta_meta);
1493 if (preview) {
1494 in.close();
1495 return true;
1496 }
1497 replay_internal.m_flight_recorder->reinit(continuous->m_in_config);
1498 clear(replay_internal);
1499 fillRecycler(replay_internal);
1500 continuous->m_in_time_last = -1;
1501 continuous->m_in_frame_time_last = -1;
1502 continuous->m_in_time_to_frameinfo.clear();
1503 continuous->m_num_frames_extra_properties = 0;
1504 continuous->m_num_frames_multiplayer = 0;
1505 continuous->m_indexing_in.open(filename.str());
1506 continuous->m_indexing_pos = in.tellg();
1507 continuous->m_in_compression = continuous
1508 ->m_in_config->getNode("meta/continuous-compression", true /*create*/)
1509 ->getIntValue();
1510 continuous->m_replay_create_video = create_video;
1511 continuous->m_replay_fixed_dt = fixed_dt;
1512 SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_in_compression=" << continuous->m_in_compression);
1513 SG_LOG(SG_SYSTEMS, SG_DEBUG, "filerequest=" << file_request.get());
1514
1515 // Make an in-memory index of the recording.
1516 if (file_request) {
1517 auto p_replay_internal = &replay_internal;
1518 file_request->setCallback(
1519 [p_replay_internal](const void* data, size_t numbytes) {
1520 ::indexContinuousRecording(*p_replay_internal, data, numbytes);
1521 });
1522 } else {
1523 ::indexContinuousRecording(replay_internal, nullptr, 0);
1524 }
1525
1526 if (continuous->m_replay_fixed_dt) {
1527 SG_LOG(SG_GENERAL, SG_ALERT, "Replaying with fixed_dt=" << continuous->m_replay_fixed_dt);
1528 continuous->m_replay_fixed_dt_prev = fgGetDouble("/sim/time/fixed-dt");
1529 fgSetDouble("/sim/time/fixed-dt", continuous->m_replay_fixed_dt);
1530 } else {
1531 continuous->m_replay_fixed_dt_prev = -1;
1532 }
1533
1534 bool ok = true;
1535 if (continuous->m_replay_create_video) {
1536 SG_LOG(SG_GENERAL, SG_ALERT, "Replaying with create-video");
1537 auto view_mgr = globals->get_subsystem<FGViewMgr>();
1538 if (view_mgr) {
1539 ok = view_mgr->video_start(
1540 "" /*name*/,
1541 "" /*codec*/,
1542 -1 /*quality*/,
1543 -1 /*speed*/,
1544 0 /*bitrate*/
1545 );
1546 } else {
1547 SG_LOG(SG_GENERAL, SG_ALERT, "Cannot handle create_video=true because FGViewMgr not available");
1548 ok = false;
1549 }
1550 }
1551
1552 if (ok) ok = replay_internal.start(true /*new_tape*/);
1553
1554 return ok;
1555}
1556
1557
1558struct NumberCommasHelper : std::numpunct<char> {
1559 virtual char do_thousands_sep() const
1560 {
1561 return ',';
1562 }
1563
1564 virtual std::string do_grouping() const
1565 {
1566 return "\03";
1567 }
1568};
1569
1570static std::locale s_comma_locale(std::locale(), new NumberCommasHelper());
1571
1572/* Returns number as string with commas every 3 digits. */
1573static std::string numberCommas(size_t n)
1574{
1575 std::stringstream buffer;
1576 buffer.imbue(s_comma_locale);
1577 buffer << n;
1578 return buffer.str();
1579}
1580
1581
1590 const SGPath& filename,
1591 bool preview,
1592 bool create_video,
1593 double fixed_dt,
1594 SGPropertyNode& meta_meta,
1595 simgear::HTTP::FileRequestRef file_request)
1596{
1597 SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << preview << " Filename=" << filename);
1598
1599 /* Try to load a Continuous recording first. */
1600 m_replay_error->setBoolValue(false);
1601 std::ifstream in_preview;
1602 std::ifstream& in(preview ? in_preview : m_continuous->m_in);
1603 in.open(filename.str());
1604 if (!in) {
1605 SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open"
1606 << " Filename=" << filename.str() << " in.is_open()=" << in.is_open());
1607 return false;
1608 }
1609 size_t file_size = filename.sizeInBytes();
1610 meta_meta.setLongValue("tape-size", file_size);
1611 meta_meta.setStringValue("tape-size-str", numberCommas(file_size));
1612 m_continuous->m_in_config = new SGPropertyNode;
1613 int e = loadContinuousHeader(filename.str(), &in, m_continuous->m_in_config);
1614 if (e == 0) {
1615 return loadTapeContinuous(*this, in, filename, preview, create_video, fixed_dt, meta_meta, file_request);
1616 }
1617
1618 // Not a continuous recording.
1619 in.close();
1620 if (file_request) {
1621 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Cannot load filename=" << filename << " because it is download but not Continuous recording");
1622 return false;
1623 }
1624 bool ok = true;
1625
1626 /* Open as a gzipped Normal recording. ********************************************/
1627 simgear::gzContainerReader input(filename, FlightRecorderFileMagic);
1628 if (input.eof() || !input.good()) {
1629 SG_LOG(SG_SYSTEMS, SG_ALERT, "Cannot open file " << filename);
1630 ok = false;
1631 }
1632
1633 SGPropertyNode_ptr meta_data_props = new SGPropertyNode();
1634
1635 /* read meta data ***********************************************/
1636 if (ok) {
1637 char* meta_data = NULL;
1638 size_t size = 0;
1639 simgear::ContainerType type = ReplayContainer::Invalid;
1640 if (!input.readContainer(&type, &meta_data, &size) || size < 1) {
1641 SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << filename << ". Invalid meta data.");
1642 ok = false;
1643 } else if (type != ReplayContainer::MetaData) {
1644 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Invalid header. Container type " << type);
1645 SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << filename);
1646 ok = false;
1647 } else {
1648 SG_LOG(SG_SYSTEMS, SG_BULK, "meta_data is:\n"
1649 << meta_data);
1650 try {
1651 readProperties(meta_data, size - 1, meta_data_props);
1652 copyProperties(meta_data_props->getNode("meta", 0, true), &meta_meta);
1653 } catch (const sg_exception& exc) {
1654 SG_LOG(SG_SYSTEMS, SG_ALERT, "Error reading flight recorder tape: " << filename << ", XML parser message:" << exc.getFormattedMessage());
1655 ok = false;
1656 }
1657 }
1658
1659 if (meta_data) {
1660 //printf("%s\n", meta_data);
1661 free(meta_data);
1662 meta_data = NULL;
1663 }
1664 }
1665
1666 /* read flight recorder configuration **************************/
1667 if (ok && !preview) {
1668 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Loading flight recorder data...");
1669 char* config_xml = nullptr;
1670 size_t size = 0;
1671 simgear::ContainerType type = ReplayContainer::Invalid;
1672 if (!input.readContainer(&type, &config_xml, &size) || size < 1) {
1673 SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized."
1674 << " This is not a valid FlightGear flight recorder tape: " << filename << ". Invalid configuration container.");
1675 ok = false;
1676 } else if (!config_xml || type != ReplayContainer::Properties) {
1677 SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized."
1678 " This is not a valid FlightGear flight recorder tape: "
1679 << filename << ". Unexpected container type, expected \"properties\".");
1680 ok = false;
1681 }
1682
1683 SGPropertyNode_ptr config = new SGPropertyNode();
1684 if (ok) {
1685 SG_LOG(SG_SYSTEMS, SG_BULK, "config_xml is:\n"
1686 << config_xml);
1687 try {
1688 readProperties(config_xml, size - 1, config);
1689 } catch (const sg_exception& exc) {
1690 SG_LOG(SG_SYSTEMS, SG_ALERT, "Error reading flight recorder tape: " << filename << ", XML parser message:" << exc.getFormattedMessage());
1691 ok = false;
1692 }
1693 if (ok) {
1694 // reconfigure the recorder - and wipe old data (no longer matches the current recorder)
1695 m_flight_recorder->reinit(config);
1696 clear(*this);
1697 fillRecycler(*this);
1698 }
1699 }
1700
1701 if (config_xml) {
1702 free(config_xml);
1703 config_xml = NULL;
1704 }
1705
1706 /* read raw data ***********************************************/
1707 if (ok) {
1708 size_t record_size = m_flight_recorder->getRecordSize();
1709 SG_LOG(SG_SYSTEMS, SG_DEBUG, "RecordSize=" << record_size);
1710 size_t original_size = config->getIntValue("recorder/record-size", 0);
1711 SG_LOG(SG_SYSTEMS, SG_DEBUG, "OriginalSize=" << original_size);
1712
1713 // check consistency - ugly things happen when data vs signals mismatch
1714 if (original_size != record_size && original_size != 0) {
1715 ok = false;
1716 SG_LOG(SG_SYSTEMS, SG_ALERT, "Error: Data inconsistency."
1717 << " Flight recorder tape has record size " << record_size << ", expected size was " << original_size << ".");
1718 }
1719
1720 bool multiplayer = false;
1721 bool multiplayer_legacy = false;
1722 for (auto data : meta_meta.getChildren("data")) {
1723 if (data->getStringValue() == "multiplayer") {
1724 multiplayer = true;
1725 }
1726 }
1727 if (!multiplayer) {
1728 multiplayer_legacy = meta_meta.getBoolValue("multiplayer", 0);
1729 if (multiplayer_legacy) {
1730 multiplayer = true;
1731 }
1732 }
1733 SG_LOG(SG_SYSTEMS, SG_ALERT, "multiplayer=" << multiplayer);
1734 if (ok) ok = loadRawReplayData(input, m_short_term, record_size, multiplayer, multiplayer_legacy);
1735 if (ok) ok = loadRawReplayData(input, m_medium_term, record_size, multiplayer, multiplayer_legacy);
1736 if (ok) ok = loadRawReplayData(input, m_long_term, record_size, multiplayer, multiplayer_legacy);
1737
1738 // restore replay messages
1739 if (ok) {
1740 copyProperties(
1741 meta_data_props->getNode("messages", 0, true),
1742 fgGetNode("/sim/replay/messages", 0, true));
1743 }
1744 m_sim_time = get_end_time(*this);
1745 // TODO: we could (re)store these too
1747 }
1748 /* done *********************************************************/
1749 }
1750
1751 input.close();
1752
1753 if (!preview) {
1754 if (ok) {
1755 guiMessage("Flight recorder tape loaded successfully!");
1756 start(true);
1757 } else
1758 guiMessage("Failed to load tape. See log output.");
1759 }
1760
1761 return ok;
1762}
1763
1767bool listTapes(bool same_aircraft_filter, const SGPath& tape_directory)
1768{
1769 const std::string aircraft_type = simgear::strutils::uppercase(fgGetString("/sim/aircraft", "unknown"));
1770
1771 // process directory listing of ".fgtape" files
1772 simgear::Dir dir(tape_directory);
1773 simgear::PathList list = dir.children(simgear::Dir::TYPE_FILE, ".fgtape");
1774
1775 SGPropertyNode* tape_list = fgGetNode("/sim/replay/tape-list", true);
1776 tape_list->removeChildren("tape");
1777 int index = 0;
1778 size_t l = aircraft_type.size();
1779 for (auto it = list.begin(); it != list.end(); ++it) {
1780 SGPath file(it->file());
1781 std::string name(file.base());
1782 if (!same_aircraft_filter || !simgear::strutils::uppercase(name).compare(0, l, aircraft_type)) {
1783 tape_list->getNode("tape", index++, true)->setStringValue(name);
1784 }
1785 }
1786 return true;
1787}
1788
1789std::string FGReplayInternal::makeTapePath(const std::string& tape_name)
1790{
1791 std::string path = tape_name;
1792 if (simgear::strutils::ends_with(path, ".fgtape")) {
1793 return path;
1794 }
1795 SGPath path2(fgGetString("/sim/replay/tape-directory", ""));
1796 path2.append(path + ".fgtape");
1797 return path2.str();
1798}
1799
1800int FGReplayInternal::loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties)
1801{
1802 return ::loadContinuousHeader(path, in, properties);
1803}
1804
1806bool FGReplayInternal::loadTape(const SGPropertyNode* config_data)
1807{
1808 SGPath tape_directory(fgGetString("/sim/replay/tape-directory", ""));
1809
1810 // see if shall really load the file - or just obtain the meta data for preview
1811 bool preview = config_data->getBoolValue("preview", 0);
1812
1813 // file/tape to be loaded
1814 std::string tape = config_data->getStringValue("tape", "");
1815
1816 if (tape.empty()) {
1817 if (preview) return true;
1818 return listTapes(config_data->getBoolValue("same-aircraft", 0), tape_directory);
1819 } else {
1820 SGPropertyNode* meta_meta = fgGetNode("/sim/gui/dialogs/flightrecorder/preview", true);
1821 std::string path = makeTapePath(tape);
1822 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Checking flight recorder file " << path << ", preview: " << preview);
1823 return loadTape(
1824 path,
1825 preview,
1826 config_data->getBoolValue("create-video"),
1827 config_data->getDoubleValue("fixed-dt", -1),
1828 *meta_meta);
1829 }
1830}
#define i(x)
bool continuousWriteFrame(Continuous &continuous, FGReplayData *r, std::ostream &out, SGPropertyNode_ptr config, FGTapeType tape_type)
bool replayContinuous(FGReplayInternal &self, double time)
void continuous_replay_video_end(Continuous &continuous)
SGPropertyNode_ptr continuousWriteHeader(Continuous &continuous, FGFlightRecorder *flight_recorder, std::ofstream &out, const SGPath &path, FGTapeType tape_type)
static void popupTip(const char *message, int delay)
const char * name
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
Definition fg_props.cxx:556
bool fgSetInt(const char *name, int val)
Set an int value for a property.
Definition fg_props.cxx:568
FGGlobals * globals
Definition globals.cxx:142
#define MAX_CALLSIGN_LEN
@ Properties
XML data describing the recorded flight recorder properties.
@ MetaData
XML data / properties with arbitrary data, such as description, aircraft type, ...
@ RawData
Actual binary data blobs (the recorder's tape).
@ Header
Used for initial file header (fixed identification string).
bool fgSetDouble(const char *name, double defaultValue)
Set a double value for a property.
Definition proptest.cpp:31
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
bool fgSetBool(char const *name, bool val)
Set a bool value for a property.
Definition proptest.cpp:24
double fgGetDouble(const char *name, double defaultValue)
Get a double value for a property.
Definition proptest.cpp:30
bool fgSetString(char const *name, char const *str)
Set a string value for a property.
Definition proptest.cpp:26
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
double get_end_time(FGReplayInternal &self)
double get_start_time(FGReplayInternal &self)
static FGReplayData * record(FGReplayInternal &self, double sim_time)
int loadContinuousHeader(const std::string &path, std::istream *in, SGPropertyNode *properties)
static void loadMessages(FGReplayInternal &self)
std::ostream & operator<<(std::ostream &out, const FGFrameInfo &frame_info)
bool loadTapeContinuous(FGReplayInternal &replay_internal, std::ifstream &in, const SGPath &filename, bool preview, bool create_video, double fixed_dt, SGPropertyNode &meta_meta, simgear::HTTP::FileRequestRef file_request)
static bool saveTape2(FGReplayInternal &self, const SGPath &filename, SGPropertyNode_ptr metadata)
Write flight recorder tape with given filename and meta properties to disk.
static void MoveFrontMultiplayerPackets(std::deque< FGReplayData * > &list)
static bool saveRawReplayData(simgear::gzContainerWriter &output, const std::deque< FGReplayData * > &replay_data, size_t record_size, SGPropertyNode *meta)
Save raw replay data in a separate container.
static const char * MultiplayerMessageCallsign(const std::vector< char > &raw_message)
const char *const FlightRecorderFileMagic
Magic string to verify valid FG flight recorder tapes.
static std::string numberCommas(size_t n)
static void guiMessage(const char *message)
static void replayMessage(FGReplayInternal &self, double time)
static std::locale s_comma_locale(std::locale(), new NumberCommasHelper())
static bool loadRawReplayData(simgear::gzContainerReader &input, std::deque< FGReplayData * > &replay_data, size_t record_size, bool multiplayer, bool multiplayer_legacy)
Load raw replay data from a separate container.
void fillRecycler(FGReplayInternal &self)
static void writeRaw(std::ostream &out, const T &data)
static void interpolate(FGReplayInternal &self, double time, const std::deque< FGReplayData * > &list)
interpolate a specific time from a specific list
bool listTapes(bool same_aircraft_filter, const SGPath &tape_directory)
List available tapes in current directory.
bool replay(FGReplayInternal &self, double time)
Replay a saved frame based on time, interpolate from the two nearest saved frames.
static void readRaw(std::istream &in, T &data)
bool replayNormal(FGReplayInternal &self, double time)
static void printTimeStr(char *pStrBuffer, int bufferSize, double _Time, bool ShowDecimal=true)
SGPath makeSavePath(FGTapeType type, SGPath *path_timeless)
static void clear(FGReplayInternal &self)
SGPropertyNode_ptr saveSetup(const SGPropertyNode *extra, const SGPath &path, double duration, FGTapeType tape_type, int continuous_compression)
static int PropertiesRead(std::istream &in, SGPropertyNode *node)
static void setTimeStr(const char *property_path, double t, bool show_decimal=false)
static void indexContinuousRecording(FGReplayInternal &self, const void *data, size_t numbytes)
static bool CallsignsEqual(const std::vector< char > &a_message, const std::vector< char > &b_message)
static void popupTip(const char *message, int delay)
static void replayNormal2(FGReplayInternal &self, double time, const FGReplayData *current_frame, const FGReplayData *old_frame=nullptr, int *xpos=nullptr, int *ypos=nullptr, int *xsize=nullptr, int *ysize=nullptr)
given two FGReplayData elements and a time, interpolate between them
SGPropertyNode_ptr saveSetup(const SGPropertyNode *extra, const SGPath &path, double duration, FGTapeType tape_type, int continuous_compression=0)
SGPath makeSavePath(FGTapeType type, SGPath *path_timeless=nullptr)
const char *const FlightRecorderFileMagic
Magic string to verify valid FG flight recorder tapes.
@ FGTapeType_NORMAL
@ FGTapeType_CONTINUOUS
@ FGTapeType_RECOVERY
size_t m_num_multiplayer_messages
std::vector< std::shared_ptr< std::vector< char > > > multiplayer_messages
static SGPropertyNode_ptr s_prop_num
static size_t s_bytes_multiplayer_messages
static size_t s_num
static void resetStatisticsProperties()
static size_t s_bytes_raw_data
static SGPropertyNode_ptr s_prop_bytes_raw_data
static SGPropertyNode_ptr s_prop_bytes_multiplayer_messages
std::vector< char > raw_data
size_t m_bytes_multiplayer_messages
static size_t s_num_multiplayer_messages
static SGPropertyNode_ptr s_prop_num_multiplayer_messages
SGPropertyNode_ptr m_sim_startup_xpos
FGMultiplayMgr * m_MultiplayMgr
SGPropertyNode_ptr m_recovery_period
SGPropertyNode_ptr m_replay_multiplayer
std::unique_ptr< struct Continuous > m_continuous
SGPropertyNode_ptr m_replay_time
SGPropertyNode_ptr m_record_normal_begin
SGPropertyNode_ptr m_replay_time_str
SGPropertyNode_ptr m_replay_duration_act
SGPropertyNode_ptr m_log_frame_times
SGPropertyNode_ptr m_replay_master
SGPropertyNode_ptr m_replay_master_eof
bool start(bool NewTape=false)
Start replay session.
SGPropertyNode_ptr m_record_normal_end
static int loadContinuousHeader(const std::string &path, std::istream *in, SGPropertyNode *properties)
SGPropertyNode_ptr m_speed_up
SGPropertyNode_ptr m_sim_startup_ypos
std::vector< FGReplayMessages >::iterator m_current_msg
std::deque< FGReplayData * > m_medium_term
std::deque< FGReplayData * > m_long_term
SGPropertyNode_ptr m_sim_startup_ysize
SGPropertyNode_ptr m_sim_startup_xsize
void update(double dt)
static std::string makeTapePath(const std::string &tape_name)
std::deque< FGReplayData * > m_short_term
std::shared_ptr< FGFlightRecorder > m_flight_recorder
bool loadTape(const SGPropertyNode *ConfigData)
Load a flight recorder tape from disk.
SGPropertyNode_ptr m_simple_time_enabled
SGPropertyNode_ptr m_replay_error
SGPropertyNode_ptr m_disable_replay
std::vector< FGReplayMessages > m_replay_messages
SGPropertyNode_ptr m_replay_looped
std::deque< FGReplayData * > m_recycler
bool saveTape(const SGPropertyNode *ConfigData)
Write flight recorder tape to disk.
virtual std::string do_grouping() const
virtual char do_thousands_sep() const
char Callsign[8]