1// TEST YOUR (TTY) MIGHT: DOOM FIRE!
2// (c) 2022, 2023, 2024, 2025 const void*
3//
4// Please see GPL3 license at bottom of source.
5//
6const builtin = @import("builtin");
7const std = @import("std");
8
9const allocator = std.heap.page_allocator;
10
11var g_tty_win: win32.HANDLE = undefined;
12
13///////////////////////////////////
14// Tested on M1 osx15.3, Windows 10 on Intel + Artix Linux.
15// fast - vs code terminal
16// slow - Terminal.app
17///////////////////////////////////
18
19// TODO
20// - How can we better offer cross platform support for multiple bit architectures?
21// Currently memory is fixed w/x64 unsigned integers to support high res displays.
22// This should probably be a comptime setting so that the integer size can be identified
23// based on platform (x64 vs x86 vs ... ).
24// - windows kernel32 *W API expect UTF16; this is very difficult as zig uses UTF8. Converting from utf8 to uf16 seems like
25// a lot of work, so for now, windows doesn't get fancy UTF8 glyphs.
26
27// Windows Notes
28// - Bugs are my own (const void*)
29//
30// - Windows Console Info contains two key pieces of information - the size, which is not the WINDOW SIZE, but the BUFFER size.
31// For rendering graphics, we do not want to use the buffer size--while good to know, it is unhelpful. Windows Console Info
32// also includes the window coordinates, assumably the coordinates of the buffer that is displayed; this means, to get the
33// potential terminal size, we have to calculate the width and height of the view buffer. I am picturing a massive canvas,
34// the buffer, and the window size is a 3x5" notecard overlay of that canvas. We don't need more much more information,
35// but it is interesting to think how scrollbars could impact the display.
36//
37// - Windows did not seem to enjoy newline characters - \n is not good.
38//
39// When the console was allowed to treat \n as a newline character, it did not appear as if the ansi cursor movement sequences
40// were respected, with some sort of infinite scroll. This may have been due to something else entirely.
41//
42// When the console was told to ignore newline characters, now ... cursor movement worked, but now ... \n was totally
43// ignored, as advertised.
44//
45// No matter. We craft our own, ANSI equivalent of new line:
46// 1. Fill the end of the line with the current color
47// 2. Go to the first character of the next line, which we do by moving the cursor down one line, then to the first character.
48//
49// IMPACT: Never use \n in a string, always use emit(nl)
50//
51// - Windows seems to want to use UTF16 to render UTF8 glyphs; while the "Ansi" WriteConsoleA function accepts
52// utf8 strings, which zig groks natively; sadly, WriteConsoleA seems to only render Ansi characters. the "Wide"
53// WriteConsoleW function accepts utf16 strings, however zig uses utf8 strings. I am unsure what code page to use
54// for ut16 outputs...is CP_UTF8 good enough? I am shelving the complexities of this problem...for now.
55//
56
57///////////////////////////////////
58// credits / helpful articles / inspirations
59///////////////////////////////////
60
61// doom fire - https://github.com/filipedeschamps/doom-fire-algorithm
62// - https://github.com/fabiensanglard/DoomFirePSX/blob/master/flames.html
63// color layout - https://en.wikipedia.org/wiki/ANSI_escape_code
64// ansi codes - http://xfree86.org/current/ctlseqs.html
65// str, zig - https://www.huy.rocks/everyday/01-04-2022-zig-strings-in-5-minutes
66// emit - https://zig.news/kristoff/where-is-print-in-zig-57e9
67// term sz, zig - https://github.com/jessrud/zbox/blob/master/src/prim.zig
68// osx term sz - https://github.com/sindresorhus/macos-term-size/blob/main/term-size.c
69// px char - https://github.com/cronvel/terminal-kit
70// win - https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
71
72///////////////////////////////////
73// zig hints
74///////////////////////////////////
75// `try` everywhere and !<return type>
76//
77// External Windows references are 100% contained inside win32 structure.
78//
79// cross platform constants: const CONSTANT = if (builtin.os.tag == .windows ) <windows constant> else <linux/macos constant>
80//
81// cross platform functions: OS type is function suffix
82// pub fn someFuncWin() !void { ... Windows specific logic ... }
83// pub fn someFuncLinux() !void { ... Linux/MacOS specific logic... }
84// pub fn someFunc() !void { return if (buildin.os.tag == .windows) try someFuncWin() else try someFuncLinux(); }
85//
86
87///////////////////////////////////
88// zig helpers
89///////////////////////////////////
90
91//// consts, vars, settings
92var rand: std.Random = undefined;
93
94//// functions
95
96// seed & prep for rng
97pub fn initRNG() void {
98 //rnd setup -- seed with timestamp for sufficient entropy
99 const instant = std.time.Instant.now() catch {
100 // Fallback to a compile-time seed if time is unavailable
101 var prng = std.Random.DefaultPrng.init(0xDEADBEEF);
102 rand = prng.random();
103 return;
104 };
105 // Use the timestamp's nsec component as seed
106 const seed: u64 = if (builtin.os.tag == .windows)
107 instant.timestamp
108 else
109 @as(u64, @intCast(instant.timestamp.sec)) *% std.time.ns_per_s +% @as(u64, @intCast(instant.timestamp.nsec));
110 var prng = std.Random.DefaultPrng.init(seed);
111 rand = prng.random();
112}
113
114// print - uses libc write for cross-platform stdout writing
115pub fn emit(s: []const u8) !void {
116 if (builtin.os.tag == .windows) {
117 var sz: win32.DWORD = 0;
118
119 const rv = win32.WriteConsoleA(g_tty_win, s.ptr, @intCast(s.len), &sz, undefined);
120 if (rv == 0) {
121 return;
122 }
123 return;
124 } else {
125 // Use libc write directly since we're linking libc
126 _ = std.c.write(std.posix.STDOUT_FILENO, s.ptr, s.len);
127 return;
128 }
129}
130
131// sleep helper - takes seconds and nanoseconds
132fn sleep(sec: isize, nsec: isize) void {
133 const ts = std.c.timespec{ .sec = sec, .nsec = nsec };
134 _ = std.c.nanosleep(&ts, null);
135}
136
137// format a string then print
138pub fn emitFmt(comptime s: []const u8, args: anytype) !void {
139 const t = try std.fmt.allocPrint(allocator, s, args);
140 defer allocator.free(t);
141 try emit(t);
142}
143
144///////////////////////////////////
145// TTY/Terminal Helpers
146///////////////////////////////////
147
148//// Settings
149
150// Get this value from libc.
151const TIOCGWINSZ = std.c.T.IOCGWINSZ; // ioctl flag
152
153//term size
154const TermSz = struct { height: usize, width: usize };
155var term_sz: TermSz = .{ .height = 0, .width = 0 }; // set via initTermSz
156
157//ansi escape codes
158const esc = "\x1B";
159const csi = esc ++ "[";
160
161const line_prior = csi ++ "1F"; //prior line
162const line_next = csi ++ "1E"; //next line
163const line_start = csi ++ "1G"; //carriage return
164const line_new = line_clear_to_eol ++ line_next ++ line_start; //new line
165const line_up = line_prior ++ line_start; //pup one
166const nl = if (builtin.os.tag == .windows) line_new else "\n";
167
168const cursor_save = esc ++ "7"; // or csi++s
169const cursor_load = esc ++ "8"; // or csi++u
170
171const cursor_show = csi ++ "?25h"; //h=high
172const cursor_hide = csi ++ "?25l"; //l=low
173const cursor_home = csi ++ "1;1H"; //1,1
174
175const screen_reset = csi ++ "!p";
176const screen_clear = csi ++ "2J";
177const screen_buf_on = csi ++ "?1049h"; //h=high
178const screen_buf_off = csi ++ "?1049l"; //l=low
179
180const char_set_ascii = esc ++ "(B"; // ascii borders
181const char_set_ansi = esc ++ "(0"; // ansi aka DEC borders
182
183const line_clear_to_eol = csi ++ "0K";
184
185const color_reset = csi ++ "0m";
186const color_fg = "38;5;";
187const color_bg = "48;5;";
188
189const color_fg_def = csi ++ color_fg ++ "15m"; // white
190const color_bg_def = csi ++ color_bg ++ "0m"; // black
191const color_def = color_bg_def ++ color_fg_def;
192const color_italic = csi ++ "3m";
193const color_not_italic = csi ++ "23m";
194
195const term_on = screen_buf_on ++ cursor_hide ++ cursor_home ++ color_def ++ screen_clear;
196const term_off = screen_buf_off ++ cursor_show ++ nl;
197
198const sep = if (builtin.os.tag == .windows) "|" else "▏";
199
200//colors
201const MAX_COLOR = 256;
202const LAST_COLOR = MAX_COLOR - 1;
203
204var fg: [MAX_COLOR][]u8 = undefined;
205var bg: [MAX_COLOR][]u8 = undefined;
206
207//// functions
208
209// cache fg/bg ansi codes
210pub fn initColor() !void {
211 var color_idx: u32 = 0;
212 while (color_idx < MAX_COLOR) : (color_idx += 1) {
213 fg[color_idx] = try std.fmt.allocPrint(allocator, "{s}38;5;{d}m", .{ csi, color_idx });
214 bg[color_idx] = try std.fmt.allocPrint(allocator, "{s}48;5;{d}m", .{ csi, color_idx });
215 }
216}
217
218//todo - free bg/fg color ache
219// defer freeColor(); just too lazy right now.
220
221pub fn getTermSzWin() !TermSz {
222 //Microsoft Windows Case
223 var info: win32.CONSOLE_SCREEN_BUFFER_INFO = .{ .dwSize = .{ .X = 0, .Y = 0 }, .dwCursorPosition = .{ .X = 0, .Y = 0 }, .wAttributes = 0, .srWindow = .{ .Left = 0, .Top = 0, .Right = 0, .Bottom = 0 }, .dwMaximumWindowSize = .{ .X = 0, .Y = 0 } };
224
225 if (0 == win32.GetConsoleScreenBufferInfo(g_tty_win, &info)) switch (std.os.windows.kernel32.GetLastError()) {
226 else => |e| return std.os.windows.unexpectedError(e),
227 };
228
229 return TermSz{
230 .height = @intCast(info.srWindow.Bottom - info.srWindow.Top + 1),
231 .width = @intCast(info.srWindow.Right - info.srWindow.Left + 1),
232 };
233}
234
235pub fn getTermSzLinux() !TermSz {
236 //Linux-MacOS Case
237
238 //base case - invoked from cmd line
239 const tty_nix = std.posix.STDOUT_FILENO;
240 var winsz = std.c.winsize{ .col = 0, .row = 0, .xpixel = 0, .ypixel = 0 };
241 const rv = std.c.ioctl(tty_nix, TIOCGWINSZ, @intFromPtr(&winsz));
242 const err = std.posix.errno(rv);
243
244 if (rv >= 0) {
245 if (winsz.row == 0 or winsz.col == 0) { // ltty IOCTL failed ie in lldb
246 //alt case - invoked from inside lldb or terminal size unavailable
247 //Return a sensible default if we can't get terminal size
248 return TermSz{ .height = 24, .width = 80 };
249 } else {
250 return TermSz{ .height = winsz.row, .width = winsz.col };
251 }
252 } else {
253 std.process.exit(0);
254 //TODO this is a pretty terrible way to handle issues...
255 return std.posix.unexpectedErrno(err);
256 }
257}
258
259//get terminal size
260pub fn getTermSz() !TermSz {
261 return if (builtin.os.tag == .windows) try getTermSzWin() else try getTermSzLinux();
262}
263
264pub fn initTermSize() !void {
265 term_sz = try getTermSz();
266}
267
268// Windows specific terminal initialization
269pub fn initTermWin() !void {
270 // credit: n-prat ( https://github.com/const-void/DOOM-fire-zig/issues/17#issuecomment-2587481269 )
271 var consoleMode: win32.DWORD = 0;
272
273 // pull tty from std.os.windows
274 g_tty_win = std.os.windows.GetStdHandle(win32.STD_OUTPUT_HANDLE) catch return std.os.windows.unexpectedError(std.os.windows.kernel32.GetLastError());
275
276 //get console mode
277 if (0 == win32.GetConsoleMode(g_tty_win, &consoleMode)) switch (std.os.windows.kernel32.GetLastError()) {
278 else => |e| return std.os.windows.unexpectedError(e),
279 };
280
281 // activate ANSI power!
282 consoleMode |= win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING | win32.DISABLE_NEWLINE_AUTO_RETURN;
283 //win32.ENABLE_PROCESSED_OUTPUT does not appear to be needed
284
285 // apply updated consoleMode to console
286 if (0 == win32.SetConsoleMode(g_tty_win, consoleMode)) switch (std.os.windows.kernel32.GetLastError()) {
287 else => |e| return std.os.windows.unexpectedError(e),
288 };
289
290 // todo - UINT WINAPI GetConsoleCP(void); https://learn.microsoft.com/en-us/windows/console/getconsolecp
291 // g_cur_consolecp = win32.GetConsoleCP();
292 // then, in complete, restore code page.
293
294 // enable UTF8
295 if (0 == win32.SetConsoleOutputCP(win32.CP_UTF8)) switch (std.os.windows.kernel32.GetLastError()) {
296 else => |e| return std.os.windows.unexpectedError(e),
297 };
298}
299
300pub fn initTerm() !void {
301 try initColor();
302
303 if (builtin.os.tag == .windows) {
304 try initTermWin();
305 }
306
307 try initTermSize();
308 initRNG();
309
310 try emit(term_on);
311 if (builtin.os.tag == .windows) {
312 //try emit(csi ++ "0;0r");
313 try emit(char_set_ascii);
314 }
315}
316
317// initTerm(); defer complete();
318pub fn complete() !void {
319 //todo -- free colors
320 //todo if win, completeWin() -> restore code page
321 try emit(term_off);
322 try emit("Complete!" ++ nl);
323}
324
325/////////////////
326// doom-fire
327/////////////////
328
329pub fn pause() !void {
330 //todo - poll / read a keystroke w/out echo, \n etc
331
332 try emit(color_reset);
333 try emit("Press return to continue...");
334 var buffer: [1]u8 = undefined;
335 _ = std.posix.read(std.posix.STDIN_FILENO, buffer[0..]) catch undefined;
336 const b = buffer[0];
337
338 if (b == 'q') {
339 //exit cleanly
340 try complete();
341 std.process.exit(0);
342 }
343}
344
345/// Part I - Terminal Size Check
346/// showTermCap() needs about 120x22; if screen is too small, give user a chance to abort and try again.
347///
348/// no biggie if they don't.
349///
350
351// do nothing if term sz is big enough
352pub fn checkTermSz() !void {
353 const min_w = 120;
354 const min_h = 22;
355 var w_ok = true;
356 var h_ok = true;
357
358 // chk cur < min
359 if (term_sz.width < min_w) {
360 w_ok = false;
361 }
362 if (term_sz.height < min_h) {
363 h_ok = false;
364 }
365
366 if (w_ok and h_ok) {
367 return;
368 } else {
369 //screen is too small
370
371 //red text
372 try emit(fg[9]);
373
374 //check conditions
375 if (w_ok and !h_ok) {
376 try emitFmt("Screen may be too short - height is {d} and need {d}.", .{ term_sz.height, min_h });
377 } else if (!w_ok and h_ok) {
378 try emitFmt("Screen may be too narrow - width is {d} and need {d}.", .{ term_sz.width, min_w });
379 } else if (term_sz.width == 0) {
380 // IOCTL succesfully failed! We believe the call to retrieve terminal dimensions succeeded,
381 // however our structure is zero.
382 try emit(bg[1]);
383 try emit(fg[15]);
384 try emitFmt("Call to retreive terminal dimensions may have failed" ++ nl ++
385 "Width is {d} (ZERO!) and we need {d}." ++ nl ++
386 "We will allocate 0 bytes of screen buffer, resulting in immediate failure.", .{ term_sz.width, min_w });
387 try emit(color_reset);
388 } else if (term_sz.height == 0) {
389 // IOCTL succesfully failed! We believe the call to retrieve terminal dimensions succeeded,
390 // however our structure is zero.
391 try emit(bg[1]);
392 try emit(fg[15]);
393 try emitFmt("Call to retreive terminal dimensions may have failed" ++ nl ++
394 "Height is {d} (ZERO!) and we need {d}." ++ nl ++
395 "We will allocate 0 bytes of screen buffer, resulting in immediate failure.", .{ term_sz.height, min_h });
396 try emit(color_reset);
397 } else {
398 try emitFmt("Screen is too small - have {d} x {d} and need {d} x {d}", .{ term_sz.width, term_sz.height, min_w, min_h });
399 }
400
401 try emit(nl);
402 try emit(nl);
403
404 //warn user w/white on red
405 try emit(bg[1]);
406 try emit(fg[15]);
407 try emit("There may be rendering issues on the next screen; to correct, <q><enter>, resize and try again.");
408 try emit(line_clear_to_eol);
409 try emit(color_reset);
410 try emit(nl ++ nl ++ "Continue?" ++ nl ++ nl);
411
412 //assume ok...pause will exit for us.
413 try pause();
414
415 //clear all the warning text and keep on trucking!
416 try emit(color_reset);
417 try emit(cursor_home);
418 try emit(screen_clear);
419 }
420}
421
422/// Part II - Show terminal capabilities
423///
424/// Since user terminals vary in capabilities, handy to have a screen that renders ACTUAL colors
425/// and exercises various terminal commands prior to DOOM fire.
426///
427pub fn showTermSz() !void {
428 //todo - show os, os ver, zig ver
429 try emit(color_def); // this should probably be windows only
430 try emitFmt("Screen size: {d}w x {d}h", .{ term_sz.width, term_sz.height });
431 try emit(nl); //window doesn't appear to like \n
432 try emit(nl);
433}
434
435pub fn showLabel(label: []const u8) !void {
436 try emit(color_def);
437 try emitFmt("{s}{s}:", .{ color_def, label });
438 try emit(nl);
439}
440
441pub fn showStdColors() !void {
442 try showLabel("Standard colors");
443
444 //first 8 colors (standard)
445 try emit(fg[15]);
446 var color_idx: u8 = 0;
447 while (color_idx < 8) : (color_idx += 1) {
448 try emit(bg[color_idx]);
449 if (color_idx == 7) {
450 try emit(fg[0]);
451 }
452 try emitFmt("{s} {d:2} ", .{ sep, color_idx });
453 }
454 try emit(color_def);
455 try emit(nl);
456
457 //next 8 colors ("hilight")
458 try emit(fg[15]);
459 while (color_idx < 16) : (color_idx += 1) {
460 try emit(bg[color_idx]);
461 if (color_idx == 15) {
462 try emit(fg[0]);
463 }
464 try emitFmt("{s} {d:2} ", .{ sep, color_idx });
465 }
466 try emit(color_def);
467 try emit(nl);
468 try emit(nl);
469}
470
471pub fn show216Colors() !void {
472 try showLabel("216 colors");
473
474 var color_addendum: u8 = 0;
475 var bg_idx: u8 = 0;
476 var fg_idx: u8 = 0;
477
478 //show remaining of colors in 6 blocks of 6x6
479
480 // 6 rows of color
481 var color_shift: u8 = 0;
482 while (color_shift < 6) : (color_shift += 1) {
483 color_addendum = color_shift * 36 + 16;
484
485 // colors are pre-organized into blocks
486 var color_idx: u8 = 0;
487 while (color_idx < 36) : (color_idx += 1) {
488 bg_idx = color_idx + color_addendum;
489
490 // invert color id for readability
491 if (color_idx > 17) {
492 fg_idx = 0;
493 } else {
494 fg_idx = 15;
495 }
496
497 // display color
498 try emit(bg[bg_idx]);
499 try emit(fg[fg_idx]);
500 try emitFmt("{d:3}", .{bg_idx});
501 }
502 try emit(color_def);
503 try emit(nl);
504 }
505 try emit(color_def);
506 try emit(nl);
507}
508
509pub fn showGrayscale() !void {
510 try showLabel("Grayscale");
511
512 var fg_idx: u8 = 15;
513 try emit(fg[fg_idx]);
514
515 var bg_idx: u32 = 232;
516 while (bg_idx < 256) : (bg_idx += 1) {
517 if (bg_idx > 243) {
518 fg_idx = 0;
519 try emit(fg[fg_idx]);
520 }
521
522 try emit(bg[bg_idx]);
523 try emitFmt("{s}{d} ", .{ sep, bg_idx });
524 }
525 try emit(color_def);
526 try emit(nl);
527
528 //cleanup
529 try emit(color_def);
530 try emit(nl);
531}
532
533pub fn scrollMarquee() !void {
534 //marquee - 4 lines of yellowish background
535 const bg_idx: u8 = 222;
536 const marquee_row = if (builtin.os.tag == .windows) nl else line_clear_to_eol ++ nl;
537 const marquee_bg = marquee_row ++ marquee_row ++ marquee_row ++ marquee_row;
538
539 //init marquee background
540 try emit(cursor_save); // save character position
541 try emit(bg[bg_idx]); // set yellow background
542 try emit(marquee_bg); // paint four rows
543
544 //quotes - will confirm animations are working on current terminal
545 const txt = [_][]const u8{ " Things move along so rapidly nowadays that people saying " ++ color_italic ++ "It can't be done" ++ color_not_italic ++ " are always being interrupted", " by somebody doing it. " ++ color_italic ++ "-- Puck, 1902" ++ color_not_italic, " Test your might!", " " ++ color_italic ++ "-- Mortal Kombat" ++ color_not_italic, " How much is the fish?", " " ++ color_italic ++ "-- Scooter" ++ color_not_italic };
546 const txt_len: u8 = txt.len / 2; // print two rows at a time
547
548 //fade txt in and out
549 const fade_seq = [_]u8{ 222, 221, 220, 215, 214, 184, 178, 130, 235, 58, 16 };
550 const fade_len: u8 = fade_seq.len;
551
552 var fade_idx: u8 = 0;
553 var txt_idx: u8 = 0;
554
555 while (txt_idx < txt_len) : (txt_idx += 1) {
556 //fade in
557 fade_idx = 0;
558 while (fade_idx < fade_len) : (fade_idx += 1) {
559 //reset to 1,1 of marquee
560 try emit(cursor_load);
561 try emit(bg[bg_idx]);
562 try emit(nl);
563
564 //print marquee txt
565 try emit(fg[fade_seq[fade_idx]]);
566 try emit(txt[txt_idx * 2]);
567 try emit(line_clear_to_eol);
568 try emit(nl);
569 try emit(txt[txt_idx * 2 + 1]);
570 try emit(line_clear_to_eol);
571 try emit(nl);
572
573 sleep(0, 10 * std.time.ns_per_ms);
574 }
575
576 //let quote chill for a second
577 sleep(1, 0);
578
579 //fade out
580 fade_idx = fade_len - 1;
581 while (fade_idx > 0) : (fade_idx -= 1) {
582 //reset to 1,1 of marquee
583 try emit(cursor_load);
584 try emit(bg[bg_idx]);
585 try emit(nl);
586
587 //print marquee txt
588 try emit(fg[fade_seq[fade_idx]]);
589 try emit(txt[txt_idx * 2]);
590 try emit(line_clear_to_eol);
591 try emit(nl);
592 try emit(txt[txt_idx * 2 + 1]);
593 try emit(line_clear_to_eol);
594 try emit(nl);
595 sleep(0, 10 * std.time.ns_per_ms);
596 }
597 try emit(nl);
598 }
599}
600
601// prove out terminal implementation by rendering colors and some simple animations
602pub fn showTermCap() !void {
603 try showTermSz();
604 try showStdColors();
605 try show216Colors();
606 try showGrayscale();
607 try scrollMarquee();
608
609 try pause();
610}
611
612/// DOOM Fire
613/// Slowest - raw emit()
614/// Slower - raw emit() + \n
615/// Below - moderately faster
616
617//pixel character
618const px = "▀";
619
620//bs = buffer string
621var bs: []u8 = undefined;
622var bs_idx: u64 = 0;
623var bs_len: u64 = 0;
624var bs_sz_min: u64 = 0;
625var bs_sz_max: u64 = 0;
626var bs_sz_avg: u64 = 0;
627var bs_frame_tic: u64 = 0;
628var timer: std.time.Timer = undefined;
629var t_dur: f64 = 0.0;
630var fps: f64 = 0.0;
631
632pub fn initBuf() !void {
633 //some lazy guesswork to make sure we have enough of a buffer to render DOOM fire.
634 const px_char_sz = px.len;
635 const px_color_sz = bg[LAST_COLOR].len + fg[LAST_COLOR].len;
636 const px_sz = px_color_sz + px_char_sz;
637 const screen_sz: u64 = @as(u64, px_sz * term_sz.width * term_sz.width);
638 const overflow_sz: u64 = px_char_sz * 100;
639 const bs_sz: u64 = screen_sz + overflow_sz;
640
641 bs = try allocator.alloc(u8, bs_sz * 2);
642 timer = try std.time.Timer.start();
643 resetBuf();
644}
645
646//reset buffer indexes to start of buffer
647pub fn resetBuf() void {
648 bs_idx = 0;
649 bs_len = 0;
650}
651
652//copy input string to buffer string
653pub fn drawBuf(s: []const u8) void {
654 for (s) |b| {
655 bs[bs_idx] = b;
656 bs_idx += 1;
657 bs_len += 1;
658 }
659}
660
661//print buffer to string...can be a decent amount of text!
662pub fn paintBuf() !void {
663 try emit(bs[0 .. bs_len - 1]);
664 const elapsed_ns = timer.read();
665 bs_frame_tic += 1;
666 if (bs_sz_min == 0) {
667 //first frame
668 bs_sz_min = bs_len;
669 bs_sz_max = bs_len;
670 bs_sz_avg = bs_len;
671 } else {
672 if (bs_len < bs_sz_min) {
673 bs_sz_min = bs_len;
674 }
675 if (bs_len > bs_sz_max) {
676 bs_sz_max = bs_len;
677 }
678 bs_sz_avg = bs_sz_avg * (bs_frame_tic - 1) / bs_frame_tic + bs_len / bs_frame_tic;
679 }
680
681 t_dur = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, std.time.ns_per_s);
682 fps = @as(f64, @floatFromInt(bs_frame_tic)) / t_dur;
683
684 try emit(fg[0]);
685 try emitFmt("mem: {d} min / {d} avg / {d} max [ {d:.2} fps ]", .{ bs_sz_min, bs_sz_avg, bs_sz_max, fps });
686}
687
688// initBuf(); defer freeBuf();
689pub fn freeBuf() void {
690 allocator.free(bs);
691}
692
693pub fn showDoomFire() !void {
694 //term size => fire size
695 const FIRE_H: u64 = @as(u64, @intCast(term_sz.height)) * 2;
696 const FIRE_W: u64 = @as(u64, @intCast(term_sz.width));
697 const FIRE_SZ: u64 = FIRE_H * FIRE_W;
698 const FIRE_LAST_ROW: u64 = (FIRE_H - 1) * FIRE_W;
699
700 //colors - tinker w/palette as needed!
701 const fire_palette = [_]u8{ 0, 233, 234, 52, 53, 88, 89, 94, 95, 96, 130, 131, 132, 133, 172, 214, 215, 220, 220, 221, 3, 226, 227, 230, 195, 230 };
702 const fire_black: u8 = 0;
703 const fire_white: u8 = fire_palette.len - 1;
704
705 //screen buf default color is black
706 var screen_buf: []u8 = undefined; //{fire_black}**FIRE_SZ;
707 screen_buf = try allocator.alloc(u8, FIRE_SZ);
708 defer allocator.free(screen_buf);
709
710 //init buffer
711 var buf_idx: u64 = 0;
712 while (buf_idx < FIRE_SZ) : (buf_idx += 1) {
713 screen_buf[buf_idx] = fire_black;
714 }
715
716 //last row is white...white is "fire source"
717 buf_idx = 0;
718 while (buf_idx < FIRE_W) : (buf_idx += 1) {
719 screen_buf[FIRE_LAST_ROW + buf_idx] = fire_white;
720 }
721
722 //reset terminal
723 try emit(cursor_home);
724 try emit(color_reset);
725 try emit(color_def);
726 try emit(screen_clear);
727
728 //scope cache ////////////////////
729 //scope cache - update fire buf
730 var doFire_x: u64 = 0;
731 var doFire_y: u64 = 0;
732 var doFire_idx: u64 = 0;
733
734 //scope cache - spread fire
735 var spread_px: u8 = 0;
736 var spread_rnd_idx: u8 = 0;
737 var spread_dst: u64 = 0;
738
739 //scope cache - frame reset
740 const init_frame = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ cursor_home, bg[0], fg[0] });
741 defer allocator.free(init_frame);
742
743 //scope cache - fire 2 screen buffer
744 var frame_x: u64 = 0;
745 var frame_y: u64 = 0;
746 var px_hi: u8 = fire_black;
747 var px_lo: u8 = fire_black;
748 var px_prev_hi = px_hi;
749 var px_prev_lo = px_lo;
750
751 //get to work!
752 try initBuf();
753 defer freeBuf();
754
755 //when there is an ez way to poll for key stroke...do that. for now, ctrl+c!
756 const ok = true;
757 while (ok) {
758
759 //update fire buf
760 doFire_x = 0;
761 while (doFire_x < FIRE_W) : (doFire_x = doFire_x + 1) {
762 doFire_y = 0;
763 while (doFire_y < FIRE_H) : (doFire_y += 1) {
764 doFire_idx = doFire_y * FIRE_W + doFire_x;
765
766 //spread fire
767 spread_px = screen_buf[doFire_idx];
768
769 //bounds checking
770 if ((spread_px == 0) and (doFire_idx >= FIRE_W)) {
771 screen_buf[doFire_idx - FIRE_W] = 0;
772 } else {
773 spread_rnd_idx = rand.intRangeAtMost(u8, 0, 3);
774 if (doFire_idx >= (spread_rnd_idx + 1)) {
775 spread_dst = doFire_idx - spread_rnd_idx + 1;
776 } else {
777 spread_dst = doFire_idx;
778 }
779 if (spread_dst >= FIRE_W) {
780 if (spread_px > (spread_rnd_idx & 1)) {
781 screen_buf[spread_dst - FIRE_W] = spread_px - (spread_rnd_idx & 1);
782 } else {
783 screen_buf[spread_dst - FIRE_W] = 0;
784 }
785 }
786 }
787 }
788 }
789
790 //paint fire buf
791 resetBuf();
792 drawBuf(init_frame);
793
794 // for each row
795 frame_y = 0;
796 while (frame_y < FIRE_H) : (frame_y += 2) { // 'paint' two rows at a time because of half height char
797 // for each col
798 frame_x = 0;
799 while (frame_x < FIRE_W) : (frame_x += 1) {
800 //each character rendered is actually to rows of 'pixels'
801 // - "hi" (current px row => fg char)
802 // - "low" (next row => bg color)
803 px_hi = screen_buf[frame_y * FIRE_W + frame_x];
804 px_lo = screen_buf[(frame_y + 1) * FIRE_W + frame_x];
805
806 // only *update* color if prior color is actually diff
807 if (px_lo != px_prev_lo) {
808 drawBuf(bg[fire_palette[px_lo]]);
809 }
810 if (px_hi != px_prev_hi) {
811 drawBuf(fg[fire_palette[px_hi]]);
812 }
813 drawBuf(px);
814
815 //cache current colors
816 px_prev_hi = px_hi;
817 px_prev_lo = px_lo;
818 }
819 drawBuf(nl); //is this needed?
820 }
821 try paintBuf();
822 resetBuf();
823 }
824}
825
826///////////////////////////////////
827// main
828///////////////////////////////////
829
830pub fn main() anyerror!void {
831 try initTerm();
832 defer complete() catch {};
833
834 try checkTermSz();
835 try showTermCap();
836 try showDoomFire();
837}
838
839// -- NOTE -- Keep the below aligned w/Microsoft Windows (MS WIN) specification
840// Changes to the below that are unaligned will cause panics or strange bugs
841
842const win32 = struct {
843 // --------------
844 // define MS WIN base types in zig terms
845 // part of std.os.windows
846 pub const WORD: type = std.os.windows.WORD; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f8573df3-a44a-4a50-b070-ac4c3aa78e3c
847 pub const BOOL: type = std.os.windows.BOOL; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/9d81be47-232e-42cf-8f0d-7a3b29bf2eb2
848 pub const HANDLE: type = std.os.windows.HANDLE; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/929187f0-f25c-4b05-9497-16b066d8a912
849 pub const SHORT: type = std.os.windows.SHORT; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/47b1e7d6-b5a1-48c3-986e-b5e5eb3f06d2
850 pub const DWORD: type = std.os.windows.DWORD; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/262627d8-3418-4627-9218-4ffe110850b2
851 pub const UINT: type = std.os.windows.UINT; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/52ddd4c3-55b9-4e03-8287-5392aac0627f
852 pub const LPVOID: type = std.os.windows.LPVOID;
853 //void https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c0b7741b-f577-4eed-aff3-2e909df10a4d
854
855 //
856 // --------------
857 // define MS WIN constants in terms of MS WIN base types
858 //
859 // screen buffer output constants c/o https://learn.microsoft.com/en-us/windows/console/setconsolemode
860 pub const ENABLE_PROCESSED_OUTPUT: DWORD = 0x0001; // USED
861 pub const ENABLE_WRAP_AT_EOL_OUTPUT: DWORD = 0x0002;
862 pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING: DWORD = 0x0004; // USED
863 pub const DISABLE_NEWLINE_AUTO_RETURN: DWORD = 0x0008;
864 pub const ENABLE_LVB_GRID_WORLDWIDE: DWORD = 0x0010;
865
866 // https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page
867 pub const CP_UTF8: UINT = 65001;
868 pub const CP_ANSI: UINT = 1252;
869
870 // https://learn.microsoft.com/en-us/windows/console/getstdhandle
871 // part of std.os.windows
872 // for use with https://ziglang.org/documentation/master/std/#std.os.windows.GetStdHandle
873 pub const STD_INPUT_HANDLE = std.os.windows.STD_INPUT_HANDLE;
874 pub const STD_OUTPUT_HANDLE = std.os.windows.STD_OUTPUT_HANDLE;
875 pub const STD_ERR_HANDLE = std.os.windows.STD_ERROR_HANDLE;
876
877 // --------------
878 // define MS WIN structures in terms of MS WIN base types
879 // CORD = https://learn.microsoft.com/en-us/windows/console/coord-str
880 pub const COORD = extern struct {
881 X: SHORT,
882 Y: SHORT,
883 };
884
885 //SMALL_RECT = https://learn.microsoft.com/en-us/windows/console/small-rect-str
886 pub const SMALL_RECT = extern struct {
887 Left: SHORT,
888 Top: SHORT,
889 Right: SHORT,
890 Bottom: SHORT,
891 };
892
893 // CONSOLE_SCREEN_BUFFER_INFO = https://learn.microsoft.com/en-us/windows/console/console-screen-buffer-info-str
894 pub const CONSOLE_SCREEN_BUFFER_INFO = extern struct {
895 dwSize: COORD, //For terminal init, do not use - buffer size -- not the window size
896 dwCursorPosition: COORD,
897 wAttributes: WORD,
898 srWindow: SMALL_RECT, //For terminal init, use - window
899 dwMaximumWindowSize: COORD,
900 };
901
902 // -----------------
903 // define MS WIN console API in terms of MS WIN base types and MS WIN structures
904 // reference: https://learn.microsoft.com/en-us/windows/console/console-functions
905
906 // https://learn.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo
907 pub extern "kernel32" fn GetConsoleScreenBufferInfo(
908 hConsoleOutput: ?HANDLE,
909 lpConsoleScreenBufferInfo: ?*CONSOLE_SCREEN_BUFFER_INFO,
910 ) callconv(std.os.windows.WINAPI) BOOL;
911
912 // https://learn.microsoft.com/en-us/windows/console/getconsolemode
913 pub extern "kernel32" fn GetConsoleMode(hConsoleOutput: ?HANDLE, lpOutMode: ?*DWORD) callconv(std.os.windows.WINAPI) BOOL;
914
915 // https://learn.microsoft.com/en-us/windows/console/setconsolemode
916 pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: HANDLE, in_Mode: DWORD) callconv(std.os.windows.WINAPI) BOOL;
917
918 // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp
919 pub extern "kernel32" fn SetConsoleOutputCP(in_wCodePageID: UINT) callconv(std.os.windows.WINAPI) BOOL;
920
921 // https://learn.microsoft.com/en-us/windows/console/writeconsole
922 // W = Wide = UTF8
923 pub extern "kernel32" fn WriteConsoleW(in_hConsoleOutput: HANDLE, in_lpBuffer: [*]const u8, in_nNumberOfCharsToWrite: DWORD, out_lpNumberOfCharsWritten: ?*DWORD, lpReserved: *void) callconv(std.os.windows.WINAPI) BOOL;
924
925 // https://learn.microsoft.com/en-us/windows/console/writeconsole
926 // A = ASCII
927 pub extern "kernel32" fn WriteConsoleA(in_hConsoleOutput: HANDLE, in_lpBuffer: [*]const u8, in_nNumberOfCharsToWrite: DWORD, out_lpNumberOfCharsWritten: ?*DWORD, lpReserved: *void) callconv(std.os.windows.WINAPI) BOOL;
928};
929
930//LICENSE --
931//DOOM-fire-zig is free software: you can redistribute it and/or modify it under the terms of
932//the GNU General Public License as published by the Free Software Foundation, either
933//version 3 of the License, or (at your option) any later version.
934//
935//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
936//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
937//
938//See the GNU General Public License for more details.
939//
940//You should have received a copy of the GNU General Public License along with this program.
941//
942//If not, see https://www.gnu.org/licenses/.
943//---------