Files
vn/src/main.cpp
T
maksw a2671bed5d image.cpp: remove unused macros, in-place conversion from rgba to argb.
main.cpp: change move assignment in generic raii wrapper, change the Interpreter from struct to class to better reflect what it actually is.
window.cpp: implement a workaround for the window to not freeze during dragging. fuck you ms.
2026-04-15 18:23:28 +02:00

660 lines
23 KiB
C++

#include <cstdio>
#include <cstdint>
#include <cstring>
#include <chrono>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <window.h>
#include <text.h>
#include <compositor.h>
#include <image.h>
#include <audio.h>
// ---------------------------------------------------------------------------
// RAII handles
// ---------------------------------------------------------------------------
// A generic RAII wrapper.
template <typename T, void(*DestroyFn)(T*)>
struct EngineHandle {
T* ptr;
// Constructor
EngineHandle(T* p = nullptr) : ptr(p) {}
// Destructor
~EngineHandle() { if (ptr) DestroyFn(ptr); }
// DELETE Copy Constructor (Prevents accidental cloning)
EngineHandle(const EngineHandle&) = delete;
// DELETE Copy Assignment (Prevents accidental overwrites)
EngineHandle& operator=(const EngineHandle&) = delete;
// ALLOW Move Constructor (Safely transfer ownership)
EngineHandle(EngineHandle&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // Nullify the old pointer so it doesn't get destroyed
}
// Move Assignment using Swap
EngineHandle& operator=(EngineHandle&& other) noexcept {
T* temp = other.ptr;
other.ptr = ptr;
ptr = temp;
return *this;
}
// Implicit conversion operator
operator T* () const { return ptr; }
};
using WindowHandle = EngineHandle<window_t, window_destroy>;
using CompositorHandle = EngineHandle<compositor_t, compositor_destroy>;
using TextHandle = EngineHandle<text_t, text_destroy>;
using AudioHandle = EngineHandle<audio_t, audio_destroy>;
using AudioDataHandle = EngineHandle<uint8_t, audio_free_data>;
using ImageHandle = EngineHandle<uint32_t, image_free>;
// ---------------------------------------------------------------------------
// Script interpreter
// ---------------------------------------------------------------------------
// A command is an opcode plus a closure that does the actual work.
// Keeping the closure allows the command set to directly capture backend
// pointers without a fat context struct threaded through everything.
//
// The closure returns true when the interpreter should YIELD (stop executing
// commands this frame and wait). Returning false means continue immediately
// to the next command.
struct Command {
std::string opcode; // for debugging
std::function<bool()> execute;
};
enum class WaitKind {
NONE,
CLICK, // wait for mouse click or space
ANIM_ALL, // wait until compositor_is_animating() == false
ANIM_ELEM, // wait until a specific element stops animating
END, // script finished, keep rendering until told to quit
};
class Interpreter {
public:
std::vector<Command> script;
size_t pc = 0;
WaitKind waiting = WaitKind::NONE;
element_handle_t wait_elem = 0; // valid when waiting == ANIM_ELEM
// Callbacks into the engine, set before first tick
compositor_t* compositor = nullptr;
text_t* dialogue = nullptr;
text_t* name_box = nullptr;
// Advance the interpreter by one frame.
// clicked: true on the frame a left-click or spacebar press was detected.
// Returns true when the script has fully ended and the engine may quit.
bool tick(bool clicked) {
// ---- resolve current wait condition --------------------------------
switch (waiting) {
case WaitKind::CLICK:
if (clicked) {
// If the typewriter is still running, snap it and stay on
// this line - let the player see the full text first.
if (text_is_animating(dialogue)) {
text_show_all_current(dialogue);
return false;
}
waiting = WaitKind::NONE;
}
else {
return false;
}
break;
case WaitKind::ANIM_ALL:
if (compositor_is_animating(compositor))
return false;
waiting = WaitKind::NONE;
break;
case WaitKind::ANIM_ELEM:
if (compositor_is_element_animating(compositor, wait_elem))
return false;
waiting = WaitKind::NONE;
break;
case WaitKind::END:
return true;
case WaitKind::NONE:
break;
}
// ---- burn through commands until a yield ---------------------------
while (pc < script.size()) {
const Command& cmd = script[pc++];
bool yield = cmd.execute();
if (yield) return false;
}
// Ran off the end of the script without an explicit END.
waiting = WaitKind::END;
return true;
}
};
// ---------------------------------------------------------------------------
// Script builder helpers
//
// These push Command objects onto the script vector. They capture the
// interpreter and backend pointers by reference/value as appropriate.
// All helpers return void and operate on a passed-in script vector so the
// build function stays readable.
// ---------------------------------------------------------------------------
using Script = std::vector<Command>;
static void cmd_speaker(Script& s, text_t* name_box,
element_handle_t name_id, compositor_t* comp,
const char* speaker)
{
s.push_back({ "SPEAKER", [=]() -> bool {
text_clear(name_box);
if (speaker && speaker[0])
text_push(name_box, speaker, 0.0f, true);
compositor_mark_content_dirty(comp, name_id);
return false;
} });
}
static void cmd_line(Script& s, text_t* dialogue,
element_handle_t dialogue_id, compositor_t* comp,
const char* text, float speed = 30.0f)
{
s.push_back({ "LINE", [=]() -> bool {
text_clear(dialogue);
text_push(dialogue, text, speed, true);
compositor_mark_content_dirty(comp, dialogue_id);
return false;
} });
}
static void cmd_clear(Script& s, text_t* dialogue,
element_handle_t dialogue_id, compositor_t* comp)
{
s.push_back({ "CLEAR", [=]() -> bool {
text_clear(dialogue);
compositor_mark_content_dirty(comp, dialogue_id);
return false;
} });
}
static void cmd_wait_click(Script& s, Interpreter& interp)
{
s.push_back({ "WAIT_CLICK", [&interp]() -> bool {
interp.waiting = WaitKind::CLICK;
return true;
} });
}
static void cmd_wait_anim_all(Script& s, Interpreter& interp)
{
s.push_back({ "WAIT_ANIM_ALL", [&interp]() -> bool {
interp.waiting = WaitKind::ANIM_ALL;
return true;
} });
}
static void cmd_wait_anim_elem(Script& s, Interpreter& interp,
element_handle_t elem)
{
s.push_back({ "WAIT_ANIM_ELEM", [&interp, elem]() -> bool {
interp.waiting = WaitKind::ANIM_ELEM;
interp.wait_elem = elem;
return true;
} });
}
static void cmd_finish_all(Script& s, compositor_t* comp)
{
s.push_back({ "ANIM_FINISH_ALL", [=]() -> bool {
compositor_animation_finish_all(comp);
return false;
} });
}
static void cmd_anim_scale(Script& s, compositor_t* comp,
element_handle_t elem, float to,
float dur, easing_t ease, bool queue, bool parallel)
{
s.push_back({ "ANIM_SCALE", [=]() -> bool {
compositor_animate_scale(comp, elem, to, dur, ease, queue, parallel);
return false;
} });
}
static void cmd_anim_move(Script& s, compositor_t* comp,
element_handle_t elem,
int32_t x, int32_t y,
float dur, easing_t ease, bool queue, bool parallel)
{
s.push_back({ "ANIM_MOVE", [=]() -> bool {
compositor_animate_move(comp, elem, x, y, dur, ease, queue, parallel);
return false;
} });
}
static void cmd_anim_fade(Script& s, compositor_t* comp,
element_handle_t elem, float to_alpha,
float dur, easing_t ease, bool queue, bool parallel)
{
s.push_back({ "ANIM_FADE", [=]() -> bool {
compositor_animate_fade(comp, elem, to_alpha, dur, ease, queue, parallel);
return false;
} });
}
static void cmd_set_visible(Script& s, compositor_t* comp,
element_handle_t elem, bool visible)
{
s.push_back({ "ELEM_SHOW/HIDE", [=]() -> bool {
compositor_set_visible(comp, elem, visible);
return false;
} });
}
static void cmd_set_scale(Script& s, compositor_t* comp,
element_handle_t elem, float scale)
{
s.push_back({ "ELEM_SET_SCALE", [=]() -> bool {
compositor_set_scale(comp, elem, scale);
return false;
} });
}
static void cmd_set_alpha(Script& s, compositor_t* comp,
element_handle_t elem, float alpha)
{
s.push_back({ "ELEM_SET_ALPHA", [=]() -> bool {
compositor_set_alpha(comp, elem, alpha);
return false;
} });
}
static void cmd_audio_fade(Script& s, audio_t* audio,
audio_stream_id_t id,
float to_vol, float dur)
{
s.push_back({ "MUSIC_FADE", [=]() -> bool {
audio_animate_volume(audio, id, to_vol, dur);
return false;
} });
}
static void cmd_end(Script& s, Interpreter& interp)
{
s.push_back({ "END", [&interp]() -> bool {
interp.waiting = WaitKind::END;
return true;
} });
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main() {
srand((unsigned int)time(NULL));
const int WIDTH = 1280;
const int HEIGHT = 720;
const int CHAR_WIDTH = 400;
const int CHAR_HEIGHT = 600;
const int CENTER_X = WIDTH / 2;
const int CHAR_BASELINE_Y = HEIGHT - 140;
const int LEFT_CHAR_CENTER_X = 60 + (CHAR_WIDTH / 2);
const int RIGHT_CHAR_CENTER_X = WIDTH - 60 - (CHAR_WIDTH / 2);
const int OFFSCREEN_LEFT_X = LEFT_CHAR_CENTER_X - 660;
const int OFFSCREEN_RIGHT_X = RIGHT_CHAR_CENTER_X + 660;
// ---- window / compositor / audio ---------------------------------------
WindowHandle window(window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine"));
if (!window) { printf("Failed to create window\n"); return 1; }
CompositorHandle compositor(compositor_init(WIDTH, HEIGHT));
if (!compositor) { printf("Failed to initialize compositor\n"); return 1; }
AudioHandle audio(audio_init());
if (!audio) { printf("Failed to initialize audio\n"); return 1; }
audio_set_global_volume(audio, 1.0f);
typedef struct {
const char* title;
const char* composer;
const char* file;
} music_track_t;
static const music_track_t strauss_tracks[] = {
{"Freikugeln", "Johann Strauss II", "assets/Freikugeln.wav"},
{"Perpetuum Mobile", "Johann Strauss II", "assets/Perpetuum_Mobile.wav"},
{"Ohne Sorgen!", "Josef Strauss", "assets/Ohne_Sorgen!.wav"},
{"Auf Ferienreisen", "Josef Strauss", "assets/Auf_Ferienreisen.wav"},
{"Dynamiden", "Josef Strauss", "assets/Dynamiden.wav"}
};
int track_idx = rand() % (sizeof(strauss_tracks) / sizeof(strauss_tracks[0]));
const music_track_t* track = &strauss_tracks[track_idx];
int music_size, music_channels, music_rate, music_total_samples;
AudioDataHandle music_data(audio_load_madpcm(
track->file,
&music_size, &music_channels, &music_rate, &music_total_samples));
if (!music_data) { printf("Failed to load audio\n"); return 1; }
audio_stream_id_t music_id = audio_play(
audio, music_data, music_size, music_channels, music_rate, true);
// ---- assets / compositor elements -------------------------------------
ImageHandle bg(image_load_resized("assets/background.png", WIDTH, HEIGHT));
if (bg) {
element_handle_t bg_id = compositor_insert(
compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT);
compositor_set_alpha(compositor, bg_id, 1.0f);
}
ImageHandle textbox(image_load_resized("assets/textbox.png", WIDTH, 180));
if (textbox)
compositor_insert(compositor, textbox, WIDTH, 180,
0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT);
ImageHandle maksw_happy(image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT));
element_handle_t maksw_happy_id = compositor_insert(
compositor, maksw_happy, CHAR_WIDTH, CHAR_HEIGHT,
OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER);
ImageHandle maksw_neutral(image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT));
element_handle_t maksw_neutral_id = compositor_insert(
compositor, maksw_neutral, CHAR_WIDTH, CHAR_HEIGHT,
LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER);
compositor_set_visible(compositor, maksw_neutral_id, false);
ImageHandle wikor_img(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT));
element_handle_t wikor_id = compositor_insert(
compositor, wikor_img, CHAR_WIDTH, CHAR_HEIGHT,
OFFSCREEN_RIGHT_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER);
TextHandle name_text(text_init(300, 100));
text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f);
element_handle_t name_id = compositor_insert(
compositor, text_getfb(name_text),
300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT);
TextHandle dialogue_text(text_init(WIDTH - 140, 160));
text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f);
element_handle_t dialogue_id = compositor_insert(
compositor, text_getfb(dialogue_text),
WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT);
// mask: a wipe graphic, starts off-screen above, slides down to cover
ImageHandle mask(image_load_resized("assets/mask.png", 1280, 1080));
element_handle_t mask_id = compositor_insert(
compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT);
compositor_set_visible(compositor, mask_id, false);
// mask2: slides off downward to reveal the new scene
ImageHandle mask2(image_load_resized("assets/mask2.png", 1280, 1080));
element_handle_t mask2_id = compositor_insert(
compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT);
compositor_set_visible(compositor, mask2_id, false);
// ---- build the script -------------------------------------------------
//
// Shorthand references so the builder lines stay readable.
compositor_t* comp = compositor;
text_t* dlg = dialogue_text;
text_t* nmb = name_text;
element_handle_t dlg_id = dialogue_id;
Interpreter interp;
interp.compositor = comp;
interp.dialogue = dlg;
interp.name_box = nmb;
Script& S = interp.script;
// Convenience aliases so each script line stays short.
// All lambdas below capture by value (element handles, pointers) which is
// safe because all resources outlive the interpreter.
auto speaker = [&](const char* who) {
cmd_speaker(S, nmb, name_id, comp, who);
};
auto line = [&](const char* text, float speed = 30.0f) {
cmd_line(S, dlg, dlg_id, comp, text, speed);
};
auto clear = [&]() { cmd_clear(S, dlg, dlg_id, comp); };
auto wait_click = [&]() { cmd_wait_click(S, interp); };
auto wait_anim_all = [&]() { cmd_wait_anim_all(S, interp); };
auto wait_anim = [&](element_handle_t e) { cmd_wait_anim_elem(S, interp, e); };
auto finish_all = [&]() { cmd_finish_all(S, comp); };
auto anim_scale = [&](element_handle_t e, float to, float dur,
easing_t ease, bool q = false, bool p = false) {
cmd_anim_scale(S, comp, e, to, dur, ease, q, p);
};
auto anim_move = [&](element_handle_t e, int32_t x, int32_t y,
float dur, easing_t ease, bool q = false, bool p = false) {
cmd_anim_move(S, comp, e, x, y, dur, ease, q, p);
};
auto anim_fade = [&](element_handle_t e, float a, float dur,
easing_t ease, bool q = false, bool p = false) {
cmd_anim_fade(S, comp, e, a, dur, ease, q, p);
};
auto show = [&](element_handle_t e) { cmd_set_visible(S, comp, e, true); };
auto hide = [&](element_handle_t e) { cmd_set_visible(S, comp, e, false); };
auto scale = [&](element_handle_t e, float v) { cmd_set_scale(S, comp, e, v); };
auto alpha = [&](element_handle_t e, float v) { cmd_set_alpha(S, comp, e, v); };
// ------------------------------------------------------------------
// Scene 1 - maksw slides in, introduces the engine
// ------------------------------------------------------------------
anim_move(maksw_happy_id, CENTER_X, CHAR_BASELINE_Y,
1.5f, EASE_OUT_CUBIC, true);
anim_scale(maksw_happy_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC, /*queue=*/true, /*parallel=*/false);
speaker("maksw");
line("Hello and welcome to the Obwarzanek Narrative Engine demo!");
wait_click();
line("Today I'll present to you-");
wait_click();
anim_move(wikor_id, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.4f, EASE_OUT_CUBIC);
anim_move(maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20,
0.3f, EASE_OUT_CUBIC);
anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("wikor");
line("Yeah, hello to you too.");
wait_click();
const char* greetings[] = {
"Oh Wiktor, what a nice surprise!",
"Oh hey Wiktor, didn't expect to see you!",
"Wiktor! What brings you here?"
};
const char* responses[] = {
"No, I was just walking by.",
"Nah, just passing through.",
"Not really, just wandering."
};
int greeting_idx = rand() % 3;
int response_idx = rand() % 3;
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("maksw");
line(greetings[greeting_idx]);
wait_click();
line("...Do you need anything?");
wait_click();
anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("wikor");
line(responses[response_idx]);
wait_click();
line("Cool engine though.");
wait_click();
finish_all();
show(mask_id);
anim_move(mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC);
wait_anim(mask_id);
hide(maksw_happy_id);
show(maksw_neutral_id);
scale(maksw_neutral_id, 1.0f);
scale(wikor_id, 1.05f);
hide(mask_id);
show(mask2_id);
anim_move(mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC);
speaker("wikor");
clear();
wait_anim(mask2_id);
line("Anyways, how's your day going?");
wait_click();
// ------------------------------------------------------------------
// Scene 2 - neutral maksw, wikor is chatty
// ------------------------------------------------------------------
finish_all();
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("maksw");
line("You hear what I'm hearing?");
wait_click();
finish_all();
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("wikor");
line("What? That music?");
wait_click();
finish_all();
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("maksw");
char music_line[256];
snprintf(music_line, sizeof(music_line),
"Yep! %s by %s!", track->title, track->composer);
line(music_line);
wait_click();
finish_all();
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
speaker("wikor");
line("Sure thing...");
wait_click();
finish_all();
line("Like I know who he is.");
wait_click();
// ------------------------------------------------------------------
// Ending - fade to black, fade out music, then quit
// ------------------------------------------------------------------
finish_all();
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
// Reuse mask as a simple alpha-fade overlay (no positional wipe needed)
show(mask_id);
alpha(mask_id, 0.0f);
anim_fade(mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC);
cmd_audio_fade(S, audio, music_id, 0.0f, 0.6f);
wait_anim(mask_id); // wait until black, then exit
cmd_end(S, interp);
// ---- main loop --------------------------------------------------------
bool running = true;
auto last_frame_time = std::chrono::high_resolution_clock::now();
auto last_report_time = last_frame_time;
uint64_t frame_count = 0;
float highest_frame_time = 0.0f;
float frame_time_sum = 0.0f;
const std::chrono::duration<float> REPORT_INTERVAL = std::chrono::seconds(1);
bool mouse_was_down = false;
bool key_was_down = false;
while (running) {
if (window_pollevent(window)) running = false;
auto current_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> delta_duration = current_time - last_frame_time;
float dt = delta_duration.count();
last_frame_time = current_time;
// FPS reporting
frame_count++;
frame_time_sum += dt;
if (dt > highest_frame_time) highest_frame_time = dt;
std::chrono::duration<float> since_report = current_time - last_report_time;
if (since_report >= REPORT_INTERVAL) {
if (frame_count > 0) {
float fps = 1.0f / (frame_time_sum / (float)frame_count);
printf("FPS: %.2f (over %llu frames in %.2f seconds), highest frame time: %.2fms\n",
fps, frame_count, since_report.count(), highest_frame_time * 1000.0f);
}
frame_count = 0; frame_time_sum = 0.0f; highest_frame_time = 0.0f;
last_report_time = current_time;
}
// Edge-detect click and keypress
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
bool key_is_down = window_is_key_down(window, KEY_SPACE);
bool clicked = (mouse_is_down && !mouse_was_down) ||
(key_is_down && !key_was_down);
mouse_was_down = mouse_is_down;
key_was_down = key_is_down;
// Tick the interpreter
bool script_done = interp.tick(clicked);
if (script_done) running = false;
// Tick subsystems
compositor_tick(compositor, dt);
if (text_nextframe(name_text, dt)) compositor_mark_content_dirty(compositor, name_id);
if (text_nextframe(dialogue_text, dt)) compositor_mark_content_dirty(compositor, dialogue_id);
const uint32_t* fb = compositor_compose(compositor);
window_blit(window, fb);
audio_tick(audio, dt);
}
audio_stop(audio, music_id);
return 0;
}