@ Necklace of the Eye: development guide
Disclaimer
NotEye is still under active development. I try to make the changes backward
compatible, but it is likely that changes to NotEye make older NotEye games
stop working with new versions of NotEye. For this reason, NotEye is not
documented very well. If you want to use some of the more sophisticated
features of NotEye, you might run into lack of documentation. Do not let
this detract you, just send me an e-mail to zeno@attnam.com -- I will try
to answer your specific question, as well as you will push me to improve
the online documentation. Note that the NotEye port of PRIME has been made
almost without my help, so it is possible to do with the current docs, at
least if you are an experienced programmer.
Licensing
NotEye is released under
GPL (GNU General Public License) version 3, with the following
motivations:
I believe that, ideally, art should be for everyone to experience and share freely
(once it is
created), but still, authors should be credited for their work. For this reason, you
can use NotEye for free in your game/library, as long as the whole game/library is
free and open source.
If you want to use NotEye in a closed source game, just contact me for a permission
(a licensing exception) -- that's how it was done for ADOM (I think Thomas Biskup has
good reasons to make ADOM closed source). For example, you probably cannot use Oryx's
roguelike tileset in a GPL game, as its license is not compatible. So, if you want
to use Oryx's tileset together with NotEye in a closed source game, pay me for
a licensing exception of NotEye, as you have paid for using Oryx's tileset.
It would be wrong for someone to use NotEye in a paid game without
giving me a share, so NotEye's license forbids it. If you want to give me a share,
though, please contact me -- I am open for negotiations (depending on how "evil" your
idea actually is, for example a free (as in free beer) closed-source game and a paid
deluxe version which does not change the gameplay is probably OK with me, but I would
need to analyze this in case-by-case basis).
If you prefer your work to be licensed permissively (without GPL's restrictions),
that is OK with me, and GPL allows it too --
just put your own work under a permissive license, and distribute it together with
NotEye. The whole package will be licensed under GPL, but if somebody wants to
use your code for something that GPL does not allow, they can take your code without
NotEye and do what they want.
We have been discussing this
here.
Introduction
NotEye is a frontend for roguelikes. Its capabilities are listed
on
NotEye's main website.
This page is for developers and modders, and documents how to use NotEye with
new roguelikes.
Typically, if you want to use NotEye with a game, you need two things:
- the roguelike itself, which draws its ASCII screen in some kind of a console,
- a game script, which tells NotEye how to interpret data provided by the
roguelike.
Game script
The
game script has to be written in Lua.
NotEye has some sensible defaults, so a very simple
game script, which only specifies the map region (e.g. it tells NotEye
that the map resides in the rectangle from (0,2) to (80,22)), already
works -- it draws '#'
characters as walls, '.' characters as floors, etc. And right away
you get most of features of NotEye, like using Shift/Ctrl for diagonals,
isometric and FPP modes, screenshots, beautiful console with a selection of
fonts and palettes, and so on. More complicated game scripts
tell NotEye how to display the '+'
character in the map, how to animate the characters, how to display menus,
how to display several items on the same tile, how to animate missiles,
etc. Game scripts appear
in the "games" subdirectory, for example "games/hydra.noe" is the
(very complicated) game script for Hydra Slayer. Look at the examples
to get the idea.
Roguelike
A
roguelike can be made NotEye-compatible in one of the following ways:
- Simply use the system console. Most old roguelikes,
and some of the new ones, use the system console.
If you have such a game, but you have no access to the source code,
or you do not want to try to understand it, it is the easiest way
to go. However, it is not always possible or easy to interpret the ASCII
screen. For example, DoomRL uses the red '+' symbol both for doors and
for medkits, and thus doors might look like medkits; and Frozen Depths
uses the '@' symbol for some NPCs, and it might confuse the player character
with these NPCs. This approach is currently used for
NLarn, DoomRL, Frozen Depths, and Darren Grey's old
roguelikes, among others. If you are writing a new roguelike, this option
is not recommended -- use the NotEye library directly, with one of the
options below. You do not lose features of the system console, such as
SSH or blind people access -- NotEye can output to the system console,
thus it is possible to play via SSH or a screen reader. (Note that libtcod
does not currently have this feature.)
- Use libtcod. The Doryen Library is a library which displays
ASCII screens beautifully on modern hardware. Substitute the libtcod
library with NotEye's version, and NotEye can connect to them. Again,
this is relatively easy if you have a game using Libtcod, and no source
code access is required. Unfortunately, the process is quite cumbersome,
and the game needs to use Libtcod 1.5.1 (the only version for which NotEye's
substitute is compiled). I do not recommend this options for new games,
though. Even with the simplest possible game script, NotEye gives you more
presentation features than Libtcod. However, note that NotEye is just an
input/output library, while Libtcod also gives utilities which are useful in
roguelike programming, such as pathfinding, field-of-vision, or RNG -- if
you do not feel comfortable enough with programming such algorithms,
Libtcod might be better for you.
This approach is currently used for Brogue and Drakefire Chasm,
among others.
- Write the whole game in Lua. Basically, there is no separate
game executable, there is only the game script which is the whole game.
Look at "games/lusample.noe" for a sample game written completely in Lua.
This game is activated by pressing Shift+L in the NotEye menu. Lua is
commonly used in moddable games. It is also used by the T-Engine
(a roguelike engine), although it is currently impossible to use T-Engine
modules with NotEye.
- a C/C++ game, using the NotEye library. This is
recommended for C/C++ experts. If you have
a game written using the Curses library, or know how to use Curses,
this is straightforward -- simply replace Curses calls with their NotEye
versions, like move(y,x) with noteye_move(y,x). (You can also do #define
NOTEYE_TRANSLATE before including to make this
translation automatic.) Only the most basic Curses calls are currently
supported, though. This is what Hydra Slayer does, and it is
included with NotEye. You can also look at sample/c/sample.cpp, which is
a much simpler game created with this method. ADOM and PRIME
also do this, but they are not included. Since NotEye is written in C++
itself,
you can access many things directly, such as NotEye's objects,
or the SDL or Lua functions -- bindings for other programming languages
generally export less functions.
- a Pascal game, using the NotEye library. Pascal is not very popular anymore,
but there is an example in sample/pascal/sample.pas.
- a Python game, using the NotEye library. Many people recommend Python as
a great way to learn programming. Even the expert programmers use Python
because of how quick it is to write programs in it. It is straightforward
to use Python with C++ libraries, such as NotEye. Look at sample/python/sample.py
for a sample NotEye roguelike written in Python.
- a Java game, using the NotEye native library. Java is one of the programming languages
more frequently used in the industry. NotEye includes a sample roguelike
in Java, look at sample/java/Sample.java. If you are a Java programmer,
and you want a nice console roguelike, using NotEye is probably one of
your best choices. However, this comes at a cost -- Java is designed so that
its executables can be executed on any system, but Java+NotEye roguelikes
will be only available on systems where the NotEye library is compiled natively.
Starting to work with NotEye
Note: you can run NotEye with a parameter (e.g.
noteye -N -L noteye.log),
this way any problems will be sent to a diagnostic log file.
NotEye menu has some hidden options which help with development.
Press F5 to reload the scripts (so you can immediately see the effects of
your changes). Press F6 to see the reference about icons available in
RLTiles (F1), Vapors tiles (F2) and the font (F3). Press F7 to display some
helpful data about the game's display (coordinates, symbols, and color of
the character under the mouse cursor).
You should probably start from one of the configurations provided as examples
(
frozen.noe,
nlarn.noe,
rogue.noe,
sample.noe,
crawl.noe,
doomrl.noe,
adom.noe —
hydra.noe contains lots
of hacks to make NotEye display hex and is more difficult). Copy it
to
gamename.noe and add it to the menu in the
noteye.noe file.
The amount of work depends on how original the game's display is, and how
complete do you want your tileset to be. In some cases it should be enough
to simply copy one of the provided configuration files, and modify some
numbers. Usually you will need to modify the
mapregion variable.
It says which part of the screen contains the map in the given roguelike.
If there is something original, or you want more complicated features,
you will probably need to learn some Lua and understand how NotEye works.
'caller' is a script that either runs the game or tells the users to care for
themselves. The first parameter of caller3 is the full game of the name,
and the second one is used as both the name of the executable and the name of
the directory where the game is expected to be found.
Lua is designed so that you can replace functions by your own implementations
(which can call the original ones), and NotEye uses this. While the difficult
tasks like drawing the sprites are done natively in C++, most of the library
is written in Lua. If NotEye's implementation of something does not fit your
needs, you can replace NotEye's implementation with one of your own.
If you want to
provide tiles for more things, it is probably the easiest to
use "generic-tiles.noe" and modify the
xtileaux2 function (copy the implementation from the NLarn config,
for example, and modify it). If the game incorrectly guesses the position of the player character,
you can try changing the ispc function (like
hydra.noe which makes sure
that the @ sign is white, so that we don't take an e-mail address for a map), or
the
copymap function if the first method does not work. If you don't like
how some things are colored on the minimap, change the
xminimap function.
And so on.
NotEye library, accessed from the game script
NotEye library handles images, tiles, screens (rectangular blocks of tiles),
fonts, and game processes. Typically, the game script
accesses functions provided by the NotEye library; these are written natively
in C++, so they work fast, but you cannot change the implementation. Typically
they are not used by the game itself, but you can do this if you want
(in C++ games, with Python or Java it is probably harder).
For example,
Hydra Slayer directly manipulates Images, so that it
can procedurally generate hydra pictures.
Here is a short description of concepts available in NotEye:
Object
Objects include images, fonts, tiles, screens, and game processes. You create
objects by calling functions implemented by NotEye, and use them by calling
other functions. These functions return numbers which act as identifiers for
these objects. You can use
delete(x) to delete objects, and
objcount() to know how many objects have been allocated so far.
Image
Images are just that, images, that is, rectangular blocks of pixels.
The following Lua functions work on images:
- newimage(x, y, color) — create a new image of given
size, filled by the given color (returns the index of the image)
- loadimage(filename) — create a new image by loading
it from the file (returns the index)
- saveimage(I, filename) — save an image to a file
- setpixel(I, x, y, color) — sets a pixel in an image;
color is given in RGB, e.g. 0xFF8000 = orange.
Columns are indexed from 0 to X-1, and rows
are indexed from 0 (top) to Y-1 (bottom).
- getpixel(I, x, y) — gets a pixel from an image
- imgcopy(I1, x1, y1, I2, x2, y2, width, height) — copy a
fragment of I1 to I2
Graphics to display
Gfx is an Image, so saveimage, setpixel, getpixel, and imgcopy
functions work on it — you need to give Gfx (which equals 1) as the
image. Also the following are required:
- setvideomode(X, Y, F) - set the given resolution, F says
whether we want to set full screen mode. Returns true if successful, false
if not successful.
- updaterect(X, Y, W, H) - copy a rectangle from Gfx to
be in fact displayed on the screen.
Screen
A screen is a rectangular block of Tiles. Screens can be drawn on an Image,
and also can be connected to a Process, so that the Screen contains
the console output from a roguelike.
- newscreen(X, Y) - create a new screen of given size
(returns the index). Columns are indexed from 0 to X-1, and rows
are indexed from 0 (top) to Y-1 (bottom).
- scrwrite(S, X, Y, text, Font, color) - write on the
screen S at position (X,Y), using given font and color.
- scrget(S, X, Y) - get a Tile at the given position.
- scrset(S, X, Y, Tile) - set a Tile at the given position.
- scrcopy(S1, X1, Y1, S2, X2, Y2, SX, SY, F) - copy
a SX x SY fragment of the screen S1 at position (X1,Y1) to screen S2
at position (X2,Y2). This function can not only copy the screen, but
also modify it in arbitrary ways, for example replace a character
with its minimap or graphical tile. Function F(T,x,y) is called for
each tile, with parameters T - the tile in the original screen,
x,y - the coordinates (relative to the S1). (You can use eq for F
if you want to simply copy.)
- scrfill(S, X, Y, SX, SY, T) - fill a SX x SY rectangle at
position (X,Y) in screen S with tile T.
- scrsave(S, filename, type) - save a HTML or phpBB screenshot
(depending on type) of S to filename.
- drawscreen(I, S, X, Y, TX, TY) - draw the screen S on image I,
at offset (X,Y), with tiles of size (TX, TY).
Tiles
Screens are made of Tiles. This is a general concept that encompasses both
console characters that roguelikes output and the tiles shown on screen,
and even is allowed to include 3D information about how a tile will look
in the FPP view. You can create new tiles with the following:
- addtile(I, X, Y, SX, SY, trans) - create a tile by cutting
the rectangle from the image I at (X,Y) size (SX,SY). The value 'trans'
contains the color that is interpreted as transparent (use -1 if the tile
is not transparent). Note: tiles are sometimes cached, so if you modify the
image, the tile might be changed or not.
- tilefill(color) - a tile filled with color C. (In FPP mode
this is currently supported only for floors and ceilings.)
- tileshade(color) - a semi-transparent tile filled with color C.
(Also partially supported in FPP)
- tilemerge(T1, T2) - create a new tile by superimposing
T2 on T1.
- tilerecolor(T, C) - recolor a tile using color C. Recoloring
does not change the shades of gray, only the pixels which have some
saturation. Technically, it calculates the minimum and maximum component,
and replaces each component by a value that is also between this minimum
and maximum, but based on C.
- tilespatial(T, S) - specify how the tile T will look in 3D
(by returning a new index which has this information inside). You can
use spFloor, spCeil, spWall, (draw as a part of the floor/ceiling/wall,
respectively), spMonst (draw large objects in front of the cell),
spItem (draw small objects). You also need to add spFlat if you want the
tile to be shown on 2D screens too.
- tiletransform(T, DX, DY, SX, SY) - moves the tile by
(DX,DY) and resizes it (SX,SY) times.
For example, tiletransform(T, -0.5, 0, 2, 1) makes the tile two times wider
(2), and the left edge moved by 0.5 of the original width to the left
(in effect, the center remains the same). This is not supported in FPP.
These functions return the tile index that you can use on screens.
Warning: tile indices are not recycled yet. This means that every combination
will use up some memory. In typical 15-color ASCII games which do not use
background colors there is no problem (15 colors times 100 ASCII characters
= 1500), but with true color and animated tiles, the siutation becomes more
complicated. Will probably fix it someday.
Fonts
Fonts are created with
F = newfont(I, x, y, trans). I is an image which
contains the characters. There are x characters in each row, and
y columns (there should be x*y == 256). The value 'trans' contains
the color treated as transparent. You can get the tile for the given
character with
fget(F, ch), e.g.
fget(F, "@").
Process
Processes are games, command shells, and other things that NotEye can run and
process its screens.
- newprocess(Scr, Font, cmdline) - create a new process. The
process will use the screen Scr as its display, and use the font Font.
Commandline is given as cmdline.
- sendkey(P, Event) - send a keydown/keyup event to the process P
(see events later).
- proccur(P) - get the cursor position from the process P,
as a {x=..., y=...} table.
- processactive(P) - tell if the process is still active (although
you should know that anyway).
- vgaget(i) - processes use a 16 color palette. This function gives the
24-bit color associated with the given index; also the next four bits contain
the original palette index (this has no use yet, but in future is planned to
be used to translate the color back to a 16 color palette). (Currently this
is not associated to a process, although it maybe should.)
- vgaset(i, color) - change the palette.
The screen associated to a process is filled with tiles of form:
tilemerge(tilefill(vgaget(BACKGROUND)), tilerecolor(fget(Font, CHAR), vgaget(FOREGROUND)))
which can be correctly displayed using
drawscreen. You can also
process this form using functions gch (get character), gco (get foreground color
(as a full color, not palette index), gba (get background color),
gp2 (get just the character, without the background).
(to be continued)
NotEye common script
Most of the NotEye logic (like graphical modes, menus, etc.) are
implemented in Lua. This means that if something does not fit your needs,
you can replace NotEye's implementation with one of your own.
All this is placed in the
common directory. Just look there.
Hopefully, all the functions and variables are commented. Of course,
if you have seen a feature in some NotEye game, it might be possible that
this feature is implemented only in this game's game script -- you can read
the game script and learn from it, or copy the feature from it. For example,
hex display is implemented in the
Hydra Slayer's game script. Typically,
such stuff is moved to
common when I feel that it is generic enough to
be used in more than one roguelike.
Here are some highlights:
(none yet)
NotEye library, accessed from the game itself
If you want to access the NotEye library directly from your game,
read one of the samples. Simple Samples in C, Pascal, Python, and Java are included
with NotEye. Hydra Slayer (C++) is also included, although it is also much more
complicated. You can also download the source of
PRIME, which also
uses NotEye.
We use C/C++ in the following. The names of functions might be a bit different in other
languages. Note that in C++ you need to use the namespace to access the functions:
using namespace noteye;.
Initialization
The following functions are typically called during the initialization:
- noteye_args(int argc, char ** argv) -- this function provides
arguments to NotEye. They can be accessed with the argv(i) function
in the script. You probably want to call this if you want to make it possible to the
players to give arguments to NotEye, but is not very important.
- noteye_init -- this initializes NotEye. NotEye is closed with
noteye_halt.
- noteye_globalstr(const char *name, const char *val) -- set a global variable
in the Lua script. Samples set the "noteyedir" variable to make sure that the games
find all the NotEye files. There are also similar functions noteye_globalint
and noteye_globalfun. (You could also use the Lua functions directly.)
- noteye_run(const char *filename, bool applyenv) --
run the Lua script given by filename. Typically this sets some basic variables
(like game_to_launch to tell which game to launch) and calls NotEye's main
script (common/noteye.noe). After that, the NotEye script runs in a separate
thread ("UI thread") (actually, a Lua coroutine).
Output
Output is based on the Curses functions.
Use
noteye_getinternalx() and
noteye_getinternaly()
to get the console size.
Use
noteye_move(int y, int x) to move the cursor to
the given location (
y and
x are 0-based). Use
noteye_addch(char ch)
to write a character at this location, or
noteye_addstr(const char *s)
to write a string. You can also use
noteye_mvaddch
or
noteye_mvaddstr which moves and writes in one call. Change the color
with
setTextAttr(int fore, int back), where
fore
and
back are foreground and background color indices, using the palette (as
shown by NotEye), or with
setTextAttr32(int fore, int back),
which accepts fore and back arguments in true color -- use the 0xFRRGGBB format, where F
is the index of color in the palette (NotEye has a 16-color terminal output, and this is
useful if you want to make your game look decently here). You can also use
int getVGAcolor(int c) to get the true color value of the palette color
number
c.
The screen is automatically refreshed if the UI thread is resumed, which happens
when you wait for input (see the next section) or you call the noteye_refresh() function.
Input
Normally, use
noteye_getch() to wait for a keypress and return.
Note that keypresses are not read directly from the keyboard -- the gamescript may
perform some preprocessing (usually, direction keys are preprocessed in isometric/FPP
modes).
Normal characters are returned as their ASCII values. Function keys are returned as
KEY_F0 + number (thus, F1 is KEY_F0 + 1). Directions are DBASE + i, where i is from
0 (east) counterclokwise to 7 (southeast). (Use the dx/dy tables from the samples
in order to translate these directions to vectors which can be added to positions.)
Keypad center is DBASE + 8.
For more complicated keyboard handling, you can use
noteye_getchev().
This returns on all keypresses (including the non-printable ones such as Shift)
and key releases. You can call
noteye_getlastkeyevent(), which returns
the last key event in the SDL key event format (
noteye_getlastkeyevent()->type
tells whether it is a keypress or release,
noteye_getlastkeyevent()->key.keysym.sym
gives the SDL symbol of the key,
noteye_getlastkeyevent()->key.keysym.unicode tells the Unicode value of the
character,
noteye_getlastkeyevent()->key.keysym.mod gives information about
the modifier keys used, etc.)
Use
noteye_halfdelay(int i) to wait at most 100
i ms for the
next keypress. Use
i=0 for a quick return (after resuming the Lua thread
for one iteration of the cycle). Use
noteye_cbreak() to wait forever.
Error handling
Use the
noteye_handleerror to add your own error handler, which will
be executed in case of an error in the Lua script. If errors in the Lua script have
caused the UI thread to crash,
noteye_getch() will return NOTEYEERR. This
usually can be fixed by reloading the NotEye scripts -- halt NotEye (
noteye_halt()),
initialize it (
noteye_init), and run the gamescript (tt>noteye_run)
again (no need to reset the globals).
How to call functions from Lua
(Note: In C++ you could also use the Lua functions directly. NotEye simply provides
wrappers so that you do not have to have Lua libs installed to compile your game.)
See the samples (this is shown in all of them). During the initialization, do
noteye_globalfun to add a global function accessible from Lua.
Your function gets one argument,
struct lua_State *L.
Access the arguments with
noteye_argcount(L) (the number of arguments),
and
noteye_argInt(L, i) (i-th argument as an integer),
noteye_argNum(L, i) (i-th argument as a floating point number),
noteye_argBool,
noteye_argStr (likewise). (NotEye
does not include wrappers for table arguments yet.)
Your function returns an
int, which is the number of the arguments returned.
Use
noteye_retInt(L, i) to return the integer
i, similarly
there ar functions
noteye_retBool and
noteye_retStr. If you want to return a table, call the
noteye_table_new function, then other
noteye_table_*
functions to set its fields (see the samples).