yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
cutscene_camera_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <fstream>
6#include <iterator>
7#include <vector>
8
9#include "absl/strings/str_format.h"
15#include "core/project.h"
17#include "imgui/imgui.h"
18
19namespace yaze::editor {
20
22
23namespace {
24
25constexpr float kMarkerRadius = 5.0f;
26constexpr const char* kSizeLabels[] = {"From ROM / vanilla", "512 x 512",
27 "1024 x 1024", "1024 x 512 (wide)",
28 "512 x 1024 (tall)"};
29
30ImU32 ToU32(ImVec4 color, float alpha = 1.0f) {
31 color.w *= alpha;
32 return ImGui::ColorConvertFloat4ToU32(color);
33}
34
35bool InputHex(const char* label, int* value, int max) {
36 ImGui::SetNextItemWidth(90.0f);
37 const bool changed = ImGui::InputInt(label, value, 1, 16,
38 ImGuiInputTextFlags_CharsHexadecimal);
39 *value = std::clamp(*value, 0, max);
40 return changed;
41}
42
43bool InputWorld(const char* label, int* value) {
44 ImGui::SetNextItemWidth(110.0f);
45 const bool changed = ImGui::InputInt(label, value, 1, 8);
46 *value = std::clamp(*value, 0, 0xFFFF);
47 return changed;
48}
49
50} // namespace
51
52std::optional<std::filesystem::path> CutsceneCameraPanel::ShotsPath() const {
53 const auto* project = ContentRegistry::Context::current_project();
54 if (project == nullptr || !project->project_opened() ||
55 project->cutscene_shots.empty()) {
56 return std::nullopt;
57 }
58 return std::filesystem::path(
59 project->GetAbsolutePath(project->cutscene_shots));
60}
61
63 loaded_once_ = true;
64 dirty_ = false;
65 shots_ = {};
66 selected_ = -1;
67 loaded_bytes_.reset();
68 const auto path = ShotsPath();
69 if (!path.has_value()) {
70 loaded_path_.clear();
71 status_ =
72 "No [files] cutscene_shots in the project: read-only, use Copy JSON.";
73 status_is_error_ = false;
74 return;
75 }
76 loaded_path_ = path->string();
77 std::ifstream in(*path, std::ios::binary);
78 if (!in) {
79 loaded_bytes_ = std::nullopt; // Saving will create it.
80 status_ = absl::StrFormat("%s does not exist yet; Save creates it.",
82 status_is_error_ = false;
83 return;
84 }
85 const std::string bytes((std::istreambuf_iterator<char>(in)), {});
86 auto parsed = zelda3::ParseCutsceneShots(bytes);
87 if (!parsed.ok()) {
88 status_ = std::string(parsed.status().message());
89 status_is_error_ = true;
90 return;
91 }
92 shots_ = std::move(*parsed);
93 loaded_bytes_ = bytes;
94 selected_ = shots_.shots.empty() ? -1 : 0;
95 status_ = absl::StrFormat("Loaded %zu shot(s) from %s", shots_.shots.size(),
97 status_is_error_ = false;
98}
99
101 const auto path = ShotsPath();
102 if (!path.has_value()) {
103 return;
104 }
106 if (!text.ok()) {
107 status_ = std::string(text.status().message());
108 status_is_error_ = true;
109 return;
110 }
111 std::error_code ec;
112 std::filesystem::create_directories(path->parent_path(), ec);
114 .subject = "cutscene shots", .published_file = "cutscene shots"};
115 auto lock = core::AcquireSourceArtifactPublicationLock({*path}, labels);
116 if (!lock.ok()) {
117 status_ = std::string(lock.status().message());
118 status_is_error_ = true;
119 return;
120 }
121 // Refuse to overwrite edits made outside yaze since the last load. Read
122 // and close the handle before any write below: on Windows an open handle
123 // without FILE_SHARE_DELETE blocks the atomic replace that follows,
124 // failing every save after the first with "Access is denied".
125 std::optional<std::string> on_disk;
126 {
127 std::ifstream in(*path, std::ios::binary);
128 if (in) {
129 on_disk = std::string((std::istreambuf_iterator<char>(in)), {});
130 }
131 }
132 if (on_disk != loaded_bytes_) {
133 status_ = absl::StrFormat(
134 "%s changed outside yaze since it was loaded. Reload before saving.",
135 path->string());
136 status_is_error_ = true;
137 return;
138 }
139 absl::Status status;
140 if (on_disk.has_value()) {
141 std::vector<core::SourceArtifactUpdate> updates;
142 updates.push_back(core::SourceArtifactUpdate{
143 .target = *path, .before = on_disk, .after = *text});
145 **lock, std::move(updates),
147 } else {
148 // First save: the publisher replaces existing files only. Write a temp
149 // file beside the target and move it into place while holding the lock.
150 const std::filesystem::path temp =
151 path->parent_path() / (path->filename().string() + ".yaze-new");
152 {
153 std::ofstream out(temp, std::ios::binary | std::ios::trunc);
154 out << *text;
155 if (!out) {
156 status = absl::InternalError(
157 absl::StrFormat("Could not write %s", temp.string()));
158 }
159 }
160 if (status.ok()) {
161 std::filesystem::rename(temp, *path, ec);
162 if (ec) {
163 status = absl::InternalError(absl::StrFormat(
164 "Could not create %s: %s", path->string(), ec.message()));
165 }
166 }
167 std::filesystem::remove(temp, ec);
168 }
169 if (!status.ok()) {
170 status_ = std::string(status.message());
171 status_is_error_ = true;
172 return;
173 }
174 loaded_bytes_ = *text;
175 dirty_ = false;
176 status_ = absl::StrFormat("Saved %zu shot(s) to %s", shots_.shots.size(),
177 path->string());
178 status_is_error_ = false;
179}
180
182 auto* editor = CurrentOverworldEditor();
183 // The editor exists before its overworld loads, and a failed load leaves
184 // the map list empty; only a loaded overworld can answer area queries.
185 if (editor == nullptr || !editor->overworld().is_loaded()) {
186 return nullptr;
187 }
188 return editor;
189}
190
192 const zelda3::CutsceneShot& shot) const {
193 if (size_override_ == 0) {
194 if (auto* source = OverworldSource()) {
195 if (const auto* map = source->overworld().overworld_map(shot.area)) {
196 return zelda3::AreaExtentFromParent(map->parent(), map->area_size());
197 }
198 }
199 }
200 const int area = shot.area & 0x3F;
201 const int grid_x = (area % 8) * 512;
202 const int grid_y = (area / 8) * 512;
203 switch (size_override_) {
204 case 1:
205 return {grid_x, grid_y, 512, 512};
206 case 2:
207 return {grid_x, grid_y, 1024, 1024};
208 case 3:
209 return {grid_x, grid_y, 1024, 512};
210 case 4:
211 return {grid_x, grid_y, 512, 1024};
212 default: {
213 auto extent = zelda3::VanillaAreaExtent(shot.area);
214 return extent.ok() ? *extent : zelda3::AreaExtent{grid_x, grid_y};
215 }
216 }
217}
218
219void CutsceneCameraPanel::Draw(bool* p_open) {
220 (void)p_open;
221 if (!loaded_once_) {
222 Load();
223 }
224 const auto path = ShotsPath();
225 const bool writable = path.has_value();
226
227 if (ImGui::Button(ICON_MD_REFRESH " Reload")) {
228 Load();
229 }
230 ImGui::SameLine();
231 ImGui::BeginDisabled(!writable || !dirty_);
232 if (ImGui::Button(ICON_MD_SAVE " Save")) {
233 Save();
234 }
235 ImGui::EndDisabled();
236 ImGui::SameLine();
237 if (ImGui::Button(ICON_MD_CONTENT_COPY " Copy JSON")) {
239 if (text.ok()) {
240 ImGui::SetClipboardText(text->c_str());
241 status_ = "Copied the shots JSON to the clipboard.";
242 status_is_error_ = false;
243 } else {
244 status_ = std::string(text.status().message());
245 status_is_error_ = true;
246 }
247 }
248 ImGui::SameLine();
249 ImGui::TextColored(
251 status_.c_str());
252 ImGui::Separator();
253
254 if (ImGui::BeginTable(
255 "##cutscene_layout", 2,
256 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
257 ImGui::TableSetupColumn("Shots", ImGuiTableColumnFlags_WidthFixed, 300.0f);
258 ImGui::TableSetupColumn("Frame", ImGuiTableColumnFlags_WidthStretch);
259 ImGui::TableNextRow();
260 ImGui::TableNextColumn();
261 DrawShotList();
262 if (selected_ >= 0 && selected_ < static_cast<int>(shots_.shots.size())) {
264 }
265 ImGui::TableNextColumn();
266 if (selected_ >= 0 && selected_ < static_cast<int>(shots_.shots.size())) {
268 } else {
269 ImGui::TextDisabled("Add or select a shot.");
270 }
271 ImGui::EndTable();
272 }
273}
274
276 for (int i = 0; i < static_cast<int>(shots_.shots.size()); ++i) {
277 ImGui::PushID(i);
278 const auto& shot = shots_.shots[i];
279 const std::string label =
280 absl::StrFormat("%s (area 0x%02X)", shot.name, shot.area);
281 if (ImGui::Selectable(label.c_str(), selected_ == i)) {
282 selected_ = i;
283 }
284 ImGui::PopID();
285 }
286 if (ImGui::Button(ICON_MD_ADD " Add shot")) {
288 int n = static_cast<int>(shots_.shots.size()) + 1;
289 auto taken = [&](const std::string& name) {
290 return std::any_of(shots_.shots.begin(), shots_.shots.end(),
291 [&](const auto& s) { return s.name == name; });
292 };
293 while (taken(absl::StrFormat("shot_%d", n)))
294 ++n;
295 shot.name = absl::StrFormat("shot_%d", n);
296 shot.area = selected_ >= 0 ? shots_.shots[selected_].area : 0x2D;
297 const auto extent = ExtentFor(shot);
298 shot.camera_x = extent.origin_x;
299 shot.camera_y = extent.origin_y;
300 shot.link = {extent.origin_x + 128, extent.origin_y + 112, 2};
301 shots_.shots.push_back(shot);
302 selected_ = static_cast<int>(shots_.shots.size()) - 1;
303 dirty_ = true;
304 }
305 ImGui::SameLine();
306 ImGui::BeginDisabled(selected_ < 0);
307 if (ImGui::Button(ICON_MD_DELETE " Delete")) {
308 shots_.shots.erase(shots_.shots.begin() + selected_);
309 selected_ = std::min(selected_, static_cast<int>(shots_.shots.size()) - 1);
310 dirty_ = true;
311 }
312 ImGui::EndDisabled();
313 ImGui::Separator();
314}
315
317 char name[64];
318 absl::SNPrintF(name, sizeof(name), "%s", shot.name);
319 ImGui::SetNextItemWidth(200.0f);
320 if (ImGui::InputText("Name", name, sizeof(name),
321 ImGuiInputTextFlags_CharsNoBlank)) {
322 shot.name = name;
323 dirty_ = true;
324 }
325 dirty_ |= InputHex("Area", &shot.area, 0x7F);
326 ImGui::SetNextItemWidth(200.0f);
327 ImGui::Combo("Area size", &size_override_, kSizeLabels,
328 IM_ARRAYSIZE(kSizeLabels));
329
330 const auto extent = ExtentFor(shot);
331 const auto camera = zelda3::ClampCamera(extent, shot.camera_x, shot.camera_y);
332 ImGui::SeparatorText("Camera");
333 dirty_ |= InputWorld("X##cam", &shot.camera_x);
334 dirty_ |= InputWorld("Y##cam", &shot.camera_y);
335 ImGui::Text("$E2 = 0x%04X $E8 = 0x%04X", camera.x, camera.y);
336 if (camera.clamped()) {
337 ImGui::TextColored(gui::GetWarningColor(),
338 "Requested %d, %d is outside the area's camera limits; "
339 "the game shows %d, %d.",
340 camera.requested_x, camera.requested_y, camera.x,
341 camera.y);
342 if (ImGui::SmallButton("Use clamped position")) {
343 shot.camera_x = camera.x;
344 shot.camera_y = camera.y;
345 dirty_ = true;
346 }
347 }
348
349 ImGui::SeparatorText("Link");
350 dirty_ |= InputWorld("X##link", &shot.link.x);
351 dirty_ |= InputWorld("Y##link", &shot.link.y);
352 ImGui::SetNextItemWidth(110.0f);
353 dirty_ |= ImGui::SliderInt("Facing##link", &shot.link.facing, 0, 3);
354
355 ImGui::SeparatorText("Actors");
356 for (int i = 0; i < static_cast<int>(shot.actors.size()); ++i) {
357 ImGui::PushID(i);
358 auto& actor = shot.actors[i];
359 dirty_ |= InputHex("Sprite", &actor.sprite, 0xFF);
360 dirty_ |= InputWorld("X", &actor.x);
361 ImGui::SameLine();
362 dirty_ |= InputWorld("Y", &actor.y);
363 ImGui::SetNextItemWidth(110.0f);
364 dirty_ |= ImGui::SliderInt("Facing", &actor.facing, 0, 3);
365 char note[96];
366 absl::SNPrintF(note, sizeof(note), "%s", actor.note);
367 ImGui::SetNextItemWidth(200.0f);
368 if (ImGui::InputText("Note", note, sizeof(note))) {
369 actor.note = note;
370 dirty_ = true;
371 }
372 if (ImGui::SmallButton(ICON_MD_DELETE " Remove actor")) {
373 shot.actors.erase(shot.actors.begin() + i);
374 dirty_ = true;
375 ImGui::PopID();
376 break;
377 }
378 ImGui::Separator();
379 ImGui::PopID();
380 }
381 if (ImGui::Button(ICON_MD_PERSON_ADD " Add actor")) {
382 shot.actors.push_back({0, camera.x + 128, camera.y + 112, 0, ""});
383 dirty_ = true;
384 }
385}
386
388 const auto extent = ExtentFor(shot);
389 const float avail = std::max(ImGui::GetContentRegionAvail().x, 128.0f);
390 const float scale = std::min(1.0f, avail / static_cast<float>(extent.width));
391 const ImVec2 size(extent.width * scale, extent.height * scale);
392 const ImVec2 origin = ImGui::GetCursorScreenPos();
393 ImDrawList* draw = ImGui::GetWindowDrawList();
394
395 ImGui::InvisibleButton("##cutscene_canvas", size);
396 const bool hovered = ImGui::IsItemHovered();
397 const ImVec2 mouse = ImGui::GetIO().MousePos;
398 const int mouse_world_x =
399 extent.origin_x +
400 static_cast<int>(std::floor((mouse.x - origin.x) / scale));
401 const int mouse_world_y =
402 extent.origin_y +
403 static_cast<int>(std::floor((mouse.y - origin.y) / scale));
404
405 // Area background and 512-pixel screen seams.
406 draw->AddRectFilled(origin, ImVec2(origin.x + size.x, origin.y + size.y),
407 ToU32(ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)));
408 for (int seam = 512; seam < extent.width; seam += 512) {
409 draw->AddLine(ImVec2(origin.x + seam * scale, origin.y),
410 ImVec2(origin.x + seam * scale, origin.y + size.y),
411 ToU32(gui::GetDisabledColor(), 0.5f));
412 }
413 for (int seam = 512; seam < extent.height; seam += 512) {
414 draw->AddLine(ImVec2(origin.x, origin.y + seam * scale),
415 ImVec2(origin.x + size.x, origin.y + seam * scale),
416 ToU32(gui::GetDisabledColor(), 0.5f));
417 }
418
419 // The area's own screens, when the Overworld editor can provide them.
420 bool drew_map = false;
421 if (auto* source = OverworldSource()) {
422 const int world_base = shot.area & 0x40;
423 const int parent_screen =
424 (extent.origin_y / 512) * 8 + extent.origin_x / 512;
425 for (int dy = 0; dy < extent.height / 512; ++dy) {
426 for (int dx = 0; dx < extent.width / 512; ++dx) {
427 const auto* bitmap =
428 source->AreaScreenBitmap(world_base + parent_screen + dx + dy * 8);
429 if (bitmap == nullptr) {
430 continue;
431 }
432 const ImVec2 p0(origin.x + dx * 512 * scale,
433 origin.y + dy * 512 * scale);
434 draw->AddImage((ImTextureID)(intptr_t)bitmap->texture(), p0,
435 ImVec2(p0.x + 512 * scale, p0.y + 512 * scale),
436 ImVec2(0, 0), ImVec2(1, 1),
437 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
438 drew_map = true;
439 }
440 }
441 }
442
443 const auto camera = zelda3::ClampCamera(extent, shot.camera_x, shot.camera_y);
444 const auto view = zelda3::ViewportOnMap(extent, camera.x, camera.y, scale);
445 const ImVec2 view_min(origin.x + view.x, origin.y + view.y);
446 const ImVec2 view_max(view_min.x + view.width, view_min.y + view.height);
447
448 auto marker_pos = [&](int x, int y) {
449 const auto p = zelda3::PointOnMap(extent, x, y, scale);
450 return ImVec2(origin.x + p.x, origin.y + p.y);
451 };
452 auto near = [&](ImVec2 p) {
453 const float dx = mouse.x - p.x;
454 const float dy = mouse.y - p.y;
455 return dx * dx + dy * dy <= (kMarkerRadius + 3) * (kMarkerRadius + 3);
456 };
457
458 // Start a drag: markers win over the viewport.
459 if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
461 if (near(marker_pos(shot.link.x, shot.link.y))) {
463 } else {
464 for (int i = 0; i < static_cast<int>(shot.actors.size()); ++i) {
465 if (near(marker_pos(shot.actors[i].x, shot.actors[i].y))) {
467 drag_actor_ = i;
468 break;
469 }
470 }
471 }
472 if (drag_ == Drag::kNone && mouse.x >= view_min.x && mouse.x < view_max.x &&
473 mouse.y >= view_min.y && mouse.y < view_max.y) {
475 }
476 }
477 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
479 }
480 if (drag_ != Drag::kNone &&
481 ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0)) {
482 const ImVec2 delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0);
483 const int dx = static_cast<int>(std::lround(delta.x / scale));
484 const int dy = static_cast<int>(std::lround(delta.y / scale));
485 if (dx != 0 || dy != 0) {
486 auto move = [&](int& x, int& y) {
487 x = std::clamp(x + dx, 0, 0xFFFF);
488 y = std::clamp(y + dy, 0, 0xFFFF);
489 };
490 if (drag_ == Drag::kViewport) {
491 const auto moved =
492 zelda3::ClampCamera(extent, camera.x + dx, camera.y + dy);
493 shot.camera_x = moved.x;
494 shot.camera_y = moved.y;
495 } else if (drag_ == Drag::kLink) {
496 move(shot.link.x, shot.link.y);
497 } else if (drag_actor_ >= 0 &&
498 drag_actor_ < static_cast<int>(shot.actors.size())) {
499 move(shot.actors[drag_actor_].x, shot.actors[drag_actor_].y);
500 }
501 ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left);
502 dirty_ = true;
503 }
504 }
505 // Arrow keys nudge the camera: 1 pixel, 8 with Shift.
506 if (hovered) {
507 const int step = ImGui::GetIO().KeyShift ? 8 : 1;
508 int nx = shot.camera_x;
509 int ny = shot.camera_y;
510 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
511 nx -= step;
512 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
513 nx += step;
514 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
515 ny -= step;
516 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
517 ny += step;
518 if (nx != shot.camera_x || ny != shot.camera_y) {
519 const auto moved = zelda3::ClampCamera(extent, nx, ny);
520 shot.camera_x = moved.x;
521 shot.camera_y = moved.y;
522 dirty_ = true;
523 }
524 }
525
526 // Viewport and markers (markers outside the viewport are dimmed).
527 draw->AddRect(view_min, view_max, ToU32(gui::GetInfoColor()), 0, 0, 2.0f);
528 auto draw_marker = [&](int x, int y, ImVec4 color, const std::string& label) {
529 const bool inside = zelda3::PointInViewport(camera.x, camera.y, x, y);
530 const float alpha = inside ? 1.0f : 0.35f;
531 const ImVec2 p = marker_pos(x, y);
532 draw->AddCircleFilled(p, kMarkerRadius, ToU32(color, alpha));
533 draw->AddText(ImVec2(p.x + kMarkerRadius + 2, p.y - 7),
534 ToU32(ImGui::GetStyleColorVec4(ImGuiCol_Text), alpha),
535 label.c_str());
536 };
537 draw_marker(shot.link.x, shot.link.y, gui::GetSuccessColor(),
538 absl::StrFormat("Link (%d)", shot.link.facing));
539 for (const auto& actor : shot.actors) {
540 draw_marker(actor.x, actor.y, gui::GetWarningColor(),
541 absl::StrFormat("0x%02X", actor.sprite));
542 }
543
544 if (hovered) {
545 ImGui::Text("Mouse: world %d, %d screen %d, %d", mouse_world_x,
546 mouse_world_y, mouse_world_x - camera.x,
547 mouse_world_y - camera.y);
548 } else {
549 ImGui::TextDisabled(
550 "Drag the viewport or a marker; arrow keys nudge "
551 "(Shift: 8 px).");
552 }
553 if (!drew_map) {
554 ImGui::TextDisabled(
555 "Outline only: open the Overworld editor to draw the area's screens.");
556 }
557}
558
559} // namespace yaze::editor
void DrawCanvas(zelda3::CutsceneShot &shot)
std::optional< std::filesystem::path > ShotsPath() const
void DrawShotFields(zelda3::CutsceneShot &shot)
zelda3::AreaExtent ExtentFor(const zelda3::CutsceneShot &shot) const
void Draw(bool *p_open) override
Draw the panel content.
class OverworldEditor * OverworldSource() const
std::optional< std::string > loaded_bytes_
Main UI class for editing overworld maps in A Link to the Past.
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_PERSON_ADD
Definition icons.h:1416
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
::yaze::project::YazeProject * current_project()
Get the current project instance.
Editors are the view controllers for the application.
OverworldEditor * CurrentOverworldEditor()
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
bool InputHex(const char *label, uint64_t *data)
Definition input.cc:470
ImVec4 GetDisabledColor()
Definition ui_helpers.cc:74
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImVec4 GetInfoColor()
Definition ui_helpers.cc:64
absl::StatusOr< std::string > SerializeCutsceneShots(const CutsceneShotSet &set)
absl::StatusOr< AreaExtent > VanillaAreaExtent(int area)
MapRect PointOnMap(const AreaExtent &area, int world_x, int world_y, float scale)
MapRect ViewportOnMap(const AreaExtent &area, int camera_x, int camera_y, float scale)
absl::StatusOr< CutsceneShotSet > ParseCutsceneShots(std::string_view json)
bool PointInViewport(int camera_x, int camera_y, int world_x, int world_y)
AreaExtent AreaExtentFromParent(int parent, AreaSizeEnum size)
ClampedCamera ClampCamera(const AreaExtent &area, int x, int y)
#define REGISTER_PANEL(PanelClass)
Auto-registration macro for panels with default constructors.
std::vector< CutsceneShot > shots
std::vector< CutsceneActor > actors