This script allows you to create a timelapse video from your OpenTTD game. It will use your previously saved games. No need to remember to take screenshots during the game, ad since you play it safe and save before and after each big civil engineering work (ooops ?), you will have enough frames to create a nice work to put into your Company heritage cabinet.

The resulting video is zoomed into a defined location and works with large games (tested on a 512x512 map).

After opening an old game by mistake, I realized how much civil engineering my region had undergone. Having a timelapse of all that heavy work sprung to my mind. But I never saved any screenshot of my games and opening each game one by one is not my favorite thing. Better spend more time to create an automated script.

Basics

OpenTTD has a console that allows some basic commands mainly intended for network games), and more importantly allows you to take screenshots. A second sweet feature, is the support for start up scripts, on openttd level and/or on a game level.

What we need

OpenTTD (obviously)

Python with PIL support

ffmpeg or avconv

Some saved games

Take the right screenshot

OpenTTD’s console allows you take different types of screenshots:

screenshot : just grabs the screen with all the GUI present

: just grabs the screen with all the GUI present screenshot no_con : same as above but without the console window

: same as above but without the console window screenshot big : a zoomed in version

: a zoomed in version screenshot giant : the whole map

The location and zoom level are stored within the saved game. Meaning that when you open a game your visible area is how you left it. You can go to a specific tile with the console command scrollto , but you can’t set a specific zoom level. The first three screenshot options are dependent on zoom level, you won’t get a consistent area for each saved game.

This leaves us only with the giant option, which creates really huge files: 32000x16000, 40MB for a 512x512 map.

Re-sampling the images

Re-sampling the images to 1920x1080 and using it as could be a choice, but you loose the sight of the detail and you end up with only a generic overview. Don’t you prefer to show the amazing landscaping you made to fit this airport and good’s station into the city?

We will cut out the interesting region from the main file, to simulate a zoom-in and achieve something similar to screenshot big , but without having to worry about the zoom.

The steps

I ran this script on Ubuntu 14.04, it will work on other Linux distro or OSX and should work on Windows too (still someone using Windows, seriously ?) as long as you have all the tools installed. The paths in the sample are for Ubuntu, just replace them with the ones for your installation.

Screenshot and exit

To take a screenshot and exit immediately after starting a game, simply create the file ~/.openttd/scripts/game_start.scr , with this content:

screenshot giant exit

Start a game

You can start OpenTTD and directly open a game:

openttd -x -g game_filename

Crop and resize images

PIL is surprisingly handling this quite well. My first approach was to use ImageMagick, but the time and CPU taken to crop the huge source file was below expectations.

Display the right area

I was too lazy to write a tile_to_pixel method by using matrix calculation and geographic projections, so I went the easy route by using the position relative to the source image.

The final script

