534 lines
20 KiB
Lua
534 lines
20 KiB
Lua
local M = {}
|
|
|
|
local floor = math.floor
|
|
local mtype = math.type
|
|
local UNKNOWN = {}
|
|
M.UNKNOWN = UNKNOWN
|
|
|
|
local ARRAY_MT = { __array = true }
|
|
local SOUND_MT = { __sound = true }
|
|
local SWG_OBJ = { __swg = true }
|
|
|
|
local function is_array(v) return type(v) == "table" and getmetatable(v) == ARRAY_MT end
|
|
local function new_array(n) return setmetatable({ n = n or 0 }, ARRAY_MT) end
|
|
|
|
-- ------------------------------------------------------------------ values
|
|
local function tostr(v)
|
|
if v == nil then return "undefined" end
|
|
local t = type(v)
|
|
if t == "boolean" then return v and "true" or "false" end
|
|
if t == "number" then
|
|
if v == floor(v) and v > -1e15 and v < 1e15 then return string.format("%d", v) end
|
|
return tostring(v)
|
|
end
|
|
if t == "string" then return v end
|
|
return ""
|
|
end
|
|
|
|
local function num(v)
|
|
local t = type(v)
|
|
if t == "number" then return v end
|
|
if t == "boolean" then return v and 1 or 0 end
|
|
if t == "string" then return tonumber(v) or 0 end
|
|
return 0
|
|
end
|
|
|
|
local function truthy(v)
|
|
if v == nil then return false end
|
|
local t = type(v)
|
|
if t == "boolean" then return v end
|
|
if t == "number" then return v ~= 0 end
|
|
if t == "string" then return v ~= "" end
|
|
return true
|
|
end
|
|
|
|
local function eq(a, b)
|
|
local ta, tb = type(a), type(b)
|
|
if ta == tb then return a == b end
|
|
if (ta == "number" or ta == "boolean") and (tb == "number" or tb == "boolean" or tb == "string") then
|
|
return num(a) == num(b)
|
|
end
|
|
if ta == "string" and (tb == "number" or tb == "boolean") then return num(a) == num(b) end
|
|
return false
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ matrices / colours
|
|
local function mul(p, q)
|
|
local a, b, c, d, tx, ty = p[1], p[2], p[3], p[4], p[5], p[6]
|
|
local e, f, g, h, ux, uy = q[1], q[2], q[3], q[4], q[5], q[6]
|
|
return { a * e + c * f, b * e + d * f, a * g + c * h, b * g + d * h, a * ux + c * uy + tx, b * ux + d * uy + ty }
|
|
end
|
|
local IDENT = { 1, 0, 0, 1, 0, 0 }
|
|
|
|
local function compose(parent, child)
|
|
if parent == nil then return child end
|
|
if child == nil then return parent end
|
|
local r = {}
|
|
for i = 1, 4 do
|
|
r[i] = child[i] * parent[i]
|
|
r[4 + i] = child[4 + i] * parent[i] + parent[4 + i]
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ instances
|
|
local Inst = {}
|
|
Inst.__index = Inst
|
|
|
|
local function new_inst(mv, cid, name, parent)
|
|
local self = setmetatable({
|
|
mv = mv, cid = cid, name = name, parent = parent, sp = mv.m.dict[cid],
|
|
frame = 0, playing = true, entered = -1,
|
|
children = {}, order = {}, depth_child = {}, vars = {},
|
|
visible = true, alpha = 1.0,
|
|
}, Inst)
|
|
if mv.host and mv.host.on_new then mv.host.on_new(self) end
|
|
return self
|
|
end
|
|
|
|
function Inst:nframes() return #self.sp.frames end
|
|
|
|
function Inst:go(f, play)
|
|
if type(f) == "string" then
|
|
f = self.sp.labels[f]
|
|
if f == nil then return end
|
|
end
|
|
local n = self:nframes()
|
|
f = floor(f)
|
|
if f < 0 then f = 0 elseif f > n - 1 then f = n - 1 end
|
|
self.frame, self.playing, self.entered = f, play, -1
|
|
end
|
|
|
|
-- goto from an action: the target frame is entered after the running script (AVM1 queues the new
|
|
-- frame's actions, so gotoAndStop(x); play() plays on unless x's own actions stop it)
|
|
function Inst:jump(f, play)
|
|
self:go(f, play)
|
|
local mv = self.mv
|
|
if mv.run_depth > 0 then
|
|
for _, p in ipairs(mv.pending) do if p == self then return end end
|
|
mv.pending[#mv.pending + 1] = self
|
|
else
|
|
self:enter()
|
|
end
|
|
end
|
|
|
|
function Inst:enter()
|
|
local mv = self.mv
|
|
local items = self.sp.frames[self.frame + 1]
|
|
local live, order, depth_child, created = {}, {}, {}, {}
|
|
for _, it in ipairs(items) do
|
|
if it[1] == "L" and mv.m.dict[it[3]].k == 5 then
|
|
local ch = self.children[it[2]]
|
|
if ch == nil then
|
|
local nm = (it[4] >= 0) and mv.m.names[it[4]] or nil
|
|
ch = new_inst(mv, it[3], nm, self)
|
|
created[ch] = true
|
|
end
|
|
live[it[2]] = ch
|
|
order[#order + 1] = ch
|
|
depth_child[it[8]] = ch
|
|
end
|
|
end
|
|
self.children, self.order, self.depth_child = live, order, depth_child
|
|
for _, it in ipairs(items) do -- onClipEvent(load) of the children created now
|
|
if it[1] == "C" and it[3] == 0 then
|
|
local ch = depth_child[it[2]]
|
|
if ch and created[ch] then mv:run(it[4], ch) end
|
|
end
|
|
end
|
|
if self.entered ~= self.frame then
|
|
self.entered = self.frame
|
|
for _, it in ipairs(items) do
|
|
if it[1] == "S" then mv:sound(it[2])
|
|
elseif it[1] == "A" then mv:run(it[2], self) end
|
|
end
|
|
end
|
|
for _, ch in ipairs(self.order) do ch:enter() end
|
|
end
|
|
|
|
function Inst:advance()
|
|
for _, ch in ipairs(self.order) do ch:advance() end
|
|
if self.playing then
|
|
if self.frame + 1 < self:nframes() then self.frame = self.frame + 1 else self.frame = 0 end
|
|
end
|
|
end
|
|
|
|
function Inst:child(name)
|
|
for _, c in ipairs(self.order) do
|
|
if c.name == name then return c end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- the axis-aligned box (movie pixels) of everything an item draws: a mask's clip rectangle
|
|
local function box_add(b, M, w, h)
|
|
for _, c in ipairs({ { 0, 0 }, { w, 0 }, { 0, h }, { w, h } }) do
|
|
local x = M[1] * c[1] + M[3] * c[2] + M[5]
|
|
local y = M[2] * c[1] + M[4] * c[2] + M[6]
|
|
if x < b[1] then b[1] = x end
|
|
if y < b[2] then b[2] = y end
|
|
if x > b[3] then b[3] = x end
|
|
if y > b[4] then b[4] = y end
|
|
end
|
|
end
|
|
|
|
local function intersect(a, b)
|
|
if not a then return b end
|
|
if not b then return a end
|
|
return { math.max(a[1], b[1]), math.max(a[2], b[2]), math.min(a[3], b[3]), math.min(a[4], b[4]) }
|
|
end
|
|
|
|
function Inst:bounds(parent, it, b)
|
|
local m = self.mv.m
|
|
local Mx = mul(parent, m.mat[it[5]])
|
|
local d = m.dict[it[3]]
|
|
if d.k == 0 then
|
|
for _, fl in ipairs(d.fills) do
|
|
local SM = mul(Mx, m.mat[fl[2]])
|
|
box_add(b, mul(SM, { 1, 0, 0, 1, fl[3], fl[4] }), fl[5], fl[6])
|
|
end
|
|
elseif d.k == 5 then
|
|
local ch = self.children[it[2]]
|
|
if ch then
|
|
for _, it2 in ipairs(ch.sp.frames[ch.frame + 1]) do
|
|
if it2[1] == "L" then ch:bounds(Mx, it2, b) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Inst:walk(parent, pcol, fn, clip)
|
|
local m = self.mv.m
|
|
local mask_box = nil
|
|
for _, it in ipairs(self.sp.frames[self.frame + 1]) do
|
|
if it[1] == "L" and it[7] == "mask" then
|
|
local b = { math.huge, math.huge, -math.huge, -math.huge }
|
|
self:bounds(parent, it, b)
|
|
mask_box = b
|
|
elseif it[1] == "L" then
|
|
local c = clip
|
|
if it[7] == "masked" then c = intersect(clip, mask_box or { 0, 0, 0, 0 }) end
|
|
local Mx = mul(parent, m.mat[it[5]])
|
|
local col = compose(pcol, (it[6] >= 0) and m.col[it[6]] or nil)
|
|
if not (col and col[4] <= 0 and col[8] <= 0) then
|
|
local d = m.dict[it[3]]
|
|
if d.k == 0 then
|
|
for _, fl in ipairs(d.fills) do
|
|
local SM = mul(Mx, m.mat[fl[2]])
|
|
fn(fl[1], mul(SM, { 1, 0, 0, 1, fl[3], fl[4] }), fl[5], fl[6], col, c)
|
|
end
|
|
elseif d.k == 5 then
|
|
local ch = self.children[it[2]]
|
|
if ch and ch.visible then
|
|
local c2 = col
|
|
if ch.alpha < 1 then c2 = compose(col, { 1, 1, 1, ch.alpha, 0, 0, 0, 0 }) end
|
|
ch:walk(Mx, c2, fn, c)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ movie
|
|
local Movie = {}
|
|
Movie.__index = Movie
|
|
|
|
function M.new(m, host)
|
|
local self = setmetatable({ m = m, host = host, globals = {} }, Movie)
|
|
self.run_depth, self.pending, self.draining = 0, {}, false
|
|
self.root = new_inst(self, m.root, "_root", nil)
|
|
return self
|
|
end
|
|
|
|
function Movie:sound(name)
|
|
if self.host and self.host.sound then self.host.sound(name) end
|
|
end
|
|
|
|
function Movie:tick() self.root:enter() end
|
|
function Movie:step() self.root:advance() end
|
|
|
|
function Movie:find(name, inst)
|
|
inst = inst or self.root
|
|
if inst.name == name then return inst end
|
|
for _, c in ipairs(inst.order) do
|
|
local r = self:find(name, c)
|
|
if r then return r end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Movie:get_var(name, this)
|
|
if name == "_root" then return self.root end
|
|
if name == "_parent" then return this.parent end
|
|
if name == "this" then return this end
|
|
if name == "SWG" then return SWG_OBJ end
|
|
if type(name) == "string" then
|
|
local v = this.vars[name]
|
|
if v ~= nil then return v end
|
|
local ch = this:child(name)
|
|
if ch then return ch end
|
|
return self.globals[name]
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Movie:get_member(obj, member)
|
|
if getmetatable(obj) == Inst then
|
|
if type(member) == "number" then
|
|
local p = floor(member)
|
|
if p == 4 then return obj.frame + 1 end
|
|
if p == 5 then return obj:nframes() end
|
|
if p == 6 then return obj.alpha * 100 end
|
|
if p == 7 then return obj.visible end
|
|
if p == 13 then return obj.name end
|
|
return nil
|
|
end
|
|
if member == "_parent" then return obj.parent end -- _parent._parent... (the gauge meter)
|
|
if member == "_currentframe" then return obj.frame + 1 end
|
|
if member == "_totalframes" then return obj:nframes() end
|
|
if member == "_name" then return obj.name end
|
|
local v = obj.vars[member]
|
|
if v ~= nil then return v end
|
|
return obj:child(member)
|
|
end
|
|
if is_array(obj) then
|
|
if member == "length" then return obj.n end
|
|
return obj[floor(num(member))]
|
|
end
|
|
if type(obj) == "table" and obj ~= SWG_OBJ and getmetatable(obj) ~= SOUND_MT then return obj[member] end
|
|
return nil
|
|
end
|
|
|
|
function Movie:set_member(obj, member, val)
|
|
if getmetatable(obj) == Inst then
|
|
if type(member) == "number" then
|
|
local p = floor(member)
|
|
if p == 7 then obj.visible = truthy(val) elseif p == 6 then obj.alpha = num(val) / 100 end
|
|
return
|
|
end
|
|
if member == "_visible" then obj.visible = truthy(val); return end
|
|
if member == "_alpha" then obj.alpha = num(val) / 100; return end
|
|
obj.vars[member] = val
|
|
elseif is_array(obj) then
|
|
local i = floor(num(member))
|
|
obj[i] = val
|
|
if i + 1 > obj.n then obj.n = i + 1 end
|
|
elseif type(obj) == "table" and obj ~= SWG_OBJ then
|
|
obj[member] = val
|
|
end
|
|
end
|
|
|
|
function Movie:call(obj, meth, args, this)
|
|
if obj == SWG_OBJ then
|
|
if self.host and self.host.call then return self.host.call(meth, args) end
|
|
return 0
|
|
end
|
|
if getmetatable(obj) == SOUND_MT then
|
|
if meth == "attachSound" and args[1] then obj.name = args[1]
|
|
elseif meth == "start" and obj.name then self:sound(obj.name)
|
|
elseif meth == "stop" and obj.name and self.host and self.host.stop_sound then self.host.stop_sound(obj.name) end
|
|
return nil
|
|
end
|
|
if getmetatable(obj) == Inst then
|
|
local a0 = args[1]
|
|
if meth == "gotoAndPlay" or meth == "gotoAndStop" then
|
|
if type(a0) == "string" then obj:jump(a0, meth == "gotoAndPlay")
|
|
elseif a0 ~= nil then obj:jump(floor(num(a0)) - 1, meth == "gotoAndPlay") end
|
|
elseif meth == "play" then obj.playing = true
|
|
elseif meth == "stop" then obj.playing = false
|
|
elseif meth == "nextFrame" then obj:jump(obj.frame + 1, false)
|
|
elseif meth == "prevFrame" then obj:jump(obj.frame - 1, false) end
|
|
return nil
|
|
end
|
|
if is_array(obj) and meth == "push" then
|
|
for _, v in ipairs(args) do obj[obj.n] = v; obj.n = obj.n + 1 end
|
|
return obj.n
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local TWO = { [6] = true, [9] = true, [47] = true, [50] = true, [52] = true, [85] = true, [98] = true }
|
|
|
|
function Movie:run(ci, this)
|
|
self.run_depth = self.run_depth + 1
|
|
local ok, err = pcall(self.run_code, self, ci, this)
|
|
self.run_depth = self.run_depth - 1
|
|
if self.run_depth == 0 and not self.draining then
|
|
self.draining = true
|
|
while #self.pending > 0 do
|
|
table.remove(self.pending, 1):enter()
|
|
end
|
|
self.draining = false
|
|
end
|
|
if not ok then error(err, 0) end
|
|
end
|
|
|
|
function Movie:run_code(ci, this)
|
|
local m = self.m
|
|
local code = m.code[ci]
|
|
local strs = m.strs
|
|
local stack, sp = {}, 0
|
|
local regs = {}
|
|
local function push(v) sp = sp + 1; stack[sp] = v end
|
|
local function pop()
|
|
if sp == 0 then return nil end
|
|
local v = stack[sp]; stack[sp] = nil; sp = sp - 1; return v
|
|
end
|
|
local pc = 0 -- word index (0-based), code[pc + 1]
|
|
local ncode = #code
|
|
for _ = 1, 100000 do
|
|
if pc >= ncode then return end
|
|
local op = code[pc + 1]
|
|
if op == 0 then return end
|
|
if op == 46 then
|
|
local n = code[pc + 2]
|
|
local k = 0
|
|
while k < n // 4 do
|
|
local t, v = code[pc + 3 + k], code[pc + 4 + k]
|
|
if t == 0 then push(strs[v] or "")
|
|
elseif t == 1 then push(string.unpack("<f", string.pack("<i4", v)))
|
|
elseif t == 4 then push(regs[v])
|
|
elseif t == 5 then push(v ~= 0)
|
|
elseif t == 2 or t == 3 then push(nil)
|
|
else push(v) end
|
|
k = k + 2
|
|
end
|
|
pc = pc + 2 + n // 4
|
|
else
|
|
local arg = nil
|
|
if TWO[op] then arg = code[pc + 2]; pc = pc + 2 else pc = pc + 1 end
|
|
if op == 1 then this.playing = true
|
|
elseif op == 2 then this.playing = false
|
|
elseif op == 6 then
|
|
local lab = m.labels[arg]
|
|
if lab then this:jump(lab, false) end
|
|
elseif op == 9 or op == 98 then
|
|
-- skipped
|
|
elseif op == 11 or op == 66 then
|
|
local b, a = pop(), pop()
|
|
if op == 66 and (type(a) == "string" or type(b) == "string")
|
|
and not (type(a) == "number" and type(b) == "number") then
|
|
push(tostr(a) .. tostr(b))
|
|
else push(num(a) + num(b)) end
|
|
elseif op == 12 then local b, a = pop(), pop(); push(num(a) - num(b))
|
|
elseif op == 13 then local b, a = pop(), pop(); push(num(a) * num(b))
|
|
elseif op == 14 then
|
|
local b, a = pop(), pop(); b = num(b)
|
|
push(b ~= 0 and num(a) / b or 0)
|
|
elseif op == 58 then
|
|
local b, a = pop(), pop(); b = num(b)
|
|
push(b ~= 0 and math.fmod(num(a), b) or 0)
|
|
elseif op == 19 then push(not truthy(pop()))
|
|
elseif op == 23 then pop()
|
|
elseif op == 25 then push(self:get_var(pop(), this))
|
|
elseif op == 26 or op == 55 then
|
|
local v = pop(); local name = pop()
|
|
if name ~= nil then this.vars[tostr(name)] = v end
|
|
elseif op == 47 then pc = pc + arg // 4
|
|
elseif op == 48 then pop(); pop()
|
|
elseif op == 50 then
|
|
if truthy(pop()) then pc = pc + arg // 4 end
|
|
elseif op == 52 then
|
|
local f = pop()
|
|
if type(f) == "string" then this:jump(f, (arg & 1) == 1)
|
|
elseif f ~= nil then this:jump(floor(num(f)) - 1, (arg & 1) == 1) end
|
|
elseif op == 59 then
|
|
local cls = pop(); local argc = floor(num(pop()))
|
|
local args = {}
|
|
for i = 1, argc do args[i] = pop() end
|
|
local obj
|
|
if cls == "Sound" then obj = setmetatable({}, SOUND_MT)
|
|
elseif cls == "Array" then
|
|
if argc == 1 and type(args[1]) == "number" then obj = new_array(floor(args[1]))
|
|
else
|
|
obj = new_array(0)
|
|
for i = 1, argc do obj[i - 1] = args[i] end
|
|
obj.n = argc
|
|
end
|
|
else obj = {} end
|
|
push(obj); push(obj) -- pushed twice (the code pops one)
|
|
elseif op == 61 then
|
|
local n = floor(num(pop()))
|
|
local arr = new_array(n)
|
|
for i = 0, n - 1 do arr[i] = pop() end
|
|
push(arr)
|
|
elseif op == 67 then local b, a = pop(), pop(); push(num(a) < num(b))
|
|
elseif op == 68 or op == 91 then local b, a = pop(), pop(); push(eq(a, b))
|
|
elseif op == 92 then local b, a = pop(), pop(); push(num(a) > num(b))
|
|
elseif op == 71 then local v = pop(); push(v); push(v)
|
|
elseif op == 72 then local b, a = pop(), pop(); push(b); push(a)
|
|
elseif op == 73 then
|
|
local member = pop(); local obj = pop()
|
|
if obj == "" then obj = this end
|
|
push(self:get_member(obj, member))
|
|
elseif op == 74 then
|
|
local v = pop(); local member = pop(); local obj = pop()
|
|
if obj == "" then obj = this end
|
|
self:set_member(obj, member, v)
|
|
elseif op == 75 then push(num(pop()) + 1)
|
|
elseif op == 76 then push(num(pop()) - 1)
|
|
elseif op == 77 then
|
|
local meth = pop(); local obj = pop(); local argc = floor(num(pop()))
|
|
local args = {}
|
|
for i = 1, argc do args[i] = pop() end
|
|
push(self:call(obj, meth, args, this))
|
|
elseif op == 85 then regs[arg] = (sp > 0) and stack[sp] or nil
|
|
else
|
|
UNKNOWN[op] = (UNKNOWN[op] or 0) + 1
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ drawing
|
|
-- 2x3 matrix of a w x h quad -> top-left, size, rotation (deg, about the top-left), vertical flip
|
|
local function decompose(Mx, w, h)
|
|
local a, b, c, d, tx, ty = Mx[1], Mx[2], Mx[3], Mx[4], Mx[5], Mx[6]
|
|
local sx = math.sqrt(a * a + b * b)
|
|
local rot = math.atan(b, a)
|
|
local sy = (sx ~= 0) and (a * d - b * c) / sx or 0
|
|
if sy < 0 then
|
|
return c * h + tx, d * h + ty, w * sx, h * -sy, math.deg(rot), true
|
|
end
|
|
return tx, ty, w * sx, h * sy, math.deg(rot), false
|
|
end
|
|
|
|
function Movie:draw(texture, size, K, ox, oy, fade, texture_w, additive)
|
|
ox, oy, fade = ox or 0, oy or 0, fade or 1
|
|
self.root:walk(IDENT, nil, function(ti, Mx, w, h, col, clip)
|
|
local t = texture(ti)
|
|
local sz = size[ti]
|
|
if not (t and sz) then return end
|
|
if clip and (clip[3] <= clip[1] or clip[4] <= clip[2]) then return end -- fully masked
|
|
local x, y, ww, hh, rot, flip = decompose(Mx, w, h)
|
|
local cr, cg, cb, ca, ar, ag, ab, aa = 1, 1, 1, 1, 0, 0, 0, 0
|
|
if col then cr, cg, cb, ca, ar, ag, ab, aa = col[1], col[2], col[3], col[4], col[5], col[6], col[7], col[8] end
|
|
local alpha = ca + aa / 255
|
|
if alpha <= 0.004 then return end
|
|
if alpha > 1 then alpha = 1 end
|
|
local function c255(v) v = floor(v * 255 + 0.5); if v < 0 then return 0 elseif v > 255 then return 255 end return v end
|
|
local p = {
|
|
x = (x + ox) * K, y = (y + oy) * K, x2 = ww * K - sz[1], y2 = hh * K - sz[2],
|
|
rotation = rot, mirror = flip and "vertical" or nil, fade = alpha * fade,
|
|
color = { c255(cr), c255(cg), c255(cb), 255 },
|
|
blend = additive and additive[ti] and "additive" or nil,
|
|
}
|
|
if clip then
|
|
tex.begin_scissor((clip[1] + ox) * K, (clip[2] + oy) * K, (clip[3] - clip[1]) * K, (clip[4] - clip[2]) * K)
|
|
end
|
|
tex.draw_texture(t, p)
|
|
if ar + ag + ab > 2 then
|
|
p.blend = "additive"
|
|
p.color = { c255(ar / 255), c255(ag / 255), c255(ab / 255), 255 }
|
|
tex.draw_texture(texture_w and texture_w(ti) or t, p)
|
|
end
|
|
if clip then tex.end_scissor() end
|
|
end)
|
|
end
|
|
|
|
M.Inst, M.Movie = Inst, Movie
|
|
M.new_array, M.is_array = new_array, is_array
|
|
return M
|