Table of contents
What you will learn here:
- General
- Color format
- Coordinates
- Callbacks
- on_paint(boolean not_synth)
- on_video()
- Functions
- gui family
- number color gui.color(number red, number green, number blue, [number alpha])
- number color gui.rainbow(number steps, number steps_per_revolution, number base_color)
- number width, number height gui.resolution()
- number OldGap gui.left_gap(number pixels)
- number OldGap gui.right_gap(number pixels)
- number OldGap gui.top_gap(number pixels)
- number OldGap gui.bottom_gap(number pixels)
- number OldGap gui.delta_left_gap(number pixels)
- number OldGap gui.delta_right_gap(number pixels)
- number OldGap gui.delta_top_gap(number pixels)
- number OldGap gui.delta_bottom_gap(number pixels)
- gui.text(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textH(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textV(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textHV(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.pixel(Number x, Number y, [Number color])
- gui.line(Number x1, Number y1, Number x2, Number y2, [Number color])
- gui.crosshair(Number x, Number y, Number size, [Number color])
- gui.rectangle(Number x, Number y, Number w, Number h, [Number thickness], [Number outline_color], [Number fill_color])
- gui.circle(Number x, Number y, Number r, [Number thickness], [Number outline_color], [Number fill_color])
- gui.box(Number x, Number y, Number w, Number h, [Number thickness], [Number hilight_color], [Number shadow_color], [Number fill_color])
- gui family
Drawing in your display
There are many gui functions. They are listed in lua functions, but I will work through them in a different order than listed there. Each section here is stand-alone, so if you get confused at one section, continue to the next, or just take a break. There's no better way to learn than to actually work these functions as you read, so I recommend you do just that.
You will define a global function named on_paint for nearly everything that you need to draw. If you wish to encode a video with a lua display imposed in it, you will define a global function named on_video which will instead paint the video file with the gui functions rather than the lsnes display you see.
You don't need to stick all your gui function calls in a single on_paint function. You can have other functions with gui calls, and on_paint or on_video can call those to update the display or video as needed.
lsnes passes on_paint a boolean value. It is true if the display was done automatically (such as after a frame advance), and false if done manually by a user command or the lua script called gui.repaint(). The distinction usually isn't important for drawing, but if there's something that needs you to look at the frame count, this is helpful.
on_video does not get this boolean value. There isn't any case where you would be able to update the video manually like you would the lsnes display.
The format of colors
Drawing functions in lsnes use a number to determine color. As follows:
- 0xRRGGBB - Opaque color of choice
- 0xaaRRGGBB - Color with selected transparency
Each pair of hexadecimal digits correspond to transparency (aa) or the intensity of red (RR), green (GG), or blue (BB). Getting other colors is to use a mix of these latter three. Yellow uses 0xFFFF00, cyan uses 0x00FFFF, magenta uses 0xFF00FF, white uses 0xFFFFFF. Understanding why this is requires knowing human color perception.
A larger alpha (aa) will make the color more transparent. An alpha of 255 or 0xFF is almost fully transparent. A fully transparent color uses a special number, -1. Not -0x01000000, but simply -1. If you can't see it, it doesn't matter what color it is anyway.
There are functions that will return an lsnes-readable color, if you prefer that instead of dealing with hexadecimal numbers.
- number color gui.color(number red, number green, number blue, [number alpha])
Returns a number that lsnes gui functions use for colors.
This function takes three numbers, each from 0 to 255. gui.color will then return an lsnes-readable number used as an opaque color. You can specify a fourth number, an alpha of 0 to 256, to determine transparency of this number.
- number color gui.rainbow(number steps, number steps_per_revolution, number base_color)
This function takes three numbers. It works in a circle: The second number tells you how big the circle is, and the first number tells you how far you traveled around the circle. One full circle takes you back to the same color. The rotation here is the change in the hue of the color, which you've given as the third number.
The hue is what changes. Increasing your steps will take you: Red -> Yellow -> Green -> Cyan -> Blue -> Magenta -> Red. The saturation (how little grey), luminescence (how bright), and opacity are preserved.
The right coordinates
All gui functions that paint something on the screen ask for coordinates, by pixel, as the first two arguments. Coordinates given to lsnes does not necessarily reflect game coordinates, but whatever drawing area lsnes uses.
- number width, number height gui.resolution()
TODO: Work out the coordinates using on_paint.
Gaps and their value
- number OldGap gui.left_gap(number pixels)
- number OldGap gui.right_gap(number pixels)
- number OldGap gui.top_gap(number pixels)
- number OldGap gui.bottom_gap(number pixels)
- number OldGap gui.delta_left_gap(number pixels)
- number OldGap gui.delta_right_gap(number pixels)
- number OldGap gui.delta_top_gap(number pixels)
- number OldGap gui.delta_bottom_gap(number pixels)
These functions produce a black area off the sides of the game area. This area gives you more drawing room for your Lua scripts, and a place to draw useful numbers without covering the game area. They need to be called inside on_paint every time, as lsnes will default to a gap of zero the next time it repaints the display if the gap function isn't called the next time.
Keep in mind that, even with left_gap and top_gap, coordinates 0,0 is still the top-left pixel of the game area, and not the top-left corner of the new drawing area. If you need to draw there, negative coordinates are to be used.
To draw off the right and bottom edges, it's best to use gui.resolution() first to get the position of the right and bottom edges. The right and bottom gap drawing areas will start from the x or y coordinate returned by gui.resolution() and increase the further right or down you go.
The difference between the delta and non-delta versions are that, if you call the same side gap a second time, the non-delta will override the old gap, while the delta will instead add to the old gap. The difference is meaningless if you don't plan on chaining scripts together, but the delta gaps are useful if you wish to produce scripts that use gaps without needing to know what other scripts are doing.
These functions also return a number. It's the size of the gap in pixels before your latest call. It's always zero if it's the first call for that gap in on_paint, as there doesn't exist a gap yet. Subsequent calls return the old size. You'll only ever worry about these numbers when you need to make multiple scripts that use gaps.
A colorful look on gui.text
- gui.text(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textH(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textV(number x, number y, string text, [number ForeColor], [number BackColor])
- gui.textHV(number x, number y, string text, [number ForeColor], [number BackColor])
I have mentioned gui.text in an earlier tutorial, so that you have something to do in your starter scripts. Now I will give further details beyond just the fact it lets you paint text onscreen.
Required are the coordinates and text to paint. The first three parameters specify these. The fourth parameter specifies a color the text itself will be painted in, and fifth will specify the rectangle color surrounding the text. Default colors are white for the text and transparent for the rectangle.
If you aren't placing the text in the gap area as outlined above, it is recommended to try 0x40000000 as the background color to keep your text readable. As lua syntax doesn't allow you to specify a fifth parameter without also specifying a fourth, just use 0xFFFFFF (white) there. An example: gui.text(8,8,"Easily seen.",0xFFFFFF,0x40000000)
There are four versions of gui.text. textH is double the width, and textV is double the height. textHV is double sized text. They are otherwise identical to gui.text.
Various drawing functions
- gui.pixel(Number x, Number y, [Number color])
- gui.line(Number x1, Number y1, Number x2, Number y2, [Number color])
- gui.crosshair(Number x, Number y, Number size, [Number color])
- gui.rectangle(Number x, Number y, Number w, Number h, [Number thickness], [Number outline_color], [Number fill_color])
- gui.circle(Number x, Number y, Number r, [Number thickness], [Number outline_color], [Number fill_color])
- gui.box(Number x, Number y, Number w, Number h, [Number thickness], [Number hilight_color], [Number shadow_color], [Number fill_color])
TODO: Explain these. In the meantime, try them out and learn them yourself!
How bitmaps work
All these functions go together:
- BITMAP gui.bitmap_new(number width, number height, false)
- DBITMAP gui.bitmap_new(number width, number height, true)
- BITMAP/DBITMAP gui.bitmap_load(string filename)
- gui.bitmap_pset(BITMAP/DBITMAP bmp, number x, number y, number c)
- PALETTE gui.palette_new()
- gui.palette_set(PALETTE pal, number idx, number color)
- gui.bitmap_draw(Number x, Number y, BITMAP bmp, PALETTE pal)
- gui.bitmap_draw(Number x, Number y, DBITMAP bmp)
- Number width, Number height gui.bitmap_size(BITMAP/DBITMAP bmp)
- gui.bitmap_blit(BITMAP target, number dx, number dy, BITMAP source, number sx, number sy, number w, number h, [number colorkey])
- gui.bitmap_blit(DBITMAP target, number dx, number dy, DBITMAP source, number sx, number sy, number w, number h, [number colorkey])
There are actually three separate object types to learn about here.
BITMAP is a bitmap. Each pixel uses a color index to get the color from an accompanying PALETTE.
PALETTE is the set of colors a bitmap uses. You can have multiple PALETTEs so that the same bitmap can show up with different colors.
PALETTE is the set of colors a bitmap uses. You can have multiple PALETTEs so that the same bitmap can show up with different colors.
DBITMAP is a direct bitmap. Each pixel is represented by an exact color to paint. They can be drawn with gui.bitmap_draw without an accompanying PALETTE.
Miscellaneous functions
These functions can be called anywhere. They serve a variety of other purposes that isn't drawing directly to the screen.
- gui.screenshot(string filename)
A snapshot of the game's current display is stored in a file. The string you provide is the destination file.
- gui.repaint()
Requests lsnes to update the display as soon as convenient. This is usually when the current lua code finishes executing. This will also trigger the on_paint() callback, passing it a false value. Generally a good idea if you use on_keyhook or on_button for various controls to the script. Also good to have outside of any function so that you get immediate feedback upon running your script.
lsnes will refuse to respond to this function in an on_paint() callback, instead throwing a lua error. Otherwise, this would be a fast way to cause a lock-up. If you really need to call on_paint from within on_paint, do so directly, but be sure to have some sort of escape condition for any recursive calls.
- gui.subframe_update(boolean enabled)
If you give it true, lsnes will update the display whenever it would call on_input, passing the on_paint callback a value of false. If you give it false, lsnes will resume default behavior to automatically update display only on frame boundaries.
This will slow down emulation, as now lsnes is busier trying to keep the display updated.
- gui.status(string name, string value)
Adds a new item to the status panel. The given name and value will show up on the panel. lsnes will not have duplicate names on the panel, instead updating the existing named item with the new value you pass. Passing an empty string value will remove the named item from the status panel. You can use this as some sort of alternate memory watch, if you really want to avoid on_paint for some reason.
Even a Reset Lua VM won't clear any changes made by gui.status.
Breaking old colors
If you have drawn using lua scripts in other emulators, you may have used these formats:
- Number: 0xRRGGBBaa
- String: "#RRGGBB"
- String: "#RRGGBBaa"
- String: Specific names such as "red", "green", ...
- Table: {r = 0xRR, g = 0xGG, b = 0xBB, a = 0xaa}
- Table: {0xRR, 0xGG, 0xBB, 0xaa}
In some emulators, the number format is broken if the red value is greater than or equal to 0x80, due to the point where a 32-bit signed integer won't fit such a large positive value. The other formats are glitch-free in such emulators.
In lsnes, you can only use a number format, and even there, some differences exist. First, lsnes uses 0xaaRRGGBB, not 0xRRGGBBaa. Second, higher values of alpha makes the color less opaque, with 0xFF000000 being almost fully transparent. This design was made in mind so that, if you didn't want to include alpha in your color, you can type 0xRRGGBB and have a fully opaque color of your choice.
If you need a 100% transparent color, lsnes uses a special number for that: -1
There are functions you can call in lsnes if you prefer it giving you a color to use instead of having to recall lsnes number format. gui.color will return a number that lsnes can use. gui.rainbow will return a number with a different hue but same saturation and opacity as one you put in.
- number color gui.color(number red, number green, number blue, [number alpha])
Returns a number that lsnes gui functions use for colors. - number color gui.rainbow(number, number, number base_color)
FatRatKnight: I still need to produce examples. I'll fix them up at some point. I lack any encoding experience, so on_video hasn't been touched by me.