Ved home   Plugins   Technical documentation

Ved technical documentation

This page is intended to document Ved's internals as well as possible. The page is still being worked on, and more things are being added over time. If you'd like to know more about anything specific in Ved that I haven't explained yet here, or not well enough (except for the easter eggs :P), feel free to ask! If you'd like more information on how to make plugins, check this page. The repository is here.

Last updated: Thursday 4 November 2021 16:57 (UTC) (this is the last edit date of the file)


The following source files are used in Ved. Note that this table may not be entirely accurate - the "Added" column may not always be filled in even if a file didn't exist since the early days of Ved for example, some files may have been removed from the list altogether when they were removed, some changes may not yet have been documented here, and some files may have had more complicated overhauls. Version numbers in parentheses indicate that basically, the file had either been renamed, or something else is the matter that should be explained in the description.

callback_*.lua1.8.4Contain all love.* callbacks. For example, callback_load.lua loads most other source files and assets.
clargs.luaStores and formats the command line help output when requested.
conf.luaLÖVE's configuration file, controlling default window settings and loaded LÖVE modules.
const.luaConstants - Contains known scripting commands, music names, and other lookup tables. Also contained tileset data before 1.8.4.
coordsdialog.lua1.4.0Contains code related to the little room coordinates input after hitting Q in the main editor. Before 1.4.0, this was part of dialog.lua.
corefunc.luaContains a few functions that are used so early in loading (and/or are used on the crash screen), they must exist before things like plugins and the error handler are loaded.
coretext.luaContains functions for loading fonts, language files, and printing text.
devstrings.luaUsed for defining new text strings during development of a new version, before putting them in all the language files.
dialog.luaContains code related to dialog boxes. Before 1.4.0, this also contained code for right click menus, scrollbars and VVVVVV-style text boxes, which have each been moved to their own separate files as of 1.4.0.
dialog_uses.luaContains callback functions and definitions of fields for dialogs, which are used as arguments for dialog.create(...)
drawhelp.lua1.8.4Holds drawhelp(), called by love.draw() in state 15 (the help state). The help system is also used for level notes and the plugins list.
As of 1.8.4, this code was moved to uis/help/draw.lua.
drawlevelslist.lua1.8.4Holds drawlevelslist(), called by love.draw() in state 6 (the loading screen state).
As of 1.8.4, this code was moved to uis/levelslist/draw.lua.
drawmaineditor.lua1.8.4Holds drawmaineditor(), called by love.draw() in state 1 (the main editor state).
As of 1.8.4, this code was moved to uis/maineditor/draw.lua.
drawmap.lua1.8.4Holds drawmap(), called by love.draw() in state 12 (the map state).
As of 1.8.4, this code was moved to uis/map/draw.lua.
drawscripteditor.lua1.8.4Holds drawscripteditor(), called by love.draw() in state 3 (the script editor state).
As of 1.8.4, this code was moved to uis/scripteditor/draw.lua.
drawsearch.lua1.8.4Holds drawsearch(), called by love.draw() in state 11 (the search state).
As of 1.8.4, this code was moved to uis/search/draw.lua.
entity_mousedown.lua1.9.0Contains handle_entity_mousedown(). Handles (right) clicking on entities and creating right click menus for them.
errorhandler.luaContains code for both the crash screen and the plugin error screen.
filefunc_linmac.lua1.5.0Since Ved 1.5.0, this contains functions necessary for accessing the VVVVVV levels and graphics folders on Linux and macOS. This uses the vedlib_filefunc_* library found in the libs folder via LuaJIT FFI. Also see love.load() in main2.lua: On Linux this library is compiled locally, if unsuccessful (due to missing gcc) we'll fallback to filefunc_lin_fallback.lua instead. On Mac, an already compiled version of the library is used.
Before Ved 1.5.0, this was split in filefunc_lin.lua and filefunc_mac.lua and used terminal utilities to list level files.
filefunc_lin_fallback.lua(1.5.0)Contains functions necessary for accessing the VVVVVV levels and graphics folders on Linux, if compiling the filefunc library was not successful (due to missing gcc). This uses command line utilities like ls to list level files and some other file-related things.
Before 1.5.0, this file was called filefunc_lin.lua, because this was the only method that existed.
filefunc_luv.luaContains fallback love.filesystem functions for accessing fallback levels and graphics folders if the operating system is something other than Windows, macOS or Linux.
filefunc_mac.lua1.5.0Contained functions necessary for accessing the VVVVVV levels and graphics folders on macOS. Used command line utilities like ls to list level files and some other file-related things.
filefunc_win.luaContains functions necessary for accessing the VVVVVV levels and graphics folders on Windows. As of Ved 1.5.0, this uses the Windows API for everything (including reading and writing level files, due to being non-Unicode on Windows), before 1.5.0, it used command line utilities like dir.
func.luaContains many functions, especially general-purpose ones and core Ved functions.
helpfunc.luaContains certain functions related to (editing) level notes, and the rest of the help system.
Contains platform-specific code for making HTTPS requests.
These files were added in Ved 1.8.3 and 1.8.4.
imagefont.lua1.4.0Loads and readies font.png for use inside Ved.
incompatmain8.lua(1.4.5)If LÖVE 0.8 or lower is used, this is loaded from main.lua. It displays a message that outdated LÖVE is being used in all available languages.
Before Ved 1.4.5, this file was called incompatmain.lua.
incompatmain9.lua1.4.5If LÖVE 0.9.0 is used, this is loaded from main.lua. It displays a message that LÖVE 0.9.0 is no longer supported in all available languages.
input.lua1.8.0Contains the new input system.
konami.lua(1.8.4)Handles the shortcut that can be used in the help screen to make text editable. Before Ved 1.8.4, this file was called keyfunc.lua.
libs/Folder containing some C and Objective-C support libraries for Linux and macOS, and C header files for those libraries and parts of the Windows API, for use with LuaJIT FFI.
loadallmetadata.luaReturns level metadata for the levels list from a different thread.
loadconfig.luaHandles anything related to the settings.
love10compat.luaLoaded only when LÖVE 0.10.0 or higher is used, and provides compatibility with those versions. Contains the new love.wheelmoved callback.
love11compat.luaLoaded only when LÖVE 11.0 or higher is used, and provides compatibility with those versions. For example, this hijacks color functions so they work with 0-255 instead of 0-1.
main.luaThe first file that is loaded. Loads the fonts, sets a few basic variables, and loads plugins.lua, errorhandler.lua and, most importantly, all the callback_*.lua files (or main2.lua before 1.8.4).
main2.lua1.8.4Contained all the LÖVE callbacks that are now split into callback_*.lua files.
mapfunc.lua1.4.2Contains functions related to rendering and updating the map overview screen.
music.lua1.6.0Handles reading and writing vvvvvvmusic.vvv, mmmmmm.vvv, and other custom music files.
ogg_vorbis_metadata.lua1.9.0Decodes some metadata from Ogg Vorbis files, like sample rate, and Vorbis comments (for loop points).
playtesting.lua1.8.0Contains code relevant to playtesting in VVVVVV.
playtestthread.lua1.8.0The thread that starts up VVVVVV (dependent on OS of course) and waits for it to be closed.
plugins.luaMakes sure plugins and their file edits and hooks are loaded
resizablebox.luaHas a system for a box that can be resized by dragging borders with the mouse. Was formerly used for resizing script boxes, but it was glitchy so it's now unused.
rightclickmenu.lua1.4.0Contains code related to right click menus. Before 1.4.0, this was part of dialog.lua.
roomfunc.luaContains functions related to rooms in levels, tiles and such.
scaling.luaHijacks/Decorates a couple of LÖVE functions to make scaling work perfectly
scriptfunc.luaContains functions related to scripts.
scrollbar.lua1.4.0Contains code related to scrollbars. Before 1.4.0, this was part of dialog.lua.
searchfunc.luaContains functions related to searching levels.
slider.luaUsed for the number controls like in the options screen
tileset_data.luaContains tile numbers for all tilesets.
tool_mousedown.lua1.9.0Contains handle_tool_mousedown(). Handles general clicking on the canvas in the main editor, including all the tools, and placing down moved entities. Excludes (right) clicking on entities, see handle_entity_mousedown() (entity_mousedown.lua) for that.
ui_elements.lua1.7.0Contains all the GUI elements
uis/1.7.0Folder with UI files for each state (see below).
Note that in 1.8.4, each state was changed from a single file to a folder with each callback in its own file.
updatecheck.lua(1.8.4)Contains functionality for the update check.
This file was added in Ved 1.8.4-pre14. Before, this filename was used for the actual update checking thread (see updatecheckthread.lua)
updatecheckthread.lua(1.8.4)Checks what the latest version of Ved is via HTTPS, and reports back. This is run inside a separate thread.
Before Ved 1.8.4-pre14, this file was called updatecheck.lua.
utf8lib_*.luaImplements or supplements necessary parts of the Lua utf8 module, depending on LÖVE version
vvvvvvfunc.luaImplements some code from VVVVVV in Lua, mostly for displaying accurate colors.
vvvvvv_textbox.lua1.4.0Contains code related to VVVVVV-style text boxes. Before 1.4.0, this was part of dialog.lua.
vvvvvvxml.luaLoads and parses levels from .vvvvvv level files, and creates and saves them. Also has a function for "loading" a blank level.


