fixed bitmap export (files were being written in text mode)
This commit is contained in:
parent
03f1a96112
commit
b8e00cf340
|
@ -1,2 +1,3 @@
|
||||||
build/
|
build/
|
||||||
test/
|
test/
|
||||||
|
backup/
|
2
ai.lua
2
ai.lua
|
@ -79,7 +79,7 @@ function t.draw( nodes )
|
||||||
end
|
end
|
||||||
|
|
||||||
function t.save( nodes )
|
function t.save( nodes )
|
||||||
return bmp.savePoints( nodes.all, "512rgb24" )
|
return bmp.ai( nodes.all )
|
||||||
end
|
end
|
||||||
|
|
||||||
return t
|
return t
|
364
bmp.lua
364
bmp.lua
|
@ -1,48 +1,17 @@
|
||||||
--Load and save the bmp formats used by DEFCON.
|
--Load and save the bmp formats used by DEFCON.
|
||||||
local t = {}
|
local t = {}
|
||||||
|
local assert = assert
|
||||||
|
local print = print
|
||||||
|
local error = error
|
||||||
|
local table = table
|
||||||
|
local math = math
|
||||||
local love = assert( love )
|
local love = assert( love )
|
||||||
local lfs = love.filesystem
|
|
||||||
|
|
||||||
--FFI bit-twiddling stuff.
|
|
||||||
local ffi = require 'ffi'
|
|
||||||
local bit = require 'bit'
|
|
||||||
|
|
||||||
local function getHeader( filename )
|
|
||||||
local offset = love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
|
|
||||||
local header, size = assert( love.filesystem.read( filename, offset ) )
|
|
||||||
print( "BMP HEADER", filename, size, "\n", string.byte( header, 1, size ) )
|
|
||||||
return header
|
|
||||||
end
|
|
||||||
|
|
||||||
local formats = {
|
local test = {}
|
||||||
["512rgb24"] = {
|
|
||||||
header = getHeader( "data/earth/africa.bmp" ),
|
|
||||||
encode = function( r ) return math.floor( r * 255 ) end,
|
|
||||||
bytesPerPixel = 3,
|
|
||||||
w = 512,
|
|
||||||
h = 285,
|
|
||||||
},
|
|
||||||
["512r4"] = {
|
|
||||||
header = getHeader( "data/earth/sailable.bmp" ),
|
|
||||||
encode = function( r ) return math.floor( r * 255 / 16 ) end,
|
|
||||||
bytesPerPixel = 0.5,
|
|
||||||
w = 512,
|
|
||||||
h = 285,
|
|
||||||
},
|
|
||||||
["800r4"] = {
|
|
||||||
header = getHeader( "data/earth/travel_nodes.bmp" ),
|
|
||||||
encode = function( r ) return math.floor( r * 255 / 16 ) end,
|
|
||||||
bytesPerPixel = 0.5,
|
|
||||||
w = 800,
|
|
||||||
h = 400,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function t.header( format )
|
function test.load( filename )
|
||||||
return formats[format] and formats[format].header
|
|
||||||
end
|
|
||||||
|
|
||||||
function t.load( filename )
|
|
||||||
local imgd = love.image.newImageData( filename )
|
local imgd = love.image.newImageData( filename )
|
||||||
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
|
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
|
||||||
local img = love.graphics.newImage( imgd )
|
local img = love.graphics.newImage( imgd )
|
||||||
|
@ -50,78 +19,149 @@ function t.load( filename )
|
||||||
return img, imgd
|
return img, imgd
|
||||||
end
|
end
|
||||||
|
|
||||||
--maps an array of 1-byte pixel values (numbers 0 to 15 inclusive)
|
function test.bitmapToWorld( x, y )
|
||||||
--to an array of half-byte pixel values (which can be concatenated into a string)
|
local w, a = 360.0, 600.0 / 800.0
|
||||||
--BUT don't touch the first element (which is assumed to be a header or something)
|
local h = 360.0 * a
|
||||||
local function foldByteArray( bytes )
|
x = w * ( x - 800 ) / 800 - w / 2 + 360
|
||||||
local nybbles = { bytes[1] }
|
y = h * ( y - 600 ) / 600 + 180
|
||||||
for j = 2, #bytes / 2 do
|
return x, y
|
||||||
local a, b = bytes[ 2 * j - 2 ], bytes[ 2 * j - 1 ]
|
|
||||||
nybbles[j] = string.char( 16 * a + b )
|
|
||||||
end
|
|
||||||
return nybbles
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function packTwentyFour( bytes )
|
function test.compareData( a, b )
|
||||||
local twentyFour = { bytes[1] }
|
if a == b then return true end
|
||||||
for j = 2, #bytes do
|
print( "lengths:", a:len(), b:len() )
|
||||||
twentyFour[j] = string.char( bytes[j], bytes[j], bytes[j] )
|
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
|
end
|
||||||
return twentyFour
|
error( "test failed. output bitmap does not match!" )
|
||||||
end
|
end
|
||||||
|
|
||||||
function t.save( data, format )
|
function test.worldToBitmap( x, y )
|
||||||
local w, h = data:getDimensions()
|
x = 800 * ( x + 180 ) / 360
|
||||||
format = assert( formats[format] )
|
y = 600 + 800 * ( y - 180 ) / 360
|
||||||
local bytes = { format.header }
|
return x, y
|
||||||
format.byte = 0
|
|
||||||
local i = 2
|
|
||||||
for y = h - 1, 0, -1 do
|
|
||||||
for x = 0, w - 1 do
|
|
||||||
bytes[i] = format.encode( data:getPixel(x, y) )
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if format.bytesPerPixel < 1 then bytes = foldByteArray( bytes )
|
|
||||||
else bytes = packTwentyFour( bytes ) end
|
|
||||||
return table.concat( bytes )
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--takes an array of world-space points in [-180, 180] x [-100, 100]
|
local function getHeader( filename )
|
||||||
--e.g { { x = 0.5, y = 0.5 }, { x = 0.4, y = 0.6 }, }, etc.
|
local offset = 2 + love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
|
||||||
function t.savePoints( points, format )
|
local header, size = assert( love.filesystem.read( filename, offset ) )
|
||||||
format = assert( formats[format] )
|
print( "BMP HEADER", filename, size, "\n", string.byte( header, 1, size ) )
|
||||||
|
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 since we only use this one header
|
||||||
|
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
|
--set up bitmap as an array of pixels
|
||||||
local w, h = format.w, format.h
|
local w, h = self.w, self.h
|
||||||
local size = 2 + format.w * format.h
|
local size = 2 + self.w * self.h
|
||||||
local bitmap = { format.header }
|
local bitmap = { self.header:sub( 1, -3 ) }
|
||||||
for j = 2, size do bitmap[j] = 0 end
|
for j = 2, size do bitmap[j] = 0 end
|
||||||
|
|
||||||
--this is black-and-white only. easy case
|
|
||||||
if format.bytesPerPixel < 1 then
|
|
||||||
for i, point in ipairs( points ) do
|
|
||||||
local wx, wy = point.x, point.y
|
|
||||||
-- get bitmap coordinates:
|
|
||||||
-- N.B., y's divided by 190 rather than 200 because of an idiosyncracy in the travel node image
|
|
||||||
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (wy + 100) / 190)
|
|
||||||
|
|
||||||
--get index into byte array
|
for i, point in ipairs( data ) do
|
||||||
local idx = 2 + y * format.w + x
|
|
||||||
print( "SAVING POINT:", wx, wy, x, format.h - y - 1, idx )
|
|
||||||
bitmap[idx] = 1 --white: this header uses an indexed palette
|
|
||||||
end
|
|
||||||
--now map pixels to 4-bit strings, slap on the header, and pass it back up
|
|
||||||
return table.concat( foldByteArray( bitmap ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
--cf. ai.lua: these points can be red or green
|
|
||||||
for i, point in ipairs( points ) do
|
|
||||||
local wx, wy = point.x, point.y
|
local wx, wy = point.x, point.y
|
||||||
--get bitmap coordinates
|
--get bitmap coordinates
|
||||||
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (1.0 - (wy + 100) / 200))
|
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)
|
||||||
|
print( "export ai marker", i, x, y, wx, wy )
|
||||||
--get index into byte array
|
--get index into byte array
|
||||||
local idx = 2 + x * format.h + y
|
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-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,
|
--in case we miss something upstream, don't corrupt these duplicate points,
|
||||||
--just saturate the attack/defense value
|
--just saturate the attack/defense value
|
||||||
|
@ -142,4 +182,138 @@ function t.savePoints( points, format )
|
||||||
|
|
||||||
return table.concat( bitmap )
|
return table.concat( bitmap )
|
||||||
end
|
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
|
||||||
|
print( "encoded:", point.idx, wx, wy, x, y )
|
||||||
|
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
|
||||||
|
|
||||||
|
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] = assert( self.palette[ math.floor( data:getPixel(x, y) * 255 )] )
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
return t
|
4
map.lua
4
map.lua
|
@ -179,10 +179,12 @@ function map.draw()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function write( filename, string )
|
local function write( filename, string )
|
||||||
|
--
|
||||||
print( "Writing", string:len(), "bytes to", filename )
|
print( "Writing", string:len(), "bytes to", filename )
|
||||||
os.rename( filename, filename..".bak" ) --just in case :^)
|
os.rename( filename, filename..".bak" ) --just in case :^)
|
||||||
local file = assert( io.open( filename, "w+" ) )
|
local file = assert( io.open( filename, "wb+" ) )
|
||||||
assert( file:write( string ) )
|
assert( file:write( string ) )
|
||||||
|
assert( file:flush() ) --your toilet is set to stun, not kill
|
||||||
file:close()
|
file:close()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -143,9 +143,9 @@ function t.computeBorder( territory, threshold, key )
|
||||||
end
|
end
|
||||||
|
|
||||||
function t.save( territory )
|
function t.save( territory )
|
||||||
local fmt = (territory.name == "sailable") and "512r4" or "512rgb24"
|
local fmt = (territory.name == "sailable") and "sailable" or "territory"
|
||||||
print( "saving bitmap: ", territory.name, fmt )
|
print( "saving bitmap: ", territory.name, fmt )
|
||||||
return bmp.save( territory.imgd, fmt )
|
return bmp[fmt]( territory.imgd )
|
||||||
end
|
end
|
||||||
|
|
||||||
return t
|
return t
|
|
@ -153,7 +153,7 @@ function t.drawConnections( nodes )
|
||||||
end
|
end
|
||||||
|
|
||||||
function t.save( nodes )
|
function t.save( nodes )
|
||||||
return bmp.savePoints( nodes.nodes, "800r4")
|
return bmp.travel( nodes.nodes )
|
||||||
end
|
end
|
||||||
|
|
||||||
return t
|
return t
|
Loading…
Reference in New Issue