339 lines
9.4 KiB
Lua
339 lines
9.4 KiB
Lua
--Load and save the bmp formats used by DEFCON.
|
|
local t = {}
|
|
local assert = assert
|
|
local print = print
|
|
local error = error
|
|
local table = table
|
|
local math = math
|
|
local love = assert( love )
|
|
|
|
|
|
|
|
local test = {}
|
|
|
|
function test.load( filename )
|
|
local imgd = love.image.newImageData( filename )
|
|
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
|
|
local img = love.graphics.newImage( imgd )
|
|
img:setFilter( "nearest", "nearest" )
|
|
return img, imgd
|
|
end
|
|
|
|
function test.bitmapToWorld( x, y )
|
|
local w, a = 360.0, 600.0 / 800.0
|
|
local h = 360.0 * a
|
|
x = w * ( x - 800 ) / 800 - w / 2 + 360
|
|
y = h * ( y - 600 ) / 600 + 180
|
|
return x, y
|
|
end
|
|
|
|
function test.compareData( a, b )
|
|
if a == b then return true end
|
|
print( "lengths:", a:len(), b:len() )
|
|
local errors = 0
|
|
for i = 1, math.min( a:len(), b:len() ) do
|
|
local a, b = a:sub( i, i ), b:sub( i, i )
|
|
if a ~= b then
|
|
errors = errors + 1
|
|
print( "mismatch:", errors, i, string.byte( a ), string.byte( b ) )
|
|
end
|
|
if errors > 1000 then break end
|
|
end
|
|
error( "test failed. output bitmap does not match!" )
|
|
end
|
|
|
|
function test.worldToBitmap( x, y )
|
|
x = 800 * ( x + 180 ) / 360
|
|
y = 600 + 800 * ( y - 180 ) / 360
|
|
return x, y
|
|
end
|
|
|
|
local function getHeader( filename )
|
|
local offset = 2 + love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
|
|
local header, size = assert( love.filesystem.read( filename, offset ) )
|
|
return header
|
|
end
|
|
|
|
--BGR24 format. Takes the input of Love2D's data:getPixel( x, y )
|
|
--on paper we would only care about one channel because all of these images are grayscale,
|
|
--but a few of the pixels in the game's default territories are not exactly gray,
|
|
--so we do it like this to make sure our output is byte-faithful when saving an unmodified vanilla map
|
|
local function bgrChar( r, g, b )
|
|
return string.char( math.floor( b * 255 ), math.floor( g * 255 ), math.floor( r * 255 ) )
|
|
end
|
|
|
|
|
|
local formats = {
|
|
["africa"] = {
|
|
header = getHeader( "data/earth/africa.bmp" ),
|
|
w = 512,
|
|
h = 285,
|
|
},
|
|
["sailable"] = {
|
|
header = getHeader( "data/earth/sailable.bmp" ),
|
|
w = 512,
|
|
h = 285,
|
|
--technically this information is in the header already but I don't want to write code to parse it we only use one palette ever
|
|
palette = {
|
|
[0] = 0,
|
|
[8] = 1,
|
|
[20] = 2,
|
|
[33] = 3,
|
|
[49] = 4,
|
|
[90] = 5,
|
|
[139] = 6,
|
|
[169] = 7,
|
|
[189] = 8,
|
|
[206] = 9,
|
|
[214] = 10,
|
|
[222] = 11,
|
|
[231] = 12,
|
|
[239] = 13,
|
|
[247] = 14,
|
|
[255] = 15
|
|
},
|
|
},
|
|
["travel"] = {
|
|
header = getHeader( "data/earth/travel_nodes.bmp" ),
|
|
w = 800,
|
|
h = 400,
|
|
},
|
|
["ai"] = {
|
|
header = getHeader( "data/earth/ai_markers.bmp" ),
|
|
w = 512,
|
|
h = 285,
|
|
},
|
|
}
|
|
|
|
function formats.ai:test( )
|
|
print "testing ai nodes"
|
|
local filename = "data/earth/ai_markers.bmp"
|
|
local img, imgd = test.load( filename )
|
|
local idx = 1
|
|
local nodes = {}
|
|
for x = 0, 511 do
|
|
for y = 0, 284 do
|
|
local r, g = imgd:getPixel( x, 284 - y )
|
|
if r > 0.5 or g > 0.5 then
|
|
local long = x * (360 / imgd:getWidth()) - 180
|
|
local lat = y * (200 / img:getHeight()) - 100
|
|
local attacking = (r > 0.5)
|
|
local node = {x = long, y = lat, attacking = attacking, idx = idx}
|
|
print( "ai marker", idx, x, y, long, lat )
|
|
nodes[ idx ] = node
|
|
idx = idx + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local encodedString = self:encode( nodes )
|
|
love.filesystem.write( "ai_markers.bmp", encodedString )
|
|
|
|
local eimg, eimgd = test.load( "ai_markers.bmp" ) --the one we just saved
|
|
for x = 0, 511 do
|
|
for y = 0, 284 do
|
|
local r, g = imgd:getPixel( x, 284 - y )
|
|
local er, eg = eimgd:getPixel( x, 284 - y )
|
|
if math.max( r, g, er, eg ) > 0.5 then
|
|
print( "node pixel: ", x, 284 - y, r, g, er, eg )
|
|
assert( ( r > 0.5 and er > 0.5 ) or ( g > 0.5 and eg > 0.5 ), "ai marker mismatch!" )
|
|
end
|
|
end
|
|
end
|
|
print( "ai markers OK" )
|
|
end
|
|
|
|
function formats.ai:encode( data )
|
|
|
|
--set up bitmap as an array of pixels
|
|
local w, h = self.w, self.h
|
|
local size = 2 + self.w * self.h
|
|
local bitmap = { self.header:sub( 1, -3 ) }
|
|
for j = 2, size do bitmap[j] = 0 end
|
|
|
|
|
|
for i, point in ipairs( data ) do
|
|
local wx, wy = point.x, point.y
|
|
--get bitmap coordinates
|
|
local x = math.floor(self.w * (wx + 180) / 360)
|
|
--idk why exactly I need to round the value here instead of truncating it
|
|
local y = math.floor(0.5 + self.h * (wy + 100) / 200)
|
|
--get index into byte array
|
|
local idx = 2 + y * self.w + x
|
|
--in-editor we could have two points in the same place, possibly of the same or different types
|
|
--in case we miss something upstream, don't corrupt these duplicate points,
|
|
--just saturate the attack/defense value
|
|
if bitmap[idx] == 0 then bitmap[idx] = { attack = point.attacking, place = not(point.attacking)}
|
|
else
|
|
bitmap[idx].attack = bitmap[idx].attack or point.attacking
|
|
bitmap[idx].place = bitmap[idx].place or not(point.attacking)
|
|
end
|
|
end
|
|
|
|
--now map pixels to 3-byte strings
|
|
for j = 2, size do
|
|
if bitmap[j] == 0
|
|
then bitmap[j] = "\0\0\0"
|
|
else bitmap[j] = string.char( 0, bitmap[j].place and 0xff or 0, bitmap[j].attack and 0xff or 0 ) --bgr24
|
|
end
|
|
end
|
|
|
|
return table.concat( bitmap )
|
|
end
|
|
|
|
function formats.travel:test()
|
|
print "testing travel nodes"
|
|
local filename = "data/earth/travel_nodes.bmp"
|
|
local img, imgd = test.load( filename )
|
|
local nodes = {}
|
|
local n = 1
|
|
for x = 0, 799 do
|
|
for y = 0, 399 do
|
|
if imgd:getPixel( x, 399 - y ) > 0 then
|
|
local long, lat = test.bitmapToWorld( x, y )
|
|
nodes[n] = {x = long, y = lat, idx = n}
|
|
print( "read:", n, long, lat, x, y )
|
|
n = n + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
print( "loaded ", n, "nodes" )
|
|
local encodedString = self:encode( nodes )
|
|
test.compareData( love.filesystem.read( filename ), encodedString )
|
|
love.filesystem.write( "travel_nodes.bmp", encodedString )
|
|
print( "travel nodes OK" )
|
|
end
|
|
|
|
--Sparse bitmap with a few white pixels.
|
|
function formats.travel:encode( points )
|
|
--set up bitmap as an array of 8-bit pixels
|
|
local w, h = self.w, self.h
|
|
local size = 2 + w * h
|
|
local bitmap = { self.header }
|
|
for j = 2, size do bitmap[j] = 0 end
|
|
|
|
--write white pixels
|
|
for i, point in ipairs( points ) do
|
|
local wx, wy = point.x, point.y
|
|
-- get bitmap coordinates:
|
|
local x = 800 * ( wx + 180 ) / 360
|
|
local y = 600 + 800 * ( wy - 180 ) / 360
|
|
local offset = math.floor( y * self.w + x - 2 )
|
|
--1 := white
|
|
bitmap[offset] = 1
|
|
end
|
|
|
|
--fold into 4-bit pixel array
|
|
local nybbles = { bitmap[1] }
|
|
for j = 2, size / 2 do
|
|
local a, b = bitmap[ 2 * j - 2 ], bitmap[ 2 * j - 1 ]
|
|
nybbles[j] = string.char( 16 * a + b )
|
|
end
|
|
|
|
return table.concat( nybbles )
|
|
end
|
|
|
|
function formats.sailable:test()
|
|
print "testing sailable"
|
|
local filename = "data/earth/sailable.bmp"
|
|
local img, imgd = test.load( filename )
|
|
local encoded = self:encode( imgd )
|
|
love.filesystem.write( "sailable_out.bmp", encoded )
|
|
test.compareData( love.filesystem.read( filename ), encoded )
|
|
print "sailable OK"
|
|
end
|
|
|
|
|
|
do --sailable
|
|
|
|
local reversePalette = {}
|
|
for eight, four in pairs( formats.sailable.palette ) do
|
|
reversePalette[ four ] = eight
|
|
end
|
|
|
|
--take the red channel in float [0, 1] format
|
|
--expand it to a byte, then quantize it to 4 bits
|
|
--according to the sailable palette
|
|
local function sailableQuantize( r )
|
|
if r == 0 then return 0 end
|
|
if r >= 1 then return 15 end
|
|
r = math.floor( r * 255 )
|
|
|
|
for four = 1, #reversePalette do
|
|
if reversePalette[ four ] > r then return four end
|
|
end
|
|
|
|
error( "Could not quantize sailable.bmp!" )
|
|
end
|
|
|
|
function formats.sailable:encode( data )
|
|
local w, h = self.w, self.h
|
|
local bytes = { self.header:sub( 1, -2 ) }
|
|
local i = 2
|
|
|
|
--y coordinates are written top to bottom
|
|
for y = h - 1, 0, -1 do
|
|
for x = 0, w - 1 do
|
|
bytes[i] = sailableQuantize( data:getPixel(x, y) )
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
--fold into 4-bit pixel array
|
|
local nybbles = { bytes[1] }
|
|
for j = 2, #bytes / 2 do
|
|
local a, b = bytes[ 2 * j ], bytes[ 2 * j + 1 ]
|
|
nybbles[j] = string.char( 16 * a + b )
|
|
end
|
|
|
|
return table.concat( nybbles )
|
|
end
|
|
|
|
function formats.africa:test()
|
|
print "testing africa"
|
|
local filename = "data/earth/africa.bmp"
|
|
local img, imgd = test.load( filename )
|
|
local encoded = self:encode( imgd )
|
|
love.filesystem.write( "africa_out.bmp", encoded )
|
|
test.compareData( love.filesystem.read( filename ), encoded )
|
|
print "africa OK"
|
|
end
|
|
end
|
|
|
|
function formats.africa:encode( data )
|
|
local w, h = self.w, self.h
|
|
local bytes = { self.header:sub( 1, -3 ) }
|
|
local i = 2
|
|
|
|
--y coordinates are written top to bottom
|
|
for y = h - 1, 0, -1 do
|
|
for x = 0, w - 1 do
|
|
bytes[i] = bgrChar( data:getPixel(x, y) )
|
|
i = i + 1
|
|
end
|
|
end
|
|
return table.concat( bytes )
|
|
end
|
|
|
|
function t.load( filename )
|
|
local imgd = love.image.newImageData( filename )
|
|
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
|
|
local img = love.graphics.newImage( imgd )
|
|
img:setFilter( "nearest", "nearest" )
|
|
return img, imgd
|
|
end
|
|
|
|
function t.save( data, format )
|
|
return assert(formats[format]):encode( data )
|
|
end
|
|
|
|
--convenience
|
|
formats.territory = formats.africa
|
|
for fmt, tbl in pairs( formats ) do
|
|
t[fmt] = function( data ) return tbl:encode( data ) end
|
|
--TESTING
|
|
--tbl:test()
|
|
end
|
|
|
|
return t |