xhartae/chapter/chapter_2.c

481 lines
27 KiB
C

/*
Copyright (c) 2023 : Ognjen 'xolatile' Milan Robovic
Xhartae is free software! You will redistribute it or modify it under the terms of the GNU General Public License by Free Software Foundation.
And when you do redistribute it or modify it, it will use either version 3 of the License, or (at yours truly opinion) any later version.
It is distributed in the hope that it will be useful or harmful, it really depends... But no warranty what so ever, seriously. See GNU/GPLv3.
*/
#ifndef CHAPTER_2_SOURCE
#define CHAPTER_2_SOURCE
#include "chapter_2.h" // We're copying function and variable declarations from this file here. This shouldn't be copied twice, more on that later...
/*
Function 'hello_world_0' uses (rougly said) a system call instead of a function, but I don't even know what will different compilers do to that line of code. What's a system call?
Well, when you're writing assembly, you have some general purpose registers, on mine and your machine, there's 16 of them, and you move some data in them, then use 'syscall'
instruction, which magically does something, like open or close a file descriptor, read or write to it, but more on that later! A lot more... Important thing to know is that when
you use keyword 'sizeof' on a string, it'll count null terminator, so it'll have value 14, and type 'size_t'. It's provided in <unistd.h> header file.
@C
write ( // mov rax 1 ; Literal of Linux write system call, defined internally.
STDOUT_FILENO, // mov rdi 1 ; Literal of standard output file descriptor, defined as 'STDOUT_FILENO'.
"Hello world!\n", // mov rsi X ; Address of our string.
sizeof ("Hello world!\n") // mov rdx [Y] ; Literal of size of our string.
); // syscall ; Ask kernel to do some work.
@
We'll talk about assembly a lot in future chapters, but you should filter that out of your brain until you learn C well...
*/
void hello_world_0 (void) {
write (STDOUT_FILENO, "Hello world!\n", sizeof ("Hello world!\n"));
}
/*
In function 'hello_world_1' we're using function 'puts' this time, which is considered unsafe because it'll fail when input string isn't null terminated. Keep in mind that it also
prints line feed (new line) on the end. Why I call it line feed? Some terminals and text editors distinguish carriage return ('\r' / 13 / CR) and line feed ('\n' / 10 / LF), and
some of them use both, like old teleprinters used to do, where the name 'tty' on UNIX-based operating systems comes from.
*/
void hello_world_1 (void) {
puts ("Hello world!");
}
/*
Now, in function 'hello_world_2' and function 'hello_world_3', we're using 'printf' function, which is in my opinion important for debugging. It has many variations, so we'll have
a separate chapter only about it. Know that it's a variadic function, which means that it can take more than one input argument, and later we'll learn to make our own variadic
functions as well. It's also a bit unsafe, if you're not careful how you use it, but nothing drastic can happen, most compilers catch those kinds of errors.
*/
void hello_world_2 (void) {
printf ("Hello world!\n");
}
/*
It's also worth noting that we're using it's arguments in function 'hello_world_3', function 'printf' will print characters one by one from its' format, until it encounters '%',
if the next character is '%', it prints one '%', if it's 's', then it'll print a string, which we provide in the next argument. When compiler optimizations are turned on, it will
change all sorts of things here, since it's obvious that we're using simple string in this example.
*/
void hello_world_3 (void) {
printf ("%s %s!\n", "Hello", "world");
}
/*
Lastly, we have a global variable, some people like to call it that, and consider it evil. It's not evil if you know the pros and cons of using them, which causes intellectual
overhead for certain sort of people. Pros, makes your code shorter and easier to refactor. Cons, it can potentially make your code less safe if you don't know what you're doing.
You should use them, external variables as I like to call them, when you're working on your programs or in a small group, but they can lead to bad things if you're working with a
lot of people, someone could change it in some bad place, and cause a bug that's difficult to track.
We could initialize it also in any part of our code later, in some function for example, as following:
@C
// One by one:
hello_world [0] = hello_world_0;
hello_world [1] = hello_world_1;
hello_world [2] = hello_world_2;
hello_world [3] = hello_world_3;
// Or use it for some completely unrelated function if we wanted to, as long as function "kind" is same, they both take no input and no output:
static void unrelated_function (void) {
puts ("I couldn't care less...");
exit (EXIT_FAILURE);
}
hello_world [2] = unrelated_function;
@
Just know that an array(s) of function pointers is my favorite kind of variable for bot AI in simple games, index can be derived from bot state.
*/
void (* hello_world [4]) (void) = {
hello_world_0,
hello_world_1,
hello_world_2,
hello_world_3
};
/*
Now, we'll talk more about cursed functions! I broke code formatting rules for this, but it's worth it, time to learn. Function or variable with 'static' instead of 'extern'
keyword before it is internal. That means it'll only be accessable in this file. That way, I can include file "chapter_2.h" in some other file, and know that I can't access or
modify some internal variable within that other file. They can only be accessed or modified in this file! Lets show some formatting examples below:
@C
static int my_size = 0;
static char * my_name = "Name";
static void * my_data = NULL;
@
Unlike when declaring external variables (seen in previous header files), you need to initialize internal variables to some value, like you see above. You shouldn't initialize
external variables, they have separate definition (initialization in this case). You can see them just below, they are initialized without 'extern' in front of them, and the same
goes for functions declared in "chapter_2.h", but there's another trick to this... You can declare internal functions before defining them, so you have 2 options for them:
@C
// Option A:
static void * my_function (char * name, int size);
int my_function (char * name, int size) {
int data = 0;
// Do some work involving function arguments and modify data.
return (data);
}
// Option B:
static int my_function (char * name, int size) {
int data = 0;
// Do some work involving function arguments and modify data.
return (data);
}
@
Okay, external variables you see lower (without 'static') are all integers. Lets briefly see what types are the internal variables.
- curses_format / curses_format: Array of characters (also known as string!), and their size is already known at the compile time, it's inside square braces.
- curses_screen: Pointer to character, we'll allocate memory for it later, so it'll be a dynamic array of characters, its' size can change.
- curses_activator: Pointer to integer, we'll also allocate memory for it, so we can't use square braces for those. Remember that for later.
- curses_action: Strictly speaking, pointer to another pointer to function of signature 'void SOMETHING (void);', but ignore it for now, don't get confused.
*/
static char curses_format [CURSES_FORMAT + 1] = "\033[-;3-m-\033[0m"; // Internal variable holding data for rendering single character, using ASCII escape sequences.
static char curses_cursor [CURSES_CURSOR + 1] = "\033[---;---H"; // Internal variable holding data for rendering cursor at some position.
static char * curses_screen = NULL; // We hold all terminal screen data here, you can think of it as an image, but better word is a framebuffer.
static int curses_action_count = 0; // Count of defined actions, these are part of event handling system.
static int * curses_activator = NULL; // Array of action signals, when they are active, action is executed.
static void (* * curses_action) (void) = NULL; // Array of function pointers, we use it to set event actions, so that user can interact with the terminal.
static struct termios curses_old_terminal; // This is <termios.h> magic for making terminal enter and exit the raw mode.
static struct termios curses_new_terminal;
// We've explained these external variables in header file already.
int curses_character = 0;
int curses_signal = SIGNAL_NONE;
int curses_screen_width = 0;
int curses_screen_height = 0;
int curses_active = 1;
/*
I need to quickly explain how I'm structuring this subprogram. You can think of functions and variables starting with 'curses_*' as tiny standalone library if it's easier. They
deal with terminal input and output. User only needs to call function 'curses_configure' only once, and necessary amount of 'curses_bind' functions, before doing any rendering,
then make an "infinite" loop that'll stop when external variable 'curses_active' is equal to zero. In that loop, user can call any amount 'curses_render_*' functions, and at the
end call function 'curses_synchronize' again, only once. So the following program structure would look something like this:
@C
// ...
int main (int argc, char * * argv) {
// ...
curses_configure ();
while (curses_active != 0) { // Notice the negative condition, the loop will stop then the condition is 0, so when 'curses_active' is 0.
// Calling functions 'curses_render_*' and doing other work...
curses_synchronize ();
}
// ...
return (0);
}
@
We don't need to use 'curses_initialize' and the begining of the program, or 'curses_deinitialize' at the end. Also, there are no functions like 'BeginDrawing' or 'EndDrawing',
with 'Draw*' functions strictly between those two, like you would see in Raylib. We won't cover SDL2 and Raylib in this book, since I think that anyone with minimal knowledge of
C can efficiently write good programs using those libraries, please don't misunderstand me, those two are good libraries. We'll simply eliminate that "begin" or "end" step in
our subprogram called 'curses'.
*/
static void curses_initialize (void) { // This function will be called when 'curses_configure' is called, automatically.
struct winsize screen_dimension;
fatal_failure (ioctl (STDOUT_FILENO, TIOCGWINSZ, & screen_dimension) == -1, // If function 'ioctl' failed, we immediately aborting the entire program.
"ioctl: Failed to get terminal dimensions."); // I split those error messages, you can find your own formatting style.
curses_screen_width = (int) screen_dimension.ws_col; // We get the dimensions of terminal window by calling that 'ioctl' function.
curses_screen_height = (int) screen_dimension.ws_row;
fatal_failure (tcgetattr (STDIN_FILENO, & curses_old_terminal) == -1, // Now we need to obtain data for current non-raw terminal to restore it later.
"tcgetattr: Failed to get default terminal attributes.");
curses_new_terminal = curses_old_terminal; // Here we set our raw terminal to be the same as the non-raw one.
curses_new_terminal.c_cc [VMIN] = (unsigned char) 0; // Now it's time to modify it to be raw, this essentially means no-echo.
curses_new_terminal.c_cc [VTIME] = (unsigned char) 1;
curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
curses_new_terminal.c_oflag &= (unsigned int) ~(OPOST);
curses_new_terminal.c_cflag |= (unsigned int) (CS8);
curses_new_terminal.c_lflag &= (unsigned int) ~(ECHO | ICANON | IEXTEN | ISIG);
fatal_failure (tcsetattr (STDIN_FILENO, TCSAFLUSH, & curses_new_terminal) == -1, // Finally, we're passing intormations to our terminal, and it becomes raw.
"tcsetattr: Failed to set reverse terminal attributes.");
}
static void curses_deinitialize (void) { // This function will be only called once, automatically, at the program exit.
curses_screen = deallocate (curses_screen); // It's important to deallocate all previously allocated memory.
curses_activator = deallocate (curses_activator);
curses_action = deallocate (curses_action);
terminal_clear (); // This only make things look prettier, we don't have mess when we exit the program.
fatal_failure (tcsetattr (STDIN_FILENO, TCSAFLUSH, & curses_old_terminal) == -1, // Again, if this fails, we're doomed, we're aborting.
"tcsetattr: Failed to set default terminal attributes.");
}
/*
Next two functions are also internal, and we'll use them in our other 'curses_*' functions. The goal of extracting that piece of code into separate functions is to eliminate code
repetition. Copying and pasting the same piece of code everywhere has pros and cons. Pros are that they're very good indication what should be factored out. The best example that
comes from top of my head is 'echo' function from chapter zero. It'd be boring to use 'out' everywhere, since it also requires the 'size' argument, and I want to just output
string literals to terminal in most cases. But that doesn't mean that 'out' is useless, as you'll see, we'll use it in 'curses_synchronize' funcion. Cons are, if you really have
repeating code all over your program, if there's a bug in one of them, there's a bug in all of them. Also, if you'll use some extracted function (also refered to as refactored in
some cases) only once, it's not a bad thing. We've extracted some code into 'curses_initialize' function, and we call it only once, inside 'curses_configure' function, but the
intention behind what it does is clear. However, I still believe that procedural code, that's executed line by line, from top to bottom, is best.
Now, we could also implement some error checking functions for 'curses_*' functions. Good idea, when something is wrong, like we want to render a character out of the screen, we
just print an error message to terminal, right? Well, no. Remember, we're using terminal as a framebuffer (about which we'll talk about a lot more in ray tracing and rasterization
chapters), so if we just print message there, it'll corrupt our framebuffer. So, we could just write them to some log file. How about binding them, whenever something is wrong,
we don't just abort the program or stop rendering, but continue running it, while also writing error messages to that same file. Then, when we exit the program, it'll print all
error messages (if any) normally. Here's how it would look like:
@C
// Enumeration for log type.
enum {
LOG_FAILURE, LOG_WARNING, LOG_SUCCESS, LOG_COUNT
};
static void log_in (int type, int condition, char * message);
static void log_out (char * name);
// Usage example:
log_in (LOG_FAILURE, condition != 0, "function_name: The kind of error that occured or a general notification.");
log_out ("file_name.log");
@
To be honest, I consider that a bad idea, because if we render something that's not supposed to be rendered (we pass wrong arguments to any of the 'curses_render_*' functions),
it'll be obvious when we look at it. So, I wrote commented-out examples below of how we'd do it, but it won't be compiled into the final object file. If we pass a wrong value to
those functions, they'll be fixed (modified), and give the wrong results, which will be obvious when we look at the screen. Nothing will crash or burn.
*/
static char * curses_screen_offset (int x, int y) {
// log_in (LOG_FAILURE, x <= -1, "curses_screen_offset: X position is below the lower bound.");
// log_in (LOG_FAILURE, y <= -1, "curses_screen_offset: Y position is below the lower bound.");
// log_in (LOG_FAILURE, x >= curses_screen_width, "curses_screen_offset: X position is above the upper bound.");
// log_in (LOG_FAILURE, y >= curses_screen_height, "curses_screen_offset: Y position is above the upper bound.");
limit (& x, 0, curses_screen_width - 1); // We're limiting the values of X and Y coordinates to screen dimensions.
limit (& y, 0, curses_screen_height - 1); // Function 'limit' uses inclusive values for minimum and maximum.
return (& curses_screen [CURSES_REVERT + CURSES_FORMAT * (y * curses_screen_width + x)]); // And returning the offset of the screen buffer.
}
static char * curses_format_character (char character, int colour, int effect) {
// log_in (LOG_WARNING, character_is_invisible (character), "curses_format_character: Can not format invisible characters.");
// log_in (LOG_FAILURE, colour >= COLOUR_COUNT, "curses_format_character: Colour is invalid enumeration value.");
// log_in (LOG_FAILURE, effect >= EFFECT_COUNT, "curses_format_character: Effect is invalid enumeration value.");
if (character_is_invisible (character) != 0) { // Since we don't want to render invisible ASCII characters, we'll just change 'character' to space.
character = ' ';
}
colour %= COLOUR_COUNT; // Instead of breaking and burning everything, we'll just "round" it down to one of the allowed values.
effect %= EFFECT_COUNT;
switch (effect) { // Effects aren't enumerated nicely as colours, so here we'll use switch statement instead of nested if-else statements.
case EFFECT_NORMAL: effect = 0; break; // We could break these into any combination of new lines, we'll just show one example below of how else you can do it.
// case EFFECT_NORMAL:
// effect = 0;
// break;
// case EFFECT_NORMAL: {
// effect = 0;
// } break;
case EFFECT_BOLD: effect = 1; break;
case EFFECT_ITALIC: effect = 3; break;
case EFFECT_UNDERLINE: effect = 4; break;
case EFFECT_BLINK: effect = 5; break;
case EFFECT_REVERSE: effect = 7; break;
default: effect = 0; break;
}
curses_format [2] = (char) effect + '0'; // Now, here comes ASCII escape sequence magic, this will format the character to have colour and effect.
curses_format [5] = (char) colour + '0';
curses_format [7] = character;
return (curses_format); // And we return the value (pointer to character) of formatted internal variables.
}
static void curses_idle (void) { return; } // If you have a lot of short functions that are intended to be in array of function pointers, you can align them like this.
static void curses_exit (void) { curses_active = 0; } // And this is our main function for quitting main loop in curses.
/*
External function definitions, those found in "chapter_2.h" header file.
*/
void curses_configure (void) {
atexit (curses_deinitialize); // Deinitialization is automatically on exit.
curses_initialize (); // Initializing curses, yaay.
curses_screen = allocate (CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR + 1); // We're requesting new memory for framebuffer.
curses_bind (SIGNAL_ESCAPE, curses_exit); // Binding universal exit key (signal).
terminal_clear ();
string_copy (& curses_screen [0], "\033[H"); // ASCII black magic to always clear screen.
}
void curses_synchronize (void) {
int signal;
curses_signal = curses_character = signal = 0; // Reassigning signals to 0.
out (curses_screen, CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR); // We output the entire framebuffer to terminal (present).
in (& signal, (int) sizeof (signal)); // We're now checking for user input to modify it.
curses_character = signal; // We need literal value of 'signal' for text input.
if (signal == '\033') { // And then we modify the actual 'curses_signal'.
curses_signal |= SIGNAL_ESCAPE;
} else if (character_is_digit ((char) signal) != 0) {
curses_signal |= SIGNAL_0 + (int) (signal - '0');
} else if (character_is_lowercase ((char) signal) != 0) {
curses_signal |= SIGNAL_A + (int) (signal - 'a');
} else if (character_is_uppercase ((char) signal) != 0) {
curses_signal |= SIGNAL_A + (int) (signal - 'A');
curses_signal |= SIGNAL_SHIFT;
} else {
curses_signal = SIGNAL_NONE;
}
for (signal = 0; signal != curses_action_count; ++signal) { // Now, it's time to loop over bound actions.
if (curses_signal == curses_activator [signal]) { // If we have a bound signal, then:
curses_action [signal] (); // We execute corresponding action (function).
}
}
}
/*
It's still to early to talk about signal binding and unbinding, so I'll skip over them as they are more complex.
*/
void curses_bind (int signal, void (* action) (void)) {
++curses_action_count;
curses_activator = reallocate (curses_activator, curses_action_count * (int) sizeof (* curses_activator));
curses_action = reallocate (curses_action, curses_action_count * (int) sizeof (* curses_action));
curses_activator [curses_action_count - 1] = signal;
curses_action [curses_action_count - 1] = action;
}
void curses_unbind (int signal) {
(void) signal;
curses_activator [curses_action_count - 1] = SIGNAL_NONE;
curses_action [curses_action_count - 1] = curses_idle;
--curses_action_count;
}
/*
Finally, we're getting to rendering, something that we can actually see on our screen. There's ASCII escape sequences involved into this, so lets explain them briefly. We
mentioned in "chapter_2.h" header file about how some of them work. Now, we're extending them, with minimal amount of error checking in order to keep things more clear. Worth
noting is that not all terminals support all of ASCII escape sequences, but most of them support at least a subset of them. There are many terminals such as ones provided by
desktop environment (xfce4-terminal, etc.), and independent ones (st, sakura, xterm, etc.), use whatever, it'll (in 99% of cases) work as intended.
If you want to know what rendering truly is, it's just copying certain value to some offset. Nothing more...
*/
void curses_render_cursor (int x, int y) {
// We're expecting that our terminal dimensions (width and height in unit of 'characters') is always less 1000. If it's not, I really don't care.
// Also, there's the application of "limited" string functions, since we don't want to copy until null character ('\0') is reached.
// Lastly, if you remember that 'curses_cursor' is string "\033[---;---H", we're just writing two numbers to "---" parts, from 0 to 999 inclusively.
// We're adding one because ASCII uses 1 ... 1000, and we use 0 ... 999, so it doesn't causes hidden bugs, and realign them to be prefixed with '0' characters.
// If we have set of values (x = 31, y = 37), then the copied string would look like "\033[031;037H". It's not complex as it may sound.
// And remember our ASCII table, thing scary thing (\033) is just octal value for number 27, which is CHARACTER_ESCAPE, hence the escape sequences start with it.
x %= 1000;
y %= 1000;
string_copy_limit (curses_cursor + 2, string_realign (number_to_string (y + 1), 3, '0'), 3);
string_copy_limit (curses_cursor + 6, string_realign (number_to_string (x + 1), 3, '0'), 3);
string_copy_limit (& curses_screen [CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height], curses_cursor, CURSES_CURSOR); // Actual rendering.
}
void curses_render_character (char character, int colour, int effect, int x, int y) {
if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) { // If any of these are true, we don't render.
return;
}
// Again, lets show some code formatting examples:
// if ((x < 0)
// || (y < 0)
// || (x > curses_screen_width - 1)
// || (y > curses_screen_height - 1)) {
// return;
// }
// Or, if you don't want to subtract one from values there:
// if ((x < 0) ||
// (y < 0) ||
// (x >= curses_screen_width) ||
// (y >= curses_screen_height)) {
// return;
// }
// Or if you really hate adding 2 more lines of code and curly braces:
// if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) return;
string_copy_limit (curses_screen_offset (x, y), curses_format_character (character, colour, effect), CURSES_FORMAT); // Again, actual rendering, copying a value to offset.
}
void curses_render_background (char character, int colour, int effect) {
int x, y; // We declare local variables like this, I usually don't initialize them right away.
for (y = 0; y != curses_screen_height; ++y) { // Iterating through rows (by height) of our framebuffer ('curses_screen').
for (x = 0; x != curses_screen_width; ++x) { // Iterating through columns (by width) of our framebuffer.
curses_render_character (character, colour, effect, x, y); // Now, we can use function 'curses_render_character' to simplify our life...
}
}
}
/*
We've mentioned before, in chapter zero, that you can implement 'string_*' functions by 'string_*_limit', and here's an example of that. Functions below are quite self
explanatory, so you can do a "homework" of reading them and trying to understand what they'll do. Since I use very verbose naming style, I hope that won't be a problem...
*/
int curses_render_string (char * string, int colour, int effect, int x, int y) { // Keep in mind, we could reimplement this to be very similar to "limited" version.
return (curses_render_string_limit (string, string_length (string), colour, effect, x, y));
}
int curses_render_number (int number, int colour, int effect, int x, int y) { // This is not particularly smart solution, but it's simple to understand.
return (curses_render_string (number_to_string (number), colour, effect, x, y));
}
int curses_render_string_limit (char * string, int limit, int colour, int effect, int x, int y) { // This uses short "fix" for blank characters, but we'll align them better later.
int offset;
for (offset = 0; offset != limit; ++offset) {
if (string [offset] == '\n') {
x = 0;
y += 1;
} else if (string [offset] == '\t') {
x += 8;
} else {
curses_render_character (string [offset], colour, effect, x, y);
x += 1;
}
}
return (limit);
}
int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { // Finally, this will align the number to the right and limit it as well.
return (curses_render_string_limit (number_to_string (number), limit, colour, effect, x, y));
}
#endif