A Weird Imagination

Displaying Factorio history

Posted in

The problem#

Last week, I got all of the Factorio saves I had been keeping around into a single directory in order by the time they were created. But what should we do with that data? We could load arbitrary saves to see what our base looked like in the past, but loading the saves individually isn't a great way to do that when there's a lot of them.

The solution#

Luckily, I'm not the only one to want screenshots of my Factorio bases, so there are existing mods to do so.

The FactorioMaps Timelapse mod will take a list of saves and generates a web page like this demo that lets you look around your base across time. As the documentation explains, this is not actually a mod you enable for your save, but a script that you place in your mods/ directory that will run Factorio repeatedly to generate screenshots.

To set it up, use a non-Steam install of Factorio and put the saves you want under its saves directory (in a subdirectory named to_screenshot/ in this example). If you downloaded the mod as ZIP file (e.g., by installing the mod from within Factorio), unzip it; alternatively you can clone the GitHub repo or my fork which includes a few minor improvements for when displaying a lot of saves, especially ones less than an hour apart.

# get to Factorio install /mods/ directory
cd factorio/mods
# clone the git repo with the mod's internal name
git clone https://github.com/dperelman/FactorioMaps.git L0laapk3_FactorioMaps
cd L0laapk3_FactorioMaps
# install dependencies
python -m venv .venv
. .venv/bin/activate
pip install --upgrade -r requirements.txt
# add saves in /saves/to_screenshot/ to timelapse "mybase"
python auto.py --standalone mybase to_screenshot/*

Then you can find the timelapse in the directory script-output/FactorioMaps/mybase/ of your Factorio install; just open index.html in any web browser. Especially if you have a lot of saves, consider adding the --dayonly option to not take twice as much time also generating screenshots of the night view.

Generating screenshots as you play#

Note that if you just want to take screenshots automatically as you play, you can use the Screenshot Toolkit mod, which is what I use for single player games. But due to the way Factorio multiplayer works, doing so impacts every player, so in a multiplayer game it may be better to just copy the saves as the game runs and generate the screenshots later.

The details#

Data flow in FactorioMaps#

Once again, this is not my code. I only made very minor modifications to it described in the later sections of this post, which required me to understand a bit about how it worked.

Python to Lua communication#

The basic workflow is that the Python script is given a list of save files to take screenshots of, so for each one it needs to run Factorio and tell it to take a screenshot, passing along the relevant settings. Factorio doesn't have command-line arguments for taking screenshots and there's no direct mechanism for a mod to be passed along information from a script calling Factorio. Mods are not even allowed to read back the files they write. So the workaround the Python script uses is that before each time it runs Factorio, it writes out a Lua file named autorun.lua containing the information it wants to pass along to the mod, and the mod reads the values set in that file to decide what to do. This is why the mod has to be unzipped: every time the script runs Factorio, it's actually running it with a different version of the mod.

Lua to Python communication#

The mod communicates back by writing out a file mapInfo.json which is used to maintain state both between runs of Factorio and between completely separate runs of the script (e.g., re-running the script to add a new save to an existing timelapse).

Using that information, the Python script crops and re-encodes the screenshots.

Python to Javascript communication#

The mapInfo.json file is also used to communicate the information to the Javascript that displays the timelapse in the web browser, albeit indirectly: it's written into mapInfo.js to avoid having to fetch the JSON file. Since fetch() is async, the code would need to be reworked somehow to be in an async context, and the straightforward solution of using top-level await requires using modules which are forbidden for file:/// URLs, and therefore would introduce the otherwise unnecessary requirement of using a web server.

Display names of saves#

The first issue I encountered is that the way FactorioMaps identified my saves was weird and confusing. It showed names like 2h, 2-1h, 2-2h, …, 2-10h, 3h, 3-1h, …. Eventually I figured out what was happening is that each save gets identified by its hours of game time and if there's a collision, then it adds a dash and an incrementing number. So those meant the first through eleventh saves at 2 hours of game time and the first and second saves at 3 hours of game time. If the saves are spaced at least an hour apart, that naming scheme makes perfect sense. But I had saves spaced five minutes apart, so it was not what I wanted.

Luckily, mapInfo.json already had a tick property associated with each save, storing how long the game has been running as Factorio measures time. By default the game runs at 60 ticks per second, so we can divide that by 60 to get seconds and by 60 again to get minutes. This change (which I offered in this issue) changes the Javascript to use that information to display the time played in h:mm format and reworks a few places in the code that assumed the name shown to the user was the same as the internal name, so a future change could choose any other desired format.

Display realtime day played#

Since I have many saves per session, I wanted to also make it easy to see where each session started and ended. An easy way to identify game sessions is just by what day we played on. Unfortunately, mapInfo.json does not contain that information. This is where I had to understand how the mapInfo.json mechanism worked to figure what I had to change to include the mtime in mapInfo.json, which required modifying the code that generates the JSON both in the Python script and the Lua mod. Then it was straightforward to display the date on the first save for each day.

Fitting many saves#

The slider for selecting which save display was not intended to handle hundreds of saves. They end up squished together with their names overlapping.

To fix that, I made it scrollable using the CSS overflow: scroll; and making the height of the slider not be limited by the height of the window. One catch is that the page uses the mouse scroll wheel for zooming the map display, so I had to fix that by adding a listener for the wheel event and stopping it from bubbling up when the scrollbar is visible. Putting that all together gets

container.addEventListener("wheel", ev => {
  // Check if there's scrollbar...
  if (container.scrollHeight > container.clientHeight) {
    // ... if so, mouse wheel should scroll that.
    ev.stopPropagation();
  }
});

I also had to adjust the positioning logic to compute using the tick instead of just the hour. Finally, I made the spacing more compact if the slider ends up taller than the window.

Removing the character#

I noticed the screenshot had my engineer always sitting at the center of the map, which didn't really make sense. As a simple fix, I removed the character so there's no characters visible in the screenshots at all. I thought this didn't work at first because looking at Factorio, the character appeared to still be there after I thought it said it had taken the screenshot. But looking at the actual screenshots, the character wasn't there.

Unimplemented visualization ideas#

I considered it might be nice to show all of the players where they currently were, but I did not end up trying to do that. To start, I added information about all players to mapInfo.json, including their name, position, color, and when they were last online, which could be used to do draw them on in the Javascript or could be taken as an example of how to get that information so they could be placed on the map in Lua before taking the screenshot. That information could also be used to create visualizations like a trail or a heatmap.

While not currently dumped, the last modified information on entities could also possibly make an interesting visualization showing who worked where.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.