#!/usr/bin/python import argparse import glob import os from PIL import Image from PIL import ImageFont from PIL import ImageDraw from distutils.spawn import find_executable import time import re import locale class OpenTTDTimelapse : args = None autoexec_script_name = "autoexec.scr" gamestart_script_name = "game_start.scr" autoexec_script_path = None gamestart_script_path = None screenshot_path = None save_path = None ottd = None openttd = None ffmpeg = None backup_autoexec = False backup_gamestart = False def __init__ ( self , args ): self . args = args self . define_paths () def define_paths ( self ): self . ottd = os . path . realpath ( os . path . expanduser ( self . args . ottd )) self . autoexec_script_path = os . path . join ( self . ottd , "scripts" , self . autoexec_script_name ) self . gamestart_script_path = os . path . join ( self . ottd , "scripts" , self . gamestart_script_name ) self . screenshot_path = os . path . join ( self , self . ottd , "screenshot" ) self . save_path = os . path . join ( self . ottd , "save" ) if not os . path . isdir ( self . ottd ): print "No such folder %s " % self . ottd exit ( 1 ) if not os . path . isdir ( os . path . join ( self . ottd , "save" )): print "Folder %s is not an OpenTTD folder" % self . ottd exit ( 1 ) self . openttd = find_executable ( "openttd" ) if self . openttd is None : print "openttd not found, please install openttd" exit ( 1 ) self . ffmpeg = find_executable ( "ffmpeg" ) if self . ffmpeg is None : self . ffmpeg = find_executable ( "avconv" ) if self . ffmpeg is None : print "ffmpeg or avconv not found, please install ffmpeg or libav-tools" exit ( 1 ) def get_file_date ( self , filename ): return time . strptime ( " " . join ( re . split ( "^([0-9]+)[a-z]*\s([a-zA-Z]+)\s([0-9]+)$" , os . path . basename ( filename ) . split ( ',' )[ - 1 ] . split ( '.' )[ 0 ] . strip ())[ 1 : 4 ]), " %d %b %Y" ) def get_save_game ( self ): files = sorted ( glob . glob ( os . path . join ( self . save_path , " %s *.sav" % self . args . company )), key = lambda x : time . strftime ( "%Y%m %d " , self . get_file_date ( x ))) return files def backup_script ( self , script_path ): backup = False if os . path . isfile ( script_path ): backup = True os . rename ( script_path , script_path + ".orig" ) return backup def make_screenshot_script ( self , screenshot_name ): f = open ( self . gamestart_script_path , "w" ) f . write ( "screenshot giant " + screenshot_name + os . linesep + "exit" ) f . close () def get_crop_info ( self , im ): width0 , height0 = im . size width1 = self . args . width * self . args . zoom height1 = self . args . height * self . args . zoom left1 = int ( round (( width0 * self . args . left / 100 ) - ( width1 / 2 ))) top1 = int ( round (( height0 * self . args . top / 100 ) - ( height1 / 2 ))) right1 = left1 + width1 bottom1 = top1 + height1 if right1 > width0 : delta = right1 - width0 left1 = left1 - delta if bottom1 > height0 : delta = bottom1 - height0 bottom1 = bottom1 - delta if right1 < 0 : right1 = 0 if top1 < 0 : top1 = 0 return ( left1 , top1 , right1 , bottom1 ) def draw_date ( self , im1 , game_file ): txt = time . strftime ( "%Y" , self . get_file_date ( game_file )) txt_margins = ( int ( round ( self . args . width * 0.015 )), int ( round ( self . args . height * 0.015 ))) draw = ImageDraw . Draw ( im1 ) font_size = int ( round ( self . args . height / 20 )) font = ImageFont . truetype ( self . args . font_path , font_size ) txt_size = font . getsize ( txt ) txt_pos = ( self . args . width - txt_margins [ 0 ] - txt_size [ 0 ], self . args . height - txt_margins [ 1 ] - 1.5 * txt_size [ 1 ]) draw . text ( txt_pos , txt ,( 255 , 255 , 255 ), font = font ) return im1 def clean ( self ): if not self . args . debug : if os . path . isfile ( self . gamestart_script_path ): os . unlink ( self . gamestart_script_path ) for frame_image in glob . glob ( os . path . join ( self . screenshot_path , "frame_*.png" )): os . unlink ( frame_image ) if self . backup_autoexec : os . rename ( self . autoexec_script_path + ".orig" , self . autoexec_script_path ) if self . backup_gamestart : os . rename ( self . gamestart_script_path + ".orig" , self . gamestart_script_path ) def create_movie ( self ): file_id = 1 company = None game_files = self . get_save_game () if len ( game_files ) == 0 : print "No game files found for %s " % self . args . company exit ( 1 ) self . backup_autoexec = self . backup_script ( self . autoexec_script_path ) self . backup_gamestart = self . backup_script ( self . gamestart_script_path ) for game_file in game_files : game = os . path . basename ( game_file ) if company is None : company = game . split ( "," )[ 0 ] . strip () screenshot_name = "timelapse_ %05d " % file_id screenshot_name_ext = screenshot_name + ".png" self . make_screenshot_script ( screenshot_name ) os . system ( self . openttd + " -x -g \" " + game + " \" " ) os . unlink ( self . gamestart_script_path ) im = Image . open ( os . path . join ( self . screenshot_path , screenshot_name_ext )) crop_info = self . get_crop_info ( im ) frame_name = "frame_ %05d .png" % file_id final_frame = os . path . join ( self . screenshot_path , frame_name ) im1 = im . crop ( crop_info ) . convert ( 'RGB' ) . resize (( self . args . width , self . args . height ), Image . BILINEAR ) if self . args . timestamp : im1 = self . draw_date ( im1 , game_file ) if self . args . check : final_frame = "timelapse_check.png" im1 . convert ( 'P' , palette = Image . ADAPTIVE , colors = 255 ) . save ( final_frame ) im = im1 = None file_id += 1 if self . args . check : break if not self . args . debug : os . unlink ( os . path . join ( self . screenshot_path , screenshot_name_ext )) if not self . args . check : ffmpeg_cmd = " %s -y -r 1 -i %s /frame_ %% 05d.png -r 24 -s %d x %d -c:v libx264 -an -vsync cfr \" %s .mp4 \" " % ( self . ffmpeg , self . screenshot_path , self . args . width , self . args . height , company ) os . system ( ffmpeg_cmd ) self . clean () if __name__ == "__main__" : parser = argparse . ArgumentParser ( description = 'Create a Timelapse from an OpenTTD game' ) parser . add_argument ( '-c' , '--company' , dest = 'company' , required = True , help = 'Game Name' ) parser . add_argument ( '-d' , '--dir' , dest = 'ottd' , metavar = 'OPENTTD_FOLDER' , default = '~/.openttd' , help = 'OpenTTD folder (default %(default)s )' ) parser . add_argument ( '-dx' , '--width' , dest = 'width' , type = int , default = 1920 , help = 'Width of frames (px) (default: %(default)s )' ) parser . add_argument ( '-dy' , '--height' , dest = 'height' , type = int , default = 1080 , help = 'Height of frames (px) (default: %(default)s )' ) parser . add_argument ( '-z' , '--zoom' , dest = 'zoom' , type = int , default = 2 , choices = xrange ( 1 , 5 ), help = 'Scale factor' ) parser . add_argument ( '-l' , '--left' , dest = 'left' , type = int , default = 50 , help = 'Position from left in percent (default: %(default)s )' ) parser . add_argument ( '-t' , '--top' , dest = 'top' , type = int , default = 50 , help = 'Position from top in percent (default: %(default)s )' ) parser . add_argument ( '-s' , '--timestamp' , dest = 'timestamp' , action = "store_true" , help = 'Display year on frames' ) parser . add_argument ( '-f' , '--font' , dest = 'font_path' , metavar = "FONT PATH" , default = '/usr/share/fonts/truetype/msttcorefonts/Arial.ttf' , help = "Path to TTF file" ) parser . add_argument ( '--debug' , dest = 'debug' , action = "store_true" , help = 'Do not clean temp files' ) parser . add_argument ( '--check' , dest = 'check' , action = "store_true" , help = 'Output first image only, useful to verify the selected area' ) args = parser . parse_args () ottd = OpenTTDTimelapse ( args ) ottd . create_movie ()

