Files
StillAlive-CLI/stillalive.c
T
2026-10-11 09:07:56 +00:00

1026 lines
31 KiB
C

/*
* 'Portal' outro recreation.
* Reproducing the 'Still Alive' terminal credits in pure c
* I really enjoyed this game and this ending!
* Made by: Sidharth "Siddhi" Sharma (sidharthify)
*/
#ifndef _WIN32
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include <signal.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <mmsystem.h>
#include <conio.h>
#include <io.h>
#ifndef STDIN_FILENO
#define STDIN_FILENO 0
#endif
#ifndef STDOUT_FILENO
#define STDOUT_FILENO 1
#endif
#ifndef STDERR_FILENO
#define STDERR_FILENO 2
#endif
#else
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#endif
#include "stillalive_data.h"
#define CHAR_DASH 0x2d
#define AUDIO_START_MS 6750
#define CREDITS_START_MS 9000
#define BLINK_MS 300
#define CREDIT_VELOCITY_MS 68.623562
#define MAX_GRID_ROWS 256
#define MAX_GRID_COLS 512
/*
* scaling modes for adapting to different terminal dimensions.
* aspect mode keeps the original four by three ratio and fills the height.
* fill mode stretches the boxes across all rows and columns.
* classic mode locks the window to the original ninety eight by thirty eight box.
*/
typedef enum {
SCALE_ASPECT = 0,
SCALE_FILL = 1,
SCALE_CLASSIC = 2,
SCALE_MAX
} scale_mode_t;
static scale_mode_t g_scale_mode = SCALE_ASPECT;
static volatile sig_atomic_t g_winch_flag = 0;
static bool g_raw_mode = false;
static bool g_running = true;
static bool g_paused = false;
#ifdef _WIN32
static DWORD g_orig_in_mode = 0;
static DWORD g_orig_out_mode = 0;
static UINT g_orig_cp = 0;
static UINT g_orig_out_cp = 0;
static bool g_audio_playing = false;
static void stop_audio(void) {
if (g_audio_playing) {
mciSendStringA("stop stillalive_bgm", NULL, 0, NULL);
mciSendStringA("close stillalive_bgm", NULL, 0, NULL);
g_audio_playing = false;
}
}
static void pause_audio(void) {
if (g_audio_playing) {
mciSendStringA("pause stillalive_bgm", NULL, 0, NULL);
}
}
static void resume_audio(void) {
if (g_audio_playing) {
mciSendStringA("resume stillalive_bgm", NULL, 0, NULL);
}
}
static void start_audio(const char *path, double start_sec) {
if (!path) return;
char full_path[MAX_PATH];
if (_fullpath(full_path, path, sizeof(full_path)) == NULL) {
strncpy(full_path, path, sizeof(full_path) - 1);
full_path[sizeof(full_path) - 1] = '\0';
}
char cmd[MAX_PATH + 64];
mciSendStringA("close stillalive_bgm", NULL, 0, NULL);
snprintf(cmd, sizeof(cmd), "open \"%s\" type mpegvideo alias stillalive_bgm", full_path);
if (mciSendStringA(cmd, NULL, 0, NULL) != 0) {
snprintf(cmd, sizeof(cmd), "open \"%s\" alias stillalive_bgm", full_path);
if (mciSendStringA(cmd, NULL, 0, NULL) != 0) {
return;
}
}
mciSendStringA("set stillalive_bgm time format milliseconds", NULL, 0, NULL);
uint32_t from_ms = (uint32_t)(start_sec * 1000.0);
snprintf(cmd, sizeof(cmd), "play stillalive_bgm from %u", from_ms);
mciSendStringA(cmd, NULL, 0, NULL);
g_audio_playing = true;
}
static BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
(void)ctrl_type;
g_running = false;
return TRUE;
}
#else
static struct termios g_orig_termios;
static pid_t g_audio_pid = 0;
static void stop_audio(void) {
if (g_audio_pid > 0) {
kill(g_audio_pid, SIGTERM);
waitpid(g_audio_pid, NULL, WNOHANG);
g_audio_pid = 0;
}
}
static void pause_audio(void) {
if (g_audio_pid > 0) {
kill(g_audio_pid, SIGSTOP);
}
}
static void resume_audio(void) {
if (g_audio_pid > 0) {
kill(g_audio_pid, SIGCONT);
}
}
#endif
static uint32_t g_credit_starts[NUM_CREDITS];
static uint32_t g_credit_durs[NUM_CREDITS];
static int g_current_art_id = ART_NONE;
static bool g_getting_faster = false;
static uint32_t g_next_shuffle_ms = 0;
static uint32_t g_shuffle_interval_ms = 5000;
static char g_grid[MAX_GRID_ROWS][MAX_GRID_COLS];
/*
* when exiting or interrupted we need to make sure the terminal is left
* in a clean usable state. that means showing the cursor again, returning
* from the alternate screen buffer, and restoring canonical echo mode.
* any child player spawned for audio is also cleaned up here.
*/
static void write_all(int fd, const void *buf, size_t count){
#ifdef _WIN32
(void)fd;
const char *p = (const char *)buf;
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
while (count > 0){
DWORD to_write = (count > 65535) ? 65535 : (DWORD)count;
DWORD written = 0;
if (!WriteFile(hOut, p, to_write, &written, NULL) || written == 0){
break;
}
p += written;
count -= (size_t)written;
}
#else
const char *p = (const char *)buf;
while (count > 0){
ssize_t n = write(fd, p, count);
if (n < 0){
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK){
struct timespec ts = { .tv_sec = 0, .tv_nsec = 500000 };
nanosleep(&ts, NULL);
continue;
}
break;
}
p += n;
count -= (size_t)n;
}
#endif
}
static void restore_terminal(void){
stop_audio();
printf("\033[?2025l\033[?7h\033[?25h\033[?1049l\033[0m");
fflush(stdout);
#ifdef _WIN32
if (g_raw_mode){
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (g_orig_in_mode != 0) {
SetConsoleMode(hIn, g_orig_in_mode);
}
if (g_orig_out_mode != 0) {
SetConsoleMode(hOut, g_orig_out_mode);
}
if (g_orig_cp != 0) {
SetConsoleCP(g_orig_cp);
}
if (g_orig_out_cp != 0) {
SetConsoleOutputCP(g_orig_out_cp);
}
g_raw_mode = false;
}
#else
if (g_raw_mode){
tcsetattr(STDIN_FILENO, TCSANOW, &g_orig_termios);
g_raw_mode = false;
}
#endif
}
static void handle_signal(int sig){
(void)sig;
g_running = false;
}
/*
* sigwinch fires whenever the user resizes their window or goes fullscreen.
* we record the event so the renderer can immediately issue a full wipe
* before rendering the newly scaled layout.
*/
#ifndef _WIN32
static void handle_winch(int sig) {
(void)sig;
g_winch_flag = 1;
}
#endif
/*
* raw mode turns off line buffering and echo so keypresses like spacebar
* or the quit key register immediately without waiting for enter.
*/
static void enable_raw_mode(void){
#ifdef _WIN32
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
g_orig_cp = GetConsoleCP();
g_orig_out_cp = GetConsoleOutputCP();
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
if (GetConsoleMode(hIn, &g_orig_in_mode)) {
DWORD in_mode = g_orig_in_mode;
in_mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT);
SetConsoleMode(hIn, in_mode);
}
if (GetConsoleMode(hOut, &g_orig_out_mode)) {
DWORD out_mode = g_orig_out_mode;
out_mode |= (ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
SetConsoleMode(hOut, out_mode);
}
g_raw_mode = true;
#else
if (tcgetattr(STDIN_FILENO, &g_orig_termios) != 0){
return;
}
struct termios raw = g_orig_termios;
raw.c_lflag &= ~(ECHO | ICANON | ISIG);
raw.c_iflag &= ~(IXON | ICRNL);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0) {
g_raw_mode = true;
}
#endif
}
static uint64_t get_time_ms(void){
#ifdef _WIN32
static LARGE_INTEGER freq;
static int freq_init = 0;
if (!freq_init) {
QueryPerformanceFrequency(&freq);
freq_init = 1;
}
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
return (uint64_t)((counter.QuadPart * 1000) / freq.QuadPart);
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
#endif
}
static void sleep_ms(uint32_t ms){
#ifdef _WIN32
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
static bool read_input_char(char *ch){
#ifdef _WIN32
if (_isatty(0)) {
if (_kbhit()) {
int c = _getch();
if (c == 0 || c == 0xE0) {
if (_kbhit()) _getch();
return false;
}
if (c == 27) {
if (_kbhit()) {
while (_kbhit()) {
int esc_ch = _getch();
if (esc_ch >= 0x40 && esc_ch <= 0x7E) break;
}
return false;
}
}
*ch = (char)c;
return true;
}
return false;
} else {
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
DWORD avail = 0;
if (PeekNamedPipe(hIn, NULL, 0, NULL, &avail, NULL) && avail > 0) {
DWORD read_bytes = 0;
if (ReadFile(hIn, ch, 1, &read_bytes, NULL) && read_bytes > 0) {
return true;
}
}
return false;
}
#else
char c = 0;
if (read(STDIN_FILENO, &c, 1) > 0) {
*ch = c;
return true;
}
return false;
#endif
}
static void get_terminal_size(int *rows, int *cols){
#ifdef _WIN32
CONSOLE_SCREEN_BUFFER_INFO csbi;
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (GetConsoleScreenBufferInfo(hOut, &csbi)) {
*cols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
*rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
} else {
*cols = 100;
*rows = 40;
}
#else
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0 || ws.ws_col == 0 || ws.ws_row == 0) {
*rows = 40;
*cols = 100;
} else {
*rows = ws.ws_row;
*cols = ws.ws_col;
}
#endif
if (*rows <= 0) *rows = 40;
if (*cols <= 0) *cols = 100;
}
/*
* calculate the cumulative typing timestamps for the credits roll.
* the velocity is roughly sixty eight milliseconds per character based on
* the original source engine scripts, which keeps the roll perfectly in sync
* with the track lyrics.
*/
static void init_credits_timeline(void) {
double cur = (double)CREDITS_START_MS;
for (int i = 0; i < NUM_CREDITS; i += 1){
g_credit_starts[i] = (uint32_t)cur;
int len = (int)strlen(g_credits[i]);
double d = (len == 0 ? 1.0 : (double)len) * CREDIT_VELOCITY_MS;
g_credit_durs[i] = (uint32_t)d;
cur += d;
}
}
static const char *find_audio_file(void){
static const char *candidates[] = {
#ifdef _WIN32
"res/song/stillalive.mp3",
"res/song/stillalive.m4a",
"res/song/stillalive.ogg",
#else
"res/song/stillalive.ogg",
"res/song/stillalive.mp3",
"res/song/stillalive.m4a",
#endif
NULL
};
for (int i = 0; candidates[i] != NULL; i += 1) {
#ifdef _WIN32
if (_access(candidates[i], 0) == 0){
return candidates[i];
}
#else
if (access(candidates[i], R_OK) == 0){
return candidates[i];
}
#endif
}
return NULL;
}
#ifndef _WIN32
/*
* rather than linking a heavy audio framework, we fork an external player.
* pipewire, pulseaudio, sox, alsa, and ffmpeg CLI tools are probed in order
* so the audio works out of the box on almost any linux or bsd install.
*/
static pid_t launch_audio_player(const char *path, double start_sec) {
if (!path){
return 0;
}
static const char *players[] = {
"pw\055play",
"play",
"ffplay",
"afplay",
"aplay",
"mpv",
NULL
};
pid_t pid = fork();
if (pid < 0) {
return 0;
}
if (pid == 0){
int devnull = open("/dev/null", O_RDWR);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
char sec_buf[32];
snprintf(sec_buf, sizeof(sec_buf), "%.2f", start_sec);
char mpv_start[64];
snprintf(mpv_start, sizeof(mpv_start), "\055\055start=%.2f", start_sec);
for (int i = 0; players[i] != NULL; i += 1){
const char *p = players[i];
if (strcmp(p, "ffplay") == 0) {
if (start_sec > 0.0){
char *args[] = {(char *)p, "\055nodisp", "\055autoexit", "\055loglevel", "quiet", "\055ss", sec_buf, (char *)path, NULL};
execvp(p, args);
} else {
char *args[] = {(char *)p, "\055nodisp", "\055autoexit", "\055loglevel", "quiet", (char *)path, NULL};
execvp(p, args);
}
} else if (strcmp(p, "mpv") == 0){
if (start_sec > 0.0) {
char *args[] = {(char *)p, "\055\055no\055video", "\055\055really\055quiet", mpv_start, (char *)path, NULL};
execvp(p, args);
} else {
char *args[] = {(char *)p, "\055\055no\055video", "\055\055really\055quiet", (char *)path, NULL};
execvp(p, args);
}
} else if (strcmp(p, "play") == 0) {
if (start_sec > 0.0){
char *args[] = {(char *)p, (char *)path, "trim", sec_buf, NULL};
execvp(p, args);
} else {
char *args[] = {(char *)p, (char *)path, NULL};
execvp(p, args);
}
} else {
char *args[] = {(char *)p, (char *)path, NULL};
execvp(p, args);
}
}
_exit(1);
}
return pid;
}
static void start_audio(const char *path, double start_sec) {
g_audio_pid = launch_audio_player(path, start_sec);
}
#endif
/*
* the main rendering engine.
* it sizes the boxes according to the active scaling mode, clears the canvas,
* draws ascii borders, types lyrics character by character, scrolls the credits,
* centers the ascii art, and writes the entire screen buffer to stdout.
* every line is wiped with an ansi erase code so resizing leaves no artifacts.
*/
static void render_screen(uint32_t elapsed_ms, int term_rows, int term_cols){
static int s_last_cols = 0;
static int s_last_rows = 0;
static scale_mode_t s_last_mode = (scale_mode_t)-1;
bool needs_clear = false;
if (g_winch_flag || term_cols != s_last_cols || term_rows != s_last_rows || g_scale_mode != s_last_mode) {
needs_clear = true;
g_winch_flag = 0;
s_last_cols = term_cols;
s_last_rows = term_rows;
s_last_mode = g_scale_mode;
}
int min_w = 98;
int min_h = 38;
int frame_w = min_w;
int frame_h = min_h;
if (g_scale_mode == SCALE_FILL){
frame_w = term_cols >= min_w ? term_cols - 2 : min_w;
frame_h = term_rows >= min_h ? term_rows - 2 : min_h;
} else if (g_scale_mode == SCALE_ASPECT) {
int avail_h = term_rows >= min_h ? term_rows - 2 : min_h;
int avail_w = term_cols >= min_w ? term_cols - 2 : min_w;
double ratio = 98.0 / 38.0;
int h = avail_h;
int w = (int)((double)h * ratio);
if (w > avail_w){
w = avail_w;
h = (int)((double)w / ratio);
}
if (w < min_w) {
w = min_w;
}
if (h < min_h){
h = min_h;
}
frame_w = w;
frame_h = h;
} else {
frame_w = min_w;
frame_h = min_h;
}
if (frame_h > MAX_GRID_ROWS) {
frame_h = MAX_GRID_ROWS;
}
if (frame_w >= MAX_GRID_COLS){
frame_w = MAX_GRID_COLS - 1;
}
int left_w = (int)((double)frame_w * 47.0 / 98.0);
if (left_w < 24) {
left_w = 24;
}
if (left_w > frame_w - 20){
left_w = frame_w - 20;
}
int gap = 2;
int right_x = left_w + gap;
int right_w = frame_w - right_x;
if (right_w < 20) {
right_w = 20;
}
int credits_h = (int)((double)frame_h * 17.0 / 38.0);
if (credits_h < 6){
credits_h = 6;
}
if (credits_h > frame_h - 10) {
credits_h = frame_h - 10;
}
for (int r = 0; r < frame_h; r += 1){
for (int c = 0; c < frame_w; c += 1) {
g_grid[r][c] = ' ';
}
g_grid[r][frame_w] = '\0';
}
/* left box borders */
for (int c = 0; c < left_w; c += 1){
g_grid[0][c] = (char)CHAR_DASH;
g_grid[frame_h - 1][c] = (char)CHAR_DASH;
}
for (int r = 1; r < frame_h - 1; r += 1) {
g_grid[r][0] = '|';
g_grid[r][left_w - 1] = '|';
}
/* right box borders */
for (int c = right_x; c < right_x + right_w; c += 1){
g_grid[0][c] = (char)CHAR_DASH;
g_grid[credits_h][c] = (char)CHAR_DASH;
}
for (int r = 1; r < credits_h; r += 1) {
g_grid[r][right_x] = '|';
g_grid[r][right_x + right_w - 1] = '|';
}
/* find active container */
int active_c = 0;
for (int i = 0; i < NUM_CONTAINERS; i += 1){
if (elapsed_ms >= g_containers[i].start_ms) {
active_c = i;
}
}
const lyric_container_t *c_ptr = &g_containers[active_c];
uint32_t rel_ms = elapsed_ms - (*c_ptr).start_ms;
/*
* toward the end of the song the terminal enters celebrate mode,
* shuffling random aperture science ascii illustrations at an
* accelerating rate until the track concludes.
*/
if ((*c_ptr).is_celebrate){
if (g_next_shuffle_ms == 0) {
g_next_shuffle_ms = elapsed_ms + g_shuffle_interval_ms;
g_current_art_id = rand() % NUM_ASCII_ARTS;
} else if (elapsed_ms >= g_next_shuffle_ms){
g_current_art_id = rand() % NUM_ASCII_ARTS;
if (g_getting_faster) {
if (g_shuffle_interval_ms > 89){
g_shuffle_interval_ms -= 39;
} else {
g_shuffle_interval_ms = 50;
}
}
g_next_shuffle_ms = elapsed_ms + g_shuffle_interval_ms;
}
} else {
for (int i = 0; i < (*c_ptr).item_count; i += 1){
const lyric_item_t *item = &(*c_ptr).items[i];
if (!(*item).is_br && rel_ms >= (*item).start_ms) {
if ((*item).art_id != ART_NONE){
g_current_art_id = (*item).art_id;
}
if ((*item).is_play_game) {
g_getting_faster = true;
}
}
}
}
/*
* typewriter calculation for the lyrics pane on the left.
* text strings are revealed progressively according to their item duration.
*/
int max_l_rows = frame_h - 2;
if (max_l_rows > 64){
max_l_rows = 64;
}
int max_l_cols = left_w - 3;
if (max_l_cols > 120) {
max_l_cols = 120;
}
char l_lines[64][128];
int l_line_lens[64];
for (int i = 0; i < 64; i += 1){
l_lines[i][0] = '\0';
l_line_lens[i] = 0;
}
int cur_r = 0;
int cur_c = 0;
for (int i = 0; i < (*c_ptr).item_count; i += 1) {
const lyric_item_t *item = &(*c_ptr).items[i];
if ((*item).is_br){
if (rel_ms >= (*item).start_ms) {
if (cur_r < max_l_rows - 1){
cur_r += 1;
cur_c = 0;
}
}
} else {
if (rel_ms >= (*item).start_ms){
const char *txt = (*item).text;
int len = (int)strlen(txt);
int total_steps = len + ((*item).append_br ? 1 : 0);
double step_dur = total_steps == 0 ? 0.0 : (double)(*item).dur_ms / (double)total_steps;
uint32_t dt = rel_ms - (*item).start_ms;
int steps = step_dur <= 0.0 ? total_steps : (int)((double)dt / step_dur);
int num_chars = steps < len ? steps : len;
for (int ch_idx = 0; ch_idx < num_chars; ch_idx += 1){
if (cur_c < max_l_cols) {
l_lines[cur_r][cur_c] = txt[ch_idx];
cur_c += 1;
l_lines[cur_r][cur_c] = '\0';
l_line_lens[cur_r] = cur_c;
}
}
if ((*item).append_br && steps >= len){
if (cur_r < max_l_rows - 1) {
cur_r += 1;
cur_c = 0;
}
} else if (!(*item).append_br && steps >= len && (*item).add_space) {
if (cur_c < max_l_cols){
l_lines[cur_r][cur_c] = ' ';
cur_c += 1;
l_lines[cur_r][cur_c] = '\0';
l_line_lens[cur_r] = cur_c;
}
}
}
}
}
/* copy lyrics lines into frame buffer with space padding */
for (int r = 0; r <= cur_r && r < max_l_rows; r += 1){
for (int c = 0; c < l_line_lens[r] && c < max_l_cols; c += 1) {
g_grid[r + 1][c + 2] = l_lines[r][c];
}
}
/* blinking underscore cursor */
bool blink_on = ((elapsed_ms / BLINK_MS) % 2) == 0;
if (blink_on && cur_r < max_l_rows && cur_c < max_l_cols){
g_grid[cur_r + 1][cur_c + 2] = '_';
}
/*
* the credits roll on the top right.
* past lines scroll upward while the latest credit types out at the bottom.
*/
int credit_cur = 0;
int c_rows = credits_h - 1;
if (elapsed_ms >= CREDITS_START_MS) {
while (credit_cur < NUM_CREDITS - 1 && elapsed_ms >= g_credit_starts[credit_cur + 1]){
credit_cur += 1;
}
for (int row_idx = 0; row_idx < c_rows - 1; row_idx += 1) {
int hist_k = credit_cur - ((c_rows - 1) - row_idx);
if (hist_k >= 0 && hist_k < NUM_CREDITS){
const char *ctext = g_credits[hist_k];
int clen = (int)strlen(ctext);
for (int c = 0; c < clen && (c + 2) < (right_w - 1); c += 1) {
g_grid[row_idx + 1][right_x + 2 + c] = ctext[c];
}
}
}
const char *act_text = g_credits[credit_cur];
int act_len = (int)strlen(act_text);
uint32_t dt = elapsed_ms - g_credit_starts[credit_cur];
double c_step_dur = act_len == 0 ? (double)g_credit_durs[credit_cur] : (double)g_credit_durs[credit_cur] / (double)act_len;
int act_chars = c_step_dur <= 0.0 ? act_len : (int)((double)dt / c_step_dur);
if (act_chars > act_len){
act_chars = act_len;
}
for (int c = 0; c < act_chars && (c + 2) < (right_w - 1); c += 1) {
g_grid[c_rows][right_x + 2 + c] = act_text[c];
}
if (blink_on && (act_chars + 2) < (right_w - 1)){
g_grid[c_rows][right_x + 2 + act_chars] = '_';
}
} else {
if (blink_on) {
g_grid[c_rows][right_x + 2] = '_';
}
}
/*
* ascii art sits inside the lower right compartment.
* we calculate centering offsets horizontally and vertically
* so drawings look well balanced regardless of window size.
*/
if (g_current_art_id >= 0 && g_current_art_id < NUM_ASCII_ARTS){
const ascii_art_t *art = &g_ascii_arts[g_current_art_id];
int art_avail_h = (frame_h - 1) - (credits_h + 1);
int art_max_w = 0;
for (int r = 0; r < (*art).line_count; r += 1) {
int len = (int)strlen((*art).lines[r]);
if (len > art_max_w){
art_max_w = len;
}
}
int art_pad_y = 0;
if (art_avail_h > (*art).line_count) {
art_pad_y = (art_avail_h - (*art).line_count) / 2;
}
int art_pad_x = 2;
if (right_w > art_max_w){
art_pad_x = (right_w - art_max_w) / 2;
}
for (int r = 0; r < (*art).line_count && (art_pad_y + r) < art_avail_h; r += 1) {
const char *aline = (*art).lines[r];
int alen = (int)strlen(aline);
int dest_r = credits_h + 1 + art_pad_y + r;
if (dest_r >= frame_h - 1){
break;
}
for (int c = 0; c < alen && (art_pad_x + c) < (right_w - 1); c += 1) {
g_grid[dest_r][right_x + art_pad_x + c] = aline[c];
}
}
}
/* center the rendered frame on the screen */
int off_x = 0;
int off_y = 0;
if (term_cols > frame_w){
off_x = (term_cols - frame_w) / 2;
}
if (term_rows > frame_h) {
off_y = (term_rows - frame_h) / 2;
}
static char out_buf[262144];
int pos = 0;
if (needs_clear){
pos += snprintf(out_buf + pos, sizeof(out_buf) - pos, "\033[?2025h\033[2J\033[H\033[38;2;222;190;95m");
} else {
pos += snprintf(out_buf + pos, sizeof(out_buf) - pos, "\033[?2025h\033[38;2;222;190;95m");
}
/*
* direct cursor positioning per line without newline characters.
* positioning with target row and column avoids redrawing blanks
* and avoids the line erasing escape that causes terminal tearing.
*/
for (int r = 0; r < frame_h; r += 1) {
int target_row = off_y + r + 1;
if (target_row > term_rows) {
break;
}
if (off_x > 0){
pos += snprintf(out_buf + pos, sizeof(out_buf) - pos, "\033[%d;%dH%s", target_row, off_x + 1, g_grid[r]);
} else {
pos += snprintf(out_buf + pos, sizeof(out_buf) - pos, "\033[%d;1H%s", target_row, g_grid[r]);
}
}
pos += snprintf(out_buf + pos, sizeof(out_buf) - pos, "\033[?2025l");
if (pos > 0){
write_all(STDOUT_FILENO, out_buf, (size_t)pos);
}
}
int main(int argc, char *argv[]) {
bool autoplay = false;
bool no_audio = false;
uint64_t initial_offset_ms = 0;
for (int i = 1; i < argc; i += 1){
if (strcmp(argv[i], "autoplay") == 0 || strcmp(argv[i], "\055a") == 0 || strcmp(argv[i], "\055\055autoplay") == 0) {
autoplay = true;
} else if (strcmp(argv[i], "silent") == 0 || strcmp(argv[i], "no\055audio") == 0 || strcmp(argv[i], "\055s") == 0){
no_audio = true;
} else if (strncmp(argv[i], "start=", 6) == 0) {
initial_offset_ms = (uint64_t)strtoull(argv[i] + 6, NULL, 10);
autoplay = true;
} else if (strcmp(argv[i], "classic") == 0){
g_scale_mode = SCALE_CLASSIC;
} else if (strcmp(argv[i], "fill") == 0) {
g_scale_mode = SCALE_FILL;
} else if (strcmp(argv[i], "aspect") == 0 || strcmp(argv[i], "fit") == 0){
g_scale_mode = SCALE_ASPECT;
} else if (strcmp(argv[i], "help") == 0 || strcmp(argv[i], "\055h") == 0 || strcmp(argv[i], "\055\055help") == 0) {
printf("usage: %s [autoplay] [silent] [start=<ms>] [aspect|fill|classic]\n", argv[0]);
printf("controls:\n");
printf(" space : pause / resume\n");
printf(" s : cycle scaling mode (aspect / fill / classic)\n");
printf(" r : restart\n");
printf(" q/esc : quit\n");
return 0;
}
}
srand((unsigned int)time(NULL));
init_credits_timeline();
#ifdef _WIN32
SetConsoleCtrlHandler(console_ctrl_handler, TRUE);
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
#else
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
signal(SIGWINCH, handle_winch);
#endif
atexit(restore_terminal);
enable_raw_mode();
printf("\033[?1049h\033[?25l\033[?7l\033[2J");
fflush(stdout);
const char *audio_path = find_audio_file();
/* initial boot screen */
if (!autoplay){
printf("\033[H\033[38;2;222;190;95m");
printf("\n");
printf(" Loading Aperture Science Genetic Lifeform and Disk Operating System Singing module...\n\n");
printf(" FULL SCREEN RECOMMENDED!\n\n");
printf(" [ Press SPACE or ENTER to begin ]\n");
printf(" [ Press 's' to cycle scaling mode ]\n");
printf(" [ Press 'q' to quit ]\n");
fflush(stdout);
while (g_running) {
char ch = 0;
if (read_input_char(&ch)){
if (ch == 'q' || ch == 'Q') {
return 0;
}
if (ch == 's' || ch == 'S'){
g_scale_mode = (g_scale_mode + 1) % SCALE_MAX;
}
if (ch == ' ' || ch == '\n' || ch == '\r') {
break;
}
}
sleep_ms(15);
}
}
printf("\033[2J");
fflush(stdout);
g_winch_flag = 1;
uint64_t start_time = get_time_ms() - initial_offset_ms;
uint64_t pause_start = 0;
bool audio_started = false;
char ch_old = 0;
char ch = 0;
while (g_running){
/* check user input */
ch_old = ch;
if (read_input_char(&ch)) {
if (ch == 'q' || ch == 'Q'){
break;
} else if (ch == 's' || ch == 'S') {
g_scale_mode = (g_scale_mode + 1) % SCALE_MAX;
g_winch_flag = 1;
} else if (ch == ' '){
g_paused = !g_paused;
if (g_paused) {
pause_start = get_time_ms();
pause_audio();
} else {
uint64_t p_dur = get_time_ms() - pause_start;
start_time += p_dur;
resume_audio();
}
} else if (ch == 'r' || ch == 'R'){
stop_audio();
start_time = get_time_ms();
audio_started = false;
g_current_art_id = ART_NONE;
g_getting_faster = false;
g_next_shuffle_ms = 0;
g_shuffle_interval_ms = 5000;
g_paused = false;
g_winch_flag = 1;
}
}
if (ch == ch_old && ch == 27) {
break;
}
if (!g_paused) {
uint64_t now = get_time_ms();
uint32_t elapsed = (uint32_t)(now - start_time);
/* launch audio at exact sync timestamp */
if (!no_audio && !audio_started && elapsed >= AUDIO_START_MS){
double seek_s = 0.0;
if (elapsed > AUDIO_START_MS) {
seek_s = (double)(elapsed - AUDIO_START_MS) / 1000.0;
}
start_audio(audio_path, seek_s);
audio_started = true;
}
int term_rows = 40;
int term_cols = 100;
get_terminal_size(&term_rows, &term_cols);
render_screen(elapsed, term_rows, term_cols);
}
sleep_ms(16);
}
return 0; // finally
}