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.