Help

python openttd_timelapse.py --help should give you all the information you need.

usage: openttd_timelapse.py [ -h ] -c COMPANY [ -d OPENTTD_FOLDER ] [ -dx WIDTH ] [ -dy HEIGHT ] [ -z { 1,2,3,4 }] [ -l LEFT ] [ -t TOP ] [ -s ] [ -f FONT PATH ] [ --debug ] [ --check ] Create a Timelapse from an OpenTTD game optional arguments: -h, --help show this help message and exit -c COMPANY, --company COMPANY Game Name -d OPENTTD_FOLDER, --dir OPENTTD_FOLDER OpenTTD folder ( default ~/.openttd ) -dx WIDTH, --width WIDTH Width of frames ( px ) ( default: 1920 ) -dy HEIGHT, --height HEIGHT Height of frames ( px ) ( default: 1080 ) -z { 1,2,3,4 } , --zoom { 1,2,3,4 } Scale factor -l LEFT, --left LEFT Position from left in percent ( default: 50 ) -t TOP, --top TOP Position from top in percent ( default: 50 ) -s, --timestamp Display year on frames -f FONT PATH, --font FONT PATH Path to TTF file --debug Do not clean temp files --check Output first image only, useful to verify the selected area

output

Movie is created in current path, with the name company_name.mp4

-z, -zoom {1,2,3,4}

Allows to cut out a smaller or bigger area from the source file which is resized to the movie size.

Zoom 1 crops out the exact size. Zoom 2 the double size. The timelapse will display a bigger regions, similar to a “zoom-out” in game.

-c, -check

Creates only the first file in current folder as timelapse_check.png. This allows you to verify that the settings are alright, before starting the generation of all images.