Ved uses state numbers to represent different screens, menus and interfaces. Blue state numbers are not normally used anymore, and/or are not normally accessible, and many of them are leftover testing states. Red, struck-through state numbers have been removed from Ved altogether (and won't be reused).

As of 1.8.2, most of the code specific to each state can be found in the uis/ directory. States have their own versions of LÖVE callbacks (such as ui.update(dt), ui.keypressed(key), ui.mousepressed(x, y, button), etc). Furthermore, user interfaces can be built up of "Elements" which may automatically implement their own callbacks based on their parameters and position. For example, buttons can be defined to automatically be drawn at the correct position, and to execute the same action when it is clicked and when a given shortcut is pressed. For more information, see the GUI elements section. It should be noted that states can also implement ui.draw(), which is called before the Elements are drawn.

#UI nameDescription
-3Black screen
-2inittostate 6
-1Display error (expected: errormsg)
0state0Jump to any state number you want. Can be accessed in debug mode by pressing F12.
1The editor (will expect things to have been loaded)
2Syntax highlighting test
3scripteditorScripting editor
4Some XML testing
5fsinfoFilesystem info
6levelslistListing of all files in the levels folder, and load a level from here (loading screen)
7spriteviewDisplay all sprites from sprites.png where you can get the number of the sprite you're hovering over
8Ancient save screen (you can type in a name and press enter)
9dialogtestDialog test, and right click menu test
10scriptlistList of scripts, and enter one to load
13optionsOptions screen
14enemypickertestEnemy picker preview
15helpHelp/Level notes/Plugins list
16Reserved for scroll bar test, never used
17Reserved for folderopendialog utility, never used
18unreinfoShow main editor undo/redo stacks
19scriptflagsFlags list
20resizableboxtestResizable box test
21overlapentinfoDisplay overlapping entities (may be a visible function later) (maybe doesn't work properly)
22Load a script file in the 3DS format (lines separated by dollars)
23Load a script file NOT in the 3DS format (lines separated by \r\n or \n)
24Simple plugins list (already never used)
25syntaxoptionsSyntax highlighting color settings
26fonttestFont test
27displayoptionsDisplay/Scale settings
28levelstatsLevel stats
29pluralformstestPlural forms test
30assetsmenuAssets viewer main menu
31audioplayerMusic player/editor, sound player
32graphicsviewerGraphics viewer
33languageLanguage screen
34inputtestNew input system test
35vvvvvvsetupoptions"VVVVVV setup" options
100 and further can be allocated by plugins (next paragraph)

State allocation

In Ved 1.1.4 and higher, plugins can allocate an amount of states for their own use, without using hardcoded state numbers, making it unnecessary to think of unique state numbers that won't interfere with any other plugins or future Ved updates. The following functions can be used:

allocate_states(name [, amount=1])
This function is used to allocate the given amount of states with identifier name.
in_astate(name [, s=0])
This function returns true if the current state is s for identifier name. These state numbers start at 0.
to_astate(name [, new=0 [, dontinitialize=false]])
Change state to state number new for identifier name, and if dontinitialize is set, call hook func_loadstate.

For example, take a plugin called My First Plugin, which uses three states. Upon startup, like in hook love_load_start or love_load_end, the plugin calls allocate_states("my_1st_plug", 3). If this is the only plugin, or the first plugin to call allocate_states(), the allocated states will now, internally, be 100, 101 and 102. Let's say My First Plugin has three buttons to go to each of the allocated states. The first button, when clicked, would call to_astate("my_1st_plug", 0), the second would call to_astate("my_1st_plug", 1) and the third would call to_astate("my_1st_plug", 2). Hook love_draw_state, would contain something like this:

if in_astate("my_1st_plug", 0) then
	-- Insert drawing code for first state!
	statecaught = true -- <- only necessary in 1.8.1 and older!
elseif in_astate("my_1st_plug", 1) then
	-- Insert drawing code for second state!
	statecaught = true
elseif in_astate("my_1st_plug", 2) then
	-- Insert drawing code for third state!
	statecaught = true

The hook func_loadstate could contain something similar for initialization code for all the states (but without statecaught = true). Speaking about statecaught = true, this variable was used to prevent an "Unknown state" screen from showing, but this screen has been removed in 1.8.2, and thus setting the variable is no longer necessary.

The identifying name can be anything, but this name should be unique to one plugin. It's also possible to allocate multiple blocks of state numbers within the same plugin, if you use different names. If your plugin only has one state, you can leave out the number (allocate_states("my_1st_plug"), in_astate("my_1st_plug"), to_astate("my_1st_plug")). And of course, this means you can have multiple states that are only referred to by string names (I can see how in_astate("my_1st_plug_menu") and in_astate("my_1st_plug_display") can be more pleasing than in_astate("my_1st_plug", 0) and in_astate("my_1st_plug", 1)). It's up to you to choose whatever you like most, or whatever works best for your plugin.

LÖVE version compatibility

Ved is compatible with all revisions of LÖVE 0.9.x, 0.10.x and 11.x (except LÖVE 0.9.0 as of Ved 1.4.2), but its code is written for 0.9.x. Compatibility with newer versions is mostly achieved by causing update changes to be undone; for example, LÖVE functions that were renamed or expect different arguments are redefined/hijacked and then called by those redefinitions if arguments or return values need to be passed differently, and callbacks that get "new-style" data from LÖVE get a bit of conversion code at the top. There are a few instances of conditionals depending on the version number in regular code, but that is not very common.

In summary: (the unsupported features per version are more detailed below)

LÖVEVed support
11.3Supported since 1.3.3
0.10.2Supported since a42
0.9.2 Supported, with the following restrictions:
  • font.png from the VVVVVV graphics folder cannot be used
  • F9 to display hotkeys never displays ⌘ on Mac and never translates keys
  • The music player can't show song durations
  • The plugin hooks love_filedropped and love_directorydropped are never called
  • (Playtesting was broken in versions of Ved below 1.8.4, now fixed)
0.9.0Support dropped in 1.4.5 (broken since 1.4.2)
0.8.0-Has never been supported, but a message is shown

Checking the LÖVE version Ved is running under

Ved has a dedicated function to check if the current LÖVE version is at least a certain version or later, love_version_meets(). E.g. love_version_meets(10) means "LÖVE version is 0.10.0 or later", love_version_meets(9, 2) means "LÖVE version is 0.9.2 or later". It automatically takes care of the difference between 0.x and 11.x, too, so love_version_meets(10) means "LÖVE version is 0.10.0 or later" while love_version_meets(11) means "LÖVE version is 11.0 or later".

Features unsupported in older LÖVE versions

Nevertheless, there are simply some features or improved behavior added in later LÖVE versions, which Ved takes advantage of, that simply can't be backported to previous LÖVE versions. None of these are particularly important features for Ved's main purpose of editing levels, but it is still good to document them.

Being able to use font.png from the VVVVVV graphics folder as the main font
This feature is only supported in LÖVE versions 0.10.0 and up.

This is because in LÖVE versions previous to 0.10.0, the font returned by automatically had 1 pixel of extra horizontal spacing, and there was no way to change this. If you used a custom font.png with 1 pixel of extra spacing for each glyph, it would look really ugly, partly because you wouldn't be used to it being rendered that way, but mostly because Ved prints text assuming there's no 1 pixel of extra space for each glyph.

This problem is fixed in LÖVE 0.10.0+ because it added an optional third argument to to specify the spacing, which also lets you use negative values.

On a side note, tinynumbers, Ved's F9 hotkey font, doesn't have this problem. This is because of a semi-hacky workaround: the font image is intentionally made with 1 less pixel of space per glyph, and then when it gets passed to, it gets 1 pixel of extra spacing either because it's below LÖVE 0.10.0 and it's forced or because we've specified 1 pixel of spacing in LÖVE 0.10.0+.

Having the F9 hotkey font change depending on operating system, language, etc.
This feature is only supported in LÖVE versions 0.10.0 and up.

This is referring to the feature where the characters on the hotkeys that show up when you hold down F9 will change to match your operating system and language. This means that, for example, Ctrl will change to Cmd on macOS, and Ctrl will change to Strg if your language is German. (If you're a German macOS user then it will still be Cmd.)

This feature depends on Font:setFallbacks(), which was only added in LÖVE 0.10.0. Not much we can do without it.

Basically anything to do with jumping around the track of the currently playing audio in the music and sound effect viewers
This feature is only supported in LÖVE versions 0.10.0 and up.

In LÖVE versions before 0.10.0, you can't jump around the track of the currently playing music or sound effect. That means you cannot click on the track to go to a certain position, nor can you use (Shift)+(kp)Left/Right to move 5 or 10 seconds forwards or backwards.

The reason is simple: we need to know the duration of the currently playing audio. The only function that does this is Source:getDuration(), and it only exists starting in LÖVE 0.10.0.

Without knowing the duration of the audio, clicking on the track becomes meaningless, because one end is supposed to be the start of the audio (time t=0) and the other end is supposed to be the end of the audio (time t=<duration of audio>). Without the duration, we don't know what timecode the other end should be. So if one song is, let's say, 2:30 long and the other is 5:00 long, then in the 5:00-long song the middle of the track is 2:30, and in the 2:30-long song the middle of the track is 1:15 - but without knowing the duration of each we don't know where each timecode is supposed to be placed on the track for each song.

Another consequence of not knowing the duration is that we can't make sure that you don't go past the end of the track when you use (Shift)+(kp)Left/Right to jump around. The end of the track is determined by its duration, which we don't know. So we wouldn't know if you went past the end or not without knowing the duration of the audio.

Debug mode

Debug mode is a special mode used to access certain features and information that can be useful for debugging and developing Ved. Enabling debug mode has the following effects:

Debug mode is enabled by the boolean variable allowdebug. It is possible to enable it in-app by going from the load screen to the Ved options, clicking and holding the OK button, and "dragging" over the Send Feedback button holding the right mouse button as well.

Editor-related variables

Current room

The coordinates for the current room are stored in roomx and roomy, these start at 0 like in internal scripting.

Selected tool

The number for the currently selected tool is stored in selectedtool (which starts at 1). The selected subtools are stored for each separate tool, and are stored in the table selectedsubtool, which has 17 elements.

Selected tileset

Even though tileset information is also stored in the room metadata, the currently selected tileset and tile color has always stored in the variables selectedtileset and selectedcolor.

Undo/redo stacks

The variable that keeps track of actions that can be undone is undobuffer, and the one that keeps track of the actions that have been undone (and can be redone) is redobuffer. These are tables. If an undoable action is taken, an entry is added to the end of undobuffer, and redobuffer is cleared. When undoing, the function undo() is called, which undoes the latest action appropriately, and moves the last entry in undobuffer to the end of redobuffer. When redoing, the function redo() is called, which redoes the latest action appropriately, and moves the last entry in redobuffer to the end of undobuffer. Entries in undobuffer and redobuffer are tables with different properties depending on the undotype it has.

Other things

The number of the currently selected tile is stored in selectedtile.

To edit roomtext and (re)name scripts in script boxes and terminals, Ved uses editingroomtext. The entity ID of the entity data attribute currently being edited is stored in editingroomtext. You can get the entity being edited by simply doing entitydata[editingroomtext]. Since tables in Lua are 1-indexed, editingroomtext cannot be 0, so to check if we are currently editing the roomtext, just do editingroomtext > 0; to check if we are not, just do editingroomtext == 0.

editingroomname is a boolean that is true when the current room's roomname is being edited, and false when it isn't. However, you should use toggleeditroomname() to start and stop editing the roomname.

When editing enemy and platform boundaries, Ved uses editingbounds. It is 0 when no boundaries are being edited. Its magnitude (i.e. its absolute value, i.e. ignore the negative sign if there is one) will be 1 for platform bounds, and 2 for enemy bounds. Its sign (i.e. whether it's positive or negative) will be negative when placing the first corner (the top-left corner), and will be positive when placing the second corner (the bottom-right corner). So to reiterate: when editing platform bounds, editingbounds will go from 0, to -1, to 1; and when editing enemy bounds, editingbounds will go from 0, to -2, to 2.
You can start a boundary edit by calling either changeplatformbounds() or changeenemybounds().

The variable that controls the eraser (i.e. whether right-clicking will erase tiles if holding a tile brush) is eraserlocked. When it is true (by default), you can erase tiles using right-click. When false, you cannot.

Whether or not enemy and platform bounds are rendered is controlled by showepbounds.

The tiles picker (e.g. what pops up when you click on "Show all", or press/hold Ctrl+Shift) being open or not is controlled by tilespicker. tilespicker_shortcut controls whether or not you are holding the shortcut, so Ved knows to close it when you release Ctrl+Shift. But using RCtrl+RShift will keep the tiles picker open (and mixing Left/Right Ctrl+Shift will be the same as LCtrl+LShift - that is, it won't "stick" and you have to keep holding the key combo).

Level-related variables and functions

Level metadata

The variable metadata is a table with the different options as key-value pairs. The map width, map height and level music are not part of the metadata in the VVVVVV level format, but internally in Ved, they are. So all of the metadata variables are as follows:

Room tiles

roomdata is a big table of tables that stores all the tiles in a level. roomdata[roomy][roomx] is a 1D array with all the tiles for the current room (the variables roomx and roomy are the current room's coordinates, and they start at 0). The tiles in the room are numbered starting at 1, so the 3rd tile from the left and top is roomdata[roomy][roomx][(2*40)+(2+1)].

To get a tile in a room, use roomdata_get(x, y, tx, ty). To get all of a room's tiles, use roomdata_get(x, y).

To set a tile, use roomdata_set(x, y, tx, ty, value). To set all of a room's tiles, use roomdata_set(x, y, values).

From 1.8.0-pre22 until 1.9.0-pre05, these functions had an altstate argument for VVVVVV-CE. See the Changelog of breaking codebase changes for 1.9.0 for more info.


entitydata contains all the entities in a level. Each element of entitydata is structured as follows:
	x = 12,
	y = 34,
	t = 17,
	p1 = 0,
	p2 = 0,
	p3 = 0,
	p4 = 0,
	p5 = 320,
	p6 = 240,
	data = "Roomtext or script name"

Room metadata

levelmetadata is the table containing room metadata, or, looking at the VVVVVV level format, each edLevelClass inside the levelMetaData tags. Since 1.7.1, this table is indexed by the room's X and Y coordinate separately: levelmetadata[roomy][roomx] (Before 1.7.1 this was one index from 1-400). Each element is structured as follows:
	tileset = 0,
	tilecol = 0,
	platx1 = 0,
	platy1 = 0,
	platx2 = 320,
	platy2 = 240,
	platv = 4,
	enemyx1 = 0,
	enemyy1 = 0,
	enemyx2 = 320,
	enemyy2 = 240,
	enemytype = 0,
	directmode = 0,
	warpdir = 0,
	roomname = "Roomname",
	auto2mode = 0,
directmode is always present, even after a VVVVVV 2.0 level is loaded. If auto2mode == 1 then multi-tileset mode is used for that room, and in that case directmode should be 0. However, when saving, directmode is set to 1 in the level file because auto2mode is not saved to it.

To get a room's metadata, use levelmetadata_get(x, y).

To set an attribute of metadata, use levelmetadata_set(x, y, attribute, value). It's also possible to use levelmetadata_set(x, y, attribute_table) to replace the entire room's metadata table.


Two tables for this one: scriptnames which is a simple table of all the script names with numeric keys in the correct order, and scripts which contain the actual scripts, with script names as keys. Each element of scripts is a table itself, with all the lines in that script. An example population can be given as follows:
scriptnames = {
	[1] = "mynewscript",
	[2] = "mynewscript_load"
scripts = {
	mynewscript_load = {
		[1] = "ifflag(2,stop)",
		[2] = "flag(2,on)",
		[3] = "iftrinkets(0,mynewscript)"
	mynewscript = {
		[1] = "reply(3)",
		[2] = "I probably don't really need it,",
		[3] = "but it might be nice to take it",
		[4] = "back to the ship to study..."
If you're wondering how Ved stores internal scripts: for both loadscript and say(-1) internal script modes, text(1,0,0,3) #v and say(x) #v are put in between each block if the script has to be split. The loadscript mode starts with squeak(off) #v and say(x) #v, and ends with loadscript(stop) #v and text(1,0,0,3) #v. The say(-1) mode starts with squeak(off) #v, say(-1) #v, text(1,0,0,3) #v, and say(x) #v, and ends with loadscript(stop) #v (with no extra text line like the loadscript internal script mode has). If you want to check, hold down the shift key while opening a script. The same goes for checking checking flag names - Ved converts them to numbers when leaving the script editor and converts them back into names when opening it, unless you hold shift while opening.


The table count keeps count of the number of trinkets, crewmates and entities in the level, and keeps track of the integer key of the start point entity. It also counts the number of sanity checks that failed when loading the level. As can be seen in vvvvvvxml.lua:
	local mycount = {trinkets = 0, crewmates = 0, entities = 0, startpoint = nil, FC = 0}  -- FC = Failed Checks
(mycount is local to that function, and will be returned and then stored to count. So this whole system of creating tables locally and then returning references to them works? Yeah, it does, funny eh? I should probably rewrite that a little bit though.)

Metadata entity (level notes, flag names)

The metadata entity is used by Ved to store data that is specific to a certain level. It is a roomtext entity at x=800 y=600, which means it's in room 21,21 (1-indexed). The data is formatted with several levels of separation characters:

|primary separator (see this as separating different "tables" of data)
$secondary separator (see this as separating different "rows" or "records" inside a "table")
@tertiary separator (see this as separating different "columns" or "properties" inside a "rows"/"records")

For example, one of the "tables" contains level notes. Each level note is separated by $, and inside a level note, the name and the contents of the note are separated by @.

Accent grave (`) is the escape character; if the real versions of certain characters need to be represented, they are escaped as follows:

(2 spaces)`_

The function despecialchars(text) encodes the raw characters to escaped format, undespecialchars(text) decodes escaped characters back to raw characters.

As said, | is the primary separator, which separates the following items:

1Metadata entity version number0?
2Flag names2
4Level variables, for use by Ved or plugins (Why haven't I documented this out of commit messages)3
5Level notes0?

(more coming soon)

Clipboard room format

Currently, when you copy a room to the clipboard, it's stored in a comma-separated format. It consists of 1215 values separated by commas, and it's structured as follows (indices start at 1, as in Lua):

12345678910111213141516 — 1215
Tileset Tilecol Platf. bounds x1,y1,x2,y2 Platv Enemy bounds x1,y1,x2,y2 Enemy type Direct mode Warp dir Room name* 1200 tile numbers

In the room name, commas are replaced by the character ´ (U+00B4 ACUTE ACCENT).

In this format, entities are not copied. I'm planning to replace this CSV system by an XML format, which will also contain entities. Data in the old format will still be pasteable, and I'll probably also leave in a way to copy as CSV.


In Ved 1.4.0, the dialogs system was overhauled. To create a new dialog, you can call dialog.create:
dialog.create(message, buttons, handler, title, fields, noclosechecker, identifier)
message is the body of the dialog box. This is the only required argument.
buttons speaks for itself as to what it is (but see below), if not specified, only an OK button will be present.
handler is a function that will be called when the dialog is closed, and will be provided with the button that was pressed and the dialog fields.
title is the title text of the dialog. Setting this to an empty string is not needed anymore.
fields defines the input fields that the dialog has, see below. If not given, the dialog has no input fields.
noclosechecker is a function of which the main purpose is to check whether a button shouldn't actually close the dialog, and should return true if so - think of apply buttons
identifier is just an extra internal label indicating the type of dialog, rarely used (it's used for the quit dialog to know that a quit dialog is already on top)


Strictly speaking, the buttons parameter accepts a list/table of buttons. For each element, if that element is a string, that string will be displayed as-is. But if possible, it should be an integer, representing one of the built-in buttons. A list of buttons is available as DB, so for example, DB.YES is a "Yes" button. The list is as follows:

ConstantValueButton label

DB.LOAD was added in Ved 1.6.0.

There's also built-in lists of buttons available as DBS, like DBS.YESNO, which stands for {DB.YES, DB.NO}, meaning a Yes and No button.


DBS.SAVECANCEL and DBS.LOADCANCEL were added in Ved 1.6.0.


The purpose of the handler function is to take action after closing a dialog. For example, if a question is asked whether the user wants to destroy something, then that should be done if (and only if) the user chooses DB.YES.

The handler is a function that can take up to five arguments:

The input fields are provided regardless of the button pressed - a Cancel button doesn't have any special meaning and the dialog handler should deal with it to ignore the input fields in that case (if that's what you want).

Dialog handlers as used in Ved's code can be found in the file dialog_uses.lua, starting with dialog.callback. An example handler is the following. Assume the buttons for this dialog are DBS.YESNOCANCEL. In this example, users press Yes if they want a new dialog to be created showing what they entered in a field with the key name, press No if they still want a dialog but don't want to know their input, and press Cancel if they want no new dialog.

function(button, fields)
	if button == DB.YES then
		dialog.create("You pressed Yes, and the input with key \"name\" is " ..
	elseif button == DB.NO then
		dialog.create("You pressed No! Apparently you don't want to know what you entered, but you still want a dialog.")
DB.CANCEL is not checked, therefore the handler does nothing if that button is pressed.

No-close checker

No-close checkers are similar to handlers, but they are called before the dialog closes. This function can stop the dialog from being closed by returning true, and thus are useful for creating error messages if the user puts in invalid input. They can also be used to not close the dialog if an Apply button is pressed, for example.

The arguments for the no-close checker are the same as for the main handler, except the fourth argument (closing prevented by no-close checker) doesn't exist, which means the fourth argument is the dialog object. If the no-close checker returns true (and thus stops the dialog from closing) the handler will still be called, and its fourth argument will be set to true as well. Here's an example of how a no-close checker can be used to give an error message in case someone enters a value for the field with key inp that is empty or above 20 characters:

function(button, fields)
	if button == DB.OK and (fields.inp == "" or fields.inp:len() > 20) then
		dialog.create("Your input must not be empty or longer than 20 characters.")
		return true
Now the handler might be defined as follows:
function(button, fields, identifier, notclosed)
	if notclosed then

	if button == DB.OK then
		-- Save your fields.inp here.
You can find more examples in dialog_uses.lua in Ved, no-close checkers are generally prefixed _validate.


Each dialog can have a list (a table) of input fields that will be shown in the dialog. Each input field is a table with sequential properties that always start with the following:
  1. Key (its identifier, like name in a HTML input field)
  2. X position in characters (blocks of 8)
  3. Y position in characters (blocks of 8)
  4. Width of input field in characters (so in blocks of 8 again)
  5. Default value (like value in HTML)
  6. Type (for example, use DF.TEXT for a text field)

The X and Y positions for the field both start at 0, which is the position the regular dialog text also starts.
These are the different types of input fields:

0DF.TEXTText input
2DF.LABELPlain text label (does not take input)
4DF.RADIOSRadio button list
5DF.FILESFiles list and directory navigation
6DF.HIDDENHidden field

The DF. constants were added in Ved 1.5.0.

dialog_uses.lua has some complete forms (but mostly functions that generate forms) under dialog.form. One general-purpose example is dialog.form.simplename, which has a single text field at position 0,1 to be used to fill in a name (for example a name for a new script or note), and dialog.form.simplename_make(default) to generate a similar form but with a value pre-filled.

More information about how the different types work:

(0) DF.TEXT - Text input

A text field is what it says it is. An example is given as follows: {"name", 0, 1, 40, "", DF.TEXT}
Here, the key is name, it is positioned on the start of the second line of text, it is 40 characters wide (but more characters will fit) and its default value is an empty string.

(1) DF.DROPDOWN - Dropdown

Dropdowns require at least one more argument:
  1. A list of items shown in the dropdown menu
  2. An optional table that converts a value to a displayable "current selection" if you want to hide how the value is passed. If not given, set this to false if you also want to supply the next argument.
  3. An optional function that gets called whenever a selection is made from the dropdown. Think of an onchange event in HTML/JS. Gets passed the selection from the dropdown as text, and may return a substitute to fill into the input field behind the scenes.

Basically, there's two forms: first the simpler one. In the simpler form, you only need a list of items that will appear in the dropdown, and whenever the user selects an item, the value of the input field is set to the text of the option that the user selected. This means what's readable as an option will be passed. You may want to set the default value to an option in the list.
An example: {"drop", 0, 0, 30, "Option A", DF.DROPDOWN, {"Option A", "Option B", "Option C"}}
The width is set to 30 because that's how wide dropdown menus are (currently). If you want a function to be called every time an option is selected in this case, there'd be two more arguments: false (as a filler for the second table) and then the function.

For the second form, let's take the example of a user selecting between percentages, let's say 50%, 100% and 200%. You want to pass this as a number instead, so if the user selects 50%, you want the actual value to be 0.5. When the user does select 50%, the "onchange" function is called, and converts the "50%" into 0.5, and returns that. The second table that was mentioned (the one that converts a value to a displayable "current selection") has this 0.5 background value as a key, and that maps to a value of "50%".
So this is that example:

	"drop2", 0, 2, 30, 0.5, DF.DROPDOWN, {"50%", "100%", "200%"}, {[0.5] = "50%", [1] = "100%", [2] = "200%"},
		if picked == "50%" then
			return 0.5
		elseif picked == "100%" then
			return 1
		elseif picked == "200%" then
			return 2

As of version 1.5.0, you can use the function generate_dropdown_tables(tuples) to generate these last three arguments. The function takes a table as an argument with key-value tuples as elements (not keys as keys and values as values). It returns the three required arguments for the second form (list of displayed items, converter key-value table and "onchange" function). For example, the previous example could be written more elegantly as follows:

	"drop2", 0, 2, 30, 0.5, DF.DROPDOWN, generate_dropdown_tables({{0.5, "50%"}, {1, "100%"}, {2, "200%"}})

(2) DF.LABEL - Plain text label

This is just a bit of text that can be displayed anywhere in the dialog you want. It can therefore be used to label other input fields without having to include those labels in the dialog contents.

The "default value" will be used as text, but it can also be a function that returns the text dynamically.

An example for a plain-text label is as follows: {"", 0, 5, 10, "Label", DF.LABEL}
The key is left empty, because it has not much use. But we can't set it to nil, otherwise Lua might think the table ends there. It is positioned on the start of the 6th line. The width is 10 characters, which means it will merely wrap beyond that point. Then the label is just a string, and will be displayed. 2 is the type.

Here's an example of a label that keeps changing: {"", 0, 5, 40, function() return love.math.random() end, DF.LABEL}
This will continuously display a different random number between 0 and 1. Note that the key and the table of fields are passed to the function, but it's not used here.

(3) DF.CHECKBOX - Checkbox

Checkboxes are useful for true/false values. The width of the checkbox is not the width of the actual checkbox, but the width of the clicking area. This is useful to make the label clickable as well.
Example: OPTIONLABEL = "Option"
{"option", 0, 5, 2+font8:getWidth(OPTIONLABEL), true, DF.CHECKBOX},
{"", 2, 5, 40, OPTIONLABEL, DF.LABEL}
The default state of this checkbox is checked, since the default value is set to true here. It is followed by a plain text label (type 2), and the label can be clicked as well to toggle the checkbox.

(4) DF.RADIOS - Radio button list

Radio buttons were added in Ved 1.5.0. These function exactly like dropdowns do, the only differences are the type and the way they behave in the GUI. The width argument is not used. You can use the same generate_dropdown_tables function for radio buttons that you can use for dropdowns.

For example, the time format picker in the language dialog works like this:

			"timeformat", 23, 8, 0, s.new_timeformat, DF.RADIOS,
				{{24, "23:59"}, {12, "11:59pm"}}

The initial value is s.new_timeformat, which is the setting for the time format, which is either 24 or 12. Selecting "23:59" sets the value to 24, selecting "11:59pm" sets the value to 12. (Note that the dialog handler should apply the change, the value that you fill in as 5th argument is merely the initial state of the 'field'.)

(5) DF.FILES - Files list and directory navigation

The files list type was added in Ved 1.6.0. The default value is the full path to the current directory. It takes 7 more arguments (note that despite me giving each of these arguments names, they don't actually have keys by those names, and this is only to make it easier to understand):

  1. menuitems - A table of files, where each file is a table of the form returned by listfiles_generic(). That is, it has the attributes of name, isdir, and lastmodified.
  2. folder_filter - If argument 7 (filter_on) is on, then the files listed will only be the ones ending in this string. Usually it'll be a file extension like .vvv. You can make this filter only directories by passing the operating system's directory separator, which should be dirsep.
  3. folder_show_hidden - Whether or not to show hidden files or not. This is passed to listfiles_generic(), which goes off of the operating system's definition of hidden.
  4. listscroll - The Y offset of the files list due to the scrollbar, as (indirectly) generated by scrollbar().
  5. folder_error - A string for indicating errors. It should be non-empty when there's an error.
  6. list_height - The amount, in blocks of 8, of the list.
  7. filter_on - Whether or not to apply the filename-ending filter from argument 2 (folder_filter).

Note that you need a field with the key of "name" to select a file, and you need a checkbox to toggle showing only directories, showing only files that are filtered, or showing hidden files.

For this reason, it is recommended to make a full file selection dialog with dialog.form.files_make() instead. In fact, all code in Ved currently uses that function instead of making the files list manually.

dialog.form.files_make(startfolder, defaultname, filter, show_hidden, list_height)
All of the arguments are required.
startfolder is the full file path to the folder you start the dialog in, just like the default value for DF.FILES.
defaultname is what to put as the default value for the "Name:" field.
filter will filter for filenames that end in this string, unless you pass it dirsep, in which case directories will be filtered instead. A checkbox toggling the filter will be created. If you decide to filter directories, then there will no longer be a "name"-key field.
show_hidden is whether or not to show hidden files, by the operating system's definition of hidden.
list_height is the height (in blocks of 8) of the list.

(6) DF.HIDDEN - Hidden field

Hidden fields were added in Ved 1.8.5. A hidden field can be used to carry data from dialog creation to dialog submission, without accepting user changes. For example, if you have a confirmation dialog to delete a certain script, you want to still know what script it was when the user presses "Yes". The type of the value can be anything you need, as long as it is not nil (again, Lua limitation, unless we changed dialog fields to have string keys for named arguments). nil values could instead be encoded as having the field be missing altogether, which will have the same effect, because for the handler, fields.the_missing_field would yield nil anyway. Position and width has no meaning for this field, so for example: {"key", 0, 0, 0, "value", DF.HIDDEN}

There is a convenience function to make a form with hidden fields from a table where the keys and values correspond to the field keys and values: dialog.form.hidden_make(values, existing_form). existing_form can be filled in if you already have a form, and simply want to add one or more hidden fields to it.
For example:
dialog.form.hidden_make({script="applebapple"}) or
dialog.form.hidden_make({script="applebapple"}, dialog.form.simplename)

Dialogs (old, deprecated system, fully removed in Ved 1.5.2)

Dialog boxes in the system before Ved 1.4.0 can be created by calling, title, showbar, buttons, questionid)
message is the body of the text box.
title will be shown in the title bar (if the title bar is shown by setting showbar to 1, which is always done in Ved. In fact, as of 1.4.0, the showbar parameter has no effect anymore). The title is often set to an empty string in Ved.
buttons decides what buttons are shown, and questionid decides what to do with the button that is pressed, 0 to take no action. The question ID system is not optimized for plugins, but the new dialog system allows plugins to specify their own question handlers for dialogs.

Dialog buttons

The fourth argument of (called buttons above) can be set to one of the following values V:

VButton 3Button 2Button 1

(6 has been added in Ved 1.3.0)

Text input

General text input can be started by a single call to startinput(). There's also a function startinputonce(), which can be used if you decide to do it in update/drawing code, but expect me to remove that sooner or later. Input can be stopped (or locked) by calling stopinput(). The text input to the left of the cursor can be found in input, and text to the right of the cursor can be found in input_r. The variable __ (two underscores) contains the text cursor and the text to the right of it. So, to display the input field, you can concatenate input and __ (input .. __).

It's also possible to include text boxes in dialogs, see the above part on fields in dialogs.


If you didn't know already, you can hold F9 to reveal hotkeys.

Since Ved 1.6.1, Ved has had a dedicated function to display any hotkey on the screen if F9 is held down, showhotkey:
showhotkey(hotkey, x, y, align, topmost, dialog_obj)
hotkey is the code (or sequence of codes) for the hotkey in question. More on what these codes are later. This is required.
x is the x-position of the hotkey. This is required.
y is the y-position of the hotkey. This is required.
align can be either one of ALIGN.LEFT (default), ALIGN.CENTER, or ALIGN.RIGHT, and will align the hotkey appropriately.
If you are calling this from a dialog, you will need to pass the next two arguments (and consequently, will need to pass align as well):
topmost is whether the given dialog is the topmost dialog or not. You should be in a cDialog drawing function and just pass the topmost from that function.
dialog_obj is the dialog object the hotkey is located on. You should be in a cDialog drawing function and just pass the self from that function.

Hotkey check function

The following function can be used in button UI elements, to easily add a functioning hotkey to a button. This function will return an anonymous function which returns true if the given hotkey, in combination with a modifier key if specified, is pressed.

hotkey(checkkey, checkmod)
checkkey: The key constant to check.
checkmod: Optional. If specified, the key constant to additionally check. This is passed to keyboard_eitherIsDown, which means 'l' and 'r' variants are checked.


In order to avoid needing to use love.keyboard.isDown("lshift", "rshift") or even love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"), the function keyboard_eitherIsDown(...) exists. This works similarly to love.keyboard.isDown(...), except for each key passed, it checks 'l' and 'r' variants of that key. It therefore only makes sense to use this with modifier keys that actually have (or may have) both left and right variants that are named as such. Example: keyboard_eitherIsDown("shift")

Hotkey codes

Each symbol in the hotkey font, tinynumbersfont, is actually mapped to one specific character, and is case-sensitive.

What this means is that, for example, a is Alt, but A is just the letter A. In fact, 0-9 and A-Z (uppercase) are all just themselves, along with a lot of other characters. You can also easily combine symbols togetter like so: aS would simply be Alt+S, and show up as such accordingly.

In the following list of usable symbols, a character is simply itself if it has no text saying otherwise:

The top row of letters of the QWERTY keyboard, lowercase, along with k and l, is F1-F12. Here they are for reference:

GUI elements

TODO: Update this for 1.8.4, each state now has a folder, not a single file.

Each state can have a list of elements in their file in uis/, which is a table ui.elements. Each of these elements at the root is drawn in order - all being given a position of 0,0 and remaining width and height as the window dimensions - and their callbacks are called when that state is active. The internal functioning of the different classes of UI elements can be found in ui_elements.lua. There are certain element classes such as elButton with all sorts of parameters controlling their behavior (for example, whether it's a button with text on it, or whether it's an icon), which actually implement the callbacks (documenting which is TODO). Then there are easy constructor functions which actually create and "configure" these classes depending on what you want (for example, a LabelButton constructor has an argument for the text to display on the button, which an ImageButton doesn't need because it has arguments to do with displaying a clickable image). The following constructors for UI elements exist:
This element simply calls a drawing function func(x, y, maxw, maxh). This function may return its actual width and height, and must do so if it's in a container that expects width and height.
FloatContainer(el, fx, fy, maxw, maxh)
Container that puts a sub-element at completely custom coordinates.
AlignContainer(el, calign, cvalign)
Aligns its sub-element left/center/right and top/center/bottom depending on parent maxw and maxh.
ScreenContainer(els, cw, ch)
Simply holds more elements as though this is another root.
cw / ch: container width and height. nil to fill the remaining parent width/height.
ListContainer(els_top, els_bot, cw, ch, align, starty, spacing, starty_bot, spacing_bot)
Vertical list container. Elements from the top are displayed at starty, elements from the bottom are starty_bot pixels away from maxh given in the draw function. If the maxh given to the draw function is infinite (nil), then bottom elements are not shown.
cw / ch: container width and height. nil to fill the remaining parent width/height.
align: the horizontal alignment of the elements. Can be ALIGN.LEFT, ALIGN.CENTER or ALIGN.RIGHT. Centered by default. This argument was added in Ved 1.8.5.
spacing: the spacing between each top element
starty_bot: if not given, this defaults to starty.
spacing_bot: if not given, this defaults to spacing.
HorizontalListContainer(els_left, els_right, cw, ch, align, startx, spacing, startx_right, spacing_right)
Horizontal list container. Works the same as a vertical list container, but "top" and "bottom" correspond to "left" and "right", and align is the vertical alignment of the elements (can be VALIGN.TOP, VALIGN.CENTER or VALIGN.BOTTOM)
RightBar(els_top, els_bot)
A right-aligned vertical list container with a width of 128, and starty and spacing of 8.
Spacer(w, h)
Filler element that just takes space.
Filler element the size of a LabelButton.
LabelButton(label, action, hotkey_text, hotkey_func, status_func, action_r, hotkey_r_func)
A clickable button with a text label. If clicked or the hotkey is used, the function action is run.
hotkey_text: The displayed hotkey when holding F9. Displayed in the tiny numbers font.
hotkey_func: A function that takes a key as argument (from love.keypressed(key)) and returns true if this button's hotkey is pressed (so that action will run). There is a convenience function hotkey(checkkey, checkmod). For more about that, see the section about it above. Example: hotkey("escape")
status_func: A function that can have three return values indicating the button's status: shown, enabled, yellow. If nil, shown and enabled default to true, yellow defaults to false.
action_r: A function to run when the button is right-clicked.
hotkey_r_func: Similar to hotkey_func, but returns true if the hotkey is pressed for executing the "right-click" action.
ImageButton(image, scale, action, hotkey_text, hotkey_func, status_func, action_r, hotkey_r_func)
A clickable image (scaled scale times) that will appear dimmed or normal depending on whether the cursor hovers over it. For argument descriptions, see LabelButton.
InvisibleButton(w, h, action, hotkey_text, hotkey_func, status_func, action_r, hotkey_r_func)
A clickable button that is not displayed. For argument descriptions, see LabelButton.
A horizontal list container with image buttons for undo, redo, cut, copy and paste.
Text(text, color_func, sx, sy)
This was added in Ved 1.8.5.
A text displayed via ved_print. text can be either a string, or a function returning a string. color_func may be a function that returns R, G, B, and optionally A values (0-255). sx and sy are horizontal and vertical scale values (default 1).
All arguments are optional except text.
WrappedText(text, maxwidth, align, color_func, sx, sy)
This was added in Ved 1.8.5.
A text displayed via ved_printf. text can be either a string, or a function returning a string. If maxwidth is not given, the remaining parent width will be filled. align is passed to ved_printf/ color_func may be a function that returns R, G, B, and optionally A values (0-255). sx and sy are horizontal and vertical scale values (default 1).
All arguments are optional except text.

Changelog of breaking codebase changes

Ved 1.8.4 introduced some pretty major changes in the code, which probably broke a lot of plugins. I thought it might be a good idea to finally start documenting breaking "API" changes at this point, instead of having everyone figure them out as commits go by and new versions get released. So this log starts at 1.8.4 and is intended to give an overview of changes that are likely to break plugins.

Depending on how likely I think changes are to break plugins and depending on the weather, changes may or may not get listed here. I'll probably list a lot that isn't necessary, and on the other hand there will probably be some changes I didn't imagine would affect anyone but turn out they do (especially when plugins find-and-replace too large blocks of code or something).

Numbers at the start of list items indicate relevant pre-versions in which the changes were introduced.


Easter eggs

Ved contains several easter eggs.
Ved home   Plugins   Technical documentation