1 2local ffi = require("ffi") 3local C = ffi.C 4 5local bcarray = require("bcarray") 6 7local assert = assert 8local error = error 9local ipairs = ipairs 10local type = type 11 12local decl = assert(decl) -- comes from above (_defs_game.lua or defs_m32.lua) 13 14local ismapster32 = (C.LUNATIC_CLIENT == C.LUNATIC_CLIENT_MAPSTER32) 15 16---------- 17 18decl[[ 19const int32_t qsetmode; 20int32_t getclosestcol_lim(int32_t r, int32_t g, int32_t b, int32_t lastokcol); 21char *palookup[256]; // MAXPALOOKUPS 22uint8_t palette[768]; 23uint8_t *basepaltable[]; 24 25const char *getblendtab(int32_t blend); 26void setblendtab(int32_t blend, const char *tab); 27 28int32_t setpalookup(int32_t palnum, const uint8_t *shtab); 29]] 30 31if (ismapster32) then 32 ffi.cdef[[ 33int32_t _getnumber16(const char *namestart, int32_t num, int32_t maxnumber, char sign, const char *(func)(int32_t)); 34const char *getstring_simple(const char *querystr, const char *defaultstr, int32_t maxlen, int32_t completion); 35 36typedef const char *(*luamenufunc_t)(void); 37void LM_Register(const char *name, luamenufunc_t funcptr, const char *description); 38void LM_Clear(void); 39]] 40end 41 42---------- 43 44 45-- The API table 46local engine = {} 47 48 49local shtab_t -- forward-decl 50 51local function cast_u8ptr(sth) 52 return ffi.cast("uint8_t *", sth) 53end 54 55local shtab_methods = { 56 -- Remap consecutive blocks of 16 color indices and return this new shade 57 -- table. 58 -- 59 -- <idxs16>: table with idxs16[0] .. idxs16[15] >= 0 and <= 15 60 -- (i.e. 0-based indices of such 16-tuples) 61 -- 62 -- For example, the table 63 -- { [0]=0,1, 2,3, 5,4, 6,7, 8,13, 10,11, 12,9, 14,15 } 64 -- TODO (...) 65 remap16 = function(sht, idxs16) 66 if (type(idxs16) ~= "table") then 67 error("invalid argument #2: must be a table", 2) 68 end 69 70 for i=0,15 do 71 local idx = idxs16[i] 72 if (not (idx==nil or type(idx)=="number" and idx >= 0 and idx <= 15)) then 73 error("invalid reordering table: elements must be numbers in [0 .. 15], or nil", 2) 74 end 75 end 76 77 local newsht = shtab_t() 78 for sh=0,31 do 79 for i=0,15 do 80 ffi.copy(cast_u8ptr(newsht[sh]) + 16*i, 81 cast_u8ptr(sht[sh]) + 16*(idxs16[i] or i), 16) 82 end 83 end 84 return newsht 85 end, 86} 87 88local function shtab_mt__index(sht, idx) 89 local method = shtab_methods[idx] 90 if (method) then 91 return method 92 end 93end 94 95local pal256_t = bcarray.new("uint8_t", 256, "color index 256-tuple") 96local SIZEOF_PAL256 = ffi.sizeof(pal256_t) 97 98-- The shade table type, effectively a bound-checked uint8_t [32][256]: 99shtab_t = bcarray.new(pal256_t, 32, "shade table", nil, nil, { __index = shtab_mt__index }) 100local SIZEOF_SHTAB = ffi.sizeof(shtab_t) 101 102local blendtab_t = bcarray.new(pal256_t, 256, "blending table") 103local SIZEOF_BLENDTAB = ffi.sizeof(blendtab_t) 104 105local RESERVEDPALS = 8 -- KEEPINSYNC build.h: assure that ours is >= theirs 106engine.RESERVEDPALS = RESERVEDPALS 107 108local MAXBLENDTABS = 256 -- KEEPINSYNC build.h 109 110local function check_palidx(i) 111 if (type(i) ~= "number" or not (i >= 0 and i <= 255-RESERVEDPALS)) then 112 error("invalid argument #1: palette swap index must be in the range [0 .. "..255-RESERVEDPALS.."]", 3) 113 end 114end 115 116local function check_blendidx(i) 117 if (type(i) ~= "number" or not (i >= 0 and i <= MAXBLENDTABS-1)) then 118 error("invalid argument #1: blending table index must be in the range [0 .. ".. MAXBLENDTABS-1 .."]", 3) 119 end 120end 121 122local function err_uncommon_shade_table(ret) 123 if (ret == -1) then 124 error("loaded engine shade tables don't have 32 gradients of shade", 3) 125 end 126end 127 128local function palookup_isdefault(palnum) -- KEEPINSYNC engine.c 129 return (C.palookup[palnum] == nil or (palnum ~= 0 and C.palookup[palnum] == C.palookup[0])) 130end 131 132function engine.shadetab() 133 return shtab_t() 134end 135 136function engine.blendtab() 137 return blendtab_t() 138end 139 140function engine.getshadetab(palidx) 141 check_palidx(palidx) 142 if (palookup_isdefault(palidx)) then 143 return nil 144 end 145 146 local ret = C.setpalookup(palidx, nil) 147 err_uncommon_shade_table(ret) 148 149 local sht = shtab_t() 150 ffi.copy(sht, C.palookup[palidx], SIZEOF_SHTAB) 151 return sht 152end 153 154function engine.getblendtab(blendidx) 155 check_blendidx(blendidx) 156 157 local ptr = C.getblendtab(blendidx) 158 if (ptr == nil) then 159 return nil 160 end 161 162 local tab = blendtab_t() 163 ffi.copy(tab, ptr, SIZEOF_BLENDTAB) 164 return tab 165end 166 167 168local function check_first_time() 169 if (not ismapster32 and C.g_elFirstTime == 0) then 170 error("may be called only while LUNATIC_FIRST_TIME is true", 3) 171 end 172end 173 174function engine.setshadetab(palidx, shtab) 175 check_first_time() 176 check_palidx(palidx) 177 178 if (not ffi.istype(shtab_t, shtab)) then 179 error("invalid argument #2: must be a shade table obtained by shadetab()", 2) 180 end 181 182 if (not ismapster32 and not palookup_isdefault(palidx)) then 183 error("attempt to override already defined shade table", 2) 184 end 185 186 local ret = C.setpalookup(palidx, cast_u8ptr(shtab)) 187 err_uncommon_shade_table(ret) 188end 189 190function engine.setblendtab(blendidx, tab) 191 check_first_time() 192 check_blendidx(blendidx) 193 194 if (not ffi.istype(blendtab_t, tab)) then 195 error("invalid argument #2: must be a blending table obtained by blendtab()", 2) 196 end 197 198 if (not ismapster32 and C.getblendtab(blendidx) ~= nil) then 199 error("attempt to override already defined blending table", 2) 200 end 201 202 C.setblendtab(blendidx, cast_u8ptr(tab)) 203end 204 205 206local function check_colcomp(a) 207 if (type(a) ~= "number" or not (a >= 0 and a < 256)) then 208 error("color component must be in the range [0 .. 256)", 3) 209 end 210end 211 212 213-- TODO: other base palettes? 214function engine.getrgb(colidx) 215 if (type(colidx) ~= "number" or not (colidx >= 0 and colidx <= 255)) then 216 error("color index must be in the range [0 .. 255]", 2) 217 end 218 219 -- NOTE: In the game, palette[255*{0..2}] is set to 0 in 220 -- G_LoadExtraPalettes() via G_Startup(). However, that's after Lua state 221 -- initialization (i.e. when LUNATIC_FIRST_TIME would be true), and in the 222 -- editor, it's never changed from the purple color. Therefore, I think 223 -- it's more useful to always return the fully black color here. 224 if (colidx == 255) then 225 return 0, 0, 0 226 end 227 228 local rgbptr = C.palette + 3*colidx 229 return rgbptr[0], rgbptr[1], rgbptr[2] 230end 231 232function engine.nearcolor(r, g, b, lastokcol) 233 check_colcomp(r) 234 check_colcomp(g) 235 check_colcomp(b) 236 237 if (lastokcol == nil) then 238 lastokcol = 255 239 elseif (type(lastokcol)~="number" or not (lastokcol >= 0 and lastokcol <= 255)) then 240 error("invalid argument #4 <lastokcol>: must be in the range [0 .. 255]", 2) 241 end 242 243 return C.getclosestcol_lim(r, g, b, lastokcol) 244end 245 246 247---------- Mapster32-only functions ---------- 248 249if (ismapster32) then 250 local io = require("io") 251 local math = require("math") 252 local string = require("string") 253 254 ffi.cdef[[size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, void * restrict stream);]] 255 256 local function validate_more_blendtabs(moreblends, kindname, gettabfunc) 257 if (moreblends == nil) then 258 return nil, nil 259 end 260 261 -- Additional blending tables: validate <moreblends> table. 262 if (type(moreblends) ~= "table") then 263 error("invalid argument #4: must be a table", 3) 264 end 265 266 local haveblend = { [0]=true } 267 local blendnumtab, blendptrtab = {}, {} 268 269 for i=1,#moreblends do 270 local tmp = moreblends[i] 271 local blendspec = (type(tmp) == "number") and { tmp, tmp } or tmp 272 273 if (not (type(blendspec) == "table" and #blendspec == 2)) then 274 error("invalid argument #4: must contain numbers or 2-tables", 3) 275 end 276 277 local blend1, blend2 = math.floor(blendspec[1]), math.floor(blendspec[2]) 278 279 if (not (type(blend1)=="number" and blend1 >= 1 and blend1 <= 255 and 280 type(blend2)=="number" and blend2 >= 1 and blend2 <= 255)) then 281 error("invalid argument #4: "..kindname.." table numbers must be in [1 .. 255]", 3) 282 end 283 284 for bi=blend1,blend2 do 285 if (haveblend[bi]) then 286 error("invalid argument #4: duplicate "..kindname.." table number "..bi, 3) 287 end 288 haveblend[bi] = true 289 290 local ptr = gettabfunc(bi) 291 if (ptr == nil) then 292 error("invalid argument #4: "..kindname.." table for number "..bi.." is void", 3) 293 end 294 295 blendnumtab[#blendnumtab+1] = bi 296 blendptrtab[#blendptrtab+1] = ptr 297 end 298 end 299 300 assert(#blendnumtab <= 255) 301 return blendnumtab, blendptrtab 302 end 303 304 -- ok, errmsg, nummoreblends = engine.savePaletteDat( 305 -- filename [, palnum [, blendnum [, moreblends [, lognumalphatabs]]]]) 306 function engine.savePaletteDat(filename, palnum, blendnum, moreblends, lognumalphatabs) 307 local sht = engine.getshadetab(palnum or 0) 308 local tab = engine.getblendtab(blendnum or 0) 309 310 if (sht == nil) then 311 return nil, "no shade table with number "..palnum 312 elseif (tab == nil) then 313 return nil, "no blending table with number "..blendnum 314 end 315 316 local blendnumtab, blendptrtab = validate_more_blendtabs( 317 moreblends, "blending", C.getblendtab) 318 319 if (lognumalphatabs ~= nil) then 320 if (not (type(lognumalphatabs)=="number" and lognumalphatabs >= 1 and lognumalphatabs <= 7)) then 321 error("invalid argument #5: must be a number in [1 .. 7]", 2) 322 end 323 end 324 325 local f, errmsg = io.open(filename, "wb+") 326 if (f == nil) then 327 return nil, errmsg 328 end 329 330 local truncpal = pal256_t() 331 ffi.copy(truncpal, C.palette, SIZEOF_PAL256) 332 for i=0,255 do 333 truncpal[i] = bit.rshift(truncpal[i], 2) 334 end 335 336 local n1 = C.fwrite(truncpal, 3, 256, f) 337 f:write("\032\000") -- int16_t numshades 338 local n3 = C.fwrite(sht, 256, 32, f) 339 local n4 = C.fwrite(tab, 256, 256, f) 340 341 if (n1 ~= 256 or n3 ~= 32 or n4 ~= 256) then 342 return nil, "failed writing classic PALETTE.DAT data" 343 end 344 345 if (blendnumtab ~= nil) then 346 f:write("MoreBlendTab") 347 f:write(string.char(#blendnumtab)) 348 349 for i=1,#blendnumtab do 350 f:write(string.char(blendnumtab[i])) 351 if (C.fwrite(blendptrtab[i], 256, 256, f) ~= 256) then 352 return nil, "failed writing additional blending table" 353 end 354 end 355 356 if (lognumalphatabs ~= nil) then 357 -- XXX: no checking whether these blending tables 1 to 358 -- 1<<lognumalphatabs have been written. 359 f:write(string.char(lognumalphatabs)) 360 end 361 end 362 363 f:close() 364 365 return true, nil, (blendnumtab ~= nil) and #blendnumtab or 0 366 end 367 368 -- ok, errmsg, numlookups = engine.saveLookupDat(filename, lookups) 369 function engine.saveLookupDat(filename, lookups) 370 if (lookups == nil) then 371 -- set to an invalid value, validate_more_blendtabs will error 372 lookups = 0 373 end 374 375 local lookupnumtab, lookupptrtab = validate_more_blendtabs( 376 lookups, "lookup", engine.getshadetab) 377 378 local f, errmsg = io.open(filename, "wb+") 379 if (f == nil) then 380 return nil, errmsg 381 end 382 383 f:write(string.char(#lookupnumtab)) 384 385 for i=1,#lookupnumtab do 386 f:write(string.char(lookupnumtab[i])) 387 if (C.fwrite(lookupptrtab[i], 1, 256, f) ~= 256) then 388 return nil, "failed writing lookup table" 389 end 390 end 391 392 -- Write five base palettes 393 for i=1,5 do 394 local bpi = (i==3 or i==4) and 4+3-i or i 395 396 local truncbasepal = pal256_t() 397 ffi.copy(truncbasepal, C.basepaltable[bpi], SIZEOF_PAL256) 398 for j=0,255 do 399 truncbasepal[j] = bit.rshift(truncbasepal[j], 2) 400 end 401 402 if (C.fwrite(truncbasepal, 1, 768, f) ~= 768) then 403 return nil, "failed writing base palette" 404 end 405 end 406 407 f:close() 408 409 return true, nil, #lookupnumtab 410 end 411 412 local hexmap = { 413 [0] = 0, -14, -- 0, 1: gray ramp 414 14, 0, -- 2, 3: skin color ramp 415 0, 14, -- 4, 5: blue ramp (second part first) 416 14, 0, -- 6, 7: nightvision yellow/green 417 14, -- 8: red first part... 418 8, -- 9: yellow (slightly more red than green) 419 14, 0, -- 10, 11: almost gray ramp, but with a slight red hue 420 8, -- 12: "dirty" orange 421 0, -- 13: ...red second part 422 8, -- 14: blue-purple-red 423 } 424 425 -- Setup base palette 1 (water) to contain one color for each consecutive 426 -- 16-tuple (which I'm calling a 'hex' for brevity), except for the last 427 -- one with the fullbrights. 428 function engine.setupDebugBasePal() 429 for i=0,14 do 430 local ptr = C.basepaltable[1] + 3*(16*i) 431 local src = C.basepaltable[0] + 3*(16*i) + 3*hexmap[i] 432 local r, g, b = src[0], src[1], src[2] 433 434 for j=0,15 do 435 local dst = ptr + 3*j 436 dst[0], dst[1], dst[2] = r, g, b 437 end 438 end 439 end 440 441 function engine.linearizeBasePal() 442 for _, begi in ipairs{0, 32, 96, 160} do 443 local ptr = C.basepaltable[0] + 3*begi 444 local refcol = ptr + 3*31 445 446 for i=0,30 do 447 for c=0,2 do 448 ptr[3*i + c] = i*refcol[c]/31 449 end 450 end 451 end 452 453 for _, begi in ipairs{128, 144, 192, 208, 224} do 454 local ptr = C.basepaltable[0] + 3*begi 455 456 for i=0,3*15+2 do 457 ptr[i] = 0 458 end 459 end 460 end 461 462 -- Interfaces to Mapster32's status bar menu 463 464 local pcall = pcall 465 466 function engine.clearMenu() 467 C.LM_Clear() 468 end 469 470 function engine.registerMenuFunc(name, func, description) 471 if (type(name) ~= "string") then 472 error("invalid argument #1: must be a string", 2) 473 end 474 if (type(func) ~= "function") then 475 error("invalid argument #2: must be a function", 2) 476 end 477 if (description~=nil and type(description)~="string") then 478 error("invalid argument #3: must be nil or a string", 2) 479 end 480 481 local safefunc = function() 482 local ok, errmsg = pcall(func) 483 if (not ok) then 484 return errmsg 485 end 486 end 487 488 C.LM_Register(name, safefunc, description) 489 end 490 491 engine.GETNUMFLAG = { 492 NEG_ALLOWED = 1, 493 AUTOCOMPL_NAMES = 2, 494 AUTOCOMPL_TAGLAB = 4, 495 RET_M1_ON_CANCEL = 8, 496 497 NEXTFREE = 16, 498 } 499 500 function engine.getnumber16(namestart, num, maxnumber, flags) 501 if (C.qsetmode == 200) then 502 error("getnumber16 must be called from 2D mode", 2) 503 end 504 if (type(namestart)~="string") then 505 error("invalid argument #1: must be a string", 2) 506 end 507 508 return C._getnumber16(namestart, num, maxnumber, flags or 8, nil) -- RET_M1_ON_CANCEL 509 end 510 511 function engine.getstring(querystr) 512 if (type(querystr) ~= "string") then 513 error("invalid argument #2: must be a string", 2) 514 end 515 local cstr = C.getstring_simple(querystr, nil, 0, 0) 516 return cstr~=nil and ffi.string(cstr) or nil 517 end 518end 519 520 521-- Done! 522return engine 523