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: Sunday 18 November 2018 21:05 (UTC) (this is the last edit date of the file)


First of all, the following source files are used in Ved:

conf.luaLÖVE's configuration file, controlling default window settings and loaded LÖVE modules.
const.luaConstants - Contains tile numbers for all tilesets, known scripting commands, music names, and other lookup tables.
coordsdialog.luaContains 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.
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.
dialog_uses.luaContains callback functions and definitions of fields for dialogs, which are used as arguments for dialog.create(...)
drawhelp.luaHolds drawhelp(), called by love.draw() in state 15 (the help state). The help system is also used for level notes and the plugins list.
drawlevelslist.luaHolds drawlevelslist(), called by love.draw() in state 6 (the loading screen state).
drawmaineditor.luaHolds drawmaineditor(), called by love.draw() in state 1 (the main editor state).
drawmap.luaHolds drawmap(), called by love.draw() in state 12 (the map state).
drawscripteditor.luaHolds drawscripteditor(), called by love.draw() in state 3 (the script editor state).
drawsearch.luaHolds drawsearch(), called by love.draw() in state 11 (the search state).
errorhandler.luaContains code for both the crash screen and the plugin error screen.
filefunc_lin.luaContains functions necessary for accessing the VVVVVV levels and graphics folders on Linux. Also has a function for opening a URL with xdg-open in case LÖVE 0.9.0 is being used (where love.system.openURL(url) doesn't exist yet)
filefunc_luv.luaContains fallback love.filesystem functions for accessing fallback levels and graphics folders if the operating system is something other than Windows, OS X or Linux.
filefunc_mac.luaContains functions necessary for accessing the VVVVVV levels and graphics folders on Mac OS X. Also has a function for opening a URL with open in case LÖVE 0.9.0 is being used (where love.system.openURL(url) doesn't exist yet)
filefunc_win.luaContains functions necessary for accessing the VVVVVV levels and graphics folders on Windows. Also has a function for opening a URL with start in case LÖVE 0.9.0 is being used (where love.system.openURL(url) doesn't exist yet)
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.
incompatmain.luaIf 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.
keyfunc.luaHandles the shortcut that can be used in the help screen to make text editable.
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, main2.lua.
main2.luaLoads most other source files and assets, and contains pretty much all love.* callbacks.
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.luaContains 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.luaContains 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 control in the options screen, holds the function int_control
updatecheck.luaChecks what the latest version of Ved is via HTTP, and reports back. This is run inside a separate thread.
vvvvvv_textbox.luaContains 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.

-3Black screen
-2tostate 6
-1Display error (expected: errormsg)
0Temp main menu (enter state). Can be accessed in debug mode by pressing F12.
1The editor (will expect things to have been loaded)
2Syntax highlighting test
3Scripting editor
4Some XML testing
5Filesystem testing
6Listing of all files in the levels folder, and load a level from here (loading screen)
7Display 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)
9Dialog test, and right click menu test
10List of scripts, and enter one to load
13Options screen
14Enemy picker preview
15Help/Level notes/Plugins list
16Scroll bar test
17folderopendialog utility
18Show undo/redo stacks
19Flags list
20Resizable box test
21Display 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 not used)
25Syntax highlighting color settings
26Font test
27Display/Scale settings
28Level stats
29Plural forms test
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 flags 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
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)

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, 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.

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.

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)].


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. Indexes for this table start at 1 (because Lua, as you may know). So the metadata for the current room is levelmetadata[roomy*20 + roomx+1], since roomx and roomy start at 0. 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.


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: say(x) #v is put at the start of the script, and loadscript(stop) #v and text(1,0,0,4) #v at the end (with additional text(1,0,0,4) #v + say(x) #v in between blocks if they have to be split.) 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

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.



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 four 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 doesn't exist. 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 of input fields that will be shown in the dialog. Each input field starts with the following sequential properties:

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:

0Text input (this is the default)
2Plain text label (does not take input)

More information about how the different types work:

0 - Text input

A text field is what it says it is. An example is given as follows: {"name", 0, 1, 40, ""}
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 less elegantly) and its default value is an empty string.

1 - Dropdown

Dropdowns require at least one more argument:

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", 1, {"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, "1", 1, {"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
You might want to generate tables for this in some way instead of hardcoding them like that, and let the function iterate over the table or index one or something, to make it a bit more elegant.

2 - 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", 2}
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, 2}
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 - 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, 3},
{"", 2, 5, 40, OPTIONLABEL, 2}
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.

Dialogs (old, deprecated system)

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.

Easter eggs

Ved contains several easter eggs.