Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
SequenceRuntime.cpp
Go to the documentation of this file.
1#include "SequenceRuntime.hpp"
2#include "AudioRuntime.hpp"
3
4#include <algorithm>
5#include <cmath>
6#include <cstring>
7#include <functional>
8#include <fstream>
9#include <limits>
10#include <map>
11#include <utility>
12
13namespace monopoly::sequence
14{
15 namespace
16 {
17 RuntimeError error(RuntimeErrorCode code, data::DataId id,
18 std::size_t offset, std::string detail)
19 { return {code, id, offset, std::move(detail), {}}; }
20
21 template<class E>
22 RuntimeError caused(RuntimeErrorCode code, data::DataId id,
23 std::size_t offset, E cause)
24 {
25 auto detail = "sequence dependency failed (DataID " +
26 std::to_string(id) + ", offset " + std::to_string(offset) + ")";
27 if constexpr (requires { cause.detail; })
28 detail += ": " + cause.detail;
29 else if constexpr (requires { static_cast<int>(cause); })
30 detail += ": error " + std::to_string(static_cast<int>(cause));
31 auto result = error(code, id, offset, std::move(detail));
32 result.cause = std::move(cause);
33 return result;
34 }
35
37 bool applied, const SequenceTransform& local, std::uint8_t dimensionality)
38 {
39 if (!applied) return local;
40 if (dimensionality == 2)
41 return multiply(std::get<Matrix2D>(tweeker), std::get<Matrix2D>(local));
42 if (dimensionality == 3)
43 return multiply(std::get<Matrix3D>(tweeker), std::get<Matrix3D>(local));
44 return local;
45 }
46 SequenceMeshChoice3D initialMeshChoice(
47 const data::LegacySequenceAttributes& attributes,
48 std::uint8_t dimensionality) noexcept
49 {
50 SequenceMeshChoice3D result{};
51 if (dimensionality != 3) return result;
52 for (const auto& attribute : attributes.values)
53 if (const auto* choice =
54 std::get_if<data::Sequence3DMeshChoiceAttribute>(&attribute))
55 result = {choice->meshIndexA, choice->meshIndexB,
56 choice->meshProportion};
57 return result;
58 }
59
60 std::optional<SequenceBounds3D> initialBounds3D(
61 const data::LegacySequenceAttributes& attributes,
62 std::uint8_t dimensionality) noexcept
63 {
64 if (dimensionality != 3) return std::nullopt;
65 std::optional<SequenceBounds3D> result;
66 for (const auto& attribute : attributes.values)
67 {
68 if (const auto* box =
69 std::get_if<data::Sequence3DBoundingBoxAttribute>(&attribute))
70 {
71 SequenceBounds3D bounds{box->points.front(), box->points.front()};
72 for (const auto& point : box->points)
73 for (std::size_t axis = 0; axis < 3; ++axis)
74 {
75 bounds.minimum[axis] =
76 std::min(bounds.minimum[axis], point[axis]);
77 bounds.maximum[axis] =
78 std::max(bounds.maximum[axis], point[axis]);
79 }
80 result = bounds;
81 }
82 else if (const auto* sphere =
83 std::get_if<data::Sequence3DBoundingSphereAttribute>(&attribute))
84 {
85 const float radius = std::abs(sphere->radius);
86 result = SequenceBounds3D{
87 {-radius, -radius, -radius},
88 {radius, radius, radius}};
89 }
90 }
91 return result;
92 }
93
94 struct SequenceAudioState
95 {
96 std::uint16_t pitch{};
97 std::uint8_t volume{100};
98 std::int8_t panning{};
99 };
100
101 SequenceAudioState initialAudioState(
102 const data::LegacySequenceRecord& record,
103 const data::LegacySequenceAttributes& attributes) noexcept
104 {
105 SequenceAudioState result;
106 if (!std::holds_alternative<data::SequenceSoundData>(record.data) &&
107 !std::holds_alternative<data::SequenceVideoData>(record.data))
108 return result;
109
110 // Monopoly's C_ArtLib.h sets CE_ARTLIB_SeqncrDefaultSoundLevelPercent
111 // to 32. L_Seqncr.cpp applies that default to sound and video nodes
112 // before any SET_SOUND_VOLUME attribute overrides it.
113 result.volume = 32;
114
115 for (const auto& attribute : attributes.values)
116 {
117 if (const auto* pitch =
118 std::get_if<data::SequenceSoundPitchAttribute>(&attribute))
119 result.pitch = pitch->pitch;
120 else if (const auto* volume =
121 std::get_if<data::SequenceSoundVolumeAttribute>(&attribute))
122 result.volume = std::min<std::uint8_t>(volume->volume, 100U);
123 else if (const auto* pan =
124 std::get_if<data::SequenceSoundPanningAttribute>(&attribute))
125 result.panning = std::clamp<std::int8_t>(
126 pan->panning, -100, 100);
127 }
128 return result;
129 }
130
131 std::uint8_t initialSequenceLabel(
132 const data::LegacySequenceRecord& record,
133 const data::LegacySequenceAttributes& attributes) noexcept
134 {
135 std::uint8_t result{};
136 for (const auto& attribute : attributes.values)
137 if (const auto* label =
138 std::get_if<data::SequenceLabelAttribute>(&attribute))
139 result = label->labelNumber;
140
141 // LI_SEQNCR_StartUpSequence processes private chunks first, then
142 // overwrites that value with the camera's built-in label.
143 if (const auto* camera =
144 std::get_if<data::SequenceCameraData>(&record.data))
145 result = camera->cameraLabel;
146 return result;
147 }
148
149 std::string externalFileName(
150 const data::LegacySequenceAttributes& attributes)
151 {
152 std::string result;
153 for (const auto& attribute : attributes.values)
154 if (const auto* file =
155 std::get_if<data::SequenceFileNameAttribute>(&attribute))
156 result = file->fileName;
157 return result;
158 }
159
160 std::expected<data::SharedDataBytes, RuntimeError> preloadExternalFile(
161 const data::ResourceSnapshot& resources,
162 std::string_view fileName,
164 std::size_t recordOffset)
165 {
166 const auto path = resources.paths().resolve(fileName);
167 if (!path)
168 return std::unexpected(caused(
170 ownerId, recordOffset, path.error()));
171
172 std::ifstream input(*path, std::ios::binary | std::ios::ate);
173 if (!input)
174 return std::unexpected(error(
177 "cannot open preloader external file"));
178
179 const auto end = input.tellg();
180 if (end < 0)
181 return std::unexpected(error(
184 "cannot size preloader external file"));
185
186 auto bytes = std::make_shared<data::DataBytes>(
187 static_cast<std::size_t>(end));
188 input.seekg(0, std::ios::beg);
189 if (!bytes->empty() &&
190 !input.read(
191 reinterpret_cast<char*>(bytes->data()),
192 static_cast<std::streamsize>(bytes->size())))
193 return std::unexpected(error(
196 "cannot read preloader external file"));
197
198 return std::shared_ptr<const data::DataBytes>(
199 std::move(bytes));
200 }
201
202 std::optional<data::Sequence2DBoundingBoxAttribute> boundingBox2D(
203 const data::LegacySequenceAttributes& attributes)
204 {
205 for (const auto& attribute : attributes.values)
206 if (const auto* box =
207 std::get_if<data::Sequence2DBoundingBoxAttribute>(&attribute))
208 return *box;
209 return std::nullopt;
210 }
211
212 std::int32_t legacyRound2D(float value) noexcept
213 {
214 const auto rounded = std::nearbyint(static_cast<double>(value));
215 return static_cast<std::int32_t>(std::clamp(
216 rounded,
217 static_cast<double>(std::numeric_limits<std::int32_t>::min()),
218 static_cast<double>(std::numeric_limits<std::int32_t>::max())));
219 }
220
221 std::int32_t transformPointX(
222 const Matrix2D& matrix, std::int32_t x, std::int32_t y) noexcept
223 {
224 const auto& m = matrix.values;
225 const float newX = m[0] * static_cast<float>(x) +
226 m[3] * static_cast<float>(y) + m[6];
227 const float newW = m[2] * static_cast<float>(x) +
228 m[5] * static_cast<float>(y) + m[8];
229 return newW == 0.0F ? 0 : legacyRound2D(newX / newW);
230 }
231
232 std::int32_t soundCenterX2D(
233 const data::LegacySequenceAttributes& attributes,
234 const Matrix2D& worldTransform) noexcept
235 {
236 const auto box = boundingBox2D(attributes);
237 const std::int32_t left = box ? box->left : 0;
238 const std::int32_t right = box ? box->right : 0;
239 const std::int32_t top = box ? box->top : 0;
240 const std::int32_t bottom = box ? box->bottom : 0;
241 const std::int64_t sum =
242 static_cast<std::int64_t>(transformPointX(worldTransform, left, top)) +
243 transformPointX(worldTransform, right, top) +
244 transformPointX(worldTransform, left, bottom) +
245 transformPointX(worldTransform, right, bottom);
246 return static_cast<std::int32_t>(sum / 4);
247 }
248
249 float initialCameraFieldOfView(const data::LegacySequenceRecord& record,
250 const data::LegacySequenceAttributes& attributes,
251 std::uint8_t dimensionality) noexcept
252 {
253 if (!std::holds_alternative<data::SequenceCameraData>(record.data)) return 0.0F;
254 float result = dimensionality == 3 ? 0.7853981633974F : 1.0F;
255 for (const auto& attribute : attributes.values)
256 if (const auto* field =
257 std::get_if<data::SequenceCameraFieldOfViewAttribute>(&attribute))
258 result = field->fieldOfView;
259 return result;
260 }
261
262 std::expected<std::optional<float>, TweekerTransformError>
264 const data::LegacySequenceAttributes& attributes,
265 std::uint8_t interpolationType, std::int32_t clock,
266 std::int32_t endTime, bool parentIsCamera) noexcept
267 {
268 if (interpolationType > 2)
269 return std::unexpected(TweekerTransformError::InvalidInterpolation);
270 if (interpolationType == 0) return std::nullopt;
271 const auto keys = selectTweekerKeys(attributes);
272 const auto* first = std::get_if<data::SequenceCameraFieldOfViewAttribute>(keys.first);
273 const auto* second = std::get_if<data::SequenceCameraFieldOfViewAttribute>(keys.second);
274 if (!first) return std::nullopt;
275 if (!parentIsCamera)
276 return std::unexpected(TweekerTransformError::DimensionalityMismatch);
277 float result = first->fieldOfView;
278 if (interpolationType == 2 && second && endTime < 1'234'567'890)
279 {
280 const float proportion = static_cast<float>(clock) /
281 static_cast<float>(endTime);
282 result += proportion * (second->fieldOfView - result);
283 }
284 return result;
285 }
286
287 struct TweekerAudioUpdate
288 {
289 std::optional<std::uint16_t> pitch;
290 std::optional<std::uint8_t> volume;
291 std::optional<std::int8_t> panning;
292
293 [[nodiscard]] bool empty() const noexcept
294 { return !pitch && !volume && !panning; }
295 };
296
297 int interpolateInteger(float proportion, int first, int second) noexcept
298 {
299 return static_cast<int>(
300 (1.0F - proportion) * static_cast<float>(first) +
301 proportion * static_cast<float>(second));
302 }
303
304 std::expected<TweekerAudioUpdate, TweekerTransformError>
306 const data::LegacySequenceAttributes& attributes,
307 std::uint8_t interpolationType, std::int32_t clock,
308 std::int32_t endTime, bool parentIsAudio) noexcept
309 {
310 TweekerAudioUpdate result;
311 // ArtLib identity tweekers only clear transform tweeks; they do
312 // not touch sound/video properties.
313 if (interpolationType == 0)
314 return result;
315 const auto keys = selectTweekerKeys(attributes);
316 const auto* pitchA = std::get_if<data::SequenceSoundPitchAttribute>(keys.first);
317 const auto* pitchB = std::get_if<data::SequenceSoundPitchAttribute>(keys.second);
318 const auto* volumeA = std::get_if<data::SequenceSoundVolumeAttribute>(keys.first);
319 const auto* volumeB = std::get_if<data::SequenceSoundVolumeAttribute>(keys.second);
320 const auto* panA = std::get_if<data::SequenceSoundPanningAttribute>(keys.first);
321 const auto* panB = std::get_if<data::SequenceSoundPanningAttribute>(keys.second);
322
323 if (!pitchA && !volumeA && !panA)
324 return result;
325 if (!parentIsAudio)
326 return std::unexpected(
328
329 const bool linear = interpolationType == 2 &&
330 endTime < 1'234'567'890;
331 const float proportion = linear
332 ? static_cast<float>(clock) / static_cast<float>(endTime)
333 : 0.0F;
334
335 if (pitchA)
336 {
337 const int value = linear && pitchB
339 proportion, pitchA->pitch, pitchB->pitch)
340 : pitchA->pitch;
341 result.pitch = static_cast<std::uint16_t>(
342 std::clamp(value, 0, 65'535));
343 }
344 if (volumeA)
345 {
346 const int value = linear && volumeB
348 proportion, volumeA->volume, volumeB->volume)
349 : volumeA->volume;
350 result.volume = static_cast<std::uint8_t>(
351 std::clamp(value, 0, 100));
352 }
353 if (panA)
354 {
355 const int value = linear && panB
357 proportion, panA->panning, panB->panning)
358 : panA->panning;
359 result.panning = static_cast<std::int8_t>(
360 std::clamp(value, -100, 100));
361 }
362 return result;
363 }
364
365 std::expected<std::optional<SequenceMeshChoice3D>, TweekerTransformError>
366 evaluateTweekerMeshChoice(const data::LegacySequenceAttributes& attributes,
367 std::uint8_t interpolationType, std::int32_t clock,
368 std::int32_t endTime, std::uint8_t parentDimensionality) noexcept
369 {
370 if (interpolationType == 0) return std::nullopt;
371 const auto keys = selectTweekerKeys(attributes);
372 const auto* first = std::get_if<data::Sequence3DMeshChoiceAttribute>(keys.first);
373 const auto* second = std::get_if<data::Sequence3DMeshChoiceAttribute>(keys.second);
374 if (!first) return std::nullopt;
375 if (parentDimensionality != 3)
376 return std::unexpected(TweekerTransformError::DimensionalityMismatch);
377 SequenceMeshChoice3D result{
378 first->meshIndexA, first->meshIndexB, first->meshProportion};
379 if (interpolationType == 2 && second && endTime < 1'234'567'890)
380 {
381 const float proportion = static_cast<float>(clock) /
382 static_cast<float>(endTime);
383 // Retail applies the complete final key at p == 1. Between
384 // keys, only the proportion changes; pose indices stay at A.
385 if (proportion == 1.0F)
386 result = {second->meshIndexA, second->meshIndexB,
387 second->meshProportion};
388 else
389 result.meshProportion = first->meshProportion + proportion *
390 (second->meshProportion - first->meshProportion);
391 }
392 return result;
393 }
394 }
395
396 std::expected<std::shared_ptr<const SequenceProgram>, RuntimeError>
398 DescriptionLimits limits)
399 {
400 if (limits.maximumDepth == 0 || limits.maximumDepth > 128 ||
401 limits.maximumDescriptions == 0)
402 return std::unexpected(error(RuntimeErrorCode::InvalidLimits, id, 0,
403 "description depth must be 1..128 and node budget nonzero"));
404 if (sourceType != data::LegacyDataType::Bitmap &&
405 sourceType != data::LegacyDataType::Uap &&
406 sourceType != data::LegacyDataType::Native)
407 return std::unexpected(error(RuntimeErrorCode::UnsupportedType, id, 0,
408 "raw bitmap sequence supports only DataBMP, DataUAP and DataNative"));
409
410 auto program = std::shared_ptr<SequenceProgram>(new SequenceProgram);
412 header.timeMultiple = 60;
413 header.endingAction = 2;
415 data::ChunkInfo{3, 0, 0, 0, 0}, header,
417 auto children = SequenceChildSchedule::read({}, id, limits.maximumReferences);
418 if (!children)
419 return std::unexpected(std::visit([&](const auto& cause) {
420 return caused(RuntimeErrorCode::DecodeFailure, id, 0, cause);
421 }, children.error()));
422 program->descriptions_.push_back({id, std::move(record),
423 std::move(*children), {}, id, {}});
424 program->descriptions_.back().zeroBitmapOffset = true;
425 return std::shared_ptr<const SequenceProgram>(std::move(program));
426 }
427
428 std::expected<std::shared_ptr<const SequenceProgram>, RuntimeError>
431 bool binkDoubleSize)
432 {
433 if (id == data::EmptyDataId || fileName.empty() ||
434 fileName.find('\0') != std::string::npos ||
435 bounds.right <= bounds.left || bounds.bottom <= bounds.top)
436 return std::unexpected(error(RuntimeErrorCode::DataFailure, id, 0,
437 "runtime video requires an ID, filename and positive destination rectangle"));
438 auto program = std::shared_ptr<SequenceProgram>(new SequenceProgram);
441 header.endingAction = 1;
442 // Video scheduling follows elapsed media time, never one decoded frame
443 // per UI event. The backend chooses/drops frames and reports real EOF.
444 header.dropFrames = true;
446 data::ChunkInfo{6, 0, 0, 0, 0}, header, options, 0};
447 auto children = SequenceChildSchedule::read({}, id, 0);
448 if (!children)
449 return std::unexpected(error(RuntimeErrorCode::DecodeFailure, id, 0,
450 "cannot construct empty runtime video child schedule"));
452 attributes.values.push_back(data::SequenceDimensionalityAttribute{{}, 2});
453 attributes.values.push_back(bounds);
454 attributes.values.push_back(
455 data::SequenceFileNameAttribute{{}, 1, std::move(fileName)});
456 program->descriptions_.push_back({id, std::move(record),
457 std::move(*children), std::move(attributes), std::nullopt, {}, binkDoubleSize});
458 return std::shared_ptr<const SequenceProgram>(std::move(program));
459 }
460
461 std::expected<std::shared_ptr<const SequenceProgram>, RuntimeError>
463 std::size_t offset, DescriptionLimits limits)
464 {
465 if (limits.maximumDepth == 0 || limits.maximumDepth > 128 ||
466 limits.maximumDescriptions == 0)
467 return std::unexpected(error(RuntimeErrorCode::InvalidLimits, id, offset,
468 "description depth must be 1..128 and node budget nonzero"));
469
470 // L_Seqncr.cpp:3637-3663, 4260-4275. LE_SEQNCR_Start accepts a raw
471 // raw bitmap/mesh DataID by synthesizing the corresponding infinite
472 // sequence. DataUAP is handled below; MESHX/HMD uses a 3D mesh with
473 // the ArtLib basic cadence (60 Hz), StayAtEnd, and modelDataID=DataID.
474 // These are the paths used by UDBoard for raw overlays and CurrentBoard.
475 auto metadata = source.metadata(id);
476 if (!metadata)
477 return std::unexpected(caused(RuntimeErrorCode::DataFailure,
478 id, offset, metadata.error()));
481 {
482 // Retail DATA converts BMP to a native bitmap before StartUpSequence.
483 // Modern DATA keeps the disk type until the renderer decodes pixels.
484 // Both disk bitmap types therefore need the synthetic 2D sequence,
485 // not the CNK reader.
486 if (offset != 0)
487 return std::unexpected(error(RuntimeErrorCode::DecodeFailure,
488 id, offset, "raw bitmap sequence must start at offset zero"));
489 return rawBitmap(id, metadata->type, limits);
490 }
491
493 {
494 if (offset != 0)
495 return std::unexpected(error(RuntimeErrorCode::DecodeFailure,
496 id, offset, "raw HMD sequence must start at offset zero"));
497 auto program = std::shared_ptr<SequenceProgram>(new SequenceProgram);
499 header.timeMultiple = 60;
500 header.endingAction = 2; // LE_SEQNCR_EndingActionStayAtEnd.
502 data::ChunkInfo{9, 0, 0, 0, 0}, header,
504 auto children = SequenceChildSchedule::read({}, id,
505 limits.maximumReferences);
506 if (!children)
507 return std::unexpected(std::visit([&](const auto& cause) {
509 }, children.error()));
510 program->descriptions_.push_back({id, std::move(record),
511 std::move(*children), {}, id, {}});
512 return std::shared_ptr<const SequenceProgram>(std::move(program));
513 }
514
515 auto program = std::shared_ptr<SequenceProgram>(new SequenceProgram);
516 struct Entry { std::size_t index; bool active; std::size_t height; };
517 std::map<std::pair<data::DataId, std::size_t>, Entry> visited;
518 std::size_t references{};
519 std::function<std::expected<std::size_t, RuntimeError>(data::DataId, std::size_t, std::size_t)> visit;
520 visit = [&](data::DataId currentId, std::size_t currentOffset,
521 std::size_t depth) -> std::expected<std::size_t, RuntimeError>
522 {
523 if (depth > limits.maximumDepth)
524 return std::unexpected(error(RuntimeErrorCode::DepthLimit, currentId,
525 currentOffset, "description path exceeds configured depth"));
526 const auto key = std::pair{currentId, currentOffset};
527 if (const auto found = visited.find(key); found != visited.end())
528 {
529 if (found->second.active)
530 return std::unexpected(error(RuntimeErrorCode::IndirectCycle,
531 currentId, currentOffset, "recursive sequence description reference"));
532 if (found->second.height > limits.maximumDepth - depth + 1)
533 return std::unexpected(error(RuntimeErrorCode::DepthLimit, currentId,
534 currentOffset, "shared description exceeds depth on this path"));
535 return found->second.index;
536 }
537 if (program->descriptions_.size() >= limits.maximumDescriptions)
538 return std::unexpected(error(RuntimeErrorCode::DescriptionLimit,
539 currentId, currentOffset, "description count budget exceeded"));
540 const auto currentMetadata = source.metadata(currentId);
541 if (!currentMetadata) return std::unexpected(caused(RuntimeErrorCode::DataFailure,
542 currentId, currentOffset, currentMetadata.error()));
544 {
545 if (currentOffset != 0)
546 return std::unexpected(error(RuntimeErrorCode::DecodeFailure,
547 currentId, currentOffset, "raw Wave sequence must start at offset zero"));
549 header.timeMultiple = 15;
550 header.endingAction = 1;
551 auto children = SequenceChildSchedule::read({}, currentId);
552 if (!children) return std::unexpected(std::visit([&](const auto& cause) {
554 }, children.error()));
555 const auto index = program->descriptions_.size();
556 program->descriptions_.push_back({currentId,
557 {data::ChunkInfo{5, 0, 0, 0, 0}, header,
559 std::move(*children), {}, currentId, {}});
560 visited.emplace(key, Entry{index, false, 1});
561 return index;
562 }
564 if (!reader) return std::unexpected(caused(RuntimeErrorCode::DataFailure,
565 currentId, currentOffset, reader.error()));
566 const auto positioned = reader->seek(currentOffset);
567 if (!positioned) return std::unexpected(caused(RuntimeErrorCode::DecodeFailure,
568 currentId, currentOffset, positioned.error()));
569 auto record = data::readLegacySequenceRecord(*reader);
570 if (!record) return std::unexpected(caused(RuntimeErrorCode::DecodeFailure,
571 currentId, currentOffset, record.error()));
572 if (record->chunk.id != 1 && record->chunk.id != 2 &&
573 record->chunk.id != 3 && record->chunk.id != 4 &&
574 record->chunk.id != 5 && record->chunk.id != 6 &&
575 record->chunk.id != 7 && record->chunk.id != 8 &&
576 record->chunk.id != 9 && record->chunk.id != 10)
577 return std::unexpected(error(RuntimeErrorCode::UnsupportedType,
578 currentId, currentOffset,
579 "runtime currently executes grouping, indirect, 2D bitmap, 3D model, sound, video intent, camera, preloader, 3D mesh and tweeker records only"));
580 auto attributes = data::readLegacySequenceAttributes(*reader);
581 if (!attributes) return std::unexpected(caused(RuntimeErrorCode::DecodeFailure,
582 currentId, currentOffset, attributes.error()));
583 for (const auto& attribute : attributes->values)
584 if (const auto* unsupported =
585 std::get_if<data::SequenceUnsupportedAttribute>(&attribute))
586 return std::unexpected(error(RuntimeErrorCode::UnsupportedAttribute,
587 currentId, unsupported->chunk.headerOffset,
588 "sequence attribute is decoded but its effect is not executed"));
589 // A raw Wave referenced by INDIRECT has one synthetic child, just
590 // as StartUpSequence does for a directly started Wave DataID.
591 std::optional<data::DataId> rawSoundChild;
592 if (const auto* indirect = std::get_if<data::SequenceIndirectData>(&record->data))
593 {
594 const auto target = data::resolveSequenceDataId(record->header,
595 indirect->subsequenceDataId, currentId);
597 {
598 const auto targetMetadata = source.metadata(target);
601 }
602 }
603 auto schedule = [&]() -> std::expected<SequenceChildSchedule, ChildScheduleError> {
604 if (!rawSoundChild)
605 return openSequenceChildSchedule(source, currentId, currentOffset,
607 auto bytes = std::make_shared<data::DataBytes>();
608 for (const auto word : {0x05000014U, 0U, 15U << 24U, 1U, *rawSoundChild})
609 for (unsigned shift = 0; shift < 32; shift += 8)
610 bytes->push_back(static_cast<std::byte>((word >> shift) & 255U));
613 }();
614 if (!schedule)
615 return std::unexpected(std::visit([&](const auto& cause) {
616 return caused(RuntimeErrorCode::DecodeFailure, currentId, currentOffset, cause);
617 }, schedule.error()));
618 if (schedule->records().size() > limits.maximumReferences - references)
619 return std::unexpected(error(RuntimeErrorCode::ReferenceLimit,
620 currentId, currentOffset, "description reference budget exceeded"));
621 references += schedule->records().size();
622 std::optional<data::DataId> contentsDataId;
623 if (const auto* bitmap = std::get_if<data::SequenceBitmapData>(&record->data))
624 contentsDataId = data::resolveSequenceDataId(record->header,
625 bitmap->bitmapDataId, currentId);
626 else if (const auto* model = std::get_if<data::SequenceModelData>(&record->data))
627 contentsDataId = data::resolveSequenceDataId(record->header,
628 model->modelDataId, currentId);
629 else if (const auto* sound =
630 std::get_if<data::SequenceSoundData>(&record->data))
631 {
632 // L_Seqncr checks the raw ID before relative remapping:
633 // EmptyItem means stream from the last FILE_NAME_1..5.
634 if (sound->soundDataId != data::EmptyDataId)
635 contentsDataId = data::resolveSequenceDataId(
636 record->header, sound->soundDataId, currentId);
637 }
638 else if (const auto* preloader =
639 std::get_if<data::SequencePreloaderData>(&record->data))
640 {
641 if (preloader->preloadDataId != data::EmptyDataId)
642 contentsDataId = data::resolveSequenceDataId(
643 record->header, preloader->preloadDataId, currentId);
644 }
645 else if (const auto* mesh = std::get_if<data::SequenceMeshData>(&record->data))
646 contentsDataId = data::resolveSequenceDataId(record->header,
647 mesh->modelDataId, currentId);
648 const auto index = program->descriptions_.size();
649 program->descriptions_.push_back({currentId, *record, *schedule,
650 std::move(*attributes), contentsDataId, {}});
651 visited.emplace(key, Entry{index, true, 1});
652 std::size_t height = 1;
653 for (const auto& child : schedule->records())
654 {
655 auto target = visit(schedule->containingDataId(), child.chunk.headerOffset, depth + 1);
656 if (!target) return std::unexpected(target.error());
657 program->descriptions_[index].childDescriptions.push_back(*target);
658 const auto childHeight = visited.at(
659 {schedule->containingDataId(), child.chunk.headerOffset}).height;
660 height = std::max(height, childHeight + 1);
661 }
662 visited.at(key) = {index, false, height};
663 return index;
664 };
665 const auto root = visit(id, offset, 1);
666 if (!root) return std::unexpected(root.error());
667 return std::shared_ptr<const SequenceProgram>(std::move(program));
668 }
669
670 std::expected<std::shared_ptr<const SequenceProgram>, RuntimeError>
671 SequenceProgram::load(std::shared_ptr<const data::ResourceSnapshot> resources,
672 data::DataId id, std::size_t offset, DescriptionLimits limits)
673 {
674 if (!resources) return std::unexpected(error(RuntimeErrorCode::DataFailure,
675 id, offset, "resource snapshot is null"));
676 auto result = load(resources->data(), id, offset, limits);
677 if (!result) return result;
678 // Created privately above; no mutable alias is exposed to callers.
679 auto program = std::const_pointer_cast<SequenceProgram>(*result);
680 const auto metadata = resources->data().metadata(id);
681 if (!metadata)
682 return std::unexpected(caused(RuntimeErrorCode::DataFailure,
683 id, offset, metadata.error()));
684
685 const auto preload = [&](data::DataId contents, std::size_t recordOffset)
686 -> std::expected<void, RuntimeError>
687 {
688 // Absolute EmptyItem is legal; relative tag zero was already
689 // resolved with the containing group while building descriptions.
690 if (contents == data::EmptyDataId) return {};
691 auto bytes = resources->data().load(contents);
692 if (!bytes)
693 {
695 contents, recordOffset, bytes.error());
696 failure.detail = "sequence preload failed: " + bytes.error().detail;
697 return std::unexpected(std::move(failure));
698 }
699 // Main.cpp requests Load without AddRef. Release this local lease
700 // now; the archive cache retains the item subject to its LRU budget.
701 return {};
702 };
703
704 // Main.cpp:342 enables PreloadData for Start. PrepareSequenceData in
705 // L_Seqncr.cpp:9257-9353 loads raw data, or only direct bitmap/sound
706 // children of a top-level grouping. It deliberately does not preload
707 // video files or recursively traverse indirect/grouping dependencies.
708 // Do not pin all prepared assets: UseReferenceCounts is false, so cache
709 // pressure may evict one before the first render/audio update, as in Source.
711 {
712 if (auto loaded = preload(id, offset); !loaded)
713 return std::unexpected(loaded.error());
714 }
715 else if (program->descriptions_.front().record.chunk.id == 1)
716 {
717 for (const auto childIndex : program->descriptions_.front().childDescriptions)
718 {
719 const auto& child = program->descriptions_[childIndex];
720 if ((std::holds_alternative<data::SequenceBitmapData>(child.record.data) ||
721 std::holds_alternative<data::SequenceSoundData>(child.record.data)) &&
722 child.contentsDataId)
723 {
724 if (auto loaded = preload(*child.contentsDataId,
725 child.record.chunk.headerOffset); !loaded)
726 return std::unexpected(loaded.error());
727 }
728 }
729 if (auto loaded = preload(id, offset); !loaded)
730 return std::unexpected(loaded.error());
731 }
732 program->resources_ = std::move(resources);
733 return result;
734 }
735 std::span<const SequenceDescription> SequenceProgram::descriptions() const noexcept
736 { return descriptions_; }
737 std::shared_ptr<const data::ResourceSnapshot> SequenceProgram::resources() const noexcept
738 { return resources_; }
739
741 {
744 std::shared_ptr<const SequenceProgram> program;
745 std::size_t description;
746 std::uint16_t priority;
749 Nodes children;
750 bool reevaluate{true};
751 bool redrawRequested{true};
753 std::uint8_t dimensionality{};
761 std::uint8_t labelNumber{};
762 std::uint16_t pitch{};
763 std::uint8_t volume{100};
764 std::int8_t panning{};
765 // User audio commands change the playback buffer, not aux.audio.
766 // Tweekers and positional mixing can subsequently replace that output.
767 std::optional<std::uint16_t> commandedPitch;
768 std::optional<std::uint8_t> commandedVolume;
769 std::optional<std::int8_t> commandedPanning;
771 std::uint64_t seekGeneration{};
775 // RuntimeTweekerInfo::currentProportion belongs to this instance.
776 // Seek and forced reevaluation do not reset an already applied key.
777 std::optional<float> tweekerProportion;
778 bool positionRecalc{true};
779 std::uint64_t movementRevision{};
781 { return program->descriptions()[description]; }
782 };
783
786 std::size_t SequenceRuntime::liveNodeCount() const noexcept { return liveNodes_; }
787 std::span<const SequenceEvent> SequenceRuntime::events() const noexcept { return events_; }
788 void SequenceRuntime::emit(SequenceEventKind kind, const Node& node)
789 {
790 const auto& def = node.definition();
791 events_.push_back({kind, node.id, node.parent ? node.parent->id : 0,
792 def.dataId, def.record.chunk.headerOffset, node.priority,
793 node.labelNumber, def.record.chunk.id, node.soundFailed ? std::uint8_t{0} : node.clock.endingAction(),
794 node.clock.clock()});
795 }
796 void SequenceRuntime::insert(Nodes& siblings, std::unique_ptr<Node> node)
797 {
798 // InsertRuntimeChild: before first GREATER OR EQUAL priority.
799 const auto position = std::lower_bound(siblings.begin(), siblings.end(), node->priority,
800 [](const auto& child, std::uint16_t priority) { return child->priority < priority; });
801 siblings.insert(position, std::move(node));
802 }
803 std::expected<std::unique_ptr<SequenceRuntime::Node>, RuntimeError>
804 SequenceRuntime::create(std::shared_ptr<const SequenceProgram> program,
805 std::size_t description, Node* parent, std::uint16_t priority,
806 ClockStartOptions options, std::uint8_t labelOverride)
807 {
808 const auto& def = program->descriptions()[description];
809 const auto id = def.dataId;
810 const auto offset = def.record.chunk.headerOffset;
811 if (liveNodes_ >= limits_.maximumLiveNodes)
812 return std::unexpected(error(RuntimeErrorCode::LiveNodeLimit, id, offset, "live node budget exceeded"));
813 if (births_ >= limits_.maximumBirthsPerOperation)
814 return std::unexpected(error(RuntimeErrorCode::BirthLimit, id, offset, "birth budget exceeded"));
815 if (nextId_ == std::numeric_limits<SequenceNodeId>::max())
816 return std::unexpected(error(RuntimeErrorCode::IdentifierExhausted, id, offset, "node identifiers exhausted"));
817 if (parent) options.parentClockAtBirth = parent->clock.clock();
818 else options.parentClockAtBirth.reset();
819
820 if (std::holds_alternative<data::SequenceSoundData>(
821 def.record.data) &&
822 !options.endTimeOverride)
823 {
824 if (const auto resources = program->resources())
825 {
827 if (def.contentsDataId)
828 {
829 auto loaded =
830 resources->data().load(*def.contentsDataId);
831 if (!loaded)
832 return std::unexpected(caused(
834 *def.contentsDataId,
835 offset,
836 loaded.error()));
837 waveBytes = *loaded;
838 }
839 else
840 {
841 const auto fileName =
842 externalFileName(def.attributes);
843 if (!fileName.empty())
844 {
846 *resources, fileName, id, offset);
847 if (!loaded)
848 return std::unexpected(loaded.error());
849 waveBytes = *loaded;
850 }
851 }
852
853 if (waveBytes && !waveBytes->empty())
854 {
855 const std::span<const std::uint8_t> riff(
856 reinterpret_cast<const std::uint8_t*>(
857 waveBytes->data()),
858 waveBytes->size());
859 const auto duration =
861 if (duration != 0 &&
862 duration <= static_cast<std::uint32_t>(
863 std::numeric_limits<std::int32_t>::max()))
864 options.endTimeOverride =
865 static_cast<std::int32_t>(duration);
866 }
867 }
868 }
869
870 auto clock = SequenceClock::start(def.record, options);
871 if (!clock) return std::unexpected(caused(RuntimeErrorCode::ClockFailure, id, offset, clock.error()));
872 const auto initial = initialSequenceTransform(def.record, def.attributes,
873 parent ? parent->dimensionality : 0);
875 initial.dimensionality == 3 ? SequenceTransform(identity3D()) :
877 const auto world = composeSequenceWorld(initial.local, initial.dimensionality,
878 parent ? parent->worldTransform : SequenceTransform(std::monostate{}),
879 parent ? parent->dimensionality : 0);
880
881 data::SharedDataBytes preloadedData;
882 if (std::holds_alternative<data::SequencePreloaderData>(def.record.data))
883 {
884 if (const auto resources = program->resources())
885 {
886 if (def.contentsDataId)
887 {
888 auto loaded = resources->data().load(*def.contentsDataId);
889 if (!loaded)
890 return std::unexpected(caused(
892 *def.contentsDataId, offset, loaded.error()));
893 preloadedData = *loaded;
894 }
895 else
896 {
897 const auto fileName = externalFileName(def.attributes);
898 if (!fileName.empty())
899 {
901 *resources, fileName, id, offset);
902 if (!loaded)
903 return std::unexpected(loaded.error());
904 preloadedData = *loaded;
905 }
906 }
907 }
908 }
909
910 auto node = std::make_unique<Node>(Node{nextId_++, parent, std::move(program),
911 description, priority, *clock, def.children, {}, true, true, false,
912 initial.dimensionality, initial.explicitlyPositioned,
913 initial.local, false, tweeker, world});
914 node->meshChoice = initialMeshChoice(def.attributes, initial.dimensionality);
915 node->cameraFieldOfView = initialCameraFieldOfView(
916 def.record, def.attributes, initial.dimensionality);
917 node->labelNumber = initialSequenceLabel(def.record, def.attributes);
918 const auto audio = initialAudioState(def.record, def.attributes);
919 node->pitch = audio.pitch;
920 node->volume = audio.volume;
921 node->panning = audio.panning;
922 node->preloadedData = std::move(preloadedData);
923 if (labelOverride != 0)
924 node->labelNumber = labelOverride;
925 if (node->labelNumber != 0)
926 labelOwners_[node->labelNumber] = node->id;
927 ++liveNodes_; ++births_;
928 emit(SequenceEventKind::Created, *node);
929 return node;
930 }
931 std::expected<SequenceNodeId, RuntimeError> SequenceRuntime::start(
932 std::shared_ptr<const SequenceProgram> program, std::uint16_t priority,
933 ClockStartOptions options, std::optional<SequenceTransform> initialTransform,
934 std::uint8_t labelOverride)
935 {
936 events_.clear(); births_ = 0;
937 if (!program || program->descriptions().empty())
938 return std::unexpected(error(RuntimeErrorCode::DataFailure, 0, 0, "no sequence program"));
939 auto node = create(
940 std::move(program), 0, nullptr, priority, options, labelOverride);
941 if (!node) return std::unexpected(node.error());
942 if (initialTransform) move(**node, *initialTransform);
943 const auto id = (*node)->id;
944 insert(roots_, std::move(*node));
945 return id;
946 }
947 void SequenceRuntime::destroyChildren(Node& node)
948 {
949 for (auto& child : node.children) destroy(child);
950 node.children.clear();
951 }
952 void SequenceRuntime::destroy(std::unique_ptr<Node>& node)
953 {
954 destroyChildren(*node);
955 if (node->labelNumber != 0 &&
956 labelOwners_[node->labelNumber] == node->id)
957 labelOwners_[node->labelNumber] = 0;
958 emit(SequenceEventKind::Destroyed, *node);
959 --liveNodes_;
960 node.reset();
961 }
962 void SequenceRuntime::clearForest()
963 {
964 for (auto& node : roots_) destroy(node);
965 roots_.clear();
966 }
967 void SequenceRuntime::stopAll() { events_.clear(); clearForest(); }
968 void SequenceRuntime::erase(Node& node)
969 {
970 auto& siblings = node.parent ? node.parent->children : roots_;
971 const auto position = std::find_if(siblings.begin(), siblings.end(),
972 [&](const auto& current) { return current.get() == &node; });
973 destroy(*position);
974 siblings.erase(position);
975 }
976 SequenceRuntime::Node* SequenceRuntime::find(SequenceNodeId id) const
977 {
978 const auto visit = [&](const auto& self, const Nodes& nodes) -> Node* {
979 for (const auto& node : nodes)
980 {
981 if (node->id == id) return node.get();
982 if (auto* child = self(self, node->children)) return child;
983 }
984 return nullptr;
985 };
986 return visit(visit, roots_);
987 }
988 void SequenceRuntime::forceAncestors(Node& node)
989 { for (auto* current = &node; current; current = current->parent) current->reevaluate = true; }
990 void SequenceRuntime::forceDescendants(Node& node)
991 {
992 node.reevaluate = true;
993 node.redrawRequested = true;
994 for (auto& child : node.children) forceDescendants(*child);
995 }
996 void SequenceRuntime::clearRedrawFlags()
997 {
998 const auto visit = [&](const auto& self, Nodes& nodes) -> void {
999 for (auto& node : nodes)
1000 {
1001 node->needsRedraw = false;
1002 self(self, node->children);
1003 }
1004 };
1005 visit(visit, roots_);
1006 }
1007 void SequenceRuntime::move(Node& node, const SequenceTransform& transform)
1008 {
1009 bool changed = true;
1010 if (node.dimensionality == 2)
1011 {
1012 if (const auto* matrix = std::get_if<Matrix2D>(&transform))
1013 {
1014 const auto& current = std::get<Matrix2D>(node.localTransform);
1015 changed = std::memcmp(current.values.data(), matrix->values.data(),
1016 sizeof(current.values)) != 0;
1017 if (changed)
1018 {
1019 node.localTransform = *matrix;
1020 node.explicitlyPositioned = true;
1021 }
1022 }
1023 else
1024 {
1025 node.localTransform = identity2D();
1026 node.explicitlyPositioned = false;
1027 }
1028 }
1029 else if (node.dimensionality == 3)
1030 {
1031 if (const auto* matrix = std::get_if<Matrix3D>(&transform))
1032 {
1033 const auto& current = std::get<Matrix3D>(node.localTransform);
1034 changed = std::memcmp(current.values.data(), matrix->values.data(),
1035 sizeof(current.values)) != 0;
1036 if (changed)
1037 {
1038 node.localTransform = *matrix;
1039 node.explicitlyPositioned = true;
1040 }
1041 }
1042 else
1043 {
1044 node.localTransform = identity3D();
1045 node.explicitlyPositioned = false;
1046 }
1047 }
1048 if (!changed) return;
1049
1050 // MarkAsNeedingPositionRecalc marks this sequence and its ancestors;
1051 // descendants are then reevaluated by the recursive update. Preserve
1052 // that observable effect even when a descendant cadence would gate it.
1053 node.positionRecalc = true;
1054 forceAncestors(node);
1055 forceDescendants(node);
1056 }
1057 std::expected<void, RuntimeError> SequenceRuntime::stop(SequenceNodeId id)
1058 {
1059 events_.clear();
1060 auto* node = find(id);
1061 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0, "node no longer exists"));
1062 erase(*node);
1063 return {};
1064 }
1065 std::expected<void, RuntimeError> SequenceRuntime::requestSoundFailure(SequenceNodeId id)
1066 {
1067 auto* node = find(id);
1068 if (!node || node->definition().record.chunk.id != 5)
1069 return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0,
1070 "failed sound references an inactive sound sequence"));
1071 // L_Rend0D marks failed audio feeding as Suicide. Defer destruction to
1072 // the regular update so event publication remains a single operation.
1073 node->soundFailed = true;
1074 forceAncestors(*node);
1075 return {};
1076 }
1077 std::expected<void, RuntimeError> SequenceRuntime::requestSoundClock(
1078 SequenceNodeId id, std::int32_t mediaClock)
1079 {
1080 auto* node = find(id);
1081 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0,
1082 "sound clock references an inactive sequence"));
1083 const auto supplied = node->clock.supplySoundClock(mediaClock);
1084 if (!supplied) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1085 node->definition().dataId, node->definition().record.chunk.headerOffset, supplied.error()));
1086 // Preserve the sound's cadence. Ancestors may otherwise hold it asleep.
1087 for (auto* parent = node->parent; parent; parent = parent->parent)
1088 parent->reevaluate = true;
1089 return {};
1090 }
1091 std::expected<void, RuntimeError> SequenceRuntime::requestVideoClock(
1092 SequenceNodeId id, std::int32_t mediaClock, std::int32_t duration, bool ended)
1093 {
1094 auto* node = find(id);
1095 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0,
1096 "video clock references an inactive sequence"));
1097 const auto supplied = node->clock.supplyVideoClock(mediaClock, duration, ended);
1098 if (!supplied) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1099 node->definition().dataId, node->definition().record.chunk.headerOffset, supplied.error()));
1100 // Do not emit/clear events here. The unique update publishes lifecycle
1101 // after queued commands, preserving ReachedEnd/Destroyed notifications.
1102 forceAncestors(*node);
1103 return {};
1104 }
1105 std::expected<void, RuntimeError> SequenceRuntime::setPaused(SequenceNodeId id, bool paused)
1106 {
1107 events_.clear();
1108 auto* node = find(id);
1109 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0, "node no longer exists"));
1110 auto result = node->clock.setPaused(paused, node->parent ? node->parent->clock.clock() : parentClock_);
1111 if (!result) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1112 node->definition().dataId, node->definition().record.chunk.headerOffset, result.error()));
1113 return {};
1114 }
1115 std::expected<void, RuntimeError> SequenceRuntime::setEndingAction(SequenceNodeId id, std::uint8_t action)
1116 {
1117 events_.clear();
1118 auto* node = find(id);
1119 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0, "node no longer exists"));
1120 const auto result = node->clock.setEndingAction(action);
1121 if (!result) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1122 node->definition().dataId, node->definition().record.chunk.headerOffset, result.error()));
1123 forceAncestors(*node);
1124 return {};
1125 }
1126
1127 std::expected<void, RuntimeError> SequenceRuntime::setVolume(
1128 SequenceNodeId id, std::uint8_t volume)
1129 {
1130 events_.clear();
1131 auto* node = find(id);
1132 if (!node)
1133 return std::unexpected(error(
1135 "node no longer exists"));
1136
1137 const auto& data = node->definition().record.data;
1138 if (std::holds_alternative<data::SequenceSoundData>(data) ||
1139 std::holds_alternative<data::SequenceVideoData>(data))
1140 node->commandedVolume = std::min<std::uint8_t>(volume, 100U);
1141 return {};
1142 }
1143
1144 std::expected<void, RuntimeError> SequenceRuntime::setPitch(
1145 SequenceNodeId id, std::uint16_t pitch)
1146 {
1147 events_.clear();
1148 auto* node = find(id);
1149 if (!node)
1150 return std::unexpected(error(
1152 "node no longer exists"));
1153 const auto& data = node->definition().record.data;
1154 if (std::holds_alternative<data::SequenceSoundData>(data) ||
1155 std::holds_alternative<data::SequenceVideoData>(data))
1156 node->commandedPitch = pitch;
1157 return {};
1158 }
1159
1160 std::expected<void, RuntimeError> SequenceRuntime::setPanning(
1161 SequenceNodeId id, std::int8_t panning)
1162 {
1163 events_.clear();
1164 auto* node = find(id);
1165 if (!node)
1166 return std::unexpected(error(
1168 "node no longer exists"));
1169 const auto& data = node->definition().record.data;
1170 if (std::holds_alternative<data::SequenceSoundData>(data) ||
1171 std::holds_alternative<data::SequenceVideoData>(data))
1172 node->commandedPanning = static_cast<std::int8_t>(
1173 std::clamp<int>(panning, -100, 100));
1174 return {};
1175 }
1176
1177 std::expected<void, RuntimeError>
1179 SequenceNodeId id, bool onScreen)
1180 {
1181 auto* node = find(id);
1182 if (!node)
1183 return std::unexpected(error(
1185 "scrolling-world visibility references an inactive sequence"));
1186 if (!node->definition().record.header.scrollingWorld)
1187 return {};
1188 if (node->scrollingOnScreen == onScreen)
1189 return {};
1190
1191 node->scrollingOnScreen = onScreen;
1192 node->scrollingHibernating = false;
1193 node->reevaluate = true;
1194 forceAncestors(*node);
1195 return {};
1196 }
1197
1198 std::expected<void, RuntimeError> SequenceRuntime::birthChildren(Node& node,
1199 std::optional<std::int32_t> previous)
1200 {
1201 const auto selected = node.schedule.select(previous, node.clock.clock());
1202 if (!selected) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1203 node.definition().dataId, node.definition().record.chunk.headerOffset, selected.error()));
1204 for (const auto index : *selected)
1205 {
1206 const auto description = node.definition().childDescriptions[index];
1207 const auto priority = node.program->descriptions()[description].record.header.priority;
1208 auto child = create(node.program, description, &node, priority, {});
1209 if (!child) return std::unexpected(child.error());
1210 insert(node.children, std::move(*child));
1211 }
1212 return {};
1213 }
1214 std::expected<void, RuntimeError> SequenceRuntime::rebuildChildren(Node& node)
1215 {
1216 destroyChildren(node);
1217 node.schedule.rewind();
1218 auto born = birthChildren(node, std::nullopt);
1219 if (!born) return born;
1220 for (auto iterator = node.children.begin(); iterator != node.children.end();)
1221 {
1222 auto& child = **iterator;
1223 const auto time = static_cast<std::int64_t>(node.clock.clock()) -
1224 child.definition().record.header.parentStartTime;
1225 if (time < std::numeric_limits<std::int32_t>::min() || time > std::numeric_limits<std::int32_t>::max())
1226 return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1227 child.definition().dataId, child.definition().record.chunk.headerOffset, ClockError::ClockOverflow));
1228 const auto alive = seekNode(child, static_cast<std::int32_t>(time));
1229 if (!alive) return std::unexpected(alive.error());
1230 if (!*alive) { destroy(*iterator); iterator = node.children.erase(iterator); }
1231 else ++iterator;
1232 }
1233 return {};
1234 }
1235 std::expected<bool, RuntimeError> SequenceRuntime::seekNode(Node& node, std::int32_t time)
1236 {
1237 const auto result = node.clock.seek(time, node.parent ? node.parent->clock.clock() : parentClock_);
1238 if (result.stopped) return false;
1239 node.reevaluate = true;
1240 ++node.seekGeneration;
1241 emit(SequenceEventKind::Rewound, node);
1242 const auto rebuilt = rebuildChildren(node);
1243 if (!rebuilt) return std::unexpected(rebuilt.error());
1244 return true;
1245 }
1246 std::expected<void, RuntimeError> SequenceRuntime::seek(SequenceNodeId id, std::int32_t time)
1247 {
1248 events_.clear(); births_ = 0;
1249 auto* node = find(id);
1250 if (!node) return std::unexpected(error(RuntimeErrorCode::InvalidHandle, 0, 0, "node no longer exists"));
1251 forceAncestors(*node);
1252 const auto alive = seekNode(*node, time);
1253 if (!alive) { clearForest(); return std::unexpected(alive.error()); }
1254 if (!*alive) erase(*node);
1255 return {};
1256 }
1257 void SequenceRuntime::updatePosition(Node& node)
1258 {
1259 const auto effectiveLocal = applyTweekerBeforeLocal(node.tweekerTransform,
1260 node.tweekerTransformApplied, node.localTransform, node.dimensionality);
1261 node.worldTransform = composeSequenceWorld(effectiveLocal,
1262 node.dimensionality,
1263 node.parent ? node.parent->worldTransform : SequenceTransform(std::monostate{}),
1264 node.parent ? node.parent->dimensionality : 0);
1265 if (node.positionRecalc)
1266 {
1267 ++node.movementRevision;
1268 node.needsRedraw = true;
1269 if (node.dimensionality == 2 &&
1270 std::holds_alternative<data::SequenceSoundData>(node.definition().record.data))
1271 {
1272 // L_Rend2D::SequenceMoved writes both buffer controls from
1273 // aux.audio.volume and the current screen position. A prior
1274 // SetVolume/SetPan override lasts only until this movement.
1275 node.commandedVolume.reset();
1276 node.commandedPanning.reset();
1277 }
1278 }
1279 }
1280
1281 std::expected<bool, RuntimeError> SequenceRuntime::updateNode(Node& node, std::int32_t parentClock)
1282 {
1283 if (node.soundFailed)
1284 {
1286 return false;
1287 }
1288 node.needsRedraw = node.redrawRequested;
1289 node.redrawRequested = false;
1290
1291 if (node.definition().record.header.scrollingWorld &&
1292 !node.scrollingOnScreen)
1293 {
1294 if (!node.scrollingHibernating)
1295 {
1296 destroyChildren(node);
1297 node.schedule.rewind();
1298 node.scrollingHibernating = true;
1299 }
1300 node.clock.hibernateScrollingWorld(parentClock);
1301 node.needsRedraw = false;
1302 // Hibernation freezes animation, not placement. Retail still
1303 // executes its positional update for an off-screen world. Without
1304 // this, moving it (or its parent) back on-screen leaves visibility
1305 // permanently evaluating the previous off-screen world matrix.
1306 if (node.positionRecalc)
1307 updatePosition(node);
1308 node.positionRecalc = false;
1309 node.reevaluate = false;
1310 return true;
1311 }
1312
1313 const auto tick = node.clock.update(parentClock, node.reevaluate);
1314 if (!tick) return std::unexpected(caused(RuntimeErrorCode::ClockFailure,
1315 node.definition().dataId, node.definition().record.chunk.headerOffset, tick.error()));
1316 if (!tick->updated) return true; // gated parent gates descendants too
1317 emit(SequenceEventKind::Updated, node);
1318 if (tick->notifyEnd) emit(SequenceEventKind::ReachedEnd, node);
1319 if (tick->stopped) return false; // children do not get a final tick
1320 const auto children = tick->restartChildren ? rebuildChildren(node) : birthChildren(node, tick->previousClock);
1321 if (!children) return std::unexpected(children.error());
1322 if (node.definition().record.chunk.id == 10)
1323 {
1324 const auto applied = applyTweeker(node);
1325 if (!applied) return std::unexpected(applied.error());
1326 }
1327
1328 // Tweeker children run before their parent's position calculation.
1329 for (auto iterator = node.children.begin(); iterator != node.children.end();)
1330 {
1331 if ((*iterator)->definition().record.chunk.id != 10) { ++iterator; continue; }
1332 const auto alive = updateNode(**iterator, node.clock.clock());
1333 if (!alive) return std::unexpected(alive.error());
1334 if (!*alive) { destroy(*iterator); iterator = node.children.erase(iterator); }
1335 else ++iterator;
1336 }
1337 updatePosition(node);
1338 for (auto iterator = node.children.begin(); iterator != node.children.end();)
1339 {
1340 if ((*iterator)->definition().record.chunk.id == 10) { ++iterator; continue; }
1341 if (node.positionRecalc)
1342 {
1343 (*iterator)->positionRecalc = true;
1344 (*iterator)->reevaluate = true;
1345 }
1346 const auto alive = updateNode(**iterator, node.clock.clock());
1347 if (!alive) return std::unexpected(alive.error());
1348 if (!*alive) { destroy(*iterator); iterator = node.children.erase(iterator); }
1349 else
1350 {
1351 if ((*iterator)->needsRedraw) node.needsRedraw = true;
1352 ++iterator;
1353 }
1354 }
1355 node.positionRecalc = false;
1356 node.reevaluate = false;
1357 return true;
1358 }
1359 std::expected<void, RuntimeError> SequenceRuntime::applyTweeker(Node& node)
1360 {
1361 if (!node.parent) return std::unexpected(error(RuntimeErrorCode::TweekerFailure,
1362 node.definition().dataId, node.definition().record.chunk.headerOffset,
1363 "a tweeker must be a child of the sequence it modifies"));
1364 const auto& tweeker = std::get<data::SequenceTweekerData>(node.definition().record.data);
1365 std::optional<float> proportion;
1366 if (tweeker.interpolationType != 0)
1367 {
1368 const auto keys = selectTweekerKeys(node.definition().attributes);
1369 proportion = tweeker.interpolationType == 2 && keys.second &&
1370 node.clock.endTime() < 1'234'567'890
1371 ? static_cast<float>(node.clock.clock()) / static_cast<float>(node.clock.endTime())
1372 : 0.0F;
1373 // DoTweeking returns before changing its parent when the same key
1374 // has already run. A later sibling's effects survive its deletion.
1375 if (node.tweekerProportion == proportion) return {};
1376 }
1377 const auto evaluated = evaluateTweekerTransform(node.definition().attributes,
1378 tweeker.interpolationType, node.clock.clock(), node.clock.endTime(),
1379 node.parent->dimensionality);
1380 if (!evaluated)
1381 return std::unexpected(error(RuntimeErrorCode::TweekerFailure,
1382 node.definition().dataId, node.definition().record.chunk.headerOffset,
1384 "tweeker interpolation type is not implemented" :
1385 "tweeker transform dimensionality does not match its parent"));
1386
1387 const auto meshChoice = evaluateTweekerMeshChoice(node.definition().attributes,
1388 tweeker.interpolationType, node.clock.clock(), node.clock.endTime(),
1389 node.parent->dimensionality);
1390 if (!meshChoice)
1391 return std::unexpected(error(RuntimeErrorCode::TweekerFailure,
1392 node.definition().dataId, node.definition().record.chunk.headerOffset,
1393 "3D mesh-choice tweeker dimensionality does not match its parent"));
1394 if (evaluated->changed)
1395 {
1396 node.parent->tweekerTransformApplied = !evaluated->identity;
1397 node.parent->tweekerTransform = evaluated->transform;
1398 node.parent->positionRecalc = true;
1399 }
1400 if (*meshChoice)
1401 {
1402 node.parent->meshChoice = **meshChoice;
1403 // Retail mesh interpolation is applied by SequenceMoved and
1404 // propagates positional reevaluation to descendants as well.
1405 node.parent->positionRecalc = true;
1406 }
1408 node.definition().attributes, tweeker.interpolationType,
1409 node.clock.clock(), node.clock.endTime(),
1410 node.parent->definition().record.chunk.id == 7);
1411 if (!cameraFov)
1412 return std::unexpected(error(RuntimeErrorCode::TweekerFailure,
1413 node.definition().dataId, node.definition().record.chunk.headerOffset,
1414 "camera field-of-view tweeker must target a camera sequence"));
1415 if (*cameraFov) node.parent->cameraFieldOfView = **cameraFov;
1416
1417 const auto& parentData = node.parent->definition().record.data;
1418 const bool parentIsAudio =
1419 std::holds_alternative<data::SequenceSoundData>(parentData) ||
1420 std::holds_alternative<data::SequenceVideoData>(parentData);
1421 const auto audio = evaluateTweekerAudio(
1422 node.definition().attributes, tweeker.interpolationType,
1423 node.clock.clock(), node.clock.endTime(), parentIsAudio);
1424 if (!audio)
1425 return std::unexpected(error(RuntimeErrorCode::TweekerFailure,
1426 node.definition().dataId,
1427 node.definition().record.chunk.headerOffset,
1428 "sound tweeker must target a sound or video sequence"));
1429 if (audio->pitch)
1430 {
1431 node.parent->pitch = *audio->pitch;
1432 node.parent->commandedPitch.reset();
1433 }
1434 const bool directAudio = node.parent->dimensionality == 0 ||
1435 std::holds_alternative<data::SequenceVideoData>(parentData);
1436 if (audio->volume)
1437 {
1438 node.parent->volume = *audio->volume;
1439 if (directAudio)
1440 node.parent->commandedVolume.reset();
1441 else
1442 node.parent->positionRecalc = true;
1443 }
1444 if (audio->panning)
1445 {
1446 node.parent->panning = *audio->panning;
1447 // A positional sound's pan tweeker changes aux.audio only; it
1448 // does not touch the buffer or replace a manual pan command.
1449 if (directAudio)
1450 node.parent->commandedPanning.reset();
1451 }
1452 if (proportion) node.tweekerProportion = proportion;
1453 return {};
1454 }
1455 std::expected<void, RuntimeError> SequenceRuntime::update(std::int32_t parentClock)
1456 {
1457 events_.clear(); births_ = 0;
1458 if (clockStarted_ && parentClock < parentClock_)
1460 clearRedrawFlags();
1461 parentClock_ = parentClock; clockStarted_ = true;
1462 for (auto iterator = roots_.begin(); iterator != roots_.end();)
1463 {
1464 const auto alive = updateNode(**iterator, parentClock);
1465 if (!alive) { clearForest(); return std::unexpected(alive.error()); }
1466 if (!*alive) { destroy(*iterator); iterator = roots_.erase(iterator); }
1467 else ++iterator;
1468 }
1469 return {};
1470 }
1471 std::vector<SequenceNodeId> SequenceRuntime::matching(data::DataId id,
1472 std::uint16_t priority, bool wholeTree) const
1473 {
1474 std::vector<SequenceNodeId> matches;
1475 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1476 for (const auto& node : nodes)
1477 {
1478 if (node->definition().dataId == id && node->priority == priority &&
1479 node->definition().record.chunk.headerOffset == 0) matches.push_back(node->id);
1480 if (wholeTree) self(self, node->children);
1481 }
1482 };
1483 visit(visit, roots_);
1484 return matches;
1485 }
1487 std::uint16_t priority, bool wholeTree)
1488 {
1489 events_.clear();
1490 const auto matches = matching(id, priority, wholeTree);
1491 std::size_t stopped{};
1492 for (const auto match : matches)
1493 {
1494 // A matching ancestor deletes matching descendants with it.
1495 // FindNextSequence likewise resumes from the surviving tree.
1496 if (auto* node = find(match))
1497 {
1498 erase(*node);
1499 ++stopped;
1500 }
1501 }
1502 return stopped;
1503 }
1504 std::expected<std::size_t, RuntimeError> SequenceRuntime::setEndingActionMatching(
1505 data::DataId id, std::uint16_t priority, std::uint8_t action, bool wholeTree)
1506 {
1507 events_.clear();
1508 if (action == 0 || action > 3)
1510 const auto matches = matching(id, priority, wholeTree);
1511 for (const auto match : matches)
1512 {
1513 auto* node = find(match);
1514 (void)node->clock.setEndingAction(action);
1515 forceAncestors(*node);
1516 }
1517 return matches.size();
1518 }
1519
1521 data::DataId id, std::uint16_t priority,
1522 std::uint8_t volume, bool wholeTree)
1523 {
1524 events_.clear();
1525 const auto matches = matching(id, priority, wholeTree);
1526 const auto clamped = std::min<std::uint8_t>(volume, 100U);
1527 for (const auto match : matches)
1528 {
1529 auto* node = find(match);
1530 if (!node) continue;
1531 const auto& payload = node->definition().record.data;
1532 if (std::holds_alternative<data::SequenceSoundData>(payload) ||
1533 std::holds_alternative<data::SequenceVideoData>(payload))
1534 node->commandedVolume = clamped;
1535 }
1536 return matches.size();
1537 }
1538
1540 data::DataId id, std::uint16_t priority,
1541 std::uint16_t pitch, bool wholeTree)
1542 {
1543 events_.clear();
1544 const auto matches = matching(id, priority, wholeTree);
1545 for (const auto match : matches)
1546 {
1547 auto* node = find(match);
1548 if (!node) continue;
1549 const auto& payload = node->definition().record.data;
1550 if (std::holds_alternative<data::SequenceSoundData>(payload) ||
1551 std::holds_alternative<data::SequenceVideoData>(payload))
1552 node->commandedPitch = pitch;
1553 }
1554 return matches.size();
1555 }
1556
1558 data::DataId id, std::uint16_t priority,
1559 std::int8_t panning, bool wholeTree)
1560 {
1561 events_.clear();
1562 const auto matches = matching(id, priority, wholeTree);
1563 const auto clamped = static_cast<std::int8_t>(
1564 std::clamp<int>(panning, -100, 100));
1565 for (const auto match : matches)
1566 {
1567 auto* node = find(match);
1568 if (!node) continue;
1569 const auto& payload = node->definition().record.data;
1570 if (std::holds_alternative<data::SequenceSoundData>(payload) ||
1571 std::holds_alternative<data::SequenceVideoData>(payload))
1572 node->commandedPanning = clamped;
1573 }
1574 return matches.size();
1575 }
1576
1578 std::uint16_t priority, const SequenceTransform& transform, bool wholeTree)
1579 {
1580 events_.clear();
1581 const auto matches = matching(id, priority, wholeTree);
1582 for (const auto match : matches)
1583 if (auto* node = find(match)) move(*node, transform);
1584 // FindNextSequence counts a target even when MoveSequence's exact
1585 // matrix comparison makes the individual mutation a no-op.
1586 return matches.size();
1587 }
1589 std::uint16_t priority, bool wholeTree)
1590 {
1591 events_.clear();
1592 const auto matches = matching(id, priority, wholeTree);
1593 for (const auto match : matches)
1594 if (auto* node = find(match))
1595 {
1596 node->redrawRequested = true;
1597 forceAncestors(*node);
1598 }
1599 return matches.size();
1600 }
1601
1602 std::optional<SequenceNodeView> SequenceRuntime::inspect(SequenceNodeId id) const
1603 {
1604 const auto* node = find(id);
1605 if (!node) return std::nullopt;
1606 SequenceNodeView view{node->id, node->parent ? node->parent->id : 0,
1607 node->definition().dataId, node->definition().record.chunk.headerOffset, node->priority,
1608 node->labelNumber, node->clock.clock(), node->clock.endTime(),
1609 node->clock.timeMultiple(), node->clock.paused(),
1610 node->needsRedraw, node->dimensionality, node->definition().contentsDataId, node->explicitlyPositioned,
1611 node->localTransform, node->tweekerTransformApplied,
1612 node->tweekerTransform, node->worldTransform, {}};
1613 view.meshChoice = node->meshChoice;
1614 // Publish the same live controls as the audio/video consumers. Keep
1615 // authored aux.audio values separate for subsequent tweeker updates.
1616 view.pitch = node->commandedPitch.value_or(node->pitch);
1617 view.volume = node->commandedVolume.value_or(node->volume);
1618 view.panning = node->commandedPanning.value_or(node->panning);
1619 for (const auto& child : node->children) view.children.push_back(child->id);
1620 return view;
1621 }
1622 std::optional<SequenceInfoView> SequenceRuntime::info(data::DataId id,
1623 std::uint16_t priority, bool wholeTree) const
1624 {
1625 const auto matches = matching(id, priority, wholeTree);
1626 if (matches.empty()) return std::nullopt;
1627 const auto* node = find(matches.front());
1628 if (!node) return std::nullopt;
1629
1630 SequenceInfoView result{node->id, node->clock.clock(), node->clock.endTime(),
1631 node->dimensionality, std::nullopt};
1632 if (node->dimensionality == 3)
1633 if (const auto* matrix = std::get_if<Matrix3D>(&node->worldTransform))
1634 result.sequenceToWorldTransformation = *matrix;
1635 return result;
1636 }
1637
1639 data::DataId id, std::uint16_t priority, bool wholeTree) const
1640 {
1641 const auto matches = matching(id, priority, wholeTree);
1642 if (matches.empty()) return true;
1643
1644 const auto* node = find(matches.front());
1645 if (!node) return true;
1646 return node->clock.clock() >= node->clock.endTime();
1647 }
1648
1649
1651 data::DataId id, std::uint16_t priority) const
1652 {
1653 const auto roots = matching(id, priority, false);
1654 if (roots.empty()) return std::nullopt;
1655 const auto* selected = find(roots.front());
1656 if (!selected) return std::nullopt;
1657
1658 const auto visit = [&](const auto& self, const Node& node) -> std::optional<Matrix3D>
1659 {
1660 if (node.definition().record.chunk.id == 9 && node.dimensionality == 3)
1661 if (const auto* matrix = std::get_if<Matrix3D>(&node.worldTransform))
1662 return *matrix;
1663 for (const auto& child : node.children)
1664 if (auto found = self(self, *child)) return found;
1665 return std::nullopt;
1666 };
1667 return visit(visit, *selected);
1668 }
1669
1670 std::vector<SequenceBitmapInstanceView> SequenceRuntime::bitmapInstances() const
1671 {
1672 std::vector<SequenceBitmapInstanceView> result;
1673 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1674 for (const auto& node : nodes)
1675 {
1676 const auto& definition = node->definition();
1677 if (definition.contentsDataId && node->dimensionality == 2 &&
1678 std::holds_alternative<data::SequenceBitmapData>(definition.record.data) &&
1679 std::holds_alternative<Matrix2D>(node->worldTransform))
1680 {
1681 const Node* root = node.get();
1682 while (root->parent) root = root->parent;
1683 result.push_back({node->id, *definition.contentsDataId,
1684 node->priority, node->clock.clock(),
1685 std::get<Matrix2D>(node->worldTransform),
1686 boundingBox2D(definition.attributes), definition.zeroBitmapOffset,
1687 root->definition().dataId, root->id, root->priority});
1688 }
1689 self(self, node->children);
1690 }
1691 };
1692 visit(visit, roots_);
1693 return result;
1694 }
1695
1696
1697 std::vector<SequenceSoundInstanceView> SequenceRuntime::soundInstances() const
1698 {
1699 std::vector<SequenceSoundInstanceView> result;
1700 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1701 for (const auto& node : nodes)
1702 {
1703 const auto& definition = node->definition();
1704 if (std::holds_alternative<data::SequenceSoundData>(
1705 definition.record.data))
1706 {
1707 std::optional<std::int32_t> centerX;
1708 if (node->dimensionality == 2 &&
1709 std::holds_alternative<Matrix2D>(node->worldTransform))
1711 definition.attributes,
1712 std::get<Matrix2D>(node->worldTransform));
1713 result.push_back({
1714 node->id,
1715 definition.contentsDataId.value_or(
1717 node->priority,
1718 node->clock.clock(),
1719 node->clock.endingAction(),
1720 node->dimensionality,
1721 node->commandedPitch.value_or(node->pitch),
1722 node->commandedVolume.value_or(node->volume),
1723 node->commandedPanning.value_or(node->panning),
1724 centerX,
1725 externalFileName(definition.attributes),
1726 node->clock.paused(), node->seekGeneration, {},
1727 node->commandedVolume, node->commandedPanning});
1728 if (centerX)
1729 result.back().spatial2D = SequenceSpatial2DView{
1730 std::get<Matrix2D>(node->worldTransform),
1731 boundingBox2D(definition.attributes).value_or(
1733 node->movementRevision};
1734 }
1735 self(self, node->children);
1736 }
1737 };
1738 visit(visit, roots_);
1739 return result;
1740 }
1741
1742 std::vector<SequenceVideoInstanceView> SequenceRuntime::videoInstances() const
1743 {
1744 std::vector<SequenceVideoInstanceView> result;
1745 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1746 for (const auto& node : nodes)
1747 {
1748 const auto& definition = node->definition();
1749 if (std::holds_alternative<data::SequenceVideoData>(
1750 definition.record.data) &&
1751 node->dimensionality == 2 &&
1752 std::holds_alternative<Matrix2D>(node->worldTransform))
1753 {
1754 result.push_back({
1755 node->id,
1756 node->priority,
1757 node->clock.clock(),
1758 std::get<data::SequenceVideoData>(definition.record.data),
1759 externalFileName(definition.attributes),
1760 boundingBox2D(definition.attributes),
1761 std::get<Matrix2D>(node->worldTransform),
1762 node->clock.endingAction(), definition.binkDoubleSize,
1763 node->clock.elapsedParentClock(),
1764 node->commandedPitch.value_or(node->pitch),
1765 node->commandedVolume.value_or(node->volume),
1766 node->commandedPanning.value_or(node->panning)});
1767 }
1768 self(self, node->children);
1769 }
1770 };
1771 visit(visit, roots_);
1772 return result;
1773 }
1774
1775 std::vector<SequenceMeshInstanceView> SequenceRuntime::meshInstances() const
1776 {
1777 std::vector<SequenceMeshInstanceView> result;
1778 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1779 for (const auto& node : nodes)
1780 {
1781 const auto& definition = node->definition();
1782 if ((definition.record.chunk.id == 4 ||
1783 definition.record.chunk.id == 9) &&
1784 definition.contentsDataId &&
1785 node->dimensionality == 3 &&
1786 std::holds_alternative<Matrix3D>(node->worldTransform))
1787 {
1788 std::optional<data::DataId> textureMapDataId;
1789 std::optional<data::DataId> jointPositionsDataId;
1790 if (const auto* model =
1791 std::get_if<data::SequenceModelData>(
1792 &definition.record.data))
1793 {
1794 if (model->textureMapDataId != data::EmptyDataId)
1795 textureMapDataId = data::resolveSequenceDataId(
1796 definition.record.header,
1797 model->textureMapDataId,
1798 definition.dataId);
1799 if (model->jointPositionsDataId != data::EmptyDataId)
1800 jointPositionsDataId = data::resolveSequenceDataId(
1801 definition.record.header,
1802 model->jointPositionsDataId,
1803 definition.dataId);
1804 }
1805
1806 const Node* root = node.get();
1807 while (root->parent)
1808 root = root->parent;
1809 const auto rootSequenceDataId =
1811
1812 result.push_back({node->id, *definition.contentsDataId,
1813 rootSequenceDataId,
1814 node->priority, node->clock.clock(),
1815 std::get<Matrix3D>(node->worldTransform),
1816 node->meshChoice,
1818 definition.attributes, node->dimensionality),
1819 textureMapDataId, jointPositionsDataId, root->priority});
1820 }
1821 self(self, node->children);
1822 }
1823 };
1824 visit(visit, roots_);
1825 return result;
1826 }
1827
1828 std::vector<SequenceScrollingWorldView>
1830 {
1831 std::vector<SequenceScrollingWorldView> result;
1832 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1833 for (const auto& node : nodes)
1834 {
1835 const auto& definition = node->definition();
1836 if (definition.record.header.scrollingWorld)
1837 {
1838 result.push_back({
1839 node->id,
1840 node->dimensionality,
1841 node->worldTransform,
1842 node->dimensionality == 2
1843 ? boundingBox2D(definition.attributes)
1844 : std::nullopt,
1846 definition.attributes, node->dimensionality),
1847 node->scrollingOnScreen});
1848 }
1849 self(self, node->children);
1850 }
1851 };
1852 visit(visit, roots_);
1853 return result;
1854 }
1855
1856 std::vector<SequenceCamera3DView> SequenceRuntime::cameraInstances() const
1857 {
1858 std::vector<SequenceCamera3DView> result;
1859 const auto visit = [&](const auto& self, const Nodes& nodes) -> void {
1860 for (const auto& node : nodes)
1861 {
1862 const auto& definition = node->definition();
1863 if (definition.record.chunk.id == 7 && node->dimensionality == 3 &&
1864 std::holds_alternative<Matrix3D>(node->worldTransform))
1865 {
1866 const auto& camera = std::get<data::SequenceCameraData>(
1867 definition.record.data);
1868 result.push_back({node->id, camera.cameraLabel, node->priority,
1869 node->clock.clock(), std::get<Matrix3D>(node->worldTransform),
1870 node->cameraFieldOfView, camera.nearClipPlaneDistance,
1871 camera.farClipPlaneDistance});
1872 }
1873 self(self, node->children);
1874 }
1875 };
1876 visit(visit, roots_);
1877 return result;
1878 }
1879
1880 std::optional<SequenceCamera2DView> SequenceRuntime::camera2DForLabel(
1881 std::uint8_t label) const
1882 {
1883 if (label == 0) return std::nullopt;
1884 const auto* node = labelOwners_[label] ? find(labelOwners_[label]) : nullptr;
1885 if (!node || node->dimensionality != 2 ||
1886 !std::holds_alternative<Matrix2D>(node->worldTransform))
1887 return std::nullopt;
1888 const bool camera = node->definition().record.chunk.id == 7;
1889 auto bounds = boundingBox2D(node->definition().attributes);
1890 if (!bounds && camera)
1891 bounds = data::Sequence2DBoundingBoxAttribute{{}, -5, -5, 5, 5};
1892 return SequenceCamera2DView{node->id,
1893 std::get<Matrix2D>(node->worldTransform),
1894 bounds.value_or(data::Sequence2DBoundingBoxAttribute{}),
1895 camera ? std::optional<float>{node->cameraFieldOfView} : std::nullopt};
1896 }
1897
1898 std::optional<SequenceCamera3DView> SequenceRuntime::cameraForLabel(
1899 std::uint8_t label) const
1900 {
1901 if (label == 0) return std::nullopt;
1902 const auto owner = labelOwners_[label];
1903 const auto* node = owner != 0 ? find(owner) : nullptr;
1904 if (!node || node->dimensionality != 3 ||
1905 !std::holds_alternative<Matrix3D>(node->worldTransform))
1906 return std::nullopt;
1907
1908 // The L_Rend3D camera update accepts any labeled 3D sequence for the
1909 // pose. Only camera chunks supply the half-FOV and clipping planes;
1910 // groups and meshes use the retail defaults instead.
1911 const auto* camera = std::get_if<data::SequenceCameraData>(
1912 &node->definition().record.data);
1913 return SequenceCamera3DView{node->id, node->labelNumber, node->priority,
1914 node->clock.clock(), std::get<Matrix3D>(node->worldTransform),
1915 camera ? node->cameraFieldOfView : 0.7853981633974F,
1916 camera ? camera->nearClipPlaneDistance : 1.0F,
1917 camera ? camera->farClipPlaneDistance : 5000.0F};
1918 }
1919
1920 std::vector<SequenceNodeId> SequenceRuntime::roots() const
1921 {
1922 std::vector<SequenceNodeId> result;
1923 for (const auto& root : roots_) result.push_back(root->id);
1924 return result;
1925 }
1926}
std::vector< std::uint8_t > data
std::size_t offset
std::size_t root
const char * header
std::atomic< float > left
std::atomic< float > right
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
int bottom
int top
data::DataId id
std::uint16_t priority
std::shared_ptr< const data::HmdTextureImage > source
std::uint64_t key
std::uint32_t height
unsigned index
std::string_view label
data::DataTag first
Kind kind
std::uint16_t pitch
std::uint8_t volume
std::int8_t panning
std::string path
std::string error
Bytes input
std::uint16_t value
Definition Timers.cpp:16
static std::expected< SequenceChildSchedule, ChildScheduleError > read(data::SharedDataBytes bytes, data::DataId containingDataId, std::size_t maximumRecords=65 '536)
static std::expected< SequenceClock, ClockError > start(const data::LegacySequenceRecord &record, ClockStartOptions options={})
std::span< const SequenceDescription > descriptions() const noexcept
std::shared_ptr< const data::ResourceSnapshot > resources() const noexcept
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > rawBitmap(data::DataId id, data::LegacyDataType sourceType, DescriptionLimits limits={})
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > load(const data::DataSource &source, data::DataId id, std::size_t offset=0, DescriptionLimits limits={})
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > runtimeVideo(data::DataId id, std::string fileName, data::SequenceVideoData options, data::Sequence2DBoundingBoxAttribute bounds, bool binkDoubleSize=false)
std::expected< void, RuntimeError > setEndingAction(SequenceNodeId node, std::uint8_t action)
std::expected< void, RuntimeError > setVolume(SequenceNodeId node, std::uint8_t volume)
SequenceRuntime(RuntimeLimits limits={})
std::vector< SequenceScrollingWorldView > scrollingWorldInstances() const
bool isSequenceFinished(data::DataId id, std::uint16_t priority, bool wholeTree=false) const
std::vector< SequenceVideoInstanceView > videoInstances() const
std::vector< SequenceNodeId > matching(data::DataId id, std::uint16_t priority, bool wholeTree=false) const
std::size_t setPitchMatching(data::DataId id, std::uint16_t priority, std::uint16_t pitch, bool wholeTree=false)
std::expected< void, RuntimeError > requestSoundClock(SequenceNodeId id, std::int32_t mediaClock)
std::size_t forceRedrawMatching(data::DataId id, std::uint16_t priority, bool wholeTree=false)
std::expected< void, RuntimeError > setPaused(SequenceNodeId node, bool paused)
std::expected< void, RuntimeError > update(std::int32_t parentClock)
std::expected< void, RuntimeError > requestVideoClock(SequenceNodeId node, std::int32_t mediaClock, std::int32_t duration, bool ended)
std::optional< Matrix3D > childMeshWorldMatrix(data::DataId id, std::uint16_t priority) const
std::size_t moveMatching(data::DataId id, std::uint16_t priority, const SequenceTransform &transform, bool wholeTree=false)
std::expected< void, RuntimeError > stop(SequenceNodeId node)
std::optional< SequenceCamera2DView > camera2DForLabel(std::uint8_t label) const
std::size_t setPanningMatching(data::DataId id, std::uint16_t priority, std::int8_t panning, bool wholeTree=false)
std::expected< void, RuntimeError > setPitch(SequenceNodeId node, std::uint16_t pitch)
std::size_t stopMatching(data::DataId id, std::uint16_t priority, bool wholeTree=false)
std::vector< SequenceNodeId > roots() const
std::expected< std::size_t, RuntimeError > setEndingActionMatching(data::DataId id, std::uint16_t priority, std::uint8_t action, bool wholeTree=false)
std::vector< SequenceBitmapInstanceView > bitmapInstances() const
std::expected< SequenceNodeId, RuntimeError > start(std::shared_ptr< const SequenceProgram > program, std::uint16_t priority=0, ClockStartOptions options={}, std::optional< SequenceTransform > initialTransform=std::nullopt, std::uint8_t labelOverride=0)
std::vector< SequenceSoundInstanceView > soundInstances() const
std::size_t liveNodeCount() const noexcept
std::size_t setVolumeMatching(data::DataId id, std::uint16_t priority, std::uint8_t volume, bool wholeTree=false)
std::span< const SequenceEvent > events() const noexcept
std::expected< void, RuntimeError > requestSoundFailure(SequenceNodeId id)
std::expected< void, RuntimeError > setScrollingWorldVisibility(SequenceNodeId node, bool onScreen)
std::vector< SequenceCamera3DView > cameraInstances() const
std::vector< SequenceMeshInstanceView > meshInstances() const
std::optional< SequenceInfoView > info(data::DataId id, std::uint16_t priority, bool wholeTree=false) const
std::expected< void, RuntimeError > setPanning(SequenceNodeId node, std::int8_t panning)
std::expected< void, RuntimeError > seek(SequenceNodeId node, std::int32_t time)
std::optional< SequenceCamera3DView > cameraForLabel(std::uint8_t label) const
std::optional< SequenceNodeView > inspect(SequenceNodeId node) const
std::uint32_t legacyWaveDurationTicks(std::span< const std::uint8_t > riffWave, std::uint32_t ticksPerSecond) noexcept
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr DataId EmptyDataId
Definition DataBanks.hpp:13
std::expected< LegacySequenceAttributes, SequenceError > readLegacySequenceAttributes(const LegacyChunkReader &reader, std::size_t maximumAttributes)
std::shared_ptr< const DataBytes > SharedDataBytes
std::expected< LegacySequenceRecord, SequenceError > readLegacySequenceRecord(LegacyChunkReader &reader)
std::expected< LegacyChunkReader, DataError > openLegacyChunkReader(const DataSource &source, DataId id)
constexpr DataId resolveSequenceDataId(const LegacySequenceHeader &header, DataId rawId, DataId containingSequenceId) noexcept
const PendingPhase & current(const GameState &state)
std::expected< EvaluatedTweekerTransform, TweekerTransformError > evaluateTweekerTransform(const data::LegacySequenceAttributes &attributes, std::uint8_t interpolationType, std::int32_t clock, std::int32_t endTime, std::uint8_t parentDimensionality) noexcept
std::expected< SequenceChildSchedule, ChildScheduleError > openSequenceChildSchedule(const data::DataSource &source, data::DataId parentDataId, std::size_t parentOffset, std::size_t maximumRecords)
Matrix2D identity2D() noexcept
std::variant< std::monostate, Matrix2D, Matrix3D > SequenceTransform
TweekerKeys selectTweekerKeys(const data::LegacySequenceAttributes &attributes) noexcept
std::uint64_t SequenceNodeId
SequenceTransform composeSequenceWorld(const SequenceTransform &local, std::uint8_t dimensionality, const SequenceTransform &parentWorld, std::uint8_t parentDimensionality) noexcept
Matrix3D identity3D() noexcept
InitialSequenceTransform initialSequenceTransform(const data::LegacySequenceRecord &record, const data::LegacySequenceAttributes &attributes, std::uint8_t parentDimensionality) noexcept
Matrix2D multiply(const Matrix2D &left, const Matrix2D &right) noexcept
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
std::vector< LegacySequenceAttribute > values
std::optional< std::uint8_t > commandedVolume
std::shared_ptr< const SequenceProgram > program
std::optional< std::int8_t > commandedPanning
std::optional< std::uint16_t > commandedPitch
const SequenceDescription & definition() const