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