Repository navigation
Expand file tree
/
Copy pathscript.cpp
More file actions
2796 lines (2710 loc) · 130 KB
/
Copy pathscript.cpp
File metadata and controls
2796 lines (2710 loc) · 130 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// script.cpp: the module state, the log file, the clip picker and clip read, the music plan, StopPlayback, the unload (OnModuleDetach), the keyboard, the bench, the world reload and the safe exit, ScriptMain.
// The other parts are in src/script_*.cpp; shared internals in script_internal.h.
#include "script_internal.h"
#include "src/reshade_check.h"
namespace
{
constexpr float kPi = 3.14159265358979323846f;
constexpr ULONGLONG kMaxRecordingMs = 60000;
constexpr float kFreeCamMouseSensitivity = 10.0f;
constexpr float kFreeCamLookResponse = 18.0f;
constexpr float kFreeCamMoveResponse = 7.0f;
volatile LONG g_toggleRecordingRequested = 0;
volatile LONG g_saveBufferRequested = 0; // Shift+F4
volatile LONG g_togglePlaybackRequested = 0;
volatile LONG g_toggleFreeCamRequested = 0;
volatile LONG g_toggleOverlayRequested = 0;
volatile LONG g_loadNextClipRequested = 0;
volatile LONG g_qaReportRequested = 0; // Shift+F1
}
// Debug mode (RedemptionEditor.ini [RedemptionEditor] Debug=1): the
// development setup, the text HUD and the direct keys, with no product-only
// steps (the editor's playback bar, the clip load presentation). Without it
// the mod runs as the product: the editor is the only interface.
bool g_debugMode = false;
namespace
{
bool g_cleanView = false; // bench camera preview: no HUD of any kind
}
volatile bool g_editorCoversScreen = false; // the ImGui editor is open over the game (read by the key handler)
// The editor draws the loading cue while the world loads (CommandType::SetLoadingCue,
// sent every 2 s): its tick count, 0 never. Within 5 s of it the core's own
// "Loading replay..." text is left out (the cue has the caption).
volatile ULONGLONG g_editorLoadingCueAt = 0;
bool EditorDrawsLoadingCue() { return g_editorLoadingCueAt != 0 && GetTickCount64() - g_editorLoadingCueAt < 5000; }
// Simple camera: Insert adds a keyframe at the playhead from the free camera,
// Delete removes the nearest one, End switches the track.
volatile LONG g_cameraKeyAddRequested = 0;
volatile LONG g_cameraKeyDeleteRequested = 0;
volatile LONG g_cameraTrackToggleRequested = 0;
int g_playbackSpeedIndex = kNormalSpeedIndex;
volatile LONG g_speedStepRequested = 0; // -1 slower, +1 faster
float g_worldTimeScaleApplied = 1.0f;
namespace
{
volatile LONG g_togglePlayheadPauseRequested = 0;
}
volatile LONG g_moveForward = 0;
volatile LONG g_slowMove = 0; // Ctrl held: the free camera moves slow
volatile LONG g_moveBackward = 0;
volatile LONG g_moveLeft = 0;
volatile LONG g_moveRight = 0;
volatile LONG g_moveUp = 0;
volatile LONG g_moveDown = 0;
volatile LONG g_turnLeft = 0;
volatile LONG g_turnRight = 0;
volatile LONG g_turnUp = 0;
volatile LONG g_turnDown = 0;
volatile LONG g_rollLeft = 0; // Z, the free camera's roll
volatile LONG g_rollRight = 0; // C
volatile LONG g_fovIn = 0;
volatile LONG g_fovOut = 0;
volatile LONG g_fastMove = 0;
// The free camera's flight (free_cam.h: Classic, Drone, acrobatic, Walk,
// collision, locked altitude), its target (follow, pick) and the markers drawn
// in the world (camera_overlay.h). Keys and editor commands only set bits
// here; the script thread takes them (ProcessFreeCamActions).
replay::freecam::Settings g_fcSettings; // FreeCam* keys of the ini and the editor's options
replay::freecam::State g_fcState; // momentum and orientation between ticks
replay::freecam::Follow g_fcFollow = replay::freecam::Follow::Off;
std::uint32_t g_fcTarget = 0; // packed replay id, 0 none
bool g_fcLocked = false; // the camera holds still (L)
bool g_fcPadActive = false; // the pad moved it last
bool g_fcTargetLost = false; // the target is not in the clip at the playhead
bool g_fcTargetFrameValid = false;
volatile LONG g_fcActionBits = 0;
bool g_camOverlayEnabled = true; // CameraOverlay=1
bool g_camOverlayWhilePlaying = false; // CameraOverlayWhilePlaying=0: shown paused, in Edit Camera and the free camera
bool g_camOverlayTimeLabels = true;
bool g_camOverlayTrail = true;
// The Spline defaults (RedemptionEditor.ini Spline*; the editor's Spline
// Defaults rows write them back). A marker's tuning overrides some fields.
replay::editor::SplineTuning g_splineDefaults = replay::editor::DefaultSplineTuning();
volatile LONG g_moduleDetaching = 0;
namespace
{
// Set when another copy of the mod runs in this process (an old ReplayProbe.asi
// or a second RedemptionEditor.asi). This copy then starts nothing: no hook, no
// thread, no recording; it only says so in the log and on screen.
volatile LONG g_refusedStart = 0;
}
// The game restarted its scripts (ScriptMain ran again in this module) and
// ShutdownSessionAfterRestart zeroed the session. The snapshot carries the
// count (sessionResetSequence): the add-on closes its screens when it grows.
// The notice for the player waits for the end of the loading screen.
std::uint32_t g_sessionResets = 0;
namespace
{
bool g_sessionRestarted = false; // this ScriptMain is a restart (set before the setup)
std::string g_sessionResetNotice;
}
// [Keys] values refused at load, said once the game is up.
std::string g_keysNotice;
namespace
{
// Why the editor add-on is not loaded (reshade_check.h): checked once per
// game when it is playable, and again on the editor key.
bool g_reshadeChecked = false;
volatile LONG g_editorKeyWithoutAddon = 0;
ULONGLONG g_reshadeKeyNoticeAt = 0;
}
// The key the table holds for an action ("F7", "J / U"), or "unbound" (texts
// of the game notices).
std::string KeyText(replay::keys::Action action)
{
const std::string text = replay::keys::Keys(replay::keys::Current(), action);
return text.empty() ? std::string("unbound") : text;
}
namespace
{
// The exit from the replay that reloads the save sets this before it pushes
// the load, with the clip and the project already saved. The restart that
// follows reads it (SESSION_RESET,why=own_reload): not a loss, nothing to tell
// the player.
replay::session::ExitReloadRequest g_exitReloadPending;
}
WorldModelSession g_world;
// The last HOOK_WARMUP (SESSION_CHECK after a restart).
MH_STATUS g_lastWarmUpStatus = MH_OK;
int g_lastWarmUpApplied = 0;
// Tick count of the last Ctrl+R (ScriptHook's reload key with the .dev file).
// The unload's DllMain cannot run natives, so a paused replay's world freeze
// is lifted by the ticks that still run between the key and the unload
// (UpdateWorldFreeze).
volatile LONG64 g_reloadKeyAt = 0;
bool g_overlayEnabled = true;
bool g_recording = false;
std::uint32_t g_savedClipMs = 0; // length of the last clip written (the editor's notice)
// How long the recording start took and the frame gaps of its first ticks.
double TicksMs(std::uint64_t ticks)
{
return static_cast<double>(ticks) * 1000.0 / static_cast<double>(replay::prof::TicksPerSecond());
}
RecordingTiming g_recTiming;
int g_recordingsThisLoad = 0;
RecordingConfig g_recConfig;
std::uint32_t g_gateReasons = 0; // the last tick's reasons
RecordKind g_recordKind = RecordKind::Manual;
std::uint32_t g_bufferHeadMs = 0; // capture clock: the ring's first kept block
bool g_manualInBuffer = false; // F4 marked a manual start inside the buffer
std::uint32_t g_manualStartMs = 0;
ULONGLONG g_bufferResumeAt = 0; // restart after a playback or a pause
// A clip loaded from disk waits for its playback: the buffer stopped for the
// load would otherwise restart on the next tick (nothing is playing) and wipe
// the loaded clip. Cleared when a playback or a recording starts.
bool g_loadedClipHoldsBuffer = false;
int g_manualPart = 0;
// The clip start's world readiness log and the staggered puppet start
// (clip_start_gate.h).
replay::clipstart::Config g_clipStartConfig;
std::uint32_t g_clipWorldLines = 0;
std::uint64_t g_clipWorldLastLogMs = 0;
std::uint32_t g_clipWorldLastBits = ~0u;
std::uint64_t g_clipStartTogglesBefore = 0; // the IPL toggles when this clip's start began (before the stop)
// Playhead, pause, scrub and timeline length live in the transport.
replay::transport::Transport g_transport;
// Scrub: the cursor speed while the key is held.
volatile LONG g_scrubBackHeld = 0; // ','
volatile LONG g_scrubForwardHeld = 0; // '.'
bool g_freeCamEnabled = false;
bool g_freeCamPlayerWasFrozen = false;
ULONGLONG g_recordingStartedAt = 0;
Ped g_freeCamPlayer = 0;
Cam g_freeCam = 0;
// The playback camera while the freecam is off: the camera keyframes when the
// track has any, else the recorded game camera.
Cam g_replayCam = 0;
bool g_replayCamRendering = false;
Vector3 g_freeCamPosition{};
Vector3 g_freeCamRotation{};
Vector3 g_freeCamVelocity{};
float g_freeCamFov = 50.0f;
float g_freeCamSmoothedLookX = 0.0f;
float g_freeCamSmoothedLookY = 0.0f;
replay::camera::Track g_cameraTrack;
bool g_cameraTrackEnabled = true;
bool g_cameraTrackManualOverride = false;
namespace
{
float DegreesToRadians(float degrees)
{
return degrees * (kPi / 180.0f);
}
Vector3 RotationToDirection(const Vector3& rotation)
{
const float pitch = DegreesToRadians(rotation.x);
const float yaw = DegreesToRadians(rotation.z);
const float horizontal = std::fabs(std::cos(pitch));
Vector3 direction{};
direction.x = -std::sin(yaw) * horizontal;
direction.y = std::cos(yaw) * horizontal;
direction.z = std::sin(pitch);
return direction;
}
bool IsPedFullyOnMount(Ped ped)
{
return PED::IS_PED_FULLY_ON_MOUNT(ped, TRUE) != FALSE;
}
}
void DrawText(const char* text, float x, float y, float scale, int r, int g, int b, int a)
{
replay::hud::DrawText(text, x, y, scale, r, g, b, a);
}
void ShowObjective(const char* message) { replay::hud::ShowObjective(message); }
namespace
{
ProbeFrame CaptureFrame()
{
// The main loop's parts without a zone get one (otherwise over a millisecond
// of the play tick falls outside every zone).
REPLAY_PROFILE_ZONE("tick.capture_frame");
ProbeFrame frame{};
frame.timeMs = GetTickCount64();
frame.playerPed = PLAYER::PLAYER_PED_ID();
if (!ENTITY::DOES_ENTITY_EXIST(frame.playerPed))
return frame;
frame.position = ENTITY::GET_ENTITY_COORDS(frame.playerPed, TRUE, FALSE);
frame.cameraPosition = CAM::GET_GAMEPLAY_CAM_COORD();
frame.cameraRotation = CAM::GET_GAMEPLAY_CAM_ROT(2);
frame.cameraFov = CAM::GET_GAMEPLAY_CAM_FOV();
// IS_PED_ON_MOUNT turns true as the mount interaction starts;
// IS_PED_FULLY_ON_MOUNT only once the rider settled in the saddle.
const bool onMount = PED::IS_PED_ON_MOUNT(frame.playerPed) != FALSE;
const Ped observedMount = onMount ? PED::GET_MOUNT(frame.playerPed) : 0;
frame.mount = onMount && IsPedFullyOnMount(frame.playerPed) ? observedMount : 0;
frame.mountTarget = observedMount;
return frame;
}
// Log rotation: past 256 MB the log is renamed with the date when the mod loads
// (never deleted: its telemetry is kept) and a new one starts. The bench host
// takes its log offset at each run's start. The log is in the base folder
// (storage_paths.h: the game folder, else Documents).
std::wstring OpenedLogName()
{
constexpr std::uint64_t kRotateBytes = 256ull * 1024 * 1024;
const std::wstring log = replay::storage::Path(replay::identity::AsciiW(replay::identity::kLogFile));
WIN32_FILE_ATTRIBUTE_DATA info{};
if (GetFileAttributesExW(log.c_str(), GetFileExInfoStandard, &info) &&
((static_cast<std::uint64_t>(info.nFileSizeHigh) << 32) | info.nFileSizeLow) > kRotateBytes)
{
SYSTEMTIME now{};
GetLocalTime(&now);
char archived[64]{};
std::snprintf(archived, sizeof(archived), "%s-%04u%02u%02u-%02u%02u%02u.log", replay::identity::kLogStem, now.wYear, now.wMonth,
now.wDay, now.wHour, now.wMinute, now.wSecond);
MoveFileExW(log.c_str(), replay::storage::Path(replay::identity::AsciiW(archived)).c_str(), 0);
}
return log;
}
}
QuietFilter g_logFilter;
// The DOF, target and streaming work (CONFIG_CAMERA2): the DOF interpolated
// in a blend, the focus hysteresis and the focus distance spring.
replay::camera::FocusConfig g_camFocusConfig;
// The view lent to another mod's script camera (ini ReplayCameraRelease,
// camera_cut.h Release), and whether it is lent now.
// The streaming focus follows the rendering camera every tick while the replay
// owns the world (world_focus_core.h); CLEAR_FOCUS only when the playback stops.
replay::worldfocus::Tracker g_worldFocusTracker;
Vector3 g_worldFocusAt{};
std::ofstream& LogFile()
{
static std::ofstream file(OpenedLogName().c_str(), std::ios::out | std::ios::app);
static const bool filtered = [] {
g_logFilter.Attach(file.rdbuf());
file.std::ios::rdbuf(&g_logFilter);
return true;
}();
(void)filtered;
return file;
}
// Edit Camera's view is resolved again once the puppets stand at the marker's
// time (UpdateFreeCam).
bool g_editResolvePending = false;
bool g_freeCamBeforeEdit = false; // restored when Edit Camera ends
namespace
{
// PoseTrack v2 entity registry, recording side. Every recorded entity gets a
// ReplayId (slot + instance). Its create and delete packets go to the
// lifecycle track, which opens every block with a full create of the live
// entities, and its pose record in replay::pose::capture is keyed by the same
// id. Entities enter and leave by the proximity scan below, and also leave when
// they stop existing or their skeleton is replaced (a new entity for the replay).
constexpr std::uint32_t kPoseV2SampleHz = replay::recorder::kSampleHz;
}
// The class capacities are replay::capacity::PedCapacity() (ini PedCapacity)
// and replay::capacity::VehicleCapacity() (8, or the vehicle pool less 1
// with WorldModel=gta).
// The clip in memory (src/replay_recording.h): the recorder fills it,
// playback reads it.
replay::Recording g_clip;
namespace
{
// The recorder's registry (src/replay_recorder.h): engine entities
// being recorded and what the recorder remembers about them.
replay::recorder::Recorder g_recorder;
}
// The puppet registry (src/replay_puppets.h) and the context the
// subsystem modules work on.
replay::playback::Puppets g_puppets;
replay::Context g_context{g_clip, g_recorder, g_puppets, g_transport, LogFile()};
std::size_t PoseV2PuppetCount() { return g_puppets.Count(); }
std::size_t PoseV2RecordedCount() { return g_recorder.entities.size(); }
// Clips on disk (src/clip_file.h): every recording is saved to the
// ReplayClips folder when it stops; F8 loads the clips of the folder in turn, newest first. The
// file carries no session pointer; image offsets are rebased with this module.
const wchar_t* kClipFolder = L"ReplayClips";
std::vector<replay::editor::ClipInfo> g_editorClips;
// The list newest first as read (g_editorClips is it sorted for the gallery; a
// sort change re-sorts this one, no disk read).
std::vector<replay::editor::ClipInfo> g_editorClipsByDate;
// The clip loads, buffer plays and recordings (the editor tells another clip
// from a rename or a first save of the same one).
std::uint32_t g_clipLoadSequence = 0;
// A gallery load waiting for the save of the clip being left (no freeze in
// FinishPendingSave while it is written).
std::wstring g_pendingLoadPath;
std::uint32_t g_clipSort = 0; // 0 newest first, 1 longest first
std::uint64_t g_editorClipListSequence = 0;
std::wstring g_currentClipPath;
// The file that holds the clip in memory (loaded, or written from it), empty
// for a buffer window played before it was saved: the editor's save writes
// there (g_currentClipPath also names the last buffer window saved, another
// clip). g_clipEdited: markers changed since.
std::wstring g_memoryClipPath;
bool g_clipEdited = false;
// `preloaded` (may be null): that file already read on a thread
// (StartClipRead); used when it is this path.
bool ReadClipFile(const std::wstring& path, std::string& error, ClipRead* preloaded = nullptr)
{
FinishPendingSave(true); // a clip being written is read once it is whole
if (g_recording && g_recordKind == RecordKind::Buffer)
StopRecording(false); // the buffer gives way to the clip; it resumes after
if (g_recording)
{
error = "recording";
return false;
}
if (g_transport.Playing())
StopPlayback();
// After the stop: leaving the open clip starts its autosave, and this may be
// that same file.
FinishPendingSave(true);
std::uintptr_t base = 0;
std::size_t size = 0;
ImageRange(base, size);
const ULONGLONG startedAt = preloaded ? preloaded->startedAt : GetTickCount64();
replay::Recording loaded;
std::vector<replay::pose::PoseRecordExport> poses;
replay::camera::Track camera;
std::uint64_t durationMs = 0;
// The background compressor stays off this file (and off the disk while it is
// read); a compressed clip is decompressed in memory by Load.
replay::clipcompress::worker::SetOpenClip(path);
replay::clipcompress::worker::SetHold(true);
replay::clipfile::LoadInfo loadInfo;
// A file with a save not written yet (held after a failure, or queued) opens
// from that save's snapshot: it is newer than the file.
bool fromQueue = false;
bool ok = false;
if (CopyQueuedClip(path, loaded, poses, camera, durationMs, error))
ok = fromQueue = true;
else if (error.empty() && preloaded && preloaded->path == path)
{
// Read on a thread.
ok = preloaded->ok;
error = preloaded->error;
loadInfo = preloaded->info;
loaded = std::move(preloaded->recording);
poses = std::move(preloaded->poses);
camera = std::move(preloaded->camera);
durationMs = preloaded->durationMs;
}
else if (error.empty())
ok = replay::clipfile::Load(path, base, size, loaded, poses, camera, durationMs, error, &loadInfo);
// The deadband smoothing (pose_deadband_smooth.h): done on the read
// thread for a preloaded read, here for a direct one. A queued save's
// snapshot holds the session's tracks (already fine or smoothed).
if (ok && !fromQueue && replay::pose::smooth::LoadSmoothing())
{
const bool onThread = preloaded && preloaded->path == path && preloaded->posesSmoothed;
replay::pose::smooth::RecordsSummary summary;
ULONGLONG smoothMs = 0;
if (onThread)
{
summary = preloaded->smoothSummary;
smoothMs = preloaded->smoothMs;
}
else
{
const ULONGLONG smoothStart = GetTickCount64();
replay::pose::smooth::SmoothRecords(poses, summary);
smoothMs = GetTickCount64() - smoothStart;
}
LogFile() << "CLIP_POSE_SMOOTH,records=" << summary.records << ",smoothed=" << summary.smoothed
<< ",failed=" << summary.failed << ",frames=" << summary.frames << ",smoothed_frames=" << summary.smoothedFrames
<< ",bytes_in=" << summary.bytesIn << ",bytes_out=" << summary.bytesOut << ",world_spikes=" << summary.worldSpikes
<< ",spike_records=" << summary.spikeRecords << ",ms=" << smoothMs
<< ",thread=" << (onThread ? "read" : "script") << '\n';
}
ok = ok && replay::pose::capture::ImportRecords(poses, error);
replay::clipcompress::worker::SetHold(SavePending() || ClipReadPending());
if (loadInfo.compressed)
LogFile() << "CLIP_DECOMPRESS,ms=" << loadInfo.decompressMs << ",bytes=" << loadInfo.clipBytes
<< ",file_bytes=" << loadInfo.fileBytes << ",format=" << (loadInfo.packed ? "packed" : "zstd")
<< ",path=" << Narrow(path) << '\n';
if (ok)
{
// The age rule reads the last access; NTFS may not keep it.
if (!fromQueue)
replay::clipcompress::StampAccess(path);
g_currentClipPath = path;
g_memoryClipPath = path;
g_clipEdited = false;
g_markerHistory = {};
++g_clipLoadSequence;
g_clip = std::move(loaded);
g_clip.fromDisk = true;
g_cameraTrack = std::move(camera);
g_cameraTrackEnabled = true;
g_cameraTrackManualOverride = g_cameraTrack.Empty();
g_transport.SetDurationMs(durationMs);
g_loadedClipHoldsBuffer = true;
}
// The optional sections the reader left out (the clip opened without them).
for (const replay::clipfile::SkippedSection& section : loadInfo.skipped)
LogFile() << "CLIP_SECTION_SKIPPED,section=" << section.section << ",reason=" << section.reason
<< ",path=" << Narrow(path) << '\n';
LogFile() << "CLIP_LOAD,path=" << Narrow(path) << ",ok=" << ok << ",duration_ms=" << durationMs
<< ",entities=" << (ok ? g_clip.created.size() : 0) << ",poses=" << poses.size()
<< ",ms=" << (GetTickCount64() - startedAt) << ",error=" << (ok ? "none" : error.c_str()) << '\n';
// A buffer window saved while the recording ran, its environment flag set
// from its track at the load (clip_file.cpp).
if (ok && g_clip.environment.repairedOnLoad)
LogFile() << "CLIP_ENV_REPAIRED,path=" << Narrow(path) << ",clock_start_s=" << g_clip.environment.clockStartS
<< ",clock_end_s=" << g_clip.environment.clockEndS << ",env_samples=" << g_clip.environment.track.size()
<< '\n';
return ok;
}
namespace
{
constexpr ULONGLONG kClipPickerIdleMs = 1500;
void PressClipPicker(ULONGLONG now)
{
if (!g_clipPickerOpen)
{
g_clipPickerFiles = ListClipFiles();
if (g_clipPickerFiles.empty())
{
ShowObjective("Redemption Editor: no clips in ReplayClips");
return;
}
g_clipPickerOpen = true;
g_clipPickerIndex = 0;
}
else
g_clipPickerIndex = (g_clipPickerIndex + 1) % g_clipPickerFiles.size();
g_clipPickerLastPress = now;
LogFile() << "CLIP_PICKER,press,index=" << g_clipPickerIndex << ",files=" << g_clipPickerFiles.size() << ",now=" << now << '\n';
}
void UpdateClipPicker(ULONGLONG now)
{
if (!g_clipPickerOpen)
return;
const std::size_t count = g_clipPickerFiles.size();
const std::size_t first = g_clipPickerIndex >= 4 ? g_clipPickerIndex - 4 : 0;
float y = 0.30f;
DrawText("Clips (F8 next, release to load)", 0.018f, y, 0.32f, 235, 195, 95, 240);
y += 0.030f;
for (std::size_t n = first; n < count && n < first + 10; ++n)
{
const std::wstring& path = g_clipPickerFiles[n];
const std::string name = Narrow(path.substr(path.find_last_of(L'\\') + 1));
const bool selected = n == g_clipPickerIndex;
DrawText(((selected ? "> " : " ") + name).c_str(), 0.018f, y, 0.30f, selected ? 255 : 200, selected ? 235 : 200,
selected ? 140 : 200, 235);
y += 0.026f;
}
if (now - g_clipPickerLastPress < kClipPickerIdleMs)
return;
g_clipPickerOpen = false;
const std::wstring path = g_clipPickerFiles[g_clipPickerIndex];
std::string error;
const bool ok = ReadClipFile(path, error);
const std::string shown = Narrow(path.substr(path.find_last_of(L'\\') + 1));
ShowObjective(ok ? ("Redemption Editor: clip loaded " + shown + " (" + KeyText(replay::keys::Action::Stop) + " plays)").c_str()
: ("Redemption Editor: could not load " + shown + ": " + error).c_str());
}
void ApplyMusicPlan(const replay::music::Plan& plan)
{
if (plan.setFlag)
AUDIO::SET_AUDIO_FLAG(replay::music::kFlagName, plan.flagValue ? TRUE : FALSE);
if (plan.stopScenes)
AUDIO::STOP_AUDIO_SCENES(); // this script's slot only (the scenes this script started)
}
}
// The replay takes the world (end of BeginClipWorld, the screen going black in
// product mode). With the save reload this goes after the scripts' cleanup:
// their FAIL/STOP events would play over a stop made before it.
void MusicEnter()
{
const bool playingBefore = AUDIO::AUDIO_IS_MUSIC_PLAYING() != FALSE;
const int playtime = AUDIO::GET_MUSIC_PLAYTIME();
const replay::music::Plan plan = g_music.Enter(replay::music::Mode::Natives, GetTickCount64());
if (plan.already)
{
LogFile() << "MUSIC_STOP,phase=enter,mode=1,already=1\n";
return;
}
ApplyMusicPlan(plan);
const bool eventOk = plan.triggerStop && AUDIO::TRIGGER_MUSIC_EVENT(replay::music::kStopEvent) != FALSE;
LogFile() << "MUSIC_STOP,phase=enter,mode=1,playing_before=" << (playingBefore ? 1 : 0) << ",playtime_ms=" << playtime
<< ",flag=1,event=" << replay::music::kStopEvent << ",event_ok=" << (eventOk ? 1 : 0)
<< ",internal=skip\n";
}
namespace
{
// The replay gives the world back: StopPlayback (every stop, also the one at
// the start of each playback and a Stop in the middle) and the unload. Does
// nothing without an entry. With the save reload, call this before the load
// is pushed (reload=1); if the mod's script dies in the load the engine frees
// its slot and the flag falls anyway, the FALSE covers the case where it
// survives.
void MusicExit(const char* why, bool reload = false)
{
const replay::music::Plan plan = g_music.Exit();
if (plan.already)
return;
ApplyMusicPlan(plan);
LogFile() << "MUSIC_STOP,phase=exit,why=" << why << ",reload=" << (reload ? 1 : 0)
<< ",flag=0,scenes_stopped=" << (plan.stopScenes ? 1 : 0) << ",restops=" << g_music.Restops() << '\n';
}
// Every tick: the sensor once a second while the flag is on.
void MusicWatch(ULONGLONG now)
{
const replay::music::WatchResult result =
g_music.Watch(now, [] { return AUDIO::AUDIO_IS_MUSIC_PLAYING() != FALSE; });
if (result == replay::music::WatchResult::Idle)
return;
const std::uint64_t since = g_music.SinceEntryMs(now);
if (result == replay::music::WatchResult::Changed || g_debugMode)
LogFile() << "MUSIC_WATCH,t_ms=" << since << ",playing=" << (g_music.Playing() ? 1 : 0) << '\n';
if (result != replay::music::WatchResult::Restop)
return;
const bool eventOk = AUDIO::TRIGGER_MUSIC_EVENT(replay::music::kStopEvent) != FALSE;
AUDIO::SET_AUDIO_FLAG(replay::music::kFlagName, TRUE); // same bit: counted once
LogFile() << "MUSIC_STOP,phase=restop,why=playing_again,t_ms=" << since << ",event_ok=" << (eventOk ? 1 : 0) << '\n';
}
#if defined(REDEMPTION_DEV) // the bench's fault commands (replay_bench.h): developer build only
bool g_benchThrowNextTick = false; // bench "safe_exit exception"
bool g_benchHoldNormalExit = false; // bench "safe_exit hold_exit": the normal exit is skipped
#endif
}
void StopPlayback()
{
// A project's clip keeps its markers in the project.
ProjectBeforePlaybackStop();
// Leaving a clip keeps its marker changes (saved, never lost).
if (g_clipEdited && g_transport.Playing())
SaveEditedClip("leave");
g_bufferResumeAt = GetTickCount64() + 1500;
StopOwnScreenEffects("playback_stop"); // also runs at every playback start
g_trimReady = false;
g_editingMarker = 0; // leaving the clip cancels Edit Camera
if (g_transport.Playing())
{
std::ofstream& log = LogFile();
replay::prof::WriteSessionReport(log, "play");
log.flush();
}
StopFreeCam();
StopReplayCamera();
ReleaseWorldFocus("playback_stop");
replay::gamelistener::Release("playback_stop", LogFile());
ThawWorld("playback_stop");
EndClipWorld();
replay::playback::Stop();
MusicExit("playback_stop");
replay::doors::EndPlayback(LogFile());
g_transport.Stop();
replay::pose::capture::ReleaseFinalizer(); // the Pose Editor may take it now
if (g_worldTimeScaleApplied != 1.0f)
{
MISC::SET_TIME_SCALE(1.0f);
g_worldTimeScaleApplied = 1.0f;
}
g_playbackSpeedIndex = kNormalSpeedIndex; // each playback starts at 100%
g_cameraTrackManualOverride = false;
std::ofstream& log = LogFile();
log << "PLAYBACK_STOP"
<< ",actor_backend=pose_track_v2"
<< '\n';
log.flush();
}
// The game's cinematic slow motion (kill cam) stretches of the clip, found at
// playback start from the camera cuts and the time scale.
std::vector<replay::camera::TimeSpan> g_cinematicSpans;
std::uint32_t g_cinematicSpanLogs = 0;
const replay::camera::TimeSpan* g_cinematicSpanShown = nullptr;
namespace
{
#if defined(REDEMPTION_DEV) // the bench's camera ops (replay_bench.h): developer build only
// Bench camera markers: marker_add <ms> key=value... with
// mode=free|game|front_low|rear|right|left|overhead pos=x,y,z rot=p,r,h
// fov= blend=none|linear|smooth ease=0|1 look=/attach=/focus_t=
// main|pedN|none mount=rigid|default|flexible shake=none|handheld|drunk|
// ground|air|explosion|walking|running|horseback|steadicam|wagon shake_i= shake_s=<index> dof=default|custom|none
// focus=auto|manual|target focus_d= dof_i= roll= dist=
std::uint32_t BenchTarget(const std::string& name)
{
if (name == "main")
return MainCharacterId();
// Bench (tool only): horseN and weaponN, the Nth puppet of that kind.
for (const auto& [prefix, kind] : {std::pair<const char*, replay::entity::EntityKind>{"horse", replay::entity::EntityKind::Horse},
std::pair<const char*, replay::entity::EntityKind>{"weapon", replay::entity::EntityKind::Weapon}})
{
const std::size_t length = std::strlen(prefix);
if (name.compare(0, length, prefix) != 0)
continue;
const int wanted = std::atoi(name.c_str() + length);
int seen = 0;
for (const replay::playback::Puppet& puppet : g_puppets.list)
if (puppet.packet.kind == kind && ++seen == wanted)
return puppet.packet.id.Packed();
return 0;
}
if (name.rfind("ped", 0) == 0)
{
const int wanted = std::atoi(name.c_str() + 3);
const std::uint32_t main = MainCharacterId();
int seen = 0;
for (const replay::playback::Puppet& puppet : g_puppets.list)
{
if (puppet.packet.kind != replay::entity::EntityKind::Ped || puppet.packet.id.Packed() == main)
continue;
if (++seen == wanted)
return puppet.packet.id.Packed();
}
}
return 0;
}
// Bench (tool only): attach_bone/look_bone=family:Label (a named bone of
// camera_bones.h, e.g. human:Head, weapon:Muzzle), near_clip, fstop and
// lens_mm in metres, f-number and millimetres.
std::string BenchBoneLabel(std::string label)
{
std::replace(label.begin(), label.end(), '_', ' '); // scenario tokens have no spaces: Right_Hand
return label;
}
bool BenchMarkerLensKey(replay::camera::Keyframe& marker, const std::string& key, const std::string& value, std::string& detail)
{
namespace bones = replay::camera::bones;
if (key == "attach_bone" || key == "look_bone")
{
const std::size_t colon = value.find(':');
std::uint32_t ref = 0;
bool found = false;
for (std::uint32_t n = 0; n < bones::kNamedCount && colon != std::string::npos; ++n)
if (value.compare(0, colon, bones::FamilyName(bones::kNamed[n].family)) == 0 && BenchBoneLabel(value.substr(colon + 1)) == bones::kNamed[n].label)
{
ref = bones::Named(n);
found = true;
break;
}
if (!found)
{
detail = "unknown_bone=" + value;
return false;
}
(key == "attach_bone" ? marker.attachBone : marker.lookAtBone) = ref;
return true;
}
const float number = static_cast<float>(std::atof(value.c_str()));
if (key == "near_clip")
marker.nearClip = number;
else if (key == "fstop")
marker.dofFStop = number;
else if (key == "lens_mm")
marker.dofLensMm = number;
return true;
}
bool BenchCameraOp(const std::string& op, const std::vector<std::string>& args, std::string& detail)
{
if (op == "marker_lens")
{
// Bench (tool only): change the lens of the marker at that time in place (what
// the editor's menu does to the same marker).
const std::uint64_t at = static_cast<std::uint64_t>(std::atoll(args[0].c_str()));
replay::camera::Keyframe* key = nullptr;
for (const replay::camera::Keyframe& k : g_cameraTrack.Keyframes())
if (k.timeMs == at)
key = g_cameraTrack.FindMutable(k.id);
if (!key)
{
detail = "no_marker_at=" + args[0];
return false;
}
for (std::size_t n = 1; n < args.size(); ++n)
{
const std::size_t eq = args[n].find('=');
if (eq == std::string::npos)
{
detail = "bad_arg=" + args[n];
return false;
}
const std::string name = args[n].substr(0, eq);
const std::string value = args[n].substr(eq + 1);
if (name == "focus_d")
key->focusDistance = static_cast<float>(std::atof(value.c_str()));
else if (name == "dof")
key->dof = value == "custom" ? replay::editor::DofMode::Custom : value == "none" ? replay::editor::DofMode::None : replay::editor::DofMode::Default;
else if (!BenchMarkerLensKey(*key, name, value, detail))
return false;
}
LogFile() << "BENCH_MARKER_LENS,id=" << key->id << ",ms=" << at << ",near_clip=" << key->nearClip << ",fstop=" << key->dofFStop
<< ",lens_mm=" << key->dofLensMm << ",focus_d=" << key->focusDistance << '\n';
detail = "id=" + std::to_string(key->id);
return true;
}
using namespace replay::editor;
// Bench: freecam on|off (the free camera from the rendered view), focus
// clear|log (clear: the next UpdateWorldFocus sets it again when WorldFocus=1),
// player_far <m> (the hidden live player that far east of the rendered camera;
// use with TrailPlayerFollow=0).
if (op == "freecam")
{
const bool on = !args.empty() && args[0] == "on";
if (on)
{
if (!g_freeCamEnabled && !StartFreeCam(GetTickCount64()))
{
detail = "freecam_failed";
return false;
}
}
else
{
StopFreeCam();
}
LogFile() << "BENCH_FREECAM,on=" << (on ? 1 : 0) << ",enabled=" << (g_freeCamEnabled ? 1 : 0) << '\n';
return true;
}
if (op == "focus")
{
const std::string what = args.empty() ? "log" : args[0];
if (what == "clear")
STREAMING::CLEAR_FOCUS();
const Ped player = PLAYER::PLAYER_PED_ID();
const bool playerFocus = player && ENTITY::DOES_ENTITY_EXIST(player) && STREAMING::IS_ENTITY_FOCUS(player) != FALSE;
char text[160];
std::snprintf(text, sizeof(text), "what=%s,world_focus=1,source=%s,player_is_focus=%d", what.c_str(),
replay::worldfocus::SourceName(g_worldFocusTracker.last), playerFocus ? 1 : 0);
detail = text;
LogFile() << "BENCH_FOCUS," << text << '\n';
return true;
}
if (op == "player_far")
{
float meters = 400.0f;
if (!args.empty())
meters = static_cast<float>(std::atof(args[0].c_str()));
const Ped player = PLAYER::PLAYER_PED_ID();
if (!player || !ENTITY::DOES_ENTITY_EXIST(player))
{
detail = "no_player";
return false;
}
const Vector3 cam = CAM::GET_FINAL_RENDERED_CAM_COORD();
const float x = cam.x + meters, y = cam.y;
float z = cam.z;
float ground = 0.0f;
if (MISC::GET_GROUND_Z_FOR_3D_COORD(x, y, cam.z + 200.0f, &ground, FALSE))
z = ground + 1.0f;
const Ped mount = PED::IS_PED_ON_MOUNT(player) ? PED::GET_MOUNT(player) : 0;
replay::pose::playback::MoveHiddenEntity(mount ? mount : player, x, y, z);
char text[160];
std::snprintf(text, sizeof(text), "meters=%.0f,at=%.1f;%.1f;%.1f,mounted=%d", meters, x, y, z, mount ? 1 : 0);
detail = text;
LogFile() << "BENCH_PLAYER_FAR," << text << '\n';
return true;
}
if (op == "marker_clear")
{
g_shakeFoldHold = 2;
g_cameraTrack.Clear();
g_cameraSpring.Reset();
return true;
}
if (op == "cam_log")
{
const Vector3 p = CAM::GET_FINAL_RENDERED_CAM_COORD();
const Vector3 r = CAM::GET_FINAL_RENDERED_CAM_ROT(2);
CameraState evaluated{};
replay::camera::SegmentPlan plan;
const bool keyed = EvaluateCameraTrack(g_transport.PlayheadMs(), evaluated, 0.0f, &plan);
char text[320];
std::snprintf(text, sizeof(text),
"playhead_ms=%llu,rendered=%.2f;%.2f;%.2f,rot=%.1f;%.1f;%.1f,fov=%.1f,keyed=%d,eval=%.2f;%.2f;%.2f,eval_rot=%.1f;%.1f;%.1f,eval_fov=%.1f,cur=%d,next=%d,blending=%d,phase=%.2f",
static_cast<unsigned long long>(g_transport.PlayheadMs()), p.x, p.y, p.z, r.x, r.y, r.z,
CAM::GET_FINAL_RENDERED_CAM_FOV(), keyed ? 1 : 0, evaluated.position.x, evaluated.position.y, evaluated.position.z,
evaluated.rotation.x, evaluated.rotation.y, evaluated.rotation.z, evaluated.fov, plan.current, plan.next,
plan.blending ? 1 : 0, plan.phase);
detail = text;
LogFile() << "BENCH_CAM," << text << '\n';
return true;
}
// spline_default key=value...: the Spline defaults for this session (not
// written to the ini; the bench restores the player's).
if (op == "spline_default")
{
replay::editor::SplineTuning d = g_splineDefaults;
for (const std::string& arg : args)
{
const std::size_t eq = arg.find('=');
bool known = false;
std::string error;
if (eq == std::string::npos || !replay::camera::spline::ParseField(arg.substr(0, eq), arg.substr(eq + 1), d, known, error))
{
detail = eq == std::string::npos ? "bad_arg=" + arg : known ? error : "unknown_key=" + arg.substr(0, eq);
return false;
}
}
d.overrides = 0;
g_splineDefaults = replay::camera::spline::Clamped(d);
LogSplineDefaults("bench");
return true;
}
// The shake events track. The events (and the legacy marker shake keys) are
// the migration's input: they wait in the track (they play by the legacy
// rules, the same picture) and TickShakeMigration folds them into the markers
// once a tick passes without a marker or event step (the whole set at once: an
// event ramps from the one before it).
if (op == "fx_clear")
{
g_shakeFoldHold = 2;
g_cameraTrack.LegacyEffects().Clear();
return true;
}
if (op == "fx_add")
{
// "fx_add <t> kind=amp value=1.5 mode=snap"; the legacy form (shake= shake_i=
// ...) is converted (camera_effects.h ConvertLegacy).
namespace fx = replay::camera::fx;
const std::uint64_t timeMs = static_cast<std::uint64_t>(std::atoll(args[0].c_str()));
const std::vector<std::string> tokens(args.begin() + 1, args.end());
std::string error;
std::vector<fx::ShakeEvent> events;
if (fx::IsLegacyLine(tokens))
{
fx::LegacyEvent old;
old.timeMs = timeMs;
if (!fx::ParseLegacyFields(tokens, old, error))
{
detail = error;
return false;
}
events = fx::ConvertLegacy({old});
}
else
{
fx::ShakeEvent event;
event.timeMs = timeMs;
if (!fx::ParseEventFields(tokens, event, error))
{
detail = error;
return false;
}
events.push_back(event);
}
std::uint32_t id = 0;
for (const fx::ShakeEvent& event : events)
{
g_shakeFoldHold = 2;
id = g_cameraTrack.LegacyEffects().Add(event);
if (!id)
break;
LogFile() << "CAMERA_FX,action=bench_add,id=" << id << ',' << fx::FormatEventFields(event) << '\n';
}
detail = "fx_id=" + std::to_string(id);
return id != 0;
}
// marker_add
g_shakeFoldHold = 2;
replay::camera::Keyframe marker;
marker.timeMs = static_cast<std::uint64_t>(std::atoll(args[0].c_str()));
const replay::camera::Keyframe* previous = nullptr;
for (const replay::camera::Keyframe& key : g_cameraTrack.Keyframes())
if (key.timeMs <= marker.timeMs)
previous = &key;
if (previous)
replay::camera::InheritFrom(marker, *previous, replay::camera::InsideBlend(g_cameraTrack, marker.timeMs));
CameraState view{};
view.fov = kMarkerFovDefault;
if (previous)
{
view.position = previous->pose.position;
view.rotation = replay::camera::Track::ToEulerDegrees(previous->pose.orientation);
view.fov = previous->pose.fov;
}
bool nearSet = false, rotSet = false;
Vec3 nearOffset{};
for (std::size_t n = 1; n < args.size(); ++n)
{
const std::size_t eq = args[n].find('=');
if (eq == std::string::npos)
{
detail = "bad_arg=" + args[n];
return false;
}
const std::string key = args[n].substr(0, eq);
const std::string value = args[n].substr(eq + 1);
float a = 0, b = 0, c = 0;
if (key == "mode")
{
static const std::pair<const char*, CameraMode> modes[] = {{"free", CameraMode::Free}, {"game", CameraMode::Game},
{"front_low", CameraMode::FrontLow}, {"rear", CameraMode::Rear}, {"right", CameraMode::RightSide},
{"left", CameraMode::LeftSide}, {"overhead", CameraMode::Overhead}};
for (const auto& [name, mode] : modes)
if (value == name)
marker.cameraMode = mode;
}
else if (key == "pos" && sscanf_s(value.c_str(), "%f,%f,%f", &a, &b, &c) == 3)
view.position = {a, b, c};
else if (key == "near" && sscanf_s(value.c_str(), "%f,%f,%f", &a, &b, &c) == 3)
{
// World metres from the attach target (else the Look At one, else the main
// character) at the playhead, looking at its head unless rot= is given.
nearSet = true;
nearOffset = {a, b, c};
}
else if (key == "rot" && sscanf_s(value.c_str(), "%f,%f,%f", &a, &b, &c) == 3)
{
view.rotation = {a, b, c};
rotSet = true;
}
else if (key == "fov")
view.fov = static_cast<float>(std::atof(value.c_str()));
else if (key == "blend")
marker.blend = value == "linear" ? Blend::Linear : value == "smooth" ? Blend::Smooth
: value == "spline" ? Blend::Spline : Blend::None;
else if (key == "ease")
marker.ease = value != "0";
else if (key == "look")
replay::camera::SetLookAt(marker, BenchTarget(value));
else if (key == "attach")
replay::camera::SetAttach(marker, BenchTarget(value));
else if (key == "mount")
replay::camera::SetAttachType(marker, value == "rigid" ? AttachType::Rigid : value == "flexible" ? AttachType::Flexible : AttachType::Default);
else if (key == "shake")
{
// The legacy keys (shake=, shake_i, shake_s, the Custom numbers) fill the
// legacy marker shake, folded like an old clip's (the scenarios play the
// same); the new keys (shake_transition, shake_gain, shake_rate,
// shake_realtime, shake_restart) the marker's own.
static const char* names[] = {"none", "handheld", "drunk", "ground", "air", "explosion",
"walking", "running", "horseback", "steadicam", "wagon", "custom"};