LÖVE File-System Introduction

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:

  1. The *.love archive containing our game, i.e. the format for packaging LÖVE games.

  2. 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 loadfile() and 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):

/home/eric/.local/share/love/LNVL

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 loadfile().

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.

Conclusion

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.

Advertisements

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s