Content of this file is cyclicaly scrolled at the bottom of initial screen. This file contains lines of ascii text. Each line is scrolled over the screen and when last letter of the line disappears at the left, first letter of a new line appears at the right. So if you want uninterrupted stream of text, you must write it into one line. Vice versa if you want text with intervals write it into several lines.
AVI is a shortcut of "ascii video". These files contain animated graphics in human readable form.
Avi files are divided into lines. Each line begins with initial character and ends with linefeed, empty lines are ignored. Initial characters are: p, a, l, s, #. File contains consecutive animation positions. Last line is list of positions.
# is a comment. Lines beginning with hash are ignored.
p is offset of current animation position. This line initiates each position. p letter is followed by comma separated pair of numbers. First number is horizontal offset, second is vertical offset. Number can be both positive and negative. Positive means move image right/down, negative move image left/up.
l is textual part of image scanline. This line contains ascii characters. It must be followed with attribute line. Number of image lines is not restricted.
a is attribute part of image scanline. Line can contain digits 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, small letters a, b, c, d, e, f and spaces. Digits 1-9 and letters a-f are colors, space and zero are transparency. Pixel is transparent apart from it's textual value (if it has transparent attribute).
s list of animation positions. Positions are numbered beginning with zero and comma separated. All animations are cyclical. So if you want have animation with positions 5, 3, 1, 7, 2, 5, 3, 5, 0, 6, 4 you simply write s5,3,1,7,2,5,3,5,0,6,4. Of course there can be only numbers of positions previously defined.
Typical avi file contains p-line followed with several couples l-line, a-line. Then p-line, several couples l-line, a-line ... And last line is s-line.
This is a list of sprites used in the game. There is name, whitespace and then filename (or path) on each line of the file. Names can't contain spaces. Sprites are used in dynamic.dat and room.dat. In these files sprites are refered to with their names.
This file contains description of playing area. It's divided into lines. Each line means one object. Line has several parts separated with whitespace.
Line begins with name of image, name must be defined in sprites.dat file. Then follows (after whitespace) one letter meaning type. Then there are two numbers (whitespace separated) meaning x and y coordinates of upper left corner of the object. Coordinates can be positive or zero.
Defined type is applied only on non-transparent pixels of a sprite. Transparent parts of sprite have no type - they don't restrict player's motion.
Sprites can overlap. Later sprite (in file) does over older one. The older one is visible through transparent pixels of the later one.
Structure of this file is same as structure of room.dat file. This file defines dynamic objects as medikits, ammo, guns and all animating objects.
One and only difference against room.dat file are object types.
All weapons are supplied with some basic ammo. There aren't any differences among medikits (all heal the same) and among ammo (every shotgun ammo adds the same, every grenade ammo adds the same, ...) though they can look different.
To alias new letter (e.g. X) with object type (e.g. T_X) edit data.c file, _convert_type() function. This function contains switch of letters so add a new line case 'X': return T_X;
When you run 0verkill client for the first time, .0verkill file appears in your home directory. This file contains address of the server, your color and name. Everytime you run 0verkill, these values will be taken as default.
Configure file is readable for humans. It's each item is on separate line. There's server address on first line, second line contains player's name and on third line nuber of player's color is. It's number from 1 to 30.
There are several graphics files necessary to run the game (like heroes, corpses, bullets and so on). They must appear in sprites.dat file. Here's list of their names:
Avi files containing hero's animations have own special structures. They stand avi file structure but each animation position (as written on the s line) have own meaning. Here it is:
Heros are in male and female version, each in 15 different colors. Not to have to edit all 30 files when something changes there are only two files hero_univ.avi and girl_univ.avi containing heroes, but instead of color which changes there are letters G (this letter isn't in any pixel part). The same is with corpses (files corpse_univ.avi and corpse_girl_univ.avi). And there's a shell script make_hero generating all 30 files. Script simply substitues G letter with digits 1...F.
Coordinates and velocities of all objects in the game are stored in fixpoint numbers. Floats are inappropriate because two float numbers can't be compared, fixpoint arithmetics is faster than float arithmetics and such accuracy is idle.
Fixpoints and all necessary fixpoint arithmetics functions are defined in file math.h. Fixpoints are stored in 32-bit integers. Original number is multiplied with 1024 and then stored in integer. To get integer from fixpoint (or vice versa) you make only one shift.
Fixpoint type is called my_double.Precision of fixpoint arithmetics is defined with PREC - it's number of bits you have to shift integer to the left to get fixpoint.
There're macros float2double, int2double to convert float (integer) number to fixpoint and double2int to convert fixpoint back to integer.
math.h file contains these fixpoint arithmetical functions (they're macros, but I'll write them as functions for better understanding):
Addition and subtraction isn't anything abnormal - it's like addition and substraction of two integers. There's no function for division because division is slow and is not used in the game.
All game constants are in file cfg.h. Now I'll describe most important of them:
Weapons in the game have lot of properties. For example name, cadence, basic ammo, rebound, ... Weapon attributes are defined in weapon table in file data.c (definition of this type is in data.h). Weapon is definite determined with it's number (weapon type) - so table is indexed with weapon type. To change weapon properties simply increase ARMS constant and add new line to the table - nothing else is needed.
When player fires server creates new object "bullet" and object "shell". Bullet has vertical velocity zero, horizontal velocity is speed. Time to live gives striking distance. Bullet's sprite is stored in bullet_sprite variable, shell's sprite is in shell_sprite (both are global variables in server.c source, they're initialized during start of the game).
Server sets shooting flag (bit 4 in hero's status) at hero's object and hero's time to live is set to weapon cadence. When shooting flag is up player can't shoot again. When hero's time to live decreases to zero shooting flag is shut down. When client gets update of a hero with wielding gun flag (bit 5 in status) he sets time to live to weapon cadence. Animation of shooting player is displayed only when hero's time to live==weapon cadence. It's here not to display shooting player everytime client gets status update.
Shotgun shooting is a bit different. Server creates shell too but it's sprite is in shotgun_shell_sprite and creates not one bullet but six slugs (slug_sprite). Their vertical velocities are not zero - they're given in sources. Thus every shotgun shot creates the same slugs with the same velocities.
Grenade throwing is absolute different because grenades have timeout and grenade isn't created when fire is pressed because hero must lock it off first. So when player fires his hero's time to live is set to grenade cadence and bit 9 in his hero's status is set to 1. When ttl is lower than GRENADE_DELAY server creates grenade object with initial velocity grenade shell_xspeed and shell_yspeed and initial time to live. Then when grenade's ttl lowers to zero grenade explodes: server deletes grenade and creates shrapnels instead.
When player is hit his health is lowered by lethalness of the weapon. Damage decreases (linearly) with bullet/slug/shrapnel time to live. It also depends on place on hero's body where bullet crashes into. It's linear interpolated too - head hit damages twice as legs hit.
If player's health lowers below zero player is dead. If healt lowers below minus OVERKILL player melts down (create_mess function is called), otherwise corpse is created (create_corpse function).
To find out whose bullet or slug or shrapnel killed a player object bullet/slug/shrapnel contains owner's hero object number in data item. It could contain a pointer to player but player could shoot and immediately leave the game (player will be removed from server's data structures) and bullet could kill someone and consequently server would crash on sigsegv because the pointer would be invalid.
When grenade shrapnel hits corpse or mess (object with type T_CORPSE_TYPE) blood gushes and raw meat flies - function create_mess is called and original object (corpse) is deleted. It offers great fun!
Not to load network so much server doesn't send bullet, shells, slugs and shrapnels (objects with type T_SHELL, T_BULLET or T_SHRAPNEL) updates. Clients compute them theirselves. It's because bullets and shells appear in the game very often and their updates would load network hard. It may be a bit unaccurate (clients could see reality a bit different) but it's worth network discharge.
In this section I'll describe how sprites are stored in memory. Data structures I'm gonna describe can be found in sprite.h file.
First I'll describe data types. Sprite=animation, animation is set of (one or more) ascii-art pictures with given order, picture is set of scan-lines (not explicit with same length), each line is set of pixels, pixel have textual part and attribute part. This is a philosophy of sprites storage in memory.
There are three structures for each described part of sprite: sprite (struct sprite), animation position (struct pos) and scan-line (struct line).
For better understanding see sprite.h file.
All sprites are stored in sprites variable - field of struct sprite. Sprite names are stored in sprite_names variable (field of strings). Sprites are searched through their names using find_sprite() function. (All this is described in data.c and data.h files.)
Avi files are loaded from sprites.dat using load_sprite() function. This function makes everything needed: fills sprites variable and sprite_names variable too.
Game expects both client and server have the same sprites.dat file. This is very important presumption - files shall not be changed. At this time there's no check that files are same. Because both client and server use the same function for loading sprites sprites are stored with the same order into memory. So to determine sprite over the network sprite number (index in sprites array) is enough.
Game is compact of static map and dynamic objects. Static map is a rectangle of pixels filled with walls, empty space, background, ... . Level dimensions are AREA_X and AREA_Y. Description of static map is in room.dat file. Each pixel of the level contains textual information (letter of the pixel) and attribute. Map is stored in area (textual data) and area_a (attributes) variables. Both are one dimensional arrays of chars. Lower 4 bits of attribute are color of the pixel, higher 4 bits are pixel type. Type can be:
These types are defined in data.h file.
Objects is everything that moves, animates or can be picked up. Game without static map are objects. The only thing that happens in the game is object moving, updating, creating and deleting. Nothing more. Heroes are objects, shooting is new objects creating, ...
Object have own unique ID, it's 24 bits long, to flow over ID there would have to be over 16 millions of objects - it would be really heavy game ;-).
Object is represented in struct it structure (data definition is in data.h file), objects are stored in bidirectional list (struct object_list). This data type was chosen because we want to create and delete objects and we also want to have access to each object. Because searching objects through their IDs is necessary objects are hashed (hash.h, hash.c sources) by their IDs. Hash table contains pointers to the list. Hash table 32k entries - table ss about 120k large.
There are functions for creating and deleting objects: new_obj() and delete_obj(). They add/remove object to/from object list and hash table. new_object functino also initializes object's data with given values. There's function find_in_table() to find object with given ID, this function returns poniter to the list of all objects.
Object type gives information about what the object is, e.g medikit, hero, grenade, shotgun, corpse, ... Object types differ in attributes. Attributes are stored in obj_attr table (in data.c, type is defined in data.h), they say for example if it falls, how much it bounces and so on.
Here is list of object types:
Similar to weapon attributes object attributes table is indexed through object type too.
To add new object type increase N_TYPES constant, add new type constant with number N_TYPES-1 (both in data.h file) and add new line describing object's attributes into obj_attr table (in data.c file). Now you can use new object type in the game.
Only heroes use all status bits, other objects use only bits 3 and 6. Bullets (object with type T_BULLET) use status for absolute different purpose. Server stores type of weapon in the status. It's a little cheat because bullets don't fall and can't be hidden thus status would be unused. And bullet must remember type of gun it was shot off.
data item in struct it is used in several ways:
Some objects (ammo, weapons, medikits, ...) a while after picking up appear again in the game. It's called respawning.
Server have time queue (time_queue variable) - time queue is a bidirectional list of objects waiting for respawning. Objects in queue have respawning time, it's stored in last_updated variable. Respawning time is time in microseconds since 1970 (at server's machine) when object's gonna appear in the game. List is sorted by respawn time (object with lowest time comes first) - that's why it's called queue.
When the object is picked up, it's not deleted, but moved to time queue (add_to_timeq() function). Queue is updated every game tick calling update_timeq() function. This function removes from queue and adds to the game objects with time older than current time.
When server moves object to the queue it sets flag hidden to the object and sends status update to all clients. So clients have the object in the game but it's invisible. When object gets back into the game hidden flag is switched off. When new player enters the game server sends all objects in the queue to the client.
Server computes collisions among objects (dynamic_collision() function in server.c file). Corpses, grenades shells doesn't collide. Weapons, ammo, armor and medikits collide with players. Bullets collide with players, shrapnels collide with players and corpses.
0verkill is a client-server game. In the game there's one server and unlimited number of clients. They communicate through UDP sockets. Server runs on an IP address and socket that are well known by the others.
Clients initiate connection with server. Each client has it's IP address and port. They are not so essential because server gets it with each packet. They don't have to be known by players.
Server and clients are numbered with IDs. Server's ID is always 0. Client's are numbered from 1. ID numbers are sent with packets too - to correctly determine sender and recipient.
Both server and client have their clock, game is updated (recomputed) every tick.
Server has entire information about the game. State of game on the server is one and only true. Clients can have a bit different informations (due to network inconsistency), but the server has absolute truth.
When everything's OK and server gets client's request he creates a new player, new hero object and sends all objects to the client. Now client's in the game and can control his hero.
As I've said server has own clock. In every tick server reads data on input - requests from clients (read_data() function), updates players (update_players() function )and updates objects in the game (update_game function).
Server has in each client's record time when last keyboard update came. Clients should send keyboard update every tick. But when client is more than 30 seconds dumb he's kicked out of the game.
This kickout wasn't implemented in earlier server's versions. So when client wanted to enter the game he sent request - it came to the server, server sent client "OK you're in the game" and created a new player. But due to a network error client didn't get it and though he he wasn't accepted and there was a dumb player in the game.
The only person client's communicating with is server. If there's a packet not only for server but for other players too, client sends it to server and server resends it farther. So networking topology is star - server's in the center.
As it was said clients have own ID. At the beginning when clients wants to enter the game he doesn't have any. So server sends information about player's acceptance to client's address but puts zero as recipient's ID to the packet and client expects recipient's ID to be zero. Then client reads his ID from the packet and hence expects all packet to have recipient's ID his ID.
Client in every tick reads data from the socket - game updates from the server, chat messages from clients ... (read_data() function), updates game - recomputes objects (update_game() function), draws view on the screen (draw_scene()), reads keyboard and sends keyboard status to the server (send_keyboard()).
Earlier client versions were sending keyboard update only when something changed. It loaded network a little bit less but on slow lines (e.g. 28.8k modem) motion was jerky. The line wasn't able to transmit all the packets so when keyboard update didn't come server though client didn't press any key so he didn't move the player.
You may think sending keyboard status even if nothing's pressed loads network too much, but during game you almost always hold any key. So it isn't so dramatic.
Clients have motion prediction to achieve smoother game. In every tick client updates his objects himself. Client knows velocities - he can recompute positions, he also knows which object slows down with friction - he can update velocities. Clients don't compute object collisions, it's server's job.
When client gets update of an object (packet contains object ID, current position, velocity, time, status and ttl) he copies velocity, status and ttl. And sets object's position to position in packet plus velocity multiplied with time difference among time in packet and current client's game time.
Packet contain not only data but also sender's and recipient's ID and CRC check.
CRC is 32-bit CRC checksum of own data (not sender's or recipient's ID !) as described in ISO 3309 standard.
There are two routines in file net.c for packet transmitting: send_packet() and recv_packet(). These functions are wraparound of sendto() and recvfrom() functions. They add crc check and sender's and recipient's ID to data and transmit it through network. They behave as there wasn't any additional information with data. recv_packet() returns number of received data bytes (not crc nor IDs) on success.
In this section I'll describe conventions used to store some data types in packets. I'll use following data types: byte, int, fixpoint, time and string.
Data in packet always start with one byte head. Head informs about type of following data. Here is overview of packets. Packet heads are defined in file net.h
Since version 0.12 0verkill contains improved version compatibility check. It guarantees that client and server with improper version numbers can't play the game.
Client sends his major and minor version number to server in the P_NEW_PLAYER packet. Server checks its version with client's version and accepts or denies the client.
If server accepts the client server sends his version number in the P_PLAYER_ACCEPTED packet. Now client checks versions and continues or sends quit to the server.
Now how check works. It is the same for both client and server only in one case server is denied and in second case client's denied, so I won't describe it extra for server and extra for client. I'll use "check fails" phrase - it means this combination of client and server won't work together.
To decrease network load, server sends most frequent packets in chunks. Packet created in one server's tick are stored in server's memory (separately for each client). When total size of packets exceeds MAX_PACKET_SIZE or when tick ends server sends a chunk packet (with head P_CHUNK) containing all the data from the buffer to the client.
This mechanism doesn't work at clients.
Chunked packets are:
And these packets aren't chunked:
0verkill has own routines for controlling screen and keyboard. They can also be used as stand-alone library. Output routines are in console.c, console.h files. Keyboard controlling routines in kbd.c and kbd.h files.
If you want to use it stand-alone, include console.h file in your source.
Keyboard has two modes. Standard and raw mode. In standard mode keyboard sends periodically information about key currently pressed. That has one disadvantage - you can't press more than one at once. During game like 0verkill it's quite essential disadvantage (you often need e.g. walk and shoot together).
Keyboard in raw mode sends information which key was pressed and which was released. But not all keyboads can be switched into this mode (when you're playing over telnet or in X). So I implemented both keyboards and if it's possible raw keyboard is used otherwise standard is used.
If you want to use keyboard you must initialize it first. To initialize keyboard function kbd_init() is called. This function tries to switch keyboard to raw mode or it it isn't possible it leaves it in standard mode. Global variable keyboard_type contains information about keyboard mode. It can be KBD_RAW or KBD_STD. Each function contains switch of keyboard_type and for each value one branch.
To shut down keyboard call kbd_close() - it returns keyboard into original mode.
Interface and data storage is raw mode orientated. There are two key tables one for current keyboard state (keyboard) and one for previous one (old_keyboard). Each table cell says if appropriate key is pressed or not.
Keyboard must be updated calling kbd_update() function. This function copies keyboard table into old table, reads keys from input and fills key table. It also handles virtual terminal switching and other events like ctrl-c pressing.
!!! NOR BREAK THE PROGRAM !!!
Function kbd_wait_for_key waits until any key is pressed.
There are two functions kbd_is_pressed() and kbd_was_pressed() for testing which key is actually pressed or which was pressed and not released since. Both functions have key as an argument. Key can be either character constant for a letter or a number or several other characters or a constant from kbd.h. They return 1 if key was pressed and 0 otherwise.
Standard keyboard has the same interface but functions are more simple and it doesn't handle virtual terminal switch nor break.
File console.c contains all necessary functions to handle console. Console is driven with ANSI terminal sequencies. Console is accessed through standard output. Output is buffered with functions fputc(), fprintf(), fwrite(). There is also my own buffering in the code, but it's commented out. It uses own buffer for output and writes to stdout using function write(). But built in buffering seems to be more effective.
This driver includes keyboard driver (which was described before) too. It wraps its functions into own functions.
To initialize console driver call c_init() function. It automatically initializes keyboard too. To shut it down call c_shutdown() function.
Because all functions for writing to console (cleaning, setting colors, writing text, ...) are buffered you must call c_refresh() function to changes take effect.
I'm gonna speak about coordinates on screen. Coordinates start in upper left corner of the screen, x coordinate goes to right, y coordinate goes down - both in positive direction. Upper left corner of the screen has coordinates [0,0].
I'm going to speak about colors too. 0verkill uses only foreground color and background color is always black. It's due to author's obsession that other background color will be ugly (or is it laziness? ;-) ) There are several functions for setting color - for setting separate highlight, separate color and to set both. It's speed optimalization - to write minimum of characters to stdout, because kernel has very sloooooooooooooow console driver.
And now description of functions:
As I've said before there are wrapped keyboard functions too. They have the same arguments, return the same value and do the same as functions in kbd.h. They are: c_pressed, c_was_pressed, c_wait_for_key and c_update_kbd.
Since version 0.11 0verkill has full support for X window system.
Functions for displaying under X are in xinterface.c file and functions driving X keyboard are in xkbd.c. All functions in these files use the same interface (defined in console.h and kbd.h) as console variants.
Game is displayed in single window, created in c_init() function. This function also initiates connection with the X server.
X interface has several global variables and constants which can affect layout. They are defined in x.h file. Most important are x_font_name which says name of the font used for displaying and x_display_name which determines X display.
Events comming from X server are caught in kbd_update() function. Interesting events are KeyPress, KeyRelease and ConfigureNotify and Expose. Last two ones raise SIGWINCH signal. Consequently picture is redrawn.
0verkill uses screenbuffer for displaying on the screen. Screenbuffer is array of size SCREEN_X and SCREEN_Y, it's stored in screen (textual part) and screen_a (attributes) variables. Whole scene is displayed into screenbuffer and then screenbuffer is flushed to stdout (blit_screen() function).
To achieve highest displaying speed attribute setting is high optimized. Number of characters written to stdout is minimized. Attributes are set only when necessary - when previous pixel color is different than color of currently drawn pixel.
There are three color setting functions: c_setcolor(), c_setcolor_3b() and c_sethlt(). When attributes differ only in highlight c_sethlt() function is called. When highlight doesn't change c_setcolor_3b() is called. In other cases the c_setcolor() is called.