Given the discussion on the
Ocean's Heart project topic about making a map menu, I wanted to test the feasibility of generating a mini-map image from the info in the map .dat file. The endeavor has been a success.
What I did is read the map .dat file from a lua script, which gives the size, position and stacking order of all tiles. Also reading the tileset .dat file used by that map gives the ground type (e.g. traversable, wall, etc.) for each tile. I assigned colors to the tiles based on the ground type. Then on a surface I drew rectangles whose size and position corresponds to each tile at 1/8th scale, drawn in the same order as on the map. I drew layers 0 to 2 separately and ended up with this result for the overworld maps:

Note that sometimes the layer 2 image got clipped because the window I was using was only 480 pixels in height. It didn't appear to be losing anything in the cases where it was clipped.
I then manually overlaid the layers, using 50% opacity for layers 1 & 2, and lined up the maps for how they fit together. In hindsight I realize I could have made the script to do the same thing.

Note that the Hourglass Fort Outside and Naerreturn Bay maps overlap with each other, so I moved the Hourglass map off to the side.
This mapping technique could be useful either in generating a reference image from which to create hand-drawn images of the maps, or a modified version of the script could be used to have the quest itself generate the map images to use for the map menu. For pixel-precise accuracy, the purple border between maps (so that edge teletranpsorters are visible) should be removed.
In order to reproduce my results and run the script, do the following:
1) Download version 0.1 of the Ocean's Heart project from the topic linked above (back up any existing save files if you already have the quest)
2) Change the quest size to 640 by 480 in quest.dat
3) In main.lua of that quest comment out line 5, require("scripts/features"), so that the HUD is not displayed
4) Replace the scripts/game_manager.lua file with this one:
local game_manager = {}
local initial_game = require("scripts/initial_game")
require("scripts/multi_events")
-- Starts the game from the given savegame file,
-- initializing it if necessary.
function game_manager:start_game(file_name)
local exists = sol.game.exists(file_name)
local game = sol.game.load(file_name)
if not exists then
-- Initialize a new savegame.
initial_game:initialize_new_savegame(game)
end
sol.video.set_window_size(640, 480) --to prevent display from scaling at 2x
game:start()
local map_layers = require("scripts/menus/display_map")
function game:on_draw(dst_surface)
dst_surface:fill_color{0,0,0} --fill black, don't want to see what's underneath
for layer = 0,2 do
local surface = map_layers[layer]
if surface then
local width, height = surface:get_size()
surface:draw(dst_surface, 1, layer*(height+1)+1) --1 px from edge of screen with images separated by 1 px
end
end
end
end
return game_manager
5) Add this script to the scripts/menus directory, named display_map.lua:
local map_id = "new_limestone/limestone_present"
--local map_id = "goatshead_island/goatshead_harbor"
--local map_id = "goatshead_island/west_goat"
--local map_id = "goatshead_island/goat_hill"
--local map_id = "goatshead_island/lighthouse_cape"
--local map_id = "goatshead_island/poplar_forest"
--local map_id = "goatshead_island/poplar_coast"
--local map_id = "goatshead_island/crabhook_village"
--local map_id = "Yarrowmouth/yarrowmouth_village"
--local map_id = "Yarrowmouth/juniper_grove"
--local map_id = "Yarrowmouth/foothills"
--local map_id = "Yarrowmouth/silent_glade"
--local map_id = "Yarrowmouth/naerreturn_bay"
--local map_id = "Yarrowmouth/kingsdown"
--local map_id = "Yarrowmouth/hourglass_fort/outside"
--local map_id = "ballast_harbor/ballast_harbor"
local tilesets = {}
local maps = {}
local SCALE_FACTOR = 8 --draw maps at 1/8th actual size
--color to draw tile based on ground property; ignore if not listed
local tile_colors = {
traversable = {0, 255, 0}, --green
wall = {127, 127, 127}, --grey
low_wall = {204, 204, 204}, --light grey
teletransporter = {255, 255, 0}, --yellow; this is a type of entity, not a tile ground property
shallow_water = {0, 255, 255}, --light blue
deep_water = {0, 0, 255}, --blue
hole = {0, 0, 0}, --black
ladder = {255, 0, 255}, --pink
lava = {255, 0, 0}, --red
prickles = {255, 127, 0}, --orange
ice = {127, 255, 255}, --pale blue
}
--load tileset .dat file to determine ground property of each tile
local function load_tileset(tileset_id)
local tileset = {}
local env = {}
function env.tile_pattern(properties)
local id = properties.id
assert(id, "tile pattern without id")
id = tonumber(id)
assert(id, "tile pattern id must be a number")
local ground = properties.ground
assert(ground, "tile pattern without ground")
if tile_colors[ground] then --ignore ground properties that don't have a color assigned
tileset[id] = tile_colors[ground] --link the color to use with the tile id
end
end
setmetatable(env, {__index = function() return function() end end})
local chunk = sol.main.load_file("tilesets/"..tileset_id..".dat")
setfenv(chunk, env)
chunk()
return tileset
end
--load map .dat file to get list of tiles used
local function load_map(map_id)
local map = { tiles = {} }
local env = {}
--properties stores the size and coordinates for the map and the tileset used
function env.properties(properties)
local x = tonumber(properties.x)
assert(x, "property x must be a number")
local y = tonumber(properties.y)
assert(y, "property y must be a number")
local width = tonumber(properties.width)
assert(width, "property width must be a number")
local height = tonumber(properties.height)
assert(height, "property height must be a number")
local tileset = properties.tileset
assert(tileset, "properties without tileset")
map.x = x
map.y = y
map.width = width
map.height = height
map.tileset = tileset
end
--each tile defines a size, coordinates and layer as well as the tile id to use
function env.tile(properties)
local pattern = properties.pattern --pattern is the tile id
assert(pattern, "tile without pattern")
pattern = tonumber(pattern)
assert(pattern, "tile pattern must be a number")
local layer = properties.layer
assert(layer, "tile without layer")
layer = tonumber(layer)
assert(layer, "tile layer must be a number")
local x = tonumber(properties.x)
assert(x, "tile x must be a number")
local y = tonumber(properties.y)
assert(y, "tile y must be a number")
local width = tonumber(properties.width)
assert(width, "tile width must be a number")
local height = tonumber(properties.height)
assert(height, "tile height must be a number")
table.insert(map.tiles, {
pattern = pattern,
layer = layer,
x = x,
y = y,
width = width,
height = height,
})
end
--also extract teletransporter usage to be able to draw teletransporter locations
function env.teletransporter(properties)
local layer = properties.layer
assert(layer, "tile without layer")
layer = tonumber(layer)
assert(layer, "tile layer must be a number")
local x = tonumber(properties.x)
assert(x, "tile x must be a number")
local y = tonumber(properties.y)
assert(y, "tile y must be a number")
local width = tonumber(properties.width)
assert(width, "tile width must be a number")
local height = tonumber(properties.height)
assert(height, "tile height must be a number")
table.insert(map.tiles, {
pattern = "teletransporter", --instead of using tile pattern to determine color, tile_colors has a "teletransporter" entry
layer = layer,
x = x,
y = y,
width = width,
height = height,
})
end
setmetatable(env, {__index = function() return function() end end})
local chunk, err = sol.main.load_file("maps/"..map_id..".dat")
setfenv(chunk, env)
chunk()
return map
end
local function generate_map(map_id)
assert(type(map_id)=="string", "Bad argument #1 to 'generate_map' (string expected)")
--load map info from .dat file if not already loaded
if not maps[map_id] then maps[map_id] = load_map(map_id) end
local map = maps[map_id]
--load tileset info from .dat file if not already loaded
if not tilesets[map.tileset] then tilesets[map.tileset] = load_tileset(map.tileset) end
local tileset = tilesets[map.tileset]
--create a surface for each layer
local layers = {}
for layer = 0,2 do
layers[layer] = sol.surface.create(map.width/SCALE_FACTOR+2, map.height/SCALE_FACTOR+2) --include room for 1px border
layers[layer]:fill_color({127, 0, 255}, 0, 0, map.width/SCALE_FACTOR+2, 1) --top 1px purple border
layers[layer]:fill_color({127, 0, 255}, 0, 0, 1, map.height/SCALE_FACTOR+2) --left 1px purple border
layers[layer]:fill_color({127, 0, 255}, 0, map.height/SCALE_FACTOR+1, map.width/SCALE_FACTOR+2, 1) --bottom 1px purple border
layers[layer]:fill_color({127, 0, 255}, map.width/SCALE_FACTOR+1, 0, 1, map.height/SCALE_FACTOR+2) --right 1px purple border
end
--draw each tile
for _,tile in ipairs(map.tiles) do
local pattern = tile.pattern
local tile_color = tile_colors[pattern] or tileset[pattern]
--draw the tile as solid color on surface corresponding to the correct layer
if tile_color and layers[tile.layer] then --ignore corner and empty tiles
layers[tile.layer]:fill_color(
tile_color,
tile.x/SCALE_FACTOR+1,
tile.y/SCALE_FACTOR+1,
tile.width/SCALE_FACTOR,
tile.height/SCALE_FACTOR
)
end
end
return layers
end
return generate_map(map_id)