Three days ago I discussed the game I am working on and mentioned the LÖVE engine that I and my teammates are using. It is a cross-platform engine designed for 2D games. We write most of our code in the Lua programming language because LÖVE provides useful hooks and modules for Lua, and we can prototype ideas more quickly in Lua than C++, a language popular through-out the game industry.
One of those LÖVE modules provides access to the file-system, which is useful for such things as saving a player’s game data. But Lua programmers unfamiliar with LÖVE may run into some confusion. So today I present a simple overview of how to deal with files in LÖVE and some pitfalls to avoid.
Goodbye Standard Library!
One component of our game is LNVL, “the LÖVE Visual Novel Engine.” We present the game’s story using scripts written in Lua, which LNVL digests and transforms into text and graphics on screen. So one of the fundamental features LNVL requires is the ability to load those scripts. This was my initial implementation of that feature:
-- This function loads an external LNVL script, i.e. one defining -- scenes and story content. The argument is the path to the file; -- the function assumes the caller has already ensured the file exists -- and will crash with an error if the file is not found. The script -- must define the 'START' scene. The function returns no value. function LNVL.loadScript(filename) assert(loadfile(filename))() LNVL.currentScene = START end
For this discussion we can ignore the details about the
START variable; the only thing to mention is that LNVL expects the script we load to define that variable so it can perform the assignment in the function above. It is more important for us to look at the first line of the function. Because LNVL scripts are valid Lua scripts the code uses an idiom from the Lua documentation for
loadstring(). The function
loadfile() returns a ‘chunk’ if the file looks like valid Lua, and returns
nil on any error. So the
assert() wrapped around the function immediately stops LNVL on any error caused when we attempt to load the script. The extra parentheses at the end executes the previously mentioned chunk;
loadfile() does not immediately execute the contents of the file, we must do that ourselves by executing the chunk as if it were a function.
To a Lua programmer that function may look fine. But it fails to work like we want in LÖVE. To understand why we need to look at how LÖVE restricts file access.
Access and Identities
LÖVE allows us to access files from two places:
*.lovearchive containing our game, i.e. the format for packaging LÖVE games.
The ‘save directory’ for our game.
Those are the only places where LÖVE allows us to access files. We cannot arbitrarily poke around the player’s hard-drive opening up whatever files we want. LÖVE sandboxes our file access for both safety (no access to system files or sensitive data) and cleanliness (no spewing game data in all kinds of different places).
LÖVE comes with the
love.filesystem module which gives us functions to read and write files while adhering to the restrictions above. But what about Lua’s standard library functions like
io.open()? LÖVE locks those down. In fact it locks them down so tightly that it forces us to go through the
love.filesystem module if we want to accomplish anything. So we have to stop using Lua’s standard functions for files and instead use LÖVE’s. With that in mind, let’s look in more detail at where we get to access files.
First is the
*.love archive for the game. The archive is just a Zip file with a different file extension. We put all of our code and assets for the game inside that archive. So the archive for LNVL, for example, has this layout inside the archive:
lnvl.love | |--- main.lua |--- LNVL.lua |--- docs/ |--- Design.md |--- Howto.md ... |--- examples/ |--- 01-Simple.lua |--- 02-TwoCharacters.lua |--- 03-CharactersWithImages.lua ... |--- src/ |--- character.lua |--- clamped-array.lua |--- color.lua |--- debug.lua ...
LÖVE allows us to access any file inside of this archive. The second, and only other place we operate on files is inside the ‘save directory’ for our game. LÖVE gives each game one directory where it can write files, read them later, and so forth. The name ‘save directory’ comes from the fact we typically use this directory to store save game files. The true location of the directory depends on the user’s operating system. But since LÖVE decides where to put that directory we do not need to know its absolute location.
Every LÖVE game has its own individual save directory. The game’s ‘identity’ determines the name of that directory. We set the identity inside a LÖVE config file. That identity name becomes the name of the game’s save directory. So if our game has the identity
LNVL then the save directory is (on my computer):
As I said earlier, the absolute location changes depending on the operating system, which is why we do not rely on that in our game code. All that matters is that the save directory will have the name
LNVL on every system.
Those are the two places LÖVE gives us for manipulating files. Time to revisit the problem with the function from earlier in the article.
The LÖVE Way to Load Files
LÖVE does not allow standard Lua functions like
loadfile() to look inside the game archive or its save directory. LNVL expects all scripts to be inside the game’s
*.love archive. But the function we looked at to load those scripts will always fail due to that restriction LÖVE places on
To correctly load scripts we must use the
love.filesystem module. Here is the correct, working version of that function:
function LNVL.loadScript(filename) love.filesystem.load(filename)() LNVL.currentScene = START end
Looks similar, right? All we did was replace
loadfile() with the function
love.filesystem.load(), and dropped the assertion since that LÖVE function will stop the game if the script we load has any syntax errors. But it still returns a chunk like
loadfile() which we must execute in order to run the code inside the script.
Let’s look at an example of opening files. One part of LNVL reads from the
src/rgb.txt file within LNVL’s LÖVE archive. This was the original code to open that file:
local rgb_file = io.open("src/rgb.txt", "r")
LÖVE bolts down
io.open() the same way it does
loadfile(). Even though that file is inside the archive we cannot open it using Lua’s standard
io.open() function. Again we must resort to LÖVE’s file-system module.
local rgb_file = love.filesystem.newFile("src/rgb.txt") rgb_file:open("r")
We need an extra line of code. The function
newFile() only accepts one parameter, a file-path, and returns a
File object. We have to call the
File:open() method separately since
newFile() itself does not open the file.
LÖVE does not let us open, read, or write files using the normal Lua functions. As these two simple examples show, we must use the
love.filesystem module to perform those tasks. Lua programmers who are new to LÖVE need to be aware of that and the restrictions the engine places on those operations. For further information you cannot beat the official module documentation.